diff options
Diffstat (limited to 'spec')
-rw-r--r-- | spec/frontend/__helpers__/test_apollo_link.js | 46 | ||||
-rw-r--r-- | spec/frontend/jobs/components/table/cells/actions_cell_spec.js | 126 | ||||
-rw-r--r-- | spec/frontend/jobs/components/table/cells/duration_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js) | 0 | ||||
-rw-r--r-- | spec/frontend/jobs/components/table/cells/job_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js) | 0 | ||||
-rw-r--r-- | spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js) | 0 | ||||
-rw-r--r-- | spec/frontend/jobs/mock_data.js | 182 | ||||
-rw-r--r-- | spec/frontend/lib/apollo/instrumentation_link_spec.js | 54 | ||||
-rw-r--r-- | spec/models/namespace_setting_spec.rb | 10 | ||||
-rw-r--r-- | spec/workers/every_sidekiq_worker_spec.rb | 1 |
9 files changed, 410 insertions, 9 deletions
diff --git a/spec/frontend/__helpers__/test_apollo_link.js b/spec/frontend/__helpers__/test_apollo_link.js new file mode 100644 index 00000000000..dde3a4e99bb --- /dev/null +++ b/spec/frontend/__helpers__/test_apollo_link.js @@ -0,0 +1,46 @@ +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { ApolloClient } from 'apollo-client'; +import { ApolloLink } from 'apollo-link'; +import gql from 'graphql-tag'; + +const FOO_QUERY = gql` + query { + foo + } +`; + +/** + * This function returns a promise that resolves to the final operation after + * running an ApolloClient query with the given ApolloLink + * + * @typedef {Object} TestApolloLinkOptions + * @property {Object} context the default context object sent along the ApolloLink chain + * + * @param {ApolloLink} subjectLink the ApolloLink which is under test + * @param {TestApolloLinkOptions} options contains options to send a long with the query + * + * @returns Promise resolving to the resulting operation after running the subjectLink + */ +export const testApolloLink = (subjectLink, options = {}) => + new Promise((resolve) => { + const { context = {} } = options; + + // Use the terminating link to capture the final operation and resolve with this. + const terminatingLink = new ApolloLink((operation) => { + resolve(operation); + + return null; + }); + + const client = new ApolloClient({ + link: ApolloLink.from([subjectLink, terminatingLink]), + // cache is a required option + cache: new InMemoryCache(), + }); + + // Trigger a query so the ApolloLink chain will be executed. + client.query({ + context, + query: FOO_QUERY, + }); + }); diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js new file mode 100644 index 00000000000..1b1e2d4df8f --- /dev/null +++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js @@ -0,0 +1,126 @@ +import { GlModal } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue'; +import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql'; +import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql'; +import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql'; +import { playableJob, retryableJob, scheduledJob } from '../../../mock_data'; + +describe('Job actions cell', () => { + let wrapper; + let mutate; + + const findRetryButton = () => wrapper.findByTestId('retry'); + const findPlayButton = () => wrapper.findByTestId('play'); + const findDownloadArtifactsButton = () => wrapper.findByTestId('download-artifacts'); + const findCountdownButton = () => wrapper.findByTestId('countdown'); + const findPlayScheduledJobButton = () => wrapper.findByTestId('play-scheduled'); + const findUnscheduleButton = () => wrapper.findByTestId('unschedule'); + + const findModal = () => wrapper.findComponent(GlModal); + + const MUTATION_SUCCESS = { data: { JobRetryMutation: { jobId: retryableJob.id } } }; + const MUTATION_SUCCESS_UNSCHEDULE = { + data: { JobUnscheduleMutation: { jobId: scheduledJob.id } }, + }; + const MUTATION_SUCCESS_PLAY = { data: { JobPlayMutation: { jobId: playableJob.id } } }; + + const $toast = { + show: jest.fn(), + }; + + const createComponent = (jobType, mutationType = MUTATION_SUCCESS, props = {}) => { + mutate = jest.fn().mockResolvedValue(mutationType); + + wrapper = shallowMountExtended(ActionsCell, { + propsData: { + job: jobType, + ...props, + }, + mocks: { + $apollo: { + mutate, + }, + $toast, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('does not display an artifacts download button', () => { + createComponent(retryableJob); + + expect(findDownloadArtifactsButton().exists()).toBe(false); + }); + + it.each` + button | action | jobType + ${findPlayButton} | ${'play'} | ${playableJob} + ${findRetryButton} | ${'retry'} | ${retryableJob} + ${findDownloadArtifactsButton} | ${'download artifacts'} | ${playableJob} + `('displays the $action button', ({ button, jobType }) => { + createComponent(jobType); + + expect(button().exists()).toBe(true); + }); + + it.each` + button | mutationResult | action | jobType | mutationFile + ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation} + ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation} + `('performs the $action mutation', ({ button, mutationResult, jobType, mutationFile }) => { + createComponent(jobType, mutationResult); + + button().vm.$emit('click'); + + expect(mutate).toHaveBeenCalledWith({ + mutation: mutationFile, + variables: { + id: jobType.id, + }, + }); + }); + + describe('Scheduled Jobs', () => { + const today = () => new Date('2021-08-31'); + + beforeEach(() => { + jest.spyOn(Date, 'now').mockImplementation(today); + }); + + it('displays the countdown, play and unschedule buttons', () => { + createComponent(scheduledJob); + + expect(findCountdownButton().exists()).toBe(true); + expect(findPlayScheduledJobButton().exists()).toBe(true); + expect(findUnscheduleButton().exists()).toBe(true); + }); + + it('unschedules a job', () => { + createComponent(scheduledJob, MUTATION_SUCCESS_UNSCHEDULE); + + findUnscheduleButton().vm.$emit('click'); + + expect(mutate).toHaveBeenCalledWith({ + mutation: JobUnscheduleMutation, + variables: { + id: scheduledJob.id, + }, + }); + }); + + it('shows the play job confirmation modal', async () => { + createComponent(scheduledJob, MUTATION_SUCCESS); + + findPlayScheduledJobButton().vm.$emit('click'); + + await nextTick(); + + expect(findModal().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js index 763a4b0eaa2..763a4b0eaa2 100644 --- a/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js diff --git a/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js index fc4e5586349..fc4e5586349 100644 --- a/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js diff --git a/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js index 1f5e0a7aa21..1f5e0a7aa21 100644 --- a/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 57f0b852ff8..43755b46bc9 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -1555,7 +1555,11 @@ export const mockJobsQueryResponse = { cancelable: false, active: false, stuck: false, - userPermissions: { readBuild: true, __typename: 'JobPermissions' }, + userPermissions: { + readBuild: true, + readJobArtifacts: true, + __typename: 'JobPermissions', + }, __typename: 'CiJob', }, ], @@ -1573,3 +1577,179 @@ export const mockJobsQueryEmptyResponse = { }, }, }; + +export const retryableJob = { + artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' }, + allowFailure: false, + status: 'SUCCESS', + scheduledAt: null, + manualJob: false, + triggered: null, + createdByTag: false, + detailedStatus: { + detailsPath: '/root/test-job-artifacts/-/jobs/1981', + group: 'success', + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + action: { + buttonTitle: 'Retry this job', + icon: 'retry', + method: 'post', + path: '/root/test-job-artifacts/-/jobs/1981/retry', + title: 'Retry', + __typename: 'StatusAction', + }, + __typename: 'DetailedStatus', + }, + id: 'gid://gitlab/Ci::Build/1981', + refName: 'main', + refPath: '/root/test-job-artifacts/-/commits/main', + tags: [], + shortSha: '75daf01b', + commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055', + pipeline: { + id: 'gid://gitlab/Ci::Pipeline/288', + path: '/root/test-job-artifacts/-/pipelines/288', + user: { + webPath: '/root', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + __typename: 'UserCore', + }, + __typename: 'Pipeline', + }, + stage: { name: 'test', __typename: 'CiStage' }, + name: 'hello_world', + duration: 7, + finishedAt: '2021-08-30T20:33:56Z', + coverage: null, + retryable: true, + playable: false, + cancelable: false, + active: false, + stuck: false, + userPermissions: { readBuild: true, __typename: 'JobPermissions' }, + __typename: 'CiJob', +}; + +export const playableJob = { + artifacts: { + nodes: [ + { + downloadPath: '/root/test-job-artifacts/-/jobs/1982/artifacts/download?file_type=trace', + __typename: 'CiJobArtifact', + }, + ], + __typename: 'CiJobArtifactConnection', + }, + allowFailure: false, + status: 'SUCCESS', + scheduledAt: null, + manualJob: true, + triggered: null, + createdByTag: false, + detailedStatus: { + detailsPath: '/root/test-job-artifacts/-/jobs/1982', + group: 'success', + icon: 'status_success', + label: 'manual play action', + text: 'passed', + tooltip: 'passed', + action: { + buttonTitle: 'Trigger this manual action', + icon: 'play', + method: 'post', + path: '/root/test-job-artifacts/-/jobs/1982/play', + title: 'Play', + __typename: 'StatusAction', + }, + __typename: 'DetailedStatus', + }, + id: 'gid://gitlab/Ci::Build/1982', + refName: 'main', + refPath: '/root/test-job-artifacts/-/commits/main', + tags: [], + shortSha: '75daf01b', + commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055', + pipeline: { + id: 'gid://gitlab/Ci::Pipeline/288', + path: '/root/test-job-artifacts/-/pipelines/288', + user: { + webPath: '/root', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + __typename: 'UserCore', + }, + __typename: 'Pipeline', + }, + stage: { name: 'test', __typename: 'CiStage' }, + name: 'hello_world_delayed', + duration: 6, + finishedAt: '2021-08-30T20:36:12Z', + coverage: null, + retryable: true, + playable: true, + cancelable: false, + active: false, + stuck: false, + userPermissions: { readBuild: true, readJobArtifacts: true, __typename: 'JobPermissions' }, + __typename: 'CiJob', +}; + +export const scheduledJob = { + artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' }, + allowFailure: false, + status: 'SCHEDULED', + scheduledAt: '2021-08-31T22:36:05Z', + manualJob: true, + triggered: null, + createdByTag: false, + detailedStatus: { + detailsPath: '/root/test-job-artifacts/-/jobs/1986', + group: 'scheduled', + icon: 'status_scheduled', + label: 'unschedule action', + text: 'delayed', + tooltip: 'delayed manual action (%{remainingTime})', + action: { + buttonTitle: 'Unschedule job', + icon: 'time-out', + method: 'post', + path: '/root/test-job-artifacts/-/jobs/1986/unschedule', + title: 'Unschedule', + __typename: 'StatusAction', + }, + __typename: 'DetailedStatus', + }, + id: 'gid://gitlab/Ci::Build/1986', + refName: 'main', + refPath: '/root/test-job-artifacts/-/commits/main', + tags: [], + shortSha: '75daf01b', + commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055', + pipeline: { + id: 'gid://gitlab/Ci::Pipeline/290', + path: '/root/test-job-artifacts/-/pipelines/290', + user: { + webPath: '/root', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + __typename: 'UserCore', + }, + __typename: 'Pipeline', + }, + stage: { name: 'test', __typename: 'CiStage' }, + name: 'hello_world_delayed', + duration: null, + finishedAt: null, + coverage: null, + retryable: false, + playable: true, + cancelable: false, + active: false, + stuck: false, + userPermissions: { readBuild: true, __typename: 'JobPermissions' }, + __typename: 'CiJob', +}; diff --git a/spec/frontend/lib/apollo/instrumentation_link_spec.js b/spec/frontend/lib/apollo/instrumentation_link_spec.js new file mode 100644 index 00000000000..ef686129257 --- /dev/null +++ b/spec/frontend/lib/apollo/instrumentation_link_spec.js @@ -0,0 +1,54 @@ +import { testApolloLink } from 'helpers/test_apollo_link'; +import { getInstrumentationLink, FEATURE_CATEGORY_HEADER } from '~/lib/apollo/instrumentation_link'; + +const TEST_FEATURE_CATEGORY = 'foo_feature'; + +describe('~/lib/apollo/instrumentation_link', () => { + const setFeatureCategory = (val) => { + window.gon.feature_category = val; + }; + + afterEach(() => { + getInstrumentationLink.cache.clear(); + }); + + describe('getInstrumentationLink', () => { + describe('with no gon.feature_category', () => { + beforeEach(() => { + setFeatureCategory(null); + }); + + it('returns null', () => { + expect(getInstrumentationLink()).toBe(null); + }); + }); + + describe('with gon.feature_category', () => { + beforeEach(() => { + setFeatureCategory(TEST_FEATURE_CATEGORY); + }); + + it('returns memoized apollo link', () => { + const result = getInstrumentationLink(); + + // expect.any(ApolloLink) doesn't work for some reason... + expect(result).toHaveProp('request'); + expect(result).toBe(getInstrumentationLink()); + }); + + it('adds a feature category header from the returned apollo link', async () => { + const defaultHeaders = { Authorization: 'foo' }; + const operation = await testApolloLink(getInstrumentationLink(), { + context: { headers: defaultHeaders }, + }); + + const { headers } = operation.getContext(); + + expect(headers).toEqual({ + ...defaultHeaders, + [FEATURE_CATEGORY_HEADER]: TEST_FEATURE_CATEGORY, + }); + }); + }); + }); +}); diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb index e8ed6f1a460..c1cc8fc3e88 100644 --- a/spec/models/namespace_setting_spec.rb +++ b/spec/models/namespace_setting_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe NamespaceSetting, type: :model do + it_behaves_like 'sanitizable', :namespace_settings, %i[default_branch_name] + # Relationships # describe "Associations" do @@ -41,14 +43,6 @@ RSpec.describe NamespaceSetting, type: :model do it_behaves_like "doesn't return an error" end - - context "when it contains javascript tags" do - it "gets sanitized properly" do - namespace_settings.update!(default_branch_name: "hello<script>alert(1)</script>") - - expect(namespace_settings.default_branch_name).to eq('hello') - end - end end describe '#allow_mfa_for_group' do diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index ea1f0153f83..5220a979426 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -452,6 +452,7 @@ RSpec.describe 'Every Sidekiq worker' do 'WaitForClusterCreationWorker' => 3, 'WebHookWorker' => 4, 'WebHooks::DestroyWorker' => 3, + 'WebHooks::LogExecutionWorker' => 3, 'Wikis::GitGarbageCollectWorker' => false, 'X509CertificateRevokeWorker' => 3 } |