diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-01 12:08:00 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-01 12:08:00 +0000 |
commit | 1a0d6dbdc2ac3047f4953a359ef27ba6e26074ae (patch) | |
tree | ddb78a8a0d1350dc767f049a21e0f7d37edaa82c /spec | |
parent | b11f7057d067885619ee3e513751f180b2e8ad85 (diff) | |
download | gitlab-ce-1a0d6dbdc2ac3047f4953a359ef27ba6e26074ae.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
18 files changed, 705 insertions, 76 deletions
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index fbf88a01eb3..8ba7a96e1ee 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -212,14 +212,86 @@ describe Groups::Settings::CiCdController do end describe 'POST create_deploy_token' do - it_behaves_like 'a created deploy token' do - let(:entity) { group } - let(:create_entity_params) { { group_id: group } } - let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] } - + context 'when ajax_new_deploy_token feature flag is disabled for the project' do before do + stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: group }) entity.add_owner(user) end + + it_behaves_like 'a created deploy token' do + let(:entity) { group } + let(:create_entity_params) { { group_id: group } } + let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] } + end + end + + context 'when ajax_new_deploy_token feature flag is enabled for the project' do + let(:good_deploy_token_params) do + { + name: 'name', + expires_at: 1.day.from_now.to_s, + username: 'deployer', + read_repository: '1', + deploy_token_type: DeployToken.deploy_token_types[:group_type] + } + end + let(:request_params) do + { + group_id: group.to_param, + deploy_token: deploy_token_params + } + end + + before do + group.add_owner(user) + end + + subject { post :create_deploy_token, params: request_params, format: :json } + + context('a good request') do + let(:deploy_token_params) { good_deploy_token_params } + let(:expected_response) do + { + 'id' => be_a(Integer), + 'name' => deploy_token_params[:name], + 'username' => deploy_token_params[:username], + 'expires_at' => Time.parse(deploy_token_params[:expires_at]), + 'token' => be_a(String), + 'scopes' => deploy_token_params.inject([]) do |scopes, kv| + key, value = kv + key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes + end + } + end + + it 'creates the deploy token' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/deploy_token') + expect(json_response).to match(expected_response) + end + end + + context('a bad request') do + let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) } + let(:expected_response) { { 'message' => "Scopes can't be blank" } } + + it 'does not create the deploy token' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to match(expected_response) + end + end + + context('an invalid request') do + let(:deploy_token_params) { good_deploy_token_params.except(:name) } + + it 'raises a validation error' do + expect { subject }.to raise_error(ActiveRecord::StatementInvalid) + end + end end end end diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index a8631389e17..87b40a02567 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do end describe 'POST create_deploy_token' do - it_behaves_like 'a created deploy token' do - let(:entity) { project } - let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } } - let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] } + context 'when ajax_new_deploy_token feature flag is disabled for the project' do + before do + stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project }) + end + + it_behaves_like 'a created deploy token' do + let(:entity) { project } + let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } } + let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] } + end + end + + context 'when ajax_new_deploy_token feature flag is enabled for the project' do + let(:good_deploy_token_params) do + { + name: 'name', + expires_at: 1.day.from_now.to_s, + username: 'deployer', + read_repository: '1', + deploy_token_type: DeployToken.deploy_token_types[:project_type] + } + end + let(:request_params) do + { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + deploy_token: deploy_token_params + } + end + + subject { post :create_deploy_token, params: request_params, format: :json } + + context('a good request') do + let(:deploy_token_params) { good_deploy_token_params } + let(:expected_response) do + { + 'id' => be_a(Integer), + 'name' => deploy_token_params[:name], + 'username' => deploy_token_params[:username], + 'expires_at' => Time.parse(deploy_token_params[:expires_at]), + 'token' => be_a(String), + 'scopes' => deploy_token_params.inject([]) do |scopes, kv| + key, value = kv + key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes + end + } + end + + it 'creates the deploy token' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/deploy_token') + expect(json_response).to match(expected_response) + end + end + + context('a bad request') do + let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) } + let(:expected_response) { { 'message' => "Scopes can't be blank" } } + + it 'does not create the deploy token' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to match(expected_response) + end + end + + context('an invalid request') do + let(:deploy_token_params) { good_deploy_token_params.except(:name) } + + it 'raises a validation error' do + expect { subject }.to raise_error(ActiveRecord::StatementInvalid) + end + end end end end diff --git a/spec/features/projects/settings/ci_cd_settings_spec.rb b/spec/features/projects/settings/ci_cd_settings_spec.rb index 8b9b1ac00c3..d8208a93bb1 100644 --- a/spec/features/projects/settings/ci_cd_settings_spec.rb +++ b/spec/features/projects/settings/ci_cd_settings_spec.rb @@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do project.add_role(user, role) sign_in(user) stub_container_registry_config(enabled: true) + stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project }) visit project_settings_ci_cd_path(project) end diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb index a9253c20896..11b06604f27 100644 --- a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb +++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb @@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do before do project.add_role(user, role) sign_in(user) + stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project }) visit(project_settings_ci_cd_path(project)) click_link('Revoke') end diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index c0126b2330d..179aa3a49d7 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -570,4 +570,65 @@ describe('Api', () => { .catch(done.fail); }); }); + + describe('createReleaseLink', () => { + const dummyProjectPath = 'gitlab-org/gitlab'; + const dummyReleaseTag = 'v1.3'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent( + dummyProjectPath, + )}/releases/${dummyReleaseTag}/assets/links`; + const expectedLink = { + url: 'https://example.com', + name: 'An example link', + }; + + describe('when the Release is successfully created', () => { + it('resolves the Promise', () => { + mock.onPost(expectedUrl, expectedLink).replyOnce(201); + + return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).then(() => { + expect(mock.history.post).toHaveLength(1); + }); + }); + }); + + describe('when an error occurs while creating the Release', () => { + it('rejects the Promise', () => { + mock.onPost(expectedUrl, expectedLink).replyOnce(500); + + return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).catch(() => { + expect(mock.history.post).toHaveLength(1); + }); + }); + }); + }); + + describe('deleteReleaseLink', () => { + const dummyProjectPath = 'gitlab-org/gitlab'; + const dummyReleaseTag = 'v1.3'; + const dummyLinkId = '4'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent( + dummyProjectPath, + )}/releases/${dummyReleaseTag}/assets/links/${dummyLinkId}`; + + describe('when the Release is successfully deleted', () => { + it('resolves the Promise', () => { + mock.onDelete(expectedUrl).replyOnce(200); + + return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).then(() => { + expect(mock.history.delete).toHaveLength(1); + }); + }); + }); + + describe('when an error occurs while deleting the Release', () => { + it('rejects the Promise', () => { + mock.onDelete(expectedUrl).replyOnce(500); + + return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).catch(() => { + expect(mock.history.delete).toHaveLength(1); + }); + }); + }); + }); }); diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js index ac4b2b9124f..e27c27b292a 100644 --- a/spec/frontend/releases/components/app_edit_spec.js +++ b/spec/frontend/releases/components/app_edit_spec.js @@ -4,6 +4,7 @@ import ReleaseEditApp from '~/releases/components/app_edit.vue'; import { release as originalRelease } from '../mock_data'; import * as commonUtils from '~/lib/utils/common_utils'; import { BACK_URL_PARAM } from '~/releases/constants'; +import AssetLinksForm from '~/releases/components/asset_links_form.vue'; describe('Release edit component', () => { let wrapper; @@ -11,7 +12,7 @@ describe('Release edit component', () => { let actions; let state; - const factory = () => { + const factory = (featureFlags = {}) => { state = { release, markdownDocsPath: 'path/to/markdown/docs', @@ -22,6 +23,7 @@ describe('Release edit component', () => { actions = { fetchRelease: jest.fn(), updateRelease: jest.fn(), + addEmptyAssetLink: jest.fn(), }; const store = new Vuex.Store({ @@ -36,6 +38,9 @@ describe('Release edit component', () => { wrapper = mount(ReleaseEditApp, { store, + provide: { + glFeatures: featureFlags, + }, }); }; @@ -132,4 +137,28 @@ describe('Release edit component', () => { expect(cancelButton.attributes().href).toBe(backUrl); }); }); + + describe('asset links form', () => { + const findAssetLinksForm = () => wrapper.find(AssetLinksForm); + + describe('when the release_asset_link_editing feature flag is disabled', () => { + beforeEach(() => { + factory({ releaseAssetLinkEditing: false }); + }); + + it('does not render the asset links portion of the form', () => { + expect(findAssetLinksForm().exists()).toBe(false); + }); + }); + + describe('when the release_asset_link_editing feature flag is enabled', () => { + beforeEach(() => { + factory({ releaseAssetLinkEditing: true }); + }); + + it('renders the asset links portion of the form', () => { + expect(findAssetLinksForm().exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 70f7432c65d..4a1790adb09 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -9,6 +9,7 @@ import createState from '~/releases/stores/modules/detail/state'; import createFlash from '~/flash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { redirectTo } from '~/lib/utils/url_utility'; +import api from '~/api'; jest.mock('~/flash', () => jest.fn()); @@ -179,40 +180,92 @@ describe('Release detail actions', () => { }); describe('updateRelease', () => { - let getReleaseUrl; + let getters; + let dispatch; + let callOrder; beforeEach(() => { - state.release = release; + state.release = convertObjectPropsToCamelCase(release); state.projectId = '18'; - state.tagName = 'v1.3'; - getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`; - }); + state.tagName = state.release.tagName; - it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => { - mock.onPut(getReleaseUrl).replyOnce(200); + getters = { + releaseLinksToDelete: [{ id: '1' }, { id: '2' }], + releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }], + }; - return testAction( - actions.updateRelease, - undefined, - state, - [], - [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }], - ); + dispatch = jest.fn(); + + callOrder = []; + jest.spyOn(api, 'updateRelease').mockImplementation(() => { + callOrder.push('updateRelease'); + return Promise.resolve(); + }); + jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => { + callOrder.push('deleteReleaseLink'); + return Promise.resolve(); + }); + jest.spyOn(api, 'createReleaseLink').mockImplementation(() => { + callOrder.push('createReleaseLink'); + return Promise.resolve(); + }); }); - it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => { - mock.onPut(getReleaseUrl).replyOnce(500); + it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => { + return actions.updateRelease({ dispatch, state, getters }).then(() => { + expect(dispatch.mock.calls).toEqual([ + ['requestUpdateRelease'], + ['receiveUpdateReleaseSuccess'], + ]); + }); + }); - return testAction( - actions.updateRelease, - undefined, - state, - [], - [ - { type: 'requestUpdateRelease' }, - { type: 'receiveUpdateReleaseError', payload: expect.anything() }, - ], - ); + it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => { + jest.spyOn(api, 'updateRelease').mockRejectedValue(error); + + return actions.updateRelease({ dispatch, state, getters }).then(() => { + expect(dispatch.mock.calls).toEqual([ + ['requestUpdateRelease'], + ['receiveUpdateReleaseError', error], + ]); + }); + }); + + it('updates the Release, then deletes all existing links, and then recreates new links', () => { + return actions.updateRelease({ dispatch, state, getters }).then(() => { + expect(callOrder).toEqual([ + 'updateRelease', + 'deleteReleaseLink', + 'deleteReleaseLink', + 'createReleaseLink', + 'createReleaseLink', + ]); + + expect(api.updateRelease.mock.calls).toEqual([ + [ + state.projectId, + state.tagName, + { + name: state.release.name, + description: state.release.description, + }, + ], + ]); + + expect(api.deleteReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToDelete.length); + getters.releaseLinksToDelete.forEach(link => { + expect(api.deleteReleaseLink).toHaveBeenCalledWith( + state.projectId, + state.tagName, + link.id, + ); + }); + + expect(api.createReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToCreate.length); + getters.releaseLinksToCreate.forEach(link => { + expect(api.createReleaseLink).toHaveBeenCalledWith(state.projectId, state.tagName, link); + }); + }); }); }); }); diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js new file mode 100644 index 00000000000..7dc95c24055 --- /dev/null +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -0,0 +1,59 @@ +import * as getters from '~/releases/stores/modules/detail/getters'; + +describe('Release detail getters', () => { + describe('releaseLinksToCreate', () => { + it("returns an empty array if state.release doesn't exist", () => { + const state = {}; + expect(getters.releaseLinksToCreate(state)).toEqual([]); + }); + + it("returns all release links that aren't empty", () => { + const emptyLinks = [ + { url: '', name: '' }, + { url: ' ', name: '' }, + { url: ' ', name: ' ' }, + { url: '\r\n', name: '\t' }, + ]; + + const nonEmptyLinks = [ + { url: 'https://example.com/1', name: 'Example 1' }, + { url: '', name: 'Example 2' }, + { url: 'https://example.com/3', name: '' }, + ]; + + const state = { + release: { + assets: { + links: [...emptyLinks, ...nonEmptyLinks], + }, + }, + }; + + expect(getters.releaseLinksToCreate(state)).toEqual(nonEmptyLinks); + }); + }); + + describe('releaseLinksToDelete', () => { + it("returns an empty array if state.originalRelease doesn't exist", () => { + const state = {}; + expect(getters.releaseLinksToDelete(state)).toEqual([]); + }); + + it('returns all links associated with the original release', () => { + const originalLinks = [ + { url: 'https://example.com/1', name: 'Example 1' }, + { url: 'https://example.com/2', name: 'Example 2' }, + ]; + + const state = { + originalRelease: { + assets: { + links: originalLinks, + }, + }, + }; + + expect(getters.releaseLinksToDelete(state)).toEqual(originalLinks); + }); + }); +}); diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index d49c8854ca2..cb5a1880b0c 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -8,11 +8,12 @@ import createState from '~/releases/stores/modules/detail/state'; import mutations from '~/releases/stores/modules/detail/mutations'; import * as types from '~/releases/stores/modules/detail/mutation_types'; -import { release } from '../../../mock_data'; +import { release as originalRelease } from '../../../mock_data'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; describe('Release detail mutations', () => { let state; - let releaseClone; + let release; beforeEach(() => { state = createState({ @@ -23,7 +24,7 @@ describe('Release detail mutations', () => { markdownPreviewPath: 'path/to/markdown/preview', updateReleaseApiDocsPath: 'path/to/api/docs', }); - releaseClone = JSON.parse(JSON.stringify(release)); + release = convertObjectPropsToCamelCase(originalRelease); }); describe(types.REQUEST_RELEASE, () => { @@ -36,13 +37,15 @@ describe('Release detail mutations', () => { describe(types.RECEIVE_RELEASE_SUCCESS, () => { it('handles a successful response from the server', () => { - mutations[types.RECEIVE_RELEASE_SUCCESS](state, releaseClone); + mutations[types.RECEIVE_RELEASE_SUCCESS](state, release); expect(state.fetchError).toEqual(undefined); expect(state.isFetchingRelease).toEqual(false); - expect(state.release).toEqual(releaseClone); + expect(state.release).toEqual(release); + + expect(state.originalRelease).toEqual(release); }); }); @@ -61,7 +64,7 @@ describe('Release detail mutations', () => { describe(types.UPDATE_RELEASE_TITLE, () => { it("updates the release's title", () => { - state.release = releaseClone; + state.release = release; const newTitle = 'The new release title'; mutations[types.UPDATE_RELEASE_TITLE](state, newTitle); @@ -71,7 +74,7 @@ describe('Release detail mutations', () => { describe(types.UPDATE_RELEASE_NOTES, () => { it("updates the release's notes", () => { - state.release = releaseClone; + state.release = release; const newNotes = 'The new release notes'; mutations[types.UPDATE_RELEASE_NOTES](state, newNotes); @@ -89,7 +92,7 @@ describe('Release detail mutations', () => { describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => { it('handles a successful response from the server', () => { - mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, releaseClone); + mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release); expect(state.updateError).toEqual(undefined); @@ -107,4 +110,65 @@ describe('Release detail mutations', () => { expect(state.updateError).toEqual(error); }); }); + + describe(types.ADD_EMPTY_ASSET_LINK, () => { + it('adds a new, empty link object to the release', () => { + state.release = release; + + const linksBefore = [...state.release.assets.links]; + + mutations[types.ADD_EMPTY_ASSET_LINK](state); + + expect(state.release.assets.links).toEqual([ + ...linksBefore, + { + id: expect.stringMatching(/^new-link-/), + url: '', + name: '', + }, + ]); + }); + }); + + describe(types.UPDATE_ASSET_LINK_URL, () => { + it('updates an asset link with a new URL', () => { + state.release = release; + + const newUrl = 'https://example.com/updated/url'; + + mutations[types.UPDATE_ASSET_LINK_URL](state, { + linkIdToUpdate: state.release.assets.links[0].id, + newUrl, + }); + + expect(state.release.assets.links[0].url).toEqual(newUrl); + }); + }); + + describe(types.UPDATE_ASSET_LINK_NAME, () => { + it('updates an asset link with a new name', () => { + state.release = release; + + const newName = 'Updated Link'; + + mutations[types.UPDATE_ASSET_LINK_NAME](state, { + linkIdToUpdate: state.release.assets.links[0].id, + newName, + }); + + expect(state.release.assets.links[0].name).toEqual(newName); + }); + }); + + describe(types.REMOVE_ASSET_LINK, () => { + it('removes an asset link from the release', () => { + state.release = release; + + const linkToRemove = state.release.assets.links[0]; + + mutations[types.REMOVE_ASSET_LINK](state, linkToRemove.id); + + expect(state.release.assets.links).not.toContainEqual(linkToRemove); + }); + }); }); diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index d9d6a324f09..282758679cb 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -53,7 +53,8 @@ describe ReleasesHelper do markdown_preview_path markdown_docs_path releases_page_path - update_release_api_docs_path) + update_release_api_docs_path + release_assets_docs_path) expect(helper.data_for_edit_release_page.keys).to eq(keys) end end diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index bffaaef4ed4..0b6fda31d7b 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -335,6 +335,54 @@ describe Gitlab::Auth::AuthFinders do end end + describe '#find_personal_access_token_from_http_basic_auth' do + def auth_header_with(token) + env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', token) + end + + context 'access token is valid' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:route_authentication_setting) { { basic_auth_personal_access_token: true } } + + it 'finds the token from basic auth' do + auth_header_with(personal_access_token.token) + + expect(find_personal_access_token_from_http_basic_auth).to eq personal_access_token + end + end + + context 'access token is not valid' do + let(:route_authentication_setting) { { basic_auth_personal_access_token: true } } + + it 'returns nil' do + auth_header_with('failing_token') + + expect(find_personal_access_token_from_http_basic_auth).to be_nil + end + end + + context 'route_setting is not set' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'returns nil' do + auth_header_with(personal_access_token.token) + + expect(find_personal_access_token_from_http_basic_auth).to be_nil + end + end + + context 'route_setting is not correct' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:route_authentication_setting) { { basic_auth_personal_access_token: false } } + + it 'returns nil' do + auth_header_with(personal_access_token.token) + + expect(find_personal_access_token_from_http_basic_auth).to be_nil + end + end + end + describe '#find_user_from_basic_auth_job' do def basic_http_auth(username, password) ActionController::HttpAuthentication::Basic.encode_credentials(username, password) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 608e3a6e938..f16b1fb136d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3681,15 +3681,15 @@ describe User, :do_not_mock_admin_mode do end it 'returns false if email can not be synced' do - stub_omniauth_setting(sync_profile_attributes: %w(location email)) + stub_omniauth_setting(sync_profile_attributes: %w(location name)) - expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_falsey end it 'returns false if location can not be synced' do - stub_omniauth_setting(sync_profile_attributes: %w(location email)) + stub_omniauth_setting(sync_profile_attributes: %w(name email)) - expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:location)).to be_falsey end it 'returns true for all syncable attributes if all syncable attributes can be synced' do diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index a729da5afad..cce84c4f357 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -53,6 +53,7 @@ describe ProjectPolicy do admin_commit_status admin_build admin_container_image admin_pipeline admin_environment admin_deployment destroy_release add_cluster daily_statistics read_deploy_token create_deploy_token destroy_deploy_token + admin_terraform_state ] end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 5440e187ba9..403e182ad99 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -526,12 +526,48 @@ describe API::MergeRequests do expect_response_contain_exactly(merge_request3.id) end - it 'returns an array of merge requests authored by the given user' do - merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') + context 'filter by author' do + let(:user3) { create(:user) } + let(:project) { create(:project, :public, :repository, creator: user3, namespace: user3.namespace, only_allow_merge_if_pipeline_succeeds: false) } + let!(:merge_request3) do + create(:merge_request, :simple, author: user3, assignees: [user3], source_project: project, target_project: project, source_branch: 'other-branch') + end - get api('/merge_requests', user), params: { author_id: user2.id, scope: :all } + context 'when only `author_id` is passed' do + it 'returns an array of merge requests authored by the given user' do + get api('/merge_requests', user), params: { + author_id: user3.id, + scope: :all + } - expect_response_contain_exactly(merge_request3.id) + expect_response_contain_exactly(merge_request3.id) + end + end + + context 'when only `author_username` is passed' do + it 'returns an array of merge requests authored by the given user(by `author_username`)' do + get api('/merge_requests', user), params: { + author_username: user3.username, + scope: :all + } + + expect_response_contain_exactly(merge_request3.id) + end + end + + context 'when both `author_id` and `author_username` are passed' do + it 'returns a 400' do + get api('/merge_requests', user), params: { + author_id: user.id, + author_username: user2.username, + scope: :all + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq( + 'author_id, author_username are mutually exclusive') + end + end end it 'returns an array of merge requests assigned to the given user' do @@ -1525,7 +1561,7 @@ describe API::MergeRequests do it "returns 400 when target_branch is missing" do post api("/projects/#{forked_project.id}/merge_requests", user2), - params: { title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id } + params: { title: 'Test merge_request', source_branch: "master", author: user2, target_project_id: project.id } expect(response).to have_gitlab_http_status(:bad_request) end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index d05d886bf85..a67c1a48ad4 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1349,19 +1349,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30') end end - - context 'when feature flag runner_job_trace_update_interval_header is disabled' do - before do - stub_feature_flags(runner_job_trace_update_interval_header: { enabled: false }) - end - - it 'does not return X-GitLab-Trace-Update-Interval header' do - patch_the_trace - - expect(response).to have_gitlab_http_status(:accepted) - expect(response.header).not_to have_key 'X-GitLab-Trace-Update-Interval' - end - end end context 'when Runner makes a force-patch' do diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb new file mode 100644 index 00000000000..b0a963db684 --- /dev/null +++ b/spec/requests/api/terraform/state_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Terraform::State do + def auth_header_for(user) + auth_header = ActionController::HttpAuthentication::Basic.encode_credentials( + user.username, + create(:personal_access_token, user: user).token + ) + { 'HTTP_AUTHORIZATION' => auth_header } + end + + let!(:project) { create(:project) } + let(:developer) { create(:user) } + let(:maintainer) { create(:user) } + let(:state_name) { 'state' } + + before do + project.add_maintainer(maintainer) + end + + describe 'GET /projects/:id/terraform/state/:name' do + it 'returns 401 if user is not authenticated' do + headers = { 'HTTP_AUTHORIZATION' => 'failing_token' } + get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: headers + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns terraform state belonging to a project of given state name' do + get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer) + + expect(response).to have_gitlab_http_status(:not_implemented) + expect(response.body).to eq('not implemented') + end + + it 'returns not found if the project does not exists' do + get api("/projects/0000/terraform/state/#{state_name}"), headers: auth_header_for(maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns forbidden if the user cannot access the state' do + project.add_developer(developer) + get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + describe 'POST /projects/:id/terraform/state/:name' do + context 'when terraform state with a given name is already present' do + it 'updates the state' do + post api("/projects/#{project.id}/terraform/state/#{state_name}"), + params: '{ "instance": "example-instance" }', + headers: { 'Content-Type' => 'text/plain' }.merge(auth_header_for(maintainer)) + + expect(response).to have_gitlab_http_status(:not_implemented) + expect(response.body).to eq('not implemented') + end + + it 'returns forbidden if the user cannot access the state' do + project.add_developer(developer) + get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when there is no terraform state of a given name' do + it 'creates a new state' do + post api("/projects/#{project.id}/terraform/state/example2"), + headers: auth_header_for(maintainer), + params: '{ "database": "example-database" }' + + expect(response).to have_gitlab_http_status(:not_implemented) + expect(response.body).to eq('not implemented') + end + end + end + + describe 'DELETE /projects/:id/terraform/state/:name' do + it 'deletes the state' do + delete api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer) + + expect(response).to have_gitlab_http_status(:not_implemented) + end + + it 'returns forbidden if the user cannot access the state' do + project.add_developer(developer) + get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 30524e4bbae..08320934ba1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -214,15 +214,62 @@ RSpec.configure do |config| # modifying a significant number of specs to test both states for admin # mode enabled / disabled. # - # See https://gitlab.com/gitlab-org/gitlab/issues/31511 - # See gitlab/spec/support/helpers/admin_mode_helpers.rb + # This will only be applied to specs below dirs in `admin_mode_mock_dirs` # - # If it is required to have the real behaviour that an admin is signed in + # See ongoing migration: https://gitlab.com/gitlab-org/gitlab/-/issues/31511 + # + # Until the migration is finished, if it is required to have the real + # behaviour in any of the mocked dirs specs that an admin is signed in # with normal user mode and needs to switch to admin mode, it is possible to # mark such tests with the `do_not_mock_admin_mode` metadata tag, e.g: # - # context 'some test with normal user mode', :do_not_mock_admin_mode do ... end - unless example.metadata[:do_not_mock_admin_mode] + # context 'some test in mocked dir', :do_not_mock_admin_mode do ... end + admin_mode_mock_dirs = %w( + ./ee/spec/controllers + ./ee/spec/elastic_integration + ./ee/spec/features + ./ee/spec/finders + ./ee/spec/lib + ./ee/spec/models + ./ee/spec/policies + ./ee/spec/requests/admin + ./ee/spec/serializers + ./ee/spec/services + ./ee/spec/support/protected_tags + ./ee/spec/support/shared_examples + ./spec/controllers + ./spec/features + ./spec/finders + ./spec/frontend + ./spec/helpers + ./spec/lib + ./spec/models + ./spec/policies + ./spec/requests + ./spec/serializers + ./spec/services + ./spec/support/cycle_analytics_helpers + ./spec/support/protected_tags + ./spec/support/shared_examples + ./spec/views + ./spec/workers + ) + + if !example.metadata[:do_not_mock_admin_mode] && example.metadata[:file_path].start_with?(*admin_mode_mock_dirs) + allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode| + current_user_mode.send(:user)&.admin? + end + end + + # Administrators have to re-authenticate in order to access administrative + # functionality when feature flag :user_mode_in_session is active. Any spec + # that requires administrative access can use the tag :enable_admin_mode + # to avoid the second auth step (provided the user is already an admin): + # + # context 'some test that requires admin mode', :enable_admin_mode do ... end + # + # See also spec/support/helpers/admin_mode_helpers.rb + if example.metadata[:enable_admin_mode] allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode| current_user_mode.send(:user)&.admin? end diff --git a/spec/support/services/deploy_token_shared_examples.rb b/spec/support/services/deploy_token_shared_examples.rb index b49f4743f7d..70efd1fcd0c 100644 --- a/spec/support/services/deploy_token_shared_examples.rb +++ b/spec/support/services/deploy_token_shared_examples.rb @@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do end it 'returns a DeployToken' do - expect(subject).to be_an_instance_of DeployToken + expect(subject[:deploy_token]).to be_an_instance_of DeployToken end end @@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } it 'sets Forever.date' do - expect(subject.read_attribute(:expires_at)).to eq(Forever.date) + expect(subject[:deploy_token].read_attribute(:expires_at)).to eq(Forever.date) end end @@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do let(:deploy_token_params) { attributes_for(:deploy_token, username: '') } it 'converts it to nil' do - expect(subject.read_attribute(:username)).to be_nil + expect(subject[:deploy_token].read_attribute(:username)).to be_nil end end @@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') } it 'keeps the provided username' do - expect(subject.read_attribute(:username)).to eq('deployer') + expect(subject[:deploy_token].read_attribute(:username)).to eq('deployer') end end |