summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/groups/dependency_proxy_auth_controller_spec.rb79
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb79
-rw-r--r--spec/controllers/profiles/gpg_keys_controller_spec.rb136
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb8
-rw-r--r--spec/factories/gpg_keys.rb5
-rw-r--r--spec/features/groups/dependency_proxy_spec.rb16
-rw-r--r--spec/features/merge_request/close_reopen_report_toggle_spec.rb (renamed from spec/features/issuables/close_reopen_report_toggle_spec.rb)55
-rw-r--r--spec/features/merge_request/merge_request_discussion_lock_spec.rb (renamed from spec/features/issuables/merge_request_discussion_lock_spec.rb)0
-rw-r--r--spec/features/merge_request/user_reopens_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_squashes_merge_request_spec.rb (renamed from spec/features/merge_requests/user_squashes_merge_request_spec.rb)0
-rw-r--r--spec/features/merge_request/user_views_diffs_commit_spec.rb (renamed from spec/features/merge_requests/user_views_diffs_commit_spec.rb)0
-rw-r--r--spec/features/merge_requests/user_sees_empty_state_spec.rb (renamed from spec/features/merge_request/user_sees_empty_state_spec.rb)0
-rw-r--r--spec/frontend/fixtures/static/whats_new_notification.html6
-rw-r--r--spec/frontend/helpers/vuex_action_helper.js41
-rw-r--r--spec/frontend/helpers/vuex_action_helper_spec.js264
-rw-r--r--spec/frontend/notes/components/multiline_comment_utils_spec.js13
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js55
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb20
-rw-r--r--spec/migrations/update_internal_ids_last_value_for_epics_spec.rb30
-rw-r--r--spec/models/dependency_proxy/registry_spec.rb7
-rw-r--r--spec/models/namespace_onboarding_action_spec.rb8
-rw-r--r--spec/requests/jwt_controller_spec.rb318
-rw-r--r--spec/routing/group_routing_spec.rb4
-rw-r--r--spec/routing/routing_spec.rb24
-rw-r--r--spec/services/auth/dependency_proxy_authentication_service_spec.rb46
-rw-r--r--spec/services/dependency_proxy/auth_token_service_spec.rb37
-rw-r--r--spec/services/onboarding_progress_service_spec.rb19
-rw-r--r--spec/services/post_receive_service_spec.rb13
-rw-r--r--spec/support/helpers/dependency_proxy_helpers.rb7
-rw-r--r--spec/support/helpers/gpg_helpers.rb139
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