From eb71b5da81ae70592f6dc5dfb6ee83469c97d1bb Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 6 Apr 2018 13:12:21 +0100 Subject: Remove modal store and mixins from global scope --- spec/javascripts/boards/modal_store_spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index e9d77f035e3..797693a21aa 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -4,12 +4,11 @@ import '~/vue_shared/models/label'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/models/assignee'; -import '~/boards/stores/modal_store'; +import Store from '~/boards/stores/modal_store'; describe('Modal store', () => { let issue; let issue2; - const Store = gl.issueBoards.ModalStore; beforeEach(() => { // Setup default state -- cgit v1.2.1 From 7009ec6562ae4313a3968c60f984643d0c4b35d6 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Fri, 9 Mar 2018 17:01:14 +0200 Subject: Move BoardBlankState vue component --- spec/javascripts/boards/board_blank_state_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index f757dadfada..664ea202e93 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,7 +1,7 @@ /* global BoardService */ import Vue from 'vue'; import '~/boards/stores/boards_store'; -import boardBlankState from '~/boards/components/board_blank_state'; +import BoardBlankState from '~/boards/components/board_blank_state.vue'; import { mockBoardService } from './mock_data'; describe('Boards blank state', () => { @@ -9,7 +9,7 @@ describe('Boards blank state', () => { let fail = false; beforeEach((done) => { - const Comp = Vue.extend(boardBlankState); + const Comp = Vue.extend(BoardBlankState); gl.issueBoards.BoardsStore.create(); gl.boardService = mockBoardService(); -- cgit v1.2.1 From db18993f652425b72c4b854e18a002e0ec44b196 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Mon, 19 Mar 2018 10:11:12 -0600 Subject: Create barebones for Deploytoken Includes: - Model, factories, create service and controller actions - As usual, includes specs for everything - Builds UI (copy from PAT) - Add revoke action Closes #31591 --- .../projects/deploy_tokens_controller_spec.rb | 55 ++++++++++++++++++++++ .../settings/repository_controller_spec.rb | 26 ++++++++++ spec/factories/deploy_tokens.rb | 22 +++++++++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/deploy_token_spec.rb | 38 +++++++++++++++ .../settings/deploy_tokens_presenter_spec.rb | 46 ++++++++++++++++++ spec/services/deploy_tokens/create_service_spec.rb | 45 ++++++++++++++++++ 7 files changed, 233 insertions(+) create mode 100644 spec/controllers/projects/deploy_tokens_controller_spec.rb create mode 100644 spec/factories/deploy_tokens.rb create mode 100644 spec/models/deploy_token_spec.rb create mode 100644 spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb create mode 100644 spec/services/deploy_tokens/create_service_spec.rb (limited to 'spec') diff --git a/spec/controllers/projects/deploy_tokens_controller_spec.rb b/spec/controllers/projects/deploy_tokens_controller_spec.rb new file mode 100644 index 00000000000..0ade61f4380 --- /dev/null +++ b/spec/controllers/projects/deploy_tokens_controller_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Projects::DeployTokensController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let!(:member) { project.add_master(user) } + + before do + sign_in(user) + end + + describe 'POST #create' do + let(:deploy_token_params) { attributes_for(:deploy_token) } + subject do + post :create, + namespace_id: project.namespace, + project_id: project, + deploy_token: deploy_token_params + end + + context 'with valid params' do + it 'should create a new DeployToken' do + expect { subject }.to change(DeployToken, :count).by(1) + end + + it 'should include a flash notice' do + subject + expect(flash[:notice]).to eq('Your new project deploy token has been created.') + end + end + + context 'with invalid params' do + let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) } + + it 'should not create a new DeployToken' do + expect { subject }.not_to change(DeployToken, :count) + end + + it 'should include a flash alert with the error message' do + subject + expect(flash[:alert]).to eq("Scopes can't be blank") + end + end + + context 'when user does not have enough permissions' do + let!(:member) { project.add_developer(user) } + + it 'responds with status 404' do + subject + + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb index 3a4014b7768..03867661483 100644 --- a/spec/controllers/projects/settings/repository_controller_spec.rb +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -16,5 +16,31 @@ describe Projects::Settings::RepositoryController do expect(response).to have_gitlab_http_status(200) expect(response).to render_template(:show) end + + context 'with no deploy token params' do + it 'should build an empty instance of DeployToken' do + get :show, namespace_id: project.namespace, project_id: project + + deploy_token = assigns(:deploy_token) + expect(deploy_token).to be_an_instance_of(DeployToken) + expect(deploy_token.name).to be_nil + expect(deploy_token.expires_at).to be_nil + expect(deploy_token.scopes).to eq([]) + end + end + + context 'when rendering an invalid deploy token' do + let(:deploy_token_attributes) { attributes_for(:deploy_token, project_id: project.id) } + + it 'should build an instance of DeployToken' do + get :show, namespace_id: project.namespace, project_id: project, deploy_token: deploy_token_attributes + + deploy_token = assigns(:deploy_token) + expect(deploy_token).to be_an_instance_of(DeployToken) + expect(deploy_token.name).to eq(deploy_token_attributes[:name]) + expect(deploy_token.expires_at.to_date).to eq(deploy_token_attributes[:expires_at].to_date) + expect(deploy_token.scopes).to match_array(deploy_token_attributes[:scopes]) + end + end end end diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb new file mode 100644 index 00000000000..fa349e10ddc --- /dev/null +++ b/spec/factories/deploy_tokens.rb @@ -0,0 +1,22 @@ +FactoryBot.define do + factory :deploy_token do + project + token { SecureRandom.hex(50) } + sequence(:name) { |n| "PDT #{n}" } + revoked false + expires_at { 5.days.from_now } + scopes %w(read_repo read_registry) + + trait :revoked do + revoked true + end + + trait :read_repo do + scopes ['read_repo'] + end + + trait :read_registry do + scopes ['read_registry'] + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index b675d5dc031..d38e665436f 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -281,6 +281,7 @@ project: - project_badges - source_of_merge_requests - internal_ids +- deploy_tokens award_emoji: - awardable - user diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb new file mode 100644 index 00000000000..bd27da63dfe --- /dev/null +++ b/spec/models/deploy_token_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe DeployToken do + it { is_expected.to belong_to :project } + + describe 'validations' do + let(:project_deploy_token) { build(:deploy_token) } + + context 'with no scopes defined' do + it 'should not be valid' do + project_deploy_token.scopes = [] + + expect(project_deploy_token).not_to be_valid + expect(project_deploy_token.errors[:scopes].first).to eq("can't be blank") + end + end + end + + describe '#ensure_token' do + let(:project_deploy_token) { build(:deploy_token) } + + it 'should ensure a token' do + project_deploy_token.token = nil + project_deploy_token.save + + expect(project_deploy_token.token).not_to be_empty + end + end + + describe '#revoke!' do + subject { create(:deploy_token) } + + it 'should update revoke attribute' do + subject.revoke! + expect(subject.revoked?).to be_truthy + end + end +end diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb new file mode 100644 index 00000000000..d3210439b05 --- /dev/null +++ b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Projects::Settings::DeployTokensPresenter do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:deploy_tokens) { create_list(:deploy_token, 3, project: project) } + + subject(:presenter) { described_class.new(deploy_tokens, current_user: user, project: project) } + + describe '#available_scopes' do + it 'returns the all the deploy token scopes' do + expect(presenter.available_scopes).to match_array(%w(read_repo read_registry)) + end + end + + describe '#scope_description' do + let(:deploy_token) { create(:deploy_token, project: project, scopes: [:read_registry]) } + + it 'returns the description for a given scope' do + description = 'Allows read-only access to the registry images' + expect(presenter.scope_description('read_registry')).to eq(description) + end + end + + describe '#length' do + it 'returns the size of deploy tokens presented' do + expect(presenter.length).to eq(3) + end + end + + describe '#new_deploy_token' do + context 'when a deploy token has been created recently' do + it 'returns the token of the deploy' do + deploy_token = ::DeployTokens::CreateService.new(project, user, attributes_for(:deploy_token)).execute + + expect(presenter.new_deploy_token).to eq(deploy_token.token) + end + end + + context 'when a deploy token has not been created recently' do + it 'does returns nil' do + expect(presenter.new_deploy_token).to be_nil + end + end + end +end diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb new file mode 100644 index 00000000000..84aa17971d6 --- /dev/null +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:deploy_token_params) { attributes_for(:deploy_token) } + + describe '#execute' do + subject { described_class.new(project, user, deploy_token_params) } + + context 'when the deploy token is valid' do + it 'should create a new DeployToken' do + expect { subject.execute }.to change { DeployToken.count }.by(1) + end + + it 'should assign the DeployToken to the project' do + subject.execute + + expect(subject.project).to eq(project) + end + + it 'should store the token on redis' do + subject.execute + redis_key = DeployToken.redis_shared_state_key(user.id) + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil + end + end + + context 'when the deploy token is invalid' do + let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) } + + it 'it should not create a new DeployToken' do + expect { subject.execute }.not_to change { DeployToken.count } + end + + it 'should not store the token on redis' do + subject.execute + redis_key = DeployToken.redis_shared_state_key(user.id) + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil + end + end + end +end -- cgit v1.2.1 From 370fc05da7f95bf6621867a71d51493cf3899e25 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 29 Mar 2018 16:56:35 -0600 Subject: Implement 'read_repo' for DeployTokens This will allow to download a repo using the token from the DeployToken --- .../projects/settings/repository_settings_spec.rb | 38 +++++++++++++++ spec/lib/gitlab/auth_spec.rb | 57 ++++++++++++++++++++-- spec/lib/gitlab/git_access_spec.rb | 27 ++++++++++ spec/models/deploy_token_spec.rb | 46 +++++++++++------ 4 files changed, 151 insertions(+), 17 deletions(-) (limited to 'spec') diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 14670e91006..1106a31d444 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -88,5 +88,43 @@ feature 'Repository settings' do expect(page).not_to have_content(private_deploy_key.title) end end + + context 'Deploy tokens' do + let(:deploy_token) { create(:deploy_token, project: project, expires_at: Date.today + 2.days) } + + before do + project.deploy_tokens << deploy_token + visit project_settings_repository_path(project) + end + + scenario 'view deploy tokens' do + within('.deploy-tokens') do + expect(page).to have_content(deploy_token.name) + expect(page).to have_content('In 1 day') + expect(page).to have_content(deploy_token.scopes.join(", ")) + end + end + + scenario 'add a new deploy token' do + fill_in 'deploy_token_name', with: 'new_deploy_key' + fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s + check 'deploy_token_scopes_read_repo' + check 'deploy_token_scopes_read_registry' + click_button 'Create deploy token' + + expect(page).to have_content('Your new project deploy token has been created') + end + + scenario 'revoke a deploy token', :js do + within('.deploy-tokens') do + click_link 'Revoke' + click_link "Revoke #{deploy_token.name}" + + expect(page).not_to have_content(deploy_token.name) + expect(page).not_to have_content('In 1 day') + expect(page).not_to have_content(deploy_token.scopes.join(", ")) + end + end + end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 18cef8ec996..685a0bb54be 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Auth do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq %i[api read_user sudo] + expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repo] end it 'OPENID_SCOPES contains all scopes for OpenID Connect' do @@ -19,7 +19,7 @@ describe Gitlab::Auth do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid] + expect(subject.optional_scopes).to eq %i[read_user sudo read_repo read_registry openid] end context 'registry_scopes' do @@ -231,7 +231,7 @@ describe Gitlab::Auth do .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end - it 'falls through oauth authentication when the username is oauth2' do + it 'fails through oauth authentication when the username is oauth2' do user = create( :user, username: 'oauth2', @@ -255,6 +255,57 @@ describe Gitlab::Auth do expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) end + + context 'while using deploy tokens' do + let(:project) { create(:project) } + let(:deploy_token) { create(:deploy_token, :read_repo, project: project) } + let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) } + let(:abilities) { %i(read_project download_code) } + + it 'succeeds when project is present, token is valid and has read_repo as scope' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_success) + end + + it 'fails if deploy token does not have read_repo as scope' do + deploy_token = create(:deploy_token, :read_registry, project: project) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', nil, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is not related to project' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', 'abcdef', project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails for any other project' do + another_project = create(:project) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: another_project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token has been revoked' do + deploy_token.revoke! + + expect(deploy_token.revoked?).to be_truthy + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') + expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + end end describe 'find_with_user_password' do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index c870997e274..928825c21fa 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -145,6 +145,33 @@ describe Gitlab::GitAccess do expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) end end + + context 'when actor is DeployToken' do + context 'when DeployToken is active and belongs to project' do + let(:actor) { create(:deploy_token, :read_repo, project: project) } + + it 'allows pull access' do + expect { pull_access_check }.not_to raise_error + end + end + + context 'when DeployToken has been revoked' do + let(:actor) { create(:deploy_token, :read_repo, project: project) } + + it 'blocks pull access' do + actor.revoke! + expect { pull_access_check }.to raise_not_found + end + end + + context 'when DeployToken does not belong to project' do + let(:actor) { create(:deploy_token, :read_repo) } + + it 'blocks pull access' do + expect { pull_access_check }.to raise_not_found + end + end + end end context 'when actor is nil' do diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index bd27da63dfe..26d846ac6c8 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -1,38 +1,56 @@ require 'spec_helper' describe DeployToken do + let(:deploy_token) { create(:deploy_token) } + it { is_expected.to belong_to :project } describe 'validations' do - let(:project_deploy_token) { build(:deploy_token) } - context 'with no scopes defined' do it 'should not be valid' do - project_deploy_token.scopes = [] + deploy_token.scopes = [] - expect(project_deploy_token).not_to be_valid - expect(project_deploy_token.errors[:scopes].first).to eq("can't be blank") + expect(deploy_token).not_to be_valid + expect(deploy_token.errors[:scopes].first).to eq("can't be blank") end end end describe '#ensure_token' do - let(:project_deploy_token) { build(:deploy_token) } - it 'should ensure a token' do - project_deploy_token.token = nil - project_deploy_token.save + deploy_token.token = nil + deploy_token.save - expect(project_deploy_token.token).not_to be_empty + expect(deploy_token.token).not_to be_empty end end describe '#revoke!' do - subject { create(:deploy_token) } - it 'should update revoke attribute' do - subject.revoke! - expect(subject.revoked?).to be_truthy + deploy_token.revoke! + expect(deploy_token.revoked?).to be_truthy + end + end + + describe "#active?" do + context "when it has been revoked" do + it 'should return false' do + deploy_token.revoke! + expect(deploy_token.active?).to be_falsy + end + end + + context "when it hasn't been revoked" do + it 'should return true' do + expect(deploy_token.active?).to be_truthy + end + end + end + + describe '#username' do + it 'returns Ghost username' do + ghost = User.ghost + expect(deploy_token.username).to eq(ghost.username) end end end -- cgit v1.2.1 From 345ac03b7afb1dc9b941c53bc45cc3dfcf22e61c Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 29 Mar 2018 20:11:36 -0600 Subject: Address UX review - Keep 'Deploy Section' open upon save, otherwise the token might get lost - When an error appears, display the error inside the form and also keep the Deploy Section open - Changue copy of revoke modal --- .../projects/deploy_tokens_controller_spec.rb | 9 +++++-- .../settings/repository_controller_spec.rb | 26 ++++++++++++++----- .../projects/settings/repository_settings_spec.rb | 6 ++--- spec/services/deploy_tokens/create_service_spec.rb | 30 +++++++++++++++------- 4 files changed, 49 insertions(+), 22 deletions(-) (limited to 'spec') diff --git a/spec/controllers/projects/deploy_tokens_controller_spec.rb b/spec/controllers/projects/deploy_tokens_controller_spec.rb index 0ade61f4380..f037aacfe8e 100644 --- a/spec/controllers/projects/deploy_tokens_controller_spec.rb +++ b/spec/controllers/projects/deploy_tokens_controller_spec.rb @@ -27,6 +27,11 @@ describe Projects::DeployTokensController do subject expect(flash[:notice]).to eq('Your new project deploy token has been created.') end + + it 'should redirect to project settings repository' do + subject + expect(response).to redirect_to project_settings_repository_path(project) + end end context 'with invalid params' do @@ -36,9 +41,9 @@ describe Projects::DeployTokensController do expect { subject }.not_to change(DeployToken, :count) end - it 'should include a flash alert with the error message' do + it 'should redirect to project settings repository' do subject - expect(flash[:alert]).to eq("Scopes can't be blank") + expect(response).to redirect_to project_settings_repository_path(project) end end diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb index 03867661483..da4d3e5732d 100644 --- a/spec/controllers/projects/settings/repository_controller_spec.rb +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::Settings::RepositoryController do +describe Projects::Settings::RepositoryController, :clean_gitlab_redis_shared_state do let(:project) { create(:project_empty_repo, :public) } let(:user) { create(:user) } @@ -17,7 +17,7 @@ describe Projects::Settings::RepositoryController do expect(response).to render_template(:show) end - context 'with no deploy token params' do + context 'with no deploy token attributes present' do it 'should build an empty instance of DeployToken' do get :show, namespace_id: project.namespace, project_id: project @@ -29,17 +29,29 @@ describe Projects::Settings::RepositoryController do end end - context 'when rendering an invalid deploy token' do - let(:deploy_token_attributes) { attributes_for(:deploy_token, project_id: project.id) } + context 'with deploy token attributes present' do + let(:deploy_token_key) { "gitlab:deploy_token:#{project.id}:#{user.id}:attributes" } - it 'should build an instance of DeployToken' do - get :show, namespace_id: project.namespace, project_id: project, deploy_token: deploy_token_attributes + let(:deploy_token_attributes) do + { + name: 'test-token', + expires_at: Date.today + 1.month + } + end + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set(deploy_token_key, deploy_token_attributes.to_json) + end + + get :show, namespace_id: project.namespace, project_id: project + end + + it 'should build an instance of DeployToken' do deploy_token = assigns(:deploy_token) expect(deploy_token).to be_an_instance_of(DeployToken) expect(deploy_token.name).to eq(deploy_token_attributes[:name]) expect(deploy_token.expires_at.to_date).to eq(deploy_token_attributes[:expires_at].to_date) - expect(deploy_token.scopes).to match_array(deploy_token_attributes[:scopes]) end end end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 1106a31d444..f0997b6809d 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -90,17 +90,16 @@ feature 'Repository settings' do end context 'Deploy tokens' do - let(:deploy_token) { create(:deploy_token, project: project, expires_at: Date.today + 2.days) } + let(:deploy_token) { create(:deploy_token, project: project) } before do project.deploy_tokens << deploy_token visit project_settings_repository_path(project) - end + end scenario 'view deploy tokens' do within('.deploy-tokens') do expect(page).to have_content(deploy_token.name) - expect(page).to have_content('In 1 day') expect(page).to have_content(deploy_token.scopes.join(", ")) end end @@ -121,7 +120,6 @@ feature 'Repository settings' do click_link "Revoke #{deploy_token.name}" expect(page).not_to have_content(deploy_token.name) - expect(page).not_to have_content('In 1 day') expect(page).not_to have_content(deploy_token.scopes.join(", ")) end end diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb index 84aa17971d6..df18213cf84 100644 --- a/spec/services/deploy_tokens/create_service_spec.rb +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -6,40 +6,52 @@ describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do let(:deploy_token_params) { attributes_for(:deploy_token) } describe '#execute' do - subject { described_class.new(project, user, deploy_token_params) } + subject { described_class.new(project, user, deploy_token_params).execute } context 'when the deploy token is valid' do it 'should create a new DeployToken' do - expect { subject.execute }.to change { DeployToken.count }.by(1) + expect { subject }.to change { DeployToken.count }.by(1) end - it 'should assign the DeployToken to the project' do - subject.execute + it 'returns a DeployToken' do + expect(subject).to be_an_instance_of DeployToken + end + it 'should assign the DeployToken to the project' do expect(subject.project).to eq(project) end it 'should store the token on redis' do - subject.execute - redis_key = DeployToken.redis_shared_state_key(user.id) + redis_key = subject.redis_shared_state_key(user.id) expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil end + + it 'should not store deploy token attributes on redis' do + redis_key = subject.redis_shared_state_key(user.id) + ":attributes" + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil + end end context 'when the deploy token is invalid' do let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) } it 'it should not create a new DeployToken' do - expect { subject.execute }.not_to change { DeployToken.count } + expect { subject }.not_to change { DeployToken.count } end it 'should not store the token on redis' do - subject.execute - redis_key = DeployToken.redis_shared_state_key(user.id) + redis_key = subject.redis_shared_state_key(user.id) expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil end + + it 'should store deploy token attributes on redis' do + redis_key = subject.redis_shared_state_key(user.id) + ":attributes" + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil + end end end end -- cgit v1.2.1 From aaa6d80870d5215390a7cd919d91309e5a8795b7 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Sat, 31 Mar 2018 16:45:02 -0600 Subject: Implement read_registry for DeployTokens --- spec/lib/gitlab/auth_spec.rb | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 685a0bb54be..758fb17cd81 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -270,14 +270,6 @@ describe Gitlab::Auth do .to eq(auth_success) end - it 'fails if deploy token does not have read_repo as scope' do - deploy_token = create(:deploy_token, :read_registry, project: project) - - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) - .to eq(auth_failure) - end - it 'fails if token is nil' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') expect(gl_auth.find_for_git_client('', nil, project: project, ip: 'ip')) @@ -305,6 +297,35 @@ describe Gitlab::Auth do expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip')) .to eq(auth_failure) end + + context 'when registry enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds if deploy token does have read_registry as scope' do + deploy_token = create(:deploy_token, :read_registry, project: project) + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image]) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_success) + end + end + + context 'when registry disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'fails if deploy token have read_registry as scope' do + deploy_token = create(:deploy_token, :read_registry, project: project) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + end end end -- cgit v1.2.1 From 46a6036cf976d1a92dc1e7ff4994414bd43bfc78 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Mon, 2 Apr 2018 09:45:37 -0500 Subject: Addreses frontend review Also fixes spec failures on presenter and docs --- spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb index d3210439b05..ca734019727 100644 --- a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb @@ -28,18 +28,18 @@ describe Projects::Settings::DeployTokensPresenter do end end - describe '#new_deploy_token' do + describe '#temporal_token' do context 'when a deploy token has been created recently' do it 'returns the token of the deploy' do deploy_token = ::DeployTokens::CreateService.new(project, user, attributes_for(:deploy_token)).execute - expect(presenter.new_deploy_token).to eq(deploy_token.token) + expect(presenter.temporal_token).to eq(deploy_token.token) end end context 'when a deploy token has not been created recently' do it 'does returns nil' do - expect(presenter.new_deploy_token).to be_nil + expect(presenter.temporal_token).to be_nil end end end -- cgit v1.2.1 From 7deab3172257bef7818ce834c1e0709432ddd5e0 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Tue, 3 Apr 2018 16:34:56 -0500 Subject: Removes logic from Jwt and handle different scenarios on Gitlab::Auth - When using 'read_repo' password and project are sent, so we used both of them to fetch for the token - When using 'read_registry' only the password is sent, so we only use that for fetching the token --- spec/lib/gitlab/auth_spec.rb | 151 ++++++++++++++------- spec/policies/project_policy_spec.rb | 4 +- ...ntainer_registry_authentication_service_spec.rb | 2 +- 3 files changed, 102 insertions(+), 55 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 758fb17cd81..79984787e2a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -195,7 +195,7 @@ describe Gitlab::Auth do personal_access_token = create(:personal_access_token, scopes: ['read_registry']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image])) + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_project, :build_download_code, :project_read_container_image])) end end @@ -258,72 +258,119 @@ describe Gitlab::Auth do context 'while using deploy tokens' do let(:project) { create(:project) } - let(:deploy_token) { create(:deploy_token, :read_repo, project: project) } let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) } - let(:abilities) { %i(read_project download_code) } - it 'succeeds when project is present, token is valid and has read_repo as scope' do - auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) + context 'when the deploy token has read_repo as scope' do + let(:deploy_token) { create(:deploy_token, :read_repo, project: project) } - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) - .to eq(auth_success) - end - - it 'fails if token is nil' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', nil, project: project, ip: 'ip')) - .to eq(auth_failure) - end - - it 'fails if token is not related to project' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', 'abcdef', project: project, ip: 'ip')) - .to eq(auth_failure) - end + it 'succeeds when project is present, token is valid and has read_repo as scope' do + abilities = %i(read_project download_code) + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) - it 'fails for any other project' do - another_project = create(:project) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: another_project, ip: 'ip')) - .to eq(auth_failure) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_success) + end - it 'fails if token has been revoked' do - deploy_token.revoke! + it 'fails if token is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', nil, project: project, ip: 'ip')) + .to eq(auth_failure) + end - expect(deploy_token.revoked?).to be_truthy - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') - expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip')) - .to eq(auth_failure) - end + it 'fails if token is not related to project' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', 'abcdef', project: project, ip: 'ip')) + .to eq(auth_failure) + end - context 'when registry enabled' do - before do - stub_container_registry_config(enabled: true) + it 'fails for any other project' do + another_project = create(:project) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: another_project, ip: 'ip')) + .to eq(auth_failure) end - it 'succeeds if deploy token does have read_registry as scope' do - deploy_token = create(:deploy_token, :read_registry, project: project) - auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image]) + it 'fails if token has been revoked' do + deploy_token.revoke! - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) - .to eq(auth_success) + expect(deploy_token.revoked?).to be_truthy + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') + expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) end end - context 'when registry disabled' do - before do - stub_container_registry_config(enabled: false) + context 'when the deploy token has read_registry as a scope' do + let(:deploy_token) { create(:deploy_token, :read_registry, project: project) } + + context 'when registry enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds if deploy token does have read_registry as scope' do + abilities = %i(read_project build_download_code project_read_container_image) + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_success) + end + + it 'fails if token is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is not related to project' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', 'abcdef', project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token has been revoked' do + deploy_token.revoke! + + expect(deploy_token.revoked?).to be_truthy + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') + expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_failure) + end end - it 'fails if deploy token have read_registry as scope' do - deploy_token = create(:deploy_token, :read_registry, project: project) + context 'when registry disabled' do + before do + stub_container_registry_config(enabled: false) + end - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) - .to eq(auth_failure) + it 'fails if deploy token have read_registry as scope' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is not related to project' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', 'abcdef', project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token has been revoked' do + deploy_token.revoke! + + expect(deploy_token.revoked?).to be_truthy + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') + expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_failure) + end end end end @@ -430,7 +477,7 @@ describe Gitlab::Auth do [ :read_project, :build_download_code, - :build_read_container_image, + :project_read_container_image, :build_create_container_image ] end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 905d82b3bb1..f5d9a58f83c 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -28,7 +28,7 @@ describe ProjectPolicy do end let(:team_member_reporter_permissions) do - %i[build_download_code build_read_container_image] + %i[build_download_code project_read_container_image] end let(:developer_permissions) do @@ -54,7 +54,7 @@ describe ProjectPolicy do let(:public_permissions) do %i[ download_code fork_project read_commit_status read_pipeline - read_container_image build_download_code build_read_container_image + read_container_image build_download_code project_read_container_image download_wiki_code ] end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 290eeae828e..1cb0508cdf5 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -373,7 +373,7 @@ describe Auth::ContainerRegistryAuthenticationService do let(:current_user) { create(:user) } let(:authentication_abilities) do - [:build_read_container_image, :build_create_container_image] + [:project_read_container_image, :build_create_container_image] end before do -- cgit v1.2.1 From 171b2625b128e5954ce0a150a4fc923a22164e4e Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Wed, 4 Apr 2018 18:43:41 -0500 Subject: Addreses backend review suggestions - Remove extra method for authorize_admin_project - Ensure project presence - Rename 'read_repo' to 'read_repository' to be more verbose --- spec/factories/deploy_tokens.rb | 6 +-- spec/lib/gitlab/auth_spec.rb | 10 ++--- spec/models/deploy_token_spec.rb | 1 + spec/policies/deploy_token_policy_spec.rb | 45 ++++++++++++++++++++++ .../settings/deploy_tokens_presenter_spec.rb | 2 +- 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 spec/policies/deploy_token_policy_spec.rb (limited to 'spec') diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb index fa349e10ddc..7cce55a3e14 100644 --- a/spec/factories/deploy_tokens.rb +++ b/spec/factories/deploy_tokens.rb @@ -5,14 +5,14 @@ FactoryBot.define do sequence(:name) { |n| "PDT #{n}" } revoked false expires_at { 5.days.from_now } - scopes %w(read_repo read_registry) + scopes %w(read_repository read_registry) trait :revoked do revoked true end - trait :read_repo do - scopes ['read_repo'] + trait :read_repository do + scopes ['read_repository'] end trait :read_registry do diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 79984787e2a..f704c20f598 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Auth do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repo] + expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repository] end it 'OPENID_SCOPES contains all scopes for OpenID Connect' do @@ -19,7 +19,7 @@ describe Gitlab::Auth do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user sudo read_repo read_registry openid] + expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid] end context 'registry_scopes' do @@ -260,10 +260,10 @@ describe Gitlab::Auth do let(:project) { create(:project) } let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) } - context 'when the deploy token has read_repo as scope' do - let(:deploy_token) { create(:deploy_token, :read_repo, project: project) } + context 'when the deploy token has read_repository as scope' do + let(:deploy_token) { create(:deploy_token, :read_repository, project: project) } - it 'succeeds when project is present, token is valid and has read_repo as scope' do + it 'succeeds when project is present, token is valid and has read_repository as scope' do abilities = %i(read_project download_code) auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 26d846ac6c8..50f6f441a58 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -4,6 +4,7 @@ describe DeployToken do let(:deploy_token) { create(:deploy_token) } it { is_expected.to belong_to :project } + it { is_expected.to validate_presence_of :project } describe 'validations' do context 'with no scopes defined' do diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb new file mode 100644 index 00000000000..cbb5fb815a1 --- /dev/null +++ b/spec/policies/deploy_token_policy_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe DeployTokenPolicy do + let(:current_user) { create(:user) } + let(:project) { create(:project) } + let(:deploy_token) { create(:deploy_token, project: project) } + + subject { described_class.new(current_user, deploy_token) } + + describe 'creating a deploy key' do + context 'when user is master' do + before do + project.add_master(current_user) + end + + it { is_expected.to be_allowed(:create_deploy_token) } + end + + context 'when user is not master' do + before do + project.add_developer(current_user) + end + + it { is_expected.to be_disallowed(:create_deploy_token) } + end + end + + describe 'updating a deploy key' do + context 'when user is master' do + before do + project.add_master(current_user) + end + + it { is_expected.to be_allowed(:update_deploy_token) } + end + + context 'when user is not master' do + before do + project.add_developer(current_user) + end + + it { is_expected.to be_disallowed(:update_deploy_token) } + end + end +end diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb index ca734019727..7bfe074ad30 100644 --- a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb @@ -9,7 +9,7 @@ describe Projects::Settings::DeployTokensPresenter do describe '#available_scopes' do it 'returns the all the deploy token scopes' do - expect(presenter.available_scopes).to match_array(%w(read_repo read_registry)) + expect(presenter.available_scopes).to match_array(%w(read_repository read_registry)) end end -- cgit v1.2.1 From 72220a99d1cdbcf8a914f9e765c43e63eaee2548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 5 Apr 2018 15:49:18 +0200 Subject: Support Deploy Tokens properly without hacking abilities --- spec/lib/gitlab/auth_spec.rb | 6 +++--- spec/policies/project_policy_spec.rb | 4 ++-- .../services/auth/container_registry_authentication_service_spec.rb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f704c20f598..4ed554f06ec 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -195,7 +195,7 @@ describe Gitlab::Auth do personal_access_token = create(:personal_access_token, scopes: ['read_registry']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_project, :build_download_code, :project_read_container_image])) + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_project, :build_download_code, :build_read_container_image])) end end @@ -310,7 +310,7 @@ describe Gitlab::Auth do end it 'succeeds if deploy token does have read_registry as scope' do - abilities = %i(read_project build_download_code project_read_container_image) + abilities = %i(read_project build_download_code build_read_container_image) auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') @@ -477,7 +477,7 @@ describe Gitlab::Auth do [ :read_project, :build_download_code, - :project_read_container_image, + :build_read_container_image, :build_create_container_image ] end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index f5d9a58f83c..905d82b3bb1 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -28,7 +28,7 @@ describe ProjectPolicy do end let(:team_member_reporter_permissions) do - %i[build_download_code project_read_container_image] + %i[build_download_code build_read_container_image] end let(:developer_permissions) do @@ -54,7 +54,7 @@ describe ProjectPolicy do let(:public_permissions) do %i[ download_code fork_project read_commit_status read_pipeline - read_container_image build_download_code project_read_container_image + read_container_image build_download_code build_read_container_image download_wiki_code ] end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 1cb0508cdf5..290eeae828e 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -373,7 +373,7 @@ describe Auth::ContainerRegistryAuthenticationService do let(:current_user) { create(:user) } let(:authentication_abilities) do - [:project_read_container_image, :build_create_container_image] + [:build_read_container_image, :build_create_container_image] end before do -- cgit v1.2.1 From 8315861c9a50675b4f4f4ca536f0da90f27994f3 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 5 Apr 2018 12:22:34 -0500 Subject: Include ProjectDeployTokens Also: - Changes scopes from serializer to use boolean columns - Fixes broken specs --- spec/factories/deploy_tokens.rb | 12 +----- spec/factories/project_deploy_tokens.rb | 6 +++ .../projects/settings/repository_settings_spec.rb | 14 +++--- spec/lib/gitlab/auth_spec.rb | 11 +---- spec/lib/gitlab/git_access_spec.rb | 16 ++----- spec/models/deploy_token_spec.rb | 50 +++++++++++++++------- spec/models/project_deploy_token_spec.rb | 15 +++++++ spec/models/project_spec.rb | 2 + spec/policies/deploy_token_policy_spec.rb | 2 +- .../settings/deploy_tokens_presenter_spec.rb | 18 +------- ...ntainer_registry_authentication_service_spec.rb | 1 + spec/services/deploy_tokens/create_service_spec.rb | 30 ++++++++----- 12 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 spec/factories/project_deploy_tokens.rb create mode 100644 spec/models/project_deploy_token_spec.rb (limited to 'spec') diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb index 7cce55a3e14..5fea4a9d5a6 100644 --- a/spec/factories/deploy_tokens.rb +++ b/spec/factories/deploy_tokens.rb @@ -1,22 +1,14 @@ FactoryBot.define do factory :deploy_token do - project token { SecureRandom.hex(50) } sequence(:name) { |n| "PDT #{n}" } + read_repository true + read_registry true revoked false expires_at { 5.days.from_now } - scopes %w(read_repository read_registry) trait :revoked do revoked true end - - trait :read_repository do - scopes ['read_repository'] - end - - trait :read_registry do - scopes ['read_registry'] - end end end diff --git a/spec/factories/project_deploy_tokens.rb b/spec/factories/project_deploy_tokens.rb new file mode 100644 index 00000000000..4866cb58d88 --- /dev/null +++ b/spec/factories/project_deploy_tokens.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :project_deploy_token do + project + deploy_token + end +end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index f0997b6809d..7887178a3ed 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -90,25 +90,26 @@ feature 'Repository settings' do end context 'Deploy tokens' do - let(:deploy_token) { create(:deploy_token, project: project) } + let(:deploy_token_project) { create(:project_deploy_token, project: project) } + let!(:deploy_token) { deploy_token_project.deploy_token } before do - project.deploy_tokens << deploy_token visit project_settings_repository_path(project) end scenario 'view deploy tokens' do within('.deploy-tokens') do expect(page).to have_content(deploy_token.name) - expect(page).to have_content(deploy_token.scopes.join(", ")) + expect(page).to have_content('read_repository') + expect(page).to have_content('read_registry') end end scenario 'add a new deploy token' do fill_in 'deploy_token_name', with: 'new_deploy_key' fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s - check 'deploy_token_scopes_read_repo' - check 'deploy_token_scopes_read_registry' + check 'deploy_token_read_repository' + check 'deploy_token_read_registry' click_button 'Create deploy token' expect(page).to have_content('Your new project deploy token has been created') @@ -120,7 +121,8 @@ feature 'Repository settings' do click_link "Revoke #{deploy_token.name}" expect(page).not_to have_content(deploy_token.name) - expect(page).not_to have_content(deploy_token.scopes.join(", ")) + expect(page).not_to have_content('read_repository') + expect(page).not_to have_content('read_registry') end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 4ed554f06ec..db517c25ef4 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -261,7 +261,7 @@ describe Gitlab::Auth do let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) } context 'when the deploy token has read_repository as scope' do - let(:deploy_token) { create(:deploy_token, :read_repository, project: project) } + let(:deploy_token) { create(:deploy_token, read_registry: false, projects: [project]) } it 'succeeds when project is present, token is valid and has read_repository as scope' do abilities = %i(read_project download_code) @@ -284,13 +284,6 @@ describe Gitlab::Auth do .to eq(auth_failure) end - it 'fails for any other project' do - another_project = create(:project) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: another_project, ip: 'ip')) - .to eq(auth_failure) - end - it 'fails if token has been revoked' do deploy_token.revoke! @@ -302,7 +295,7 @@ describe Gitlab::Auth do end context 'when the deploy token has read_registry as a scope' do - let(:deploy_token) { create(:deploy_token, :read_registry, project: project) } + let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) } context 'when registry enabled' do before do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 928825c21fa..000e9e86813 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -147,25 +147,17 @@ describe Gitlab::GitAccess do end context 'when actor is DeployToken' do - context 'when DeployToken is active and belongs to project' do - let(:actor) { create(:deploy_token, :read_repo, project: project) } + let(:project_deploy_token) { create(:project_deploy_token, project: project) } + let(:actor) { project_deploy_token.deploy_token } + context 'when DeployToken is active and belongs to project' do it 'allows pull access' do expect { pull_access_check }.not_to raise_error end end - context 'when DeployToken has been revoked' do - let(:actor) { create(:deploy_token, :read_repo, project: project) } - - it 'blocks pull access' do - actor.revoke! - expect { pull_access_check }.to raise_not_found - end - end - context 'when DeployToken does not belong to project' do - let(:actor) { create(:deploy_token, :read_repo) } + let(:actor) { create(:deploy_token) } it 'blocks pull access' do expect { pull_access_check }.to raise_not_found diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 50f6f441a58..395c97f13a5 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -1,28 +1,49 @@ require 'spec_helper' describe DeployToken do - let(:deploy_token) { create(:deploy_token) } + subject(:deploy_token) { create(:deploy_token) } - it { is_expected.to belong_to :project } - it { is_expected.to validate_presence_of :project } + it { is_expected.to have_many :project_deploy_tokens } + it { is_expected.to have_many(:projects).through(:project_deploy_tokens) } - describe 'validations' do - context 'with no scopes defined' do - it 'should not be valid' do - deploy_token.scopes = [] + describe '#ensure_token' do + it 'should ensure a token' do + deploy_token.token = nil + deploy_token.save + + expect(deploy_token.token).not_to be_empty + end + end + + describe '#ensure_at_least_one_scope' do + context 'with at least one scope' do + it 'should be valid' do + is_expected.to be_valid + end + end + + context 'with no scopes' do + it 'should be invalid' do + deploy_token = build(:deploy_token, read_repository: false, read_registry: false) expect(deploy_token).not_to be_valid - expect(deploy_token.errors[:scopes].first).to eq("can't be blank") + expect(deploy_token.errors[:base].first).to eq("Scopes can't be blank") end end end - describe '#ensure_token' do - it 'should ensure a token' do - deploy_token.token = nil - deploy_token.save + describe '#scopes' do + context 'with all the scopes' do + it 'should return scopes assigned to DeployToken' do + expect(deploy_token.scopes).to eq([:read_repository, :read_registry]) + end + end - expect(deploy_token.token).not_to be_empty + context 'with only one scope' do + it 'should return scopes assigned to DeployToken' do + deploy_token = create(:deploy_token, read_registry: false) + expect(deploy_token.scopes).to eq([:read_repository]) + end end end @@ -50,8 +71,7 @@ describe DeployToken do describe '#username' do it 'returns Ghost username' do - ghost = User.ghost - expect(deploy_token.username).to eq(ghost.username) + expect(deploy_token.username).to eq("gitlab+deploy-token-#{deploy_token.id}") end end end diff --git a/spec/models/project_deploy_token_spec.rb b/spec/models/project_deploy_token_spec.rb new file mode 100644 index 00000000000..ccaed23f11a --- /dev/null +++ b/spec/models/project_deploy_token_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe ProjectDeployToken, type: :model do + let(:project) { create(:project) } + let(:deploy_token) { create(:deploy_token) } + subject(:project_deploy_token) { create(:project_deploy_token, project: project, deploy_token: deploy_token) } + + it { is_expected.to belong_to :project } + it { is_expected.to belong_to :deploy_token } + it { is_expected.to accept_nested_attributes_for :deploy_token } + + it { is_expected.to validate_presence_of :deploy_token } + it { is_expected.to validate_presence_of :project } + it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:project_id) } +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7007f78e702..2675c2f52c1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -84,6 +84,8 @@ describe Project do it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:lfs_file_locks) } + it { is_expected.to have_many(:project_deploy_tokens) } + it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } context 'after initialized' do it "has a project_feature" do diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb index cbb5fb815a1..f6d8d19aac9 100644 --- a/spec/policies/deploy_token_policy_spec.rb +++ b/spec/policies/deploy_token_policy_spec.rb @@ -15,7 +15,7 @@ describe DeployTokenPolicy do it { is_expected.to be_allowed(:create_deploy_token) } end - + context 'when user is not master' do before do project.add_developer(current_user) diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb index 7bfe074ad30..f52bd46074d 100644 --- a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb @@ -3,25 +3,11 @@ require 'spec_helper' describe Projects::Settings::DeployTokensPresenter do let(:user) { create(:user) } let(:project) { create(:project) } - let(:deploy_tokens) { create_list(:deploy_token, 3, project: project) } + let!(:project_deploy_tokens) { create_list(:project_deploy_token, 3, project: project) } + let(:deploy_tokens) { project.deploy_tokens } subject(:presenter) { described_class.new(deploy_tokens, current_user: user, project: project) } - describe '#available_scopes' do - it 'returns the all the deploy token scopes' do - expect(presenter.available_scopes).to match_array(%w(read_repository read_registry)) - end - end - - describe '#scope_description' do - let(:deploy_token) { create(:deploy_token, project: project, scopes: [:read_registry]) } - - it 'returns the description for a given scope' do - description = 'Allows read-only access to the registry images' - expect(presenter.scope_description('read_registry')).to eq(description) - end - end - describe '#length' do it 'returns the size of deploy tokens presented' do expect(presenter.length).to eq(3) diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 290eeae828e..0949ec24c50 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -558,6 +558,7 @@ describe Auth::ContainerRegistryAuthenticationService do let(:project) { create(:project, :public) } context 'when pulling and pushing' do + let(:current_user) { create(:deploy_token, projects: [project]) } let(:current_params) do { scope: "repository:#{project.full_path}:pull,push" } end diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb index df18213cf84..4830f17faa8 100644 --- a/spec/services/deploy_tokens/create_service_spec.rb +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -13,42 +13,50 @@ describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do expect { subject }.to change { DeployToken.count }.by(1) end - it 'returns a DeployToken' do - expect(subject).to be_an_instance_of DeployToken + it 'should create a new ProjectDeployToken' do + expect { subject }.to change { ProjectDeployToken.count }.by(1) end - it 'should assign the DeployToken to the project' do - expect(subject.project).to eq(project) + it 'returns a DeployToken' do + expect(subject).to be_an_instance_of DeployToken end it 'should store the token on redis' do - redis_key = subject.redis_shared_state_key(user.id) + redis_key = DeployToken.redis_shared_state_key(user.id) + subject expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil end - it 'should not store deploy token attributes on redis' do - redis_key = subject.redis_shared_state_key(user.id) + ":attributes" + it 'should not store deploy token attributes on redis' do + redis_key = DeployToken.redis_shared_state_key(user.id) + ":attributes" + subject expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil end end context 'when the deploy token is invalid' do - let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) } + let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) } - it 'it should not create a new DeployToken' do + it 'should not create a new DeployToken' do expect { subject }.not_to change { DeployToken.count } end + it 'should not create a new ProjectDeployToken' do + expect { subject }.not_to change { ProjectDeployToken.count } + end + it 'should not store the token on redis' do - redis_key = subject.redis_shared_state_key(user.id) + redis_key = DeployToken.redis_shared_state_key(user.id) + subject expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil end it 'should store deploy token attributes on redis' do - redis_key = subject.redis_shared_state_key(user.id) + ":attributes" + redis_key = DeployToken.redis_shared_state_key(user.id) + ":attributes" + subject expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil end -- cgit v1.2.1 From a475411f4380ef4d0260940206e2553da3b2f3ee Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 5 Apr 2018 20:04:11 -0500 Subject: Fixes broken schema and minor changes --- spec/controllers/projects/deploy_tokens_controller_spec.rb | 1 + spec/lib/gitlab/auth_spec.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/controllers/projects/deploy_tokens_controller_spec.rb b/spec/controllers/projects/deploy_tokens_controller_spec.rb index f037aacfe8e..8de564a56af 100644 --- a/spec/controllers/projects/deploy_tokens_controller_spec.rb +++ b/spec/controllers/projects/deploy_tokens_controller_spec.rb @@ -11,6 +11,7 @@ describe Projects::DeployTokensController do describe 'POST #create' do let(:deploy_token_params) { attributes_for(:deploy_token) } + subject do post :create, namespace_id: project.namespace, diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index db517c25ef4..7be888d812f 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -264,7 +264,7 @@ describe Gitlab::Auth do let(:deploy_token) { create(:deploy_token, read_registry: false, projects: [project]) } it 'succeeds when project is present, token is valid and has read_repository as scope' do - abilities = %i(read_project download_code) + abilities = %i(download_code) auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') -- cgit v1.2.1 From c4f56a88029c1fe73bf6efb062b5f77a65282fed Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 5 Apr 2018 22:02:13 -0500 Subject: Increase test suite around deploy tokens behavior Also, fixes broken specs --- .../projects/deploy_tokens_controller_spec.rb | 61 --------------- .../settings/repository_controller_spec.rb | 40 +--------- spec/lib/gitlab/auth_spec.rb | 90 +++++++++++----------- spec/lib/gitlab/git_access_spec.rb | 49 +++++++++++- spec/lib/gitlab/import_export/all_models.yml | 4 + spec/models/deploy_token_spec.rb | 21 ++++- spec/policies/deploy_token_policy_spec.rb | 2 +- .../settings/deploy_tokens_presenter_spec.rb | 16 ---- ...ntainer_registry_authentication_service_spec.rb | 1 - spec/services/deploy_tokens/create_service_spec.rb | 28 ------- 10 files changed, 119 insertions(+), 193 deletions(-) delete mode 100644 spec/controllers/projects/deploy_tokens_controller_spec.rb (limited to 'spec') diff --git a/spec/controllers/projects/deploy_tokens_controller_spec.rb b/spec/controllers/projects/deploy_tokens_controller_spec.rb deleted file mode 100644 index 8de564a56af..00000000000 --- a/spec/controllers/projects/deploy_tokens_controller_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe Projects::DeployTokensController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let!(:member) { project.add_master(user) } - - before do - sign_in(user) - end - - describe 'POST #create' do - let(:deploy_token_params) { attributes_for(:deploy_token) } - - subject do - post :create, - namespace_id: project.namespace, - project_id: project, - deploy_token: deploy_token_params - end - - context 'with valid params' do - it 'should create a new DeployToken' do - expect { subject }.to change(DeployToken, :count).by(1) - end - - it 'should include a flash notice' do - subject - expect(flash[:notice]).to eq('Your new project deploy token has been created.') - end - - it 'should redirect to project settings repository' do - subject - expect(response).to redirect_to project_settings_repository_path(project) - end - end - - context 'with invalid params' do - let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) } - - it 'should not create a new DeployToken' do - expect { subject }.not_to change(DeployToken, :count) - end - - it 'should redirect to project settings repository' do - subject - expect(response).to redirect_to project_settings_repository_path(project) - end - end - - context 'when user does not have enough permissions' do - let!(:member) { project.add_developer(user) } - - it 'responds with status 404' do - subject - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb index da4d3e5732d..3a4014b7768 100644 --- a/spec/controllers/projects/settings/repository_controller_spec.rb +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::Settings::RepositoryController, :clean_gitlab_redis_shared_state do +describe Projects::Settings::RepositoryController do let(:project) { create(:project_empty_repo, :public) } let(:user) { create(:user) } @@ -16,43 +16,5 @@ describe Projects::Settings::RepositoryController, :clean_gitlab_redis_shared_st expect(response).to have_gitlab_http_status(200) expect(response).to render_template(:show) end - - context 'with no deploy token attributes present' do - it 'should build an empty instance of DeployToken' do - get :show, namespace_id: project.namespace, project_id: project - - deploy_token = assigns(:deploy_token) - expect(deploy_token).to be_an_instance_of(DeployToken) - expect(deploy_token.name).to be_nil - expect(deploy_token.expires_at).to be_nil - expect(deploy_token.scopes).to eq([]) - end - end - - context 'with deploy token attributes present' do - let(:deploy_token_key) { "gitlab:deploy_token:#{project.id}:#{user.id}:attributes" } - - let(:deploy_token_attributes) do - { - name: 'test-token', - expires_at: Date.today + 1.month - } - end - - before do - Gitlab::Redis::SharedState.with do |redis| - redis.set(deploy_token_key, deploy_token_attributes.to_json) - end - - get :show, namespace_id: project.namespace, project_id: project - end - - it 'should build an instance of DeployToken' do - deploy_token = assigns(:deploy_token) - expect(deploy_token).to be_an_instance_of(DeployToken) - expect(deploy_token.name).to eq(deploy_token_attributes[:name]) - expect(deploy_token.expires_at.to_date).to eq(deploy_token_attributes[:expires_at].to_date) - end - end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 7be888d812f..e3ec707076a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -195,7 +195,7 @@ describe Gitlab::Auth do personal_access_token = create(:personal_access_token, scopes: ['read_registry']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_project, :build_download_code, :build_read_container_image])) + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:build_read_container_image])) end end @@ -262,25 +262,38 @@ describe Gitlab::Auth do context 'when the deploy token has read_repository as scope' do let(:deploy_token) { create(:deploy_token, read_registry: false, projects: [project]) } + let(:login) { deploy_token.username } - it 'succeeds when project is present, token is valid and has read_repository as scope' do - abilities = %i(download_code) - auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) + it 'succeeds when login and token are valid' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code]) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: project, ip: 'ip')) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip')) .to eq(auth_success) end + it 'fails when login is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login') + expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails when token is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip')) + .to eq(auth_failure) + end + it 'fails if token is nil' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', nil, project: project, ip: 'ip')) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, nil, project: project, ip: 'ip')) .to eq(auth_failure) end it 'fails if token is not related to project' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', 'abcdef', project: project, ip: 'ip')) + another_deploy_token = create(:deploy_token) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, another_deploy_token.token, project: project, ip: 'ip')) .to eq(auth_failure) end @@ -296,30 +309,42 @@ describe Gitlab::Auth do context 'when the deploy token has read_registry as a scope' do let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) } + let(:login) { deploy_token.username } context 'when registry enabled' do before do stub_container_registry_config(enabled: true) end - it 'succeeds if deploy token does have read_registry as scope' do - abilities = %i(read_project build_download_code build_read_container_image) - auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, abilities) + it 'succeeds when login and token are valid' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:build_read_container_image]) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: nil, ip: 'ip')) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip')) .to eq(auth_success) end + it 'fails when login is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login') + expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails when token is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip')) + .to eq(auth_failure) + end + it 'fails if token is nil' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, nil, project: nil, ip: 'ip')) .to eq(auth_failure) end it 'fails if token is not related to project' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', 'abcdef', project: nil, ip: 'ip')) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, ip: 'ip')) .to eq(auth_failure) end @@ -338,30 +363,9 @@ describe Gitlab::Auth do stub_container_registry_config(enabled: false) end - it 'fails if deploy token have read_registry as scope' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', deploy_token.token, project: nil, ip: 'ip')) - .to eq(auth_failure) - end - - it 'fails if token is nil' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')) - .to eq(auth_failure) - end - - it 'fails if token is not related to project' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', 'abcdef', project: nil, ip: 'ip')) - .to eq(auth_failure) - end - - it 'fails if token has been revoked' do - deploy_token.revoke! - - expect(deploy_token.revoked?).to be_truthy - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') - expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip')) + it 'fails when login and token are valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip')) .to eq(auth_failure) end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 000e9e86813..6c625596605 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -147,21 +147,29 @@ describe Gitlab::GitAccess do end context 'when actor is DeployToken' do - let(:project_deploy_token) { create(:project_deploy_token, project: project) } - let(:actor) { project_deploy_token.deploy_token } + let(:actor) { create(:deploy_token, projects: [project]) } context 'when DeployToken is active and belongs to project' do it 'allows pull access' do expect { pull_access_check }.not_to raise_error end + + it 'blocks the push' do + expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) + end end context 'when DeployToken does not belong to project' do - let(:actor) { create(:deploy_token) } + let(:another_project) { create(:project) } + let(:actor) { create(:deploy_token, projects: [another_project]) } it 'blocks pull access' do expect { pull_access_check }.to raise_not_found end + + it 'blocks the push' do + expect { push_access_check }.to raise_not_found + end end end end @@ -613,6 +621,41 @@ describe Gitlab::GitAccess do end end + describe 'deploy token permissions' do + let(:deploy_token) { create(:deploy_token) } + let(:actor) { deploy_token } + + context 'pull code' do + context 'when project is authorized' do + before do + deploy_token.projects << project + end + + it { expect { pull_access_check }.not_to raise_error } + end + + context 'when unauthorized' do + context 'from public project' do + let(:project) { create(:project, :public, :repository) } + + it { expect { pull_access_check }.not_to raise_error } + end + + context 'from internal project' do + let(:project) { create(:project, :internal, :repository) } + + it { expect { pull_access_check }.to raise_not_found } + end + + context 'from private project' do + let(:project) { create(:project, :private, :repository) } + + it { expect { pull_access_check }.to raise_not_found } + end + end + end + end + describe 'build authentication_abilities permissions' do let(:authentication_abilities) { build_authentication_abilities } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index d38e665436f..897a5984782 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -145,6 +145,9 @@ pipeline_schedule: - pipelines pipeline_schedule_variables: - pipeline_schedule +deploy_tokens: +- project_deploy_tokens +- projects deploy_keys: - user - deploy_keys_projects @@ -281,6 +284,7 @@ project: - project_badges - source_of_merge_requests - internal_ids +- project_deploy_tokens - deploy_tokens award_emoji: - awardable diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 395c97f13a5..1adc049ca58 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -70,8 +70,27 @@ describe DeployToken do end describe '#username' do - it 'returns Ghost username' do + it 'returns a harcoded username' do expect(deploy_token.username).to eq("gitlab+deploy-token-#{deploy_token.id}") end end + + describe '#has_access_to?' do + let(:project) { create(:project) } + + subject(:deploy_token) { create(:deploy_token, projects: [project]) } + + context 'when the deploy token has access to the project' do + it 'should return true' do + expect(deploy_token.has_access_to?(project)).to be_truthy + end + end + + context 'when the deploy token does not have access to the project' do + it 'should return false' do + another_project = create(:project) + expect(deploy_token.has_access_to?(another_project)).to be_falsy + end + end + end end diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb index f6d8d19aac9..eea287d895e 100644 --- a/spec/policies/deploy_token_policy_spec.rb +++ b/spec/policies/deploy_token_policy_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe DeployTokenPolicy do let(:current_user) { create(:user) } let(:project) { create(:project) } - let(:deploy_token) { create(:deploy_token, project: project) } + let(:deploy_token) { create(:deploy_token, projects: [project]) } subject { described_class.new(current_user, deploy_token) } diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb index f52bd46074d..a416acffe94 100644 --- a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb @@ -13,20 +13,4 @@ describe Projects::Settings::DeployTokensPresenter do expect(presenter.length).to eq(3) end end - - describe '#temporal_token' do - context 'when a deploy token has been created recently' do - it 'returns the token of the deploy' do - deploy_token = ::DeployTokens::CreateService.new(project, user, attributes_for(:deploy_token)).execute - - expect(presenter.temporal_token).to eq(deploy_token.token) - end - end - - context 'when a deploy token has not been created recently' do - it 'does returns nil' do - expect(presenter.temporal_token).to be_nil - end - end - end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 0949ec24c50..290eeae828e 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -558,7 +558,6 @@ describe Auth::ContainerRegistryAuthenticationService do let(:project) { create(:project, :public) } context 'when pulling and pushing' do - let(:current_user) { create(:deploy_token, projects: [project]) } let(:current_params) do { scope: "repository:#{project.full_path}:pull,push" } end diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb index 4830f17faa8..2a308a9bf8c 100644 --- a/spec/services/deploy_tokens/create_service_spec.rb +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -20,20 +20,6 @@ describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do it 'returns a DeployToken' do expect(subject).to be_an_instance_of DeployToken end - - it 'should store the token on redis' do - redis_key = DeployToken.redis_shared_state_key(user.id) - subject - - expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil - end - - it 'should not store deploy token attributes on redis' do - redis_key = DeployToken.redis_shared_state_key(user.id) + ":attributes" - subject - - expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil - end end context 'when the deploy token is invalid' do @@ -46,20 +32,6 @@ describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do it 'should not create a new ProjectDeployToken' do expect { subject }.not_to change { ProjectDeployToken.count } end - - it 'should not store the token on redis' do - redis_key = DeployToken.redis_shared_state_key(user.id) - subject - - expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil - end - - it 'should store deploy token attributes on redis' do - redis_key = DeployToken.redis_shared_state_key(user.id) + ":attributes" - subject - - expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil - end end end end -- cgit v1.2.1 From 29913816309c6f6387b20c8702bcc8e90ef3a984 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Fri, 6 Apr 2018 09:30:21 -0500 Subject: Addresses database comments - Adds a default on expires_at datetime - Modifies deploy tokens views to handle default expires at value - Use datetime_with_timezone where possible - Remove unused scopes --- spec/features/projects/settings/repository_settings_spec.rb | 1 + spec/models/project_deploy_token_spec.rb | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 7887178a3ed..2528c7f437d 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -94,6 +94,7 @@ feature 'Repository settings' do let!(:deploy_token) { deploy_token_project.deploy_token } before do + stub_container_registry_config(enabled: true) visit project_settings_repository_path(project) end diff --git a/spec/models/project_deploy_token_spec.rb b/spec/models/project_deploy_token_spec.rb index ccaed23f11a..9e2e40c2e8f 100644 --- a/spec/models/project_deploy_token_spec.rb +++ b/spec/models/project_deploy_token_spec.rb @@ -7,7 +7,6 @@ RSpec.describe ProjectDeployToken, type: :model do it { is_expected.to belong_to :project } it { is_expected.to belong_to :deploy_token } - it { is_expected.to accept_nested_attributes_for :deploy_token } it { is_expected.to validate_presence_of :deploy_token } it { is_expected.to validate_presence_of :project } -- cgit v1.2.1 From 8c276580431a750a3141f2ec57690da844a75ac4 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Fri, 6 Apr 2018 10:22:08 -0500 Subject: Removes deploy tokens presenter --- .../projects/settings/deploy_tokens_presenter_spec.rb | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb (limited to 'spec') diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb deleted file mode 100644 index a416acffe94..00000000000 --- a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe Projects::Settings::DeployTokensPresenter do - let(:user) { create(:user) } - let(:project) { create(:project) } - let!(:project_deploy_tokens) { create_list(:project_deploy_token, 3, project: project) } - let(:deploy_tokens) { project.deploy_tokens } - - subject(:presenter) { described_class.new(deploy_tokens, current_user: user, project: project) } - - describe '#length' do - it 'returns the size of deploy tokens presented' do - expect(presenter.length).to eq(3) - end - end -end -- cgit v1.2.1 From 18a1569319af918fe3aff7564e344143d04d6aca Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Fri, 6 Apr 2018 11:23:45 -0500 Subject: Handles default expires_at date directly into DeployToken model --- spec/services/deploy_tokens/create_service_spec.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb index 2a308a9bf8c..2e02b7a28b5 100644 --- a/spec/services/deploy_tokens/create_service_spec.rb +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do +describe DeployTokens::CreateService do let(:project) { create(:project) } let(:user) { create(:user) } let(:deploy_token_params) { attributes_for(:deploy_token) } @@ -22,6 +22,14 @@ describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state do end end + context 'when expires at date is not passed' do + let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } + + it 'should set FOREVER date' do + expect(subject.expires_at).to eq(DeployToken::FOREVER) + end + end + context 'when the deploy token is invalid' do let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) } -- cgit v1.2.1 From 5bc58bac2678aed9c8b2318f9f4d4825baa2b110 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Fri, 6 Apr 2018 14:48:17 -0500 Subject: Handle limit for datetime attributes on MySQL The TIMESTAMP data type is used for values that contain both date and time parts. TIMESTAMP has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC. A Forever lib class was included to handle future dates for PostgreSQL and MySQL, also changes were made to DeployToken to enforce Forever.date Also removes extra conditional from JwtController --- .../projects/settings/repository_settings_spec.rb | 14 +------- spec/lib/forever_spec.rb | 21 ++++++++++++ spec/models/deploy_token_spec.rb | 38 ++++++++++++++++++++++ spec/services/deploy_tokens/create_service_spec.rb | 4 +-- 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 spec/lib/forever_spec.rb (limited to 'spec') diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 2528c7f437d..f2c371b7df5 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -90,8 +90,7 @@ feature 'Repository settings' do end context 'Deploy tokens' do - let(:deploy_token_project) { create(:project_deploy_token, project: project) } - let!(:deploy_token) { deploy_token_project.deploy_token } + let!(:deploy_token) { create(:deploy_token, projects: [project]) } before do stub_container_registry_config(enabled: true) @@ -115,17 +114,6 @@ feature 'Repository settings' do expect(page).to have_content('Your new project deploy token has been created') end - - scenario 'revoke a deploy token', :js do - within('.deploy-tokens') do - click_link 'Revoke' - click_link "Revoke #{deploy_token.name}" - - expect(page).not_to have_content(deploy_token.name) - expect(page).not_to have_content('read_repository') - expect(page).not_to have_content('read_registry') - end - end end end end diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb new file mode 100644 index 00000000000..cf40c467c72 --- /dev/null +++ b/spec/lib/forever_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Forever do + describe '.date' do + subject { described_class.date } + + context 'when using PostgreSQL' do + it 'should return Postgresql future date' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(subject).to eq(described_class::POSTGRESQL_DATE) + end + end + + context 'when using MySQL' do + it 'should return MySQL future date' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(subject).to eq(described_class::MYSQL_DATE) + end + end + end +end diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 1adc049ca58..5a15c23def4 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -93,4 +93,42 @@ describe DeployToken do end end end + + describe '#expires_at' do + context 'when using Forever.date' do + let(:deploy_token) { create(:deploy_token, expires_at: nil) } + + it 'should return nil' do + expect(deploy_token.expires_at).to be_nil + end + end + + context 'when using a personalized date' do + let(:expires_at) { Date.today + 5.months } + let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } + + it 'should return the personalized date' do + expect(deploy_token.expires_at).to eq(expires_at) + end + end + end + + describe '#expires_at=' do + context 'when passing nil' do + let(:deploy_token) { create(:deploy_token, expires_at: nil) } + + it 'should assign Forever.date' do + expect(deploy_token.read_attribute(:expires_at)).to eq(Forever.date) + end + end + + context 'when passign a value' do + let(:expires_at) { Date.today + 5.months } + let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } + + it 'should respect the value' do + expect(deploy_token.read_attribute(:expires_at)).to eq(expires_at) + end + end + end end diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb index 2e02b7a28b5..3a2bbf1ecd1 100644 --- a/spec/services/deploy_tokens/create_service_spec.rb +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -25,8 +25,8 @@ describe DeployTokens::CreateService do context 'when expires at date is not passed' do let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } - it 'should set FOREVER date' do - expect(subject.expires_at).to eq(DeployToken::FOREVER) + it 'should set Forever.date' do + expect(subject.read_attribute(:expires_at)).to eq(Forever.date) end end -- cgit v1.2.1 From b38439a3ae3c7ea1675b7037e4882213bdc58fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 7 Apr 2018 10:35:00 +0200 Subject: Use proper auth_scope for deploy token --- spec/lib/gitlab/auth_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index e3ec707076a..9ccd0b206cc 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -195,7 +195,7 @@ describe Gitlab::Auth do personal_access_token = create(:personal_access_token, scopes: ['read_registry']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:build_read_container_image])) + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image])) end end @@ -317,7 +317,7 @@ describe Gitlab::Auth do end it 'succeeds when login and token are valid' do - auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:build_read_container_image]) + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image]) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login) expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip')) -- cgit v1.2.1 From 47b1528abe69f9b584536a3557f23d912b0ecfd8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 7 Apr 2018 22:37:32 -0700 Subject: Fix undefined method `one?` when pushing to an existing merge request An untested code path was triggering an Exception because Fixnum doesn't have `one?` implemented in Rails, while arrays and collections do. Closes #45152 --- spec/mailers/notify_spec.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 971a88e9ee9..43e419cd7de 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -390,11 +390,11 @@ describe Notify do end end - describe 'that have new commits' do + shared_examples 'a push to an existing merge request' do let(:push_user) { create(:user) } subject do - described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: merge_request.commits) + described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: merge_request.commits, existing_commits: existing_commits) end it_behaves_like 'a multiple recipients email' @@ -419,6 +419,18 @@ describe Notify do end end end + + describe 'that have new commits' do + let(:existing_commits) { [] } + + it_behaves_like 'a push to an existing merge request' + end + + describe 'that have new commits on top of an existing one' do + let(:existing_commits) { [merge_request.commits.first] } + + it_behaves_like 'a push to an existing merge request' + end end context 'for issue notes' do -- cgit v1.2.1 From d2482f15e19695e2f89284d9a0219c6b4b4f401c Mon Sep 17 00:00:00 2001 From: Fabian Schneider Date: Sat, 31 Mar 2018 13:10:16 +0200 Subject: Fix template selector menu visibility --- .../projects/files/template_selector_menu_spec.rb | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 spec/features/projects/files/template_selector_menu_spec.rb (limited to 'spec') diff --git a/spec/features/projects/files/template_selector_menu_spec.rb b/spec/features/projects/files/template_selector_menu_spec.rb new file mode 100644 index 00000000000..b549a69ddf3 --- /dev/null +++ b/spec/features/projects/files/template_selector_menu_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +feature 'Template selector menu', :js do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in user + end + + context 'editing a non-matching file' do + before do + create_and_edit_file('README.md') + end + + scenario 'is not displayed' do + check_template_selector_menu_display(false) + end + + context 'user toggles preview' do + before do + click_link 'Preview' + end + + scenario 'template selector menu is not displayed' do + check_template_selector_menu_display(false) + click_link 'Write' + check_template_selector_menu_display(false) + end + end + end + + context 'editing a matching file' do + before do + visit project_edit_blob_path(project, File.join(project.default_branch, 'LICENSE')) + end + + scenario 'is displayed' do + check_template_selector_menu_display(true) + end + + context 'user toggles preview' do + before do + click_link 'Preview' + end + + scenario 'template selector menu is hidden and shown correctly' do + check_template_selector_menu_display(false) + click_link 'Write' + check_template_selector_menu_display(true) + end + end + end +end + +def check_template_selector_menu_display(is_visible) + count = is_visible ? 1 : 0 + expect(page).to have_css('.template-selectors-menu', count: count) +end + +def create_and_edit_file(file_name) + visit project_new_blob_path(project, 'master', file_name: file_name) + click_button "Commit changes" + visit project_edit_blob_path(project, File.join(project.default_branch, file_name)) +end -- cgit v1.2.1 From 31dd86b636a42e251e346dd5207281fda99c413f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20L=C3=B3pez?= Date: Sun, 8 Apr 2018 10:20:05 +0000 Subject: Projects and groups badges settings UI --- spec/features/groups/settings/group_badges_spec.rb | 124 +++++ .../projects/settings/project_badges_spec.rb | 125 +++++ .../badges/components/badge_form_spec.js | 171 ++++++ .../badges/components/badge_list_row_spec.js | 97 ++++ .../badges/components/badge_list_spec.js | 88 +++ .../badges/components/badge_settings_spec.js | 109 ++++ spec/javascripts/badges/components/badge_spec.js | 147 +++++ spec/javascripts/badges/dummy_badge.js | 23 + spec/javascripts/badges/store/actions_spec.js | 607 +++++++++++++++++++++ spec/javascripts/badges/store/mutations_spec.js | 418 ++++++++++++++ spec/javascripts/fixtures/one_white_pixel.png | Bin 0 -> 68 bytes .../helpers/vue_mount_component_helper.js | 6 + spec/javascripts/matchers.js | 35 ++ spec/javascripts/test_bundle.js | 11 +- spec/javascripts/test_constants.js | 4 + 15 files changed, 1962 insertions(+), 3 deletions(-) create mode 100644 spec/features/groups/settings/group_badges_spec.rb create mode 100644 spec/features/projects/settings/project_badges_spec.rb create mode 100644 spec/javascripts/badges/components/badge_form_spec.js create mode 100644 spec/javascripts/badges/components/badge_list_row_spec.js create mode 100644 spec/javascripts/badges/components/badge_list_spec.js create mode 100644 spec/javascripts/badges/components/badge_settings_spec.js create mode 100644 spec/javascripts/badges/components/badge_spec.js create mode 100644 spec/javascripts/badges/dummy_badge.js create mode 100644 spec/javascripts/badges/store/actions_spec.js create mode 100644 spec/javascripts/badges/store/mutations_spec.js create mode 100644 spec/javascripts/fixtures/one_white_pixel.png create mode 100644 spec/javascripts/matchers.js create mode 100644 spec/javascripts/test_constants.js (limited to 'spec') diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb new file mode 100644 index 00000000000..92217294446 --- /dev/null +++ b/spec/features/groups/settings/group_badges_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' + +feature 'Group Badges' do + include WaitForRequests + + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:badge_link_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'} + let(:badge_image_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'} + let!(:badge_1) { create(:group_badge, group: group) } + let!(:badge_2) { create(:group_badge, group: group) } + + before do + group.add_owner(user) + sign_in(user) + + visit(group_settings_badges_path(group)) + end + + it 'shows a list of badges', :js do + page.within '.badge-settings' do + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[0]).to have_content badge_1.link_url + expect(rows[1]).to have_content badge_2.link_url + end + end + + context 'adding a badge', :js do + it 'user can preview a badge' do + page.within '.badge-settings form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + within '#badge-preview' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + + it do + page.within '.badge-settings' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Add badge' + wait_for_requests + + within '.panel-body' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + end + + context 'editing a badge', :js do + it 'form is shown when clicking edit button in list' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + + within 'form' do + expect(find('#badge-link-url').value).to eq badge_2.link_url + expect(find('#badge-image-url').value).to eq badge_2.image_url + end + end + end + + it 'updates a badge when submitting the edit form' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + within 'form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Save changes' + wait_for_requests + end + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[1]).to have_content badge_link_url + end + end + end + + context 'deleting a badge', :js do + def click_delete_button(badge_row) + badge_row.find('[aria-label="Delete"]').click + end + + it 'shows a modal when deleting a badge' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + + click_delete_button(rows[1]) + + expect(find('.modal .modal-title')).to have_content 'Delete badge?' + end + + it 'deletes a badge when confirming the modal' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + click_delete_button(rows[1]) + + find('.modal .btn-danger').click + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 1 + expect(rows[0]).to have_content badge_1.link_url + end + end +end diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb new file mode 100644 index 00000000000..cc3551a4c21 --- /dev/null +++ b/spec/features/projects/settings/project_badges_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +feature 'Project Badges' do + include WaitForRequests + + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:badge_link_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'} + let(:badge_image_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'} + let!(:project_badge) { create(:project_badge, project: project) } + let!(:group_badge) { create(:group_badge, group: group) } + + before do + group.add_master(user) + sign_in(user) + + visit(project_settings_badges_path(project)) + end + + it 'shows a list of badges', :js do + page.within '.badge-settings' do + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[0]).to have_content group_badge.link_url + expect(rows[1]).to have_content project_badge.link_url + end + end + + context 'adding a badge', :js do + it 'user can preview a badge' do + page.within '.badge-settings form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + within '#badge-preview' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + + it do + page.within '.badge-settings' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Add badge' + wait_for_requests + + within '.panel-body' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + end + + context 'editing a badge', :js do + it 'form is shown when clicking edit button in list' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + + within 'form' do + expect(find('#badge-link-url').value).to eq project_badge.link_url + expect(find('#badge-image-url').value).to eq project_badge.image_url + end + end + end + + it 'updates a badge when submitting the edit form' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + within 'form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Save changes' + wait_for_requests + end + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[1]).to have_content badge_link_url + end + end + end + + context 'deleting a badge', :js do + def click_delete_button(badge_row) + badge_row.find('[aria-label="Delete"]').click + end + + it 'shows a modal when deleting a badge' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + + click_delete_button(rows[1]) + + expect(find('.modal .modal-title')).to have_content 'Delete badge?' + end + + it 'deletes a badge when confirming the modal' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + click_delete_button(rows[1]) + + find('.modal .btn-danger').click + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 1 + expect(rows[0]).to have_content group_badge.link_url + end + end +end diff --git a/spec/javascripts/badges/components/badge_form_spec.js b/spec/javascripts/badges/components/badge_form_spec.js new file mode 100644 index 00000000000..dd21ec279cb --- /dev/null +++ b/spec/javascripts/badges/components/badge_form_spec.js @@ -0,0 +1,171 @@ +import Vue from 'vue'; +import store from '~/badges/store'; +import BadgeForm from '~/badges/components/badge_form.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeForm component', () => { + const Component = Vue.extend(BadgeForm); + let vm; + + beforeEach(() => { + setFixtures(` +
+ `); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { + isEditing: false, + }, + }); + }); + + describe('onCancel', () => { + it('calls stopEditing', () => { + spyOn(vm, 'stopEditing'); + + vm.onCancel(); + + expect(vm.stopEditing).toHaveBeenCalled(); + }); + }); + + describe('onSubmit', () => { + describe('if isEditing is true', () => { + beforeEach(() => { + spyOn(vm, 'saveBadge').and.returnValue(Promise.resolve()); + store.replaceState({ + ...store.state, + isSaving: false, + badgeInEditForm: createDummyBadge(), + }); + vm.isEditing = true; + }); + + it('returns immediately if imageUrl is empty', () => { + store.state.badgeInEditForm.imageUrl = ''; + + vm.onSubmit(); + + expect(vm.saveBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if linkUrl is empty', () => { + store.state.badgeInEditForm.linkUrl = ''; + + vm.onSubmit(); + + expect(vm.saveBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if isSaving is true', () => { + store.state.isSaving = true; + + vm.onSubmit(); + + expect(vm.saveBadge).not.toHaveBeenCalled(); + }); + + it('calls saveBadge', () => { + vm.onSubmit(); + + expect(vm.saveBadge).toHaveBeenCalled(); + }); + }); + + describe('if isEditing is false', () => { + beforeEach(() => { + spyOn(vm, 'addBadge').and.returnValue(Promise.resolve()); + store.replaceState({ + ...store.state, + isSaving: false, + badgeInAddForm: createDummyBadge(), + }); + vm.isEditing = false; + }); + + it('returns immediately if imageUrl is empty', () => { + store.state.badgeInAddForm.imageUrl = ''; + + vm.onSubmit(); + + expect(vm.addBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if linkUrl is empty', () => { + store.state.badgeInAddForm.linkUrl = ''; + + vm.onSubmit(); + + expect(vm.addBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if isSaving is true', () => { + store.state.isSaving = true; + + vm.onSubmit(); + + expect(vm.addBadge).not.toHaveBeenCalled(); + }); + + it('calls addBadge', () => { + vm.onSubmit(); + + expect(vm.addBadge).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('if isEditing is false', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { + isEditing: false, + }, + }); + }); + + it('renders one button', () => { + const buttons = vm.$el.querySelectorAll('.row-content-block button'); + expect(buttons.length).toBe(1); + const buttonAddElement = buttons[0]; + expect(buttonAddElement).toBeVisible(); + expect(buttonAddElement).toHaveText('Add badge'); + }); + }); + + describe('if isEditing is true', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { + isEditing: true, + }, + }); + }); + + it('renders two buttons', () => { + const buttons = vm.$el.querySelectorAll('.row-content-block button'); + expect(buttons.length).toBe(2); + const buttonSaveElement = buttons[0]; + expect(buttonSaveElement).toBeVisible(); + expect(buttonSaveElement).toHaveText('Save changes'); + const buttonCancelElement = buttons[1]; + expect(buttonCancelElement).toBeVisible(); + expect(buttonCancelElement).toHaveText('Cancel'); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_list_row_spec.js b/spec/javascripts/badges/components/badge_list_row_spec.js new file mode 100644 index 00000000000..21bd00d82f0 --- /dev/null +++ b/spec/javascripts/badges/components/badge_list_row_spec.js @@ -0,0 +1,97 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants'; +import store from '~/badges/store'; +import BadgeListRow from '~/badges/components/badge_list_row.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeListRow component', () => { + const Component = Vue.extend(BadgeListRow); + let badge; + let vm; + + beforeEach(() => { + setFixtures(` + +
+ `); + store.replaceState({ + ...store.state, + kind: PROJECT_BADGE, + }); + badge = createDummyBadge(); + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { badge }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the badge', () => { + const badgeElement = vm.$el.querySelector('.project-badge'); + expect(badgeElement).not.toBeNull(); + expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl); + }); + + it('renders the badge link', () => { + expect(vm.$el).toContainText(badge.linkUrl); + }); + + it('renders the badge kind', () => { + expect(vm.$el).toContainText('Project Badge'); + }); + + it('shows edit and delete buttons', () => { + const buttons = vm.$el.querySelectorAll('.table-button-footer button'); + expect(buttons).toHaveLength(2); + const buttonEditElement = buttons[0]; + expect(buttonEditElement).toBeVisible(); + expect(buttonEditElement).toHaveSpriteIcon('pencil'); + const buttonDeleteElement = buttons[1]; + expect(buttonDeleteElement).toBeVisible(); + expect(buttonDeleteElement).toHaveSpriteIcon('remove'); + }); + + it('calls editBadge when clicking then edit button', () => { + spyOn(vm, 'editBadge'); + + const editButton = vm.$el.querySelector('.table-button-footer button:first-of-type'); + editButton.click(); + + expect(vm.editBadge).toHaveBeenCalled(); + }); + + it('calls updateBadgeInModal and shows modal when clicking then delete button', done => { + spyOn(vm, 'updateBadgeInModal'); + $('#delete-badge-modal').on('shown.bs.modal', () => done()); + + const deleteButton = vm.$el.querySelector('.table-button-footer button:last-of-type'); + deleteButton.click(); + + expect(vm.updateBadgeInModal).toHaveBeenCalled(); + }); + + describe('for a group badge', () => { + beforeEach(done => { + badge.kind = GROUP_BADGE; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('renders the badge kind', () => { + expect(vm.$el).toContainText('Group Badge'); + }); + + it('hides edit and delete buttons', () => { + const buttons = vm.$el.querySelectorAll('.table-button-footer button'); + expect(buttons).toHaveLength(0); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js new file mode 100644 index 00000000000..9439c578973 --- /dev/null +++ b/spec/javascripts/badges/components/badge_list_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants'; +import store from '~/badges/store'; +import BadgeList from '~/badges/components/badge_list.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeList component', () => { + const Component = Vue.extend(BadgeList); + const numberOfDummyBadges = 3; + let vm; + + beforeEach(() => { + setFixtures('
'); + const badges = []; + for (let id = 0; id < numberOfDummyBadges; id += 1) { + badges.push({ id, ...createDummyBadge() }); + } + store.replaceState({ + ...store.state, + badges, + kind: PROJECT_BADGE, + isLoading: false, + }); + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a header with the badge count', () => { + const header = vm.$el.querySelector('.panel-heading'); + expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`)); + }); + + it('renders a row for each badge', () => { + const rows = vm.$el.querySelectorAll('.gl-responsive-table-row'); + expect(rows).toHaveLength(numberOfDummyBadges); + }); + + it('renders a message if no badges exist', done => { + store.state.badges = []; + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('This project has no badges'); + }) + .then(done) + .catch(done.fail); + }); + + it('shows a loading icon when loading', done => { + store.state.isLoading = true; + + Vue.nextTick() + .then(() => { + const loadingIcon = vm.$el.querySelector('.fa-spinner'); + expect(loadingIcon).toBeVisible(); + }) + .then(done) + .catch(done.fail); + }); + + describe('for group badges', () => { + beforeEach(done => { + store.state.kind = GROUP_BADGE; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('renders a message if no badges exist', done => { + store.state.badges = []; + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('This group has no badges'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_settings_spec.js b/spec/javascripts/badges/components/badge_settings_spec.js new file mode 100644 index 00000000000..3db02982ad4 --- /dev/null +++ b/spec/javascripts/badges/components/badge_settings_spec.js @@ -0,0 +1,109 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import store from '~/badges/store'; +import BadgeSettings from '~/badges/components/badge_settings.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeSettings component', () => { + const Component = Vue.extend(BadgeSettings); + let vm; + + beforeEach(() => { + setFixtures(` +
+ + `); + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('displays modal if button is clicked', done => { + const badge = createDummyBadge(); + store.state.badgeInModal = badge; + const modal = vm.$el.querySelector('#delete-badge-modal'); + const button = document.getElementById('dummy-modal-button'); + + $(modal).on('shown.bs.modal', () => { + expect(modal).toContainText('Delete badge?'); + const badgeElement = modal.querySelector('img.project-badge'); + expect(badgeElement).not.toBe(null); + expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl); + + done(); + }); + + Vue.nextTick() + .then(() => { + button.click(); + }) + .catch(done.fail); + }); + + it('displays a form to add a badge', () => { + const form = vm.$el.querySelector('form:nth-of-type(2)'); + expect(form).not.toBe(null); + const button = form.querySelector('.btn-success'); + expect(button).not.toBe(null); + expect(button).toHaveText(/Add badge/); + }); + + it('displays badge list', () => { + const badgeListElement = vm.$el.querySelector('.panel'); + expect(badgeListElement).not.toBe(null); + expect(badgeListElement).toBeVisible(); + expect(badgeListElement).toContainText('Your badges'); + }); + + describe('when editing', () => { + beforeEach(done => { + store.state.isEditing = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays a form to edit a badge', () => { + const form = vm.$el.querySelector('form:nth-of-type(1)'); + expect(form).not.toBe(null); + const submitButton = form.querySelector('.btn-success'); + expect(submitButton).not.toBe(null); + expect(submitButton).toHaveText(/Save changes/); + const cancelButton = form.querySelector('.btn-cancel'); + expect(cancelButton).not.toBe(null); + expect(cancelButton).toHaveText(/Cancel/); + }); + + it('displays no badge list', () => { + const badgeListElement = vm.$el.querySelector('.panel'); + expect(badgeListElement).toBeHidden(); + }); + }); + + describe('methods', () => { + describe('onSubmitModal', () => { + it('triggers ', () => { + spyOn(vm, 'deleteBadge').and.callFake(() => Promise.resolve()); + const modal = vm.$el.querySelector('#delete-badge-modal'); + const deleteButton = modal.querySelector('.btn-danger'); + + deleteButton.click(); + + const badge = store.state.badgeInModal; + expect(vm.deleteBadge).toHaveBeenCalledWith(badge); + }); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/javascripts/badges/components/badge_spec.js new file mode 100644 index 00000000000..fd1ecc9cdd8 --- /dev/null +++ b/spec/javascripts/badges/components/badge_spec.js @@ -0,0 +1,147 @@ +import Vue from 'vue'; +import Badge from '~/badges/components/badge.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants'; + +describe('Badge component', () => { + const Component = Vue.extend(Badge); + const dummyProps = { + imageUrl: DUMMY_IMAGE_URL, + linkUrl: `${TEST_HOST}/badge/link/url`, + }; + let vm; + + const findElements = () => { + const buttons = vm.$el.querySelectorAll('button'); + return { + badgeImage: vm.$el.querySelector('img.project-badge'), + loadingIcon: vm.$el.querySelector('.fa-spinner'), + reloadButton: buttons[buttons.length - 1], + }; + }; + + const createComponent = (props, el = null) => { + vm = mountComponent(Component, props, el); + const { badgeImage } = findElements(); + return new Promise(resolve => badgeImage.addEventListener('load', resolve)).then(() => + Vue.nextTick(), + ); + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('watchers', () => { + describe('imageUrl', () => { + it('sets isLoading and resets numRetries and hasError', done => { + const props = { ...dummyProps }; + createComponent(props) + .then(() => { + expect(vm.isLoading).toBe(false); + vm.hasError = true; + vm.numRetries = 42; + + vm.imageUrl = `${props.imageUrl}#something/else`; + + return Vue.nextTick(); + }) + .then(() => { + expect(vm.isLoading).toBe(true); + expect(vm.numRetries).toBe(0); + expect(vm.hasError).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('methods', () => { + beforeEach(done => { + createComponent({ ...dummyProps }) + .then(done) + .catch(done.fail); + }); + + it('onError resets isLoading and sets hasError', () => { + vm.hasError = false; + vm.isLoading = true; + + vm.onError(); + + expect(vm.hasError).toBe(true); + expect(vm.isLoading).toBe(false); + }); + + it('onLoad sets isLoading', () => { + vm.isLoading = true; + + vm.onLoad(); + + expect(vm.isLoading).toBe(false); + }); + + it('reloadImage resets isLoading and hasError and increases numRetries', () => { + vm.hasError = true; + vm.isLoading = false; + vm.numRetries = 0; + + vm.reloadImage(); + + expect(vm.hasError).toBe(false); + expect(vm.isLoading).toBe(true); + expect(vm.numRetries).toBe(1); + }); + }); + + describe('behavior', () => { + beforeEach(done => { + setFixtures('
'); + createComponent({ ...dummyProps }, '#dummy-element') + .then(done) + .catch(done.fail); + }); + + it('shows a badge image after loading', () => { + expect(vm.isLoading).toBe(false); + expect(vm.hasError).toBe(false); + const { badgeImage, loadingIcon, reloadButton } = findElements(); + expect(badgeImage).toBeVisible(); + expect(loadingIcon).toBeHidden(); + expect(reloadButton).toBeHidden(); + expect(vm.$el.innerText).toBe(''); + }); + + it('shows a loading icon when loading', done => { + vm.isLoading = true; + + Vue.nextTick() + .then(() => { + const { badgeImage, loadingIcon, reloadButton } = findElements(); + expect(badgeImage).toBeHidden(); + expect(loadingIcon).toBeVisible(); + expect(reloadButton).toBeHidden(); + expect(vm.$el.innerText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + + it('shows an error and reload button if loading failed', done => { + vm.hasError = true; + + Vue.nextTick() + .then(() => { + const { badgeImage, loadingIcon, reloadButton } = findElements(); + expect(badgeImage).toBeHidden(); + expect(loadingIcon).toBeHidden(); + expect(reloadButton).toBeVisible(); + expect(reloadButton).toHaveSpriteIcon('retry'); + expect(vm.$el.innerText.trim()).toBe('No badge image'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/badges/dummy_badge.js b/spec/javascripts/badges/dummy_badge.js new file mode 100644 index 00000000000..6aaff21c503 --- /dev/null +++ b/spec/javascripts/badges/dummy_badge.js @@ -0,0 +1,23 @@ +import { PROJECT_BADGE } from '~/badges/constants'; +import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants'; + +export const createDummyBadge = () => { + const id = Math.floor(1000 * Math.random()); + return { + id, + imageUrl: `${TEST_HOST}/badges/${id}/image/url`, + isDeleting: false, + linkUrl: `${TEST_HOST}/badges/${id}/link/url`, + kind: PROJECT_BADGE, + renderedImageUrl: `${DUMMY_IMAGE_URL}?id=${id}`, + renderedLinkUrl: `${TEST_HOST}/badges/${id}/rendered/link/url`, + }; +}; + +export const createDummyBadgeResponse = () => ({ + image_url: `${TEST_HOST}/badge/image/url`, + link_url: `${TEST_HOST}/badge/link/url`, + kind: PROJECT_BADGE, + rendered_image_url: DUMMY_IMAGE_URL, + rendered_link_url: `${TEST_HOST}/rendered/badge/link/url`, +}); diff --git a/spec/javascripts/badges/store/actions_spec.js b/spec/javascripts/badges/store/actions_spec.js new file mode 100644 index 00000000000..bb6263c6de4 --- /dev/null +++ b/spec/javascripts/badges/store/actions_spec.js @@ -0,0 +1,607 @@ +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; +import actions, { transformBackendBadge } from '~/badges/store/actions'; +import mutationTypes from '~/badges/store/mutation_types'; +import createState from '~/badges/store/state'; +import { TEST_HOST } from 'spec/test_constants'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge'; + +describe('Badges store actions', () => { + const dummyEndpointUrl = `${TEST_HOST}/badges/endpoint`; + const dummyBadges = [{ ...createDummyBadge(), id: 5 }, { ...createDummyBadge(), id: 6 }]; + + let axiosMock; + let badgeId; + let state; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + state = { + ...createState(), + apiEndpointUrl: dummyEndpointUrl, + badges: dummyBadges, + }; + badgeId = state.badges[0].id; + }); + + afterEach(() => { + axiosMock.restore(); + }); + + describe('requestNewBadge', () => { + it('commits REQUEST_NEW_BADGE', done => { + testAction( + actions.requestNewBadge, + null, + state, + [{ type: mutationTypes.REQUEST_NEW_BADGE }], + [], + done, + ); + }); + }); + + describe('receiveNewBadge', () => { + it('commits RECEIVE_NEW_BADGE', done => { + const newBadge = createDummyBadge(); + testAction( + actions.receiveNewBadge, + newBadge, + state, + [{ type: mutationTypes.RECEIVE_NEW_BADGE, payload: newBadge }], + [], + done, + ); + }); + }); + + describe('receiveNewBadgeError', () => { + it('commits RECEIVE_NEW_BADGE_ERROR', done => { + testAction( + actions.receiveNewBadgeError, + null, + state, + [{ type: mutationTypes.RECEIVE_NEW_BADGE_ERROR }], + [], + done, + ); + }); + }); + + describe('addBadge', () => { + let badgeInAddForm; + let dispatch; + let endpointMock; + + beforeEach(() => { + endpointMock = axiosMock.onPost(dummyEndpointUrl); + dispatch = jasmine.createSpy('dispatch'); + badgeInAddForm = createDummyBadge(); + state = { + ...state, + badgeInAddForm, + }; + }); + + it('dispatches requestNewBadge and receiveNewBadge for successful response', done => { + const dummyResponse = createDummyBadgeResponse(); + + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInAddForm.imageUrl, + link_url: badgeInAddForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]); + dispatch.calls.reset(); + return [200, dummyResponse]; + }); + + const dummyBadge = transformBackendBadge(dummyResponse); + actions + .addBadge({ state, dispatch }) + .then(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadge', dummyBadge]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestNewBadge and receiveNewBadgeError for error response', done => { + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInAddForm.imageUrl, + link_url: badgeInAddForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .addBadge({ state, dispatch }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadgeError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('requestDeleteBadge', () => { + it('commits REQUEST_DELETE_BADGE', done => { + testAction( + actions.requestDeleteBadge, + badgeId, + state, + [{ type: mutationTypes.REQUEST_DELETE_BADGE, payload: badgeId }], + [], + done, + ); + }); + }); + + describe('receiveDeleteBadge', () => { + it('commits RECEIVE_DELETE_BADGE', done => { + testAction( + actions.receiveDeleteBadge, + badgeId, + state, + [{ type: mutationTypes.RECEIVE_DELETE_BADGE, payload: badgeId }], + [], + done, + ); + }); + }); + + describe('receiveDeleteBadgeError', () => { + it('commits RECEIVE_DELETE_BADGE_ERROR', done => { + testAction( + actions.receiveDeleteBadgeError, + badgeId, + state, + [{ type: mutationTypes.RECEIVE_DELETE_BADGE_ERROR, payload: badgeId }], + [], + done, + ); + }); + }); + + describe('deleteBadge', () => { + let dispatch; + let endpointMock; + + beforeEach(() => { + endpointMock = axiosMock.onDelete(`${dummyEndpointUrl}/${badgeId}`); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('dispatches requestDeleteBadge and receiveDeleteBadge for successful response', done => { + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]); + dispatch.calls.reset(); + return [200, '']; + }); + + actions + .deleteBadge({ state, dispatch }, { id: badgeId }) + .then(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadge', badgeId]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestDeleteBadge and receiveDeleteBadgeError for error response', done => { + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .deleteBadge({ state, dispatch }, { id: badgeId }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadgeError', badgeId]]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('editBadge', () => { + it('commits START_EDITING', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.editBadge, + dummyBadge, + state, + [{ type: mutationTypes.START_EDITING, payload: dummyBadge }], + [], + done, + ); + }); + }); + + describe('requestLoadBadges', () => { + it('commits REQUEST_LOAD_BADGES', done => { + const dummyData = 'this is not real data'; + testAction( + actions.requestLoadBadges, + dummyData, + state, + [{ type: mutationTypes.REQUEST_LOAD_BADGES, payload: dummyData }], + [], + done, + ); + }); + }); + + describe('receiveLoadBadges', () => { + it('commits RECEIVE_LOAD_BADGES', done => { + const badges = dummyBadges; + testAction( + actions.receiveLoadBadges, + badges, + state, + [{ type: mutationTypes.RECEIVE_LOAD_BADGES, payload: badges }], + [], + done, + ); + }); + }); + + describe('receiveLoadBadgesError', () => { + it('commits RECEIVE_LOAD_BADGES_ERROR', done => { + testAction( + actions.receiveLoadBadgesError, + null, + state, + [{ type: mutationTypes.RECEIVE_LOAD_BADGES_ERROR }], + [], + done, + ); + }); + }); + + describe('loadBadges', () => { + let dispatch; + let endpointMock; + + beforeEach(() => { + endpointMock = axiosMock.onGet(dummyEndpointUrl); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('dispatches requestLoadBadges and receiveLoadBadges for successful response', done => { + const dummyData = 'this is just some data'; + const dummyReponse = [ + createDummyBadgeResponse(), + createDummyBadgeResponse(), + createDummyBadgeResponse(), + ]; + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]); + dispatch.calls.reset(); + return [200, dummyReponse]; + }); + + actions + .loadBadges({ state, dispatch }, dummyData) + .then(() => { + const badges = dummyReponse.map(transformBackendBadge); + expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadges', badges]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestLoadBadges and receiveLoadBadgesError for error response', done => { + const dummyData = 'this is just some data'; + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .loadBadges({ state, dispatch }, dummyData) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadgesError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('requestRenderedBadge', () => { + it('commits REQUEST_RENDERED_BADGE', done => { + testAction( + actions.requestRenderedBadge, + null, + state, + [{ type: mutationTypes.REQUEST_RENDERED_BADGE }], + [], + done, + ); + }); + }); + + describe('receiveRenderedBadge', () => { + it('commits RECEIVE_RENDERED_BADGE', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.receiveRenderedBadge, + dummyBadge, + state, + [{ type: mutationTypes.RECEIVE_RENDERED_BADGE, payload: dummyBadge }], + [], + done, + ); + }); + }); + + describe('receiveRenderedBadgeError', () => { + it('commits RECEIVE_RENDERED_BADGE_ERROR', done => { + testAction( + actions.receiveRenderedBadgeError, + null, + state, + [{ type: mutationTypes.RECEIVE_RENDERED_BADGE_ERROR }], + [], + done, + ); + }); + }); + + describe('renderBadge', () => { + let dispatch; + let endpointMock; + let badgeInForm; + + beforeEach(() => { + badgeInForm = createDummyBadge(); + state = { + ...state, + badgeInAddForm: badgeInForm, + }; + const urlParameters = [ + `link_url=${encodeURIComponent(badgeInForm.linkUrl)}`, + `image_url=${encodeURIComponent(badgeInForm.imageUrl)}`, + ].join('&'); + endpointMock = axiosMock.onGet(`${dummyEndpointUrl}/render?${urlParameters}`); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('returns immediately if imageUrl is empty', done => { + spyOn(axios, 'get'); + badgeInForm.imageUrl = ''; + + actions + .renderBadge({ state, dispatch }) + .then(() => { + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('returns immediately if linkUrl is empty', done => { + spyOn(axios, 'get'); + badgeInForm.linkUrl = ''; + + actions + .renderBadge({ state, dispatch }) + .then(() => { + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('escapes user input', done => { + spyOn(axios, 'get').and.callFake(() => Promise.resolve({ data: createDummyBadgeResponse() })); + badgeInForm.imageUrl = '&make-sandwhich=true'; + badgeInForm.linkUrl = ''; + + actions + .renderBadge({ state, dispatch }) + .then(() => { + expect(axios.get.calls.count()).toBe(1); + const url = axios.get.calls.argsFor(0)[0]; + expect(url).toMatch(`^${dummyEndpointUrl}/render?`); + expect(url).toMatch('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&'); + expect(url).toMatch('&image_url=%26make-sandwhich%3Dtrue$'); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', done => { + const dummyReponse = createDummyBadgeResponse(); + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]); + dispatch.calls.reset(); + return [200, dummyReponse]; + }); + + actions + .renderBadge({ state, dispatch }) + .then(() => { + const renderedBadge = transformBackendBadge(dummyReponse); + expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadge', renderedBadge]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestRenderedBadge and receiveRenderedBadgeError for error response', done => { + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .renderBadge({ state, dispatch }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadgeError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('requestUpdatedBadge', () => { + it('commits REQUEST_UPDATED_BADGE', done => { + testAction( + actions.requestUpdatedBadge, + null, + state, + [{ type: mutationTypes.REQUEST_UPDATED_BADGE }], + [], + done, + ); + }); + }); + + describe('receiveUpdatedBadge', () => { + it('commits RECEIVE_UPDATED_BADGE', done => { + const updatedBadge = createDummyBadge(); + testAction( + actions.receiveUpdatedBadge, + updatedBadge, + state, + [{ type: mutationTypes.RECEIVE_UPDATED_BADGE, payload: updatedBadge }], + [], + done, + ); + }); + }); + + describe('receiveUpdatedBadgeError', () => { + it('commits RECEIVE_UPDATED_BADGE_ERROR', done => { + testAction( + actions.receiveUpdatedBadgeError, + null, + state, + [{ type: mutationTypes.RECEIVE_UPDATED_BADGE_ERROR }], + [], + done, + ); + }); + }); + + describe('saveBadge', () => { + let badgeInEditForm; + let dispatch; + let endpointMock; + + beforeEach(() => { + badgeInEditForm = createDummyBadge(); + state = { + ...state, + badgeInEditForm, + }; + endpointMock = axiosMock.onPut(`${dummyEndpointUrl}/${badgeInEditForm.id}`); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('dispatches requestUpdatedBadge and receiveUpdatedBadge for successful response', done => { + const dummyResponse = createDummyBadgeResponse(); + + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInEditForm.imageUrl, + link_url: badgeInEditForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]); + dispatch.calls.reset(); + return [200, dummyResponse]; + }); + + const updatedBadge = transformBackendBadge(dummyResponse); + actions + .saveBadge({ state, dispatch }) + .then(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadge', updatedBadge]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestUpdatedBadge and receiveUpdatedBadgeError for error response', done => { + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInEditForm.imageUrl, + link_url: badgeInEditForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .saveBadge({ state, dispatch }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadgeError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('stopEditing', () => { + it('commits STOP_EDITING', done => { + testAction( + actions.stopEditing, + null, + state, + [{ type: mutationTypes.STOP_EDITING }], + [], + done, + ); + }); + }); + + describe('updateBadgeInForm', () => { + it('commits UPDATE_BADGE_IN_FORM', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.updateBadgeInForm, + dummyBadge, + state, + [{ type: mutationTypes.UPDATE_BADGE_IN_FORM, payload: dummyBadge }], + [], + done, + ); + }); + + describe('updateBadgeInModal', () => { + it('commits UPDATE_BADGE_IN_MODAL', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.updateBadgeInModal, + dummyBadge, + state, + [{ type: mutationTypes.UPDATE_BADGE_IN_MODAL, payload: dummyBadge }], + [], + done, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/badges/store/mutations_spec.js b/spec/javascripts/badges/store/mutations_spec.js new file mode 100644 index 00000000000..8d26f83339d --- /dev/null +++ b/spec/javascripts/badges/store/mutations_spec.js @@ -0,0 +1,418 @@ +import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants'; +import store from '~/badges/store'; +import types from '~/badges/store/mutation_types'; +import createState from '~/badges/store/state'; +import { createDummyBadge } from '../dummy_badge'; + +describe('Badges store mutations', () => { + let dummyBadge; + + beforeEach(() => { + dummyBadge = createDummyBadge(); + store.replaceState(createState()); + }); + + describe('RECEIVE_DELETE_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1 }, + dummyBadge, + { ...dummyBadge, id: dummyBadge.id + 1 }, + ]; + + store.replaceState({ + ...store.state, + badges, + }); + }); + + it('removes deleted badge', () => { + const badgeCount = store.state.badges.length; + + store.commit(types.RECEIVE_DELETE_BADGE, dummyBadge.id); + + expect(store.state.badges.length).toBe(badgeCount - 1); + expect(store.state.badges.indexOf(dummyBadge)).toBe(-1); + }); + }); + + describe('RECEIVE_DELETE_BADGE_ERROR', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1, isDeleting: false }, + { ...dummyBadge, isDeleting: true }, + { ...dummyBadge, id: dummyBadge.id + 1, isDeleting: true }, + ]; + + store.replaceState({ + ...store.state, + badges, + }); + }); + + it('sets isDeleting to false', () => { + const badgeCount = store.state.badges.length; + + store.commit(types.RECEIVE_DELETE_BADGE_ERROR, dummyBadge.id); + + expect(store.state.badges.length).toBe(badgeCount); + expect(store.state.badges[0].isDeleting).toBe(false); + expect(store.state.badges[1].isDeleting).toBe(false); + expect(store.state.badges[2].isDeleting).toBe(true); + }); + }); + + describe('RECEIVE_LOAD_BADGES', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isLoading: 'not false', + }); + }); + + it('sets badges and isLoading to false', () => { + const badges = [createDummyBadge()]; + store.commit(types.RECEIVE_LOAD_BADGES, badges); + + expect(store.state.isLoading).toBe(false); + expect(store.state.badges).toBe(badges); + }); + }); + + describe('RECEIVE_LOAD_BADGES_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isLoading: 'not false', + }); + }); + + it('sets isLoading to false', () => { + store.commit(types.RECEIVE_LOAD_BADGES_ERROR); + + expect(store.state.isLoading).toBe(false); + }); + }); + + describe('RECEIVE_NEW_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1, kind: GROUP_BADGE }, + { ...dummyBadge, id: dummyBadge.id + 1, kind: GROUP_BADGE }, + { ...dummyBadge, id: dummyBadge.id - 1, kind: PROJECT_BADGE }, + { ...dummyBadge, id: dummyBadge.id + 1, kind: PROJECT_BADGE }, + ]; + store.replaceState({ + ...store.state, + badgeInAddForm: createDummyBadge(), + badges, + isSaving: 'dummy value', + renderedBadge: createDummyBadge(), + }); + }); + + it('resets the add form', () => { + store.commit(types.RECEIVE_NEW_BADGE, dummyBadge); + + expect(store.state.badgeInAddForm).toBe(null); + expect(store.state.isSaving).toBe(false); + expect(store.state.renderedBadge).toBe(null); + }); + + it('inserts group badge at correct position', () => { + const badgeCount = store.state.badges.length; + dummyBadge = { ...dummyBadge, kind: GROUP_BADGE }; + + store.commit(types.RECEIVE_NEW_BADGE, dummyBadge); + + expect(store.state.badges.length).toBe(badgeCount + 1); + expect(store.state.badges.indexOf(dummyBadge)).toBe(1); + }); + + it('inserts project badge at correct position', () => { + const badgeCount = store.state.badges.length; + dummyBadge = { ...dummyBadge, kind: PROJECT_BADGE }; + + store.commit(types.RECEIVE_NEW_BADGE, dummyBadge); + + expect(store.state.badges.length).toBe(badgeCount + 1); + expect(store.state.badges.indexOf(dummyBadge)).toBe(3); + }); + }); + + describe('RECEIVE_NEW_BADGE_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to false', () => { + store.commit(types.RECEIVE_NEW_BADGE_ERROR); + + expect(store.state.isSaving).toBe(false); + }); + }); + + describe('RECEIVE_RENDERED_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isRendering: 'dummy value', + renderedBadge: 'dummy value', + }); + }); + + it('sets renderedBadge', () => { + store.commit(types.RECEIVE_RENDERED_BADGE, dummyBadge); + + expect(store.state.isRendering).toBe(false); + expect(store.state.renderedBadge).toBe(dummyBadge); + }); + }); + + describe('RECEIVE_RENDERED_BADGE_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isRendering: 'dummy value', + }); + }); + + it('sets isRendering to false', () => { + store.commit(types.RECEIVE_RENDERED_BADGE_ERROR); + + expect(store.state.isRendering).toBe(false); + }); + }); + + describe('RECEIVE_UPDATED_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1 }, + dummyBadge, + { ...dummyBadge, id: dummyBadge.id + 1 }, + ]; + store.replaceState({ + ...store.state, + badgeInEditForm: createDummyBadge(), + badges, + isEditing: 'dummy value', + isSaving: 'dummy value', + renderedBadge: createDummyBadge(), + }); + }); + + it('resets the edit form', () => { + store.commit(types.RECEIVE_UPDATED_BADGE, dummyBadge); + + expect(store.state.badgeInAddForm).toBe(null); + expect(store.state.isSaving).toBe(false); + expect(store.state.renderedBadge).toBe(null); + }); + + it('replaces the updated badge', () => { + const badgeCount = store.state.badges.length; + const badgeIndex = store.state.badges.indexOf(dummyBadge); + const newBadge = { id: dummyBadge.id, dummy: 'value' }; + + store.commit(types.RECEIVE_UPDATED_BADGE, newBadge); + + expect(store.state.badges.length).toBe(badgeCount); + expect(store.state.badges[badgeIndex]).toBe(newBadge); + }); + }); + + describe('RECEIVE_UPDATED_BADGE_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to false', () => { + store.commit(types.RECEIVE_NEW_BADGE_ERROR); + + expect(store.state.isSaving).toBe(false); + }); + }); + + describe('REQUEST_DELETE_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1, isDeleting: false }, + { ...dummyBadge, isDeleting: false }, + { ...dummyBadge, id: dummyBadge.id + 1, isDeleting: true }, + ]; + + store.replaceState({ + ...store.state, + badges, + }); + }); + + it('sets isDeleting to true', () => { + const badgeCount = store.state.badges.length; + + store.commit(types.REQUEST_DELETE_BADGE, dummyBadge.id); + + expect(store.state.badges.length).toBe(badgeCount); + expect(store.state.badges[0].isDeleting).toBe(false); + expect(store.state.badges[1].isDeleting).toBe(true); + expect(store.state.badges[2].isDeleting).toBe(true); + }); + }); + + describe('REQUEST_LOAD_BADGES', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + apiEndpointUrl: 'some endpoint', + docsUrl: 'some url', + isLoading: 'dummy value', + kind: 'some kind', + }); + }); + + it('sets isLoading to true and initializes the store', () => { + const dummyData = { + apiEndpointUrl: 'dummy endpoint', + docsUrl: 'dummy url', + kind: 'dummy kind', + }; + + store.commit(types.REQUEST_LOAD_BADGES, dummyData); + + expect(store.state.isLoading).toBe(true); + expect(store.state.apiEndpointUrl).toBe(dummyData.apiEndpointUrl); + expect(store.state.docsUrl).toBe(dummyData.docsUrl); + expect(store.state.kind).toBe(dummyData.kind); + }); + }); + + describe('REQUEST_NEW_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to true', () => { + store.commit(types.REQUEST_NEW_BADGE); + + expect(store.state.isSaving).toBe(true); + }); + }); + + describe('REQUEST_RENDERED_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isRendering: 'dummy value', + }); + }); + + it('sets isRendering to true', () => { + store.commit(types.REQUEST_RENDERED_BADGE); + + expect(store.state.isRendering).toBe(true); + }); + }); + + describe('REQUEST_UPDATED_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to true', () => { + store.commit(types.REQUEST_NEW_BADGE); + + expect(store.state.isSaving).toBe(true); + }); + }); + + describe('START_EDITING', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInEditForm: 'dummy value', + isEditing: 'dummy value', + renderedBadge: 'dummy value', + }); + }); + + it('initializes the edit form', () => { + store.commit(types.START_EDITING, dummyBadge); + + expect(store.state.isEditing).toBe(true); + expect(store.state.badgeInEditForm).toEqual(dummyBadge); + expect(store.state.renderedBadge).toEqual(dummyBadge); + }); + }); + + describe('STOP_EDITING', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInEditForm: 'dummy value', + isEditing: 'dummy value', + renderedBadge: 'dummy value', + }); + }); + + it('resets the edit form', () => { + store.commit(types.STOP_EDITING); + + expect(store.state.isEditing).toBe(false); + expect(store.state.badgeInEditForm).toBe(null); + expect(store.state.renderedBadge).toBe(null); + }); + }); + + describe('UPDATE_BADGE_IN_FORM', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInAddForm: 'dummy value', + badgeInEditForm: 'dummy value', + }); + }); + + it('sets badgeInEditForm if isEditing is true', () => { + store.state.isEditing = true; + + store.commit(types.UPDATE_BADGE_IN_FORM, dummyBadge); + + expect(store.state.badgeInEditForm).toBe(dummyBadge); + }); + + it('sets badgeInAddForm if isEditing is false', () => { + store.state.isEditing = false; + + store.commit(types.UPDATE_BADGE_IN_FORM, dummyBadge); + + expect(store.state.badgeInAddForm).toBe(dummyBadge); + }); + }); + + describe('UPDATE_BADGE_IN_MODAL', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInModal: 'dummy value', + }); + }); + + it('sets badgeInModal', () => { + store.commit(types.UPDATE_BADGE_IN_MODAL, dummyBadge); + + expect(store.state.badgeInModal).toBe(dummyBadge); + }); + }); +}); diff --git a/spec/javascripts/fixtures/one_white_pixel.png b/spec/javascripts/fixtures/one_white_pixel.png new file mode 100644 index 00000000000..073fcf40a18 Binary files /dev/null and b/spec/javascripts/fixtures/one_white_pixel.png differ diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index 34acdfbfba9..effacbcff4e 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -3,6 +3,12 @@ export const createComponentWithStore = (Component, store, propsData = {}) => ne propsData, }); +export const mountComponentWithStore = (Component, { el, props, store }) => + new Component({ + store, + propsData: props || { }, + }).$mount(el); + export default (Component, props = {}, el = null) => new Component({ propsData: props, }).$mount(el); diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js new file mode 100644 index 00000000000..7cc5e753c22 --- /dev/null +++ b/spec/javascripts/matchers.js @@ -0,0 +1,35 @@ +export default { + toHaveSpriteIcon: () => ({ + compare(element, iconName) { + if (!iconName) { + throw new Error('toHaveSpriteIcon is missing iconName argument!'); + } + + if (!(element instanceof HTMLElement)) { + throw new Error(`${element} is not a DOM element!`); + } + + const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); + const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`)); + const result = { + pass: !!matchingIcon, + }; + + if (result.pass) { + result.message = `${element.outerHTML} contains the sprite icon "${iconName}"!`; + } else { + result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; + + const existingIcons = iconReferences.map((reference) => { + const iconUrl = reference.getAttribute('xlink:href'); + return `"${iconUrl.replace(/^.+#/, '')}"`; + }); + if (existingIcons.length > 0) { + result.message += ` (only found ${existingIcons.join(',')})`; + } + } + + return result; + }, + }), +}; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 1bcfdfe72b6..d158786e484 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -7,6 +7,9 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import { getDefaultAdapter } from '~/lib/utils/axios_utils'; +import { FIXTURES_PATH, TEST_HOST } from './test_constants'; + +import customMatchers from './matchers'; const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent); Vue.config.devtools = !isHeadlessChrome; @@ -27,15 +30,17 @@ Vue.config.errorHandler = function (err) { Vue.use(VueResource); // enable test fixtures -jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; -jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; +jasmine.getFixtures().fixturesPath = FIXTURES_PATH; +jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH; + +beforeAll(() => jasmine.addMatchers(customMatchers)); // globalize common libraries window.$ = window.jQuery = $; // stub expected globals window.gl = window.gl || {}; -window.gl.TEST_HOST = 'http://test.host'; +window.gl.TEST_HOST = TEST_HOST; window.gon = window.gon || {}; window.gon.test_env = true; diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js new file mode 100644 index 00000000000..df59195e9f6 --- /dev/null +++ b/spec/javascripts/test_constants.js @@ -0,0 +1,4 @@ +export const FIXTURES_PATH = '/base/spec/javascripts/fixtures'; +export const TEST_HOST = 'http://test.host'; + +export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`; -- cgit v1.2.1 From 5c735af3b507d4d1514d1fd66318679a4a2e648e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 8 Apr 2018 05:47:54 -0700 Subject: Handle legacy repository archive requests with no ref given The legacy endpoint requires no reference and defaults to the root ref. Closes #45154 --- spec/controllers/projects/repositories_controller_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'spec') diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 31b1b52fdd1..c3b71458e38 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -34,6 +34,12 @@ describe Projects::RepositoriesController do expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end + it 'handles legacy queries with no ref' do + get :archive, namespace_id: project.namespace, project_id: project, format: "zip" + + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + context "when the service raises an error" do before do allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") -- cgit v1.2.1 From 0e78c2e9c925d180a443d132658691adf18f26a1 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Tue, 27 Mar 2018 16:31:43 +1100 Subject: Allow group owner to enable runners from subgroups (#41981) --- spec/models/user_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'spec') diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 73266c0085f..35db7616efb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1850,6 +1850,21 @@ describe User do it_behaves_like :member end + + context 'with subgroup with different owner for project runner', :nested_groups do + let(:group) { create(:group) } + let(:another_user) { create(:user) } + let(:subgroup) { create(:group, parent: group) } + let(:project) { create(:project, group: subgroup) } + + def add_user(access) + group.add_user(user, access) + group.add_user(another_user, :owner) + subgroup.add_user(another_user, :owner) + end + + it_behaves_like :member + end end describe '#projects_with_reporter_access_limited_to' do -- cgit v1.2.1 From f68aab1945679f76a0e9a51069cdc4f41e11821d Mon Sep 17 00:00:00 2001 From: Riccardo Padovani Date: Mon, 9 Apr 2018 09:39:03 +0000 Subject: Make email handler clearer --- spec/lib/gitlab/email/handler_spec.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'spec') diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb index 650b01c4df4..386d73e6115 100644 --- a/spec/lib/gitlab/email/handler_spec.rb +++ b/spec/lib/gitlab/email/handler_spec.rb @@ -14,4 +14,28 @@ describe Gitlab::Email::Handler do expect(described_class.for('email', '')).to be_nil end end + + describe 'regexps are set properly' do + let(:addresses) do + %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path/to/project+merge-request+user_email_token path/to/project+user_email_token) + end + + it 'picks each handler at least once' do + matched_handlers = addresses.map do |address| + described_class.for('email', address).class + end + + expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler::HANDLERS) + end + + it 'can pick exactly one handler for each address' do + addresses.each do |address| + matched_handlers = Gitlab::Email::Handler::HANDLERS.select do |handler| + handler.new('email', address).can_handle? + end + + expect(matched_handlers.count).to eq(1), "#{address} matches #{matched_handlers.count} handlers: #{matched_handlers}" + end + end + end end -- cgit v1.2.1