diff options
Diffstat (limited to 'spec')
30 files changed, 1105 insertions, 326 deletions
diff --git a/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb new file mode 100644 index 00000000000..857e0570621 --- /dev/null +++ b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::DependencyProxyAuthController do + include DependencyProxyHelpers + + describe 'GET #authenticate' do + subject { get :authenticate } + + context 'feature flag disabled' do + before do + stub_feature_flags(dependency_proxy_for_private_groups: false) + end + + it 'returns successfully', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'without JWT' do + it 'returns unauthorized with oauth realm', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + expect(response.headers['WWW-Authenticate']).to eq DependencyProxy::Registry.authenticate_header + end + end + + context 'with valid JWT' do + let_it_be(:user) { create(:user) } + let(:jwt) { build_jwt(user) } + let(:token_header) { "Bearer #{jwt.encoded}" } + + before do + request.headers['HTTP_AUTHORIZATION'] = token_header + end + + it { is_expected.to have_gitlab_http_status(:success) } + end + + context 'with invalid JWT' do + context 'bad user' do + let(:jwt) { build_jwt(double('bad_user', id: 999)) } + let(:token_header) { "Bearer #{jwt.encoded}" } + + before do + request.headers['HTTP_AUTHORIZATION'] = token_header + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'token with no user id' do + let(:token_header) { "Bearer #{build_jwt.encoded}" } + + before do + request.headers['HTTP_AUTHORIZATION'] = token_header + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'expired token' do + let_it_be(:user) { create(:user) } + let(:jwt) { build_jwt(user, expire_time: Time.zone.now - 1.hour) } + let(:token_header) { "Bearer #{jwt.encoded}" } + + before do + request.headers['HTTP_AUTHORIZATION'] = token_header + end + + it { is_expected.to have_gitlab_http_status(:unauthorized) } + end + end + end +end diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 615b56ff22f..87956cc7287 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -3,8 +3,77 @@ require 'spec_helper' RSpec.describe Groups::DependencyProxyForContainersController do + include HttpBasicAuthHelpers + include DependencyProxyHelpers + + let_it_be(:user) { create(:user) } let(:group) { create(:group) } let(:token_response) { { status: :success, token: 'abcd1234' } } + let(:jwt) { build_jwt(user) } + let(:token_header) { "Bearer #{jwt.encoded}" } + + shared_examples 'without a token' do + before do + request.headers['HTTP_AUTHORIZATION'] = nil + end + + context 'feature flag disabled' do + before do + stub_feature_flags(dependency_proxy_for_private_groups: false) + end + + it { is_expected.to have_gitlab_http_status(:ok) } + end + + it { is_expected.to have_gitlab_http_status(:unauthorized) } + end + + shared_examples 'feature flag disabled with private group' do + before do + stub_feature_flags(dependency_proxy_for_private_groups: false) + end + + it 'redirects', :aggregate_failures do + group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + + subject + + expect(response).to have_gitlab_http_status(:redirect) + expect(response.location).to end_with(new_user_session_path) + end + end + + shared_examples 'without permission' do + context 'with invalid user' do + before do + user = double('bad_user', id: 999) + token_header = "Bearer #{build_jwt(user).encoded}" + request.headers['HTTP_AUTHORIZATION'] = token_header + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'with valid user that does not have access' do + let(:group) { create(:group, :private) } + + before do + user = double('bad_user', id: 999) + token_header = "Bearer #{build_jwt(user).encoded}" + request.headers['HTTP_AUTHORIZATION'] = token_header + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when user is not found' do + before do + allow(User).to receive(:find).and_return(nil) + end + + it { is_expected.to have_gitlab_http_status(:unauthorized) } + end + end shared_examples 'not found when disabled' do context 'feature disabled' do @@ -27,6 +96,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance| allow(instance).to receive(:execute).and_return(token_response) end + + request.headers['HTTP_AUTHORIZATION'] = token_header end describe 'GET #manifest' do @@ -46,6 +117,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do enable_dependency_proxy end + it_behaves_like 'without a token' + it_behaves_like 'without permission' + it_behaves_like 'feature flag disabled with private group' + context 'remote token request fails' do let(:token_response) do { @@ -113,6 +188,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do enable_dependency_proxy end + it_behaves_like 'without a token' + it_behaves_like 'without permission' + it_behaves_like 'feature flag disabled with private group' + context 'remote blob request fails' do let(:blob_response) do { diff --git a/spec/controllers/profiles/gpg_keys_controller_spec.rb b/spec/controllers/profiles/gpg_keys_controller_spec.rb new file mode 100644 index 00000000000..1860bb0c93b --- /dev/null +++ b/spec/controllers/profiles/gpg_keys_controller_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Profiles::GpgKeysController do + let(:user) { create(:user, email: GpgHelpers::User1.emails[0]) } + + describe 'POST #create' do + before do + sign_in(user) + end + + it 'creates a new key' do + expect do + post :create, params: { gpg_key: build(:gpg_key).attributes } + end.to change { GpgKey.count }.by(1) + end + end + + describe "#get_keys" do + describe "non existent user" do + it "does not generally work" do + get :get_keys, params: { username: 'not-existent' } + + expect(response).not_to be_successful + end + end + + describe "user with no keys" do + it "does generally work" do + get :get_keys, params: { username: user.username } + + expect(response).to be_successful + end + + it "renders all keys separated with a new line" do + get :get_keys, params: { username: user.username } + + expect(response.body).to eq("") + end + + it "responds with text/plain content type" do + get :get_keys, params: { username: user.username } + + expect(response.content_type).to eq("text/plain") + end + end + + describe "user with keys" do + let!(:gpg_key) { create(:gpg_key, user: user) } + let!(:another_gpg_key) { create(:another_gpg_key, user: user) } + + describe "while signed in" do + before do + sign_in(user) + end + + it "does generally work" do + get :get_keys, params: { username: user.username } + + expect(response).to be_successful + end + + it "renders all verified keys separated with a new line" do + get :get_keys, params: { username: user.username } + + expect(response.body).not_to eq('') + expect(response.body).to eq(user.gpg_keys.select(&:verified?).map(&:key).join("\n")) + + expect(response.body).to include(gpg_key.key) + expect(response.body).to include(another_gpg_key.key) + end + + it "responds with text/plain content type" do + get :get_keys, params: { username: user.username } + + expect(response.content_type).to eq("text/plain") + end + end + + describe 'when logged out' do + before do + sign_out(user) + end + + it "still does generally work" do + get :get_keys, params: { username: user.username } + + expect(response).to be_successful + end + + it "renders all verified keys separated with a new line" do + get :get_keys, params: { username: user.username } + + expect(response.body).not_to eq('') + expect(response.body).to eq(user.gpg_keys.map(&:key).join("\n")) + + expect(response.body).to include(gpg_key.key) + expect(response.body).to include(another_gpg_key.key) + end + + it "responds with text/plain content type" do + get :get_keys, params: { username: user.username } + + expect(response.content_type).to eq("text/plain") + end + end + + describe 'when revoked' do + before do + sign_in(user) + another_gpg_key.revoke + end + + it "doesn't render revoked keys" do + get :get_keys, params: { username: user.username } + + expect(response.body).not_to eq('') + + expect(response.body).to include(gpg_key.key) + expect(response.body).not_to include(another_gpg_key.key) + end + + it "doesn't render revoked keys for non-authorized users" do + sign_out(user) + get :get_keys, params: { username: user.username } + + expect(response.body).not_to eq('') + + expect(response.body).to include(gpg_key.key) + expect(response.body).not_to include(another_gpg_key.key) + end + end + end + end +end diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 307c5b4725a..1cebb6a4804 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -167,6 +167,14 @@ RSpec.describe Repositories::GitHttpController do Projects::DailyStatisticsFinder.new(container).total_fetch_count }.from(0).to(1) end + + it 'records a namespace onboarding progress action' do + expect_next_instance_of(OnboardingProgressService) do |service| + expect(service).to receive(:execute).with(action: :git_read) + end + + send_request + end end end end diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb index 9f321643174..5967d9ba9d3 100644 --- a/spec/factories/gpg_keys.rb +++ b/spec/factories/gpg_keys.rb @@ -10,5 +10,10 @@ FactoryBot.define do factory :gpg_key_with_subkeys do key { GpgHelpers::User1.public_key_with_extra_signing_key } end + + factory :another_gpg_key do + key { GpgHelpers::User1.public_key2 } + user + end end end diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb index 9bbfdc488fb..51371ddc532 100644 --- a/spec/features/groups/dependency_proxy_spec.rb +++ b/spec/features/groups/dependency_proxy_spec.rb @@ -79,13 +79,19 @@ RSpec.describe 'Group Dependency Proxy' do sign_in(developer) end - context 'group is private' do - let(:group) { create(:group, :private) } + context 'feature flag is disabled' do + before do + stub_feature_flags(dependency_proxy_for_private_groups: false) + end - it 'informs user that feature is only available for public groups' do - visit path + context 'group is private' do + let(:group) { create(:group, :private) } - expect(page).to have_content('Dependency proxy feature is limited to public groups for now.') + it 'informs user that feature is only available for public groups' do + visit path + + expect(page).to have_content('Dependency proxy feature is limited to public groups for now.') + end end end diff --git a/spec/features/issuables/close_reopen_report_toggle_spec.rb b/spec/features/merge_request/close_reopen_report_toggle_spec.rb index 30cabce8fa2..8a4277d87c9 100644 --- a/spec/features/issuables/close_reopen_report_toggle_spec.rb +++ b/spec/features/merge_request/close_reopen_report_toggle_spec.rb @@ -7,44 +7,6 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do let(:user) { create(:user) } - shared_examples 'an issuable close/reopen/report toggle' do - let(:container) { find('.issuable-close-dropdown') } - let(:human_model_name) { issuable.model_name.human.downcase } - - it 'shows toggle' do - expect(page).to have_button("Close #{human_model_name}") - expect(page).to have_selector('.issuable-close-dropdown') - end - - it 'opens a dropdown when toggle is clicked' do - container.find('.dropdown-toggle').click - - expect(container).to have_selector('.dropdown-menu') - expect(container).to have_content("Close #{human_model_name}") - expect(container).to have_content('Report abuse') - expect(container).to have_content("Report #{human_model_name.pluralize} that are abusive, inappropriate or spam.") - - if issuable.is_a?(MergeRequest) - page.within('.js-issuable-close-dropdown') do - expect(page).to have_link('Close merge request') - end - else - expect(container).to have_selector('.close-item.droplab-item-selected') - end - - expect(container).to have_selector('.report-item') - expect(container).not_to have_selector('.report-item.droplab-item-selected') - expect(container).not_to have_selector('.reopen-item') - end - - it 'links to Report Abuse' do - container.find('.dropdown-toggle').click - container.find('.report-abuse-link').click - - expect(page).to have_content('Report abuse to admin') - end - end - context 'on a merge request' do let(:container) { find('.detail-page-header-actions') } let(:project) { create(:project, :repository) } @@ -60,7 +22,22 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do visit project_merge_request_path(project, issuable) end - it_behaves_like 'an issuable close/reopen/report toggle' + context 'close/reopen/report toggle' do + it 'opens a dropdown when toggle is clicked' do + click_button 'Toggle dropdown' + + expect(container).to have_link("Close merge request") + expect(container).to have_link('Report abuse') + expect(container).to have_text("Report merge requests that are abusive, inappropriate or spam.") + end + + it 'links to Report Abuse' do + click_button 'Toggle dropdown' + click_link 'Report abuse' + + expect(page).to have_content('Report abuse to admin') + end + end context 'when the merge request is open' do let(:issuable) { create(:merge_request, :opened, source_project: project) } diff --git a/spec/features/issuables/merge_request_discussion_lock_spec.rb b/spec/features/merge_request/merge_request_discussion_lock_spec.rb index 4e0265839f6..4e0265839f6 100644 --- a/spec/features/issuables/merge_request_discussion_lock_spec.rb +++ b/spec/features/merge_request/merge_request_discussion_lock_spec.rb diff --git a/spec/features/merge_request/user_reopens_merge_request_spec.rb b/spec/features/merge_request/user_reopens_merge_request_spec.rb index 4a05a3be59a..7cb8ca280cc 100644 --- a/spec/features/merge_request/user_reopens_merge_request_spec.rb +++ b/spec/features/merge_request/user_reopens_merge_request_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'User reopens a merge requests', :js do end it 'reopens a merge request' do - find('.js-issuable-close-dropdown .dropdown-toggle').click + find('.detail-page-header .dropdown-toggle').click click_link('Reopen merge request', match: :first) diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_request/user_squashes_merge_request_spec.rb index 84964bd0637..84964bd0637 100644 --- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb +++ b/spec/features/merge_request/user_squashes_merge_request_spec.rb diff --git a/spec/features/merge_requests/user_views_diffs_commit_spec.rb b/spec/features/merge_request/user_views_diffs_commit_spec.rb index cf92603972e..cf92603972e 100644 --- a/spec/features/merge_requests/user_views_diffs_commit_spec.rb +++ b/spec/features/merge_request/user_views_diffs_commit_spec.rb diff --git a/spec/features/merge_request/user_sees_empty_state_spec.rb b/spec/features/merge_requests/user_sees_empty_state_spec.rb index ac07b31731d..ac07b31731d 100644 --- a/spec/features/merge_request/user_sees_empty_state_spec.rb +++ b/spec/features/merge_requests/user_sees_empty_state_spec.rb diff --git a/spec/frontend/fixtures/static/whats_new_notification.html b/spec/frontend/fixtures/static/whats_new_notification.html new file mode 100644 index 00000000000..30d5eea91cc --- /dev/null +++ b/spec/frontend/fixtures/static/whats_new_notification.html @@ -0,0 +1,6 @@ +<div class='whats-new-notification-fixture-root'> + <div class='app' data-storage-key='storage-key'></div> + <div class='header-help'> + <div class='js-whats-new-notification-count'></div> + </div> +</div> diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js index 6c3569a2247..64dd3888d47 100644 --- a/spec/frontend/helpers/vuex_action_helper.js +++ b/spec/frontend/helpers/vuex_action_helper.js @@ -4,7 +4,7 @@ const noop = () => {}; * Helper for testing action with expected mutations inspired in * https://vuex.vuejs.org/en/testing.html * - * @param {Function} action to be tested + * @param {(Function|Object)} action to be tested, or object of named parameters * @param {Object} payload will be provided to the action * @param {Object} state will be provided to the action * @param {Array} [expectedMutations=[]] mutations expected to be committed @@ -39,15 +39,42 @@ const noop = () => {}; * [], // expected actions * ).then(done) * .catch(done.fail); + * + * @example + * await testAction({ + * action: actions.actionName, + * payload: { deleteListId: 1 }, + * state: { lists: [1, 2, 3] }, + * expectedMutations: [ { type: types.MUTATION} ], + * expectedActions: [], + * }) */ export default ( - action, - payload, - state, - expectedMutations = [], - expectedActions = [], - done = noop, + actionArg, + payloadArg, + stateArg, + expectedMutationsArg = [], + expectedActionsArg = [], + doneArg = noop, ) => { + let action = actionArg; + let payload = payloadArg; + let state = stateArg; + let expectedMutations = expectedMutationsArg; + let expectedActions = expectedActionsArg; + let done = doneArg; + + if (typeof actionArg !== 'function') { + ({ + action, + payload, + state, + expectedMutations = [], + expectedActions = [], + done = noop, + } = actionArg); + } + const mutations = []; const actions = []; diff --git a/spec/frontend/helpers/vuex_action_helper_spec.js b/spec/frontend/helpers/vuex_action_helper_spec.js index 61d05762a04..4d7bf21820a 100644 --- a/spec/frontend/helpers/vuex_action_helper_spec.js +++ b/spec/frontend/helpers/vuex_action_helper_spec.js @@ -1,166 +1,174 @@ import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; -import testAction from './vuex_action_helper'; - -describe('VueX test helper (testAction)', () => { - let originalExpect; - let assertion; - let mock; - const noop = () => {}; - - beforeEach(() => { - mock = new MockAdapter(axios); - /** - * In order to test the helper properly, we need to overwrite the Jest - * `expect` helper. We test that the testAction helper properly passes the - * dispatched actions/committed mutations to the Jest helper. - */ - originalExpect = expect; - assertion = null; - global.expect = actual => ({ - toEqual: () => { - originalExpect(actual).toEqual(assertion); - }, - }); - }); +import testActionFn from './vuex_action_helper'; - afterEach(() => { - mock.restore(); - global.expect = originalExpect; - }); +const testActionFnWithOptionsArg = (...args) => { + const [action, payload, state, expectedMutations, expectedActions, done] = args; + return testActionFn({ action, payload, state, expectedMutations, expectedActions, done }); +}; - it('properly passes state and payload to action', () => { - const exampleState = { FOO: 12, BAR: 3 }; - const examplePayload = { BAZ: 73, BIZ: 55 }; +describe.each([testActionFn, testActionFnWithOptionsArg])( + 'VueX test helper (testAction)', + testAction => { + let originalExpect; + let assertion; + let mock; + const noop = () => {}; - const action = ({ state }, payload) => { - originalExpect(state).toEqual(exampleState); - originalExpect(payload).toEqual(examplePayload); - }; + beforeEach(() => { + mock = new MockAdapter(axios); + /** + * In order to test the helper properly, we need to overwrite the Jest + * `expect` helper. We test that the testAction helper properly passes the + * dispatched actions/committed mutations to the Jest helper. + */ + originalExpect = expect; + assertion = null; + global.expect = actual => ({ + toEqual: () => { + originalExpect(actual).toEqual(assertion); + }, + }); + }); - assertion = { mutations: [], actions: [] }; + afterEach(() => { + mock.restore(); + global.expect = originalExpect; + }); - testAction(action, examplePayload, exampleState); - }); + it('properly passes state and payload to action', () => { + const exampleState = { FOO: 12, BAR: 3 }; + const examplePayload = { BAZ: 73, BIZ: 55 }; - describe('given a sync action', () => { - it('mocks committing mutations', () => { - const action = ({ commit }) => { - commit('MUTATION'); + const action = ({ state }, payload) => { + originalExpect(state).toEqual(exampleState); + originalExpect(payload).toEqual(examplePayload); }; - assertion = { mutations: [{ type: 'MUTATION' }], actions: [] }; + assertion = { mutations: [], actions: [] }; - testAction(action, null, {}, assertion.mutations, assertion.actions, noop); + testAction(action, examplePayload, exampleState); }); - it('mocks dispatching actions', () => { - const action = ({ dispatch }) => { - dispatch('ACTION'); - }; + describe('given a sync action', () => { + it('mocks committing mutations', () => { + const action = ({ commit }) => { + commit('MUTATION'); + }; - assertion = { actions: [{ type: 'ACTION' }], mutations: [] }; + assertion = { mutations: [{ type: 'MUTATION' }], actions: [] }; - testAction(action, null, {}, assertion.mutations, assertion.actions, noop); - }); + testAction(action, null, {}, assertion.mutations, assertion.actions, noop); + }); - it('works with done callback once finished', done => { - assertion = { mutations: [], actions: [] }; + it('mocks dispatching actions', () => { + const action = ({ dispatch }) => { + dispatch('ACTION'); + }; - testAction(noop, null, {}, assertion.mutations, assertion.actions, done); - }); + assertion = { actions: [{ type: 'ACTION' }], mutations: [] }; - it('returns a promise', done => { - assertion = { mutations: [], actions: [] }; + testAction(action, null, {}, assertion.mutations, assertion.actions, noop); + }); - testAction(noop, null, {}, assertion.mutations, assertion.actions) - .then(done) - .catch(done.fail); - }); - }); - - describe('given an async action (returning a promise)', () => { - let lastError; - const data = { FOO: 'BAR' }; - - const asyncAction = ({ commit, dispatch }) => { - dispatch('ACTION'); - - return axios - .get(TEST_HOST) - .catch(error => { - commit('ERROR'); - lastError = error; - throw error; - }) - .then(() => { - commit('SUCCESS'); - return data; - }); - }; + it('works with done callback once finished', done => { + assertion = { mutations: [], actions: [] }; - beforeEach(() => { - lastError = null; + testAction(noop, null, {}, assertion.mutations, assertion.actions, done); + }); + + it('returns a promise', done => { + assertion = { mutations: [], actions: [] }; + + testAction(noop, null, {}, assertion.mutations, assertion.actions) + .then(done) + .catch(done.fail); + }); }); - it('works with done callback once finished', done => { - mock.onGet(TEST_HOST).replyOnce(200, 42); + describe('given an async action (returning a promise)', () => { + let lastError; + const data = { FOO: 'BAR' }; - assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + const asyncAction = ({ commit, dispatch }) => { + dispatch('ACTION'); - testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done); - }); + return axios + .get(TEST_HOST) + .catch(error => { + commit('ERROR'); + lastError = error; + throw error; + }) + .then(() => { + commit('SUCCESS'); + return data; + }); + }; - it('returns original data of successful promise while checking actions/mutations', done => { - mock.onGet(TEST_HOST).replyOnce(200, 42); + beforeEach(() => { + lastError = null; + }); - assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + it('works with done callback once finished', done => { + mock.onGet(TEST_HOST).replyOnce(200, 42); - testAction(asyncAction, null, {}, assertion.mutations, assertion.actions) - .then(res => { - originalExpect(res).toEqual(data); - done(); - }) - .catch(done.fail); - }); + assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done); + }); + + it('returns original data of successful promise while checking actions/mutations', done => { + mock.onGet(TEST_HOST).replyOnce(200, 42); - it('returns original error of rejected promise while checking actions/mutations', done => { - mock.onGet(TEST_HOST).replyOnce(500, ''); + assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; - assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] }; + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions) + .then(res => { + originalExpect(res).toEqual(data); + done(); + }) + .catch(done.fail); + }); - testAction(asyncAction, null, {}, assertion.mutations, assertion.actions) - .then(done.fail) - .catch(error => { - originalExpect(error).toBe(lastError); - done(); - }); + it('returns original error of rejected promise while checking actions/mutations', done => { + mock.onGet(TEST_HOST).replyOnce(500, ''); + + assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] }; + + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions) + .then(done.fail) + .catch(error => { + originalExpect(error).toBe(lastError); + done(); + }); + }); }); - }); - it('works with async actions not returning promises', done => { - const data = { FOO: 'BAR' }; + it('works with async actions not returning promises', done => { + const data = { FOO: 'BAR' }; - const asyncAction = ({ commit, dispatch }) => { - dispatch('ACTION'); + const asyncAction = ({ commit, dispatch }) => { + dispatch('ACTION'); - axios - .get(TEST_HOST) - .then(() => { - commit('SUCCESS'); - return data; - }) - .catch(error => { - commit('ERROR'); - throw error; - }); - }; + axios + .get(TEST_HOST) + .then(() => { + commit('SUCCESS'); + return data; + }) + .catch(error => { + commit('ERROR'); + throw error; + }); + }; - mock.onGet(TEST_HOST).replyOnce(200, 42); + mock.onGet(TEST_HOST).replyOnce(200, 42); - assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; - testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done); - }); -}); + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done); + }); + }, +); diff --git a/spec/frontend/notes/components/multiline_comment_utils_spec.js b/spec/frontend/notes/components/multiline_comment_utils_spec.js index af4394cc648..99b33e7cd5f 100644 --- a/spec/frontend/notes/components/multiline_comment_utils_spec.js +++ b/spec/frontend/notes/components/multiline_comment_utils_spec.js @@ -34,8 +34,17 @@ describe('Multiline comment utilities', () => { expect(getSymbol(type)).toEqual(result); }); }); - describe('getCommentedLines', () => { - const diffLines = [{ line_code: '1' }, { line_code: '2' }, { line_code: '3' }]; + const inlineDiffLines = [{ line_code: '1' }, { line_code: '2' }, { line_code: '3' }]; + const parallelDiffLines = inlineDiffLines.map(line => ({ + left: { ...line }, + right: { ...line }, + })); + + describe.each` + view | diffLines + ${'inline'} | ${inlineDiffLines} + ${'parallel'} | ${parallelDiffLines} + `('getCommentedLines $view view', ({ diffLines }) => { it('returns a default object when `selectedCommentPosition` is not provided', () => { expect(getCommentedLines(undefined, diffLines)).toEqual({ startLine: 4, endLine: 4 }); }); diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js new file mode 100644 index 00000000000..e3e390f4394 --- /dev/null +++ b/spec/frontend/whats_new/utils/notification_spec.js @@ -0,0 +1,55 @@ +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import { setNotification, getStorageKey } from '~/whats_new/utils/notification'; + +describe('~/whats_new/utils/notification', () => { + useLocalStorageSpy(); + + let wrapper; + + const findNotificationEl = () => wrapper.querySelector('.header-help'); + const findNotificationCountEl = () => wrapper.querySelector('.js-whats-new-notification-count'); + const getAppEl = () => wrapper.querySelector('.app'); + + beforeEach(() => { + loadFixtures('static/whats_new_notification.html'); + wrapper = document.querySelector('.whats-new-notification-fixture-root'); + }); + + afterEach(() => { + wrapper.remove(); + }); + + describe('setNotification', () => { + const subject = () => setNotification(getAppEl()); + + it("when storage key doesn't exist it adds notifications class", () => { + const notificationEl = findNotificationEl(); + + expect(notificationEl.classList).not.toContain('with-notifications'); + + subject(); + + expect(findNotificationCountEl()).toExist(); + expect(notificationEl.classList).toContain('with-notifications'); + }); + + it('removes class and count element when storage key is true', () => { + const notificationEl = findNotificationEl(); + notificationEl.classList.add('with-notifications'); + localStorage.setItem('storage-key', 'false'); + + expect(findNotificationCountEl()).toExist(); + + subject(); + + expect(findNotificationCountEl()).not.toExist(); + expect(notificationEl.classList).not.toContain('with-notifications'); + }); + }); + + describe('getStorageKey', () => { + it('retrieves the storage key data attribute from the el', () => { + expect(getStorageKey(getAppEl())).toBe('storage-key'); + }); + }); +}); diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 153dc19335b..377e2c43a72 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -6,26 +6,6 @@ RSpec.describe MergeRequestsHelper do include ActionView::Helpers::UrlHelper include ProjectForksHelper - describe 'ci_build_details_path' do - let(:project) { create(:project) } - let(:merge_request) { MergeRequest.new } - let(:ci_service) { CiService.new } - let(:last_commit) { Ci::Pipeline.new({}) } - - before do - allow(merge_request).to receive(:source_project).and_return(project) - allow(merge_request).to receive(:last_commit).and_return(last_commit) - allow(project).to receive(:ci_service).and_return(ci_service) - allow(last_commit).to receive(:sha).and_return('12d65c') - end - - it 'does not include api credentials in a link' do - allow(ci_service) - .to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c") - expect(helper.ci_build_details_path(merge_request)).not_to match("secret") - end - end - describe '#state_name_with_icon' do using RSpec::Parameterized::TableSyntax diff --git a/spec/migrations/update_internal_ids_last_value_for_epics_spec.rb b/spec/migrations/update_internal_ids_last_value_for_epics_spec.rb new file mode 100644 index 00000000000..b53db5db0f8 --- /dev/null +++ b/spec/migrations/update_internal_ids_last_value_for_epics_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20201202081429_update_internal_ids_last_value_for_epics.rb') + +RSpec.describe UpdateInternalIdsLastValueForEpics, :migration, schema: 20201124185639 do + let(:namespaces) { table(:namespaces) } + let(:users) { table(:users) } + let(:epics) { table(:epics) } + let(:internal_ids) { table(:internal_ids) } + + let!(:author) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 0) } + let!(:group1) { namespaces.create!(type: 'Group', name: 'group1', path: 'group1') } + let!(:group2) { namespaces.create!(type: 'Group', name: 'group2', path: 'group2') } + let!(:group3) { namespaces.create!(type: 'Group', name: 'group3', path: 'group3') } + let!(:epic_last_value1) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group1.id) } + let!(:epic_last_value2) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group2.id) } + let!(:epic_last_value3) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group3.id) } + let!(:epic_1) { epics.create!(iid: 110, title: 'from epic 1', group_id: group1.id, author_id: author.id, title_html: 'any') } + let!(:epic_2) { epics.create!(iid: 5, title: 'from epic 1', group_id: group2.id, author_id: author.id, title_html: 'any') } + let!(:epic_3) { epics.create!(iid: 3, title: 'from epic 1', group_id: group3.id, author_id: author.id, title_html: 'any') } + + it 'updates out of sync internal_ids last_value' do + migrate! + + expect(internal_ids.find_by(usage: 4, namespace_id: group1.id).last_value).to eq(110) + expect(internal_ids.find_by(usage: 4, namespace_id: group2.id).last_value).to eq(5) + expect(internal_ids.find_by(usage: 4, namespace_id: group3.id).last_value).to eq(5) + end +end diff --git a/spec/models/dependency_proxy/registry_spec.rb b/spec/models/dependency_proxy/registry_spec.rb index 5bfa75a2eed..a888ee2b7f7 100644 --- a/spec/models/dependency_proxy/registry_spec.rb +++ b/spec/models/dependency_proxy/registry_spec.rb @@ -54,4 +54,11 @@ RSpec.describe DependencyProxy::Registry, type: :model do end end end + + describe '#authenticate_header' do + it 'returns the OAuth realm and service header' do + expect(described_class.authenticate_header) + .to eq("Bearer realm=\"#{Gitlab.config.gitlab.url}/jwt/auth\",service=\"dependency_proxy\"") + end + end end diff --git a/spec/models/namespace_onboarding_action_spec.rb b/spec/models/namespace_onboarding_action_spec.rb index 40ff965c134..70dcb989b32 100644 --- a/spec/models/namespace_onboarding_action_spec.rb +++ b/spec/models/namespace_onboarding_action_spec.rb @@ -5,7 +5,13 @@ require 'spec_helper' RSpec.describe NamespaceOnboardingAction do let(:namespace) { build(:namespace) } - it { is_expected.to belong_to :namespace } + describe 'associations' do + it { is_expected.to belong_to(:namespace).required } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:action) } + end describe '.completed?' do let(:action) { :subscription_created } diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index fe6c0f0a556..e154e691d5f 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -5,13 +5,13 @@ require 'spec_helper' RSpec.describe JwtController do include_context 'parsed logs' - let(:service) { double(execute: {}) } - let(:service_class) { double(new: service) } - let(:service_name) { 'test' } + let(:service) { double(execute: {} ) } + let(:service_class) { Auth::ContainerRegistryAuthenticationService } + let(:service_name) { 'container_registry' } let(:parameters) { { service: service_name } } before do - stub_const('JwtController::SERVICES', service_name => service_class) + allow(service_class).to receive(:new).and_return(service) end shared_examples 'user logging' do @@ -22,194 +22,266 @@ RSpec.describe JwtController do end end - context 'existing service' do - subject! { get '/jwt/auth', params: parameters } + context 'authenticating against container registry' do + context 'existing service' do + subject! { get '/jwt/auth', params: parameters } - it { expect(response).to have_gitlab_http_status(:ok) } + it { expect(response).to have_gitlab_http_status(:ok) } - context 'returning custom http code' do - let(:service) { double(execute: { http_status: 505 }) } + context 'returning custom http code' do + let(:service) { double(execute: { http_status: 505 }) } - it { expect(response).to have_gitlab_http_status(:http_version_not_supported) } + it { expect(response).to have_gitlab_http_status(:http_version_not_supported) } + end end - end - context 'when using authenticated request' do - shared_examples 'rejecting a blocked user' do - context 'with blocked user' do - let(:user) { create(:user, :blocked) } + context 'when using authenticated request' do + shared_examples 'rejecting a blocked user' do + context 'with blocked user' do + let(:user) { create(:user, :blocked) } - it 'rejects the request as unauthorized' do - expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('HTTP Basic: Access denied') + it 'rejects the request as unauthorized' do + expect(response).to have_gitlab_http_status(:unauthorized) + expect(response.body).to include('HTTP Basic: Access denied') + end end end - end - context 'using CI token' do - let(:user) { create(:user) } - let(:build) { create(:ci_build, :running, user: user) } - let(:project) { build.project } - let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } } + context 'using CI token' do + let(:user) { create(:user) } + let(:build) { create(:ci_build, :running, user: user) } + let(:project) { build.project } + let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } } - context 'project with enabled CI' do - subject! { get '/jwt/auth', params: parameters, headers: headers } - - it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) } + context 'project with enabled CI' do + subject! { get '/jwt/auth', params: parameters, headers: headers } - it_behaves_like 'user logging' - end + it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) } - context 'project with disabled CI' do - before do - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + it_behaves_like 'user logging' end - subject! { get '/jwt/auth', params: parameters, headers: headers } + context 'project with disabled CI' do + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end - it { expect(response).to have_gitlab_http_status(:unauthorized) } - end + subject! { get '/jwt/auth', params: parameters, headers: headers } - context 'using deploy tokens' do - let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) } - let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } } + it { expect(response).to have_gitlab_http_status(:unauthorized) } + end - subject! { get '/jwt/auth', params: parameters, headers: headers } + context 'using deploy tokens' do + let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) } + let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } } - it 'authenticates correctly' do - expect(response).to have_gitlab_http_status(:ok) - expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!) - end + subject! { get '/jwt/auth', params: parameters, headers: headers } - it 'does not log a user' do - expect(log_data.keys).not_to include(%w(username user_id)) + it 'authenticates correctly' do + expect(response).to have_gitlab_http_status(:ok) + expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!) + end + + it 'does not log a user' do + expect(log_data.keys).not_to include(%w(username user_id)) + end end - end - context 'using personal access tokens' do - let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } - let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + context 'using personal access tokens' do + let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } + let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } - before do - stub_container_registry_config(enabled: true) + before do + stub_container_registry_config(enabled: true) + end + + subject! { get '/jwt/auth', params: parameters, headers: headers } + + it 'authenticates correctly' do + expect(response).to have_gitlab_http_status(:ok) + expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) + end + + it_behaves_like 'rejecting a blocked user' + it_behaves_like 'user logging' end + end + + context 'using User login' do + let(:user) { create(:user) } + let(:headers) { { authorization: credentials(user.username, user.password) } } subject! { get '/jwt/auth', params: parameters, headers: headers } - it 'authenticates correctly' do - expect(response).to have_gitlab_http_status(:ok) - expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) - end + it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) } it_behaves_like 'rejecting a blocked user' - it_behaves_like 'user logging' - end - end - - context 'using User login' do - let(:user) { create(:user) } - let(:headers) { { authorization: credentials(user.username, user.password) } } - subject! { get '/jwt/auth', params: parameters, headers: headers } + context 'when passing a flat array of scopes' do + # We use this trick to make rails to generate a query_string: + # scope=scope1&scope=scope2 + # It works because :scope and 'scope' are the same as string, but different objects + let(:parameters) do + { + :service => service_name, + :scope => 'scope1', + 'scope' => 'scope2' + } + end - it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) } + let(:service_parameters) do + ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit! + end - it_behaves_like 'rejecting a blocked user' + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) } - context 'when passing a flat array of scopes' do - # We use this trick to make rails to generate a query_string: - # scope=scope1&scope=scope2 - # It works because :scope and 'scope' are the same as string, but different objects - let(:parameters) do - { - :service => service_name, - :scope => 'scope1', - 'scope' => 'scope2' - } + it_behaves_like 'user logging' end - let(:service_parameters) do - ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit! + context 'when user has 2FA enabled' do + let(:user) { create(:user, :two_factor) } + + context 'without personal token' do + it 'rejects the authorization attempt' do + expect(response).to have_gitlab_http_status(:unauthorized) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + + context 'with personal token' do + let(:access_token) { create(:personal_access_token, user: user) } + let(:headers) { { authorization: credentials(user.username, access_token.token) } } + + it 'accepts the authorization attempt' do + expect(response).to have_gitlab_http_status(:ok) + end + end end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) } + it 'does not cause session based checks to be activated' do + expect(Gitlab::Session).not_to receive(:with_session) + + get '/jwt/auth', params: parameters, headers: headers - it_behaves_like 'user logging' + expect(response).to have_gitlab_http_status(:ok) + end end - context 'when user has 2FA enabled' do - let(:user) { create(:user, :two_factor) } + context 'using invalid login' do + let(:headers) { { authorization: credentials('invalid', 'password') } } - context 'without personal token' do + context 'when internal auth is enabled' do it 'rejects the authorization attempt' do + get '/jwt/auth', params: parameters, headers: headers + expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end - context 'with personal token' do - let(:access_token) { create(:personal_access_token, user: user) } - let(:headers) { { authorization: credentials(user.username, access_token.token) } } + context 'when internal auth is disabled' do + it 'rejects the authorization attempt with personal access token message' do + allow_next_instance_of(ApplicationSetting) do |instance| + allow(instance).to receive(:password_authentication_enabled_for_git?) { false } + end + get '/jwt/auth', params: parameters, headers: headers - it 'accepts the authorization attempt' do - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:unauthorized) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end end + end - it 'does not cause session based checks to be activated' do - expect(Gitlab::Session).not_to receive(:with_session) - - get '/jwt/auth', params: parameters, headers: headers + context 'when using unauthenticated request' do + it 'accepts the authorization attempt' do + get '/jwt/auth', params: parameters expect(response).to have_gitlab_http_status(:ok) end + + it 'allows read access' do + expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities) + + get '/jwt/auth', params: parameters + end end - context 'using invalid login' do - let(:headers) { { authorization: credentials('invalid', 'password') } } + context 'unknown service' do + subject! { get '/jwt/auth', params: { service: 'unknown' } } - context 'when internal auth is enabled' do - it 'rejects the authorization attempt' do - get '/jwt/auth', params: parameters, headers: headers + it { expect(response).to have_gitlab_http_status(:not_found) } + end - expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') - end - end + def credentials(login, password) + ActionController::HttpAuthentication::Basic.encode_credentials(login, password) + end + end - context 'when internal auth is disabled' do - it 'rejects the authorization attempt with personal access token message' do - allow_next_instance_of(ApplicationSetting) do |instance| - allow(instance).to receive(:password_authentication_enabled_for_git?) { false } - end - get '/jwt/auth', params: parameters, headers: headers + context 'authenticating against dependency proxy' do + let_it_be(:user) { create(:user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :private, group: group) } + let_it_be(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) } + let_it_be(:project_deploy_token) { create(:deploy_token, :project, projects: [project]) } + let_it_be(:service_name) { 'dependency_proxy' } + let(:headers) { { authorization: credentials(credential_user, credential_password) } } + let(:params) { { account: credential_user, client_id: 'docker', offline_token: true, service: service_name } } + + before do + stub_config(dependency_proxy: { enabled: true }) + end - expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') - end + subject { get '/jwt/auth', params: params, headers: headers } + + shared_examples 'with valid credentials' do + it 'returns token successfully' do + subject + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['token']).to be_present end end - end - context 'when using unauthenticated request' do - it 'accepts the authorization attempt' do - get '/jwt/auth', params: parameters + context 'with personal access token' do + let(:credential_user) { nil } + let(:credential_password) { personal_access_token.token } - expect(response).to have_gitlab_http_status(:ok) + it_behaves_like 'with valid credentials' end - it 'allows read access' do - expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities) + context 'with user credentials token' do + let(:credential_user) { user.username } + let(:credential_password) { user.password } - get '/jwt/auth', params: parameters + it_behaves_like 'with valid credentials' end - end - context 'unknown service' do - subject! { get '/jwt/auth', params: { service: 'unknown' } } + context 'with group deploy token' do + let(:credential_user) { group_deploy_token.username } + let(:credential_password) { group_deploy_token.token } - it { expect(response).to have_gitlab_http_status(:not_found) } + it_behaves_like 'with valid credentials' + end + + context 'with project deploy token' do + let(:credential_user) { project_deploy_token.username } + let(:credential_password) { project_deploy_token.token } + + it_behaves_like 'with valid credentials' + end + + context 'with invalid credentials' do + let(:credential_user) { 'foo' } + let(:credential_password) { 'bar' } + + it 'returns unauthorized' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end def credentials(login, password) diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb index f4d5ccc81b6..f171c2faf5e 100644 --- a/spec/routing/group_routing_spec.rb +++ b/spec/routing/group_routing_spec.rb @@ -81,6 +81,10 @@ RSpec.describe "Groups", "routing" do end describe 'dependency proxy for containers' do + it 'routes to #authenticate' do + expect(get('/v2')).to route_to('groups/dependency_proxy_auth#authenticate') + end + context 'image name without namespace' do it 'routes to #manifest' do expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6')) diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 73d20cad4dd..0f931e7cc9e 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -179,6 +179,30 @@ RSpec.describe Profiles::KeysController, "routing" do end end +# keys GET /gpg_keys gpg_keys#index +# key POST /gpg_keys gpg_keys#create +# PUT /gpg_keys/:id gpg_keys#revoke +# DELETE /gpg_keys/:id gpg_keys#desroy +RSpec.describe Profiles::GpgKeysController, "routing" do + it "to #index" do + expect(get("/profile/gpg_keys")).to route_to('profiles/gpg_keys#index') + end + + it "to #create" do + expect(post("/profile/gpg_keys")).to route_to('profiles/gpg_keys#create') + end + + it "to #destroy" do + expect(delete("/profile/gpg_keys/1")).to route_to('profiles/gpg_keys#destroy', id: '1') + end + + it "to #get_keys" do + allow_any_instance_of(::Constraints::UserUrlConstrainer).to receive(:matches?).and_return(true) + + expect(get("/foo.gpg")).to route_to('profiles/gpg_keys#get_keys', username: 'foo') + end +end + # emails GET /emails(.:format) emails#index # POST /keys(.:format) emails#create # DELETE /keys/:id(.:format) keys#destroy diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb new file mode 100644 index 00000000000..ba50149f53a --- /dev/null +++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Auth::DependencyProxyAuthenticationService do + let_it_be(:user) { create(:user) } + let(:service) { Auth::DependencyProxyAuthenticationService.new(nil, user) } + + before do + stub_config(dependency_proxy: { enabled: true }) + end + + describe '#execute' do + subject { service.execute(authentication_abilities: nil) } + + context 'dependency proxy is not enabled' do + before do + stub_config(dependency_proxy: { enabled: false }) + end + + it 'returns not found' do + result = subject + + expect(result[:http_status]).to eq(404) + expect(result[:message]).to eq('dependency proxy not enabled') + end + end + + context 'without a user' do + let(:user) { nil } + + it 'returns forbidden' do + result = subject + + expect(result[:http_status]).to eq(403) + expect(result[:message]).to eq('access forbidden') + end + end + + context 'with a user' do + it 'returns a token' do + expect(subject[:token]).not_to be_nil + end + end + end +end diff --git a/spec/services/dependency_proxy/auth_token_service_spec.rb b/spec/services/dependency_proxy/auth_token_service_spec.rb new file mode 100644 index 00000000000..4b96f9d75a9 --- /dev/null +++ b/spec/services/dependency_proxy/auth_token_service_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe DependencyProxy::AuthTokenService do + include DependencyProxyHelpers + + describe '.decoded_token_payload' do + let_it_be(:user) { create(:user) } + let_it_be(:token) { build_jwt(user) } + + subject { described_class.decoded_token_payload(token.encoded) } + + it 'returns the user' do + result = subject + + expect(result['user_id']).to eq(user.id) + end + + it 'raises an error if the token is expired' do + travel_to(Time.zone.now + Auth::DependencyProxyAuthenticationService.token_expire_at + 1.minute) do + expect { subject }.to raise_error(JWT::ExpiredSignature) + end + end + + it 'raises an error if decoding fails' do + allow(JWT).to receive(:decode).and_raise(JWT::DecodeError) + + expect { subject }.to raise_error(JWT::DecodeError) + end + + it 'raises an error if signature is immature' do + allow(JWT).to receive(:decode).and_raise(JWT::ImmatureSignature) + + expect { subject }.to raise_error(JWT::ImmatureSignature) + end + end +end diff --git a/spec/services/onboarding_progress_service_spec.rb b/spec/services/onboarding_progress_service_spec.rb new file mode 100644 index 00000000000..edf40dfeed1 --- /dev/null +++ b/spec/services/onboarding_progress_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe OnboardingProgressService do + describe '#execute' do + let_it_be(:namespace) { build(:namespace) } + let(:action) { :namespace_action } + + subject(:execute_service) { described_class.new(namespace).execute(action: action) } + + it 'records a namespace onboarding progress action' do + expect(NamespaceOnboardingAction).to receive(:create_action) + .with(namespace, :namespace_action) + + subject + end + end +end diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb index 7c4b7f51cc3..4e303bfc20a 100644 --- a/spec/services/post_receive_service_spec.rb +++ b/spec/services/post_receive_service_spec.rb @@ -45,6 +45,12 @@ RSpec.describe PostReceiveService do it 'does not return error' do expect(subject).to be_empty end + + it 'does not record a namespace onboarding progress action' do + expect(NamespaceOnboardingAction).not_to receive(:create_action) + + subject + end end context 'when repository is nil' do @@ -80,6 +86,13 @@ RSpec.describe PostReceiveService do expect(response.reference_counter_decreased).to be(true) end + + it 'records a namespace onboarding progress action' do + expect(NamespaceOnboardingAction).to receive(:create_action) + .with(project.namespace, :git_write) + + subject + end end context 'with Project' do diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb index 545b9d1f4d0..0074cfb7931 100644 --- a/spec/support/helpers/dependency_proxy_helpers.rb +++ b/spec/support/helpers/dependency_proxy_helpers.rb @@ -25,6 +25,13 @@ module DependencyProxyHelpers .to_return(status: status, body: body) end + def build_jwt(user = nil, expire_time: nil) + JSONWebToken::HMACToken.new(::Auth::DependencyProxyAuthenticationService.secret).tap do |jwt| + jwt['user_id'] = user.id if user + jwt.expire_time = expire_time || jwt.issued_at + 1.minute + end + end + private def registry diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb index f4df1cf601c..389e5818dbe 100644 --- a/spec/support/helpers/gpg_helpers.rb +++ b/spec/support/helpers/gpg_helpers.rb @@ -144,6 +144,145 @@ module GpgHelpers '5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D' end + def secret_key2 + <<~KEY.strip + -----BEGIN PGP PRIVATE KEY BLOCK----- + + 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 + 4f6xMfFu2A== + =3YL6 + -----END PGP PRIVATE KEY BLOCK----- + KEY + end + + def public_key2 + <<~KEY.strip + -----BEGIN PGP PUBLIC KEY BLOCK----- + + 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 + =RAwd + -----END PGP PUBLIC KEY BLOCK----- + KEY + end + + def fingerprint2 + 'C447A6F6BFD9CEF8FB371785571625A930241179' + end + def names ['Nannie Bernhard'] end |