diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-04 18:17:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-04 18:17:13 +0300 |
commit | 4bdfcf93f224edb9c4daff90d95b0c6c92766ea3 (patch) | |
tree | cedf1f94561571d00033c48846ad3959af64449b /spec | |
parent | fb5d3cceb8d43f8c2dc22a5d8c74327e9397f2e8 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
15 files changed, 428 insertions, 83 deletions
diff --git a/spec/factories/service_desk/custom_email_verification.rb b/spec/factories/service_desk/custom_email_verification.rb index 614be5da71e..3f3a2ea570d 100644 --- a/spec/factories/service_desk/custom_email_verification.rb +++ b/spec/factories/service_desk/custom_email_verification.rb @@ -2,7 +2,10 @@ FactoryBot.define do factory :service_desk_custom_email_verification, class: '::ServiceDesk::CustomEmailVerification' do + state { 'started' } + token { 'XXXXXXXXXXXX' } project - state { "running" } + triggerer factory: :user + triggered_at { Time.current } end end diff --git a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb index 1fc6609d1f5..a01ae9ae0c2 100644 --- a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb +++ b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb @@ -406,7 +406,7 @@ RSpec.describe 'Issues > Labels bulk assignment', feature_category: :team_planni context 'cannot bulk assign labels' do it do - expect(page).not_to have_button 'Edit issues' + expect(page).not_to have_button 'Bulk edit' expect(page).not_to have_unchecked_field 'Select all' expect(page).not_to have_unchecked_field issue1.title end @@ -462,7 +462,7 @@ RSpec.describe 'Issues > Labels bulk assignment', feature_category: :team_planni def enable_bulk_update visit project_issues_path(project) wait_for_requests - click_button 'Edit issues' + click_button 'Bulk edit' end def disable_bulk_update diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb index 5696bde4069..3e119d86c05 100644 --- a/spec/features/issues/user_bulk_edits_issues_spec.rb +++ b/spec/features/issues/user_bulk_edits_issues_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor it 'sets to closed', :js do visit project_issues_path(project) - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' click_button 'Select status' click_button 'Closed' @@ -29,7 +29,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor create_closed visit project_issues_path(project, state: 'closed') - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' click_button 'Select status' click_button 'Open' @@ -43,7 +43,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor it 'updates to current user' do visit project_issues_path(project) - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' click_update_assignee_button click_button user.username @@ -61,7 +61,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor expect(find('.issue:first-of-type')).to have_link "Assigned to #{user.name}" - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' click_update_assignee_button click_button 'Unassigned' @@ -77,7 +77,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor it 'updates milestone' do visit project_issues_path(project) - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' click_button 'Select milestone' click_button milestone.title @@ -94,7 +94,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor expect(find('.issue:first-of-type')).to have_text milestone.title - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' click_button 'Select milestone' click_button 'No milestone' @@ -110,7 +110,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor it 'after selecting all issues, unchecking one issue only unselects that one issue' do visit project_issues_path(project) - click_button 'Edit issues' + click_button 'Bulk edit' check 'Select all' uncheck issue.title diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb index b0be76d386a..45d57cf8374 100644 --- a/spec/features/merge_requests/user_mass_updates_spec.rb +++ b/spec/features/merge_requests/user_mass_updates_spec.rb @@ -44,7 +44,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod merge_request.close visit project_merge_requests_path(project, state: 'merged') - click_button 'Edit merge requests' + click_button 'Bulk edit' expect(page).not_to have_button 'Select status' end @@ -108,7 +108,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod end def change_status(text) - click_button 'Edit merge requests' + click_button 'Bulk edit' check 'Select all' click_button 'Select status' click_button text @@ -116,7 +116,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod end def change_assignee(text) - click_button 'Edit merge requests' + click_button 'Bulk edit' check 'Select all' within 'aside[aria-label="Bulk update"]' do click_button 'Select assignee' @@ -127,7 +127,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod end def change_milestone(text) - click_button 'Edit merge requests' + click_button 'Bulk edit' check 'Select all' click_button 'Select milestone' click_button text diff --git a/spec/fixtures/packages/npm/payload_with_empty_attachment.json b/spec/fixtures/packages/npm/payload_with_empty_attachment.json new file mode 100644 index 00000000000..299ff32baf3 --- /dev/null +++ b/spec/fixtures/packages/npm/payload_with_empty_attachment.json @@ -0,0 +1,29 @@ +{ + "_id": "@root/npm-test", + "name": "@root/npm-test", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "dist-tags": { + "latest": "1.0.1" + }, + "versions": { + "1.0.1": { + "name": "@root/npm-test", + "version": "1.0.1", + "main": "app.js", + "dependencies": { + "express": "^4.16.4" + }, + "dist": { + "shasum": "f572d396fae9206628714fb2ce00f72e94f2258f", + "tarball": "http://localhost/npm/package.tgz" + } + } + }, + "_attachments": { + "@root/npm-test-1.0.1.tgz": { + "data": "" + } + }, + "id": "10", + "package_name": "@root/npm-test" +} diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js index 5773ab33e05..246c6499a97 100644 --- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js +++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js @@ -14,11 +14,11 @@ const ImportProjectsTableStub = { describe('BitbucketStatusTable', () => { let wrapper; - function createComponent(propsData, importProjectsTableStub = true, slots) { + function createComponent(propsData, slots) { wrapper = shallowMount(BitbucketStatusTable, { propsData, stubs: { - ImportProjectsTable: importProjectsTableStub, + ImportProjectsTable: ImportProjectsTableStub, }, slots, }); @@ -30,20 +30,23 @@ describe('BitbucketStatusTable', () => { }); it('passes alert in incompatible-repos-warning slot', () => { - createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub); + createComponent({ providerTitle: 'Test' }); expect(wrapper.findComponent(GlAlert).exists()).toBe(true); }); it('passes actions slot to import project table component', () => { const actionsSlotContent = 'DEMO'; - createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, { - actions: actionsSlotContent, - }); + createComponent( + { providerTitle: 'Test' }, + { + actions: actionsSlotContent, + }, + ); expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent); }); it('dismisses alert when requested', async () => { - createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub); + createComponent({ providerTitle: 'Test' }); wrapper.findComponent(GlAlert).vm.$emit('dismiss'); await nextTick(); diff --git a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js new file mode 100644 index 00000000000..b6f96cd6a23 --- /dev/null +++ b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js @@ -0,0 +1,97 @@ +import { GlCollapsibleListbox } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { mount } from '@vue/test-utils'; +import { captureException } from '@sentry/browser'; +import { nextTick } from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { createAlert } from '~/alert'; + +import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue'; + +jest.mock('@sentry/browser'); +jest.mock('~/alert'); + +const MOCK_RESPONSE = { + provider_groups: [{ name: 'alpha-1' }, { name: 'alpha-2' }, { name: 'beta-1' }], +}; + +describe('GithubOrganizationsBox component', () => { + let wrapper; + let mockAxios; + + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const mockGithubGroupPath = '/mock/groups.json'; + + const createComponent = (props) => { + wrapper = mount(GithubOrganizationsBox, { + propsData: { + value: 'some-org', + ...props, + }, + provide: () => ({ + statusImportGithubGroupPath: mockGithubGroupPath, + }), + }); + }; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, MOCK_RESPONSE); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('has underlying listbox as loading while loading organizations', () => { + createComponent(); + expect(findListbox().props('loading')).toBe(true); + }); + + it('clears underlying listbox when loading is complete', async () => { + createComponent(); + await axios.waitForAll(); + expect(findListbox().props('loading')).toBe(false); + }); + + it('sets toggle-text to all organizations when selection is not provided', () => { + createComponent({ value: '' }); + expect(findListbox().props('toggleText')).toBe(GithubOrganizationsBox.i18n.allOrganizations); + }); + + it('sets toggle-text to organization name when it is provided', () => { + const ORG_NAME = 'org'; + createComponent({ value: ORG_NAME }); + + expect(findListbox().props('toggleText')).toBe(ORG_NAME); + }); + + it('emits selected organization from underlying listbox', () => { + createComponent(); + + findListbox().vm.$emit('select', 'org-id'); + expect(wrapper.emitted('input').at(-1)).toStrictEqual(['org-id']); + }); + + it('filters list for underlying listbox', async () => { + createComponent(); + await axios.waitForAll(); + + findListbox().vm.$emit('search', 'alpha'); + await nextTick(); + + // 2 matches + 'All organizations' + expect(findListbox().props('items')).toHaveLength(3); + }); + + it('reports error to sentry on load', async () => { + mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + createComponent(); + await axios.waitForAll(); + + expect(captureException).toHaveBeenCalled(); + expect(createAlert).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js new file mode 100644 index 00000000000..7eebff7364c --- /dev/null +++ b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js @@ -0,0 +1,125 @@ +import { GlTabs, GlSearchBoxByClick } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; + +import { stubComponent } from 'helpers/stub_component'; +import GithubStatusTable from '~/import_entities/import_projects/components/github_status_table.vue'; +import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue'; +import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue'; +import initialState from '~/import_entities/import_projects/store/state'; +import * as getters from '~/import_entities/import_projects/store/getters'; + +const ImportProjectsTableStub = stubComponent(ImportProjectsTable, { + importAllButtonText: 'IMPORT_ALL_TEXT', + methods: { + showImportAllModal: jest.fn(), + }, + template: + '<div><slot name="filter" v-bind="{ importAllButtonText: $options.importAllButtonText, showImportAllModal }"></slot></div>', +}); + +Vue.use(Vuex); + +describe('GithubStatusTable', () => { + let wrapper; + + const setFilterAction = jest.fn().mockImplementation(({ state }, filter) => { + state.filter = { ...state.filter, ...filter }; + }); + + const findFilterField = () => wrapper.findComponent(GlSearchBoxByClick); + const selectTab = (idx) => { + wrapper.findComponent(GlTabs).vm.$emit('input', idx); + return nextTick(); + }; + + function createComponent() { + const store = new Vuex.Store({ + state: { ...initialState() }, + getters, + actions: { + setFilter: setFilterAction, + }, + }); + + wrapper = mount(GithubStatusTable, { + store, + propsData: { + providerTitle: 'Github', + }, + stubs: { + ImportProjectsTable: ImportProjectsTableStub, + GithubOrganizationsBox: stubComponent(GithubOrganizationsBox), + GlTabs: false, + GlTab: false, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + it('renders import table component', () => { + expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true); + }); + + it('sets relation_type filter to owned repositories by default', () => { + expect(setFilterAction).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ relation_type: 'owned' }), + ); + }); + + it('updates relation_type and resets organization filter when tab is switched', async () => { + const NEW_ACTIVE_TAB_IDX = 1; + await selectTab(NEW_ACTIVE_TAB_IDX); + + expect(setFilterAction).toHaveBeenCalledTimes(2); + expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), { + ...GithubStatusTable.relationTypes[NEW_ACTIVE_TAB_IDX].backendFilter, + organization_login: '', + filter: '', + }); + }); + + it('renders name filter disabled when tab with organization filter is selected and organization is not set', async () => { + const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex( + (entry) => entry.showOrganizationFilter, + ); + await selectTab(NEW_ACTIVE_TAB_IDX); + expect(findFilterField().props('disabled')).toBe(true); + }); + + it('enables name filter disabled when organization is set', async () => { + const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex( + (entry) => entry.showOrganizationFilter, + ); + await selectTab(NEW_ACTIVE_TAB_IDX); + + wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org'); + await nextTick(); + + expect(findFilterField().props('disabled')).toBe(false); + }); + + it('updates filter when search box is changed', async () => { + const NEW_FILTER = 'test'; + findFilterField().vm.$emit('submit', NEW_FILTER); + await nextTick(); + + expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), { + filter: NEW_FILTER, + }); + }); + + it('updates organization_login filter when GithubOrganizationsBox emits input', () => { + const NEW_ORG = 'some-org'; + wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG); + + expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), { + organization_login: NEW_ORG, + }); + }); +}); diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js index f78016eefcf..3b94db37801 100644 --- a/spec/frontend/import_entities/import_projects/store/actions_spec.js +++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js @@ -220,12 +220,14 @@ describe('import_projects store actions', () => { describe('when rate limited', () => { it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => { - mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_TOO_MANY_REQUESTS); + mock + .onGet(`${TEST_HOST}/endpoint.json?filtered_field=filter`) + .reply(HTTP_STATUS_TOO_MANY_REQUESTS); await testAction( fetchRepos, null, - { ...localState, filter: 'filter' }, + { ...localState, filter: { filtered_field: 'filter' } }, [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }], [], ); @@ -238,12 +240,12 @@ describe('import_projects store actions', () => { describe('when filtered', () => { it('fetches repos with filter applied', () => { - mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_OK, payload); + mock.onGet(`${TEST_HOST}/endpoint.json?some_filter=filter`).reply(HTTP_STATUS_OK, payload); return testAction( fetchRepos, null, - { ...localState, filter: 'filter' }, + { ...localState, filter: { some_filter: 'filter' } }, [ { type: REQUEST_REPOS }, { type: SET_PAGE, payload: 1 }, @@ -374,12 +376,12 @@ describe('import_projects store actions', () => { describe('when filtered', () => { beforeEach(() => { - localState.filter = 'filter'; + localState.filter = { some_filter: 'filter' }; }); it('fetches realtime changes with filter applied', () => { mock - .onGet(`${TEST_HOST}/endpoint.json?filter=filter`) + .onGet(`${TEST_HOST}/endpoint.json?some_filter=filter`) .reply(HTTP_STATUS_OK, updatedProjects); return testAction( diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js index 514a168553a..07d247630cc 100644 --- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js @@ -25,7 +25,7 @@ describe('import_projects store mutations', () => { beforeEach(() => { state = { - filter: 'some-value', + filter: { someField: 'some-value' }, repositories: ['some', ' repositories'], pageInfo: { page: 1, @@ -47,6 +47,17 @@ describe('import_projects store mutations', () => { expect(state.pageInfo.endCursor).toBe(null); expect(state.pageInfo.hasNextPage).toBe(true); }); + + it('merges filter updates', () => { + const originalFilter = { ...state.filter }; + const anotherFilter = { anotherField: 'another-value' }; + mutations[types.SET_FILTER](state, anotherFilter); + + expect(state.filter).toStrictEqual({ + ...originalFilter, + ...anotherFilter, + }); + }); }); describe(`${types.REQUEST_REPOS}`, () => { diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 076fdc4a991..af24b547545 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -304,13 +304,13 @@ describe('CE IssuesListApp component', () => { it('renders when user has permissions', () => { wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); - expect(findGlButton().text()).toBe('Edit issues'); + expect(findGlButton().text()).toBe('Bulk edit'); }); it('does not render when user does not have permissions', () => { wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount }); - expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0); + expect(findGlButtons().filter((button) => button.text() === 'Bulk edit')).toHaveLength(0); }); it('emits "issuables:enableBulkEdit" event to legacy bulk edit class', async () => { diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js index b7ac7449045..8c0346e6198 100644 --- a/spec/frontend/vue_compat_test_setup.js +++ b/spec/frontend/vue_compat_test_setup.js @@ -96,15 +96,14 @@ if (global.document) { $slots: slots = {}, $scopedSlots: scopedSlots = {}, $parent: parent, - $vnode: { children }, + $vnode: vnode, } = this; - const hasDefaultSlot = 'default' in slots || 'default' in scopedSlots; - const isTheOnlyChild = parent && parent.$.subTree.children === children; - + const hasStaticDefaultSlot = 'default' in slots && !('default' in scopedSlots); + const isTheOnlyChild = parent?.$.subTree === vnode; // this condition should be altered when https://github.com/vuejs/vue-test-utils/pull/2068 is merged // and our codebase will be updated to include it (@vue/test-utils@1.3.6 I assume) - const shouldRenderAllSlots = !hasDefaultSlot && isTheOnlyChild; + const shouldRenderAllSlots = !hasStaticDefaultSlot && isTheOnlyChild; const renderSlotByName = (slotName) => { const slot = scopedSlots[slotName] || slots[slotName]; diff --git a/spec/models/service_desk/custom_email_verification_spec.rb b/spec/models/service_desk/custom_email_verification_spec.rb index f0a6028f21d..f114367cfbf 100644 --- a/spec/models/service_desk/custom_email_verification_spec.rb +++ b/spec/models/service_desk/custom_email_verification_spec.rb @@ -3,89 +3,149 @@ require 'spec_helper' RSpec.describe ServiceDesk::CustomEmailVerification, feature_category: :service_desk do - let(:user) { build_stubbed(:user) } - let(:project) { build_stubbed(:project) } - let(:verification) { build_stubbed(:service_desk_custom_email_verification, project: project) } - let(:token) { 'XXXXXXXXXXXX' } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:generate_token_pattern) { /\A\p{Alnum}{12}\z/ } describe '.generate_token' do it 'matches expected output' do - expect(described_class.generate_token).to match(/\A\p{Alnum}{12}\z/) + expect(described_class.generate_token).to match(generate_token_pattern) end end describe 'validations' do + subject { build(:service_desk_custom_email_verification, project: project) } + it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:state) } + + context 'when status is :started' do + before do + subject.mark_as_started!(user) + end + + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_length_of(:token).is_equal_to(12) } + + it 'matches .generate_token pattern' do + expect(subject.token).to match(generate_token_pattern) + end + + it { is_expected.to validate_presence_of(:triggerer) } + it { is_expected.to validate_presence_of(:triggered_at) } + it { is_expected.to validate_absence_of(:error) } + end + + context 'when status is :finished' do + before do + subject.mark_as_started!(user) + subject.mark_as_finished! + end + + it { is_expected.to validate_absence_of(:token) } + it { is_expected.to validate_absence_of(:error) } + end + + context 'when status is :failed' do + before do + subject.mark_as_started!(user) + subject.mark_as_failed!(:smtp_host_issue) + end + + it { is_expected.to validate_presence_of(:error) } + it { is_expected.to validate_absence_of(:token) } + end end - describe '#accepted_until' do - context 'when no custom email is set up' do - it 'returns nil' do - expect(subject.accepted_until).to be_nil + describe 'status state machine' do + subject { build(:service_desk_custom_email_verification, project: project) } + + describe 'transitioning to started' do + it 'records the started at time and generates token' do + subject.mark_as_started!(user) + + is_expected.to be_started + expect(subject.token).to be_present + expect(subject.triggered_at).to be_present + expect(subject.triggerer).to eq(user) end end - context 'when custom email is set up' do - subject { verification.accepted_until } + describe 'transitioning to finished' do + it 'removes the generated token' do + subject.mark_as_started!(user) + subject.mark_as_finished! - it { is_expected.to be_nil } + is_expected.to be_finished + expect(subject.token).not_to be_present + end + end - context 'when verification process started' do - let(:triggered_at) { 2.minutes.ago } + describe 'transitioning to failed' do + let(:error) { :smtp_host_issue } - before do - verification.assign_attributes( - state: "running", - triggered_at: triggered_at, - triggerer: user, - token: token - ) - end + it 'removes the generated token' do + subject.mark_as_started!(user) + subject.mark_as_failed!(error) - it { is_expected.to eq(described_class::TIMEFRAME.since(triggered_at)) } + is_expected.to be_failed + expect(subject.token).not_to be_present + expect(subject.error).to eq(error.to_s) end end end - describe '#in_timeframe?' do - context 'when no custom email is set up' do - it 'returns false' do - expect(subject).not_to be_in_timeframe - end + describe '#accepted_until' do + it 'returns nil' do + expect(subject.accepted_until).to be_nil end - context 'when custom email is set up' do - it { is_expected.not_to be_in_timeframe } + context 'when state is :started and successfully transitioned' do + let(:triggered_at) { 2.minutes.ago } - context 'when verification process started' do - let(:triggered_at) { 1.second.ago } + before do + subject.project = project + subject.mark_as_started!(user) + end - before do - subject.assign_attributes( - state: "running", - triggered_at: triggered_at, - triggerer: user, - token: token - ) + it 'returns correct timeframe end time' do + expect(subject.accepted_until).to eq(described_class::TIMEFRAME.since(subject.triggered_at)) + end + + context 'when triggered_at is not set' do + it 'returns nil' do + subject.triggered_at = nil + expect(subject.accepted_until).to be nil end + end + end + end - it { is_expected.to be_in_timeframe } + describe '#in_timeframe?' do + it { is_expected.not_to be_in_timeframe } - context 'and timeframe was missed' do - let(:triggered_at) { (described_class::TIMEFRAME + 1).ago } + context 'when state is :started and successfully transitioned' do + before do + subject.project = project + subject.mark_as_started!(user) + end - before do - subject.triggered_at = triggered_at - end + it { is_expected.to be_in_timeframe } - it { is_expected.not_to be_in_timeframe } + context 'and timeframe was missed' do + before do + subject.triggered_at = (described_class::TIMEFRAME + 1).ago end + + it { is_expected.not_to be_in_timeframe } end end end describe 'encrypted #token' do + let(:token) { 'XXXXXXXXXXXX' } + subject { build_stubbed(:service_desk_custom_email_verification, token: token) } it 'saves and retrieves the encrypted token and iv correctly' do @@ -101,9 +161,10 @@ RSpec.describe ServiceDesk::CustomEmailVerification, feature_category: :service_ it { is_expected.to belong_to(:triggerer) } it 'can access service desk setting from project' do - setting = build_stubbed(:service_desk_setting, project: project) + subject.project = project + setting = build_stubbed(:service_desk_setting, project: subject.project) - expect(verification.service_desk_setting).to eq(setting) + expect(subject.service_desk_setting).to eq(setting) end end end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index f621af5d968..1f5ebc80824 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -214,6 +214,14 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'not a package tracking event' end end + + context 'invalid package attachment data' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_empty_attachment.json') } + + it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'not a package tracking event' + end end context 'valid package params' do diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index b2ae89f0f9d..a12d86412d8 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -294,6 +294,13 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r end end + context 'with empty attachment data' do + let(:params) { super().merge({ _attachments: { "#{package_name}-#{version}.tgz" => { data: '' } } }) } + + it { expect(subject[:http_status]).to eq 400 } + it { expect(subject[:message]).to eq 'Attachment data is empty.' } + end + it 'obtains a lease to create a new package' do expect_to_obtain_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT) |