summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb82
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb80
-rw-r--r--spec/features/projects/settings/ci_cd_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb1
-rw-r--r--spec/frontend/api_spec.js61
-rw-r--r--spec/frontend/releases/components/app_edit_spec.js31
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js105
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js59
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js80
-rw-r--r--spec/helpers/releases_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb48
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/policies/project_policy_spec.rb1
-rw-r--r--spec/requests/api/merge_requests_spec.rb46
-rw-r--r--spec/requests/api/runner_spec.rb13
-rw-r--r--spec/requests/api/terraform/state_spec.rb97
-rw-r--r--spec/spec_helper.rb57
-rw-r--r--spec/support/services/deploy_token_shared_examples.rb8
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