diff options
Diffstat (limited to 'spec')
69 files changed, 1809 insertions, 685 deletions
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 36ce1119100..2dca2c3976f 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -392,7 +392,7 @@ describe Projects::EnvironmentsController do context 'when requesting metrics as JSON' do it 'returns a metrics JSON document' do - get :additional_metrics, params: environment_params(format: :json) + additional_metrics expect(response).to have_gitlab_http_status(204) expect(json_response).to eq({}) @@ -412,7 +412,7 @@ describe Projects::EnvironmentsController do end it 'returns a metrics JSON document' do - get :additional_metrics, params: environment_params(format: :json) + additional_metrics expect(response).to be_ok expect(json_response['success']).to be(true) @@ -420,6 +420,32 @@ describe Projects::EnvironmentsController do expect(json_response['last_update']).to eq(42) end end + + context 'when only one time param is provided' do + context 'when :metrics_time_window feature flag is disabled' do + before do + stub_feature_flags(metrics_time_window: false) + expect(environment).to receive(:additional_metrics).with(no_args).and_return(nil) + end + + it 'returns a time-window agnostic response' do + additional_metrics(start: '1552647300.651094') + + expect(response).to have_gitlab_http_status(204) + expect(json_response).to eq({}) + end + end + + it 'raises an error when start is missing' do + expect { additional_metrics(start: '1552647300.651094') } + .to raise_error(ActionController::ParameterMissing) + end + + it 'raises an error when end is missing' do + expect { additional_metrics(start: '1552647300.651094') } + .to raise_error(ActionController::ParameterMissing) + end + end end describe 'GET #search' do @@ -500,4 +526,8 @@ describe Projects::EnvironmentsController do project_id: project, id: environment.id) end + + def additional_metrics(opts = {}) + get :additional_metrics, params: environment_params(format: :json, **opts) + end end diff --git a/spec/features/commits/user_uses_quick_actions_spec.rb b/spec/features/commits/user_uses_quick_actions_spec.rb index 9a4b7bd2444..4b7e7465df1 100644 --- a/spec/features/commits/user_uses_quick_actions_spec.rb +++ b/spec/features/commits/user_uses_quick_actions_spec.rb @@ -22,27 +22,6 @@ describe 'Commit > User uses quick actions', :js do let(:tag_message) { 'Stable release' } let(:truncated_commit_sha) { Commit.truncate_sha(commit.sha) } - it 'tags this commit' do - add_note("/tag #{tag_name} #{tag_message}") - - expect(page).to have_content 'Commands applied' - expect(page).to have_content "tagged commit #{truncated_commit_sha}" - expect(page).to have_content tag_name - - visit project_tag_path(project, tag_name) - expect(page).to have_content tag_name - expect(page).to have_content tag_message - expect(page).to have_content truncated_commit_sha - end - - describe 'preview', :js do - it 'removes quick action from note and explains it' do - preview_note("/tag #{tag_name} #{tag_message}") - - expect(page).not_to have_content '/tag' - expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"} - expect(page).to have_content tag_name - end - end + it_behaves_like 'tag quick action' end end diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb index 1a53e7c9512..fc5777e8c7c 100644 --- a/spec/features/group_variables_spec.rb +++ b/spec/features/group_variables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Group variables', :js do let(:user) { create(:user) } let(:group) { create(:group) } - let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', group: group) } + let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', masked: true, group: group) } let(:page_path) { group_settings_ci_cd_path(group) } before do diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index dc0862be6fc..e5770905dbd 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -67,18 +67,7 @@ describe 'Merge request > User posts notes', :js do end end - describe 'when reply_to_individual_notes feature flag is disabled' do - before do - stub_feature_flags(reply_to_individual_notes: false) - visit project_merge_request_path(project, merge_request) - end - - it 'does not show a reply button' do - expect(page).to have_no_selector('.js-reply-button') - end - end - - describe 'when reply_to_individual_notes feature flag is not set' do + describe 'reply button' do before do visit project_merge_request_path(project, merge_request) end diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb index a2b5859bd1e..56774896795 100644 --- a/spec/features/merge_request/user_uses_quick_actions_spec.rb +++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb @@ -56,6 +56,8 @@ describe 'Merge request > User uses quick actions', :js do project.add_maintainer(user) end + it_behaves_like 'merge quick action' + describe 'toggling the WIP prefix in the title from note' do context 'when the current user can toggle the WIP prefix' do before do @@ -103,74 +105,6 @@ describe 'Merge request > User uses quick actions', :js do end end - describe 'merging the MR from the note' do - context 'when the current user can merge the MR' do - before do - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - it 'merges the MR' do - add_note("/merge") - - expect(page).to have_content 'Commands applied' - - expect(merge_request.reload).to be_merged - end - end - - context 'when the head diff changes in the meanwhile' do - before do - merge_request.source_branch = 'another_branch' - merge_request.save - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - it 'does not merge the MR' do - add_note("/merge") - - expect(page).not_to have_content 'Your commands have been executed!' - - expect(merge_request.reload).not_to be_merged - end - end - - context 'when the current user cannot merge the MR' do - before do - project.add_guest(guest) - sign_in(guest) - visit project_merge_request_path(project, merge_request) - end - - it 'does not merge the MR' do - add_note("/merge") - - expect(page).not_to have_content 'Your commands have been executed!' - - expect(merge_request.reload).not_to be_merged - end - end - end - - describe 'adding a due date from note' do - before do - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - it_behaves_like 'due quick action not available' - end - - describe 'removing a due date from note' do - before do - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - it_behaves_like 'remove_due_date action not available' - end - describe '/target_branch command in merge request' do let(:another_project) { create(:project, :public, :repository) } let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb index 6bdf5df1036..76abc640077 100644 --- a/spec/features/project_variables_spec.rb +++ b/spec/features/project_variables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Project variables', :js do let(:user) { create(:user) } let(:project) { create(:project) } - let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value') } + let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value', masked: true) } let(:page_path) { project_settings_ci_cd_path(project) } before do diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index c8dc72a34ec..3e75890725e 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -35,7 +35,7 @@ describe 'Download buttons in branches page' do it 'shows download artifacts button' do href = latest_succeeded_project_artifacts_path(project, 'binary-encoding/download', job: 'build') - expect(page).to have_link "Download '#{build.name}'", href: href + expect(page).to have_link build.name, href: href end end end diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb index 03cb3530e2b..111972a6b00 100644 --- a/spec/features/projects/files/download_buttons_spec.rb +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -30,7 +30,7 @@ describe 'Projects > Files > Download buttons in files tree' do it 'shows download artifacts button' do href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build') - expect(page).to have_link "Download '#{build.name}'", href: href + expect(page).to have_link build.name, href: href end end end diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb index 3a2dcc5aa55..fee5f8001b0 100644 --- a/spec/features/projects/show/download_buttons_spec.rb +++ b/spec/features/projects/show/download_buttons_spec.rb @@ -35,11 +35,10 @@ describe 'Projects > Show > Download buttons' do it 'shows download artifacts button' do href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build') - expect(page).to have_link "Download '#{build.name}'", href: href + expect(page).to have_link build.name, href: href end it 'download links have download attribute' do - expect(page).to have_selector('a', text: 'Download') page.all('a', text: 'Download').each do |link| expect(link[:download]).to eq '' end diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb index fbfd8cee7aa..4c8ec53836a 100644 --- a/spec/features/projects/tags/download_buttons_spec.rb +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -36,7 +36,7 @@ describe 'Download buttons in tags page' do it 'shows download artifacts button' do href = latest_succeeded_project_artifacts_path(project, "#{tag}/download", job: 'build') - expect(page).to have_link "Download '#{build.name}'", href: href + expect(page).to have_link build.name, href: href end end end diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb index 3b48ea4786d..d2cdade88d1 100644 --- a/spec/features/user_sees_revert_modal_spec.rb +++ b/spec/features/user_sees_revert_modal_spec.rb @@ -17,12 +17,14 @@ describe 'Merge request > User sees revert modal', :js do end it 'shows the revert modal' do - expect(page).to have_content('Revert this merge request') + page.within('.modal-header') do + expect(page).to have_content 'Revert this merge request' + end end it 'closes the revert modal with escape keypress' do find('#modal-revert-commit').send_keys(:escape) - expect(page).not_to have_content('Revert this merge request') + expect(page).not_to have_selector('#modal-revert-commit', visible: true) end end diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json index cd50be00418..918f2c4b47d 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json @@ -119,6 +119,12 @@ "merge_status", "sha", "merge_commit_sha", "user_notes_count", "should_remove_source_branch", "force_remove_source_branch", "web_url", "squash" - ] + ], + "head_pipeline": { + "oneOf": [ + { "type": "null" }, + { "$ref": "pipeline/detail.json" } + ] + } } } diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json index 56f86856dd4..a7207d2d991 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json @@ -13,6 +13,5 @@ "ref": { "type": "string" }, "status": { "type": "string" }, "web_url": { "type": "string" } - }, - "additionalProperties": false + } } diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json new file mode 100644 index 00000000000..63e130d4055 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "basic.json" }, + { + "properties": { + "before_sha": { "type": ["string", "null"] }, + "tag": { "type": ["boolean"] }, + "yaml_errors": { "type": ["string", "null"] }, + "user": { + "anyOf": [ + { "type": ["object", "null"] }, + { "$ref": "../user/basic.json" } + ] + }, + "created_at": { "type": ["date", "null"] }, + "updated_at": { "type": ["date", "null"] }, + "started_at": { "type": ["date", "null"] }, + "finished_at": { "type": ["date", "null"] }, + "committed_at": { "type": ["date", "null"] }, + "duration": { "type": ["number", "null"] }, + "coverage": { "type": ["string", "null"] }, + "detailed_status": { + "oneOf": [ + { "type": "null" }, + { "$ref": "../../../status/ci_detailed_status.json" } + ] + } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json index 6f6b044115b..305071a6b3f 100644 --- a/spec/fixtures/api/schemas/variable.json +++ b/spec/fixtures/api/schemas/variable.json @@ -4,12 +4,14 @@ "id", "key", "value", + "masked", "protected" ], "properties": { "id": { "type": "integer" }, "key": { "type": "string" }, "value": { "type": "string" }, + "masked": { "type": "boolean" }, "protected": { "type": "boolean" }, "environment_scope": { "type": "string", "optional": true } }, diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js index fe791aa2b74..aa1fa0373db 100644 --- a/spec/frontend/ide/lib/files_spec.js +++ b/spec/frontend/ide/lib/files_spec.js @@ -1,5 +1,5 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; -import { decorateFiles, splitParent } from '~/ide/lib/files'; +import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files'; import { decorateData } from '~/ide/stores/utils'; const TEST_BRANCH_ID = 'lorem-ipsum'; @@ -20,7 +20,7 @@ const createEntries = paths => { id: path, name, path, - url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${path}`), + url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${escapeFileUrl(path)}`), type, previewMode: viewerInformationForPath(path), parentPath: parent, @@ -28,7 +28,7 @@ const createEntries = paths => { ? parentEntry.url : createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}`), }), - tree: children.map(childName => jasmine.objectContaining({ name: childName })), + tree: children.map(childName => expect.objectContaining({ name: childName })), }; return acc; @@ -36,10 +36,10 @@ const createEntries = paths => { const entries = paths.reduce(createEntry, {}); - // Wrap entries in jasmine.objectContaining. + // Wrap entries in expect.objectContaining. // We couldn't do this earlier because we still need to select properties from parent entries. return Object.keys(entries).reduce((acc, key) => { - acc[key] = jasmine.objectContaining(entries[key]); + acc[key] = expect.objectContaining(entries[key]); return acc; }, {}); @@ -47,13 +47,14 @@ const createEntries = paths => { describe('IDE lib decorate files', () => { it('creates entries and treeList', () => { - const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'README.md']; + const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'app/#weird#file?.txt', 'README.md']; const expectedEntries = createEntries([ - { path: 'app', type: 'tree', children: ['assets', 'bugs.js'] }, + { path: 'app', type: 'tree', children: ['assets', '#weird#file?.txt', 'bugs.js'] }, { path: 'app/assets', type: 'tree', children: ['apples'] }, { path: 'app/assets/apples', type: 'tree', children: ['foo.js'] }, { path: 'app/assets/apples/foo.js', type: 'blob', children: [] }, { path: 'app/bugs.js', type: 'blob', children: [] }, + { path: 'app/#weird#file?.txt', type: 'blob', children: [] }, { path: 'README.md', type: 'blob', children: [] }, ]); @@ -64,7 +65,7 @@ describe('IDE lib decorate files', () => { }); // Here we test the keys and then each key/value individually because `expect(entries).toEqual(expectedEntries)` - // was taking a very long time for some reason. Probably due to large objects and nested `jasmine.objectContaining`. + // was taking a very long time for some reason. Probably due to large objects and nested `expect.objectContaining`. const entryKeys = Object.keys(entries); expect(entryKeys).toEqual(Object.keys(expectedEntries)); diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 2bc3933809f..6808ed86c9a 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -230,5 +230,18 @@ describe BlobHelper do expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master") end + + it 'escapes special characters' do + Rails.application.routes.default_url_options[:script_name] = nil + + expect(helper.ide_edit_path(project, "testing/#hashes", "readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/readme.md%23test") + expect(helper.ide_edit_path(project, "testing/#hashes", "src#/readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/src%23/readme.md%23test") + end + + it 'does not escape "/" character' do + Rails.application.routes.default_url_options[:script_name] = nil + + expect(helper.ide_edit_path(project, "testing/slashes", "readme.md/")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md/") + end end end diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index 6e6b3e6950b..d08ee41802b 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -103,4 +103,18 @@ describe('Board component', () => { }) .catch(done.fail); }); + + it('does render add issue button', () => { + expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull(); + }); + + it('does not render add issue button when list type is blank', done => { + vm.list.type = 'blank'; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.issue-count-badge-add-button')).toBeNull(); + + done(); + }); + }); }); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 70f49469300..394e60fc22c 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -127,20 +127,25 @@ describe('VariableList', () => { variableList.init(); }); - it('should add another row when editing the last rows protected checkbox', done => { + it('should not add another row when editing the last rows protected checkbox', done => { const $row = $wrapper.find('.js-row:last-child'); $row.find('.ci-variable-protected-item .js-project-feature-toggle').click(); getSetTimeoutPromise() .then(() => { - expect($wrapper.find('.js-row').length).toBe(2); + expect($wrapper.find('.js-row').length).toBe(1); + }) + .then(done) + .catch(done.fail); + }); - // Check for the correct default in the new row - const $protectedInput = $wrapper - .find('.js-row:last-child') - .find('.js-ci-variable-input-protected'); + it('should not add another row when editing the last rows masked checkbox', done => { + const $row = $wrapper.find('.js-row:last-child'); + $row.find('.ci-variable-masked-item .js-project-feature-toggle').click(); - expect($protectedInput.val()).toBe('false'); + getSetTimeoutPromise() + .then(() => { + expect($wrapper.find('.js-row').length).toBe(1); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index d9b298e84da..ef4589ada48 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -141,18 +141,16 @@ describe('DiffFile', () => { it('should have too large warning and blob link', done => { const BLOB_LINK = '/file/view/path'; vm.file.viewer.error = diffViewerErrors.too_large; + vm.file.viewer.error_message = + 'This source diff could not be displayed because it is too large'; vm.file.view_path = BLOB_LINK; + vm.file.renderIt = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain( 'This source diff could not be displayed because it is too large', ); - expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined(); - expect( - vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK), - ).toBeGreaterThan(-1); - done(); }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index d604e90b529..0cfcc994234 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -128,87 +128,33 @@ describe('noteActions', () => { }); }); - describe('with feature flag replyToIndividualNotes enabled', () => { + describe('for showReply = true', () => { beforeEach(() => { - gon.features = { - replyToIndividualNotes: true, - }; - }); - - afterEach(() => { - gon.features = {}; - }); - - describe('for showReply = true', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: true, - }); - }); - - it('shows a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); - - expect(replyButton.exists()).toBe(true); + wrapper = shallowMountNoteActions({ + ...props, + showReply: true, }); }); - describe('for showReply = false', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: false, - }); - }); - - it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); + it('shows a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); - expect(replyButton.exists()).toBe(false); - }); + expect(replyButton.exists()).toBe(true); }); }); - describe('with feature flag replyToIndividualNotes disabled', () => { + describe('for showReply = false', () => { beforeEach(() => { - gon.features = { - replyToIndividualNotes: false, - }; - }); - - afterEach(() => { - gon.features = {}; - }); - - describe('for showReply = true', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: true, - }); - }); - - it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); - - expect(replyButton.exists()).toBe(false); + wrapper = shallowMountNoteActions({ + ...props, + showReply: false, }); }); - describe('for showReply = false', () => { - beforeEach(() => { - wrapper = shallowMountNoteActions({ - ...props, - showReply: false, - }); - }); - - it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); + it('does not show a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); - expect(replyButton.exists()).toBe(false); - }); + expect(replyButton.exists()).toBe(false); }); }); }); diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js new file mode 100644 index 00000000000..a89952ee435 --- /dev/null +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js @@ -0,0 +1,167 @@ +import $ from 'jquery'; +import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars +import TimezoneDropdown, { + formatUtcOffset, + formatTimezone, +} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; + +describe('Timezone Dropdown', function() { + preloadFixtures('pipeline_schedules/edit.html'); + + let $inputEl = null; + let $dropdownEl = null; + let $wrapper = null; + const tzListSel = '.dropdown-content ul li a.is-active'; + + describe('Initialize', () => { + describe('with dropdown already loaded', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.dropdown'); + $inputEl = $('#schedule_cron_timezone'); + $dropdownEl = $('.js-timezone-dropdown'); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + }); + }); + + it('can take an $inputEl in the constructor', () => { + const tzStr = '[UTC + 5.5] Sri Jayawardenepura'; + const tzValue = 'Asia/Colombo'; + + expect($inputEl.val()).toBe('UTC'); + + $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click'); + + const val = $inputEl.val(); + + expect(val).toBe(tzValue); + expect(val).not.toBe('UTC'); + }); + + it('will format data array of timezones into a list of offsets', () => { + const data = $dropdownEl.data('data'); + const formatted = $wrapper.find(tzListSel).text(); + + data.forEach(item => { + expect(formatted).toContain(formatTimezone(item)); + }); + }); + + it('will default the timezone to UTC', () => { + const tz = $inputEl.val(); + + expect(tz).toBe('UTC'); + }); + }); + + describe('without dropdown loaded', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.dropdown'); + $inputEl = $('#schedule_cron_timezone'); + $dropdownEl = $('.js-timezone-dropdown'); + }); + + it('will populate the list of UTC offsets after the dropdown is loaded', () => { + expect($wrapper.find(tzListSel).length).toEqual(0); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + }); + + expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length); + }); + + it('will call a provided handler when a new timezone is selected', () => { + const onSelectTimezone = jasmine.createSpy('onSelectTimezoneMock'); + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + onSelectTimezone, + }); + + $wrapper + .find(tzListSel) + .first() + .trigger('click'); + + expect(onSelectTimezone).toHaveBeenCalled(); + }); + }); + }); + + describe('formatUtcOffset', () => { + it('will convert negative utc offsets in seconds to hours and minutes', () => { + expect(formatUtcOffset(-21600)).toEqual('- 6'); + }); + + it('will convert positive utc offsets in seconds to hours and minutes', () => { + expect(formatUtcOffset(25200)).toEqual('+ 7'); + expect(formatUtcOffset(49500)).toEqual('+ 13.75'); + }); + + it('will return 0 when given a string', () => { + expect(formatUtcOffset('BLAH')).toEqual('0'); + expect(formatUtcOffset('$%$%')).toEqual('0'); + }); + + it('will return 0 when given an array', () => { + expect(formatUtcOffset(['an', 'array'])).toEqual('0'); + }); + + it('will return 0 when given an object', () => { + expect(formatUtcOffset({ some: '', object: '' })).toEqual('0'); + }); + + it('will return 0 when given null', () => { + expect(formatUtcOffset(null)).toEqual('0'); + }); + + it('will return 0 when given undefined', () => { + expect(formatUtcOffset(undefined)).toEqual('0'); + }); + + it('will return 0 when given empty input', () => { + expect(formatUtcOffset('')).toEqual('0'); + }); + }); + + describe('formatTimezone', () => { + it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => { + expect( + formatTimezone({ + name: 'Chatham Is.', + offset: 49500, + identifier: 'Pacific/Chatham', + }), + ).toEqual('[UTC + 13.75] Chatham Is.'); + }); + + it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => { + expect( + formatTimezone({ + name: 'Saskatchewan', + offset: -21600, + identifier: 'America/Regina', + }), + ).toEqual('[UTC - 6] Saskatchewan'); + }); + + it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => { + expect( + formatTimezone({ + name: 'Accra', + offset: 0, + identifier: 'Africa/Accra', + }), + ).toEqual('[UTC 0] Accra'); + }); + }); +}); diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js index 657e88ecb96..f46ea5a0499 100644 --- a/spec/javascripts/sidebar/todo_spec.js +++ b/spec/javascripts/sidebar/todo_spec.js @@ -116,7 +116,7 @@ describe('SidebarTodo', () => { const dataAttributes = { issuableId: '1', issuableType: 'epic', - originalTitle: 'Mark todo as done', + originalTitle: '', placement: 'left', container: 'body', boundary: 'viewport', @@ -130,6 +130,10 @@ describe('SidebarTodo', () => { }); }); + it('check button label computed property', () => { + expect(vm.buttonLabel).toEqual('Mark todo as done'); + }); + it('renders button label element when `collapsed` prop is `false`', () => { const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner'); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 02c476f2871..cd77b0ab815 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -15,6 +15,16 @@ describe('MRWidgetHeader', () => { gon.relative_url_root = ''; }); + const expectDownloadDropdownItems = () => { + const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches'); + const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff'); + + expect(downloadEmailPatchesEl.textContent.trim()).toEqual('Email patches'); + expect(downloadEmailPatchesEl.getAttribute('href')).toEqual('/mr/email-patches'); + expect(downloadPlainDiffEl.textContent.trim()).toEqual('Plain diff'); + expect(downloadPlainDiffEl.getAttribute('href')).toEqual('/mr/plainDiffPath'); + }; + describe('computed', () => { describe('shouldShowCommitsBehindText', () => { it('return true when there are divergedCommitsCount', () => { @@ -207,21 +217,7 @@ describe('MRWidgetHeader', () => { }); it('renders download dropdown with links', () => { - expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual( - 'Email patches', - ); - - expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual( - '/mr/email-patches', - ); - - expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual( - 'Plain diff', - ); - - expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual( - '/mr/plainDiffPath', - ); + expectDownloadDropdownItems(); }); }); @@ -250,10 +246,8 @@ describe('MRWidgetHeader', () => { expect(button).toEqual(null); }); - it('does not render download dropdown with links', () => { - expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null); - - expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null); + it('renders download dropdown with links', () => { + expectDownloadDropdownItems(); }); }); diff --git a/spec/lib/api/entities/job_request/image_spec.rb b/spec/lib/api/entities/job_request/image_spec.rb new file mode 100644 index 00000000000..092c181ae9c --- /dev/null +++ b/spec/lib/api/entities/job_request/image_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::JobRequest::Image do + let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]} + let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)} + let(:entity) { described_class.new(image) } + + subject { entity.as_json } + + it 'returns the image name' do + expect(subject[:name]).to eq 'image_name' + end + + it 'returns the entrypoint' do + expect(subject[:entrypoint]).to eq ['foo'] + end + + it 'returns the ports' do + expect(subject[:ports]).to eq ports + end + + context 'when the ports param is nil' do + let(:ports) { nil } + + it 'does not return the ports' do + expect(subject[:ports]).to be_nil + end + end +end diff --git a/spec/lib/api/entities/job_request/port_spec.rb b/spec/lib/api/entities/job_request/port_spec.rb new file mode 100644 index 00000000000..40ab4cd6231 --- /dev/null +++ b/spec/lib/api/entities/job_request/port_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::API::Entities::JobRequest::Port do + let(:port) { double(number: 80, protocol: 'http', name: 'name')} + let(:entity) { described_class.new(port) } + + subject { entity.as_json } + + it 'returns the port number' do + expect(subject[:number]).to eq 80 + end + + it 'returns if the port protocol' do + expect(subject[:protocol]).to eq 'http' + end + + it 'returns the port name' do + expect(subject[:name]).to eq 'name' + end +end diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 773a52cdfbc..6e20e0ef5c3 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do it 'populates fabricated object with the proper name attribute' do expect(subject.name).to eq(image_name) end + + it 'does not populate the ports' do + expect(subject.ports).to be_empty + end end context 'when image is defined as hash' do let(:entrypoint) { '/bin/sh' } - let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) } + + let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) } it 'fabricates an object of the proper class' do is_expected.to be_kind_of(described_class) @@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do expect(subject.name).to eq(image_name) expect(subject.entrypoint).to eq(entrypoint) end + + it 'populates the ports' do + port = subject.ports.first + expect(port.number).to eq 80 + expect(port.protocol).to eq 'http' + expect(port.name).to eq 'default_port' + end end context 'when image name is empty' do @@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do expect(subject.first).to be_kind_of(described_class) expect(subject.first.name).to eq(service_image_name) end + + it 'does not populate the ports' do + expect(subject.first.ports).to be_empty + end end context 'when service is defined as hash' do @@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do let(:service_command) { 'sleep 30' } let(:job) do create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, - alias: service_alias, command: service_command }] }) + alias: service_alias, command: service_command, ports: [80] }] }) end it 'fabricates an non-empty array of objects' do @@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.alias).to eq(service_alias) expect(subject.first.command).to eq(service_command) + + port = subject.first.ports.first + expect(port.number).to eq 80 + expect(port.protocol).to eq 'http' + expect(port.name).to eq 'default_port' end end diff --git a/spec/lib/gitlab/ci/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb new file mode 100644 index 00000000000..1413780dfa6 --- /dev/null +++ b/spec/lib/gitlab/ci/build/port_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Port do + subject { described_class.new(port) } + + context 'when port is defined as an integer' do + let(:port) { 80 } + + it 'populates the object' do + expect(subject.number).to eq 80 + expect(subject.protocol).to eq described_class::DEFAULT_PORT_PROTOCOL + expect(subject.name).to eq described_class::DEFAULT_PORT_NAME + end + end + + context 'when port is defined as hash' do + let(:port) { { number: 80, protocol: 'https', name: 'port_name' } } + + it 'populates the object' do + expect(subject.number).to eq 80 + expect(subject.protocol).to eq 'https' + expect(subject.name).to eq 'port_name' + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 1a4d9ed5517..1ebdda398b9 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do expect(entry.entrypoint).to be_nil end end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to be_nil + end + end end context 'when configuration is a hash' do @@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do expect(entry.entrypoint).to eq %w(/bin/sh run) end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } } + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("image config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to eq ports + end + end + end + end end context 'when entry value is not correct' do @@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'image config should be a hash or a string' + expect(entry.errors.first) + .to match /config should be a hash or a string/ end end @@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'image config contains unknown keys: non_existing' + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ end end diff --git a/spec/lib/gitlab/ci/config/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb new file mode 100644 index 00000000000..5f8f294334e --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Port do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is a string' do + let(:config) { 80 } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq(number: 80) + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "is nil" do + expect(entry.protocol).to be_nil + end + end + + describe '#name' do + it "is nil" do + expect(entry.name).to be_nil + end + end + end + + context 'when configuration is a hash' do + context 'with the complete hash' do + let(:config) do + { number: 80, + protocol: 'http', + name: 'foobar' } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq config + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "returns port protocol" do + expect(entry.protocol).to eq 'http' + end + end + + describe '#name' do + it "returns port name" do + expect(entry.name).to eq 'foobar' + end + end + end + + context 'with only the port number' do + let(:config) { { number: 80 } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq(number: 80) + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "is nil" do + expect(entry.protocol).to be_nil + end + end + + describe '#name' do + it "is nil" do + expect(entry.name).to be_nil + end + end + end + + context 'without the number' do + let(:config) { { protocol: 'http' } } + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + end + + context 'when configuration is invalid' do + let(:config) { '80' } + + describe '#valid?' do + it 'is valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when protocol' do + let(:config) { { number: 80, protocol: protocol, name: 'foobar' } } + + context 'is http' do + let(:protocol) { 'http' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'is https' do + let(:protocol) { 'https' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'is neither http nor https' do + let(:protocol) { 'foo' } + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors).to include("port protocol should be http or https") + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/ports_spec.rb b/spec/lib/gitlab/ci/config/entry/ports_spec.rb new file mode 100644 index 00000000000..2063bd1d86c --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/ports_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Ports do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is valid' do + let(:config) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq(config) + end + end + end + + context 'when configuration is invalid' do + let(:config) { 'postgresql:9.5' } + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + + context 'when any of the ports' do + before do + expect(entry).not_to be_valid + expect(entry.errors.count).to eq 1 + end + + context 'have the same name' do + let(:config) do + [{ number: 80, protocol: 'http', name: 'foobar' }, + { number: 81, protocol: 'http', name: 'foobar' }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors.first).to match /each port name must be different/ + end + end + end + + context 'have the same port' do + let(:config) do + [{ number: 80, protocol: 'http', name: 'foobar' }, + { number: 80, protocol: 'http', name: 'foobar1' }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors.first).to match /each port number can only be referenced once/ + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 9ebf947a751..d5bd139b5f1 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do expect(entry.command).to be_nil end end + + describe '#ports' do + it "returns service's ports" do + expect(entry.ports).to be_nil + end + end end context 'when configuration is a hash' do @@ -81,6 +87,40 @@ describe Gitlab::Ci::Config::Entry::Service do expect(entry.entrypoint).to eq %w(/bin/sh run) end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } + end + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to eq ports + end + end + end + end end context 'when entry value is not correct' do @@ -88,8 +128,8 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'service config should be a hash or a string' + expect(entry.errors.first) + .to match /config should be a hash or a string/ end end @@ -105,8 +145,8 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'service config contains unknown keys: non_existing' + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ end end @@ -116,4 +156,26 @@ describe Gitlab::Ci::Config::Entry::Service do end end end + + context 'when service has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) do + { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } + end + + it 'alias field is mandatory' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service alias can't be blank") + end + end + + context 'when service does not have ports' do + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) } + end + + it 'alias field is optional' do + expect(entry).to be_valid + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index 7c4319aee63..d5a1316f665 100644 --- a/spec/lib/gitlab/ci/config/entry/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do end end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] } + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }]) + end + end + + describe 'services alias' do + context 'when they are not unique' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'postgres_old', ports: [81] }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("services config alias must be unique in services with ports") + end + end + end + + context 'when they are unique' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'ruby', ports: [81] }] + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when one of the duplicated alias is in a service without ports' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'postgres_old' }] + end + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when there are not any ports' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old' }, + { name: 'ruby', alias: 'postgres_old' }] + end + + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 18f255c1ab7..00b2753c5fc 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -123,6 +123,63 @@ describe Gitlab::Ci::Config do ) end end + + context 'when ports have been set' do + context 'in the main image' do + let(:yml) do + <<-EOS + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("image config contains disallowed keys: ports") + end + end + + context 'in the job image' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("jobs:test:image config contains disallowed keys: ports") + end + end + + context 'in the services' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: ruby:2.2 + services: + - name: test + alias: test + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("jobs:test:services:service config contains disallowed keys: ports") + end + end + end end context "when using 'include' directive" do diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index fbbd58280a9..4e3681cd943 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -4,6 +4,9 @@ require 'spec_helper' describe "CI YML Templates" do ABSTRACT_TEMPLATES = %w[Serverless].freeze + # These templates depend on the presence of the `project` + # param to enable processing of `include:` within CI config. + PROJECT_DEPENDENT_TEMPLATES = %w[Auto-DevOps DAST].freeze def self.concrete_templates Gitlab::Template::GitlabCiYmlTemplate.all.reject do |template| @@ -20,7 +23,10 @@ describe "CI YML Templates" do describe 'concrete templates with CI/CD jobs' do concrete_templates.each do |template| it "#{template.name} template should be valid" do - expect { Gitlab::Ci::YamlProcessor.new(template.content) } + # Trigger processing of included files + project = create(:project, :test_repo) if PROJECT_DEPENDENT_TEMPLATES.include?(template.name) + + expect { Gitlab::Ci::YamlProcessor.new(template.content, project: project) } .not_to raise_error end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 29638ef47c5..63a0d54dcfc 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1233,7 +1233,7 @@ module Gitlab config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string") end it "returns errors if job services parameter is not an array" do @@ -1247,7 +1247,7 @@ module Gitlab config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string") end it "returns error if job configuration is invalid" do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8ba6862392c..fc8f590068a 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -152,13 +152,14 @@ describe Gitlab::Git::Repository, :seed_helper do let(:append_sha) { true } let(:ref) { 'master' } let(:format) { nil } + let(:path) { nil } let(:expected_extension) { 'tar.gz' } let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" } let(:expected_path) { File.join(storage_path, cache_key, expected_filename) } let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" } - subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) } + subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) } it 'sets CommitId to the commit SHA' do expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID) @@ -176,6 +177,14 @@ describe Gitlab::Git::Repository, :seed_helper do expect(metadata['ArchivePath']).to eq(expected_path) end + context 'path is set' do + let(:path) { 'foo/bar' } + + it 'appends the path to the prefix' do + expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar") + end + end + context 'append_sha varies archive path and filename' do where(:append_sha, :ref, :expected_prefix) do sha = SeedRepo::LastCommit::ID diff --git a/spec/lib/gitlab/graphql/tracing_spec.rb b/spec/lib/gitlab/graphql/tracing_spec.rb deleted file mode 100644 index 6bae737d0f6..00000000000 --- a/spec/lib/gitlab/graphql/tracing_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::Graphql::Tracing do - let!(:graphql_duration_seconds) { double('Gitlab::Metrics::NullMetric') } - - before do - allow(Gitlab::Metrics) - .to receive(:histogram) - .with(:graphql_duration_seconds, 'GraphQL execution time') - .and_return(graphql_duration_seconds) - end - - it 'updates graphql histogram with expected labels' do - query = 'query { users { id } }' - - expect_metric('graphql.lex', 'lex') - expect_metric('graphql.parse', 'parse') - expect_metric('graphql.validate', 'validate') - expect_metric('graphql.analyze', 'analyze_multiplex') - expect_metric('graphql.execute', 'execute_query_lazy') - expect_metric('graphql.execute', 'execute_multiplex') - - GitlabSchema.execute(query) - end - - private - - def expect_metric(platform_key, key) - expect(graphql_duration_seconds) - .to receive(:observe) - .with({ platform_key: platform_key, key: key }, be > 0.0) - end -end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb new file mode 100644 index 00000000000..e70fde09edd --- /dev/null +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Transaction do + let(:transaction) { described_class.new } + let(:metric) { transaction.metrics[0] } + + let(:sensitive_tags) do + { + path: 'private', + branch: 'sensitive' + } + end + + shared_examples 'tag filter' do |sane_tags| + it 'filters potentially sensitive tags' do + expect(metric.tags).to eq(sane_tags) + end + end + + describe '#duration' do + it 'returns the duration of a transaction in seconds' do + transaction.run { } + + expect(transaction.duration).to be > 0 + end + end + + describe '#allocated_memory' do + it 'returns the allocated memory in bytes' do + transaction.run { 'a' * 32 } + + expect(transaction.allocated_memory).to be_a_kind_of(Numeric) + end + end + + describe '#run' do + it 'yields the supplied block' do + expect { |b| transaction.run(&b) }.to yield_control + end + + it 'stores the transaction in the current thread' do + transaction.run do + expect(described_class.current).to eq(transaction) + end + end + + it 'removes the transaction from the current thread upon completion' do + transaction.run { } + + expect(described_class.current).to be_nil + end + end + + describe '#add_metric' do + it 'adds a metric to the transaction' do + transaction.add_metric('foo', value: 1) + + expect(metric.series).to eq('rails_foo') + expect(metric.tags).to eq({}) + expect(metric.values).to eq(value: 1) + end + + context 'with sensitive tags' do + before do + transaction + .add_metric('foo', { value: 1 }, **sensitive_tags.merge(sane: 'yes')) + end + + it_behaves_like 'tag filter', sane: 'yes' + end + end + + describe '#method_call_for' do + it 'returns a MethodCall' do + method = transaction.method_call_for('Foo#bar', :Foo, '#bar') + + expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall) + end + end + + describe '#increment' do + it 'increments a counter' do + transaction.increment(:time, 1) + transaction.increment(:time, 2) + + values = metric_values(time: 3) + + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#set' do + it 'sets a value' do + transaction.set(:number, 10) + + values = metric_values(number: 10) + + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#finish' do + it 'tracks the transaction details and submits them to Sidekiq' do + expect(transaction).to receive(:track_self) + expect(transaction).to receive(:submit) + + transaction.finish + end + end + + describe '#track_self' do + it 'adds a metric for the transaction itself' do + values = metric_values + + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#submit' do + it 'submits the metrics to Sidekiq' do + transaction.track_self + + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([an_instance_of(Hash)]) + + transaction.submit + end + + it 'adds the action as a tag for every metric' do + allow(transaction) + .to receive(:labels) + .and_return(controller: 'Foo', action: 'bar') + + transaction.track_self + + hash = { + series: 'rails_transactions', + tags: { action: 'Foo#bar' }, + values: metric_values, + timestamp: a_kind_of(Integer) + } + + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([hash]) + + transaction.submit + end + + it 'does not add an action tag for events' do + allow(transaction) + .to receive(:labels) + .and_return(controller: 'Foo', action: 'bar') + + transaction.add_event(:meow) + + hash = { + series: 'events', + tags: { event: :meow }, + values: { count: 1 }, + timestamp: a_kind_of(Integer) + } + + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([hash]) + + transaction.submit + end + end + + describe '#add_event' do + it 'adds a metric' do + transaction.add_event(:meow) + + expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric) + end + + it "does not prefix the metric's series name" do + transaction.add_event(:meow) + + expect(metric.series).to eq(described_class::EVENT_SERIES) + end + + it 'tracks a counter for every event' do + transaction.add_event(:meow) + + expect(metric.values).to eq(count: 1) + end + + it 'tracks the event name' do + transaction.add_event(:meow) + + expect(metric.tags).to eq(event: :meow) + end + + it 'allows tracking of custom tags' do + transaction.add_event(:meow, animal: 'cat') + + expect(metric.tags).to eq(event: :meow, animal: 'cat') + end + + context 'with sensitive tags' do + before do + transaction.add_event(:meow, **sensitive_tags.merge(sane: 'yes')) + end + + it_behaves_like 'tag filter', event: :meow, sane: 'yes' + end + end + + private + + def metric_values(opts = {}) + { + duration: 0.0, + allocated_memory: a_kind_of(Numeric) + }.merge(opts) + end +end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb index 5a88b23aa82..a6589f0c0a3 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb @@ -9,9 +9,35 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery do let(:query_params) { [environment.id] } it 'queries using specific time' do - expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) - + expect(client).to receive(:query_range) + .with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) expect(query_result).not_to be_nil end + + context 'when start and end time parameters are provided' do + let(:query_params) { [environment.id, start_time, end_time] } + + context 'as unix timestamps' do + let(:start_time) { 4.hours.ago.to_f } + let(:end_time) { 2.hours.ago.to_f } + + it 'queries using the provided times' do + expect(client).to receive(:query_range) + .with(anything, start: start_time, stop: end_time) + expect(query_result).not_to be_nil + end + end + + context 'as Date/Time objects' do + let(:start_time) { 4.hours.ago } + let(:end_time) { 2.hours.ago } + + it 'queries using the provided times converted to unix' do + expect(client).to receive(:query_range) + .with(anything, start: start_time.to_f, stop: end_time.to_f) + expect(query_result).not_to be_nil + end + end + end end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index d88086b01b1..fed7834e2a9 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -16,20 +16,12 @@ describe Gitlab::Workhorse do let(:ref) { 'master' } let(:format) { 'zip' } let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path } - let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) } - let(:gitaly_params) do - base_params.merge( - 'GitalyServer' => { - 'address' => Gitlab::GitalyClient.address(project.repository_storage), - 'token' => Gitlab::GitalyClient.token(project.repository_storage) - }, - 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys - ) - end + let(:path) { 'some/path' } + let(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: nil, path: path) } let(:cache_disabled) { false } subject do - described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil) + described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil, path: path) end before do @@ -41,7 +33,22 @@ describe Gitlab::Workhorse do expect(key).to eq('Gitlab-Workhorse-Send-Data') expect(command).to eq('git-archive') - expect(params).to include(gitaly_params) + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'ArchivePath' => metadata['ArchivePath'], + 'GetArchiveRequest' => Base64.urlsafe_encode64( + Gitaly::GetArchiveRequest.new( + repository: repository.gitaly_repository, + commit_id: metadata['CommitId'], + prefix: metadata['ArchivePrefix'], + format: Gitaly::GetArchiveRequest::Format::ZIP, + path: path + ).to_proto + ) + }.deep_stringify_keys) end context 'when archive caching is disabled' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 3c8897ed37c..5fa1369c00a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -30,6 +30,19 @@ describe Notify do description: 'My awesome description!') end + describe 'with HTML-encoded entities' do + before do + described_class.test_email('test@test.com', 'Subject', 'Some body with —').deliver + end + + subject { ActionMailer::Base.deliveries.last } + + it 'retains 7bit encoding' do + expect(subject.body.ascii_only?).to eq(true) + expect(subject.body.encoding).to eq('7bit') + end + end + context 'for a project' do shared_examples 'an assignee email' do it 'is sent to the assignee as the author' do diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb index 7148261b1e4..25a2d290f76 100644 --- a/spec/models/concerns/prometheus_adapter_spec.rb +++ b/spec/models/concerns/prometheus_adapter_spec.rb @@ -6,14 +6,15 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do include PrometheusHelpers include ReactiveCachingHelpers - class TestClass - include PrometheusAdapter - end - let(:project) { create(:prometheus_project) } let(:service) { project.prometheus_service } - let(:described_class) { TestClass } + let(:described_class) do + Class.new do + include PrometheusAdapter + end + end + let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery } describe '#query' do @@ -76,6 +77,28 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do end end end + + describe 'additional_metrics' do + let(:additional_metrics_environment_query) { Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery } + let(:environment) { build_stubbed(:environment, slug: 'env-slug') } + let(:time_window) { [1552642245.067, 1552642095.831] } + + around do |example| + Timecop.freeze { example.run } + end + + context 'with valid data' do + subject { service.query(:additional_metrics_environment, environment, *time_window) } + + before do + stub_reactive_cache(service, prometheus_data, additional_metrics_environment_query, environment.id, *time_window) + end + + it 'returns reactive data' do + expect(subject).to eq(prometheus_data) + end + end + end end describe '#calculate_reactive_cache' do @@ -120,4 +143,24 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do end end end + + describe '#build_query_args' do + subject { service.build_query_args(*args) } + + context 'when active record models are included' do + let(:args) { [double(:environment, id: 12)] } + + it 'serializes by id' do + is_expected.to eq [12] + end + end + + context 'when args are safe for serialization' do + let(:args) { ['stringy arg', 5, 6.0, :symbolic_arg] } + + it 'does nothing' do + is_expected.to eq args + end + end + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 448ed35cb1e..cfe7c7ef0b0 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -592,7 +592,9 @@ describe Environment do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do it 'returns the terminals from the deployment service' do - expect(project.deployment_platform) + deployment_platform_target = Gitlab.ee? ? environment : project + + expect(deployment_platform_target.deployment_platform) .to receive(:terminals).with(environment) .and_return(:fake_terminals) @@ -685,7 +687,8 @@ describe Environment do describe '#additional_metrics' do let(:project) { create(:prometheus_project) } - subject { environment.additional_metrics } + let(:metric_params) { [] } + subject { environment.additional_metrics(*metric_params) } context 'when the environment has additional metrics' do before do @@ -693,12 +696,26 @@ describe Environment do end it 'returns the additional metrics from the deployment service' do - expect(environment.prometheus_adapter).to receive(:query) - .with(:additional_metrics_environment, environment) - .and_return(:fake_metrics) + expect(environment.prometheus_adapter) + .to receive(:query) + .with(:additional_metrics_environment, environment) + .and_return(:fake_metrics) is_expected.to eq(:fake_metrics) end + + context 'when time window arguments are provided' do + let(:metric_params) { [1552642245.067, Time.now] } + + it 'queries with the expected parameters' do + expect(environment.prometheus_adapter) + .to receive(:query) + .with(:additional_metrics_environment, environment, *metric_params.map(&:to_f)) + .and_return(:fake_metrics) + + is_expected.to eq(:fake_metrics) + end + end end context 'when the environment does not have metrics' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 892fdc4e4e9..6f34ef9c1bc 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -805,6 +805,14 @@ describe MergeRequest do expect(merge_request.commits).not_to be_empty expect(merge_request.related_notes.count).to eq(3) end + + it "excludes system notes for commits" do + system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id, + project: merge_request.project) + + expect(merge_request.related_notes.count).to eq(2) + expect(merge_request.related_notes).not_to include(system_note) + end end describe '#for_fork?' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2158d3cf3e6..33e514cd7b9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2143,6 +2143,15 @@ describe Project do expect(project.add_import_job).to eq(import_jid) end + + context 'without repository' do + it 'schedules RepositoryImportWorker' do + project = create(:project, import_url: generate(:url)) + + expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid) + expect(project.add_import_job).to eq(import_jid) + end + end end context 'not forked' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index a2d4fad9292..77c88a04cde 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -195,6 +195,30 @@ describe ProjectTeam do end end + describe '#add_users' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:project) { create(:project) } + + it 'add the given users to the team' do + project.team.add_users([user1, user2], :reporter) + + expect(project.team.reporter?(user1)).to be(true) + expect(project.team.reporter?(user2)).to be(true) + end + end + + describe '#add_user' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + it 'add the given user to the team' do + project.team.add_user(user, :reporter) + + expect(project.team.reporter?(user)).to be(true) + end + end + describe "#human_max_access" do it 'returns Maintainer role' do user = create(:user) diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 7ea67f31534..2525a6aebe0 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -1,4 +1,3 @@ -# coding: utf-8 # frozen_string_literal: true require "spec_helper" diff --git a/spec/presenters/ci/bridge_presenter_spec.rb b/spec/presenters/ci/bridge_presenter_spec.rb new file mode 100644 index 00000000000..986818a7b9e --- /dev/null +++ b/spec/presenters/ci/bridge_presenter_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Ci::BridgePresenter do + set(:project) { create(:project) } + set(:pipeline) { create(:ci_pipeline, project: project) } + set(:bridge) { create(:ci_bridge, pipeline: pipeline, status: :failed) } + + subject(:presenter) do + described_class.new(bridge) + end + + it 'presents information about recoverable state' do + expect(presenter).to be_recoverable + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4259fda7f04..1d139200535 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -729,6 +729,14 @@ describe API::MergeRequests do end describe "GET /projects/:id/merge_requests/:merge_request_iid" do + it 'matches json schema' do + merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/merge_request') + end + it 'exposes known attributes' do create(:award_emoji, :downvote, awardable: merge_request) create(:award_emoji, :upvote, awardable: merge_request) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 52599db9a9e..c26d31c5e0d 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -399,6 +399,13 @@ describe API::Pipelines do describe 'GET /projects/:id/pipelines/:pipeline_id' do context 'authorized user' do + it 'exposes known attributes' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pipeline/detail') + end + it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 3ccedd8dd06..5fdc7c64030 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -470,11 +470,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(json_response['token']).to eq(job.token) expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh', 'ports' => [] }) expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, - 'alias' => nil, 'command' => nil }, + 'alias' => nil, 'command' => nil, 'ports' => [] }, { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', - 'alias' => 'docker', 'command' => 'sleep 30' }]) + 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to eq(expected_cache) @@ -853,6 +853,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + describe 'port support' do + let(:job) { create(:ci_build, pipeline: pipeline, options: options) } + + context 'when job image has ports' do + let(:options) do + { + image: { + name: 'ruby', + ports: [80] + }, + services: ['mysql'] + } + end + + it 'returns the image ports' do + request_job + + expect(response).to have_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]), + 'services' => all(a_hash_including('name' => 'mysql'))) + end + end + + context 'when job services settings has ports' do + let(:options) do + { + image: 'ruby', + services: [ + { + name: 'tomcat', + ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }] + } + ] + } + end + + it 'returns the service ports' do + request_job + + expect(response).to have_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => a_hash_including('name' => 'ruby'), + 'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }]))) + end + end + end + def request_job(token = runner.token, **params) new_params = params.merge(token: token, last_update: last_update) post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 24707cd2d41..866d709d446 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -306,6 +306,56 @@ describe Ci::CreatePipelineService do it_behaves_like 'a failed pipeline' end + + context 'when config has ports' do + context 'in the main image' do + let(:ci_yaml) do + <<-EOS + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it_behaves_like 'a failed pipeline' + end + + context 'in the job image' do + let(:ci_yaml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it_behaves_like 'a failed pipeline' + end + + context 'in the service' do + let(:ci_yaml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: ruby:2.2 + services: + - name: test + ports: + - 80 + EOS + end + + it_behaves_like 'a failed pipeline' + end + end end context 'when commit contains a [ci skip] directive' do diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 19446ce1cf8..1cca89d31d7 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -33,14 +33,22 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end end - shared_examples 'error logging' do + shared_examples 'error handling' do context 'when installation raises a Kubeclient::HttpError' do let(:cluster) { create(:cluster, :provided_by_user, :project) } + let(:logger) { service.send(:logger) } + let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) } before do application.update!(cluster: cluster) - expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + expect(service).to receive(:installation_phase).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'Unauthorized' } + let(:error_code) { 401 } end it 'shows the response code from the error' do @@ -49,12 +57,6 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do expect(application).to be_errored.or(be_update_errored) expect(application.status_reason).to eq('Kubernetes error: 401') end - - it 'should log error' do - expect(service.send(:logger)).to receive(:error) - - service.execute - end end end @@ -66,7 +68,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do context 'when application is updating' do let(:application) { create(:clusters_applications_helm, :updating) } - include_examples 'error logging' + include_examples 'error handling' RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } @@ -127,7 +129,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context 'when application is installing' do - include_examples 'error logging' + include_examples 'error handling' RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb index 018d9822d3e..db0c33a95b2 100644 --- a/spec/services/clusters/applications/install_service_spec.rb +++ b/spec/services/clusters/applications/install_service_spec.rb @@ -39,51 +39,34 @@ describe Clusters::Applications::InstallService do expect(helm_client).to receive(:install).with(install_command).and_raise(error) end + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'system failure' } + let(:error_code) { 500 } + end + it 'make the application errored' do service.execute expect(application).to be_errored expect(application.status_reason).to match('Kubernetes error: 500') end - - it 'logs errors' do - expect(service.send(:logger)).to receive(:error).with( - { - exception: 'Kubeclient::HttpError', - message: 'system failure', - service: 'Clusters::Applications::InstallService', - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: 500 - } - ) - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( - error, - extra: { - exception: 'Kubeclient::HttpError', - message: 'system failure', - service: 'Clusters::Applications::InstallService', - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: 500 - } - ) - - service.execute - end end context 'a non kubernetes error happens' do let(:application) { create(:clusters_applications_helm, :scheduled) } - let(:error) { StandardError.new("something bad happened") } + let(:error) { StandardError.new('something bad happened') } before do expect(application).to receive(:make_installing!).once.and_raise(error) end + include_examples 'logs kubernetes errors' do + let(:error_name) { 'StandardError' } + let(:error_message) { 'something bad happened' } + let(:error_code) { nil } + end + it 'make the application errored' do expect(helm_client).not_to receive(:install) @@ -92,35 +75,6 @@ describe Clusters::Applications::InstallService do expect(application).to be_errored expect(application.status_reason).to eq("Can't start installation process.") end - - it 'logs errors' do - expect(service.send(:logger)).to receive(:error).with( - { - exception: 'StandardError', - error_code: nil, - message: 'something bad happened', - service: 'Clusters::Applications::InstallService', - app_id: application.id, - project_ids: application.cluster.projects.pluck(:id), - group_ids: [] - } - ) - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( - error, - extra: { - exception: 'StandardError', - error_code: nil, - message: 'something bad happened', - service: 'Clusters::Applications::InstallService', - app_id: application.id, - project_ids: application.cluster.projects.pluck(:id), - group_ids: [] - } - ) - - service.execute - end end end end diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb index d4ee3243b84..10b1379a127 100644 --- a/spec/services/clusters/applications/patch_service_spec.rb +++ b/spec/services/clusters/applications/patch_service_spec.rb @@ -41,47 +41,30 @@ describe Clusters::Applications::PatchService do expect(helm_client).to receive(:update).with(update_command).and_raise(error) end + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'system failure' } + let(:error_code) { 500 } + end + it 'make the application errored' do service.execute expect(application).to be_update_errored expect(application.status_reason).to match('Kubernetes error: 500') end - - it 'logs errors' do - expect(service.send(:logger)).to receive(:error).with( - { - exception: 'Kubeclient::HttpError', - message: 'system failure', - service: 'Clusters::Applications::PatchService', - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: 500 - } - ) - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( - error, - extra: { - exception: 'Kubeclient::HttpError', - message: 'system failure', - service: 'Clusters::Applications::PatchService', - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: 500 - } - ) - - service.execute - end end context 'a non kubernetes error happens' do let(:application) { create(:clusters_applications_knative, :scheduled) } let(:error) { StandardError.new('something bad happened') } + include_examples 'logs kubernetes errors' do + let(:error_name) { 'StandardError' } + let(:error_message) { 'something bad happened' } + let(:error_code) { nil } + end + before do expect(application).to receive(:make_updating!).once.and_raise(error) end @@ -94,35 +77,6 @@ describe Clusters::Applications::PatchService do expect(application).to be_update_errored expect(application.status_reason).to eq("Can't start update process.") end - - it 'logs errors' do - expect(service.send(:logger)).to receive(:error).with( - { - exception: 'StandardError', - error_code: nil, - message: 'something bad happened', - service: 'Clusters::Applications::PatchService', - app_id: application.id, - project_ids: application.cluster.projects.pluck(:id), - group_ids: [] - } - ) - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( - error, - extra: { - exception: 'StandardError', - error_code: nil, - message: 'something bad happened', - service: 'Clusters::Applications::PatchService', - app_id: application.id, - project_ids: application.cluster.projects.pluck(:id), - group_ids: [] - } - ) - - service.execute - end end end end diff --git a/spec/services/clusters/applications/upgrade_service_spec.rb b/spec/services/clusters/applications/upgrade_service_spec.rb index 1822fc38dbd..dd2e6e94e4f 100644 --- a/spec/services/clusters/applications/upgrade_service_spec.rb +++ b/spec/services/clusters/applications/upgrade_service_spec.rb @@ -41,41 +41,18 @@ describe Clusters::Applications::UpgradeService do expect(helm_client).to receive(:update).with(install_command).and_raise(error) end + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'system failure' } + let(:error_code) { 500 } + end + it 'make the application errored' do service.execute expect(application).to be_update_errored expect(application.status_reason).to match('Kubernetes error: 500') end - - it 'logs errors' do - expect(service.send(:logger)).to receive(:error).with( - { - exception: 'Kubeclient::HttpError', - message: 'system failure', - service: 'Clusters::Applications::UpgradeService', - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: 500 - } - ) - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( - error, - extra: { - exception: 'Kubeclient::HttpError', - message: 'system failure', - service: 'Clusters::Applications::UpgradeService', - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: 500 - } - ) - - service.execute - end end context 'a non kubernetes error happens' do @@ -86,6 +63,12 @@ describe Clusters::Applications::UpgradeService do expect(application).to receive(:make_updating!).once.and_raise(error) end + include_examples 'logs kubernetes errors' do + let(:error_name) { 'StandardError' } + let(:error_message) { 'something bad happened' } + let(:error_code) { nil } + end + it 'make the application errored' do expect(helm_client).not_to receive(:update) @@ -94,35 +77,6 @@ describe Clusters::Applications::UpgradeService do expect(application).to be_update_errored expect(application.status_reason).to eq("Can't start upgrade process.") end - - it 'logs errors' do - expect(service.send(:logger)).to receive(:error).with( - { - exception: 'StandardError', - error_code: nil, - message: 'something bad happened', - service: 'Clusters::Applications::UpgradeService', - app_id: application.id, - project_ids: application.cluster.projects.pluck(:id), - group_ids: [] - } - ) - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( - error, - extra: { - exception: 'StandardError', - error_code: nil, - message: 'something bad happened', - service: 'Clusters::Applications::UpgradeService', - app_id: application.id, - project_ids: application.cluster.projects.pluck(:id), - group_ids: [] - } - ) - - service.execute - end end end end diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index e151db5827f..2d960fc9f08 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -31,6 +31,20 @@ describe Git::TagPushService do end end + describe 'System Hooks' do + let!(:push_data) { service.tap(&:execute).push_data } + + it "executes system hooks after pushing a tag" do + expect_next_instance_of(SystemHooksService) do |system_hooks_service| + expect(system_hooks_service) + .to receive(:execute_hooks) + .with(push_data, :tag_push_hooks) + end + + service.execute + end + end + describe "Pipelines" do subject { service.execute } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 55e7b46248b..dc5d1cf2f04 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -263,19 +263,6 @@ describe MergeRequests::CreateService do expect(merge_request.actual_head_pipeline).to be_merge_request_event end end - - context "when the 'ci_merge_request_pipeline' feature flag is disabled" do - before do - stub_feature_flags(ci_merge_request_pipeline: false) - end - - it 'does not create a detached merge request pipeline' do - expect(merge_request).to be_persisted - - merge_request.reload - expect(merge_request.merge_request_pipelines.count).to eq(0) - end - end end context "when .gitlab-ci.yml does not have merge_requests keywords" do diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 25cbac6d7ee..5cf3577f01f 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -230,17 +230,6 @@ describe MergeRequests::RefreshService do end.not_to change { @merge_request.merge_request_pipelines.count } end end - - context "when the 'ci_merge_request_pipeline' feature flag is disabled" do - before do - stub_feature_flags(ci_merge_request_pipeline: false) - end - - it 'does not create a detached merge request pipeline' do - expect { subject } - .not_to change { @merge_request.merge_request_pipelines.count } - end - end end context "when .gitlab-ci.yml does not have merge_requests keywords" do diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index af4daff336b..96fff20f7fb 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -128,37 +128,19 @@ describe Notes::BuildService do subject { described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute } - shared_examples 'an individual note reply' do - it 'builds another individual note' do - expect(subject).to be_valid - expect(subject).to be_a(Note) - expect(subject.discussion_id).not_to eq(note.discussion_id) - end + it 'sets the note up to be in reply to that note' do + expect(subject).to be_valid + expect(subject).to be_a(DiscussionNote) + expect(subject.discussion_id).to eq(note.discussion_id) end - context 'when reply_to_individual_notes is disabled' do - before do - stub_feature_flags(reply_to_individual_notes: false) - end - - it_behaves_like 'an individual note reply' - end + context 'when noteable does not support replies' do + let(:note) { create(:note_on_commit) } - context 'when reply_to_individual_notes is enabled' do - before do - stub_feature_flags(reply_to_individual_notes: true) - end - - it 'sets the note up to be in reply to that note' do + it 'builds another individual note' do expect(subject).to be_valid - expect(subject).to be_a(DiscussionNote) - expect(subject.discussion_id).to eq(note.discussion_id) - end - - context 'when noteable does not support replies' do - let(:note) { create(:note_on_commit) } - - it_behaves_like 'an individual note reply' + expect(subject).to be_a(Note) + expect(subject.discussion_id).not_to eq(note.discussion_id) end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 8d8e81173ff..bcbb8950910 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -298,41 +298,20 @@ describe Notes::CreateService do subject { described_class.new(project, user, reply_opts).execute } - context 'when reply_to_individual_notes is disabled' do - before do - stub_feature_flags(reply_to_individual_notes: false) - end - - it 'creates an individual note' do - expect(subject.type).to eq(nil) - expect(subject.discussion_id).not_to eq(existing_note.discussion_id) - end - - it 'does not convert existing note' do - expect { subject }.not_to change { existing_note.reload.type } - end + it 'creates a DiscussionNote in reply to existing note' do + expect(subject).to be_a(DiscussionNote) + expect(subject.discussion_id).to eq(existing_note.discussion_id) end - context 'when reply_to_individual_notes is enabled' do - before do - stub_feature_flags(reply_to_individual_notes: true) - end - - it 'creates a DiscussionNote in reply to existing note' do - expect(subject).to be_a(DiscussionNote) - expect(subject.discussion_id).to eq(existing_note.discussion_id) - end - - it 'converts existing note to DiscussionNote' do - expect do - existing_note + it 'converts existing note to DiscussionNote' do + expect do + existing_note - Timecop.freeze(Time.now + 1.minute) { subject } + Timecop.freeze(Time.now + 1.minute) { subject } - existing_note.reload - end.to change { existing_note.type }.from(nil).to('DiscussionNote') - .and change { existing_note.updated_at } - end + existing_note.reload + end.to change { existing_note.type }.from(nil).to('DiscussionNote') + .and change { existing_note.updated_at } end end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 8b0f9c8ade2..c7e5cca324f 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -10,6 +10,7 @@ describe QuickActions::InterpretService do let(:milestone) { create(:milestone, project: project, title: '9.10') } let(:commit) { create(:commit, project: project) } let(:inprogress) { create(:label, project: project, title: 'In Progress') } + let(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') } let(:bug) { create(:label, project: project, title: 'Bug') } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } let(:service) { described_class.new(project, developer) } @@ -94,6 +95,26 @@ describe QuickActions::InterpretService do end end + shared_examples 'multiword label name starting without ~' do + it 'fetches label ids and populates add_label_ids if content contains /label' do + helmchart # populate the label + _, updates = service.execute(content, issuable) + + expect(updates).to eq(add_label_ids: [helmchart.id]) + end + end + + shared_examples 'label name is included in the middle of another label name' do + it 'ignores the sublabel when the content contains the includer label name' do + helmchart # populate the label + create(:label, project: project, title: 'Chart') + + _, updates = service.execute(content, issuable) + + expect(updates).to eq(add_label_ids: [helmchart.id]) + end + end + shared_examples 'unlabel command' do it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do issuable.update!(label_ids: [inprogress.id]) # populate the label @@ -624,6 +645,26 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + it_behaves_like 'multiword label name starting without ~' do + let(:content) { %(/label "#{helmchart.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'multiword label name starting without ~' do + let(:content) { %(/label "#{helmchart.title}") } + let(:issuable) { merge_request } + end + + it_behaves_like 'label name is included in the middle of another label name' do + let(:content) { %(/label ~"#{helmchart.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'label name is included in the middle of another label name' do + let(:content) { %(/label ~"#{helmchart.title}") } + let(:issuable) { merge_request } + end + it_behaves_like 'unlabel command' do let(:content) { %(/unlabel ~"#{inprogress.title}") } let(:issuable) { issue } diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index 73156d18c1b..693b796fbdc 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -23,10 +23,13 @@ shared_examples 'variable list' do end end - it 'adds empty variable' do + it 'adds a new protected variable' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('') + find('.js-ci-variable-input-value').set('key_value') + find('.ci-variable-protected-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') end click_button('Save variables') @@ -37,17 +40,17 @@ shared_examples 'variable list' do # We check the first row because it re-sorts to alphabetical order on refresh page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') end end - it 'adds new protected variable' do + it 'defaults to masked' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') find('.js-ci-variable-input-value').set('key_value') - find('.ci-variable-protected-item .js-project-feature-toggle').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') end click_button('Save variables') @@ -59,7 +62,7 @@ shared_examples 'variable list' do page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do expect(find('.js-ci-variable-input-key').value).to eq('key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') end end @@ -163,27 +166,6 @@ shared_examples 'variable list' do end end - it 'edits variable with empty value' do - page.within('.js-ci-variable-list-section') do - click_button('Reveal value') - - page.within('.js-row:nth-child(1)') do - find('.js-ci-variable-input-key').set('new_key') - find('.js-ci-variable-input-value').set('') - end - - click_button('Save variables') - wait_for_requests - - visit page_path - - page.within('.js-row:nth-child(1)') do - expect(find('.js-ci-variable-input-key').value).to eq('new_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('') - end - end - end - it 'edits variable to be protected' do # Create the unprotected variable page.within('.js-ci-variable-list-section .js-row:last-child') do @@ -251,6 +233,57 @@ shared_examples 'variable list' do end end + it 'edits variable to be unmasked' do + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + + find('.ci-variable-masked-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + end + end + + it 'edits variable to be masked' do + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + + find('.ci-variable-masked-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + + find('.ci-variable-masked-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + end + end + it 'handles multiple edits and deletion in the middle' do page.within('.js-ci-variable-list-section') do # Create 2 variables @@ -297,11 +330,11 @@ shared_examples 'variable list' do it 'shows validation error box about duplicate keys' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value1') + find('.js-ci-variable-input-value').set('value123') end page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value2') + find('.js-ci-variable-input-value').set('value456') end click_button('Save variables') @@ -314,4 +347,34 @@ shared_examples 'variable list' do expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/) end end + + it 'shows validation error box about empty values' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('empty_value') + find('.js-ci-variable-input-value').set('') + end + + click_button('Save variables') + wait_for_requests + + page.within('.js-ci-variable-list-section') do + expect(all('.js-ci-variable-error-box ul li').count).to eq(1) + expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) + end + end + + it 'shows validation error box about unmaskable values' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('unmaskable_value') + find('.js-ci-variable-input-value').set('???') + end + + click_button('Save variables') + wait_for_requests + + page.within('.js-ci-variable-list-section') do + expect(all('.js-ci-variable-error-box ul li').count).to eq(1) + expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) + end + end end diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb index 8bcd26ec0cd..3ad6e067674 100644 --- a/spec/support/shared_context/policies/project_policy_shared_context.rb +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do read_project_for_iids read_issue_iid read_label read_milestone read_project_snippet read_project_member read_note create_project create_issue create_note upload_file create_merge_request_in - award_emoji read_release + award_emoji ] end @@ -24,7 +24,7 @@ RSpec.shared_context 'ProjectPolicy context' do download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue + read_merge_request download_wiki_code read_sentry_issue read_release ] end diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb index 4604d867507..b337a1c18d8 100644 --- a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb @@ -1,4 +1,28 @@ # frozen_string_literal: true shared_examples 'tag quick action' do + context "post note to existing commit" do + it 'tags this commit' do + add_note("/tag #{tag_name} #{tag_message}") + + expect(page).to have_content 'Commands applied' + expect(page).to have_content "tagged commit #{truncated_commit_sha}" + expect(page).to have_content tag_name + + visit project_tag_path(project, tag_name) + expect(page).to have_content tag_name + expect(page).to have_content tag_message + expect(page).to have_content truncated_commit_sha + end + end + + context 'preview', :js do + it 'removes quick action from note and explains it' do + preview_note("/tag #{tag_name} #{tag_message}") + + expect(page).not_to have_content '/tag' + expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"} + expect(page).to have_content tag_name + end + end end diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb index 31d88183f0d..c454ddc4bba 100644 --- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb @@ -1,4 +1,51 @@ # frozen_string_literal: true shared_examples 'merge quick action' do + context 'when the current user can merge the MR' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'merges the MR' do + add_note("/merge") + + expect(page).to have_content 'Commands applied' + + expect(merge_request.reload).to be_merged + end + end + + context 'when the head diff changes in the meanwhile' do + before do + merge_request.source_branch = 'another_branch' + merge_request.save + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'does not merge the MR' do + add_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + + context 'when the current user cannot merge the MR' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not merge the MR' do + add_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end end diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb new file mode 100644 index 00000000000..78a8e49fd76 --- /dev/null +++ b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +shared_examples 'logs kubernetes errors' do + let(:error_hash) do + { + service: service.class.name, + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: error_code + } + end + + let(:logger_hash) do + error_hash.merge( + exception: error_name, + message: error_message, + backtrace: instance_of(Array) + ) + end + + it 'logs into kubernetes.log and Sentry' do + expect(service.send(:logger)).to receive(:error).with(logger_hash) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: hash_including(error_hash) + ) + + service.execute + end +end diff --git a/spec/support/shared_examples/time_tracking_shared_examples.rb b/spec/support/shared_examples/time_tracking_shared_examples.rb deleted file mode 100644 index 909d4e2ee8d..00000000000 --- a/spec/support/shared_examples/time_tracking_shared_examples.rb +++ /dev/null @@ -1,85 +0,0 @@ -shared_examples 'issuable time tracker' do - it 'renders the sidebar component empty state' do - page.within '.time-tracking-no-tracking-pane' do - expect(page).to have_content 'No estimate or time spent' - end - end - - it 'updates the sidebar component when estimate is added' do - submit_time('/estimate 3w 1d 1h') - - wait_for_requests - page.within '.time-tracking-estimate-only-pane' do - expect(page).to have_content '3w 1d 1h' - end - end - - it 'updates the sidebar component when spent is added' do - submit_time('/spend 3w 1d 1h') - - wait_for_requests - page.within '.time-tracking-spend-only-pane' do - expect(page).to have_content '3w 1d 1h' - end - end - - it 'shows the comparison when estimate and spent are added' do - submit_time('/estimate 3w 1d 1h') - submit_time('/spend 3w 1d 1h') - - wait_for_requests - page.within '.time-tracking-comparison-pane' do - expect(page).to have_content '3w 1d 1h' - end - end - - it 'updates the sidebar component when estimate is removed' do - submit_time('/estimate 3w 1d 1h') - submit_time('/remove_estimate') - - page.within '.time-tracking-component-wrap' do - expect(page).to have_content 'No estimate or time spent' - end - end - - it 'updates the sidebar component when spent is removed' do - submit_time('/spend 3w 1d 1h') - submit_time('/remove_time_spent') - - page.within '.time-tracking-component-wrap' do - expect(page).to have_content 'No estimate or time spent' - end - end - - it 'shows the help state when icon is clicked' do - page.within '.time-tracking-component-wrap' do - find('.help-button').click - expect(page).to have_content 'Track time with quick actions' - expect(page).to have_content 'Learn more' - end - end - - it 'hides the help state when close icon is clicked' do - page.within '.time-tracking-component-wrap' do - find('.help-button').click - find('.close-help-button').click - - expect(page).not_to have_content 'Track time with quick actions' - expect(page).not_to have_content 'Learn more' - end - end - - it 'displays the correct help url' do - page.within '.time-tracking-component-wrap' do - find('.help-button').click - - expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') - end - end -end - -def submit_time(quick_action) - fill_in 'note[note]', with: quick_action - find('.js-comment-submit-button').click - wait_for_requests -end |