diff options
Diffstat (limited to 'spec')
17 files changed, 800 insertions, 26 deletions
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index e0b61526ba0..0f6845dc5ee 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -7,7 +7,7 @@ FactoryBot.define do project trait :token do - token { SecureRandom.hex(10) } + token { generate(:token) } end trait :all_events_enabled do diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb index c10fab8588d..fd7f9223965 100644 --- a/spec/factories/sequences.rb +++ b/spec/factories/sequences.rb @@ -22,4 +22,5 @@ FactoryBot.define do sequence(:job_name) { |n| "job #{n}" } sequence(:work_item_type_name) { |n| "bug#{n}" } sequence(:short_text) { |n| "someText#{n}" } + sequence(:token) { SecureRandom.hex(10) } end diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json index e721525f00c..12dbabf833b 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/project.json +++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json @@ -2361,14 +2361,53 @@ "releases": [ { "id": 1, + "tag": "release-1.0", + "description": "Some release notes", + "project_id": 5, + "created_at": "2019-12-25T10:17:14.621Z", + "updated_at": "2019-12-25T10:17:14.621Z", + "author_id": null, + "name": "release-1.0", + "sha": "902de3a8bd5573f4a049b1457d28bc1592baaa2e", + "released_at": "2019-12-25T10:17:14.615Z", + "links": [ + { + "id": 1, + "release_id": 1, + "url": "http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download", + "name": "release-1.0.dmg", + "created_at": "2019-12-25T10:17:14.621Z", + "updated_at": "2019-12-25T10:17:14.621Z" + } + ], + "milestone_releases": [ + { + "milestone_id": 1349, + "release_id": 9172, + "milestone": { + "id": 1, + "title": "test milestone", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1 + } + } + ] + }, + { + "id": 2, "tag": "release-1.1", "description": "Some release notes", "project_id": 5, "created_at": "2019-12-26T10:17:14.621Z", "updated_at": "2019-12-26T10:17:14.621Z", - "author_id": 1, + "author_id": 16, "name": "release-1.1", - "sha": "901de3a8bd5573f4a049b1457d28bc1592ba6bf9", + "sha": "902de3a8bd5573f4a049b1457d28bc1592ba6bg9", "released_at": "2019-12-26T10:17:14.615Z", "links": [ { @@ -2397,6 +2436,45 @@ } } ] + }, + { + "id": 3, + "tag": "release-1.2", + "description": "Some release notes", + "project_id": 5, + "created_at": "2019-12-27T10:17:14.621Z", + "updated_at": "2019-12-27T10:17:14.621Z", + "author_id": 1, + "name": "release-1.2", + "sha": "903de3a8bd5573f4a049b1457d28bc1592ba6bf9", + "released_at": "2019-12-27T10:17:14.615Z", + "links": [ + { + "id": 1, + "release_id": 1, + "url": "http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download", + "name": "release-1.2.dmg", + "created_at": "2019-12-27T10:17:14.621Z", + "updated_at": "2019-12-27T10:17:14.621Z" + } + ], + "milestone_releases": [ + { + "milestone_id": 1349, + "release_id": 9172, + "milestone": { + "id": 1, + "title": "test milestone", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1 + } + } + ] } ], "project_members": [ diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson index a194898cb5a..dfbde1f2598 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson +++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson @@ -1 +1,3 @@ -{"id":1,"tag":"release-1.1","description":"Some release notes","project_id":5,"created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z","author_id":1,"name":"release-1.1","sha":"901de3a8bd5573f4a049b1457d28bc1592ba6bf9","released_at":"2019-12-26T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.1.dmg","created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]} +{"id":1,"tag":"release-1.0","description":"Some release notes","project_id":5,"created_at":"2019-12-25T10:17:14.621Z","updated_at":"2019-12-25T10:17:14.621Z","author_id":null,"name":"release-1.0","sha":"901de3a8bd5573f4a049b1457d28bc1592baaa2e","released_at":"2019-12-25T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.0.dmg","created_at":"2019-12-25T10:17:14.621Z","updated_at":"2019-12-25T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]} +{"id":2,"tag":"release-1.1","description":"Some release notes","project_id":5,"created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z","author_id":16,"name":"release-1.1","sha":"902de3a8bd5573f4a049b1457d28bc1592ba6bg9","released_at":"2019-12-26T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.1.dmg","created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]} +{"id":3,"tag":"release-1.2","description":"Some release notes","project_id":5,"created_at":"2019-12-27T10:17:14.621Z","updated_at":"2019-12-27T10:17:14.621Z","author_id":1,"name":"release-1.2","sha":"903de3a8bd5573f4a049b1457d28bc1592ba6bf9","released_at":"2019-12-27T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.2.dmg","created_at":"2019-12-27T10:17:14.621Z","updated_at":"2019-12-27T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]} diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index b8a2da4fa47..8ee57f97754 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -332,7 +332,7 @@ describe('Description component', () => { findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); - expect($toast.show).toHaveBeenCalledWith('Work item deleted'); + expect($toast.show).toHaveBeenCalledWith('Task deleted'); }); }); diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js index 34784a88ba9..3eaf06e0656 100644 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -30,10 +30,16 @@ import * as Api from '~/pipelines/components/graph_shared/api'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import * as parsingUtils from '~/pipelines/components/parsing_utils'; import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; +import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql'; import * as sentryUtils from '~/pipelines/utils'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { mockRunningPipelineHeaderData } from '../mock_data'; -import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data'; +import { + mapCallouts, + mockCalloutsResponse, + mockPipelineResponse, + mockPerformanceInsightsResponse, +} from './mock_data'; const defaultProvide = { graphqlResourceEtag: 'frog/amphibirama/etag/', @@ -89,11 +95,15 @@ describe('Pipeline graph wrapper', () => { const callouts = mapCallouts(calloutsList); const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)); const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData); + const getPerformanceInsightsHandler = jest + .fn() + .mockResolvedValue(mockPerformanceInsightsResponse); const requestHandlers = [ [getPipelineHeaderData, getPipelineHeaderDataHandler], [getPipelineDetails, getPipelineDetailsHandler], [getUserCallouts, getUserCalloutsHandler], + [getPerformanceInsights, getPerformanceInsightsHandler], ]; const apolloProvider = createMockApollo(requestHandlers); diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js index 4e79c7e73cc..1397500bdc7 100644 --- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js +++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js @@ -1,10 +1,19 @@ import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants'; import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql'; +import { mockPerformanceInsightsResponse } from './mock_data'; + +Vue.use(VueApollo); describe('the graph view selector component', () => { let wrapper; + let trackingSpy; const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]'); const findViewTypeSelector = () => wrapper.findComponent(GlButtonGroup); @@ -13,11 +22,13 @@ describe('the graph view selector component', () => { const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]'); const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon); const findHoverTip = () => wrapper.findComponent(GlAlert); + const findPipelineInsightsBtn = () => wrapper.find('[data-testid="pipeline-insights-btn"]'); const defaultProps = { showLinks: false, tipPreviouslyDismissed: false, type: STAGE_VIEW, + isPipelineComplete: true, }; const defaultData = { @@ -27,6 +38,14 @@ describe('the graph view selector component', () => { showLinksActive: false, }; + const getPerformanceInsightsHandler = jest + .fn() + .mockResolvedValue(mockPerformanceInsightsResponse); + + const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]]; + + const apolloProvider = createMockApollo(requestHandlers); + const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => { wrapper = mountFn(GraphViewSelector, { propsData: { @@ -39,6 +58,7 @@ describe('the graph view selector component', () => { ...data, }; }, + apolloProvider, }); }; @@ -202,5 +222,44 @@ describe('the graph view selector component', () => { expect(findHoverTip().exists()).toBe(false); }); }); + + describe('pipeline insights', () => { + it.each` + isPipelineComplete | shouldShow + ${true} | ${true} + ${false} | ${false} + `( + 'button should display $shouldShow if isPipelineComplete is $isPipelineComplete ', + ({ isPipelineComplete, shouldShow }) => { + createComponent({ + props: { + isPipelineComplete, + }, + }); + + expect(findPipelineInsightsBtn().exists()).toBe(shouldShow); + }, + ); + }); + + describe('tracking', () => { + beforeEach(() => { + createComponent(); + + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks performance insights button click', () => { + findPipelineInsightsBtn().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_insights_button', { + label: 'performance_insights', + }); + }); + }); }); }); diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index 6124d67af09..959bbcefc98 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -1038,3 +1038,245 @@ export const triggerJob = { action: null, }, }; + +export const mockPerformanceInsightsResponse = { + data: { + project: { + __typename: 'Project', + id: 'gid://gitlab/Project/20', + pipeline: { + __typename: 'Pipeline', + id: 'gid://gitlab/Ci::Pipeline/97', + jobs: { + __typename: 'CiJobConnection', + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + }, + nodes: [ + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Bridge/2502', + duration: null, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2502-2502', + detailsPath: '/root/lots-of-jobs-project/-/pipelines/98', + }, + name: 'trigger_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/303', + name: 'deploy', + }, + startedAt: null, + queuedDuration: 424850.376278, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2501', + duration: 10, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2501-2501', + detailsPath: '/root/ci-project/-/jobs/2501', + }, + name: 'artifact_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/303', + name: 'deploy', + }, + startedAt: '2022-07-01T16:31:41Z', + queuedDuration: 2.621553, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2500', + duration: 4, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2500-2500', + detailsPath: '/root/ci-project/-/jobs/2500', + }, + name: 'coverage_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/302', + name: 'test', + }, + startedAt: '2022-07-01T16:31:33Z', + queuedDuration: 14.388869, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2499', + duration: 4, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2499-2499', + detailsPath: '/root/ci-project/-/jobs/2499', + }, + name: 'test_job_two', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/302', + name: 'test', + }, + startedAt: '2022-07-01T16:31:28Z', + queuedDuration: 15.792664, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2498', + duration: 4, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2498-2498', + detailsPath: '/root/ci-project/-/jobs/2498', + }, + name: 'test_job_one', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/302', + name: 'test', + }, + startedAt: '2022-07-01T16:31:17Z', + queuedDuration: 8.317072, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2497', + duration: 5, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'failed-2497-2497', + detailsPath: '/root/ci-project/-/jobs/2497', + }, + name: 'allow_failure_test_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/302', + name: 'test', + }, + startedAt: '2022-07-01T16:31:22Z', + queuedDuration: 3.547553, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2496', + duration: null, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'manual-2496-2496', + detailsPath: '/root/ci-project/-/jobs/2496', + }, + name: 'test_manual_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/302', + name: 'test', + }, + startedAt: null, + queuedDuration: null, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2495', + duration: 5, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2495-2495', + detailsPath: '/root/ci-project/-/jobs/2495', + }, + name: 'large_log_output', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/301', + name: 'build', + }, + startedAt: '2022-07-01T16:31:11Z', + queuedDuration: 79.128625, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2494', + duration: 5, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2494-2494', + detailsPath: '/root/ci-project/-/jobs/2494', + }, + name: 'build_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/301', + name: 'build', + }, + startedAt: '2022-07-01T16:31:05Z', + queuedDuration: 73.286895, + }, + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Build/2493', + duration: 16, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2493-2493', + detailsPath: '/root/ci-project/-/jobs/2493', + }, + name: 'wait_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/301', + name: 'build', + }, + startedAt: '2022-07-01T16:30:48Z', + queuedDuration: 56.258856, + }, + ], + }, + }, + }, + }, +}; + +export const mockPerformanceInsightsNextPageResponse = { + data: { + project: { + __typename: 'Project', + id: 'gid://gitlab/Project/20', + pipeline: { + __typename: 'Pipeline', + id: 'gid://gitlab/Ci::Pipeline/97', + jobs: { + __typename: 'CiJobConnection', + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + }, + nodes: [ + { + __typename: 'CiJob', + id: 'gid://gitlab/Ci::Bridge/2502', + duration: null, + detailedStatus: { + __typename: 'DetailedStatus', + id: 'success-2502-2502', + detailsPath: '/root/lots-of-jobs-project/-/pipelines/98', + }, + name: 'trigger_job', + stage: { + __typename: 'CiStage', + id: 'gid://gitlab/Ci::Stage/303', + name: 'deploy', + }, + startedAt: null, + queuedDuration: 424850.376278, + }, + ], + }, + }, + }, + }, +}; diff --git a/spec/frontend/pipelines/performance_insights_modal_spec.js b/spec/frontend/pipelines/performance_insights_modal_spec.js new file mode 100644 index 00000000000..b745eb1d78e --- /dev/null +++ b/spec/frontend/pipelines/performance_insights_modal_spec.js @@ -0,0 +1,122 @@ +import { GlAlert, GlLink, GlModal } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import PerformanceInsightsModal from '~/pipelines/components/performance_insights_modal.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { trimText } from 'helpers/text_helper'; +import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql'; +import { + mockPerformanceInsightsResponse, + mockPerformanceInsightsNextPageResponse, +} from './graph/mock_data'; + +Vue.use(VueApollo); + +describe('Performance insights modal', () => { + let wrapper; + + const findModal = () => wrapper.findComponent(GlModal); + const findAlert = () => wrapper.findComponent(GlAlert); + const findLink = () => wrapper.findComponent(GlLink); + const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data'); + const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link'); + const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data'); + const findExecutedCardLink = () => wrapper.findByTestId('insights-executed-card-link'); + const findSlowJobsStage = (index) => wrapper.findAllByTestId('insights-slow-job-stage').at(index); + const findSlowJobsLink = (index) => wrapper.findAllByTestId('insights-slow-job-link').at(index); + + const getPerformanceInsightsHandler = jest + .fn() + .mockResolvedValue(mockPerformanceInsightsResponse); + + const getPerformanceInsightsNextPageHandler = jest + .fn() + .mockResolvedValue(mockPerformanceInsightsNextPageResponse); + + const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]]; + + const createComponent = (handlers = requestHandlers) => { + wrapper = shallowMountExtended(PerformanceInsightsModal, { + provide: { + pipelineIid: '1', + pipelineProjectPath: 'root/ci-project', + }, + apolloProvider: createMockApollo(handlers), + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('without next page', () => { + beforeEach(async () => { + createComponent(); + + await waitForPromises(); + }); + + it('displays modal', () => { + expect(findModal().exists()).toBe(true); + }); + + it('does not dispaly alert', () => { + expect(findAlert().exists()).toBe(false); + }); + + describe('queued duration card', () => { + it('displays card data', () => { + expect(trimText(findQueuedCardData().text())).toBe('4.9 days'); + }); + it('displays card link', () => { + expect(findQueuedCardLink().attributes('href')).toBe( + '/root/lots-of-jobs-project/-/pipelines/98', + ); + }); + }); + + describe('executed duration card', () => { + it('displays card data', () => { + expect(trimText(findExecutedCardData().text())).toBe('trigger_job'); + }); + it('displays card link', () => { + expect(findExecutedCardLink().attributes('href')).toBe( + '/root/lots-of-jobs-project/-/pipelines/98', + ); + }); + }); + + describe('slow jobs', () => { + it.each` + index | expectedStage | expectedName | expectedLink + ${0} | ${'build'} | ${'wait_job'} | ${'/root/ci-project/-/jobs/2493'} + ${1} | ${'deploy'} | ${'artifact_job'} | ${'/root/ci-project/-/jobs/2501'} + ${2} | ${'test'} | ${'allow_failure_test_job'} | ${'/root/ci-project/-/jobs/2497'} + ${3} | ${'build'} | ${'large_log_output'} | ${'/root/ci-project/-/jobs/2495'} + ${4} | ${'build'} | ${'build_job'} | ${'/root/ci-project/-/jobs/2494'} + `( + 'should display slow job correctly', + ({ index, expectedStage, expectedName, expectedLink }) => { + expect(findSlowJobsStage(index).text()).toBe(expectedStage); + expect(findSlowJobsLink(index).text()).toBe(expectedName); + expect(findSlowJobsLink(index).attributes('href')).toBe(expectedLink); + }, + ); + }); + }); + + describe('limit alert', () => { + it('displays limit alert when there is a next page', async () => { + createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + expect(findLink().attributes('href')).toBe( + 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902', + ); + }); + }); +}); diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js index 1c23a7e4fcf..a82390fae22 100644 --- a/spec/frontend/pipelines/utils_spec.js +++ b/spec/frontend/pipelines/utils_spec.js @@ -8,10 +8,14 @@ import { removeOrphanNodes, getMaxNodes, } from '~/pipelines/components/parsing_utils'; -import { createNodeDict } from '~/pipelines/utils'; +import { createNodeDict, calculateJobStats, calculateSlowestFiveJobs } from '~/pipelines/utils'; import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data'; -import { generateResponse, mockPipelineResponse } from './graph/mock_data'; +import { + generateResponse, + mockPipelineResponse, + mockPerformanceInsightsResponse, +} from './graph/mock_data'; describe('DAG visualization parsing utilities', () => { const nodeDict = createNodeDict(mockParsedGraphQLNodes); @@ -158,4 +162,40 @@ describe('DAG visualization parsing utilities', () => { expect(columns).toMatchSnapshot(); }); }); + + describe('performance insights', () => { + const { + data: { + project: { + pipeline: { jobs }, + }, + }, + } = mockPerformanceInsightsResponse; + + describe('calculateJobStats', () => { + const expectedJob = jobs.nodes[0]; + + it('returns the job that spent this longest time queued', () => { + expect(calculateJobStats(jobs, 'queuedDuration')).toEqual(expectedJob); + }); + + it('returns the job that was executed last', () => { + expect(calculateJobStats(jobs, 'startedAt')).toEqual(expectedJob); + }); + }); + + describe('calculateSlowestFiveJobs', () => { + it('returns the slowest five jobs of the pipeline', () => { + const expectedJobs = [ + jobs.nodes[9], + jobs.nodes[1], + jobs.nodes[5], + jobs.nodes[7], + jobs.nodes[8], + ]; + + expect(calculateSlowestFiveJobs(jobs)).toEqual(expectedJobs); + }); + }); + }); }); diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index f7f5b2ca865..a9fe293dd69 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -143,6 +143,22 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do end end end + + context 'up to 15.1.0' do + let(:available_runner_releases) { %w[14.9.1 14.9.2 14.10.0 14.10.1 15.0.0 15.1.0] } + + context 'with Gitlab::VERSION set to 15.2.0-pre' do + let(:gitlab_version) { '15.2.0-pre' } + + context 'with unknown runner version' do + let(:runner_version) { '14.11.0~beta.29.gd0c550e3' } + + it 'recommends 15.1.0 since 14.11 is an unknown release and 15.1.0 is available' do + is_expected.to eq({ recommended: Gitlab::VersionInfo.new(15, 1, 0) }) + end + end + end + end end end end diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index fac960d8df2..157cd408da9 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -383,21 +383,52 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end end - it 'restores releases with links & milestones' do - release = @project.releases.last - link = release.links.last + context 'restores releases' do + it 'with links & milestones' do + release = @project.releases.last + link = release.links.last + + aggregate_failures do + expect(release.tag).to eq('release-1.2') + expect(release.description).to eq('Some release notes') + expect(release.name).to eq('release-1.2') + expect(release.sha).to eq('903de3a8bd5573f4a049b1457d28bc1592ba6bf9') + expect(release.released_at).to eq('2019-12-27T10:17:14.615Z') + expect(release.milestone_releases.count).to eq(1) + expect(release.milestone_releases.first.milestone.title).to eq('test milestone') + + expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download') + expect(link.name).to eq('release-1.2.dmg') + end + end - aggregate_failures do - expect(release.tag).to eq('release-1.1') - expect(release.description).to eq('Some release notes') - expect(release.name).to eq('release-1.1') - expect(release.sha).to eq('901de3a8bd5573f4a049b1457d28bc1592ba6bf9') - expect(release.released_at).to eq('2019-12-26T10:17:14.615Z') - expect(release.milestone_releases.count).to eq(1) - expect(release.milestone_releases.first.milestone.title).to eq('test milestone') - - expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download') - expect(link.name).to eq('release-1.1.dmg') + context 'with author' do + it 'as ghost user when imported release author is empty' do + release = @project.releases.first + + aggregate_failures do + expect(release.tag).to eq('release-1.0') + expect(release.author_id).to eq(User.select(:id).ghost.id) + end + end + + it 'as existing member when imported release author is matched with existing user' do + release = @project.releases.second + + aggregate_failures do + expect(release.tag).to eq('release-1.1') + expect(release.author_id).to eq(@existing_members.first.id) + end + end + + it 'as import user when imported release author cannot be matched' do + release = @project.releases.last + + aggregate_failures do + expect(release.tag).to eq('release-1.2') + expect(release.author_id).to eq(@user.id) + end + end end end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index fb4d1cee606..fb3968777bf 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -78,6 +78,32 @@ RSpec.describe WebHook do expect(hook.url).to eq('https://example.com') end + + context 'when there are URL variables' do + subject { hook } + + before do + hook.url_variables = { 'one' => 'a', 'two' => 'b' } + end + + it { is_expected.to allow_value('http://example.com').for(:url) } + it { is_expected.to allow_value('http://example.com/{one}/{two}').for(:url) } + it { is_expected.to allow_value('http://example.com/{one}').for(:url) } + it { is_expected.to allow_value('http://example.com/{two}').for(:url) } + it { is_expected.to allow_value('http://user:s3cret@example.com/{two}').for(:url) } + it { is_expected.to allow_value('http://{one}:{two}@example.com').for(:url) } + + it { is_expected.not_to allow_value('http://example.com/{one}/{two}/{three}').for(:url) } + it { is_expected.not_to allow_value('http://example.com/{foo}').for(:url) } + it { is_expected.not_to allow_value('http:{user}:{pwd}//example.com/{foo}').for(:url) } + + it 'mentions all missing variable names' do + hook.url = 'http://example.com/{one}/{foo}/{two}/{three}' + + expect(hook).to be_invalid + expect(hook.errors[:url].to_sentence).to eq "Invalid URL template. Missing keys: [\"foo\", \"three\"]" + end + end end describe 'token' do @@ -559,4 +585,54 @@ RSpec.describe WebHook do expect(hook.to_json(unsafe_serialization_hash: true)).not_to include('encrypted_url_variables') end end + + describe '#interpolated_url' do + subject(:hook) { build(:project_hook, project: project) } + + context 'when the hook URL does not contain variables' do + before do + hook.url = 'http://example.com' + end + + it { is_expected.to have_attributes(interpolated_url: hook.url) } + end + + it 'is not vulnerable to malicious input' do + hook.url = 'something%{%<foo>2147483628G}' + hook.url_variables = { 'foo' => '1234567890.12345678' } + + expect(hook).to have_attributes(interpolated_url: hook.url) + end + + context 'when the hook URL contains variables' do + before do + hook.url = 'http://example.com/{path}/resource?token={token}' + hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' } + end + + it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') } + + context 'when a variable is missing' do + before do + hook.url_variables = { 'path' => 'present' } + end + + it 'raises an error' do + # We expect validations to prevent this entirely - this is not user-error + expect { hook.interpolated_url } + .to raise_error(described_class::InterpolationError, include('Missing key token')) + end + end + + context 'when the URL appears to include percent formatting' do + before do + hook.url = 'http://example.com/%{path}/resource?token=%{token}' + end + + it 'succeeds, interpolates correctly' do + expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz' + end + end + end + end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 79edfdd2b3f..2bd23340a88 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -310,6 +310,26 @@ RSpec.describe "Authentication", "routing" do expect(post("/users/auth/ldapmain/callback")).not_to be_routable end end + + context 'with multiple LDAP providers configured' do + let(:ldap_settings) do + { + enabled: true, + servers: { + main: { 'provider_name' => 'ldapmain' }, + secondary: { 'provider_name' => 'ldapsecondary' } + } + } + end + + it 'POST /users/auth/ldapmain/callback' do + expect(post("/users/auth/ldapmain/callback")).to route_to('ldap/omniauth_callbacks#ldapmain') + end + + it 'POST /users/auth/ldapsecondary/callback' do + expect(post("/users/auth/ldapsecondary/callback")).to route_to('ldap/omniauth_callbacks#ldapsecondary') + end + end end end diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 068550ec234..339ffc44e4d 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -84,8 +84,74 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid) end + context 'when there is an interpolation error' do + let(:error) { ::WebHook::InterpolationError.new('boom') } + + before do + stub_full_request(project_hook.url, method: :post) + allow(project_hook).to receive(:interpolated_url).and_raise(error) + end + + it 'logs the error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error) + + expect(service_instance).to receive(:log_execution).with( + execution_duration: (be > 0), + response: have_attributes(code: 200) + ) + + service_instance.execute + end + end + + context 'when there are URL variables' do + before do + project_hook.update!( + url: 'http://example.com/{one}/{two}', + url_variables: { 'one' => 'a', 'two' => 'b' } + ) + end + + it 'POSTs to the interpolated URL, and logs the hook.url' do + stub_full_request(project_hook.interpolated_url, method: :post) + + expect(service_instance).to receive(:queue_log_execution_with_retry).with( + include(url: project_hook.url), + :ok + ) + + service_instance.execute + + expect(WebMock) + .to have_requested(:post, stubbed_hostname(project_hook.interpolated_url)).once + end + + context 'there is userinfo' do + before do + project_hook.update!(url: 'http://{one}:{two}@example.com') + stub_full_request('http://example.com', method: :post) + end + + it 'POSTs to the interpolated URL, and logs the hook.url' do + expect(service_instance).to receive(:queue_log_execution_with_retry).with( + include(url: project_hook.url), + :ok + ) + + service_instance.execute + + expect(WebMock) + .to have_requested(:post, stubbed_hostname('http://example.com')) + .with(headers: headers.merge('Authorization' => 'Basic YTpi')) + .once + end + end + end + context 'when token is defined' do - let_it_be(:project_hook) { create(:project_hook, :token) } + before do + project_hook.token = generate(:token) + end it 'POSTs to the webhook URL' do stub_full_request(project_hook.url, method: :post) diff --git a/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb index 8635c9a8ff9..b31fe9ee0d1 100644 --- a/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb +++ b/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb @@ -14,6 +14,8 @@ RSpec.shared_context 'Ldap::OmniauthCallbacksController' do { main: ldap_config_defaults(:main) } end + let(:multiple_ldap_servers_license_available) { true } + def ldap_config_defaults(key, hash = {}) { provider_name: "ldap#{key}", @@ -23,6 +25,7 @@ RSpec.shared_context 'Ldap::OmniauthCallbacksController' do end before do + stub_licensed_features(multiple_ldap_servers: multiple_ldap_servers_license_available) stub_ldap_setting(ldap_settings) described_class.define_providers! Rails.application.reload_routes! diff --git a/spec/views/layouts/_flash.html.haml_spec.rb b/spec/views/layouts/_flash.html.haml_spec.rb index 82c06feb4fb..a4bed09368f 100644 --- a/spec/views/layouts/_flash.html.haml_spec.rb +++ b/spec/views/layouts/_flash.html.haml_spec.rb @@ -9,7 +9,11 @@ RSpec.describe 'layouts/_flash' do end describe 'closable flash messages' do - %w(alert notice success).each do |flash_type| + where(:flash_type) do + %w[alert notice success] + end + + with_them do let(:flash) { { flash_type => 'This is a closable flash message' } } it 'shows a close button' do @@ -19,10 +23,14 @@ RSpec.describe 'layouts/_flash' do end describe 'non closable flash messages' do - %w(error message toast warning).each do |flash_type| + where(:flash_type) do + %w[error message toast warning] + end + + with_them do let(:flash) { { flash_type => 'This is a non closable flash message' } } - it 'shows a close button' do + it 'does not show a close button' do expect(rendered).not_to include('js-close-icon') end end |