Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml3
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/blob/utils.js19
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestone_info.vue12
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js24
-rw-r--r--app/models/email.rb7
-rw-r--r--app/views/shared/snippets/_form.html.haml4
-rw-r--r--changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml5
-rw-r--r--changelogs/unreleased/215919-email-validation.yml5
-rw-r--r--changelogs/unreleased/215975-remove-monaco-snippets-flag.yml5
-rw-r--r--doc/api/vulnerability_exports.md70
-rw-r--r--doc/development/documentation/index.md2
-rw-r--r--doc/user/packages/index.md4
-rw-r--r--doc/user/permissions.md1
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb18
-rw-r--r--spec/features/help_pages_spec.rb32
-rw-r--r--spec/features/profiles/emails_spec.rb9
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb15
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb15
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb17
-rw-r--r--spec/fixtures/sample_doc.md1
-rw-r--r--spec/frontend/blob/utils_spec.js42
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js46
-rw-r--r--spec/frontend/pipelines/stage_spec.js156
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js4
-rw-r--r--spec/frontend/snippet/snippet_bundle_spec.js141
-rw-r--r--spec/javascripts/pipelines/pipelines_artifacts_spec.js38
-rw-r--r--spec/javascripts/pipelines/stage_spec.js136
-rw-r--r--spec/models/email_spec.rb2
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/support/shared_examples/models/email_format_shared_examples.rb41
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/**/*"
diff --git a/Gemfile b/Gemfile
index 3d9326a20cd..f37e85c3007 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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">&bull;</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