diff options
34 files changed, 441 insertions, 448 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 044136fb564..ec018e9b379 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -83,7 +83,6 @@ - "{,ee/}fixtures/**/*" - "{,ee/}rubocop/**/*" - "{,ee/}spec/**/*" - - "doc/README.md" # Some RSpec test rely on this file .code-patterns: &code-patterns - "{package.json,yarn.lock}" @@ -126,7 +125,6 @@ - "{,ee/}fixtures/**/*" - "{,ee/}rubocop/**/*" - "{,ee/}spec/**/*" - - "doc/README.md" # Some RSpec test rely on this file .code-qa-patterns: &code-qa-patterns - "{package.json,yarn.lock}" @@ -168,7 +166,6 @@ - "{,ee/}fixtures/**/*" - "{,ee/}rubocop/**/*" - "{,ee/}spec/**/*" - - "doc/README.md" # Some RSpec test rely on this file # QA changes - ".dockerignore" - "qa/**/*" @@ -495,3 +495,6 @@ gem 'mail', '= 2.7.1' # File encryption gem 'lockbox', '~> 0.3.3' + +# Email validation +gem 'valid_email', '~> 0.1' diff --git a/Gemfile.lock b/Gemfile.lock index edd8b9c73c5..e68b0d9e819 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -842,7 +842,7 @@ GEM rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) rainbow (3.0.0) - raindrops (0.19.0) + raindrops (0.19.1) rake (12.3.3) rb-fsevent (0.10.2) rb-inotify (0.9.10) @@ -1109,6 +1109,9 @@ GEM equalizer (~> 0.0.9) parser (>= 2.6.5) procto (~> 0.0.2) + valid_email (0.1.3) + activemodel + mail (>= 2.6.1) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -1402,6 +1405,7 @@ DEPENDENCIES unicorn (~> 5.5) unicorn-worker-killer (~> 0.4.4) unleash (~> 0.1.5) + valid_email (~> 0.1) validates_hostname (~> 1.0.6) version_sorter (~> 2.2.4) vmstat (~> 2.3.0) diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js index dc2ec642e59..840a3dbe450 100644 --- a/app/assets/javascripts/blob/utils.js +++ b/app/assets/javascripts/blob/utils.js @@ -1,22 +1,15 @@ -/* global ace */ import Editor from '~/editor/editor_lite'; export function initEditorLite({ el, blobPath, blobContent }) { if (!el) { throw new Error(`"el" parameter is required to initialize Editor`); } - let editor; - - if (window?.gon?.features?.monacoSnippets) { - editor = new Editor(); - editor.createInstance({ - el, - blobPath, - blobContent, - }); - } else { - editor = ace.edit(el); - } + const editor = new Editor(); + editor.createInstance({ + el, + blobPath, + blobContent, + }); return editor; } diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 7426936515a..569920a4f31 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -137,7 +137,7 @@ export default { }, isDropdownOpen() { - return this.$el.classList.contains('open'); + return this.$el.classList.contains('show'); }, pipelineActionRequestComplete() { diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue index 01ad0cbf732..d9fbd2884b7 100644 --- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue +++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue @@ -3,7 +3,7 @@ import { GlProgressBar, GlLink, GlBadge, - GlDeprecatedButton, + GlButton, GlTooltipDirective, GlSprintf, } from '@gitlab/ui'; @@ -17,7 +17,7 @@ export default { GlProgressBar, GlLink, GlBadge, - GlDeprecatedButton, + GlButton, GlSprintf, }, directives: { @@ -134,13 +134,9 @@ export default { <span :key="'bullet-' + milestone.id" class="append-right-4">•</span> </template> <template v-if="shouldRenderShowMoreLink(index)"> - <gl-deprecated-button - :key="'more-button-' + milestone.id" - variant="link" - @click="toggleShowAll" - > + <gl-button :key="'more-button-' + milestone.id" variant="link" @click="toggleShowAll"> {{ moreText }} - </gl-deprecated-button> + </gl-button> </template> </template> </div> diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index a3ed8d9c632..76a1f6d1458 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -3,18 +3,6 @@ import setupCollapsibleInputs from './collapsible_input'; let editor; -const initAce = () => { - const editorEl = document.getElementById('editor'); - const form = document.querySelector('.snippet-form-holder form'); - const content = document.querySelector('.snippet-file-content'); - - editor = initEditorLite({ el: editorEl }); - - form.addEventListener('submit', () => { - content.value = editor.getValue(); - }); -}; - const initMonaco = () => { const editorEl = document.getElementById('editor'); const contentEl = document.querySelector('.snippet-file-content'); @@ -36,15 +24,7 @@ const initMonaco = () => { }); }; -export const initEditor = () => { - if (window?.gon?.features?.monacoSnippets) { - initMonaco(); - } else { - initAce(); - } - setupCollapsibleInputs(); -}; - export default () => { - initEditor(); + initMonaco(); + setupCollapsibleInputs(); }; diff --git a/app/models/email.rb b/app/models/email.rb index 580633d3232..8d20f2019d1 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -6,7 +6,8 @@ class Email < ApplicationRecord belongs_to :user, optional: false - validates :email, presence: true, uniqueness: true, devise_email: true + validates :email, presence: true, uniqueness: true + validate :validate_email_format validate :unique_email, if: ->(email) { email.email_changed? } scope :confirmed, -> { where.not(confirmed_at: nil) } @@ -30,6 +31,10 @@ class Email < ApplicationRecord user.accept_pending_invitations! end + def validate_email_format + self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email) + end + # once email is confirmed, update the gpg signatures def update_invalid_gpg_signatures user.update_invalid_gpg_signatures if confirmed? diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index cf2adebde95..c87e73f14dd 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,7 +1,3 @@ -- if Feature.disabled?(:monaco_snippets) - - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/ace.js') - - if Feature.enabled?(:snippets_edit_vue) #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } } - else diff --git a/changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml b/changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml new file mode 100644 index 00000000000..760b7fff331 --- /dev/null +++ b/changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml @@ -0,0 +1,5 @@ +--- +title: Updated deprecated buttons in release page +merge_request: 30941 +author: Özgür Adem Işıklı @iozguradem +type: added diff --git a/changelogs/unreleased/215919-email-validation.yml b/changelogs/unreleased/215919-email-validation.yml new file mode 100644 index 00000000000..19783ff8720 --- /dev/null +++ b/changelogs/unreleased/215919-email-validation.yml @@ -0,0 +1,5 @@ +--- +title: Change validation rules for profile email addresses +merge_request: 30633 +author: +type: fixed diff --git a/changelogs/unreleased/215975-remove-monaco-snippets-flag.yml b/changelogs/unreleased/215975-remove-monaco-snippets-flag.yml new file mode 100644 index 00000000000..a122f79362d --- /dev/null +++ b/changelogs/unreleased/215975-remove-monaco-snippets-flag.yml @@ -0,0 +1,5 @@ +--- +title: Enable Monaco for editing Snippets by default +merge_request: 30892 +author: +type: added diff --git a/doc/api/vulnerability_exports.md b/doc/api/vulnerability_exports.md index f2666783087..42dafc1612a 100644 --- a/doc/api/vulnerability_exports.md +++ b/doc/api/vulnerability_exports.md @@ -1,6 +1,6 @@ -# Project Vulnerabilities API **(ULTIMATE)** +# Vulnerability export API **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. [Updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30397) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0. CAUTION: **Caution:** This API is currently in development and is protected by a **disabled** @@ -17,21 +17,21 @@ across GitLab releases. Every API call to vulnerability exports must be [authenticated](README.md#authentication). +## Create a project-level vulnerability export + +Creates a new vulnerability export for a project. + Vulnerability export permissions inherit permissions from their project. If a project is private and a user isn't a member of the project to which the vulnerability belongs, requests to that project return a `404 Not Found` status code. Vulnerability exports can be only accessed by the export's author. -## Create vulnerability export - -Creates a new vulnerability export. - If an authenticated user doesn't have permission to [create a new vulnerability](../user/permissions.md#project-members-permissions), this request results in a `403` status code. ```plaintext -POST /projects/:id/vulnerability_exports +POST /security/projects/:id/vulnerability_exports ``` | Attribute | Type | Required | Description | @@ -39,7 +39,7 @@ POST /projects/:id/vulnerability_exports | `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project which the authenticated user is a member of | ```shell -curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports +curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/projects/1/vulnerability_exports ``` The created vulnerability export will be automatically deleted after 1 hour. @@ -56,8 +56,40 @@ Example response: "started_at": null, "finished_at": null, "_links": { - "self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2", - "download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download" + "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2", + "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download" + } +} +``` + +## Create an instance-level vulnerability export + +Creates a new vulnerability export for the projects of the user selected in the Security Dashboard. + +```plaintext +POST /security/vulnerability_exports +``` + +```shell +curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports +``` + +The created vulnerability export is automatically deleted after one hour. + +Example response: + +```json +{ + "id": 2, + "created_at": "2020-03-30T09:35:38.746Z", + "project_id": null, + "format": "csv", + "status": "created", + "started_at": null, + "finished_at": null, + "_links": { + "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2", + "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download" } } ``` @@ -67,16 +99,15 @@ Example response: Gets a single vulnerability export. ```plaintext -POST /projects/:id/vulnerability_exports/:vulnerability_export_id +GET /security/vulnerability_exports/:id ``` | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer or string | yes | The vulnerability's ID | -| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID | +| `id` | integer or string | yes | The vulnerability export's ID | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2 +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2 ``` If the vulnerability export isn't finished, the response is `202 Accepted`. @@ -93,8 +124,8 @@ Example response: "started_at": "2020-03-30T09:36:54.469Z", "finished_at": "2020-03-30T09:36:55.008Z", "_links": { - "self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2", - "download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download" + "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2", + "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download" } } ``` @@ -104,16 +135,15 @@ Example response: Downloads a single vulnerability export. ```plaintext -POST /projects/:id/vulnerability_exports/:vulnerability_export_id/download +GET /security/vulnerability_exports/:id/download ``` | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer or string | yes | The vulnerability's ID | -| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID | +| `id` | integer or string | yes | The vulnerability export's ID | ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download ``` The response will be `404 Not Found` if the vulnerability export is not finished yet or was not found. diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 2724958314c..ac96cfd77d8 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -405,8 +405,6 @@ merge request with new or changed docs is submitted, are: - [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L69) checks that all internal anchors (ex: `[link](../index.md#internal_anchor)`) are valid. -- If any code or the `doc/README.md` file is changed, a full pipeline will run, which - runs tests for [`/help`](#gitlab-help-tests). ### Running tests diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index cb3cb26ebb1..d7072a7a2a0 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -20,8 +20,8 @@ The Packages feature allows GitLab to act as a repository for the following: If you cannot find the **{package}** **Packages > List** entry under your project's sidebar, it is not enabled in your GitLab instance. Ask your -administrator to enable GitLab Package Registry following the administration -documentation. +administrator to enable GitLab Package Registry following the [administration +documentation](../../administration/packages/index.md). Once enabled for your GitLab instance, to enable Package Registry for your project: diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 854744f3090..07fba61ca34 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -150,6 +150,7 @@ The following table depicts the various user permission levels in a project. | Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | +| Remove fork relationship | | | | | ✓ | | Remove project | | | | | ✓ | | Delete issues | | | | | ✓ | | Disable notification emails | | | | | ✓ | diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index e4e69241bd9..1f8ba51913e 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -43,7 +43,6 @@ module Gitlab # Initialize gon.features with any flags that should be # made globally available to the frontend push_frontend_feature_flag(:snippets_vue, default_enabled: false) - push_frontend_feature_flag(:monaco_snippets, default_enabled: false) push_frontend_feature_flag(:monaco_blobs, default_enabled: false) push_frontend_feature_flag(:monaco_ci, default_enabled: false) push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false) diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb index 7c6b1863202..91850e429a5 100644 --- a/spec/controllers/profiles/emails_controller_spec.rb +++ b/spec/controllers/profiles/emails_controller_spec.rb @@ -10,12 +10,20 @@ describe Profiles::EmailsController do end describe '#create' do - let(:email_params) { { email: "add_email@example.com" } } + context 'when email address is valid' do + let(:email_params) { { email: "add_email@example.com" } } - it 'sends an email confirmation' do - expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size } - expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]] - expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions" + it 'sends an email confirmation' do + expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size } + end + end + + context 'when email address is invalid' do + let(:email_params) { { email: "test.@example.com" } } + + it 'does not send an email confirmation' do + expect { post(:create, params: { email: email_params }) }.not_to change { ActionMailer::Base.deliveries.size } + end end end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 88a7aa51326..1ba3849fe2c 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -4,35 +4,9 @@ require 'spec_helper' describe 'Help Pages' do describe 'Get the main help page' do - shared_examples_for 'help page' do |prefix: ''| - it 'prefixes links correctly' do - expect(page).to have_selector(%(div.documentation-index > table tbody tr td a[href="#{prefix}/help/api/README.md"])) - end - end - - context 'without a trailing slash' do - before do - visit help_path - end - - it_behaves_like 'help page' - end - - context 'with a trailing slash' do - before do - visit help_path + '/' - end - - it_behaves_like 'help page' - end - - context 'with a relative installation' do - before do - stub_config_setting(relative_url_root: '/gitlab') - visit help_path - end - - it_behaves_like 'help page', prefix: '/gitlab' + before do + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with(Rails.root.join('doc', 'README.md')).and_return(fixture_file('sample_doc.md')) end context 'quick link shortcuts', :js do diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb index 4d2cd0f8b56..5dfc03d711a 100644 --- a/spec/features/profiles/emails_spec.rb +++ b/spec/features/profiles/emails_spec.rb @@ -31,6 +31,15 @@ describe 'Profile > Emails' do expect(email).to be_nil expect(page).to have_content('Email has already been taken') end + + it 'does not add an invalid email' do + fill_in('Email', with: 'test.@example.com') + click_button('Add email address') + + email = user.emails.find_by(email: email) + expect(email).to be_nil + expect(page).to have_content('Email is invalid') + end end it 'User removes email' do diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb index d883a1fc39c..794b1fdb97c 100644 --- a/spec/features/projects/snippets/create_snippet_spec.rb +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' shared_examples_for 'snippet editor' do before do stub_feature_flags(snippets_edit_vue: false) - stub_feature_flags(monaco_snippets: flag) end def description_field @@ -20,7 +19,7 @@ shared_examples_for 'snippet editor' do fill_in 'project_snippet_description', with: 'My Snippet **Description**' page.within('.file-editor') do - el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false) + el = find('.inputarea') el.send_keys 'Hello World!' end end @@ -145,15 +144,5 @@ describe 'Projects > Snippets > Create Snippet', :js do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } - context 'when using Monaco' do - it_behaves_like "snippet editor" do - let(:flag) { true } - end - end - - context 'when using ACE' do - it_behaves_like "snippet editor" do - let(:flag) { false } - end - end + it_behaves_like "snippet editor" end diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb index bdcc1cc56e7..d7b181dc678 100644 --- a/spec/features/snippets/spam_snippets_spec.rb +++ b/spec/features/snippets/spam_snippets_spec.rb @@ -13,7 +13,6 @@ shared_examples_for 'snippet editor' do stub_feature_flags(allow_possible_spam: false) stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_edit_vue: false) - stub_feature_flags(monaco_snippets: flag) stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') Gitlab::CurrentSettings.update!( @@ -35,7 +34,7 @@ shared_examples_for 'snippet editor' do find('#personal_snippet_visibility_level_20').set(true) page.within('.file-editor') do - el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false) + el = find('.inputarea') el.send_keys 'Hello World!' end end @@ -126,15 +125,5 @@ end describe 'User creates snippet', :js do let_it_be(:user) { create(:user) } - context 'when using Monaco' do - it_behaves_like "snippet editor" do - let(:flag) { true } - end - end - - context 'when using ACE' do - it_behaves_like "snippet editor" do - let(:flag) { false } - end - end + it_behaves_like "snippet editor" end diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index 5d3a84dd7bc..c4279bdb212 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -6,7 +6,6 @@ shared_examples_for 'snippet editor' do before do stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_edit_vue: false) - stub_feature_flags(monaco_snippets: flag) sign_in(user) visit new_snippet_path end @@ -23,7 +22,7 @@ shared_examples_for 'snippet editor' do fill_in 'personal_snippet_description', with: 'My Snippet **Description**' page.within('.file-editor') do - el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false) + el = find('.inputarea') el.send_keys 'Hello World!' end end @@ -136,7 +135,7 @@ shared_examples_for 'snippet editor' do fill_in 'personal_snippet_title', with: 'My Snippet Title' page.within('.file-editor') do find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' - el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false) + el = find('.inputarea') el.send_keys 'Hello World!' end @@ -154,15 +153,5 @@ describe 'User creates snippet', :js do let_it_be(:user) { create(:user) } - context 'when using Monaco' do - it_behaves_like "snippet editor" do - let(:flag) { true } - end - end - - context 'when using ACE' do - it_behaves_like "snippet editor" do - let(:flag) { false } - end - end + it_behaves_like "snippet editor" end diff --git a/spec/fixtures/sample_doc.md b/spec/fixtures/sample_doc.md new file mode 100644 index 00000000000..84080dd1089 --- /dev/null +++ b/spec/fixtures/sample_doc.md @@ -0,0 +1 @@ +[GitLab API](api/README.md) diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js index 39a73aae444..119ed2dfe7a 100644 --- a/spec/frontend/blob/utils_spec.js +++ b/spec/frontend/blob/utils_spec.js @@ -8,11 +8,6 @@ jest.mock('~/editor/editor_lite', () => { }); }); -const mockCreateAceInstance = jest.fn(); -global.ace = { - edit: mockCreateAceInstance, -}; - describe('Blob utilities', () => { beforeEach(() => { Editor.mockClear(); @@ -29,21 +24,6 @@ describe('Blob utilities', () => { }); describe('Monaco editor', () => { - let origProp; - - beforeEach(() => { - origProp = window.gon; - window.gon = { - features: { - monacoSnippets: true, - }, - }; - }); - - afterEach(() => { - window.gon = origProp; - }); - it('initializes the Editor Lite', () => { utils.initEditorLite({ el: editorEl }); expect(Editor).toHaveBeenCalled(); @@ -69,27 +49,5 @@ describe('Blob utilities', () => { ]); }); }); - describe('ACE editor', () => { - let origProp; - - beforeEach(() => { - origProp = window.gon; - window.gon = { - features: { - monacoSnippets: false, - }, - }; - }); - - afterEach(() => { - window.gon = origProp; - }); - - it('does not initialize the Editor Lite', () => { - utils.initEditorLite({ el: editorEl }); - expect(Editor).not.toHaveBeenCalled(); - expect(mockCreateAceInstance).toHaveBeenCalledWith(editorEl); - }); - }); }); }); diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js new file mode 100644 index 00000000000..a93cc8a62ab --- /dev/null +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from '@vue/test-utils'; +import PipelineArtifacts from '~/pipelines/components/pipelines_artifacts.vue'; +import { GlLink } from '@gitlab/ui'; + +describe('Pipelines Artifacts dropdown', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(PipelineArtifacts, { + propsData: { + artifacts: [ + { + name: 'artifact', + path: '/download/path', + }, + { + name: 'artifact two', + path: '/download/path-two', + }, + ], + }, + }); + }; + + const findGlLink = () => wrapper.find(GlLink); + const findAllGlLinks = () => wrapper.find('.dropdown-menu').findAll(GlLink); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render a dropdown with all the provided artifacts', () => { + expect(findAllGlLinks()).toHaveLength(2); + }); + + it('should render a link with the provided path', () => { + expect(findGlLink().attributes('href')).toEqual('/download/path'); + + expect(findGlLink().text()).toContain('artifact'); + }); +}); diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js new file mode 100644 index 00000000000..b020aaedd06 --- /dev/null +++ b/spec/frontend/pipelines/stage_spec.js @@ -0,0 +1,156 @@ +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import StageComponent from '~/pipelines/components/stage.vue'; +import eventHub from '~/pipelines/event_hub'; +import { stageReply } from './mock_data'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Pipelines stage component', () => { + let wrapper; + let mock; + + const defaultProps = { + stage: { + status: { + group: 'success', + icon: 'status_success', + title: 'success', + }, + dropdown_path: 'path.json', + }, + updateDropdown: false, + }; + + const createComponent = (props = {}) => { + wrapper = mount(StageComponent, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + mock.restore(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a dropdown with the status icon', () => { + expect(wrapper.attributes('class')).toEqual('dropdown'); + expect(wrapper.find('svg').exists()).toBe(true); + expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown'); + }); + }); + + describe('with successful request', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + createComponent(); + }); + + it('should render the received data and emit `clickedDropdown` event', () => { + jest.spyOn(eventHub, '$emit'); + wrapper.find('button').trigger('click'); + + return waitForPromises().then(() => { + expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( + stageReply.latest_statuses[0].name, + ); + + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + }); + }); + }); + + describe('when request fails', () => { + beforeEach(() => { + mock.onGet('path.json').reply(500); + createComponent(); + }); + + it('should close the dropdown', () => { + wrapper.setMethods({ + closeDropdown: jest.fn(), + isDropdownOpen: jest.fn().mockReturnValue(false), + }); + + wrapper.find('button').trigger('click'); + + return waitForPromises().then(() => { + expect(wrapper.vm.closeDropdown).toHaveBeenCalled(); + }); + }); + }); + + describe('update endpoint correctly', () => { + beforeEach(() => { + const copyStage = Object.assign({}, stageReply); + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + }); + + it('should update the stage to request the new endpoint provided', () => { + return wrapper.vm + .$nextTick() + .then(() => { + wrapper.find('button').trigger('click'); + return waitForPromises(); + }) + .then(() => { + expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( + 'this is the updated content', + ); + }); + }); + }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + + createComponent({ type: 'PIPELINES_TABLE' }); + }); + + describe('within pipeline table', () => { + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', () => { + jest.spyOn(eventHub, '$emit'); + + wrapper.find('button').trigger('click'); + + return waitForPromises() + .then(() => { + wrapper.find('.js-ci-action').trigger('click'); + + return waitForPromises(); + }) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js index 0b65b6cab96..0e79c45b337 100644 --- a/spec/frontend/releases/components/release_block_milestone_info_spec.js +++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { GlProgressBar, GlLink, GlBadge, GlDeprecatedButton } from '@gitlab/ui'; +import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui'; import { trimText } from 'helpers/text_helper'; import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue'; import { milestones as originalMilestones } from '../mock_data'; @@ -106,7 +106,7 @@ describe('Release block milestone info', () => { const clickShowMoreFewerButton = () => { milestoneListContainer() - .find(GlDeprecatedButton) + .find(GlButton) .trigger('click'); return wrapper.vm.$nextTick(); diff --git a/spec/frontend/snippet/snippet_bundle_spec.js b/spec/frontend/snippet/snippet_bundle_spec.js index 12d20d5cd85..38d05243c65 100644 --- a/spec/frontend/snippet/snippet_bundle_spec.js +++ b/spec/frontend/snippet/snippet_bundle_spec.js @@ -1,94 +1,85 @@ import Editor from '~/editor/editor_lite'; -import { initEditor } from '~/snippet/snippet_bundle'; +import initEditor from '~/snippet/snippet_bundle'; import { setHTMLFixture } from 'helpers/fixtures'; jest.mock('~/editor/editor_lite', () => jest.fn()); describe('Snippet editor', () => { - describe('Monaco editor for Snippets', () => { - let oldGon; - let editorEl; - let contentEl; - let fileNameEl; - let form; - - const mockName = 'foo.bar'; - const mockContent = 'Foo Bar'; - const updatedMockContent = 'New Foo Bar'; - - const mockEditor = { - createInstance: jest.fn(), - updateModelLanguage: jest.fn(), - getValue: jest.fn().mockReturnValueOnce(updatedMockContent), - }; - Editor.mockImplementation(() => mockEditor); - - function setUpFixture(name, content) { - setHTMLFixture(` - <div class="snippet-form-holder"> - <form> - <input class="js-snippet-file-name" type="text" value="${name}"> - <input class="snippet-file-content" type="hidden" value="${content}"> - <pre id="editor"></pre> - </form> - </div> - `); - } - - function bootstrap(name = '', content = '') { - setUpFixture(name, content); - editorEl = document.getElementById('editor'); - contentEl = document.querySelector('.snippet-file-content'); - fileNameEl = document.querySelector('.js-snippet-file-name'); - form = document.querySelector('.snippet-form-holder form'); - - initEditor(); - } - - function createEvent(name) { - return new Event(name, { - view: window, - bubbles: true, - cancelable: true, - }); - } - - beforeEach(() => { - oldGon = window.gon; - window.gon = { features: { monacoSnippets: true } }; - bootstrap(mockName, mockContent); + let editorEl; + let contentEl; + let fileNameEl; + let form; + + const mockName = 'foo.bar'; + const mockContent = 'Foo Bar'; + const updatedMockContent = 'New Foo Bar'; + + const mockEditor = { + createInstance: jest.fn(), + updateModelLanguage: jest.fn(), + getValue: jest.fn().mockReturnValueOnce(updatedMockContent), + }; + Editor.mockImplementation(() => mockEditor); + + function setUpFixture(name, content) { + setHTMLFixture(` + <div class="snippet-form-holder"> + <form> + <input class="js-snippet-file-name" type="text" value="${name}"> + <input class="snippet-file-content" type="hidden" value="${content}"> + <pre id="editor"></pre> + </form> + </div> + `); + } + + function bootstrap(name = '', content = '') { + setUpFixture(name, content); + editorEl = document.getElementById('editor'); + contentEl = document.querySelector('.snippet-file-content'); + fileNameEl = document.querySelector('.js-snippet-file-name'); + form = document.querySelector('.snippet-form-holder form'); + + initEditor(); + } + + function createEvent(name) { + return new Event(name, { + view: window, + bubbles: true, + cancelable: true, }); + } - afterEach(() => { - window.gon = oldGon; - }); + beforeEach(() => { + bootstrap(mockName, mockContent); + }); - it('correctly initializes Editor', () => { - expect(mockEditor.createInstance).toHaveBeenCalledWith({ - el: editorEl, - blobPath: mockName, - blobContent: mockContent, - }); + it('correctly initializes Editor', () => { + expect(mockEditor.createInstance).toHaveBeenCalledWith({ + el: editorEl, + blobPath: mockName, + blobContent: mockContent, }); + }); - it('listens to file name changes and updates syntax highlighting of code', () => { - expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled(); + it('listens to file name changes and updates syntax highlighting of code', () => { + expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled(); - const event = createEvent('change'); + const event = createEvent('change'); - fileNameEl.value = updatedMockContent; - fileNameEl.dispatchEvent(event); + fileNameEl.value = updatedMockContent; + fileNameEl.dispatchEvent(event); - expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent); - }); + expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent); + }); - it('listens to form submit event and populates the hidden field with most recent version of the content', () => { - expect(contentEl.value).toBe(mockContent); + it('listens to form submit event and populates the hidden field with most recent version of the content', () => { + expect(contentEl.value).toBe(mockContent); - const event = createEvent('submit'); + const event = createEvent('submit'); - form.dispatchEvent(event); - expect(contentEl.value).toBe(updatedMockContent); - }); + form.dispatchEvent(event); + expect(contentEl.value).toBe(updatedMockContent); }); }); diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js deleted file mode 100644 index 7705d5a19bf..00000000000 --- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import Vue from 'vue'; -import artifactsComp from '~/pipelines/components/pipelines_artifacts.vue'; - -describe('Pipelines Artifacts dropdown', () => { - let component; - let artifacts; - - beforeEach(() => { - const ArtifactsComponent = Vue.extend(artifactsComp); - - artifacts = [ - { - name: 'artifact', - path: '/download/path', - }, - ]; - - component = new ArtifactsComponent({ - propsData: { - artifacts, - }, - }).$mount(); - }); - - it('should render a dropdown with the provided artifacts', () => { - expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(artifacts.length); - }); - - it('should render a link with the provided path', () => { - expect(component.$el.querySelector('.dropdown-menu li a').getAttribute('href')).toEqual( - artifacts[0].path, - ); - - expect(component.$el.querySelector('.dropdown-menu li a').textContent).toContain( - artifacts[0].name, - ); - }); -}); diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js deleted file mode 100644 index b99688ec371..00000000000 --- a/spec/javascripts/pipelines/stage_spec.js +++ /dev/null @@ -1,136 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import axios from '~/lib/utils/axios_utils'; -import stage from '~/pipelines/components/stage.vue'; -import eventHub from '~/pipelines/event_hub'; -import { stageReply } from './mock_data'; - -describe('Pipelines stage component', () => { - let StageComponent; - let component; - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - - StageComponent = Vue.extend(stage); - - component = mountComponent(StageComponent, { - stage: { - status: { - group: 'success', - icon: 'status_success', - title: 'success', - }, - dropdown_path: 'path.json', - }, - updateDropdown: false, - }); - }); - - afterEach(() => { - component.$destroy(); - mock.restore(); - }); - - it('should render a dropdown with the status icon', () => { - expect(component.$el.getAttribute('class')).toEqual('dropdown'); - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown'); - }); - - describe('with successful request', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - }); - - it('should render the received data and emit `clickedDropdown` event', done => { - spyOn(eventHub, '$emit'); - component.$el.querySelector('button').click(); - - setTimeout(() => { - expect( - component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(), - ).toContain(stageReply.latest_statuses[0].name); - - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); - done(); - }, 0); - }); - }); - - describe('when request fails', () => { - beforeEach(() => { - mock.onGet('path.json').reply(500); - }); - - it('should close the dropdown', () => { - component.$el.click(); - - setTimeout(() => { - expect(component.$el.classList.contains('open')).toEqual(false); - }, 0); - }); - }); - - describe('update endpoint correctly', () => { - beforeEach(() => { - const copyStage = Object.assign({}, stageReply); - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(200, copyStage); - }); - - it('should update the stage to request the new endpoint provided', done => { - component.stage = { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }; - - Vue.nextTick(() => { - component.$el.querySelector('button').click(); - - setTimeout(() => { - expect( - component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(), - ).toContain('this is the updated content'); - done(); - }); - }); - }); - }); - - describe('pipelineActionRequestComplete', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); - }); - - describe('within pipeline table', () => { - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => { - spyOn(eventHub, '$emit'); - - component.type = 'PIPELINES_TABLE'; - component.$el.querySelector('button').click(); - - setTimeout(() => { - component.$el.querySelector('.js-ci-action').click(); - setTimeout(() => { - component - .$nextTick() - .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }) - .then(done) - .catch(done.fail); - }, 0); - }, 0); - }); - }); - }); -}); diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index aa3a60b867a..dabf2bb80b5 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe Email do describe 'validations' do - it_behaves_like 'an object with email-formated attributes', :email do + it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email do subject { build(:email) } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7649b09aa8e..83f274f1894 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -296,7 +296,7 @@ describe User, :do_not_mock_admin_mode do subject { build(:user) } end - it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do + it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :public_email, :notification_email do subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } } end @@ -916,7 +916,6 @@ describe User, :do_not_mock_admin_mode do user.tap { |u| u.update!(email: new_email) }.reload end.to change(user, :unconfirmed_email).to(new_email) end - it 'does not change :notification_email' do expect do user.tap { |u| u.update!(email: new_email) }.reload diff --git a/spec/support/shared_examples/models/email_format_shared_examples.rb b/spec/support/shared_examples/models/email_format_shared_examples.rb index 6797836e383..a8115e440a4 100644 --- a/spec/support/shared_examples/models/email_format_shared_examples.rb +++ b/spec/support/shared_examples/models/email_format_shared_examples.rb @@ -44,3 +44,44 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes end end end + +RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes| + attributes.each do |attribute| + describe "specifically its :#{attribute} attribute" do + %w[ + info@example.com + info+test@example.com + o'reilly@example.com + ].each do |valid_email| + context "with a value of '#{valid_email}'" do + let(:email_value) { valid_email } + + it 'is valid' do + subject.send("#{attribute}=", valid_email) + + expect(subject).to be_valid + end + end + end + + %w[ + foobar + test@test@example.com + test.test.@example.com + .test.test@example.com + mailto:test@example.com + lol!'+=?><#$%^&*()@gmail.com + ].each do |invalid_email| + context "with a value of '#{invalid_email}'" do + let(:email_value) { invalid_email } + + it 'is invalid' do + subject.send("#{attribute}=", invalid_email) + + expect(subject).to be_invalid + end + end + end + end + end +end |