summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/__helpers__/test_apollo_link.js46
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js126
-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.js182
-rw-r--r--spec/frontend/lib/apollo/instrumentation_link_spec.js54
-rw-r--r--spec/models/namespace_setting_spec.rb10
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
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
}