Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-16 09:09:18 +00:00
parent 0ab17699c8
commit 14cb5b3d79
46 changed files with 723 additions and 145 deletions

View File

@ -32,6 +32,7 @@ export default {
default: false,
},
},
inject: ['blobHash'],
computed: {
downloadUrl() {
return `${this.rawPath}?inline=false`;
@ -39,6 +40,9 @@ export default {
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;
},
getBlobHashTarget() {
return `[data-blob-hash="${this.blobHash}"]`;
},
},
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
@ -53,7 +57,7 @@ export default {
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
data-clipboard-target="#blob-code-content"
:data-clipboard-target="getBlobHashTarget"
data-testid="copyContentsButton"
icon="copy-to-clipboard"
category="primary"

View File

@ -18,7 +18,7 @@ export default {
};
},
computed: {
...mapGetters({ issue: 'activeIssue' }),
...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }),
hasDueDate() {
return this.issue.dueDate != null;
},
@ -36,10 +36,6 @@ export default {
return dateInWords(this.parsedDueDate, true);
},
projectPath() {
const referencePath = this.issue.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
},
methods: {
...mapActions(['setActiveIssueDueDate']),
@ -53,7 +49,7 @@ export default {
try {
const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPath });
await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
} catch (e) {
createFlash({ message: this.$options.i18n.updateDueDateError });
} finally {

View File

@ -21,9 +21,9 @@ export default {
},
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: {
...mapGetters({ issue: 'activeIssue' }),
...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
selectedLabels() {
const { labels = [] } = this.issue;
const { labels = [] } = this.activeIssue;
return labels.map(label => ({
...label,
@ -31,17 +31,13 @@ export default {
}));
},
issueLabels() {
const { labels = [] } = this.issue;
const { labels = [] } = this.activeIssue;
return labels.map(label => ({
...label,
scoped: isScopedLabel(label),
}));
},
projectPath() {
const { referencePath = '' } = this.issue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
},
methods: {
...mapActions(['setActiveIssueLabels']),
@ -55,7 +51,7 @@ export default {
.filter(label => !payload.find(selected => selected.id === label.id))
.map(label => label.id);
const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath };
const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred while updating labels.') });
@ -68,7 +64,7 @@ export default {
try {
const removeLabelIds = [getIdFromGraphQLId(id)];
const input = { removeLabelIds, projectPath: this.projectPath };
const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred when removing the label.') });

View File

@ -0,0 +1,71 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlToggle } from '@gitlab/ui';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
export default {
i18n: {
header: {
title: __('Notifications'),
/* Any change to subscribeDisabledDescription
must be reflected in app/helpers/notifications_helper.rb */
subscribeDisabledDescription: __(
'Notifications have been disabled by the project or group owner',
),
},
updateSubscribedErrorMessage: s__(
'IssueBoards|An error occurred while setting notifications status.',
),
},
components: {
GlToggle,
},
data() {
return {
loading: false,
};
},
computed: {
...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
notificationText() {
return this.activeIssue.emailsDisabled
? this.$options.i18n.header.subscribeDisabledDescription
: this.$options.i18n.header.title;
},
},
methods: {
...mapActions(['setActiveIssueSubscribed']),
async handleToggleSubscription() {
this.loading = true;
try {
await this.setActiveIssueSubscribed({
subscribed: !this.activeIssue.subscribed,
projectPath: this.projectPathForActiveIssue,
});
} catch (error) {
createFlash({ message: this.$options.i18n.updateSubscribedErrorMessage });
} finally {
this.loading = false;
}
},
},
};
</script>
<template>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between"
data-testid="sidebar-notifications"
>
<span data-testid="notification-header-text"> {{ notificationText }} </span>
<gl-toggle
v-if="!activeIssue.emailsDisabled"
:value="activeIssue.subscribed"
:is-loading="loading"
data-testid="notification-subscribe-toggle"
@change="handleToggleSubscription"
/>
</div>
</template>

View File

@ -0,0 +1,8 @@
mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
issueSetSubscription(input: $input) {
issue {
subscribed
}
errors
}
}

View File

@ -24,6 +24,7 @@ import destroyBoardListMutation from '../queries/board_list_destroy.mutation.gra
import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@ -423,6 +424,29 @@ export default {
});
},
setActiveIssueSubscribed: async ({ commit, getters }, input) => {
const { data } = await gqlClient.mutate({
mutation: issueSetSubscriptionMutation,
variables: {
input: {
iid: String(getters.activeIssue.iid),
projectPath: input.projectPath,
subscribedState: input.subscribed,
},
},
});
if (data.issueSetSubscription?.errors?.length > 0) {
throw new Error(data.issueSetSubscription.errors);
}
commit(types.UPDATE_ISSUE_BY_ID, {
issueId: getters.activeIssue.id,
prop: 'subscribed',
value: data.issueSetSubscription.issue.subscribed,
});
},
fetchBacklog: () => {
notImplemented();
},

View File

@ -24,6 +24,11 @@ export default {
return state.issues[state.activeId] || {};
},
projectPathForActiveIssue: (_, getters) => {
const referencePath = getters.activeIssue.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
getListByLabelId: state => labelId => {
return find(state.boardLists, l => l.label?.id === labelId);
},

View File

@ -6,6 +6,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-admin-licensed-user-count-threshold',
'.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
'.js-registration-enabled-callout',
];
const initCallouts = () => {

View File

@ -51,6 +51,13 @@ export default {
required: true,
},
},
provide() {
return {
blobHash: Math.random()
.toString()
.split('.')[1],
};
},
data() {
return {
blobContent: '',

View File

@ -9,6 +9,7 @@ export default {
GlIcon,
},
mixins: [ViewerMixin],
inject: ['blobHash'],
data() {
return {
highlightedLine: null,
@ -64,7 +65,7 @@ export default {
</a>
</div>
<div class="blob-content">
<pre class="code highlight"><code id="blob-code-content" v-html="content"></code></pre>
<pre class="code highlight"><code :data-blob-hash="blobHash" v-html="content"></code></pre>
</div>
</div>
</template>

View File

@ -394,6 +394,10 @@ module ApplicationSettingsHelper
def show_documentation_base_url_field?
Feature.enabled?(:help_page_documentation_redirect)
end
def signup_enabled?
!!Gitlab::CurrentSettings.signup_enabled
end
end
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')

View File

@ -67,6 +67,7 @@ module NotificationsHelper
when :custom
_('You will only receive notifications for the events you choose')
when :owner_disabled
# Any change must be reflected in board_sidebar_subscription.vue
_('Notifications have been disabled by the project or group owner')
end
end

View File

@ -10,6 +10,7 @@ module UserCalloutsHelper
WEBHOOKS_MOVED = 'webhooks_moved'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
@ -55,6 +56,10 @@ module UserCalloutsHelper
!user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
end
def show_registration_enabled_user_callout?
current_user&.admin? && signup_enabled? && !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
end
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)

View File

@ -105,7 +105,7 @@ module Ci
raise ArgumentError, 'Offset is out of range' if offset < 0 || offset > size
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
in_lock(*lock_params) { unsafe_append_data!(new_data, offset) }
in_lock(lock_key, **lock_params) { unsafe_append_data!(new_data, offset) }
schedule_to_persist! if full?
end
@ -151,7 +151,7 @@ module Ci
# acquired
#
def persist_data!
in_lock(*lock_params) do # exclusive Redis lock is acquired first
in_lock(lock_key, **lock_params) do # exclusive Redis lock is acquired first
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
self.reset.then do |chunk| # we ensure having latest lock_version
@ -289,11 +289,16 @@ module Ci
build.trace_chunks.maximum(:chunk_index).to_i
end
def lock_key
"trace_write:#{build_id}:chunks:#{chunk_index}"
end
def lock_params
["trace_write:#{build_id}:chunks:#{chunk_index}",
{ ttl: WRITE_LOCK_TTL,
retries: WRITE_LOCK_RETRY,
sleep_sec: WRITE_LOCK_SLEEP }]
{
ttl: WRITE_LOCK_TTL,
retries: WRITE_LOCK_RETRY,
sleep_sec: WRITE_LOCK_SLEEP
}
end
def metrics

View File

@ -25,7 +25,8 @@ class UserCallout < ApplicationRecord
personal_access_token_expiry: 21, # EE-only
suggest_pipeline: 22,
customize_homepage: 23,
feature_flags_new_version: 24
feature_flags_new_version: 24,
registration_enabled_callout: 25
}
validates :user, presence: true

View File

@ -31,7 +31,7 @@ module MergeRequests
return error('Failed to create keep around refs.') unless kept_around?
return error('Failed to cache merge ref sha.') unless cache_merge_ref_sha
delete_refs
delete_refs if repository.exists?
return error('Failed to update schedule.') unless update_schedule

View File

@ -16,6 +16,7 @@ module MergeRequests
merge_request.update_project_counter_caches
merge_request.cache_merge_request_closes_issues!(current_user)
merge_request.cleanup_schedule&.destroy
merge_request.update_column(:merge_ref_sha, nil)
end
merge_request

View File

@ -14,7 +14,7 @@
- if @project.last_repository_check_failed?
.row
.col-md-12
.gl-alert.gl-alert-danger.gl-mb-5
.gl-alert.gl-alert-danger.gl-mb-5{ data: { testid: 'last-repository-check-failed-alert' } }
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
- last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")

View File

@ -10,6 +10,7 @@
= render_if_exists "layouts/header/token_expiry_notification"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/header/registration_enabled_callout"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/ping_consent"

View File

@ -0,0 +1,15 @@
- return unless show_registration_enabled_user_callout?
%div{ class: [container_class, @content_class, 'gl-pt-5!'] }
.gl-alert.gl-alert-warning.js-registration-enabled-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: user_callouts_path } }
= sprite_icon('warning', size: 16, css_class: 'gl-alert-icon')
%button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-registration-enabled-callout' } }
= sprite_icon('close', size: 16)
.gl-alert-title
= _('Open registration is enabled on your instance.')
.gl-alert-body
= html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\">".html_safe, anchorClose: '</a>'.html_safe }
.gl-alert-actions
= link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-info btn-md gl-button' do
%span.gl-button-text
= _('View setting')

View File

@ -0,0 +1,5 @@
---
title: Add user callout to alert admins that registration is open by default
merge_request: 47425
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Do not fail when cleaning up MR with no repository
merge_request: 47744
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fixed copy contents functionality for snippets
merge_request: 47646
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Clear cached merge_ref_sha on reopen
merge_request: 47747
author:
type: fixed

View File

@ -106,7 +106,8 @@ test its execution using `CREATE INDEX CONCURRENTLY` in the `#database-lab` Slac
- Write the raw SQL in the MR description. Preferably formatted
nicely with [pgFormatter](https://sqlformat.darold.net) or
[paste.depesz.com](https://paste.depesz.com).
[paste.depesz.com](https://paste.depesz.com) and using regular quotes
(e.g. `"projects"."id"`) and avoiding smart quotes (e.g. `“projects”.“id”`).
- Include the output of `EXPLAIN (ANALYZE, BUFFERS)` of the relevant
queries in the description. If the output is too long, wrap it in
`<details>` blocks, paste it in a GitLab Snippet, or provide the

View File

@ -4,34 +4,34 @@ group: Ecosystem
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/#designated-technical-writers
---
# Setting up a development environment
# Set up a development environment
The following are required to install and test the app:
1. A Jira Cloud instance
- A Jira Cloud instance. Atlassian provides [free instances for development and testing](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app).
- A GitLab instance available over the internet. For the app to work, Jira Cloud should
be able to connect to the GitLab instance through the internet. To easily expose your
local development environment, you can use tools like:
- [serveo](https://medium.com/automationmaster/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf)
- [ngrok](https://ngrok.com).
Atlassian provides free instances for development and testing. [Click here to sign up](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app).
These also take care of SSL for you because Jira requires all connections to the app
host to be over SSL.
1. A GitLab instance available over the internet
## Install the app in Jira
For the app to work, Jira Cloud should be able to connect to the GitLab instance through the internet.
To install the app in Jira:
To easily expose your local development environment, you can use tools like
[serveo](https://medium.com/automationmaster/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf)
or [ngrok](https://ngrok.com). These also take care of SSL for you because Jira
requires all connections to the app host to be over SSL.
1. Enable Jira development mode to install apps that are not from the Atlassian
Marketplace:
## Installing the app in Jira
1. Enable Jira development mode to install apps that are not from the Atlassian Marketplace
1. Navigate to **Jira settings** (cog icon) > **Apps** > **Manage apps**.
1. In Jira, navigate to **Jira settings > Apps > Manage apps**.
1. Scroll to the bottom of the **Manage apps** page and click **Settings**.
1. Select **Enable development mode** and click **Apply**.
1. Install the app
1. Install the app:
1. Navigate to Jira, then choose **Jira settings** (cog icon) > **Apps** > **Manage apps**.
1. In Jira, navigate to **Jira settings > Apps > Manage apps**.
1. Click **Upload app**.
1. In the **From this URL** field, provide a link to the app descriptor. The host and port must point to your GitLab instance.

View File

@ -356,6 +356,9 @@ msgstr ""
msgid "%{address} is an invalid IP address range"
msgstr ""
msgid "%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance."
msgstr ""
msgid "%{author_link} wrote:"
msgstr ""
@ -8410,9 +8413,6 @@ msgstr ""
msgid "DastProfiles|Copy HTTP header to clipboard"
msgstr ""
msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not create the scanner profile. Please try again."
msgstr ""
@ -8437,9 +8437,6 @@ msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not update the scanner profile. Please try again."
msgstr ""
@ -8554,9 +8551,6 @@ msgstr ""
msgid "DastProfiles|Site is not validated yet, please follow the steps."
msgstr ""
msgid "DastProfiles|Site must be validated to run an active scan."
msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
@ -8602,27 +8596,12 @@ msgstr ""
msgid "DastProfiles|Validate"
msgstr ""
msgid "DastProfiles|Validate target site"
msgstr ""
msgid "DastProfiles|Validating..."
msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method."
msgstr ""
msgid "DastProfiles|Validation failed. Please try again."
msgstr ""
msgid "DastProfiles|Validation is in progress..."
msgstr ""
msgid "DastProfiles|Validation must be turned off to change the target URL"
msgstr ""
msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site."
msgstr ""
msgid "Data is still calculating..."
msgstr ""
@ -15098,6 +15077,9 @@ msgstr ""
msgid "IssueAnalytics|Weight"
msgstr ""
msgid "IssueBoards|An error occurred while setting notifications status."
msgstr ""
msgid "IssueBoards|Board"
msgstr ""
@ -19068,6 +19050,9 @@ msgstr ""
msgid "Open raw"
msgstr ""
msgid "Open registration is enabled on your instance."
msgstr ""
msgid "Open sidebar"
msgstr ""
@ -29895,6 +29880,9 @@ msgstr ""
msgid "View replaced file @ "
msgstr ""
msgid "View setting"
msgstr ""
msgid "View supported languages and frameworks"
msgstr ""

View File

@ -5,10 +5,10 @@ module QA
class File < Base
attr_accessor :author_email,
:author_name,
:branch,
:content,
:commit_message,
:name
attr_writer :branch
attribute :project do
Project.fabricate! do |resource|
@ -29,6 +29,10 @@ module QA
@commit_message = 'QA Test - Commit message'
end
def branch
@branch ||= "master"
end
def fabricate!
project.visit!
@ -42,12 +46,6 @@ module QA
end
end
def resource_web_url(resource)
super
rescue ResourceURLMissingError
# this particular resource does not expose a web_url property
end
def api_get_path
"/projects/#{CGI.escape(project.path_with_namespace)}/repository/files/#{CGI.escape(@name)}"
end
@ -58,13 +56,20 @@ module QA
def api_post_body
{
branch: @branch || "master",
branch: branch,
author_email: @author_email || Runtime::User.default_email,
author_name: @author_name || Runtime::User.username,
content: content,
commit_message: commit_message
}
end
private
def transform_api_resource(api_resource)
api_resource[:web_url] = "#{Runtime::Scenario.gitlab_address}/#{project.full_path}/-/tree/#{branch}/#{api_resource[:file_path]}"
api_resource
end
end
end
end

View File

@ -1,68 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Files management' do
it 'user creates, edits and deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/451' do
Flow::Login.sign_in
# Create
file_name = 'QA Test - File name'
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
Resource::File.fabricate_via_browser_ui! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
end
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
expect(file).to have_file(file_name)
expect(file).to have_file_content(file_content)
expect(file).to have_commit_message(commit_message_for_create)
end
end
# Edit
updated_file_content = 'QA Test - Updated file content'
commit_message_for_update = 'QA Test - Update file'
Page::File::Show.perform(&:click_edit)
Page::File::Form.perform do |file|
file.remove_content
file.add_content(updated_file_content)
file.add_commit_message(commit_message_for_update)
file.commit_changes
end
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
expect(file).to have_notice('Your changes have been successfully committed.')
expect(file).to have_file_content(updated_file_content)
expect(file).to have_commit_message(commit_message_for_update)
end
end
# Delete
commit_message_for_delete = 'QA Test - Delete file'
Page::File::Show.perform do |file|
file.click_delete
file.add_commit_message(commit_message_for_delete)
file.click_delete_file
end
Page::Project::Show.perform do |project|
aggregate_failures 'file details' do
expect(project).to have_notice('The file has been successfully deleted.')
expect(project).to have_commit_message(commit_message_for_delete)
expect(project).not_to have_file(file_name)
end
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
context 'File management' do
file_name = 'QA Test - File name'
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
before do
Flow::Login.sign_in
end
it 'user creates a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1093' do
Resource::File.fabricate_via_browser_ui! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
end
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
expect(file).to have_file(file_name)
expect(file).to have_file_content(file_content)
expect(file).to have_commit_message(commit_message_for_create)
end
end
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
context 'File management' do
let(:file) { Resource::File.fabricate_via_api! }
commit_message_for_delete = 'QA Test - Delete file'
before do
Flow::Login.sign_in
file.visit!
end
it 'user deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1095' do
Page::File::Show.perform do |file|
file.click_delete
file.add_commit_message(commit_message_for_delete)
file.click_delete_file
end
Page::Project::Show.perform do |project|
aggregate_failures 'file details' do
expect(project).to have_notice('The file has been successfully deleted.')
expect(project).to have_commit_message(commit_message_for_delete)
expect(project).not_to have_file(file.name)
end
end
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
context 'File management' do
let(:file) { Resource::File.fabricate_via_api! }
updated_file_content = 'QA Test - Updated file content'
commit_message_for_update = 'QA Test - Update file'
before do
Flow::Login.sign_in
file.visit!
end
it 'user edits a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1094' do
Page::File::Show.perform(&:click_edit)
Page::File::Form.perform do |file|
file.remove_content
file.add_content(updated_file_content)
file.add_commit_message(commit_message_for_update)
file.commit_changes
end
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
expect(file).to have_notice('Your changes have been successfully committed.')
expect(file).to have_file_content(updated_file_content)
expect(file).to have_commit_message(commit_message_for_update)
end
end
end
end
end
end

View File

@ -46,7 +46,7 @@ RSpec.describe 'Admin uses repository checks', :request_store, :clean_gitlab_red
)
visit_admin_project_page(project)
page.within('.gl-alert') do
page.within('[data-testid="last-repository-check-failed-alert"]') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Registration enabled callout' do
let_it_be(:admin) { create(:admin) }
let_it_be(:non_admin) { create(:user) }
context 'when "Sign-up enabled" setting is `true`' do
before do
stub_application_setting(signup_enabled: true)
end
context 'when an admin is logged in' do
before do
sign_in(admin)
visit root_dashboard_path
end
it 'displays callout' do
expect(page).to have_content 'Open registration is enabled on your instance.'
expect(page).to have_link 'View setting', href: general_admin_application_settings_path(anchor: 'js-signup-settings')
end
context 'when callout is dismissed', :js do
before do
find('[data-testid="close-registration-enabled-callout"]').click
visit root_dashboard_path
end
it 'does not display callout' do
expect(page).not_to have_content 'Open registration is enabled on your instance.'
end
end
end
context 'when a non-admin is logged in' do
before do
sign_in(non_admin)
visit root_dashboard_path
end
it 'does not display callout' do
expect(page).not_to have_content 'Open registration is enabled on your instance.'
end
end
end
end

View File

@ -14,8 +14,13 @@ describe('Blob Header Default Actions', () => {
let btnGroup;
let buttons;
const blobHash = 'foo-bar';
function createComponent(propsData = {}) {
wrapper = mount(BlobHeaderActions, {
provide: {
blobHash,
},
propsData: {
rawPath: Blob.rawPath,
...propsData,

View File

@ -11,7 +11,11 @@ describe('Blob Header Default Actions', () => {
function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) {
const method = shouldMount ? mount : shallowMount;
const blobHash = 'foo-bar';
wrapper = method.call(this, BlobHeader, {
provide: {
blobHash,
},
propsData: {
blob: { ...Blob, ...blobProps },
...propsData,

View File

@ -0,0 +1,157 @@
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import * as types from '~/boards/stores/mutation_types';
import { createStore } from '~/boards/stores';
import { mockActiveIssue } from '../../mock_data';
import createFlash from '~/flash';
jest.mock('~/flash.js');
const localVue = createLocalVue();
localVue.use(Vuex);
describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
let wrapper;
let store;
const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
const findToggle = () => wrapper.find(GlToggle);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const createComponent = (activeIssue = { ...mockActiveIssue }) => {
store = createStore();
store.state.issues = { [activeIssue.id]: activeIssue };
store.state.activeId = activeIssue.id;
wrapper = mount(BoardSidebarSubscription, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
store = null;
jest.clearAllMocks();
});
describe('Board sidebar subscription component template', () => {
it('displays "notifications" heading', () => {
createComponent();
expect(findNotificationHeader().text()).toBe('Notifications');
});
it('renders toggle as "off" when currently not subscribed', () => {
createComponent();
expect(findToggle().exists()).toBe(true);
expect(findToggle().props('value')).toBe(false);
});
it('renders toggle as "on" when currently subscribed', () => {
createComponent({
...mockActiveIssue,
subscribed: true,
});
expect(findToggle().exists()).toBe(true);
expect(findToggle().props('value')).toBe(true);
});
describe('when notification emails have been disabled', () => {
beforeEach(() => {
createComponent({
...mockActiveIssue,
emailsDisabled: true,
});
});
it('displays a message that notification have been disabled', () => {
expect(findNotificationHeader().text()).toBe(
'Notifications have been disabled by the project or group owner',
);
});
it('does not render the toggle button', () => {
expect(findToggle().exists()).toBe(false);
});
});
});
describe('Board sidebar subscription component `behavior`', () => {
const mockSetActiveIssueSubscribed = subscribedState => {
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
store.commit(types.UPDATE_ISSUE_BY_ID, {
issueId: mockActiveIssue.id,
prop: 'subscribed',
value: subscribedState,
});
});
};
it('subscribing to notification', async () => {
createComponent();
mockSetActiveIssueSubscribed(true);
expect(findGlLoadingIcon().exists()).toBe(false);
findToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
subscribed: true,
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(false);
expect(findToggle().props('value')).toBe(true);
});
it('unsubscribing from notification', async () => {
createComponent({
...mockActiveIssue,
subscribed: true,
});
mockSetActiveIssueSubscribed(false);
expect(findGlLoadingIcon().exists()).toBe(false);
findToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
subscribed: false,
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
expect(findGlLoadingIcon().exists()).toBe(true);
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(false);
expect(findToggle().props('value')).toBe(false);
});
it('flashes an error message when setting the subscribed state fails', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
throw new Error();
});
findToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenNthCalledWith(1, {
message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
});
});
});
});

View File

@ -176,6 +176,14 @@ export const mockIssue = {
},
};
export const mockActiveIssue = {
...mockIssue,
id: 436,
iid: '27',
subscribed: false,
emailsDisabled: false,
};
export const mockIssueWithModel = new ListIssue(mockIssue);
export const mockIssue2 = {

View File

@ -9,6 +9,7 @@ import {
rawIssue,
mockIssues,
labels,
mockActiveIssue,
} from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
@ -833,6 +834,57 @@ describe('setActiveIssueDueDate', () => {
});
});
describe('setActiveIssueSubscribed', () => {
const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } };
const getters = { activeIssue: mockActiveIssue };
const subscribedState = true;
const input = {
subscribedState,
projectPath: 'gitlab-org/gitlab-test',
};
it('should commit subscribed status', done => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueSetSubscription: {
issue: {
subscribed: subscribedState,
},
errors: [],
},
},
});
const payload = {
issueId: getters.activeIssue.id,
prop: 'subscribed',
value: subscribedState,
};
testAction(
actions.setActiveIssueSubscribed,
input,
{ ...state, ...getters },
[
{
type: types.UPDATE_ISSUE_BY_ID,
payload,
},
],
[],
done,
);
});
it('throws error if fails', async () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error);
});
});
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});

View File

@ -124,6 +124,22 @@ describe('Boards - Getters', () => {
});
});
describe('projectPathByIssueId', () => {
it('returns project path for the active issue', () => {
const mockActiveIssue = {
referencePath: 'gitlab-org/gitlab-test#1',
};
expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
'gitlab-org/gitlab-test',
);
});
it('returns empty string as project when active issue is an empty object', () => {
const mockActiveIssue = {};
expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
});
});
describe('getIssuesByList', () => {
const boardsState = {
issuesByListId: mockIssuesByListId,

View File

@ -59,7 +59,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
class="code highlight"
>
<code
id="blob-code-content"
data-blob-hash="foo-bar"
>
<span
id="LC1"

View File

@ -5,9 +5,13 @@ import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/const
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
function createComponent(content = contentMock) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
},
propsData: {
content,
type: 'text',

View File

@ -166,4 +166,32 @@ RSpec.describe ApplicationSettingsHelper do
it { is_expected.to eq(false) }
end
end
describe '.signup_enabled?' do
subject { helper.signup_enabled? }
context 'when signup is enabled' do
before do
stub_application_setting(signup_enabled: true)
end
it { is_expected.to be true }
end
context 'when signup is disabled' do
before do
stub_application_setting(signup_enabled: false)
end
it { is_expected.to be false }
end
context 'when `signup_enabled` is nil' do
before do
stub_application_setting(signup_enabled: nil)
end
it { is_expected.to be false }
end
end
end

View File

@ -161,4 +161,50 @@ RSpec.describe UserCalloutsHelper do
it { is_expected.to be_falsy }
end
end
describe '.show_registration_enabled_user_callout?' do
let_it_be(:admin) { create(:user, :admin) }
subject { helper.show_registration_enabled_user_callout? }
context 'when `current_user` is not an admin' do
before do
allow(helper).to receive(:current_user).and_return(user)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
end
it { is_expected.to be false }
end
context 'when signup is disabled' do
before do
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: false)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
end
it { is_expected.to be false }
end
context 'when user has dismissed callout' do
before do
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { true }
end
it { is_expected.to be false }
end
context 'when `current_user` is an admin, signup is enabled, and user has not dismissed callout' do
before do
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
end
it { is_expected.to be true }
end
end
end

View File

@ -115,6 +115,19 @@ RSpec.describe MergeRequests::CleanupRefsService do
it_behaves_like 'service that does not clean up merge request refs'
end
context 'when repository no longer exists' do
before do
Repositories::DestroyService.new(merge_request.project.repository).execute
end
it 'does not fail and still mark schedule as complete' do
aggregate_failures do
expect(result[:status]).to eq(:success)
expect(merge_request.cleanup_schedule.completed_at).to be_present
end
end
end
end
shared_examples_for 'service that does not clean up merge request refs' do

View File

@ -24,6 +24,7 @@ RSpec.describe MergeRequests::ReopenService do
before do
allow(service).to receive(:execute_hooks)
merge_request.create_cleanup_schedule(scheduled_at: Time.current)
merge_request.update_column(:merge_ref_sha, 'abc123')
perform_enqueued_jobs do
service.execute(merge_request)
@ -48,6 +49,10 @@ RSpec.describe MergeRequests::ReopenService do
expect(merge_request.reload.cleanup_schedule).to be_nil
end
it 'clears the cached merge_ref_sha' do
expect(merge_request.reload.merge_ref_sha).to be_nil
end
context 'note creation' do
it 'creates resource state event about merge_request reopen' do
event = merge_request.resource_state_events.last