summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb17
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb4
-rw-r--r--spec/features/projects/environments/environment_spec.rb10
-rw-r--r--spec/fixtures/api/schemas/deployment.json2
-rw-r--r--spec/frontend/releases/detail/components/app_spec.js70
-rw-r--r--spec/frontend/releases/detail/store/actions_spec.js217
-rw-r--r--spec/frontend/releases/detail/store/mutations_spec.js119
-rw-r--r--spec/helpers/environment_helper_spec.rb25
-rw-r--r--spec/javascripts/test_bundle.js6
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/ansi2json/parser_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/ansi2json/style_spec.rb166
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb544
-rw-r--r--spec/lib/gitlab/diff/position_collection_spec.rb13
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb62
-rw-r--r--spec/lib/gitlab/health_checks/probes/liveness_spec.rb17
-rw-r--r--spec/lib/gitlab/health_checks/probes/readiness_spec.rb39
-rw-r--r--spec/lib/gitlab_spec.rb78
-rw-r--r--spec/models/application_setting_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb13
-rw-r--r--spec/models/environment_spec.rb15
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/requests/api/deployments_spec.rb162
-rw-r--r--spec/requests/api/members_spec.rb29
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/services/deployments/after_create_service_spec.rb (renamed from spec/services/update_deployment_service_spec.rb)2
-rw-r--r--spec/services/deployments/create_service_spec.rb92
-rw-r--r--spec/services/deployments/update_service_spec.rb15
-rw-r--r--spec/services/git/base_hooks_service_spec.rb29
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb54
-rw-r--r--spec/support/features/rss_shared_examples.rb8
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb4
-rw-r--r--spec/workers/deployments/success_worker_spec.rb12
-rw-r--r--spec/workers/post_receive_spec.rb2
34 files changed, 1912 insertions, 124 deletions
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index b9ee69a617b..66112c95742 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -75,15 +75,13 @@ describe Projects::DeploymentsController do
}
end
- before do
+ it 'returns a metrics JSON document' do
expect_next_instance_of(DeploymentMetrics) do |deployment_metrics|
allow(deployment_metrics).to receive(:has_metrics?).and_return(true)
expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics)
end
- end
- it 'returns a metrics JSON document' do
get :metrics, params: deployment_params(id: deployment.to_param)
expect(response).to be_ok
@@ -91,6 +89,19 @@ describe Projects::DeploymentsController do
expect(json_response['metrics']).to eq({})
expect(json_response['last_update']).to eq(42)
end
+
+ it 'returns a 404 if the deployment failed' do
+ failed_deployment = create(
+ :deployment,
+ :failed,
+ project: project,
+ environment: environment
+ )
+
+ get :metrics, params: deployment_params(id: failed_deployment.to_param)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index e677e836145..5c02e8d6461 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -82,9 +82,9 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'when note has no position' do
+ context 'when note is a legacy diff note' do
before do
- create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request, position: nil)
+ create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request)
end
it 'serializes merge request diff collection' do
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 25823b75d18..dd690699ff6 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -66,8 +66,8 @@ describe 'Environment' do
create(:deployment, :running, environment: environment, deployable: build)
end
- it 'does not show deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
end
@@ -79,8 +79,8 @@ describe 'Environment' do
create(:deployment, :failed, environment: environment, deployable: build)
end
- it 'does not show deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
end
@@ -175,7 +175,7 @@ describe 'Environment' do
#
# In EE we have to stub EE::Environment since it overwrites
# the "terminals" method.
- allow_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ allow_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals) { nil }
visit terminal_project_environment_path(project, environment)
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index b1e3c000ddf..0cfeadfe548 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -61,7 +61,7 @@
"type": "array",
"items": { "$ref": "job/job.json" }
},
- "status": { "type": "string" }
+ "status": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/frontend/releases/detail/components/app_spec.js b/spec/frontend/releases/detail/components/app_spec.js
new file mode 100644
index 00000000000..f8eb33a69a8
--- /dev/null
+++ b/spec/frontend/releases/detail/components/app_spec.js
@@ -0,0 +1,70 @@
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+import ReleaseDetailApp from '~/releases/detail/components/app';
+import { release } from '../../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('Release detail component', () => {
+ let wrapper;
+ let releaseClone;
+ let actions;
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+
+ releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
+
+ const state = {
+ release: releaseClone,
+ markdownDocsPath: 'path/to/markdown/docs',
+ };
+
+ actions = {
+ fetchRelease: jest.fn(),
+ updateRelease: jest.fn(),
+ navigateToReleasesPage: jest.fn(),
+ };
+
+ const store = new Vuex.Store({ actions, state });
+
+ wrapper = mount(ReleaseDetailApp, { store });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls fetchRelease when the component is created', () => {
+ expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders the description text at the top of the page', () => {
+ expect(wrapper.find('.js-subtitle-text').text()).toBe(
+ 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
+ );
+ });
+
+ it('renders the correct tag name in the "Tag name" field', () => {
+ expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
+ });
+
+ it('renders the correct release title in the "Release title" field', () => {
+ expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
+ });
+
+ it('renders the release notes in the "Release notes" textarea', () => {
+ expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description);
+ });
+
+ it('renders the "Save changes" button as type="submit"', () => {
+ expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+ });
+
+ it('calls updateRelease when the form is submitted', () => {
+ wrapper.find('form').trigger('submit');
+ expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls navigateToReleasesPage when the "Cancel" button is clicked', () => {
+ wrapper.find('.js-cancel-button').vm.$emit('click');
+ expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/releases/detail/store/actions_spec.js b/spec/frontend/releases/detail/store/actions_spec.js
new file mode 100644
index 00000000000..f1c7f3c1048
--- /dev/null
+++ b/spec/frontend/releases/detail/store/actions_spec.js
@@ -0,0 +1,217 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import * as actions from '~/releases/detail/store/actions';
+import testAction from 'helpers/vuex_action_helper';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+import state from '~/releases/detail/store/state';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+jest.mock('~/flash', () => jest.fn());
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
+}));
+
+describe('Release detail actions', () => {
+ let stateClone;
+ let releaseClone;
+ let mock;
+ let error;
+
+ beforeEach(() => {
+ stateClone = state();
+ releaseClone = JSON.parse(JSON.stringify(release));
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+ error = { message: 'An error occurred' };
+ createFlash.mockClear();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setInitialState', () => {
+ it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
+ const initialState = {};
+
+ return testAction(actions.setInitialState, initialState, stateClone, [
+ { type: types.SET_INITIAL_STATE, payload: initialState },
+ ]);
+ });
+ });
+
+ describe('requestRelease', () => {
+ it(`commits ${types.REQUEST_RELEASE}`, () =>
+ testAction(actions.requestRelease, undefined, stateClone, [{ type: types.REQUEST_RELEASE }]));
+ });
+
+ describe('receiveReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
+ testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [
+ { type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone },
+ ]));
+ });
+
+ describe('receiveReleaseError', () => {
+ it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
+ testAction(actions.receiveReleaseError, error, stateClone, [
+ { type: types.RECEIVE_RELEASE_ERROR, payload: error },
+ ]));
+
+ it('shows a flash with an error message', () => {
+ actions.receiveReleaseError({ commit: jest.fn() }, error);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while getting the release details',
+ );
+ });
+ });
+
+ describe('fetchRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ stateClone.projectId = '18';
+ stateClone.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ });
+
+ it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
+ mock.onGet(getReleaseUrl).replyOnce(200, releaseClone);
+
+ return testAction(
+ actions.fetchRelease,
+ undefined,
+ stateClone,
+ [],
+ [
+ { type: 'requestRelease' },
+ {
+ type: 'receiveReleaseSuccess',
+ payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }),
+ },
+ ],
+ );
+ });
+
+ it(`dispatches requestRelease and receiveReleaseError with an error object`, () => {
+ mock.onGet(getReleaseUrl).replyOnce(500);
+
+ return testAction(
+ actions.fetchRelease,
+ undefined,
+ stateClone,
+ [],
+ [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
+ );
+ });
+ });
+
+ describe('updateReleaseTitle', () => {
+ it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
+ const newTitle = 'The new release title';
+ return testAction(actions.updateReleaseTitle, newTitle, stateClone, [
+ { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
+ ]);
+ });
+ });
+
+ describe('updateReleaseNotes', () => {
+ it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
+ const newReleaseNotes = 'The new release notes';
+ return testAction(actions.updateReleaseNotes, newReleaseNotes, stateClone, [
+ { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
+ ]);
+ });
+ });
+
+ describe('requestUpdateRelease', () => {
+ it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
+ testAction(actions.requestUpdateRelease, undefined, stateClone, [
+ { type: types.REQUEST_UPDATE_RELEASE },
+ ]));
+ });
+
+ describe('receiveUpdateReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
+ testAction(
+ actions.receiveUpdateReleaseSuccess,
+ undefined,
+ stateClone,
+ [{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }],
+ [{ type: 'navigateToReleasesPage' }],
+ ));
+ });
+
+ describe('receiveUpdateReleaseError', () => {
+ it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
+ testAction(actions.receiveUpdateReleaseError, error, stateClone, [
+ { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
+ ]));
+
+ it('shows a flash with an error message', () => {
+ actions.receiveUpdateReleaseError({ commit: jest.fn() }, error);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while saving the release details',
+ );
+ });
+ });
+
+ describe('updateRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ stateClone.release = releaseClone;
+ stateClone.projectId = '18';
+ stateClone.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ });
+
+ it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
+ mock.onPut(getReleaseUrl).replyOnce(200);
+
+ return testAction(
+ actions.updateRelease,
+ undefined,
+ stateClone,
+ [],
+ [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
+ );
+ });
+
+ it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => {
+ mock.onPut(getReleaseUrl).replyOnce(500);
+
+ return testAction(
+ actions.updateRelease,
+ undefined,
+ stateClone,
+ [],
+ [
+ { type: 'requestUpdateRelease' },
+ { type: 'receiveUpdateReleaseError', payload: expect.anything() },
+ ],
+ );
+ });
+ });
+
+ describe('navigateToReleasesPage', () => {
+ it(`calls redirectTo() with the URL to the releases page`, () => {
+ const releasesPagePath = 'path/to/releases/page';
+ stateClone.releasesPagePath = releasesPagePath;
+
+ actions.navigateToReleasesPage({ state: stateClone });
+
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(releasesPagePath);
+ });
+ });
+});
diff --git a/spec/frontend/releases/detail/store/mutations_spec.js b/spec/frontend/releases/detail/store/mutations_spec.js
new file mode 100644
index 00000000000..106a40c812e
--- /dev/null
+++ b/spec/frontend/releases/detail/store/mutations_spec.js
@@ -0,0 +1,119 @@
+/* eslint-disable jest/valid-describe */
+/*
+ * ESLint disable directive ↑ can be removed once
+ * https://github.com/jest-community/eslint-plugin-jest/issues/203
+ * is resolved
+ */
+
+import state from '~/releases/detail/store/state';
+import mutations from '~/releases/detail/store/mutations';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+
+describe('Release detail mutations', () => {
+ let stateClone;
+ let releaseClone;
+
+ beforeEach(() => {
+ stateClone = state();
+ releaseClone = JSON.parse(JSON.stringify(release));
+ });
+
+ describe(types.SET_INITIAL_STATE, () => {
+ it('populates the state with initial values', () => {
+ const initialState = {
+ projectId: '18',
+ tagName: 'v1.3',
+ releasesPagePath: 'path/to/releases/page',
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
+ };
+
+ mutations[types.SET_INITIAL_STATE](stateClone, initialState);
+
+ expect(stateClone).toEqual(expect.objectContaining(initialState));
+ });
+ });
+
+ describe(types.REQUEST_RELEASE, () => {
+ it('set state.isFetchingRelease to true', () => {
+ mutations[types.REQUEST_RELEASE](stateClone);
+
+ expect(stateClone.isFetchingRelease).toEqual(true);
+ });
+ });
+
+ describe(types.RECEIVE_RELEASE_SUCCESS, () => {
+ it('handles a successful response from the server', () => {
+ mutations[types.RECEIVE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+ expect(stateClone.fetchError).toEqual(undefined);
+
+ expect(stateClone.isFetchingRelease).toEqual(false);
+
+ expect(stateClone.release).toEqual(releaseClone);
+ });
+ });
+
+ describe(types.RECEIVE_RELEASE_ERROR, () => {
+ it('handles an unsuccessful response from the server', () => {
+ const error = { message: 'An error occurred!' };
+ mutations[types.RECEIVE_RELEASE_ERROR](stateClone, error);
+
+ expect(stateClone.isFetchingRelease).toEqual(false);
+
+ expect(stateClone.release).toBeUndefined();
+
+ expect(stateClone.fetchError).toEqual(error);
+ });
+ });
+
+ describe(types.UPDATE_RELEASE_TITLE, () => {
+ it("updates the release's title", () => {
+ stateClone.release = releaseClone;
+ const newTitle = 'The new release title';
+ mutations[types.UPDATE_RELEASE_TITLE](stateClone, newTitle);
+
+ expect(stateClone.release.name).toEqual(newTitle);
+ });
+ });
+
+ describe(types.UPDATE_RELEASE_NOTES, () => {
+ it("updates the release's notes", () => {
+ stateClone.release = releaseClone;
+ const newNotes = 'The new release notes';
+ mutations[types.UPDATE_RELEASE_NOTES](stateClone, newNotes);
+
+ expect(stateClone.release.description).toEqual(newNotes);
+ });
+ });
+
+ describe(types.REQUEST_UPDATE_RELEASE, () => {
+ it('set state.isUpdatingRelease to true', () => {
+ mutations[types.REQUEST_UPDATE_RELEASE](stateClone);
+
+ expect(stateClone.isUpdatingRelease).toEqual(true);
+ });
+ });
+
+ describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
+ it('handles a successful response from the server', () => {
+ mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+ expect(stateClone.updateError).toEqual(undefined);
+
+ expect(stateClone.isUpdatingRelease).toEqual(false);
+ });
+ });
+
+ describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
+ it('handles an unsuccessful response from the server', () => {
+ const error = { message: 'An error occurred!' };
+ mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](stateClone, error);
+
+ expect(stateClone.isUpdatingRelease).toEqual(false);
+
+ expect(stateClone.updateError).toEqual(error);
+ });
+ });
+});
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
new file mode 100644
index 00000000000..53953d72b06
--- /dev/null
+++ b/spec/helpers/environment_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnvironmentHelper do
+ describe '#render_deployment_status' do
+ context 'when using a manual deployment' do
+ it 'renders a span tag' do
+ deploy = build(:deployment, deployable: nil, status: :success)
+ html = helper.render_deployment_status(deploy)
+
+ expect(html).to have_css('span.ci-status.ci-success')
+ end
+ end
+
+ context 'when using a deployment from a build' do
+ it 'renders a link tag' do
+ deploy = build(:deployment, status: :success)
+ html = helper.render_deployment_status(deploy)
+
+ expect(html).to have_css('a.ci-status.ci-success')
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 191df3cc709..cb6b158f01c 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -70,7 +70,7 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
-window.gon.ee = process.env.IS_GITLAB_EE;
+window.gon.ee = process.env.IS_EE;
gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
@@ -118,7 +118,7 @@ const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
const testContexts = [require.context('spec', true, /_spec$/)];
-if (process.env.IS_GITLAB_EE) {
+if (process.env.IS_EE) {
testContexts.push(require.context('ee_spec', true, /_spec$/));
}
@@ -207,7 +207,7 @@ if (process.env.BABEL_ENV === 'coverage') {
describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
- if (process.env.IS_GITLAB_EE) {
+ if (process.env.IS_EE) {
sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
}
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
new file mode 100644
index 00000000000..4b5c3f9489e
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Line do
+ let(:offset) { 0 }
+ let(:style) { Gitlab::Ci::Ansi2json::Style.new }
+
+ subject { described_class.new(offset: offset, style: style) }
+
+ describe '#<<' do
+ it 'appends new data to the current segment' do
+ expect { subject << 'test 1' }.to change { subject.current_segment.text }
+ expect(subject.current_segment.text).to eq('test 1')
+
+ expect { subject << ', test 2' }.to change { subject.current_segment.text }
+ expect(subject.current_segment.text).to eq('test 1, test 2')
+ end
+ end
+
+ describe '#style' do
+ context 'when style is passed to the initializer' do
+ let(:style) { double }
+
+ it 'returns the same style' do
+ expect(subject.style).to eq(style)
+ end
+ end
+
+ context 'when style is not passed to the initializer' do
+ it 'returns the default style' do
+ expect(subject.style.set?).to be_falsey
+ end
+ end
+ end
+
+ describe '#update_style' do
+ let(:expected_style) do
+ Gitlab::Ci::Ansi2json::Style.new(
+ fg: 'term-fg-l-yellow',
+ bg: 'term-bg-blue',
+ mask: 1)
+ end
+
+ it 'sets the style' do
+ subject.update_style(%w[1 33 44])
+
+ expect(subject.style).to eq(expected_style)
+ end
+ end
+
+ describe '#add_section' do
+ it 'appends a new section to the list' do
+ subject.add_section('section_1')
+ subject.add_section('section_2')
+
+ expect(subject.sections).to eq(%w[section_1 section_2])
+ end
+ end
+
+ describe '#set_as_section_header' do
+ it 'change the section_header to true' do
+ expect { subject.set_as_section_header }
+ .to change { subject.section_header }
+ .to be_truthy
+ end
+ end
+
+ describe '#set_section_duration' do
+ it 'sets and formats the section_duration' do
+ subject.set_section_duration(75)
+
+ expect(subject.section_duration).to eq('01:15')
+ end
+ end
+
+ describe '#flush_current_segment!' do
+ context 'when current segment is not empty' do
+ before do
+ subject << 'some data'
+ end
+
+ it 'adds the segment to the list' do
+ expect { subject.flush_current_segment! }.to change { subject.segments.count }.by(1)
+
+ expect(subject.segments.map { |s| s[:text] }).to eq(['some data'])
+ end
+
+ it 'updates the current segment pointer propagating the style' do
+ previous_segment = subject.current_segment
+
+ subject.flush_current_segment!
+
+ expect(subject.current_segment).not_to eq(previous_segment)
+ expect(subject.current_segment.style).to eq(previous_segment.style)
+ end
+ end
+
+ context 'when current segment is empty' do
+ it 'does not add any segments to the list' do
+ expect { subject.flush_current_segment! }.not_to change { subject.segments.count }
+ end
+
+ it 'does not change the current segment' do
+ expect { subject.flush_current_segment! }.not_to change { subject.current_segment }
+ end
+ end
+ end
+
+ describe '#to_h' do
+ before do
+ subject << 'some data'
+ subject.update_style(['1'])
+ end
+
+ context 'when sections are present' do
+ before do
+ subject.add_section('section_1')
+ subject.add_section('section_2')
+ end
+
+ context 'when section header is set' do
+ before do
+ subject.set_as_section_header
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_header: true
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+
+ context 'when section duration is set' do
+ before do
+ subject.set_section_duration(75)
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_duration: '01:15'
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+ end
+
+ context 'when there are no sections' do
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }]
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
new file mode 100644
index 00000000000..e161e74c1ff
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# The rest of the specs for this class are covered in style_spec.rb
+describe Gitlab::Ci::Ansi2json::Parser do
+ subject { described_class }
+
+ describe 'bold?' do
+ it 'returns true if style mask matches bold format' do
+ expect(subject.bold?(0x01)).to be_truthy
+ end
+
+ it 'returns false if style mask does not match bold format' do
+ expect(subject.bold?(0x02)).to be_falsey
+ end
+ end
+
+ describe 'matching_formats' do
+ it 'returns matching formats given a style mask' do
+ expect(subject.matching_formats(0x01)).to eq(%w[term-bold])
+ expect(subject.matching_formats(0x03)).to eq(%w[term-bold term-italic])
+ expect(subject.matching_formats(0x07)).to eq(%w[term-bold term-italic term-underline])
+ end
+
+ it 'returns an empty array if no formats match the style mask' do
+ expect(subject.matching_formats(0)).to eq([])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
new file mode 100644
index 00000000000..88a0ca35859
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Style do
+ describe '#set?' do
+ subject { described_class.new(params).set? }
+
+ context 'when fg color is set' do
+ let(:params) { { fg: 'term-fg-black' } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when bg color is set' do
+ let(:params) { { bg: 'term-bg-black' } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when mask is set' do
+ let(:params) { { mask: 0x01 } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'nothing is set' do
+ let(:params) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#reset!' do
+ let(:style) { described_class.new(fg: 'term-fg-black', bg: 'term-bg-yellow', mask: 0x01) }
+
+ it 'set the style params to default' do
+ style.reset!
+
+ expect(style.fg).to be_nil
+ expect(style.bg).to be_nil
+ expect(style.mask).to be_zero
+ end
+ end
+
+ describe 'update formats to mimic terminals' do
+ subject { described_class.new(params) }
+
+ context 'when fg color present' do
+ let(:params) { { fg: 'term-fg-black', mask: mask } }
+
+ context 'when mask is set to bold' do
+ let(:mask) { 0x01 }
+
+ it 'changes the fg color to a lighter version' do
+ expect(subject.fg).to eq('term-fg-l-black')
+ end
+ end
+
+ context 'when mask set to another format' do
+ let(:mask) { 0x02 }
+
+ it 'does not change the fg color' do
+ expect(subject.fg).to eq('term-fg-black')
+ end
+ end
+
+ context 'when mask is not set' do
+ let(:mask) { 0 }
+
+ it 'does not change the fg color' do
+ expect(subject.fg).to eq('term-fg-black')
+ end
+ end
+ end
+ end
+
+ describe '#update' do
+ where(:initial_state, :ansi_commands, :result, :description) do
+ [
+ # add format
+ [[], %w[0], '', 'does not set any style'],
+ [[], %w[1], 'term-bold', 'enables format bold'],
+ [[], %w[3], 'term-italic', 'enables format italic'],
+ [[], %w[4], 'term-underline', 'enables format underline'],
+ [[], %w[8], 'term-conceal', 'enables format conceal'],
+ [[], %w[9], 'term-cross', 'enables format cross'],
+ # remove format
+ [%w[1], %w[21], '', 'disables format bold'],
+ [%w[1 3], %w[21], 'term-italic', 'disables format bold and leaves italic'],
+ [%w[1], %w[22], '', 'disables format bold using command 22'],
+ [%w[1 3], %w[22], 'term-italic', 'disables format bold and leaves italic using command 22'],
+ [%w[3], %w[23], '', 'disables format italic'],
+ [%w[1 3], %w[23], 'term-bold', 'disables format italic and leaves bold'],
+ [%w[4], %w[24], '', 'disables format underline'],
+ [%w[1 4], %w[24], 'term-bold', 'disables format underline and leaves bold'],
+ [%w[8], %w[28], '', 'disables format conceal'],
+ [%w[1 8], %w[28], 'term-bold', 'disables format conceal and leaves bold'],
+ [%w[9], %w[29], '', 'disables format cross'],
+ [%w[1 9], %w[29], 'term-bold', 'disables format cross and leaves bold'],
+ # set fg color
+ [[], %w[30], 'term-fg-black', 'sets fg color black'],
+ [[], %w[31], 'term-fg-red', 'sets fg color red'],
+ [[], %w[32], 'term-fg-green', 'sets fg color green'],
+ [[], %w[33], 'term-fg-yellow', 'sets fg color yellow'],
+ [[], %w[34], 'term-fg-blue', 'sets fg color blue'],
+ [[], %w[35], 'term-fg-magenta', 'sets fg color magenta'],
+ [[], %w[36], 'term-fg-cyan', 'sets fg color cyan'],
+ [[], %w[37], 'term-fg-white', 'sets fg color white'],
+ # sets xterm fg color
+ [[], %w[38 5 1], 'xterm-fg-1', 'sets xterm fg color 1'],
+ [[], %w[38 5 2], 'xterm-fg-2', 'sets xterm fg color 2'],
+ [[], %w[38 1], 'term-bold', 'ignores 38 command if not followed by 5 and sets format bold'],
+ # set bg color
+ [[], %w[40], 'term-bg-black', 'sets bg color black'],
+ [[], %w[41], 'term-bg-red', 'sets bg color red'],
+ [[], %w[42], 'term-bg-green', 'sets bg color green'],
+ [[], %w[43], 'term-bg-yellow', 'sets bg color yellow'],
+ [[], %w[44], 'term-bg-blue', 'sets bg color blue'],
+ [[], %w[45], 'term-bg-magenta', 'sets bg color magenta'],
+ [[], %w[46], 'term-bg-cyan', 'sets bg color cyan'],
+ [[], %w[47], 'term-bg-white', 'sets bg color white'],
+ # set xterm bg color
+ [[], %w[48 5 1], 'xterm-bg-1', 'sets xterm bg color 1'],
+ [[], %w[48 5 2], 'xterm-bg-2', 'sets xterm bg color 2'],
+ [[], %w[48 1], 'term-bold', 'ignores 48 command if not followed by 5 and sets format bold'],
+ # set light fg color
+ [[], %w[90], 'term-fg-l-black', 'sets fg color light black'],
+ [[], %w[91], 'term-fg-l-red', 'sets fg color light red'],
+ [[], %w[92], 'term-fg-l-green', 'sets fg color light green'],
+ [[], %w[93], 'term-fg-l-yellow', 'sets fg color light yellow'],
+ [[], %w[94], 'term-fg-l-blue', 'sets fg color light blue'],
+ [[], %w[95], 'term-fg-l-magenta', 'sets fg color light magenta'],
+ [[], %w[96], 'term-fg-l-cyan', 'sets fg color light cyan'],
+ [[], %w[97], 'term-fg-l-white', 'sets fg color light white'],
+ # set light bg color
+ [[], %w[100], 'term-bg-l-black', 'sets bg color light black'],
+ [[], %w[101], 'term-bg-l-red', 'sets bg color light red'],
+ [[], %w[102], 'term-bg-l-green', 'sets bg color light green'],
+ [[], %w[103], 'term-bg-l-yellow', 'sets bg color light yellow'],
+ [[], %w[104], 'term-bg-l-blue', 'sets bg color light blue'],
+ [[], %w[105], 'term-bg-l-magenta', 'sets bg color light magenta'],
+ [[], %w[106], 'term-bg-l-cyan', 'sets bg color light cyan'],
+ [[], %w[107], 'term-bg-l-white', 'sets bg color light white'],
+ # reset
+ [%w[1], %w[0], '', 'resets style from format bold'],
+ [%w[1 3], %w[0], '', 'resets style from format bold and italic'],
+ [%w[1 3 term-fg-l-red term-bg-yellow], %w[0], '', 'resets all formats and colors'],
+ # misc
+ [[], %w[1 30 42 3], 'term-fg-l-black term-bg-green term-bold term-italic', 'adds fg color, bg color and formats from no style'],
+ [%w[3 31], %w[23 1 43], 'term-fg-l-red term-bg-yellow term-bold', 'replaces format italic with bold and adds a yellow background']
+ ]
+ end
+
+ with_them do
+ it 'change the style' do
+ style = described_class.new
+ style.update(initial_state)
+
+ style.update(ansi_commands)
+
+ expect(style.to_s).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
new file mode 100644
index 00000000000..3c6bc46436b
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -0,0 +1,544 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json do
+ subject { described_class }
+
+ describe 'lines' do
+ it 'prints non-ansi as-is' do
+ expect(convert_json('Hello')).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] }
+ ])
+ end
+
+ it 'adds new line in a separate element' do
+ expect(convert_json("Hello\nworld")).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] },
+ { offset: 6, content: [{ text: 'world' }] }
+ ])
+ end
+
+ it 'recognizes color changing ANSI sequences' do
+ expect(convert_json("\e[31mHello\e[0m")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] }
+ ])
+ end
+
+ it 'recognizes color changing ANSI sequences across multiple lines' do
+ expect(convert_json("\e[31mHello\nWorld\e[0m")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] },
+ { offset: 11, content: [{ text: 'World', style: 'term-fg-red' }] }
+ ])
+ end
+
+ it 'recognizes background and foreground colors' do
+ expect(convert_json("\e[31;44mHello")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red term-bg-blue' }] }
+ ])
+ end
+
+ it 'recognizes style changes within the same line' do
+ expect(convert_json("\e[31;44mHello\e[0m world")).to eq([
+ { offset: 0, content: [
+ { text: 'Hello', style: 'term-fg-red term-bg-blue' },
+ { text: ' world' }
+ ] }
+ ])
+ end
+
+ context 'with section markers' do
+ let(:section_name) { 'prepare-script' }
+ let(:section_duration) { 63.seconds }
+ let(:section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+ it 'marks the first line of the section as header' do
+ expect(convert_json("Hello#{section_start}world!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script',
+ section_header: true
+ }
+ ])
+ end
+
+ it 'does not marks the other lines of the section as header' do
+ expect(convert_json("outside section#{section_start}Hello\nworld!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'outside section' }]
+ },
+ {
+ offset: 15,
+ content: [{ text: 'Hello' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 65,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script'
+ }
+ ])
+ end
+
+ it 'marks the last line of the section as footer' do
+ expect(convert_json("#{section_start}Good\nmorning\nworld!#{section_end}")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Good' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 49,
+ content: [{ text: 'morning' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 57,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 63,
+ content: [],
+ section_duration: '01:03',
+ section: 'prepare-script'
+ },
+ {
+ offset: 63,
+ content: []
+ }
+ ])
+ end
+
+ it 'marks the first line as header and footer if is the only line in the section' do
+ expect(convert_json("#{section_start}Hello world!#{section_end}")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello world!' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 56,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 56,
+ content: []
+ }
+ ])
+ end
+
+ it 'does not add sections attribute to lines after the section is closed' do
+ expect(convert_json("#{section_start}Hello#{section_end}world")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 49,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 49,
+ content: [{ text: 'world' }]
+ }
+ ])
+ end
+
+ it 'ignores section_end marker if no section_start exists' do
+ expect(convert_json("Hello #{section_end}world")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello world' }]
+ }
+ ])
+ end
+
+ context 'when section name contains .-_ and capital letters' do
+ let(:section_name) { 'a.Legit-SeCtIoN_namE' }
+
+ it 'sanitizes the section name' do
+ expect(convert_json("Hello#{section_start}world!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'world!' }],
+ section: 'a-legit-section-name',
+ section_header: true
+ }
+ ])
+ end
+ end
+
+ context 'when section name includes $' do
+ let(:section_name) { 'my_$ection' }
+
+ it 'ignores the section' do
+ expect(convert_json("#{section_start}hello")).to eq([
+ {
+ offset: 0,
+ content: [{ text: "#{section_start.gsub("\033[0K", '')}hello" }]
+ }
+ ])
+ end
+ end
+
+ context 'when section name includes <' do
+ let(:section_name) { '<a_tag>' }
+
+ it 'ignores the section' do
+ expect(convert_json("#{section_start}hello")).to eq([
+ {
+ offset: 0,
+ content: [{ text: "#{section_start.gsub("\033[0K", '').gsub('<', '&lt;')}hello" }]
+ }
+ ])
+ end
+ end
+
+ it 'prevents XSS injection' do
+ trace = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}"
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: "section_end:1:2&lt;script>alert('XSS Hack!');&lt;/script>" }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 95,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 95,
+ content: []
+ }
+ ])
+ end
+
+ context 'with nested section' do
+ let(:nested_section_name) { 'prepare-script-nested' }
+ let(:nested_section_duration) { 2.seconds }
+ let(:nested_section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:nested_section_end_time) { nested_section_start_time + nested_section_duration }
+ let(:nested_section_start) { "section_start:#{nested_section_start_time.to_i}:#{nested_section_name}\r\033[0K"}
+ let(:nested_section_end) { "section_end:#{nested_section_end_time.to_i}:#{nested_section_name}\r\033[0K"}
+
+ it 'adds multiple sections to the lines inside the nested section' do
+ trace = "Hello#{section_start}foo#{nested_section_start}bar#{nested_section_end}baz#{section_end}world"
+
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'foo' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 52,
+ content: [{ text: 'bar' }],
+ section: 'prepare-script-nested',
+ section_header: true
+ },
+ {
+ offset: 106,
+ content: [],
+ section: 'prepare-script-nested',
+ section_duration: '00:02'
+ },
+ {
+ offset: 106,
+ content: [{ text: 'baz' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 158,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 158,
+ content: [{ text: 'world' }]
+ }
+ ])
+ end
+
+ it 'adds multiple sections to the lines inside the nested section and closes all sections together' do
+ trace = "Hello#{section_start}\e[91mfoo\e[0m#{nested_section_start}bar#{nested_section_end}#{section_end}"
+
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'foo', style: 'term-fg-l-red' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 61,
+ content: [{ text: 'bar' }],
+ section: 'prepare-script-nested',
+ section_header: true
+ },
+ {
+ offset: 115,
+ content: [],
+ section: 'prepare-script-nested',
+ section_duration: '00:02'
+ },
+ {
+ offset: 115,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 164,
+ content: []
+ }
+ ])
+ end
+ end
+ end
+
+ describe 'incremental updates' do
+ let(:pass1_stream) { StringIO.new(pre_text) }
+ let(:pass2_stream) { StringIO.new(pre_text + text) }
+ let(:pass1) { subject.convert(pass1_stream) }
+ let(:pass2) { subject.convert(pass2_stream, pass1.state) }
+
+ context 'with split word' do
+ let(:pre_text) { "\e[1mHello " }
+ let(:text) { "World" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [{ text: 'Hello World', style: 'term-bold' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split word on second line' do
+ let(:pre_text) { "Good\nmorning " }
+ let(:text) { "World" }
+
+ let(:lines) do
+ [
+ { offset: 5, content: [{ text: 'morning World' }] }
+ ]
+ end
+
+ it 'returns all lines since last partially processed line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split sequence across multiple lines' do
+ let(:pre_text) { "\e[1mgood\nmorning\n" }
+ let(:text) { "\e[3mworld" }
+
+ let(:lines) do
+ [
+ { offset: 17, content: [{ text: 'world', style: 'term-bold term-italic' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split partial sequence' do
+ let(:pre_text) { "hello\e" }
+ let(:text) { "[1m world" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [
+ { text: 'hello' },
+ { text: ' world', style: 'term-bold' }
+ ] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split new line' do
+ let(:pre_text) { "hello\r" }
+ let(:text) { "\nworld" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [{ text: 'hello' }] },
+ { offset: 7, content: [{ text: 'world' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split section' do
+ let(:section_name) { 'prepare-script' }
+ let(:section_duration) { 63.seconds }
+ let(:section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+ context 'with split section body' do
+ let(:pre_text) { "#{section_start}this is a header\nand " }
+ let(:text) { "this\n is a body" }
+
+ let(:lines) do
+ [
+ {
+ offset: 61,
+ content: [{ text: 'and this' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 70,
+ content: [{ text: ' is a body' }],
+ section: 'prepare-script'
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split section where header is also split' do
+ let(:pre_text) { "#{section_start}this is " }
+ let(:text) { "a header\nand body" }
+
+ let(:lines) do
+ [
+ {
+ offset: 0,
+ content: [{ text: 'this is a header' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 61,
+ content: [{ text: 'and body' }],
+ section: 'prepare-script'
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split section end' do
+ let(:pre_text) { "#{section_start}this is a header\nthe" }
+ let(:text) { " body\nthe end#{section_end}" }
+
+ let(:lines) do
+ [
+ {
+ offset: 61,
+ content: [{ text: 'the body' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 70,
+ content: [{ text: 'the end' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 77,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 77,
+ content: []
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe 'trucates' do
+ let(:text) { "Hello World" }
+ let(:stream) { StringIO.new(text) }
+ let(:subject) { described_class.convert(stream) }
+
+ before do
+ stream.seek(3, IO::SEEK_SET)
+ end
+
+ it "returns truncated output" do
+ expect(subject.truncated).to be_truthy
+ end
+
+ it "does not append output" do
+ expect(subject.append).to be_falsey
+ end
+ end
+
+ def convert_json(data)
+ stream = StringIO.new(data)
+ subject.convert(stream).lines
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_collection_spec.rb b/spec/lib/gitlab/diff/position_collection_spec.rb
index de0e631ab03..f2a8312587c 100644
--- a/spec/lib/gitlab/diff/position_collection_spec.rb
+++ b/spec/lib/gitlab/diff/position_collection_spec.rb
@@ -35,14 +35,15 @@ describe Gitlab::Diff::PositionCollection do
let(:text_position) { build_text_position }
let(:folded_text_position) { build_text_position(old_line: 1, new_line: 1) }
let(:image_position) { build_image_position }
+ let(:invalid_position) { 'a position' }
let(:head_sha) { merge_request.diff_head_sha }
let(:collection) do
- described_class.new([text_position, folded_text_position, image_position], head_sha)
+ described_class.new([text_position, folded_text_position, image_position, invalid_position], head_sha)
end
describe '#to_a' do
- it 'returns all positions' do
+ it 'returns all positions that are Gitlab::Diff::Position' do
expect(collection.to_a).to eq([text_position, folded_text_position, image_position])
end
end
@@ -59,6 +60,14 @@ describe Gitlab::Diff::PositionCollection do
expect(collection.unfoldable).to be_empty
end
end
+
+ context 'when given head_sha is nil' do
+ let(:head_sha) { nil }
+
+ it 'returns unfoldable diff positions unfiltered by head_sha' do
+ expect(collection.unfoldable).to eq([folded_text_position])
+ end
+ end
end
describe '#concat' do
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
new file mode 100644
index 00000000000..33efc640257
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::Probes::Collection do
+ let(:readiness) { described_class.new(*checks) }
+
+ describe '#call' do
+ subject { readiness.execute }
+
+ context 'with all checks' do
+ let(:checks) do
+ [
+ Gitlab::HealthChecks::DbCheck,
+ Gitlab::HealthChecks::Redis::RedisCheck,
+ Gitlab::HealthChecks::Redis::CacheCheck,
+ Gitlab::HealthChecks::Redis::QueuesCheck,
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
+ ]
+ end
+
+ it 'responds with readiness checks data' do
+ expect(subject.http_status).to eq(200)
+
+ expect(subject.json[:status]).to eq('ok')
+ expect(subject.json['db_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['gitaly_check']).to contain_exactly(
+ status: 'ok', labels: { shard: 'default' })
+ end
+
+ context 'when Redis fails' do
+ before do
+ allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+ end
+
+ it 'responds with failure' do
+ expect(subject.http_status).to eq(503)
+
+ expect(subject.json[:status]).to eq('failed')
+ expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['redis_check']).to contain_exactly(
+ status: 'failed', message: 'check error')
+ end
+ end
+ end
+
+ context 'without checks' do
+ let(:checks) { [] }
+
+ it 'responds with success' do
+ expect(subject.http_status).to eq(200)
+
+ expect(subject.json).to eq(status: 'ok')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/probes/liveness_spec.rb b/spec/lib/gitlab/health_checks/probes/liveness_spec.rb
deleted file mode 100644
index 91066cb8ba0..00000000000
--- a/spec/lib/gitlab/health_checks/probes/liveness_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::Probes::Liveness do
- let(:liveness) { described_class.new }
-
- describe '#call' do
- subject { liveness.execute }
-
- it 'responds with liveness checks data' do
- expect(subject.http_status).to eq(200)
-
- expect(subject.json[:status]).to eq('ok')
- end
- end
-end
diff --git a/spec/lib/gitlab/health_checks/probes/readiness_spec.rb b/spec/lib/gitlab/health_checks/probes/readiness_spec.rb
deleted file mode 100644
index d88ffd984c2..00000000000
--- a/spec/lib/gitlab/health_checks/probes/readiness_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::Probes::Readiness do
- let(:readiness) { described_class.new }
-
- describe '#call' do
- subject { readiness.execute }
-
- it 'responds with readiness checks data' do
- expect(subject.http_status).to eq(200)
-
- expect(subject.json[:status]).to eq('ok')
- expect(subject.json['db_check']).to contain_exactly(status: 'ok')
- expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
- expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
- expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
- expect(subject.json['gitaly_check']).to contain_exactly(
- status: 'ok', labels: { shard: 'default' })
- end
-
- context 'when Redis fails' do
- before do
- allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
- Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
- end
-
- it 'responds with failure' do
- expect(subject.http_status).to eq(503)
-
- expect(subject.json[:status]).to eq('failed')
- expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
- expect(subject.json['redis_check']).to contain_exactly(
- status: 'failed', message: 'check error')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index c1d171815ba..6bf837f1d3f 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -146,7 +146,7 @@ describe Gitlab do
describe '.ee?' do
before do
- stub_env('IS_GITLAB_EE', nil) # Make sure the ENV is clean
+ stub_env('FOSS_ONLY', nil) # Make sure the ENV is clean
described_class.instance_variable_set(:@is_ee, nil)
end
@@ -154,42 +154,66 @@ describe Gitlab do
described_class.instance_variable_set(:@is_ee, nil)
end
- it 'returns true when using Enterprise Edition' do
- root = Pathname.new('dummy')
- license_path = double(:path, exist?: true)
+ context 'for EE' do
+ before do
+ root = Pathname.new('dummy')
+ license_path = double(:path, exist?: true)
- allow(described_class)
- .to receive(:root)
- .and_return(root)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(root)
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ allow(root)
+ .to receive(:join)
+ .with('ee/app/models/license.rb')
+ .and_return(license_path)
+ end
- expect(described_class.ee?).to eq(true)
- end
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
- it 'returns false when using Community Edition' do
- root = double(:path)
- license_path = double(:path, exists?: false)
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
+ end
- allow(described_class)
- .to receive(:root)
- .and_return(Pathname.new('dummy'))
+ context 'when using FOSS_ONLY=0' do
+ before do
+ stub_env('FOSS_ONLY', '0')
+ end
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
- expect(described_class.ee?).to eq(false)
+ context 'when using default FOSS_ONLY' do
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
end
- it 'returns true when the IS_GITLAB_EE variable is not empty' do
- stub_env('IS_GITLAB_EE', '1')
+ context 'for CE' do
+ before do
+ root = double(:path)
+ license_path = double(:path, exists?: false)
- expect(described_class.ee?).to eq(true)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(Pathname.new('dummy'))
+
+ allow(root)
+ .to receive(:join)
+ .with('ee/app/models/license.rb')
+ .and_return(license_path)
+ end
+
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 84c25b93fc6..702a6fab0e6 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -56,6 +56,10 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:protected_paths) }
it { is_expected.to allow_value([]).for(:protected_paths) }
+ it { is_expected.to allow_value(3).for(:push_event_hooks_limit) }
+ it { is_expected.not_to allow_value('three').for(:push_event_hooks_limit) }
+ it { is_expected.not_to allow_value(nil).for(:push_event_hooks_limit) }
+
context "when user accepted let's encrypt terms of service" do
before do
setting.update(lets_encrypt_terms_of_service_accepted: true)
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 51ed8e9421b..3a0b3c46ad0 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -348,4 +348,17 @@ describe Deployment do
expect(deployment.deployed_by).to eq(build_user)
end
end
+
+ describe '.find_successful_deployment!' do
+ it 'returns a successful deployment' do
+ deploy = create(:deployment, :success)
+
+ expect(described_class.find_successful_deployment!(deploy.iid)).to eq(deploy)
+ end
+
+ it 'raises when no deployment is found' do
+ expect { described_class.find_successful_deployment!(-1) }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 521c4704c87..786f3b832c4 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -882,4 +882,19 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '.find_or_create_by_name' do
+ it 'finds an existing environment if it exists' do
+ env = create(:environment)
+
+ expect(described_class.find_or_create_by_name(env.name)).to eq(env)
+ end
+
+ it 'creates an environment if it does not exist' do
+ env = project.environments.find_or_create_by_name('kittens')
+
+ expect(env).to be_an_instance_of(described_class)
+ expect(env).to be_persisted
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 6093464c949..e61a064e82c 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -40,14 +40,14 @@ describe ProjectPolicy do
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image
- create_environment create_deployment create_release update_release
+ create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 3dac7225b7a..ad7be531979 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Deployments do
@@ -96,4 +98,164 @@ describe API::Deployments do
end
end
end
+
+ describe 'POST /projects/:id/deployments' do
+ let!(:project) { create(:project, :repository) }
+ let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ context 'as a maintainer' do
+ it 'creates a new deployment' do
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(201)
+
+ expect(json_response['sha']).to eq(sha)
+ expect(json_response['ref']).to eq('master')
+ expect(json_response['environment']['name']).to eq('production')
+ end
+
+ it 'errors when creating a deployment with an invalid name' do
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'a' * 300,
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(500)
+ end
+ end
+
+ context 'as a developer' do
+ it 'creates a new deployment' do
+ developer = create(:user)
+
+ project.add_developer(developer)
+
+ post(
+ api("/projects/#{project.id}/deployments", developer),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(201)
+
+ expect(json_response['sha']).to eq(sha)
+ expect(json_response['ref']).to eq('master')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ post(
+ api( "/projects/#{project.id}/deployments", non_member),
+ params: {
+ environment: 'production',
+ sha: '123',
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/deployments/:deployment_id' do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, :failed, project: project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:deploy) do
+ create(
+ :deployment,
+ :failed,
+ project: project,
+ environment: environment,
+ deployable: nil
+ )
+ end
+
+ context 'as a maintainer' do
+ it 'returns a 403 when updating a deployment with a build' do
+ deploy.update(deployable: build)
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a deployment without an associated build' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('success')
+ end
+ end
+
+ context 'as a developer' do
+ let(:developer) { create(:user) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'returns a 403 when updating a deployment with a build' do
+ deploy.update(deployable: build)
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a deployment without an associated build' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('success')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", non_member),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 7e67ee28bef..eb55d747179 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -87,6 +87,15 @@ describe API::Members do
expect(json_response.first['username']).to eq(maintainer.username)
end
+ it 'finds members with the given user_ids' do
+ get api(members_url, developer), params: { user_ids: [maintainer.id, developer.id, stranger.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |u| u['id'] }).to contain_exactly(maintainer.id, developer.id)
+ end
+
it 'finds all members with no query specified' do
get api(members_url, developer), params: { query: '' }
@@ -155,10 +164,10 @@ describe API::Members do
end
end
- shared_examples 'GET /:source_type/:id/members/:user_id' do |source_type|
- context "with :source_type == #{source_type.pluralize}" do
+ shared_examples 'GET /:source_type/:id/members/(all/):user_id' do |source_type, all|
+ context "with :source_type == #{source_type.pluralize} and all == #{all}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", stranger) }
end
context 'when authenticated as a non-member' do
@@ -166,7 +175,7 @@ describe API::Members do
context "as a #{type}" do
it 'returns 200' do
user = public_send(type)
- get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+ get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", user)
expect(response).to have_gitlab_http_status(200)
# User attributes
@@ -434,12 +443,14 @@ describe API::Members do
end
end
- it_behaves_like 'GET /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
+ [false, true].each do |all|
+ it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'project', all do
+ let(:source) { all ? create(:project, :public, group: group) : project }
+ end
- it_behaves_like 'GET /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
+ it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'group', all do
+ let(:source) { all ? create(:group, parent: group) : group }
+ end
end
it_behaves_like 'POST /:source_type/:id/members', 'project' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index d98b9be726a..af1cf80e9d3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -72,7 +72,8 @@ describe API::Settings, 'Settings' do
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
- allow_local_requests_from_system_hooks: false
+ allow_local_requests_from_system_hooks: false,
+ push_event_hooks_limit: 2
}
expect(response).to have_gitlab_http_status(200)
@@ -102,6 +103,7 @@ describe API::Settings, 'Settings' do
expect(json_response['local_markdown_version']).to eq(3)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
+ expect(json_response['push_event_hooks_limit']).to eq(2)
end
end
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/deployments/after_create_service_spec.rb
index 343dab8a974..b34483ea85b 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/deployments/after_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe UpdateDeploymentService do
+describe Deployments::AfterCreateService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } }
diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb
new file mode 100644
index 00000000000..e41c8259ea9
--- /dev/null
+++ b/spec/services/deployments/create_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::CreateService do
+ let(:environment) do
+ double(
+ :environment,
+ deployment_platform: double(:platform, cluster_id: 1),
+ project_id: 2,
+ id: 3
+ )
+ end
+
+ let(:user) { double(:user) }
+
+ describe '#execute' do
+ let(:service) { described_class.new(environment, user, {}) }
+
+ it 'does not run the AfterCreateService service if the deployment is not persisted' do
+ deploy = double(:deployment, persisted?: false)
+
+ expect(service)
+ .to receive(:create_deployment)
+ .and_return(deploy)
+
+ expect(Deployments::AfterCreateService)
+ .not_to receive(:new)
+
+ expect(service.execute).to eq(deploy)
+ end
+
+ it 'runs the AfterCreateService service if the deployment is persisted' do
+ deploy = double(:deployment, persisted?: true)
+ after_service = double(:after_create_service)
+
+ expect(service)
+ .to receive(:create_deployment)
+ .and_return(deploy)
+
+ expect(Deployments::AfterCreateService)
+ .to receive(:new)
+ .with(deploy)
+ .and_return(after_service)
+
+ expect(after_service)
+ .to receive(:execute)
+
+ expect(service.execute).to eq(deploy)
+ end
+ end
+
+ describe '#create_deployment' do
+ it 'creates a deployment' do
+ environment = build(:environment)
+ service = described_class.new(environment, user, {})
+
+ expect(environment.deployments)
+ .to receive(:create)
+ .with(an_instance_of(Hash))
+
+ service.create_deployment
+ end
+ end
+
+ describe '#deployment_attributes' do
+ it 'only includes attributes that we want to persist' do
+ service = described_class.new(
+ environment,
+ user,
+ ref: 'master',
+ tag: true,
+ sha: '123',
+ foo: 'bar',
+ on_stop: 'stop',
+ status: 'running'
+ )
+
+ expect(service.deployment_attributes).to eq(
+ cluster_id: 1,
+ project_id: 2,
+ environment_id: 3,
+ ref: 'master',
+ tag: true,
+ sha: '123',
+ user: user,
+ on_stop: 'stop',
+ status: 'running'
+ )
+ end
+ end
+end
diff --git a/spec/services/deployments/update_service_spec.rb b/spec/services/deployments/update_service_spec.rb
new file mode 100644
index 00000000000..a923099b82c
--- /dev/null
+++ b/spec/services/deployments/update_service_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::UpdateService do
+ let(:deploy) { create(:deployment, :running) }
+ let(:service) { described_class.new(deploy, status: 'success') }
+
+ describe '#execute' do
+ it 'updates the status of a deployment' do
+ expect(service.execute).to eq(true)
+ expect(deploy.status).to eq('success')
+ end
+ end
+end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index e71900e3c0d..90b3eb38469 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -8,7 +8,6 @@ describe Git::BaseHooksService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
-
let(:oldrev) { Gitlab::Git::BLANK_SHA }
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
@@ -26,7 +25,17 @@ describe Git::BaseHooksService do
let(:project) { create(:project, :repository) }
- subject { TestService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }) }
+ let(:params) do
+ {
+ change: {
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ }
+ }
+ end
+
+ subject { TestService.new(project, user, params) }
context '#execute_hooks' do
before do
@@ -83,5 +92,21 @@ describe Git::BaseHooksService do
end
end
end
+
+ context 'execute_project_hooks param set to false' do
+ before do
+ params[:execute_project_hooks] = false
+
+ allow(project).to receive(:has_active_hooks?).and_return(true)
+ allow(project).to receive(:has_active_services?).and_return(true)
+ end
+
+ it 'does not execute hooks and services' do
+ expect(project).not_to receive(:execute_hooks)
+ expect(project).not_to receive(:execute_services)
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index 4d394a29867..eeb395f6c7b 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -28,12 +28,66 @@ describe Git::ProcessRefChangesService do
it "calls #{push_service_class}" do
expect(push_service_class)
.to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: true))
.exactly(changes.count).times
.and_return(service)
subject.execute
end
+ context 'changes exceed push_event_hooks_limit' do
+ def multiple_changes(change, count)
+ Array.new(count).map.with_index do |n, index|
+ { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
+ end
+ end
+
+ let(:push_event_hooks_limit) { 3 }
+
+ let(:changes) do
+ multiple_changes(
+ { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/test" },
+ push_event_hooks_limit + 1
+ )
+ end
+
+ before do
+ stub_application_setting(push_event_hooks_limit: push_event_hooks_limit)
+ end
+
+ context 'git_push_execute_all_project_hooks is disabled' do
+ before do
+ stub_feature_flags(git_push_execute_all_project_hooks: false)
+ end
+
+ it "calls #{push_service_class} with execute_project_hooks set to false" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: false))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+
+ context 'git_push_execute_all_project_hooks is enabled' do
+ before do
+ stub_feature_flags(git_push_execute_all_project_hooks: true)
+ end
+
+ it "calls #{push_service_class} with execute_project_hooks set to true" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: true))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+
context 'pipeline creation' do
context 'with valid .gitlab-ci.yml' do
before do
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index c97eeba87db..bbe793a81bc 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -8,7 +8,9 @@ end
shared_examples "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
- expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+ expect(page)
+ .to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+ .or have_css("a.js-rss-button[href*='feed_token=#{user.feed_token}']")
end
end
@@ -20,6 +22,8 @@ end
shared_examples "it has an RSS button without a feed token" do
it "shows the RSS button without a feed token" do
- expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+ expect(page)
+ .to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+ .or have_css("a.js-rss-button:not([href*='feed_token'])")
end
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 1aa40dcde3d..65398c13d90 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -38,14 +38,14 @@ RSpec.shared_context 'ProjectPolicy context' do
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
- create_environment create_deployment create_release update_release
+ create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb
index 1c68922b03d..7f2816d7535 100644
--- a/spec/workers/deployments/success_worker_spec.rb
+++ b/spec/workers/deployments/success_worker_spec.rb
@@ -8,8 +8,8 @@ describe Deployments::SuccessWorker do
context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) }
- it 'executes UpdateDeploymentService' do
- expect(UpdateDeploymentService)
+ it 'executes Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService)
.to receive(:new).with(deployment).and_call_original
subject
@@ -19,8 +19,8 @@ describe Deployments::SuccessWorker do
context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) }
- it 'does not execute UpdateDeploymentService' do
- expect(UpdateDeploymentService).not_to receive(:new)
+ it 'does not execute Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
@@ -29,8 +29,8 @@ describe Deployments::SuccessWorker do
context 'when deploy record does not exist' do
let(:deployment) { nil }
- it 'does not execute UpdateDeploymentService' do
- expect(UpdateDeploymentService).not_to receive(:new)
+ it 'does not execute Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 6983fea021c..34aaa9bb1e9 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -93,6 +93,8 @@ describe PostReceive do
end
context 'with changes' do
+ let(:push_service) { double(execute: true) }
+
before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])