Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json82
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js12
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js59
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js242
-rw-r--r--spec/frontend/pipelines/performance_insights_modal_spec.js122
-rw-r--r--spec/frontend/pipelines/utils_spec.js44
-rw-r--r--spec/lib/gitlab/ci/runner_upgrade_check_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb59
-rw-r--r--spec/models/hooks/web_hook_spec.rb76
-rw-r--r--spec/routing/routing_spec.rb20
-rw-r--r--spec/services/web_hook_service_spec.rb68
-rw-r--r--spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb3
-rw-r--r--spec/views/layouts/_flash.html.haml_spec.rb14
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