diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-24 15:09:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-24 15:09:32 +0300 |
commit | f296f23500b4b3758670ae0c5ce2e1779f533e8b (patch) | |
tree | 717151cb9e81d489b4ecf880988ea10d77b7224f /spec | |
parent | fd7c75bf603f4f2f1a4a4e63ef5cbc1a51cc0a15 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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'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 |