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
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-24 15:09:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-24 15:09:32 +0300
commitf296f23500b4b3758670ae0c5ce2e1779f533e8b (patch)
tree717151cb9e81d489b4ecf880988ea10d77b7224f /spec
parentfd7c75bf603f4f2f1a4a4e63ef5cbc1a51cc0a15 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/sessions_controller_spec.rb20
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb12
-rw-r--r--spec/features/profiles/user_manages_emails_spec.rb6
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb44
-rw-r--r--spec/finders/metrics/users_starred_dashboards_finder_spec.rb56
-rw-r--r--spec/frontend/groups/service/archived_projects_service_spec.js37
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js48
-rw-r--r--spec/frontend/sessions/new/components/email_verification_spec.js205
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js27
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js12
-rw-r--r--spec/frontend/work_items/mock_data.js3
-rw-r--r--spec/helpers/sessions_helper_spec.rb27
-rw-r--r--spec/models/user_spec.rb25
-rw-r--r--spec/models/users/calloutable_spec.rb11
-rw-r--r--spec/requests/verifies_with_email_spec.rb49
-rw-r--r--spec/services/personal_access_tokens/revoke_token_family_service_spec.rb7
-rw-r--r--spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb18
18 files changed, 394 insertions, 219 deletions
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index a09b3318c25..ce9703753cf 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -538,6 +538,26 @@ RSpec.describe SessionsController, feature_category: :system_access do
expect(AuthenticationEvent.last.provider).to eq("two-factor-via-webauthn-device")
end
end
+
+ context 'when the user is locked and submits a valid verification token' do
+ let(:user) { create(:user) }
+ let(:user_params) { { verification_token: 'token' } }
+ let(:session_params) { { verification_user_id: user.id } }
+ let(:post_action) { post(:create, params: { user: user_params }, session: session_params) }
+
+ before do
+ encrypted_token = Devise.token_generator.digest(User, user.email, 'token')
+ user.update!(locked_at: Time.current, unlock_token: encrypted_token)
+ end
+
+ it_behaves_like 'known sign in'
+
+ it 'successfully logs in a user' do
+ post_action
+
+ expect(subject.current_user).to eq user
+ end
+ end
end
context 'when login fails' do
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index f39d9ddaf56..3adda251e40 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Profile > GPG Keys', feature_category: :user_profile do
end
it 'saves the new key' do
+ click_button('Add a GPG key')
fill_in('Key', with: GpgHelpers::User2.public_key)
click_button('Add key')
@@ -24,6 +25,7 @@ RSpec.describe 'Profile > GPG Keys', feature_category: :user_profile do
end
it 'with multiple subkeys' do
+ click_button('Add a GPG key')
fill_in('Key', with: GpgHelpers::User3.public_key)
click_button('Add key')
@@ -52,7 +54,10 @@ RSpec.describe 'Profile > GPG Keys', feature_category: :user_profile do
click_link('Remove')
- expect(page).to have_content('Your GPG keys (0)')
+ expect(page).to have_content('Your GPG keys')
+ page.within('.gl-new-card-count') do
+ expect(page).to have_content('0')
+ end
end
it 'user revokes a key via the key index' do
@@ -63,7 +68,10 @@ RSpec.describe 'Profile > GPG Keys', feature_category: :user_profile do
click_link('Revoke')
- expect(page).to have_content('Your GPG keys (0)')
+ expect(page).to have_content('Your GPG keys')
+ page.within('.gl-new-card-count') do
+ expect(page).to have_content('0')
+ end
expect(gpg_signature.reload).to have_attributes(
verification_status: 'unknown_key',
diff --git a/spec/features/profiles/user_manages_emails_spec.rb b/spec/features/profiles/user_manages_emails_spec.rb
index b875dfec217..35f2ccf0f34 100644
--- a/spec/features/profiles/user_manages_emails_spec.rb
+++ b/spec/features/profiles/user_manages_emails_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'User manages emails', feature_category: :user_profile do
it 'adds an email', :aggregate_failures do
fill_in('email_email', with: 'my@email.com')
- click_button('Add')
+ click_button('Add email address')
email = user.emails.find_by(email: 'my@email.com')
@@ -37,7 +37,7 @@ RSpec.describe 'User manages emails', feature_category: :user_profile do
it 'does not add an email that is the primary email of another user', :aggregate_failures do
fill_in('email_email', with: other_user.email)
- click_button('Add')
+ click_button('Add email address')
email = user.emails.find_by(email: other_user.email)
@@ -51,7 +51,7 @@ RSpec.describe 'User manages emails', feature_category: :user_profile do
it 'removes an email', :aggregate_failures do
fill_in('email_email', with: 'my@email.com')
- click_button('Add')
+ click_button('Add email address')
email = user.emails.find_by(email: 'my@email.com')
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index 1854e812b73..c9b1670be82 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting, feature_category: :system_access do
+RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting, :js, feature_category: :system_access do
include EmailHelpers
let_it_be(:user) { create(:user) }
@@ -33,7 +33,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
# Expect to see the verification form on the login page
expect(page).to have_current_path(new_user_session_path)
- expect(page).to have_content('Help us protect your account')
+ expect(page).to have_content(s_('IdentityVerification|Help us protect your account'))
# Expect an instructions email to be sent with a code
code = expect_instructions_email_and_extract_code
@@ -41,7 +41,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
# Signing in again prompts for the code and doesn't send a new one
gitlab_sign_in(user)
expect(page).to have_current_path(new_user_session_path)
- expect(page).to have_content('Help us protect your account')
+ expect(page).to have_content(s_('IdentityVerification|Help us protect your account'))
# Verify the code
verify_code(code)
@@ -54,7 +54,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
# Expect a confirmation page with a meta refresh tag for 3 seconds to the root
expect(page).to have_current_path(users_successful_verification_path)
- expect(page).to have_content('Verification successful')
+ expect(page).to have_content(s_('IdentityVerification|Verification successful'))
expect(page).to have_selector("meta[http-equiv='refresh'][content='3; url=#{root_path}']", visible: false)
end
end
@@ -69,7 +69,8 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
code = expect_instructions_email_and_extract_code
# Request a new code
- click_link 'Resend code'
+ click_button s_('IdentityVerification|Resend code')
+ expect(page).to have_content(s_('IdentityVerification|A new code has been sent.'))
expect_log_message('Instructions Sent', 2)
new_code = expect_instructions_email_and_extract_code
@@ -83,22 +84,16 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
gitlab_sign_in(user)
# It shows a resend button
- expect(page).to have_link 'Resend code'
+ expect(page).to have_button s_('IdentityVerification|Resend code')
# Resend more than the rate limited amount of times
10.times do
- click_link 'Resend code'
+ click_button s_('IdentityVerification|Resend code')
end
- # Expect the link to be gone
- expect(page).not_to have_link 'Resend code'
-
- # Wait for 1 hour
- travel 1.hour
-
- # Now it's visible again
- gitlab_sign_in(user)
- expect(page).to have_link 'Resend code'
+ # Expect an error alert
+ expect(page).to have_content format(s_("IdentityVerification|You've reached the maximum amount of resends. "\
+ 'Wait %{interval} and try again.'), interval: 'about 1 hour')
end
end
@@ -118,8 +113,9 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
# Expect an error message
expect_log_message('Failed Attempt', reason: 'rate_limited')
- expect(page).to have_content("You've reached the maximum amount of tries. "\
- 'Wait 10 minutes or send a new code and try again.')
+ expect(page).to have_content(
+ format(s_("IdentityVerification|You've reached the maximum amount of tries. "\
+ 'Wait %{interval} or send a new code and try again.'), interval: '10 minutes'))
# Wait for 10 minutes
travel 10.minutes
@@ -139,7 +135,8 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
# Expect an error message
expect_log_message('Failed Attempt', reason: 'invalid')
- expect(page).to have_content('The code is incorrect. Enter it again, or send a new code.')
+ expect(page).to have_content(s_('IdentityVerification|The code is incorrect. '\
+ 'Enter it again, or send a new code.'))
end
it 'verifies expired codes' do
@@ -156,7 +153,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
# Expect an error message
expect_log_message('Failed Attempt', reason: 'expired')
- expect(page).to have_content('The code has expired. Send a new code and try again.')
+ expect(page).to have_content(s_('IdentityVerification|The code has expired. Send a new code and try again.'))
end
end
end
@@ -250,7 +247,8 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
it 'shows an error message on on the login page' do
expect(page).to have_current_path(new_user_session_path)
- expect(page).to have_content('Maximum login attempts exceeded. Wait 10 minutes and try again.')
+ expect(page).to have_content(format(s_('IdentityVerification|Maximum login attempts exceeded. '\
+ 'Wait %{interval} and try again.'), interval: '10 minutes'))
end
end
@@ -271,7 +269,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
stub_feature_flags(require_email_verification: false)
# Resending and veryfying the code work as expected
- click_link 'Resend code'
+ click_button s_('IdentityVerification|Resend code')
new_code = expect_instructions_email_and_extract_code
verify_code(code)
@@ -283,7 +281,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
verify_code(new_code)
expect(page).to have_content(s_('IdentityVerification|The code has expired. Send a new code and try again.'))
- click_link 'Resend code'
+ click_button s_('IdentityVerification|Resend code')
another_code = expect_instructions_email_and_extract_code
verify_code(another_code)
diff --git a/spec/finders/metrics/users_starred_dashboards_finder_spec.rb b/spec/finders/metrics/users_starred_dashboards_finder_spec.rb
deleted file mode 100644
index 4136cf1123a..00000000000
--- a/spec/finders/metrics/users_starred_dashboards_finder_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Metrics::UsersStarredDashboardsFinder do
- describe '#execute' do
- subject(:starred_dashboards) { described_class.new(user: user, project: project, params: params).execute }
-
- let_it_be(:user) { create(:user) }
-
- let(:project) { create(:project) }
- let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
- let(:params) { {} }
-
- context 'there are no starred dashboard records' do
- it 'returns empty array' do
- expect(starred_dashboards).to be_empty
- end
- end
-
- context 'with annotation records' do
- let!(:starred_dashboard_1) { create(:metrics_users_starred_dashboard, user: user, project: project) }
- let!(:starred_dashboard_2) { create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: dashboard_path) }
- let!(:other_project_dashboard) { create(:metrics_users_starred_dashboard, user: user, dashboard_path: dashboard_path) }
- let!(:other_user_dashboard) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: dashboard_path) }
-
- context 'user without read access to project' do
- it 'returns empty relation' do
- expect(starred_dashboards).to be_empty
- end
- end
-
- context 'user with read access to project' do
- before do
- project.add_reporter(user)
- end
-
- it 'loads starred dashboards' do
- expect(starred_dashboards).to contain_exactly starred_dashboard_1, starred_dashboard_2
- end
-
- context 'when the dashboard_path filter is present' do
- let(:params) do
- {
- dashboard_path: dashboard_path
- }
- end
-
- it 'loads filtered starred dashboards' do
- expect(starred_dashboards).to contain_exactly starred_dashboard_2
- end
- end
- end
- end
- end
-end
diff --git a/spec/frontend/groups/service/archived_projects_service_spec.js b/spec/frontend/groups/service/archived_projects_service_spec.js
index 3aec9d57ee1..8e9dfb0f971 100644
--- a/spec/frontend/groups/service/archived_projects_service_spec.js
+++ b/spec/frontend/groups/service/archived_projects_service_spec.js
@@ -18,11 +18,9 @@ describe('ArchivedProjectsService', () => {
const query = 'git';
const sort = 'created_asc';
- beforeEach(() => {
+ it('returns promise the resolves with formatted project', async () => {
Api.groupProjects.mockResolvedValueOnce({ data: projects, headers });
- });
- it('returns promise the resolves with formatted project', async () => {
await expect(service.getGroups(undefined, page, query, sort)).resolves.toEqual({
data: projects.map((project) => {
return {
@@ -47,7 +45,7 @@ describe('ArchivedProjectsService', () => {
number_users_with_delimiter: 0,
star_count: project.star_count,
updated_at: project.updated_at,
- marked_for_deletion: project.marked_for_deletion_at !== null,
+ marked_for_deletion: false,
last_activity_at: project.last_activity_at,
};
}),
@@ -63,6 +61,35 @@ describe('ArchivedProjectsService', () => {
});
describe.each`
+ markedForDeletionAt | expected
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'2023-07-21'} | ${true}
+ `(
+ 'when `marked_for_deletion_at` is $markedForDeletionAt',
+ ({ markedForDeletionAt, expected }) => {
+ it(`sets marked_for_deletion to ${expected}`, async () => {
+ Api.groupProjects.mockResolvedValueOnce({
+ data: projects.map((project) => ({
+ ...project,
+ marked_for_deletion_at: markedForDeletionAt,
+ })),
+ headers,
+ });
+
+ await expect(service.getGroups(undefined, page, query, sort)).resolves.toMatchObject({
+ data: projects.map(() => {
+ return {
+ marked_for_deletion: expected,
+ };
+ }),
+ headers,
+ });
+ });
+ },
+ );
+
+ describe.each`
sortArgument | expectedOrderByParameter | expectedSortParameter
${'name_asc'} | ${'name'} | ${'asc'}
${'name_desc'} | ${'name'} | ${'desc'}
@@ -75,6 +102,8 @@ describe('ArchivedProjectsService', () => {
'when the sort argument is $sortArgument',
({ sortArgument, expectedSortParameter, expectedOrderByParameter }) => {
it(`calls the API with sort parameter set to ${expectedSortParameter} and order_by parameter set to ${expectedOrderByParameter}`, () => {
+ Api.groupProjects.mockResolvedValueOnce({ data: projects, headers });
+
service.getGroups(undefined, page, query, sortArgument);
expect(Api.groupProjects).toHaveBeenCalledWith(groupId, query, {
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index 4c13ec555c2..87bee6afd62 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -1,4 +1,10 @@
-import { GlDropdown, GlIcon, GlDropdownItem } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlButtonGroup,
+ GlButton,
+ GlIcon,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
@@ -13,6 +19,11 @@ describe('import actions cell', () => {
isInvalid: false,
...props,
},
+ stubs: {
+ GlButtonGroup,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
});
};
@@ -22,9 +33,9 @@ describe('import actions cell', () => {
});
it('renders import dropdown', () => {
- const dropdown = wrapper.findComponent(GlDropdown);
- expect(dropdown.exists()).toBe(true);
- expect(dropdown.props('text')).toBe('Import with projects');
+ const button = wrapper.findComponent(GlButton);
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Import with projects');
});
it('does not render icon with a hint', () => {
@@ -38,9 +49,9 @@ describe('import actions cell', () => {
});
it('renders re-import dropdown', () => {
- const dropdown = wrapper.findComponent(GlDropdown);
- expect(dropdown.exists()).toBe(true);
- expect(dropdown.props('text')).toBe('Re-import with projects');
+ const button = wrapper.findComponent(GlButton);
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Re-import with projects');
});
it('renders icon with a hint', () => {
@@ -55,22 +66,22 @@ describe('import actions cell', () => {
it('does not render import dropdown when group is not available for import', () => {
createComponent({ isAvailableForImport: false });
- const dropdown = wrapper.findComponent(GlDropdown);
+ const dropdown = wrapper.findComponent(GlDisclosureDropdown);
expect(dropdown.exists()).toBe(false);
});
it('renders import dropdown as disabled when group is invalid', () => {
createComponent({ isInvalid: true, isAvailableForImport: true });
- const dropdown = wrapper.findComponent(GlDropdown);
+ const dropdown = wrapper.findComponent(GlDisclosureDropdown);
expect(dropdown.props().disabled).toBe(true);
});
it('emits import-group event when import button is clicked', () => {
createComponent({ isAvailableForImport: true });
- const dropdown = wrapper.findComponent(GlDropdown);
- dropdown.vm.$emit('click');
+ const button = wrapper.findComponent(GlButton);
+ button.vm.$emit('click');
expect(wrapper.emitted('import-group')).toHaveLength(1);
});
@@ -87,23 +98,24 @@ describe('import actions cell', () => {
});
it('render import dropdown', () => {
- const dropdown = wrapper.findComponent(GlDropdown);
- expect(dropdown.props('text')).toBe(`${expectedAction} with projects`);
- expect(dropdown.findComponent(GlDropdownItem).text()).toBe(
+ const button = wrapper.findComponent(GlButton);
+ const dropdown = wrapper.findComponent(GlDisclosureDropdown);
+ expect(button.element).toHaveText(`${expectedAction} with projects`);
+ expect(dropdown.findComponent(GlDisclosureDropdownItem).text()).toBe(
`${expectedAction} without projects`,
);
});
it('request migrate projects by default', () => {
- const dropdown = wrapper.findComponent(GlDropdown);
- dropdown.vm.$emit('click');
+ const button = wrapper.findComponent(GlButton);
+ button.vm.$emit('click');
expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: true }]);
});
it('request not to migrate projects via dropdown option', () => {
- const dropdown = wrapper.findComponent(GlDropdown);
- dropdown.findComponent(GlDropdownItem).vm.$emit('click');
+ const dropdown = wrapper.findComponent(GlDisclosureDropdown);
+ dropdown.findComponent(GlDisclosureDropdownItem).vm.$emit('action');
expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: false }]);
});
diff --git a/spec/frontend/sessions/new/components/email_verification_spec.js b/spec/frontend/sessions/new/components/email_verification_spec.js
new file mode 100644
index 00000000000..8ff139e8475
--- /dev/null
+++ b/spec/frontend/sessions/new/components/email_verification_spec.js
@@ -0,0 +1,205 @@
+import { GlForm, GlFormInput } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import EmailVerification from '~/sessions/new/components/email_verification.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+import {
+ I18N_EMAIL_EMPTY_CODE,
+ I18N_EMAIL_INVALID_CODE,
+ I18N_GENERIC_ERROR,
+ I18N_RESEND_LINK,
+ I18N_EMAIL_RESEND_SUCCESS,
+} from '~/sessions/new/constants';
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
+
+describe('EmailVerification', () => {
+ let wrapper;
+ let axiosMock;
+
+ const defaultPropsData = {
+ obfuscatedEmail: 'al**@g*****.com',
+ verifyPath: '/users/sign_in',
+ resendPath: '/users/resend_verification_code',
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(EmailVerification, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findCodeInput = () => wrapper.findComponent(GlFormInput);
+ const findSubmitButton = () => wrapper.find('[type="submit"]');
+ const findResendLink = () => wrapper.findByText(I18N_RESEND_LINK);
+ const enterCode = (code) => findCodeInput().setValue(code);
+ const submitForm = () => findForm().trigger('submit');
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ createComponent();
+ });
+
+ afterEach(() => {
+ createAlert.mockClear();
+ axiosMock.restore();
+ });
+
+ describe('rendering the form', () => {
+ it('contains the obfuscated email address', () => {
+ expect(wrapper.text()).toContain(defaultPropsData.obfuscatedEmail);
+ });
+ });
+
+ describe('verifying the code', () => {
+ describe('when successfully verifying the code', () => {
+ const redirectPath = 'root';
+
+ beforeEach(async () => {
+ enterCode('123456');
+
+ axiosMock
+ .onPost(defaultPropsData.verifyPath)
+ .reply(HTTP_STATUS_OK, { status: 'success', redirect_path: redirectPath });
+
+ await submitForm();
+ await axios.waitForAll();
+ });
+
+ it('redirects to the returned redirect path', () => {
+ expect(visitUrl).toHaveBeenCalledWith(redirectPath);
+ });
+ });
+
+ describe('error messages', () => {
+ it.each`
+ scenario | code | submit | codeValid | errorShown | message
+ ${'shows no error messages before submitting the form'} | ${''} | ${false} | ${false} | ${false} | ${null}
+ ${'shows no error messages before submitting the form'} | ${'xxx'} | ${false} | ${false} | ${false} | ${null}
+ ${'shows no error messages before submitting the form'} | ${'123456'} | ${false} | ${true} | ${false} | ${null}
+ ${'shows empty code error message when submitting the form'} | ${''} | ${true} | ${false} | ${true} | ${I18N_EMAIL_EMPTY_CODE}
+ ${'shows invalid error message when submitting the form'} | ${'xxx'} | ${true} | ${false} | ${true} | ${I18N_EMAIL_INVALID_CODE}
+ ${'shows incorrect code error message returned from the server'} | ${'123456'} | ${true} | ${true} | ${true} | ${s__('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')}
+ `(`$scenario with code $code`, async ({ code, submit, codeValid, errorShown, message }) => {
+ enterCode(code);
+
+ if (submit && codeValid) {
+ axiosMock
+ .onPost(defaultPropsData.verifyPath)
+ .replyOnce(HTTP_STATUS_OK, { status: 'failure', message });
+ }
+
+ if (submit) {
+ await submitForm();
+ await axios.waitForAll();
+ }
+
+ expect(findCodeInput().classes('is-invalid')).toBe(errorShown);
+ expect(findSubmitButton().props('disabled')).toBe(errorShown);
+ if (message) expect(wrapper.text()).toContain(message);
+ });
+
+ it('keeps showing error messages for invalid codes after submitting the form', async () => {
+ const serverErrorMessage = 'error message';
+
+ enterCode('123456');
+
+ axiosMock
+ .onPost(defaultPropsData.verifyPath)
+ .replyOnce(HTTP_STATUS_OK, { status: 'failure', message: serverErrorMessage });
+
+ await submitForm();
+ await axios.waitForAll();
+
+ expect(wrapper.text()).toContain(serverErrorMessage);
+
+ await enterCode('');
+ expect(wrapper.text()).toContain(I18N_EMAIL_EMPTY_CODE);
+
+ await enterCode('xxx');
+ expect(wrapper.text()).toContain(I18N_EMAIL_INVALID_CODE);
+ });
+
+ it('captures the error and shows an alert message when the request failed', async () => {
+ enterCode('123456');
+
+ axiosMock.onPost(defaultPropsData.verifyPath).replyOnce(HTTP_STATUS_OK, null);
+
+ await submitForm();
+ await axios.waitForAll();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: I18N_GENERIC_ERROR,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+
+ it('captures the error and shows an alert message when the request undefined', async () => {
+ enterCode('123456');
+
+ axiosMock.onPost(defaultPropsData.verifyPath).reply(HTTP_STATUS_OK, { status: undefined });
+
+ await submitForm();
+ await axios.waitForAll();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: I18N_GENERIC_ERROR,
+ captureError: true,
+ error: undefined,
+ });
+ });
+ });
+ });
+
+ describe('resending the code', () => {
+ const failedMessage = 'Failure sending the code';
+ const successAlertObject = {
+ message: I18N_EMAIL_RESEND_SUCCESS,
+ variant: VARIANT_SUCCESS,
+ };
+ const failedAlertObject = {
+ message: failedMessage,
+ };
+ const undefinedAlertObject = {
+ captureError: true,
+ error: undefined,
+ message: I18N_GENERIC_ERROR,
+ };
+ const genericAlertObject = {
+ message: I18N_GENERIC_ERROR,
+ captureError: true,
+ error: expect.any(Error),
+ };
+
+ it.each`
+ scenario | statusCode | response | alertObject
+ ${'the code was successfully resend'} | ${HTTP_STATUS_OK} | ${{ status: 'success' }} | ${successAlertObject}
+ ${'there was a problem resending the code'} | ${HTTP_STATUS_OK} | ${{ status: 'failure', message: failedMessage }} | ${failedAlertObject}
+ ${'when the request is undefined'} | ${HTTP_STATUS_OK} | ${{ status: undefined }} | ${undefinedAlertObject}
+ ${'when the request failed'} | ${HTTP_STATUS_NOT_FOUND} | ${null} | ${genericAlertObject}
+ `(`shows an alert message when $scenario`, async ({ statusCode, response, alertObject }) => {
+ enterCode('xxx');
+
+ await submitForm();
+
+ axiosMock.onPost(defaultPropsData.resendPath).replyOnce(statusCode, response);
+
+ findResendLink().trigger('click');
+
+ await axios.waitForAll();
+
+ expect(createAlert).toHaveBeenCalledWith(alertObject);
+ expect(findCodeInput().element.value).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
index e983519d9fc..03f509a3fa3 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
@@ -1,8 +1,13 @@
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import IssuableCreateRoot from '~/vue_shared/issuable/create/components/issuable_create_root.vue';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
+Vue.use(VueApollo);
+
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
descriptionHelpPath = '/help/user/markdown',
@@ -16,6 +21,7 @@ const createComponent = ({
labelsFetchPath,
labelsManagePath,
},
+ apolloProvider: createMockApollo(),
slots: {
title: `
<h1 class="js-create-title">New Issuable</h1>
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index ae2fd5ebffa..338dc80b43e 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -2,8 +2,9 @@ import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
+import { __ } from '~/locale';
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
@@ -24,7 +25,7 @@ const createComponent = ({
`,
},
stubs: {
- MarkdownField,
+ MarkdownEditor,
},
});
};
@@ -71,18 +72,20 @@ describe('IssuableForm', () => {
expect(descriptionFieldEl.exists()).toBe(true);
expect(descriptionFieldEl.find('label').text()).toBe('Description');
- expect(descriptionFieldEl.findComponent(MarkdownField).exists()).toBe(true);
- expect(descriptionFieldEl.findComponent(MarkdownField).props()).toMatchObject({
- markdownPreviewPath: wrapper.vm.descriptionPreviewPath,
+ expect(descriptionFieldEl.findComponent(MarkdownEditor).exists()).toBe(true);
+ expect(descriptionFieldEl.findComponent(MarkdownEditor).props()).toMatchObject({
+ renderMarkdownPath: wrapper.vm.descriptionPreviewPath,
markdownDocsPath: wrapper.vm.descriptionHelpPath,
- addSpacingClasses: false,
- showSuggestPopover: true,
- textareaValue: '',
+ value: '',
+ formFieldProps: {
+ ariaLabel: __('Description'),
+ class: 'rspec-issuable-form-description',
+ placeholder: __('Write a comment or drag your files here…'),
+ dataQaSelector: 'issuable_form_description_field',
+ id: 'issuable-description',
+ name: 'issuable-description',
+ },
});
- expect(descriptionFieldEl.find('textarea').exists()).toBe(true);
- expect(descriptionFieldEl.find('textarea').attributes('placeholder')).toBe(
- 'Write a comment or drag your files here…',
- );
});
it('renders labels select field', () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index dd46505bd65..e24cfe27616 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -108,8 +108,8 @@ describe('WorkItemLinks', () => {
describe('add link form', () => {
it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
await createComponent();
- findToggleFormDropdown().vm.$emit('click');
- findToggleAddFormButton().vm.$emit('click');
+ findToggleFormDropdown().vm.$emit('action');
+ findToggleAddFormButton().vm.$emit('action');
await nextTick();
expect(findAddLinksForm().exists()).toBe(true);
@@ -123,8 +123,8 @@ describe('WorkItemLinks', () => {
it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
await createComponent();
- findToggleFormDropdown().vm.$emit('click');
- findToggleCreateFormButton().vm.$emit('click');
+ findToggleFormDropdown().vm.$emit('action');
+ findToggleCreateFormButton().vm.$emit('action');
await nextTick();
expect(findAddLinksForm().exists()).toBe(true);
@@ -195,8 +195,8 @@ describe('WorkItemLinks', () => {
.fn()
.mockResolvedValue(getIssueDetailsResponse({ confidential: true })),
});
- findToggleFormDropdown().vm.$emit('click');
- findToggleAddFormButton().vm.$emit('click');
+ findToggleFormDropdown().vm.$emit('action');
+ findToggleAddFormButton().vm.$emit('action');
await nextTick();
expect(findAddLinksForm().props('parentConfidential')).toBe(true);
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d7e5c02ffbe..0c5ce179acc 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -584,6 +584,7 @@ export const workItemResponseFactory = ({
__typename: 'WorkItemWidgetProgress',
type: 'PROGRESS',
progress: 0,
+ updatedAt: new Date(),
}
: { type: 'MOCK TYPE' },
milestoneWidgetPresent
@@ -1145,6 +1146,7 @@ export const workItemObjectiveMetadataWidgets = {
type: 'PROGRESS',
__typename: 'WorkItemWidgetProgress',
progress: 10,
+ updatedAt: new Date(),
},
};
@@ -1213,6 +1215,7 @@ export const workItemObjectiveNoMetadata = {
__typename: 'WorkItemWidgetProgress',
type: 'PROGRESS',
progress: null,
+ updatedAt: null,
},
{
__typename: 'WorkItemWidgetMilestone',
diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb
index 5a46a20ce1a..f35b6b28de8 100644
--- a/spec/helpers/sessions_helper_spec.rb
+++ b/spec/helpers/sessions_helper_spec.rb
@@ -51,28 +51,15 @@ RSpec.describe SessionsHelper do
end
end
- describe '#send_rate_limited?' do
+ describe '#verification_data' do
let(:user) { build_stubbed(:user) }
- subject { helper.send_rate_limited?(user) }
-
- before do
- allow(::Gitlab::ApplicationRateLimiter)
- .to receive(:peek)
- .with(:email_verification_code_send, scope: user)
- .and_return(rate_limited)
- end
-
- context 'when rate limited' do
- let(:rate_limited) { true }
-
- it { is_expected.to eq(true) }
- end
-
- context 'when not rate limited' do
- let(:rate_limited) { false }
-
- it { is_expected.to eq(false) }
+ it 'returns the expected data' do
+ expect(helper.verification_data(user)).to eq({
+ obfuscated_email: obfuscated_email(user.email),
+ verify_path: helper.session_path(:user),
+ resend_path: users_resend_verification_code_path
+ })
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 059cbac638b..b43b149157c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -7028,31 +7028,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- describe '#dismissed_callout_before?' do
- let_it_be(:user, refind: true) { create(:user) }
- let_it_be(:feature_name) { Users::Callout.feature_names.each_key.first }
-
- context 'when no callout dismissal record exists' do
- it 'returns false' do
- expect(user.dismissed_callout_before?(feature_name, 1.day.ago)).to eq false
- end
- end
-
- context 'when dismissed callout exists' do
- before_all do
- create(:callout, user: user, feature_name: feature_name, dismissed_at: 4.months.ago)
- end
-
- it 'returns false when dismissed_before is earlier than dismissed_at' do
- expect(user.dismissed_callout_before?(feature_name, 6.months.ago)).to eq false
- end
-
- it 'returns true when dismissed_before is later than dismissed_at' do
- expect(user.dismissed_callout_before?(feature_name, 3.months.ago)).to eq true
- end
- end
- end
-
describe '#find_or_initialize_callout' do
let_it_be(:user, refind: true) { create(:user) }
let_it_be(:feature_name) { Users::Callout.feature_names.each_key.first }
diff --git a/spec/models/users/calloutable_spec.rb b/spec/models/users/calloutable_spec.rb
index a50debd84d4..457431019f8 100644
--- a/spec/models/users/calloutable_spec.rb
+++ b/spec/models/users/calloutable_spec.rb
@@ -23,15 +23,4 @@ RSpec.describe Users::Calloutable, feature_category: :shared do
expect(callout_dismissed_day_ago.dismissed_after?(15.days.ago)).to eq(true)
end
end
-
- describe '#dismissed_before?' do
- let(:some_feature_name) { Users::Callout.feature_names.keys.second }
- let(:callout_dismissed_hour_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.hour.ago) }
- let(:callout_dismissed_minute_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.minute.ago) }
-
- it 'returns whether a callout dismissed before specified date' do
- expect(callout_dismissed_hour_ago.dismissed_before?(30.minutes.ago)).to eq(true)
- expect(callout_dismissed_minute_ago.dismissed_before?(30.minutes.ago)).to eq(false)
- end
- end
end
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index f3f8e4a1a83..1c7e1bc9217 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -147,12 +147,10 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
post(user_session_path(user: { verification_token: 'token' }))
end
- it_behaves_like 'prompt for email verification'
-
it 'adds a verification error message' do
- expect(response.body)
- .to include("You&#39;ve reached the maximum amount of tries. "\
- 'Wait 10 minutes or send a new code and try again.')
+ expect(json_response)
+ .to include('message' => "You've reached the maximum amount of tries. "\
+ 'Wait 10 minutes or send a new code and try again.')
end
end
@@ -161,11 +159,10 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
post(user_session_path(user: { verification_token: 'invalid_token' }))
end
- it_behaves_like 'prompt for email verification'
-
it 'adds a verification error message' do
- expect(response.body)
- .to include((s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')))
+ expect(json_response)
+ .to include('message' => (s_('IdentityVerification|The code is incorrect. '\
+ 'Enter it again, or send a new code.')))
end
end
@@ -175,27 +172,26 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
post(user_session_path(user: { verification_token: 'token' }))
end
- it_behaves_like 'prompt for email verification'
-
it 'adds a verification error message' do
- expect(response.body)
- .to include((s_('IdentityVerification|The code has expired. Send a new code and try again.')))
+ expect(json_response)
+ .to include('message' => (s_('IdentityVerification|The code has expired. Send a new code and try again.')))
end
end
context 'when a valid verification_token param exists' do
- before do
- post(user_session_path(user: { verification_token: 'token' }))
+ subject(:submit_token) { post(user_session_path(user: { verification_token: 'token' })) }
+
+ it 'unlocks the user, create logs and records the activity', :freeze_time do
+ expect { submit_token }.to change { user.reload.unlock_token }.to(nil)
+ .and change { user.locked_at }.to(nil)
+ .and change { AuditEvent.count }.by(1)
+ .and change { AuthenticationEvent.count }.by(1)
+ .and change { user.last_activity_on }.to(Date.today)
end
- it 'unlocks the user' do
- user.reload
- expect(user.unlock_token).to be_nil
- expect(user.locked_at).to be_nil
- end
-
- it 'redirects to the successful verification path' do
- expect(response).to redirect_to(users_successful_verification_path)
+ it 'returns the success status and a redirect path' do
+ submit_token
+ expect(json_response).to eq('status' => 'success', 'redirect_path' => users_successful_verification_path)
end
end
@@ -206,8 +202,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
post user_session_path, params: { user: { login: another_user.username, password: another_user.password } }
end
- it 'does not redirect to the successful verification path' do
- expect(response).not_to redirect_to(users_successful_verification_path)
+ it 'redirects to the root path' do
+ expect(response).to redirect_to(root_path)
end
end
end
@@ -277,7 +273,6 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
end
it_behaves_like 'send verification instructions'
- it_behaves_like 'prompt for email verification'
end
context 'when exceeding the rate limit' do
@@ -301,8 +296,6 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
mail = find_email_for(user)
expect(mail).to be_nil
end
-
- it_behaves_like 'prompt for email verification'
end
end
diff --git a/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb b/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb
index 3e32200cc77..8a66efb1585 100644
--- a/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb
+++ b/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb
@@ -14,5 +14,12 @@ RSpec.describe PersonalAccessTokens::RevokeTokenFamilyService, feature_category:
expect(response).to be_success
expect(token_1.reload).to be_revoked
end
+
+ it 'does not revoke any active token not in the pat family' do
+ unrelated_token = create(:personal_access_token)
+
+ expect(response).to be_success
+ expect(unrelated_token.reload).to be_active
+ end
end
end
diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
index 3f147f942ba..77dd67c77a4 100644
--- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
@@ -9,10 +9,8 @@ RSpec.shared_examples 'known sign in' do
user.update!(current_sign_in_ip: ip)
end
- def stub_cookie(value = user.id)
- cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = {
- value: value, expires: KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY
- }
+ def stub_cookie(value = user.id, expires = KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY)
+ cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = { value: value, expires: expires }
end
context 'when the remote IP and the last sign in IP match' do
@@ -57,15 +55,13 @@ RSpec.shared_examples 'known sign in' do
end
it 'notifies the user when the cookie is expired' do
- stub_cookie
-
- travel_to((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do
- expect_next_instance_of(NotificationService) do |instance|
- expect(instance).to receive(:unknown_sign_in)
- end
+ stub_cookie(user.id, 1.day.ago)
- post_action
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:unknown_sign_in)
end
+
+ post_action
end
context 'when notify_on_unknown_sign_in global setting is false' do