summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-04-09 15:00:59 +0100
committerPhil Hughes <me@iamphill.com>2018-04-09 15:00:59 +0100
commit3af57c3cbc986a622117e5b722b06db736fd0bf1 (patch)
treeabde59d688e57b28771aa1b62255684ce41ba465 /spec
parent7c20403f28017da6a3e99b88629149478a620993 (diff)
parent2224aa3b7f28487d5d06f34cc5761103c1a682e3 (diff)
downloadgitlab-ce-3af57c3cbc986a622117e5b722b06db736fd0bf1.tar.gz
Merge branch 'master' into ide-staged-changes
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb6
-rw-r--r--spec/factories/deploy_tokens.rb14
-rw-r--r--spec/factories/project_deploy_tokens.rb6
-rw-r--r--spec/features/groups/settings/group_badges_spec.rb124
-rw-r--r--spec/features/projects/files/template_selector_menu_spec.rb66
-rw-r--r--spec/features/projects/settings/project_badges_spec.rb125
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb27
-rw-r--r--spec/javascripts/badges/components/badge_form_spec.js171
-rw-r--r--spec/javascripts/badges/components/badge_list_row_spec.js97
-rw-r--r--spec/javascripts/badges/components/badge_list_spec.js88
-rw-r--r--spec/javascripts/badges/components/badge_settings_spec.js109
-rw-r--r--spec/javascripts/badges/components/badge_spec.js147
-rw-r--r--spec/javascripts/badges/dummy_badge.js23
-rw-r--r--spec/javascripts/badges/store/actions_spec.js607
-rw-r--r--spec/javascripts/badges/store/mutations_spec.js418
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js4
-rw-r--r--spec/javascripts/boards/modal_store_spec.js3
-rw-r--r--spec/javascripts/fixtures/one_white_pixel.pngbin0 -> 68 bytes
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js6
-rw-r--r--spec/javascripts/matchers.js35
-rw-r--r--spec/javascripts/test_bundle.js11
-rw-r--r--spec/javascripts/test_constants.js4
-rw-r--r--spec/lib/forever_spec.rb21
-rw-r--r--spec/lib/gitlab/auth_spec.rb122
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb24
-rw-r--r--spec/lib/gitlab/git_access_spec.rb62
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/mailers/notify_spec.rb16
-rw-r--r--spec/models/deploy_token_spec.rb134
-rw-r--r--spec/models/project_deploy_token_spec.rb14
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/models/user_spec.rb15
-rw-r--r--spec/policies/deploy_token_policy_spec.rb45
-rw-r--r--spec/services/deploy_tokens/create_service_spec.rb45
34 files changed, 2584 insertions, 12 deletions
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")
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
new file mode 100644
index 00000000000..5fea4a9d5a6
--- /dev/null
+++ b/spec/factories/deploy_tokens.rb
@@ -0,0 +1,14 @@
+FactoryBot.define do
+ factory :deploy_token do
+ token { SecureRandom.hex(50) }
+ sequence(:name) { |n| "PDT #{n}" }
+ read_repository true
+ read_registry true
+ revoked false
+ expires_at { 5.days.from_now }
+
+ trait :revoked do
+ revoked true
+ 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/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/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
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/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 14670e91006..f2c371b7df5 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -88,5 +88,32 @@ 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, projects: [project]) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ 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('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_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')
+ end
+ 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(`
+ <div id="dummy-element"></div>
+ `);
+ });
+
+ 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(`
+ <div id="delete-badge-modal" class="modal"></div>
+ <div id="dummy-element"></div>
+ `);
+ 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('<div id="dummy-element"></div>');
+ 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(`
+ <div id="dummy-element"></div>
+ <button
+ id="dummy-modal-button"
+ type="button"
+ data-toggle="modal"
+ data-target="#delete-badge-modal"
+ >Show modal</button>
+ `);
+ 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('<div id="dummy-element"></div>');
+ 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 = '<script>I am dangerous!</script>';
+
+ 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/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();
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
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
--- /dev/null
+++ b/spec/javascripts/fixtures/one_white_pixel.png
Binary files 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`;
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/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 18cef8ec996..9ccd0b206cc 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_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_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user sudo read_repository 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,122 @@ 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(: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_registry: false, projects: [project]) }
+ let(:login) { deploy_token.username }
+
+ 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: 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: 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
+ 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
+
+ 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
+
+ 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 when login and token are valid' do
+ 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'))
+ .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: 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: login)
+ expect(gl_auth.find_for_git_client(login, '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
+
+ context 'when registry disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ 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
+ end
+ end
end
describe 'find_with_user_password' do
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
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c870997e274..6c625596605 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
+ 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(: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
context 'when actor is nil' do
@@ -594,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 b675d5dc031..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,8 @@ project:
- project_badges
- source_of_merge_requests
- internal_ids
+- project_deploy_tokens
+- deploy_tokens
award_emoji:
- awardable
- user
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
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
new file mode 100644
index 00000000000..5a15c23def4
--- /dev/null
+++ b/spec/models/deploy_token_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe DeployToken do
+ subject(:deploy_token) { create(:deploy_token) }
+
+ it { is_expected.to have_many :project_deploy_tokens }
+ it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
+
+ 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[:base].first).to eq("Scopes can't be blank")
+ end
+ end
+ end
+
+ 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
+
+ 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
+
+ describe '#revoke!' do
+ it 'should update revoke attribute' do
+ 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 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
+
+ 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/models/project_deploy_token_spec.rb b/spec/models/project_deploy_token_spec.rb
new file mode 100644
index 00000000000..9e2e40c2e8f
--- /dev/null
+++ b/spec/models/project_deploy_token_spec.rb
@@ -0,0 +1,14 @@
+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 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/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
diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb
new file mode 100644
index 00000000000..eea287d895e
--- /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, projects: [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/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb
new file mode 100644
index 00000000000..3a2bbf1ecd1
--- /dev/null
+++ b/spec/services/deploy_tokens/create_service_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe DeployTokens::CreateService 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).execute }
+
+ context 'when the deploy token is valid' do
+ it 'should create a new DeployToken' do
+ expect { subject }.to change { DeployToken.count }.by(1)
+ end
+
+ it 'should create a new ProjectDeployToken' do
+ expect { subject }.to change { ProjectDeployToken.count }.by(1)
+ end
+
+ it 'returns a DeployToken' do
+ expect(subject).to be_an_instance_of DeployToken
+ 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.read_attribute(:expires_at)).to eq(Forever.date)
+ end
+ end
+
+ context 'when the deploy token is invalid' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) }
+
+ 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
+ end
+ end
+end