diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-22 18:07:57 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-22 18:07:57 +0300 |
commit | 68aa32736b50c3609348f3bf740b81a2dfd1fb25 (patch) | |
tree | 801bc83d3ff80e58cf68cf1c9f33a164c36eb7de /spec | |
parent | fb336d5f6b8b2c8f3131ee97a68ebc80c64a0223 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
24 files changed, 523 insertions, 257 deletions
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb index bf7707f177c..c9b3cf4bca7 100644 --- a/spec/controllers/admin/applications_controller_spec.rb +++ b/spec/controllers/admin/applications_controller_spec.rb @@ -38,6 +38,30 @@ RSpec.describe Admin::ApplicationsController do end end + describe 'PUT #renew' do + let(:oauth_params) do + { + id: application.id + } + end + + subject { put :renew, params: oauth_params } + + it { is_expected.to have_gitlab_http_status(:ok) } + it { expect { subject }.to change { application.reload.secret } } + + context 'when renew fails' do + before do + allow_next_found_instance_of(Doorkeeper::Application) do |application| + allow(application).to receive(:save).and_return(false) + end + end + + it { expect { subject }.not_to change { application.reload.secret } } + it { is_expected.to redirect_to(admin_application_url(application)) } + end + end + describe 'POST #create' do context 'with hash_oauth_secrets flag off' do before do diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb index b9457770ed6..8919360d276 100644 --- a/spec/controllers/groups/settings/applications_controller_spec.rb +++ b/spec/controllers/groups/settings/applications_controller_spec.rb @@ -188,6 +188,55 @@ RSpec.describe Groups::Settings::ApplicationsController do end end + describe 'PUT #renew' do + context 'when user is owner' do + before do + group.add_owner(user) + end + + let(:oauth_params) do + { + group_id: group, + id: application.id + } + end + + subject { put :renew, params: oauth_params } + + it { is_expected.to have_gitlab_http_status(:ok) } + it { expect { subject }.to change { application.reload.secret } } + + context 'when renew fails' do + before do + allow_next_found_instance_of(Doorkeeper::Application) do |application| + allow(application).to receive(:save).and_return(false) + end + end + + it { expect { subject }.not_to change { application.reload.secret } } + it { is_expected.to redirect_to(group_settings_application_url(group, application)) } + end + end + + context 'when user is not owner' do + before do + group.add_maintainer(user) + end + + let(:oauth_params) do + { + group_id: group, + id: application.id + } + end + + it 'renders a 404' do + put :renew, params: oauth_params + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'PATCH #update' do context 'when user is owner' do before do diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index 9b16dc9a463..2b8017f1ff7 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -71,6 +71,33 @@ RSpec.describe Oauth::ApplicationsController do it_behaves_like 'redirects to 2fa setup page when the user requires it' end + describe 'PUT #renew' do + let(:oauth_params) do + { + id: application.id + } + end + + subject { put :renew, params: oauth_params } + + it { is_expected.to have_gitlab_http_status(:ok) } + it { expect { subject }.to change { application.reload.secret } } + + it_behaves_like 'redirects to login page when the user is not signed in' + it_behaves_like 'redirects to 2fa setup page when the user requires it' + + context 'when renew fails' do + before do + allow_next_found_instance_of(Doorkeeper::Application) do |application| + allow(application).to receive(:save).and_return(false) + end + end + + it { expect { subject }.not_to change { application.reload.secret } } + it { is_expected.to redirect_to(oauth_application_url(application)) } + end + end + describe 'GET #show' do subject { get :show, params: { id: application.id } } diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index 7740b2da911..dccca9ce30d 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -261,7 +261,8 @@ FactoryBot.define do app_store_issuer_id { 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' } app_store_key_id { 'ABC1' } - app_store_private_key { File.read('spec/fixtures/ssl_key.pem') } + app_store_private_key_file_name { 'auth_key.p8' } + app_store_private_key { File.read('spec/fixtures/auth_key.p8') } end # this is for testing storing values inside properties, which is deprecated and will be removed in diff --git a/spec/features/projects/integrations/apple_app_store_spec.rb b/spec/features/projects/integrations/apple_app_store_spec.rb new file mode 100644 index 00000000000..b6dc6557e20 --- /dev/null +++ b/spec/features/projects/integrations/apple_app_store_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do + include_context 'project integration activation' + + it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do + visit_project_integration('Apple App Store Connect') + + expect(page).to have_content('Drag your Private Key file here or click to upload.') + expect(page).not_to have_content('auth_key.p8') + + find("input[name='service[dropzone_file_name]']", + visible: false).set(Rails.root.join('spec/fixtures/auth_key.p8')) + + expect(find("input[name='service[app_store_private_key]']", + visible: false).value).to eq(File.read(Rails.root.join('spec/fixtures/auth_key.p8'))) + expect(find("input[name='service[app_store_private_key_file_name]']", visible: false).value).to eq('auth_key.p8') + + expect(page).not_to have_content('Drag your Private Key file here or click to upload.') + expect(page).to have_content('auth_key.p8') + end +end diff --git a/spec/fixtures/auth_key.p8 b/spec/fixtures/auth_key.p8 new file mode 100644 index 00000000000..1b53126536e --- /dev/null +++ b/spec/fixtures/auth_key.p8 @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN +SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t +PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB +kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd +j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ +uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR +5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O +AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K +EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh +Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C +m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH +EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx +63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi +nNp/xedE1YxutQ== +-----END PRIVATE KEY----- diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index 383dfb36aa5..2d74fe0a7d8 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -507,30 +507,21 @@ describe('IntegrationForm', () => { const dummyHelp = 'Foo Help'; it.each` - integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true} - ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false} - ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} - ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false} - ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false} - ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} - ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false} + integration | helpHtml | sections | shouldShowSections | shouldShowHelp + ${INTEGRATION_FORM_TYPE_SLACK} | ${''} | ${[]} | ${false} | ${false} + ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[]} | ${false} | ${true} + ${INTEGRATION_FORM_TYPE_SLACK} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} + ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true} + ${'foo'} | ${''} | ${[]} | ${false} | ${false} + ${'foo'} | ${dummyHelp} | ${[]} | ${false} | ${true} + ${'foo'} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} + ${'foo'} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false} `( - '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration', - ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => { + '$sections sections, and "$helpHtml" helpHtml for "$integration" integration', + ({ integration, helpHtml, sections, shouldShowSections, shouldShowHelp }) => { createComponent({ provide: { helpHtml, - glFeatures: { integrationSlackAppNotifications: flagIsOn }, }, customStateProps: { sections, @@ -553,20 +544,15 @@ describe('IntegrationForm', () => { ${false} | ${true} | ${'When having only the fields without a section'} `('$description', ({ hasSections, hasFieldsWithoutSections }) => { it.each` - prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert - ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true} - ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${true} | ${false} - ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${false} | ${false} - ${'does not'} | ${'foo'} | ${true} | ${true} | ${false} - ${'does not'} | ${'foo'} | ${false} | ${true} | ${false} - ${'does not'} | ${'foo'} | ${true} | ${false} | ${false} + prefix | integration | shouldUpgradeSlack | shouldShowAlert + ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} + ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${false} + ${'does not'} | ${'foo'} | ${true} | ${false} + ${'does not'} | ${'foo'} | ${false} | ${false} `( - '$prefix render the upgrade warning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections', - ({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => { + '$prefix render the upgrade warning when we are in "$integration" integration with Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections', + ({ integration, shouldUpgradeSlack, shouldShowAlert }) => { createComponent({ - provide: { - glFeatures: { integrationSlackAppNotifications: flagIsOn }, - }, customStateProps: { shouldUpgradeSlack, type: integration, diff --git a/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js new file mode 100644 index 00000000000..62f0439a13f --- /dev/null +++ b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; + +import IntegrationSectionAppleAppStore from '~/integrations/edit/components/sections/apple_app_store.vue'; +import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue'; +import { createStore } from '~/integrations/edit/store'; + +describe('IntegrationSectionAppleAppStore', () => { + let wrapper; + + const createComponent = (componentFields) => { + const store = createStore({ + customState: { ...componentFields }, + }); + wrapper = shallowMount(IntegrationSectionAppleAppStore, { + store, + }); + }; + + const componentFields = (fileName = '') => { + return { + fields: [ + { + name: 'app_store_private_key_file_name', + value: fileName, + }, + ], + }; + }; + + const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField); + + describe('computed properties', () => { + it('renders UploadDropzoneField with default values', () => { + createComponent(componentFields()); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'The Apple App Store Connect Private Key (.p8)', + helpText: '', + }); + }); + + it('renders UploadDropzoneField with custom values for an attached file', () => { + createComponent(componentFields('fileName.txt')); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'Upload a new Apple App Store Connect Private Key (replace fileName.txt)', + helpText: 'Leave empty to use your current Private Key.', + }); + }); + }); +}); diff --git a/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js new file mode 100644 index 00000000000..36e20db0022 --- /dev/null +++ b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js @@ -0,0 +1,88 @@ +import { mount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; +import { nextTick } from 'vue'; + +import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue'; +import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; +import { mockField } from '../mock_data'; + +describe('UploadDropzoneField', () => { + let wrapper; + + const contentsInputName = 'service[app_store_private_key]'; + const fileNameInputName = 'service[app_store_private_key_file_name]'; + + const createComponent = (props) => { + wrapper = mount(UploadDropzoneField, { + propsData: { + ...mockField, + ...props, + name: contentsInputName, + label: 'Input Label', + fileInputName: fileNameInputName, + }, + }); + }; + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findUploadDropzone = () => wrapper.findComponent(UploadDropzone); + const findFileContentsHiddenInput = () => wrapper.find(`input[name="${contentsInputName}"]`); + const findFileNameHiddenInput = () => wrapper.find(`input[name="${fileNameInputName}"]`); + + describe('template', () => { + it('adds the expected file inputFieldName', () => { + createComponent(); + + expect(findUploadDropzone().props('inputFieldName')).toBe('service[dropzone_file_name]'); + }); + + it('adds a disabled, hidden text input for the file contents', () => { + createComponent(); + + expect(findFileContentsHiddenInput().attributes('name')).toBe(contentsInputName); + expect(findFileContentsHiddenInput().attributes('disabled')).toBeDefined(); + }); + + it('adds a disabled, hidden text input for the file name', () => { + createComponent(); + + expect(findFileNameHiddenInput().attributes('name')).toBe(fileNameInputName); + expect(findFileNameHiddenInput().attributes('disabled')).toBeDefined(); + }); + }); + + describe('clearError', () => { + it('clears uploadError when called', async () => { + createComponent(); + + expect(findGlAlert().exists()).toBe(false); + + findUploadDropzone().vm.$emit('error'); + await nextTick(); + + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe( + 'Error: You are trying to upload something other than an allowed file.', + ); + + findGlAlert().vm.$emit('dismiss'); + await nextTick(); + + expect(findGlAlert().exists()).toBe(false); + }); + }); + + describe('onError', () => { + it('assigns uploadError to the supplied custom message', async () => { + const message = 'test error message'; + createComponent({ errorMessage: message }); + + findUploadDropzone().vm.$emit('error'); + + await nextTick(); + + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe(message); + }); + }); +}); 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 8281ce0ed1a..93b9868b38b 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -15,6 +15,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { getIssuesCountsQueryResponse, getIssuesQueryResponse, + getIssuesQueryEmptyResponse, filteredTokens, locationSearch, setSortPreferenceMutationResponse, @@ -154,7 +155,24 @@ describe('CE IssuesListApp component', () => { router = new VueRouter({ mode: 'history' }); return mountFn(IssuesListApp, { - apolloProvider: createMockApollo(requestHandlers), + apolloProvider: createMockApollo( + requestHandlers, + {}, + { + typePolicies: { + Query: { + fields: { + project: { + merge: true, + }, + group: { + merge: true, + }, + }, + }, + }, + }, + ), router, provide: { ...defaultProvide, @@ -180,7 +198,6 @@ describe('CE IssuesListApp component', () => { describe('IssuableList', () => { beforeEach(() => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -247,7 +264,6 @@ describe('CE IssuesListApp component', () => { mountFn: mount, }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -477,7 +493,12 @@ describe('CE IssuesListApp component', () => { describe('empty states', () => { describe('when there are issues', () => { beforeEach(() => { - wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); + wrapper = mountComponent({ + provide: { hasAnyIssues: true }, + mountFn: mount, + issuesQueryResponse: getIssuesQueryEmptyResponse, + }); + return waitForPromises(); }); it('shows EmptyStateWithAnyIssues empty state', () => { @@ -599,7 +620,6 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -620,8 +640,9 @@ describe('CE IssuesListApp component', () => { describe('events', () => { describe('when "click-tab" event is emitted by IssuableList', () => { - beforeEach(() => { + beforeEach(async () => { wrapper = mountComponent(); + await waitForPromises(); router.push = jest.fn(); findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); @@ -641,19 +662,25 @@ describe('CE IssuesListApp component', () => { describe.each` event | params ${'next-page'} | ${{ - page_after: 'endCursor', + page_after: 'endcursor', page_before: undefined, first_page_size: 20, last_page_size: undefined, + search: undefined, + sort: 'created_date', + state: 'opened', }} ${'previous-page'} | ${{ page_after: undefined, - page_before: 'startCursor', + page_before: 'startcursor', first_page_size: undefined, last_page_size: 20, + search: undefined, + sort: 'created_date', + state: 'opened', }} `('when "$event" event is emitted by IssuableList', ({ event, params }) => { - beforeEach(() => { + beforeEach(async () => { wrapper = mountComponent({ data: { pageInfo: { @@ -662,6 +689,7 @@ describe('CE IssuesListApp component', () => { }, }, }); + await waitForPromises(); router.push = jest.fn(); findIssuableList().vm.$emit(event); @@ -735,7 +763,6 @@ describe('CE IssuesListApp component', () => { provide: { isProject }, issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -761,7 +788,6 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response()), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -793,8 +819,6 @@ describe('CE IssuesListApp component', () => { router.push = jest.fn(); findIssuableList().vm.$emit('sort', sortKey); - jest.runOnlyPendingTimers(); - await nextTick(); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ sort: urlSortParams[sortKey] }), @@ -914,13 +938,13 @@ describe('CE IssuesListApp component', () => { ${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false} ${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true} ${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false} - `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => { + `('$description', async ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => { const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse); wrapper = mountComponent({ provide: { isPublicVisibilityRestricted, isSignedIn }, issuesQueryResponse: mockQuery, }); - jest.runOnlyPendingTimers(); + await waitForPromises(); expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers })); }); @@ -929,7 +953,6 @@ describe('CE IssuesListApp component', () => { describe('fetching issues', () => { beforeEach(() => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); }); it('fetches issue, incident, test case, and task types', () => { diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index 1e8a81116f3..0332f68ddb6 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -101,6 +101,26 @@ export const getIssuesQueryResponse = { }, }; +export const getIssuesQueryEmptyResponse = { + data: { + project: { + id: '1', + __typename: 'Project', + issues: { + __persist: true, + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'startcursor', + endCursor: 'endcursor', + }, + nodes: [], + }, + }, + }, +}; + export const getIssuesCountsQueryResponse = { data: { project: { diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js index f63af2fe0a4..509ddc7ce86 100644 --- a/spec/frontend/lib/utils/file_upload_spec.js +++ b/spec/frontend/lib/utils/file_upload_spec.js @@ -1,5 +1,9 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload'; +import fileUpload, { + getFilename, + validateImageName, + validateFileFromAllowList, +} from '~/lib/utils/file_upload'; describe('File upload', () => { beforeEach(() => { @@ -89,3 +93,19 @@ describe('file name validator', () => { expect(validateImageName(file)).toBe('image.png'); }); }); + +describe('validateFileFromAllowList', () => { + it('returns true if the file type is in the allowed list', () => { + const allowList = ['.foo', '.bar']; + const fileName = 'file.foo'; + + expect(validateFileFromAllowList(fileName, allowList)).toBe(true); + }); + + it('returns false if the file type is in the allowed list', () => { + const allowList = ['.foo', '.bar']; + const fileName = 'file.baz'; + + expect(validateFileFromAllowList(fileName, allowList)).toBe(false); + }); +}); diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index bc847a3e159..1d072c0ba3c 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -68,18 +68,23 @@ describe('HelpCenter component', () => { }); describe('showKeyboardShortcuts', () => { + let button; + beforeEach(() => { jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); - window.toggleShortcutsHelp = jest.fn(); - findButton('Keyboard shortcuts ?').click(); + + button = findButton('Keyboard shortcuts ?'); }); it('closes the dropdown', () => { + button.click(); expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled(); }); it('shows the keyboard shortcuts modal', () => { - expect(window.toggleShortcutsHelp).toHaveBeenCalled(); + // This relies on the event delegation set up by the Shortcuts class in + // ~/behaviors/shortcuts/shortcuts.js. + expect(button.classList.contains('js-shortcuts-modal-trigger')).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js index c8351ed61d7..c891c4700c9 100644 --- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js @@ -1,4 +1,4 @@ -import { GlDropdownItem, GlDropdown } from '@gitlab/ui'; +import { GlDropdownItem, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; @@ -9,7 +9,9 @@ describe('Deploy freeze timezone dropdown', () => { let wrapper; let store; - const createComponent = (searchTerm, selectedTimezone) => { + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = async (searchTerm, selectedTimezone) => { wrapper = shallowMountExtended(TimezoneDropdown, { store, propsData: { @@ -19,9 +21,8 @@ describe('Deploy freeze timezone dropdown', () => { }, }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ searchTerm }); + findSearchBox().vm.$emit('input', searchTerm); + await nextTick(); }; const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); @@ -35,8 +36,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('No time zones found', () => { - beforeEach(() => { - createComponent('UTC timezone'); + beforeEach(async () => { + await createComponent('UTC timezone'); }); it('renders empty results message', () => { @@ -45,8 +46,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Search term is empty', () => { - beforeEach(() => { - createComponent(''); + beforeEach(async () => { + await createComponent(''); }); it('renders all timezones when search term is empty', () => { @@ -55,8 +56,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Time zones found', () => { - beforeEach(() => { - createComponent('Alaska'); + beforeEach(async () => { + await createComponent('Alaska'); }); it('renders only the time zone searched for', () => { @@ -87,8 +88,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Selected time zone not found', () => { - beforeEach(() => { - createComponent('', 'Berlin'); + beforeEach(async () => { + await createComponent('', 'Berlin'); }); it('renders empty selections', () => { @@ -101,8 +102,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Selected time zone found', () => { - beforeEach(() => { - createComponent('', 'Europe/Berlin'); + beforeEach(async () => { + await createComponent('', 'Europe/Berlin'); }); it('renders selected time zone as dropdown label', () => { diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 64a7502671e..38e1903ad47 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -420,6 +420,12 @@ describe('WorkItemDetail component', () => { expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName); }); + it('shows parent title and iid', () => { + expect(findParentButton().text()).toBe( + `${mockParent.parent.title} #${mockParent.parent.iid}`, + ); + }); + it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => { expect(findParentButton().attributes().href).toBe('../../issues/5'); }); @@ -441,6 +447,11 @@ describe('WorkItemDetail component', () => { expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl); }); + + it('shows work item type and iid', () => { + const { iid, workItemType } = workItemQueryResponse.data.workItem; + expect(findParent().text()).toContain(`${workItemType.name} #${iid}`); + }); }); }); diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb index 983fc19ec0f..c84e04a738f 100644 --- a/spec/lib/sidebars/menu_spec.rb +++ b/spec/lib/sidebars/menu_spec.rb @@ -4,20 +4,14 @@ require 'spec_helper' RSpec.describe Sidebars::Menu, feature_category: :navigation do let(:menu) { described_class.new(context) } - let(:context) do - Sidebars::Context.new(current_user: nil, container: nil, route_is_active: ->(x) { x[:controller] == 'fooc' }) - end - - let(:menu_item) do - Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: { controller: 'fooc' }) - end + let(:context) { Sidebars::Context.new(current_user: nil, container: nil) } let(:nil_menu_item) { Sidebars::NilMenuItem.new(item_id: :foo) } describe '#all_active_routes' do it 'gathers all active routes of items and the current menu' do menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w(bar test) })) - menu.add_item(menu_item) + menu.add_item(Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: { controller: 'fooc' })) menu.add_item(Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' })) menu.add_item(nil_menu_item) @@ -29,39 +23,36 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do end describe '#serialize_for_super_sidebar' do - it 'returns itself and all renderable menu entries' do - menu.add_item(menu_item) - menu.add_item(Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' })) + it 'returns a tree-like structure of itself and all menu items' do + menu.add_item(Sidebars::MenuItem.new(title: 'Is active', link: 'foo2', active_routes: { controller: 'fooc' })) + menu.add_item(Sidebars::MenuItem.new(title: 'Not active', link: 'foo3', active_routes: { controller: 'barc' })) menu.add_item(nil_menu_item) + allow(context).to receive(:route_is_active).and_return(->(x) { x[:controller] == 'fooc' }) allow(menu).to receive(:title).and_return('Title') allow(menu).to receive(:active_routes).and_return({ path: 'foo' }) - allow(menu).to receive(:object_id).and_return(31) - expect(menu.serialize_for_super_sidebar).to eq([ + expect(menu.serialize_for_super_sidebar).to eq( { - id: 31, - parent_id: nil, title: "Title", icon: nil, link: "foo2", - is_active: false - }, - { - parent_id: 31, - title: "foo2", - icon: nil, - link: "foo2", - is_active: true - }, - { - parent_id: 31, - title: "foo3", - icon: nil, - link: "foo3", - is_active: false - } - ]) + is_active: true, + items: [ + { + title: "Is active", + icon: nil, + link: "foo2", + is_active: true + }, + { + title: "Not active", + icon: nil, + link: "foo3", + is_active: false + } + ] + }) end end diff --git a/spec/lib/sidebars/panel_spec.rb b/spec/lib/sidebars/panel_spec.rb index faba356ac70..2c1b9c73595 100644 --- a/spec/lib/sidebars/panel_spec.rb +++ b/spec/lib/sidebars/panel_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Sidebars::Panel, feature_category: :navigation do let(:panel) { Sidebars::Panel.new(context) } let(:menu1) { Sidebars::Menu.new(context) } let(:menu2) { Sidebars::Menu.new(context) } + let(:menu3) { Sidebars::Menu.new(context) } describe '#renderable_menus' do it 'returns only renderable menus' do @@ -21,57 +22,21 @@ RSpec.describe Sidebars::Panel, feature_category: :navigation do end describe '#super_sidebar_menu_items' do - it "groups items under their parent and marks parent as active if a child item is active" do + it "serializes every renderable menu and returns a flattened result" do panel.add_menu(menu1) panel.add_menu(menu2) + panel.add_menu(menu3) allow(menu1).to receive(:render?).and_return(true) + allow(menu1).to receive(:serialize_for_super_sidebar).and_return("foo") + allow(menu2).to receive(:render?).and_return(false) - allow(menu1).to receive(:serialize_for_super_sidebar).and_return([ - { - id: 31, - parent_id: nil, - title: "Title", - is_active: false - }, - { - parent_id: "non_existent_which_makes_this_top_level", - title: "Title 2", - is_active: false - }, - { - parent_id: 31, - title: "Title > Item 1", - is_active: true - }, - { - parent_id: 31, - title: "Title > Item 2", - is_active: false - } - ]) - - expect(panel.super_sidebar_menu_items).to eq([ - { - id: 31, - title: "Title", - is_active: true, - items: [ - { - title: "Title > Item 1", - is_active: true - }, - { - title: "Title > Item 2", - is_active: false - } - ] - }, - { - title: "Title 2", - is_active: false - } - ]) + allow(menu2).to receive(:serialize_for_super_sidebar).and_return("i-should-not-appear-in-results") + + allow(menu3).to receive(:render?).and_return(true) + allow(menu3).to receive(:serialize_for_super_sidebar).and_return(%w[bar baz]) + + expect(panel.super_sidebar_menu_items).to eq(%w[foo bar baz]) end end diff --git a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb b/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb deleted file mode 100644 index bb25d7d1665..00000000000 --- a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Ci::Partitionable::PartitionedFilter, :aggregate_failures, feature_category: :continuous_integration do - before do - create_tables(<<~SQL) - CREATE TABLE _test_ci_jobs_metadata ( - id serial NOT NULL, - partition_id int NOT NULL DEFAULT 10, - name text, - PRIMARY KEY (id, partition_id) - ) PARTITION BY LIST(partition_id); - - CREATE TABLE _test_ci_jobs_metadata_1 - PARTITION OF _test_ci_jobs_metadata - FOR VALUES IN (10); - SQL - end - - let(:model) do - Class.new(Ci::ApplicationRecord) do - include Ci::Partitionable::PartitionedFilter - - self.primary_key = :id - self.table_name = :_test_ci_jobs_metadata - - def self.name - 'TestCiJobMetadata' - end - end - end - - let!(:record) { model.create! } - - let(:where_filter) do - /WHERE "_test_ci_jobs_metadata"."id" = #{record.id} AND "_test_ci_jobs_metadata"."partition_id" = 10/ - end - - describe '#save' do - it 'uses id and partition_id' do - record.name = 'test' - recorder = ActiveRecord::QueryRecorder.new { record.save! } - - expect(recorder.log).to include(where_filter) - expect(record.name).to eq('test') - end - end - - describe '#update' do - it 'uses id and partition_id' do - recorder = ActiveRecord::QueryRecorder.new { record.update!(name: 'test') } - - expect(recorder.log).to include(where_filter) - expect(record.name).to eq('test') - end - end - - describe '#delete' do - it 'uses id and partition_id' do - recorder = ActiveRecord::QueryRecorder.new { record.delete } - - expect(recorder.log).to include(where_filter) - expect(model.count).to be_zero - end - end - - describe '#destroy' do - it 'uses id and partition_id' do - recorder = ActiveRecord::QueryRecorder.new { record.destroy! } - - expect(recorder.log).to include(where_filter) - expect(model.count).to be_zero - end - end - - def create_tables(table_sql) - Ci::ApplicationRecord.connection.execute(table_sql) - end -end diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb index 430ef57d493..f3d33c971c7 100644 --- a/spec/models/concerns/ci/partitionable_spec.rb +++ b/spec/models/concerns/ci/partitionable_spec.rb @@ -40,28 +40,4 @@ RSpec.describe Ci::Partitionable do it { expect(ci_model.ancestors).to include(described_class::Switch) } end - - context 'with partitioned options' do - before do - stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name]) - - ci_model.include(described_class) - ci_model.partitionable scope: ->(r) { 1 }, partitioned: partitioned - end - - context 'when partitioned is true' do - let(:partitioned) { true } - - it { expect(ci_model.ancestors).to include(described_class::PartitionedFilter) } - it { expect(ci_model).to be_partitioned } - end - - context 'when partitioned is false' do - let(:partitioned) { false } - - it { expect(ci_model.ancestors).not_to include(described_class::PartitionedFilter) } - - it { expect(ci_model).not_to be_partitioned } - end - end end diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb index 1a57f556895..b2a52c8aaf0 100644 --- a/spec/models/integrations/apple_app_store_spec.rb +++ b/spec/models/integrations/apple_app_store_spec.rb @@ -12,6 +12,7 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do it { is_expected.to validate_presence_of :app_store_issuer_id } it { is_expected.to validate_presence_of :app_store_key_id } it { is_expected.to validate_presence_of :app_store_private_key } + it { is_expected.to validate_presence_of :app_store_private_key_file_name } it { is_expected.to allow_value('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee').for(:app_store_issuer_id) } it { is_expected.not_to allow_value('abcde').for(:app_store_issuer_id) } it { is_expected.to allow_value(File.read('spec/fixtures/ssl_key.pem')).for(:app_store_private_key) } @@ -28,8 +29,8 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do describe '#fields' do it 'returns custom fields' do - expect(apple_app_store_integration.fields.pluck(:name)).to eq(%w[app_store_issuer_id app_store_key_id - app_store_private_key]) + expect(apple_app_store_integration.fields.pluck(:name)).to match_array(%w[app_store_issuer_id app_store_key_id + app_store_private_key app_store_private_key_file_name]) end end diff --git a/spec/services/clusters/agent_tokens/create_service_spec.rb b/spec/services/clusters/agent_tokens/create_service_spec.rb index dc7abd1504b..519a3ba7ce5 100644 --- a/spec/services/clusters/agent_tokens/create_service_spec.rb +++ b/spec/services/clusters/agent_tokens/create_service_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -RSpec.describe Clusters::AgentTokens::CreateService do - subject(:service) { described_class.new(container: project, current_user: user, params: params) } +RSpec.describe Clusters::AgentTokens::CreateService, feature_category: :kubernetes_management do + subject(:service) { described_class.new(agent: cluster_agent, current_user: user, params: params) } let_it_be(:user) { create(:user) } let(:cluster_agent) { create(:cluster_agent) } let(:project) { cluster_agent.project } - let(:params) { { agent_id: cluster_agent.id, description: 'token description', name: 'token name' } } + let(:params) { { description: 'token description', name: 'token name' } } describe '#execute' do subject { service.execute } @@ -75,7 +75,7 @@ RSpec.describe Clusters::AgentTokens::CreateService do it 'returns validation errors', :aggregate_failures do expect(subject.status).to eq(:error) - expect(subject.message).to eq(["Agent must exist", "Name can't be blank"]) + expect(subject.message).to eq(["Name can't be blank"]) end end end diff --git a/spec/services/clusters/agent_tokens/revoke_service_spec.rb b/spec/services/clusters/agent_tokens/revoke_service_spec.rb new file mode 100644 index 00000000000..24485a5f66d --- /dev/null +++ b/spec/services/clusters/agent_tokens/revoke_service_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::AgentTokens::RevokeService, feature_category: :kubernetes_management do + describe '#execute' do + let(:agent) { create(:cluster_agent) } + let(:agent_token) { create(:cluster_agent_token, agent: agent) } + let(:project) { agent.project } + let(:user) { agent.created_by_user } + + before do + project.add_maintainer(user) + end + + context 'when user is authorized' do + before do + project.add_maintainer(user) + end + + context 'when user revokes agent token' do + it 'succeeds' do + described_class.new(token: agent_token, current_user: user).execute + + expect(agent_token.revoked?).to be true + end + end + + context 'when there is a validation failure' do + before do + agent_token.name = '' # make the record invalid, as we require a name to be present + end + + it 'fails without raising an error', :aggregate_failures do + result = described_class.new(token: agent_token, current_user: user).execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(["Name can't be blank"]) + end + end + end + + context 'when user is not authorized' do + let(:unauthorized_user) { create(:user) } + + before do + project.add_guest(unauthorized_user) + end + + context 'when user attempts to revoke agent token' do + it 'fails' do + described_class.new(token: agent_token, current_user: unauthorized_user).execute + + expect(agent_token.revoked?).to be false + end + end + end + end +end diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb index bf5158c9a92..178dad627ed 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -74,6 +74,8 @@ Integration.available_integration_names.each do |integration| hash.merge!(k => File.read('spec/fixtures/ssl_key.pem')) elsif integration == 'apple_app_store' && k == :app_store_key_id hash.merge!(k => 'ABC1') + elsif integration == 'apple_app_store' && k == :app_store_private_key_file_name + hash.merge!(k => 'ssl_key.pem') else hash.merge!(k => "someword") end diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index b59f3f1e27b..239fd65c60a 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -36,7 +36,7 @@ RSpec.shared_examples 'manage applications' do validate_application(application_name_changed, 'No') expect(page).not_to have_link('Continue') - expect(page).to have_content _('The secret is only available when you first create the application') + expect(page).to have_content _('The secret is only available when you create the application or renew the secret.') visit_applications_path |