Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-02 09:10:59 +00:00
parent 377b57afa8
commit 78bc39880c
37 changed files with 521 additions and 161 deletions

View File

@ -16,10 +16,10 @@
GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
GITLAB_QA_ADMIN_ACCESS_TOKEN: "${REVIEW_APPS_ROOT_TOKEN}"
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
SIGNUP_DISABLED: "true"
before_script:
# Use $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA so that GitLab image built in omnibus-gitlab-mirror and QA image are in sync.
- export EE_LICENSE="$(cat $REVIEW_APPS_EE_LICENSE_FILE)"
- if [ -n "$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA" ]; then
git checkout -f ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA};
fi

View File

@ -1,6 +1,7 @@
<script>
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
import environmentAppQuery from '../graphql/queries/environmentApp.query.graphql';
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue';
export default {
@ -13,7 +14,16 @@ export default {
apollo: {
environmentApp: {
query: environmentAppQuery,
pollInterval() {
return this.interval;
},
},
interval: {
query: pollIntervalQuery,
},
},
data() {
return { interval: undefined };
},
computed: {
folders() {

View File

@ -1,6 +1,6 @@
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import environmentApp from './queries/environmentApp.query.graphql';
import environmentApp from './queries/environment_app.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';

View File

@ -0,0 +1,3 @@
query pollInterval {
interval @client
}

View File

@ -1,5 +1,12 @@
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import pollIntervalQuery from './queries/poll_interval.query.graphql';
const buildErrors = (errors = []) => ({
errors,
__typename: 'LocalEnvironmentErrors',
});
const mapNestedEnvironment = (env) => ({
...convertObjectPropsToCamelCase(env, { deep: true }),
@ -12,17 +19,27 @@ const mapEnvironment = (env) => ({
export const resolvers = (endpoint) => ({
Query: {
environmentApp() {
return axios.get(endpoint, { params: { nested: true } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapNestedEnvironment),
reviewApp: {
...convertObjectPropsToCamelCase(res.data.review_app),
__typename: 'ReviewApp',
},
stoppedCount: res.data.stopped_count,
__typename: 'LocalEnvironmentApp',
}));
environmentApp(_context, _variables, { cache }) {
return axios.get(endpoint, { params: { nested: true } }).then((res) => {
const interval = res.headers['poll-interval'];
if (interval) {
cache.writeQuery({ query: pollIntervalQuery, data: { interval } });
} else {
cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } });
}
return {
availableCount: res.data.available_count,
environments: res.data.environments.map(mapNestedEnvironment),
reviewApp: {
...convertObjectPropsToCamelCase(res.data.review_app),
__typename: 'ReviewApp',
},
stoppedCount: res.data.stopped_count,
__typename: 'LocalEnvironmentApp',
};
});
},
folder(_, { environment: { folderPath } }) {
return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
@ -32,19 +49,51 @@ export const resolvers = (endpoint) => ({
__typename: 'LocalEnvironmentFolder',
}));
},
isLastDeployment(_, { environment }) {
// eslint-disable-next-line @gitlab/require-i18n-strings
return environment?.lastDeployment?.['last?'];
},
},
Mutations: {
stopEnvironment(_, { environment: { stopPath } }) {
return axios.post(stopPath);
Mutation: {
stopEnvironment(_, { environment }) {
return axios
.post(environment.stopPath)
.then(() => buildErrors())
.catch(() => {
return buildErrors([
s__('Environments|An error occurred while stopping the environment, please try again'),
]);
});
},
deleteEnvironment(_, { environment: { deletePath } }) {
return axios.delete(deletePath);
},
rollbackEnvironment(_, { environment: { retryUrl } }) {
return axios.post(retryUrl);
rollbackEnvironment(_, { environment, isLastDeployment }) {
return axios
.post(environment?.retryUrl)
.then(() => buildErrors())
.catch(() => {
buildErrors([
isLastDeployment
? s__(
'Environments|An error occurred while re-deploying the environment, please try again',
)
: s__(
'Environments|An error occurred while rolling back the environment, please try again',
),
]);
});
},
cancelAutoStop(_, { environment: { autoStopPath } }) {
return axios.post(autoStopPath);
return axios
.post(autoStopPath)
.then(() => buildErrors())
.catch((err) =>
buildErrors([
err?.response?.data?.message ||
s__('Environments|An error occurred while canceling the auto stop, please try again'),
]),
);
},
},
});

View File

@ -33,3 +33,20 @@ type LocalEnvironmentApp {
environments: [NestedLocalEnvironment!]!
reviewApp: ReviewApp!
}
type LocalErrors {
errors: [String!]!
}
extend type Query {
environmentApp: LocalEnvironmentApp
folder(environment: NestedLocalEnvironment): LocalEnvironmentFolder
isLastDeployment: Boolean
}
extend type Mutation {
stopEnvironment(environment: LocalEnvironment): LocalErrors
deleteEnvironment(environment: LocalEnvironment): LocalErrors
rollbackEnvironment(environment: LocalEnvironment): LocalErrors
cancelAutoStop(environment: LocalEnvironment): LocalErrors
}

View File

@ -5,7 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default async function initJiraConnectBranches() {
export default function initJiraConnectBranches() {
const el = document.querySelector('.js-jira-connect-create-branch');
if (!el) {
return null;

View File

@ -7,6 +7,7 @@ import { SET_ALERT } from '../store/mutation_types';
import SubscriptionsList from './subscriptions_list.vue';
import AddNamespaceButton from './add_namespace_button.vue';
import SignInButton from './sign_in_button.vue';
import UserLink from './user_link.vue';
export default {
name: 'JiraConnectApp',
@ -18,6 +19,7 @@ export default {
SubscriptionsList,
AddNamespaceButton,
SignInButton,
UserLink,
},
inject: {
usersPath: {
@ -74,6 +76,8 @@ export default {
</template>
</gl-alert>
<user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" />
<h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7">
<template v-if="hasSubscriptions">

View File

@ -0,0 +1,67 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
export default {
components: {
GlLink,
GlSprintf,
},
inject: {
usersPath: {
default: '',
},
gitlabUserPath: {
default: '',
},
},
props: {
userSignedIn: {
type: Boolean,
required: true,
},
hasSubscriptions: {
type: Boolean,
required: true,
},
},
data() {
return {
signInURL: '',
};
},
computed: {
gitlabUserHandle() {
return `@${gon.current_username}`;
},
},
async created() {
this.signInURL = await getGitlabSignInURL(this.usersPath);
},
i18n: {
signInText: __('Sign in to GitLab'),
signedInAsUserText: __('Signed in to GitLab as %{user_link}'),
},
};
</script>
<template>
<div class="jira-connect-user gl-font-base">
<gl-sprintf v-if="userSignedIn" :message="$options.i18n.signedInAsUserText">
<template #user_link>
<gl-link data-testid="gitlab-user-link" :href="gitlabUserPath" target="_blank">
{{ gitlabUserHandle }}
</gl-link>
</template>
</gl-sprintf>
<gl-link
v-else-if="hasSubscriptions"
data-testid="sign-in-link"
:href="signInURL"
target="_blank"
>
{{ $options.i18n.signInText }}
</gl-link>
</div>
</template>

View File

@ -7,25 +7,11 @@ import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
import { getGitlabSignInURL, sizeToParent } from './utils';
import { sizeToParent } from './utils';
const store = createStore();
/**
* Add `return_to` query param to all HAML-defined GitLab sign in links.
*/
const updateSignInLinks = async () => {
await Promise.all(
Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).map(async (el) => {
const updatedLink = await getGitlabSignInURL(el.getAttribute('href'));
el.setAttribute('href', updatedLink);
}),
);
};
export async function initJiraConnect() {
await updateSignInLinks();
export function initJiraConnect() {
const el = document.querySelector('.js-jira-connect-app');
if (!el) {
return null;
@ -35,7 +21,7 @@ export async function initJiraConnect() {
Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin);
const { groupsPath, subscriptions, subscriptionsPath, usersPath } = el.dataset;
const { groupsPath, subscriptions, subscriptionsPath, usersPath, gitlabUserPath } = el.dataset;
sizeToParent();
return new Vue({
@ -46,6 +32,7 @@ export async function initJiraConnect() {
subscriptions: JSON.parse(subscriptions),
subscriptionsPath,
usersPath,
gitlabUserPath,
},
render(createElement) {
return createElement(JiraConnectApp);

View File

@ -282,7 +282,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
if merge_request.errors.present?
render json: @merge_request.errors, status: :bad_request
else
render json: serializer.represent(@merge_request, serializer: 'basic')
render json: serializer.represent(@merge_request, serializer: params[:serializer] || 'basic')
end
end
end

View File

@ -8,7 +8,8 @@ module JiraConnectHelper
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }),
subscriptions: subscriptions.map { |s| serialize_subscription(s) }.to_json,
subscriptions_path: jira_connect_subscriptions_path,
users_path: current_user ? nil : jira_connect_users_path
users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in
gitlab_user_path: current_user ? user_path(current_user) : nil
}
end

View File

@ -40,7 +40,9 @@ module SystemNoteHelper
'new_alert_added' => 'warning',
'severity' => 'information-o',
'cloned' => 'documents',
'issue_type' => 'pencil-square'
'issue_type' => 'pencil-square',
'attention_requested' => 'user',
'attention_request_removed' => 'user'
}.freeze
def system_note_icon_name(note)

View File

@ -15,11 +15,5 @@ module MergeRequestReviewerState
inclusion: { in: self.states.keys }
after_initialize :set_state, unless: :persisted?
def set_state
if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
self.state = :attention_requested
end
end
end
end

View File

@ -10,6 +10,12 @@ class MergeRequestAssignee < ApplicationRecord
scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
def set_state
if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
self.state = MergeRequestReviewer.find_by(user_id: self.user_id, merge_request_id: self.merge_request_id)&.state || :attention_requested
end
end
def cache_key
[model_name.cache_key, id, state, assignee.cache_key]
end

View File

@ -6,6 +6,12 @@ class MergeRequestReviewer < ApplicationRecord
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
def set_state
if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
self.state = MergeRequestAssignee.find_by(user_id: self.user_id, merge_request_id: self.merge_request_id)&.state || :attention_requested
end
end
def cache_key
[model_name.cache_key, id, state, reviewer.cache_key]
end

View File

@ -24,6 +24,7 @@ class SystemNoteMetadata < ApplicationRecord
opened closed merged duplicate locked unlocked outdated reviewer
tag due_date pinned_embed cherry_pick health_status approved unapproved
status alert_issue_added relate unrelate new_alert_added severity
attention_requested attention_request_removed
].freeze
validates :note, presence: true, unless: :importing?

View File

@ -19,7 +19,10 @@ module MergeRequests
update_state(assignee)
if reviewer&.attention_requested? || assignee&.attention_requested?
create_attention_request_note
notity_user
else
create_remove_attention_request_note
end
success
@ -35,6 +38,14 @@ module MergeRequests
todo_service.create_attention_requested_todo(merge_request, current_user, user)
end
def create_attention_request_note
SystemNoteService.request_attention(merge_request, merge_request.project, current_user, user)
end
def create_remove_attention_request_note
SystemNoteService.remove_attention_request(merge_request, merge_request.project, current_user, user)
end
def assignee
merge_request.find_assignee(user)
end

View File

@ -115,6 +115,14 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_status(status, source)
end
def request_attention(noteable, project, author, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).request_attention(user)
end
def remove_attention_request(noteable, project, author, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).remove_attention_request(user)
end
# Called when 'merge when pipeline succeeds' is executed
def merge_when_pipeline_succeeds(noteable, project, author, sha)
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).merge_when_pipeline_succeeds(sha)

View File

@ -323,6 +323,52 @@ module SystemNotes
existing_mentions_for(mentioned_in, noteable, notes).exists?
end
# Called when a user's attention has been requested for a Notable
#
# user - User's whos attention has been requested
#
# Example Note text:
#
# "requested attention from @eli.wisoky"
#
# Returns the created Note object
def request_attention(user)
body = "requested attention from #{user.to_reference}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'attention_requested'))
end
# Called when a user's attention request has been removed for a Notable
#
# user - User's whos attention request has been removed
#
# Example Note text:
#
# "removed attention request from @eli.wisoky"
#
# Returns the created Note object
def remove_attention_request(user)
body = "removed attention request from #{user.to_reference}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'attention_request_removed'))
end
# Called when a Noteable has been marked as the canonical Issue of a duplicate
#
# duplicate_issue - Issue that was a duplicate of this
#
# Example Note text:
#
# "marked #1234 as a duplicate of this issue"
#
# "marked other_project#5678 as a duplicate of this issue"
#
# Returns the created Note object
def mark_canonical_issue_of_duplicate(duplicate_issue)
body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
# Called when a Noteable has been marked as a duplicate of another Issue
#
# canonical_issue - Issue that this is a duplicate of
@ -342,22 +388,6 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
# Called when a Noteable has been marked as the canonical Issue of a duplicate
#
# duplicate_issue - Issue that was a duplicate of this
#
# Example Note text:
#
# "marked #1234 as a duplicate of this issue"
#
# "marked other_project#5678 as a duplicate of this issue"
#
# Returns the created Note object
def mark_canonical_issue_of_duplicate(duplicate_issue)
body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
def add_email_participants(body)
create_note(NoteSummary.new(noteable, project, author, body))
end

View File

@ -1,13 +1,6 @@
%header.jira-connect-header.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-px-5.gl-border-b-solid.gl-border-b-gray-100.gl-border-b-1.gl-bg-white
= link_to brand_header_logo, Gitlab.config.gitlab.url, target: '_blank', rel: 'noopener noreferrer'
.jira-connect-user.gl-font-base
- if current_user
- user_link = link_to(current_user.to_reference, jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in')
= _('Signed in to GitLab as %{user_link}').html_safe % { user_link: user_link }
- elsif @subscriptions.present?
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto
.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }

View File

@ -630,9 +630,13 @@ keys"](loose_foreign_keys.md).
## `config/database.yml`
GitLab will support running multiple databases in the future, for example to [separate tables for the continuous integration features](https://gitlab.com/groups/gitlab-org/-/epics/6167) from the main database. In order to prepare for this change, we [validate the structure of the configuration](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67877) in `database.yml` to ensure that only known databases are used.
GitLab is adding support to run multiple databases, for example to
[separate tables for the continuous integration features](https://gitlab.com/groups/gitlab-org/-/epics/6167)
from the main database. In order to prepare for this change, we
[validate the structure of the configuration](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67877)
in `database.yml` to ensure that only known databases are used.
Previously, the `config/database.yml` would look like this:
Previously, the `config/database.yml` looked like this:
```yaml
production:
@ -642,15 +646,16 @@ production:
...
```
With the support for many databases the support for this
syntax is deprecated and will be removed in [15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338182).
With the support for many databases this
syntax is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/338182)
and will be removed in [15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338182).
The new `config/database.yml` needs to include a database name
to define a database configuration. Only `main:` and `ci:` database
names are supported today. The `main:` needs to always be a first
names are supported. The `main:` database must always be a first
entry in a hash. This change applies to decomposed and non-decomposed
change. If an invalidate or deprecated syntax is used the error
or warning will be printed during application start.
change. If an invalid or deprecated syntax is used the error
or warning is printed during application start.
```yaml
# Non-decomposed database

View File

@ -375,7 +375,7 @@ Sometimes, you might want to abandon the unfinished reindex job and resume the i
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Advanced Search**.
1. Expand **Elasticsearch zero-downtime reindexing**.
1. Expand **Advanced Search**.
1. Clear the **Pause Elasticsearch indexing** checkbox.
## Advanced Search migrations

View File

@ -18,7 +18,7 @@ Each release of GitLab Mattermost is compiled and manually tested on an AMD 64 c
## Getting started
GitLab Mattermost expects to run on its own virtual host. In your DNS settings, you will need
GitLab Mattermost expects to run on its own virtual host. In your DNS settings, you need
two entries pointing to the same machine. For example, `gitlab.example.com` and
`mattermost.example.com`.
@ -41,7 +41,7 @@ GitLab Mattermost is disabled by default. To enable it:
The Omnibus GitLab package attempts to automatically authorize GitLab Mattermost with GitLab if the applications are running on the same server.
Automatic authorization requires access to the GitLab database. If the GitLab database is not available
you will need to manually authorize GitLab Mattermost for access to GitLab using the process described in the [Authorize GitLab Mattermost section](#authorize-gitlab-mattermost).
you need to manually authorize GitLab Mattermost for access to GitLab using the process described in the [Authorize GitLab Mattermost section](#authorize-gitlab-mattermost).
## Configuring Mattermost
@ -51,7 +51,7 @@ Mattermost settings and where they can be set is available [in the Mattermost do
While using the System Console is recommended, you can also configure Mattermost using one of the following options:
1. Edit the Mattermost configuration directly through `/var/opt/gitlab/mattermost/config.json`.
1. Specify environment variables used to run Mattermost by changing the `mattermost['env']` setting in `gitlab.rb`. Any settings configured in this way will be disabled from the System Console and cannot be changed without restarting Mattermost.
1. Specify environment variables used to run Mattermost by changing the `mattermost['env']` setting in `gitlab.rb`. Any settings configured in this way are disabled from the System Console and cannot be changed without restarting Mattermost.
## Running GitLab Mattermost with HTTPS
@ -71,7 +71,7 @@ mattermost_nginx['redirect_http_to_https'] = true
```
If you haven't named your certificate and key `mattermost.gitlab.example.crt`
and `mattermost.gitlab.example.key` then you'll need to also add the full paths
and `mattermost.gitlab.example.key` then you need to also add the full paths
as shown below.
```ruby
@ -85,7 +85,7 @@ Once the configuration is set, run `sudo gitlab-ctl reconfigure` to apply the ch
## Running GitLab Mattermost on its own server
If you want to run GitLab and GitLab Mattermost on two separate servers the GitLab services will still be set up on your GitLab Mattermost server, but they will not accept user requests or
If you want to run GitLab and GitLab Mattermost on two separate servers the GitLab services are still set up on your GitLab Mattermost server, but they do not accept user requests or
consume system resources. You can use the following settings and configuration details on the GitLab Mattermost server to effectively disable the GitLab service bundled into the Omnibus package.
```ruby
@ -124,7 +124,7 @@ http://mattermost.example.com/login/gitlab/complete
Note that you do not need to select any options under **Scopes**. Choose **Save application**.
Once the application is created you will be provided with an `Application ID` and `Secret`. One other piece of information needed is the URL of GitLab instance.
Once the application is created you are provided with an `Application ID` and `Secret`. One other piece of information needed is the URL of GitLab instance.
Return to the server running GitLab Mattermost and edit the `/etc/gitlab/gitlab.rb` configuration file as follows using the values you received above:
```ruby
@ -190,7 +190,7 @@ sudo -i -u gitlab-psql -- /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/po
#### Back up the `data` directory and `config.json`
Mattermost has a `data` directory and `config.json` file that will need to be backed up as well:
Mattermost has a `data` directory and `config.json` file that need to be backed up as well:
```shell
sudo tar -zcvf mattermost_data_$(date --rfc-3339=date).gz -C /var/opt/gitlab/mattermost data config.json

View File

@ -367,7 +367,8 @@ RSpec.describe Projects::MergeRequestsController do
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
merge_request: mr_params
merge_request: mr_params,
serializer: 'basic'
}.merge(additional_params)
put :update, params: params

View File

@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql';
import { TEST_HOST } from 'helpers/test_constants';
import { environmentsApp, resolvedEnvironmentsApp, folder, resolvedFolder } from './mock_data';
@ -21,10 +22,27 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('environmentApp', () => {
it('should fetch environments and map them to frontend data', async () => {
mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp);
const cache = { writeQuery: jest.fn() };
mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp, {});
const app = await mockResolvers.Query.environmentApp();
const app = await mockResolvers.Query.environmentApp(null, null, { cache });
expect(app).toEqual(resolvedEnvironmentsApp);
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
data: { interval: undefined },
});
});
it('should set the poll interval when there is one', async () => {
const cache = { writeQuery: jest.fn() };
mock
.onGet(ENDPOINT, { params: { nested: true } })
.reply(200, environmentsApp, { 'poll-interval': 3000 });
await mockResolvers.Query.environmentApp(null, null, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
data: { interval: 3000 },
});
});
});
describe('folder', () => {
@ -42,7 +60,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(200);
await mockResolvers.Mutations.stopEnvironment(null, { environment: { stopPath: ENDPOINT } });
await mockResolvers.Mutation.stopEnvironment(null, { environment: { stopPath: ENDPOINT } });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
@ -53,7 +71,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should post to the retry environment path', async () => {
mock.onPost(ENDPOINT).reply(200);
await mockResolvers.Mutations.rollbackEnvironment(null, {
await mockResolvers.Mutation.rollbackEnvironment(null, {
environment: { retryUrl: ENDPOINT },
});
@ -66,7 +84,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should DELETE to the delete environment path', async () => {
mock.onDelete(ENDPOINT).reply(200);
await mockResolvers.Mutations.deleteEnvironment(null, {
await mockResolvers.Mutation.deleteEnvironment(null, {
environment: { deletePath: ENDPOINT },
});
@ -79,7 +97,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should post to the auto stop path', async () => {
mock.onPost(ENDPOINT).reply(200);
await mockResolvers.Mutations.cancelAutoStop(null, {
await mockResolvers.Mutation.cancelAutoStop(null, {
environment: { autoStopPath: ENDPOINT },
});

View File

@ -5,6 +5,7 @@ import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { __ } from '~/locale';
@ -12,6 +13,7 @@ import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
getGitlabSignInURL: jest.fn(),
}));
describe('JiraConnectApp', () => {
@ -83,6 +85,22 @@ describe('JiraConnectApp', () => {
});
},
);
it('renders UserLink component', () => {
createComponent({
provide: {
usersPath: '/user',
subscriptions: [],
},
});
const userLink = wrapper.findComponent(UserLink);
expect(userLink.exists()).toBe(true);
expect(userLink.props()).toEqual({
hasSubscriptions: false,
userSignedIn: false,
});
});
});
describe('alert', () => {

View File

@ -0,0 +1,91 @@
import { GlSprintf } from '@gitlab/ui';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
getGitlabSignInURL: jest.fn().mockImplementation((path) => Promise.resolve(path)),
}));
describe('SubscriptionsList', () => {
let wrapper;
const createComponent = (propsData = {}, { provide } = {}) => {
wrapper = shallowMountExtended(UserLink, {
propsData,
provide,
stubs: {
GlSprintf,
},
});
};
const findSignInLink = () => wrapper.findByTestId('sign-in-link');
const findGitlabUserLink = () => wrapper.findByTestId('gitlab-user-link');
const findSprintf = () => wrapper.findComponent(GlSprintf);
afterEach(() => {
wrapper.destroy();
});
describe.each`
userSignedIn | hasSubscriptions | expectGlSprintf | expectGlLink
${true} | ${false} | ${true} | ${false}
${false} | ${true} | ${false} | ${true}
${true} | ${true} | ${true} | ${false}
${false} | ${false} | ${false} | ${false}
`(
'when `userSignedIn` is $userSignedIn and `hasSubscriptions` is $hasSubscriptions',
({ userSignedIn, hasSubscriptions, expectGlSprintf, expectGlLink }) => {
it('renders template correctly', () => {
createComponent({
userSignedIn,
hasSubscriptions,
});
expect(findSprintf().exists()).toBe(expectGlSprintf);
expect(findSignInLink().exists()).toBe(expectGlLink);
});
},
);
describe('sign in link', () => {
it('renders with correct href', async () => {
const mockUsersPath = '/user';
createComponent(
{
userSignedIn: false,
hasSubscriptions: true,
},
{ provide: { usersPath: mockUsersPath } },
);
await waitForPromises();
expect(findSignInLink().exists()).toBe(true);
expect(findSignInLink().attributes('href')).toBe(mockUsersPath);
});
});
describe('gitlab user link', () => {
window.gon = { current_username: 'root' };
beforeEach(() => {
createComponent(
{
userSignedIn: true,
hasSubscriptions: true,
},
{ provide: { gitlabUserPath: '/root' } },
);
});
it('renders with correct href', () => {
expect(findGitlabUserLink().attributes('href')).toBe('/root');
});
it('contains GitLab user handle', () => {
expect(findGitlabUserLink().text()).toBe('@root');
});
});
});

View File

@ -1,36 +0,0 @@
import { initJiraConnect } from '~/jira_connect/subscriptions';
import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
jest.mock('~/jira_connect/subscriptions/utils');
describe('initJiraConnect', () => {
const mockInitialHref = 'https://gitlab.com';
beforeEach(() => {
setFixtures(`
<a class="js-jira-connect-sign-in" href="${mockInitialHref}">Sign In</a>
<a class="js-jira-connect-sign-in" href="${mockInitialHref}">Another Sign In</a>
`);
});
const assertSignInLinks = (expectedLink) => {
Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
expect(el.getAttribute('href')).toBe(expectedLink);
});
};
describe('Sign in links', () => {
it('are updated on initialization', async () => {
const mockSignInLink = `https://gitlab.com?return_to=${encodeURIComponent('/test/location')}`;
getGitlabSignInURL.mockResolvedValue(mockSignInLink);
// assert the initial state
assertSignInLinks(mockInitialHref);
await initJiraConnect();
// assert the update has occurred
assertSignInLinks(mockSignInLink);
});
});
});

View File

@ -19,7 +19,9 @@ RSpec.describe JiraConnectHelper do
is_expected.to include(
:groups_path,
:subscriptions_path,
:users_path
:users_path,
:subscriptions,
:gitlab_user_path
)
end
@ -32,6 +34,10 @@ RSpec.describe JiraConnectHelper do
expect(subject[:groups_path]).to include("#{skip_groups_param}=#{subscription.namespace.id}")
end
it 'assigns gitlab_user_path to nil' do
expect(subject[:gitlab_user_path]).to be_nil
end
end
context 'user is logged in' do
@ -42,6 +48,10 @@ RSpec.describe JiraConnectHelper do
it 'assigns users_path to nil' do
expect(subject[:users_path]).to be_nil
end
it 'assigns gitlab_user_path correctly' do
expect(subject[:gitlab_user_path]).to eq(user_path(user))
end
end
end
end

View File

@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe MergeRequestAssignee do
let(:assignee) { create(:user) }
let(:merge_request) { create(:merge_request) }
subject { merge_request.merge_request_assignees.build(assignee: create(:user)) }
subject { merge_request.merge_request_assignees.build(assignee: assignee) }
describe 'associations' do
it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
@ -41,4 +42,13 @@ RSpec.describe MergeRequestAssignee do
it_behaves_like 'having unique enum values'
it_behaves_like 'having reviewer state'
describe 'syncs to reviewer state' do
before do
reviewer = merge_request.merge_request_reviewers.build(reviewer: assignee)
reviewer.update!(state: :reviewed)
end
it { is_expected.to have_attributes(state: 'reviewed') }
end
end

View File

@ -3,14 +3,24 @@
require 'spec_helper'
RSpec.describe MergeRequestReviewer do
let(:reviewer) { create(:user) }
let(:merge_request) { create(:merge_request) }
subject { merge_request.merge_request_reviewers.build(reviewer: create(:user)) }
subject { merge_request.merge_request_reviewers.build(reviewer: reviewer) }
it_behaves_like 'having unique enum values'
it_behaves_like 'having reviewer state'
describe 'syncs to assignee state' do
before do
assignee = merge_request.merge_request_assignees.build(assignee: reviewer)
assignee.update!(state: :reviewed)
end
it { is_expected.to have_attributes(state: 'reviewed') }
end
describe 'associations' do
it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
it { is_expected.to belong_to(:reviewer).class_name('User').inverse_of(:merge_request_reviewers) }

View File

@ -19,6 +19,8 @@ RSpec.describe MergeRequests::ToggleAttentionRequestedService do
allow(NotificationService).to receive(:new) { notification_service }
allow(service).to receive(:todo_service).and_return(todo_service)
allow(service).to receive(:notification_service).and_return(notification_service)
allow(SystemNoteService).to receive(:request_attention)
allow(SystemNoteService).to receive(:remove_attention_request)
project.add_developer(current_user)
project.add_developer(user)
@ -93,6 +95,12 @@ RSpec.describe MergeRequests::ToggleAttentionRequestedService do
service.execute
end
it 'creates a request attention system note' do
expect(SystemNoteService).to receive(:request_attention).with(merge_request, merge_request.project, current_user, assignee_user)
service.execute
end
end
context 'assignee is the same as reviewer' do
@ -132,6 +140,12 @@ RSpec.describe MergeRequests::ToggleAttentionRequestedService do
service.execute
end
it 'creates a remove attention request system note' do
expect(SystemNoteService).to receive(:remove_attention_request).with(merge_request, merge_request.project, current_user, user)
service.execute
end
end
end
end

View File

@ -146,6 +146,30 @@ RSpec.describe SystemNoteService do
end
end
describe '.request_attention' do
let(:user) { double }
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:request_attention).with(user)
end
described_class.request_attention(noteable, project, author, user)
end
end
describe '.remove_attention_request' do
let(:user) { double }
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:remove_attention_request).with(user)
end
described_class.remove_attention_request(noteable, project, author, user)
end
end
describe '.merge_when_pipeline_succeeds' do
it 'calls MergeRequestsService' do
sha = double

View File

@ -199,6 +199,42 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
describe '#request_attention' do
subject { service.request_attention(user) }
let(:user) { create(:user) }
it_behaves_like 'a system note' do
let(:action) { 'attention_requested' }
end
context 'when attention requested' do
it_behaves_like 'a note with overridable created_at'
it 'sets the note text' do
expect(subject.note).to eq "requested attention from @#{user.username}"
end
end
end
describe '#remove_attention_request' do
subject { service.remove_attention_request(user) }
let(:user) { create(:user) }
it_behaves_like 'a system note' do
let(:action) { 'attention_request_removed' }
end
context 'when attention request is removed' do
it_behaves_like 'a note with overridable created_at'
it 'sets the note text' do
expect(subject.note).to eq "removed attention request from @#{user.username}"
end
end
end
describe '#change_title' do
let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }

View File

@ -1,30 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'jira_connect/subscriptions/index.html.haml' do
let(:user) { build_stubbed(:user) }
before do
allow(view).to receive(:current_user).and_return(user)
assign(:subscriptions, create_list(:jira_connect_subscription, 1))
end
context 'when the user is signed in' do
it 'shows link to user profile' do
render
expect(rendered).to have_link(user.to_reference)
end
end
context 'when the user is not signed in' do
let(:user) { nil }
it 'shows "Sign in" link' do
render
expect(rendered).to have_link('Sign in to GitLab')
end
end
end