Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-30 06:12:38 +00:00
parent d6df46f3c3
commit 55cd0a88bb
8 changed files with 244 additions and 20 deletions

View file

@ -20,4 +20,9 @@ module NotifyHelper
(source.description || default_description).truncate(200, separator: ' ') (source.description || default_description).truncate(200, separator: ' ')
end end
def merge_request_approved_description(merge_request, approved_by)
format(s_('Notify|%{mr_highlight}Merge request%{highlight_end} %{mr_link} %{approved_highlight}was approved by%{highlight_end} %{approver_avatar} %{approver_link}').html_safe, mr_highlight: '<span style="font-weight: 600;color:#333333;">'.html_safe, highlight_end: '</span>'.html_safe, mr_link: link_to(merge_request.to_reference, merge_request_url(merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none").html_safe, approved_highlight: '<span>'.html_safe, approver_avatar: content_tag(:img, nil, height: "24", src: avatar_icon_for_user(approved_by, 24, only_path: false),
style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar", class: "avatar").html_safe, approver_link: link_to(approved_by.name, user_url(approved_by), style: "color:#333333;text-decoration:none;", class: "muted").html_safe)
end
end end

View file

@ -78,10 +78,11 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
%img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
- if @merge_request.respond_to? :approvals_required %span
%span Merge request was approved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required}) - if @merge_request.respond_to? :approvals_required
- else = s_('Notify|Merge request was approved (%{approvals}/%{required_approvals})') % { approvals: @merge_request.approvals.count, required_approvals: @merge_request.approvals_required }
%span Merge request was approved - else
= s_('Notify|Merge request was approved')
%tr.spacer %tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp; &nbsp;
@ -92,12 +93,7 @@
%tr{ style: 'width:100%;' } %tr{ style: 'width:100%;' }
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
%img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" } %img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
%span{ style: "font-weight: 600;color:#333333;" } Merge request = merge_request_approved_description(@merge_request, @approved_by)
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
%span was approved by
%img.avatar{ height: "24", src: avatar_icon_for_user(@approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%a.muted{ href: user_url(@approved_by), style: "color:#333333;text-decoration:none;" }
= @approved_by.name
%tr.spacer %tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp; &nbsp;
@ -106,7 +102,7 @@
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" }= _("Project")
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
@ -116,7 +112,7 @@
%a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name = @project.name
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" }= _("Branch")
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
@ -127,7 +123,7 @@
%span.muted{ style: "color:#333333;text-decoration:none;" } %span.muted{ style: "color:#333333;text-decoration:none;" }
= @merge_request.source_branch = @merge_request.source_branch
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" }= _("Author")
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody

View file

@ -0,0 +1,157 @@
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Configure OpenID Connect in Azure to retrieve temporary credentials
This tutorial demonstrates how to use a JSON web token (JWT) in a GitLab CI/CD job
to retrieve temporary credentials from Azure without needing to store secrets.
To get started, configure OpenID Connect (OIDC) for identity federation between GitLab and Azure.
For more information on using OIDC with GitLab, read [Connect to cloud services](../index.md).
Prerequisites:
- Access to an existing Azure Subscription with `Owner` access level.
- Access to the corresponding Azure Active Directory Tenant with at least the `Application Developer` access level.
- A local installation of the [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli).
Alternatively, you can follow all the steps below with the [Azure Cloud Shell](https://shell.azure.com/).
- A GitLab project.
To complete this tutorial:
1. [Create Azure AD application and service principal](#create-azure-ad-application-and-service-principal).
1. [Create Azure AD federated identity credentials](#create-azure-ad-federated-identity-credentials).
1. [Grant permissions for the service principal](#grant-permissions-for-the-service-principal).
1. [Retrieve a temporary credential](#retrieve-a-temporary-credential).
For more information, review Azure's documentation on [Workload identity federation](https://docs.microsoft.com/azure/active-directory/develop/workload-identity-federation).
## Create Azure AD application and service principal
To create an [Azure AD application](https://docs.microsoft.com/cli/azure/ad/app?view=azure-cli-latest#az-ad-app-create)
and service principal:
1. In the Azure CLI, create the AD application:
```shell
appId=$(az ad app create --display-name gitlab-oidc --query appId -otsv)
```
Save the `appId` (Application client ID) output, as you need it later
to configure your GitLab CI/CD pipeline.
1. Create a corresponding [Service Principal](https://docs.microsoft.com/cli/azure/ad/sp?view=azure-cli-latest#az-ad-sp-create):
```shell
az ad sp create --id $appId --query appId -otsv
```
Instead of the Azure CLI, you can [use the Azure Portal to create these resources](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).
## Create Azure AD federated identity credentials
To create the federated identity credentials for the above Azure AD application:
```shell
objectId=$(az ad app show --id $appId --query id -otsv)
cat <<EOF > body.json
{
"name": "gitlab-federated-identity",
"issuer": "https://gitlab.example.com",
"subject": "project_path:<mygroup>/<myproject>:ref_type:branch:ref:<branch>",
"description": "GitLab service account federated identity",
"audiences": [
"https://gitlab.example.com"
]
}
EOF
az rest --method POST --uri "https://graph.microsoft.com/beta/applications/$objectId/federatedIdentityCredentials" --body @body.json
```
For issues related to the values of `issuer`, `subject` or `audiences`, see the
[troubleshooting](#troubleshooting) details.
Optionally, you can now verify the Azure AD application and the Azure AD federated
identity credentials from the Azure Portal:
1. Open the [Azure Active Directory App Registration](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps)
view and select the appropriate app registration by searching for the display name `gitlab-oidc`.
1. On the overview page you can verify details like the `Application (client) ID`,
`Object ID`, and `Tenant ID`.
1. Under `Certificates & secrets`, go to `Federated credentials` to review your
Azure AD federated identity credentials.
## Grant permissions for the service principal
After you create the credentials, use [`role assignment`](https://docs.microsoft.com/cli/azure/role/assignment?view=azure-cli-latest#az-role-assignment-create)
to grant permissions to the above service principal to access to Azure resources:
```shell
az role assignment create --assignee $appId --role Reader --scope /subscriptions/<subscription-id>
```
You can find your subscription ID in:
- The [Azure Portal](https://docs.microsoft.com/azure/azure-portal/get-subscription-tenant-id#find-your-azure-subscription).
- The [Azure CLI](https://docs.microsoft.com/cli/azure/manage-azure-subscriptions-azure-cli#get-the-active-subscription).
## Retrieve a temporary credential
After you configure the Azure AD application and federated identity credentials,
the CI/CD job can retrieve a temporary credential by using the [Azure CLI](https://docs.microsoft.com/cli/azure/reference-index?view=azure-cli-latest#az-login):
```yaml
default:
image: mcr.microsoft.com/azure-cli:latest
variables:
AZURE_CLIENT_ID: "<client-id>"
AZURE_TENANT_ID: "<tenant-id>"
auth:
script:
- az login --service-principal -u $AZURE_CLIENT_ID -t $AZURE_TENANT_ID --federated-token $CI_JOB_JWT_V2
- az account show
```
The CI/CD variables are:
- `AZURE_CLIENT_ID`: The [application client ID you saved earlier](#create-azure-ad-application-and-service-principal).
- `AZURE_TENANT_ID`: Your Azure Active Directory. You can
[find it by using the Azure CLI or Azure Portal](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-how-to-find-tenant).
- `CI_JOB_JWT_V2`: The JSON web token is a [predefined CI/CD variable](../../variables/predefined_variables.md).
## Troubleshooting
### "No matching federated identity record found"
If you receive the error `ERROR: AADSTS70021: No matching federated identity record found for presented assertion.`
you should verify:
- The `Issuer` defined in the Azure AD federated identity credentials, for example
`https://gitlab.com` or your own GitLab URL.
- The `Subject identifier` defined in the Azure AD federated identity credentials,
for example `project_path:<mygroup>/<myproject>:ref_type:branch:ref:<branch>`.
- For the `gitlab-group/gitlab-project` project and `main` branch it would be:
`project_path:gitlab-group/gitlab-project:ref_type:branch:ref:main`.
- The correct values of `mygroup` and `myproject` can be retrieved by checking the URL
when accessing your GitLab project or by selecting the **Clone** option in the project.
- The `Audience` defined in the Azure AD federated identity credentials, for example `https://gitlab.com`
or your own GitLab URL.
You can review these settings, as well as your `AZURE_CLIENT_ID` and `AZURE_TENANT_ID`
CI/CD variables, from the Azure Portal:
1. Open the [Azure Active Directory App Registration](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps)
view and select the appropriate app registration by searching for the display name `gitlab-oidc`.
1. On the overview page you can verify details like the `Application (client) ID`,
`Object ID`, and `Tenant ID`.
1. Under `Certificates & secrets`, go to `Federated credentials` to review your
Azure AD federated identity credentials.
Review [Connect to cloud services](../index.md) for further details.

View file

@ -16,7 +16,7 @@ GitLab CI/CD supports [OpenID Connect (OIDC)](https://openid.net/connect/faq/) t
- Account on GitLab. - Account on GitLab.
- Access to a cloud provider that supports OIDC to configure authorization and create roles. - Access to a cloud provider that supports OIDC to configure authorization and create roles.
The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, GCP, and Vault. The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault.
NOTE: NOTE:
Configuring OIDC enables JWT token access to the target environments for all pipelines. Configuring OIDC enables JWT token access to the target environments for all pipelines.
@ -38,7 +38,7 @@ The `CI_JOB_JWT_V2` variable is under development [(alpha)](../../policy/alpha-b
## How it works ## How it works
Each job has a JSON web token (JWT) provided as a CI/CD [predefined variable](../variables/predefined_variables.md) named `CI_JOB_JWT` or `CI_JOB_JWT_V2`. This JWT can be used to authenticate with the OIDC-supported cloud provider such as AWS, GCP, or Vault. Each job has a JSON web token (JWT) provided as a CI/CD [predefined variable](../variables/predefined_variables.md) named `CI_JOB_JWT` or `CI_JOB_JWT_V2`. This JWT can be used to authenticate with the OIDC-supported cloud provider such as AWS, Azure, GCP, or Vault.
The following fields are included in the JWT: The following fields are included in the JWT:
@ -112,7 +112,7 @@ sequenceDiagram
``` ```
1. Create an OIDC identity provider in the cloud (for example, AWS, GCP, Vault). 1. Create an OIDC identity provider in the cloud (for example, AWS, Azure, GCP, Vault).
1. Create a conditional role in the cloud service that filters to a group, project, branch, or tag. 1. Create a conditional role in the cloud service that filters to a group, project, branch, or tag.
1. The CI/CD job includes a predefined variable `CI_JOB_JWT_V2` that is a JWT token. You can use this token for authorization with your cloud API. 1. The CI/CD job includes a predefined variable `CI_JOB_JWT_V2` that is a JWT token. You can use this token for authorization with your cloud API.
1. The cloud verifies the token, validates the conditional role from the payload, and returns a temporary credential. 1. The cloud verifies the token, validates the conditional role from the payload, and returns a temporary credential.
@ -138,4 +138,5 @@ To configure the trust between GitLab and OIDC, you must create a conditional ro
To connect with your cloud provider, see the following tutorials: To connect with your cloud provider, see the following tutorials:
- [Configure OpenID Connect in AWS](aws/index.md) - [Configure OpenID Connect in AWS](aws/index.md)
- [Configure OpenID Connect in Azure](azure/index.md)
- [Configure OpenID Connect in Google Cloud](google_cloud/index.md) - [Configure OpenID Connect in Google Cloud](google_cloud/index.md)

View file

@ -26857,6 +26857,9 @@ msgstr ""
msgid "Notify|%{member_link} requested %{member_role} access to the %{target_source_link} %{target_type}." msgid "Notify|%{member_link} requested %{member_role} access to the %{target_source_link} %{target_type}."
msgstr "" msgstr ""
msgid "Notify|%{mr_highlight}Merge request%{highlight_end} %{mr_link} %{approved_highlight}was approved by%{highlight_end} %{approver_avatar} %{approver_link}"
msgstr ""
msgid "Notify|Assignee changed from %{fromNames} to %{toNames}" msgid "Notify|Assignee changed from %{fromNames} to %{toNames}"
msgstr "" msgstr ""
@ -26902,6 +26905,12 @@ msgstr ""
msgid "Notify|Merge request URL: %{merge_request_url}" msgid "Notify|Merge request URL: %{merge_request_url}"
msgstr "" msgstr ""
msgid "Notify|Merge request was approved"
msgstr ""
msgid "Notify|Merge request was approved (%{approvals}/%{required_approvals})"
msgstr ""
msgid "Notify|Milestone changed to %{milestone}" msgid "Notify|Milestone changed to %{milestone}"
msgstr "" msgstr ""

View file

@ -7,6 +7,7 @@ import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarStore from '~/sidebar/stores/sidebar_store';
import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
import Mock from './mock_data'; import Mock from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
@ -75,7 +76,9 @@ describe('SidebarMoveIssue', () => {
it('should initialize the deprecatedJQueryDropdown', () => { it('should initialize the deprecatedJQueryDropdown', () => {
test.sidebarMoveIssue.initDropdown(); test.sidebarMoveIssue.initDropdown();
expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy(); expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeInstanceOf(
GitLabDropdown,
);
}); });
it('escapes html from project name', async () => { it('escapes html from project name', async () => {
@ -97,7 +100,7 @@ describe('SidebarMoveIssue', () => {
test.sidebarMoveIssue.onConfirmClicked(); test.sidebarMoveIssue.onConfirmClicked();
expect(test.mediator.moveIssue).toHaveBeenCalled(); expect(test.mediator.moveIssue).toHaveBeenCalled();
expect(test.$confirmButton.prop('disabled')).toBeTruthy(); expect(test.$confirmButton.prop('disabled')).toBe(true);
expect(test.$confirmButton.hasClass('is-loading')).toBe(true); expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
}); });
@ -113,7 +116,7 @@ describe('SidebarMoveIssue', () => {
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalled(); expect(createFlash).toHaveBeenCalled();
expect(test.$confirmButton.prop('disabled')).toBeFalsy(); expect(test.$confirmButton.prop('disabled')).toBe(false);
expect(test.$confirmButton.hasClass('is-loading')).toBe(false); expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
}); });
@ -139,7 +142,7 @@ describe('SidebarMoveIssue', () => {
test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
expect(test.$confirmButton.prop('disabled')).toBeTruthy(); expect(test.$confirmButton.prop('disabled')).toBe(true);
}); });
it('should set moveToProjectId on dropdown item click', async () => { it('should set moveToProjectId on dropdown item click', async () => {

View file

@ -51,6 +51,33 @@ RSpec.describe NotifyHelper do
end end
end end
describe '#merge_request_approved_description' do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
let(:avatar_icon_for_user) { 'avatar_icon_for_user' }
before do
allow(helper).to receive(:avatar_icon_for_user).and_return(avatar_icon_for_user)
end
it 'returns MR approved description' do
result = helper.merge_request_approved_description(merge_request, user)
expect(result).to eq "<span style=\"font-weight: 600;color:#333333;\">Merge request</span> " \
"#{
link_to(merge_request.to_reference, merge_request_url(merge_request),
style: "font-weight: 600;color:#3777b0;text-decoration:none")
} " \
"<span>was approved by</span> " \
"#{
content_tag(:img, nil, height: "24", src: avatar_icon_for_user,
style: "border-radius:12px;margin:-7px 0 -7px 3px;",
width: "24", alt: "Avatar", class: "avatar"
)
} " \
"#{link_to(user.name, user_url(user), style: "color:#333333;text-decoration:none;", class: "muted")}"
end
end
def reference_link(entity, url) def reference_link(entity, url)
"<a href=\"#{url}\">#{entity.to_reference}</a>" "<a href=\"#{url}\">#{entity.to_reference}</a>"
end end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
require 'email_spec'
RSpec.describe 'notify/approved_merge_request_email.html.haml' do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
before do
allow(view).to receive(:message) { instance_double(Mail::Message, subject: 'Subject') }
assign(:project, project)
assign(:approved_by, user)
assign(:merge_request, merge_request)
end
it 'contains approval information' do
render
expect(rendered).to have_content(merge_request.to_reference.to_s)
expect(rendered).to have_content("was approved by")
expect(rendered).to have_content(user.name.to_s)
end
end