diff options
Diffstat (limited to 'spec')
21 files changed, 425 insertions, 306 deletions
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js index b02d94e9cb1..3a6989a00f1 100644 --- a/spec/frontend/crm/contacts_root_spec.js +++ b/spec/frontend/crm/contacts_root_spec.js @@ -105,7 +105,7 @@ describe('Customer relations contacts root app', () => { const issueLink = findIssuesLinks().at(0); expect(issueLink.exists()).toBe(true); - expect(issueLink.attributes('href')).toBe('/issues?scope=all&state=opened&crm_contact_id=16'); + expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16'); }); }); }); diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js index 231208d938e..1780a5945a6 100644 --- a/spec/frontend/crm/organizations_root_spec.js +++ b/spec/frontend/crm/organizations_root_spec.js @@ -102,9 +102,7 @@ describe('Customer relations organizations root app', () => { const issueLink = findIssuesLinks().at(0); expect(issueLink.exists()).toBe(true); - expect(issueLink.attributes('href')).toBe( - '/issues?scope=all&state=opened&crm_organization_id=2', - ); + expect(issueLink.attributes('href')).toBe('/issues?crm_organization_id=2'); }); }); }); diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js index d3e05939527..728b8958b9b 100644 --- a/spec/frontend/issuable/components/status_box_spec.js +++ b/spec/frontend/issuable/components/status_box_spec.js @@ -1,65 +1,53 @@ -import { GlSprintf } from '@gitlab/ui'; +import { GlBadge, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import StatusBox from '~/issuable/components/status_box.vue'; let wrapper; function factory(propsData) { - wrapper = shallowMount(StatusBox, { - propsData, - stubs: { GlSprintf }, - provide: { glFeatures: { updatedMrHeader: true } }, - }); + wrapper = shallowMount(StatusBox, { propsData, stubs: { GlBadge } }); } -const testCases = [ - { - name: 'Open', - state: 'opened', - class: 'badge-success', - }, - { - name: 'Open', - state: 'locked', - class: 'badge-success', - }, - { - name: 'Closed', - state: 'closed', - class: 'badge-danger', - }, - { - name: 'Merged', - state: 'merged', - class: 'badge-info', - }, -]; - describe('Merge request status box component', () => { + const findBadge = () => wrapper.findComponent(GlBadge); + afterEach(() => { wrapper.destroy(); wrapper = null; }); - testCases.forEach((testCase) => { - describe(`when merge request is ${testCase.name}`, () => { - it('renders human readable test', () => { + describe.each` + issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon + ${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'} + ${'merge_request'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'danger'} | ${'merge-request-close'} + ${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'} + ${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'} + ${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'} + `( + 'with issuableType set to "$issuableType" and state set to "$initialState"', + ({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => { + beforeEach(() => { factory({ - initialState: testCase.state, - issuableType: 'merge_request', + initialState, + issuableType, }); + }); - expect(wrapper.text()).toContain(testCase.name); + it(`renders badge with text '${badgeText}'`, () => { + expect(findBadge().text()).toBe(badgeText); }); - it('sets css class', () => { - factory({ - initialState: testCase.state, - issuableType: 'merge_request', - }); + it(`sets badge css class as '${badgeClass}'`, () => { + expect(findBadge().classes()).toContain(badgeClass); + }); - expect(wrapper.classes()).toContain(testCase.class); + it(`sets badge variant as '${badgeVariant}`, () => { + expect(findBadge().props('variant')).toBe(badgeVariant); }); - }); - }); + + it(`sets badge icon as '${badgeIcon}'`, () => { + expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon); + }); + }, + ); }); diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js index 8a089b372ff..b4f6118ec20 100644 --- a/spec/frontend/issues/issue_spec.js +++ b/spec/frontend/issues/issue_spec.js @@ -24,11 +24,11 @@ describe('Issue', () => { const getIssueCounter = () => document.querySelector('.issue_counter'); const getOpenStatusBox = () => getByText(document, (_, el) => el.textContent.match(/Open/), { - selector: '.status-box-open', + selector: '.issuable-status-badge-open', }); const getClosedStatusBox = () => getByText(document, (_, el) => el.textContent.match(/Closed/), { - selector: '.status-box-issue-closed', + selector: '.issuable-status-badge-closed', }); describe.each` diff --git a/spec/frontend/sidebar/components/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts_spec.js index 758cff30e2d..6456829258f 100644 --- a/spec/frontend/sidebar/components/crm_contacts_spec.js +++ b/spec/frontend/sidebar/components/crm_contacts_spec.js @@ -33,7 +33,7 @@ describe('Issue crm contacts component', () => { [issueCrmContactsSubscription, subscriptionHandler], ]); wrapper = shallowMountExtended(CrmContacts, { - propsData: { issueId: '123' }, + propsData: { issueId: '123', groupIssuesPath: '/groups/flightjs/-/issues' }, apolloProvider: fakeApollo, }); }; @@ -71,8 +71,14 @@ describe('Issue crm contacts component', () => { await waitForPromises(); expect(wrapper.find('#contact_0').text()).toContain('Someone Important'); + expect(wrapper.find('#contact_0').attributes('href')).toBe( + '/groups/flightjs/-/issues?crm_contact_id=1', + ); expect(wrapper.find('#contact_container_0').text()).toContain('si@gitlab.com'); expect(wrapper.find('#contact_1').text()).toContain('Marty McFly'); + expect(wrapper.find('#contact_1').attributes('href')).toBe( + '/groups/flightjs/-/issues?crm_contact_id=5', + ); }); it('renders correct results after subscription update', async () => { @@ -83,5 +89,8 @@ describe('Issue crm contacts component', () => { contact.forEach((property) => { expect(wrapper.find('#contact_container_0').text()).toContain(property); }); + expect(wrapper.find('#contact_0').attributes('href')).toBe( + '/groups/flightjs/-/issues?crm_contact_id=13', + ); }); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js index 868b7af6da5..02579e8b3c4 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -69,9 +69,11 @@ describe('IssuableHeader', () => { it('renders issuable status icon and text', () => { createComponent(); const statusBoxEl = wrapper.findByTestId('status'); + const statusIconEl = statusBoxEl.findComponent(GlIcon); expect(statusBoxEl.exists()).toBe(true); - expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon); + expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon); + expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass); expect(statusBoxEl.text()).toContain('Open'); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js index d1eb1366225..8b027f990a2 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js @@ -49,6 +49,7 @@ describe('IssuableShowRoot', () => { const { statusBadgeClass, statusIcon, + statusIconClass, enableEdit, enableAutocomplete, editFormVisible, @@ -56,7 +57,7 @@ describe('IssuableShowRoot', () => { descriptionHelpPath, taskCompletionStatus, } = mockIssuableShowProps; - const { blocked, confidential, createdAt, author } = mockIssuable; + const { state, blocked, confidential, createdAt, author } = mockIssuable; it('renders component container element with class `issuable-show-container`', () => { expect(wrapper.classes()).toContain('issuable-show-container'); @@ -67,15 +68,17 @@ describe('IssuableShowRoot', () => { expect(issuableHeader.exists()).toBe(true); expect(issuableHeader.props()).toMatchObject({ + issuableState: state, statusBadgeClass, statusIcon, + statusIconClass, blocked, confidential, createdAt, author, taskCompletionStatus, }); - expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open'); + expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open'); expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe( true, ); diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js index f5f3ed58655..32bb9edfe08 100644 --- a/spec/frontend/vue_shared/issuable/show/mock_data.js +++ b/spec/frontend/vue_shared/issuable/show/mock_data.js @@ -36,8 +36,9 @@ export const mockIssuableShowProps = { enableTaskList: true, enableEdit: true, showFieldTitle: false, - statusBadgeClass: 'status-box-open', - statusIcon: 'issue-open-m', + statusBadgeClass: 'issuable-status-badge-open', + statusIcon: 'issues', + statusIconClass: 'gl-sm-display-none', taskCompletionStatus: { completedCount: 0, count: 5, diff --git a/spec/helpers/badges_helper_spec.rb b/spec/helpers/badges_helper_spec.rb index 5be3b4a737b..8e1f92305da 100644 --- a/spec/helpers/badges_helper_spec.rb +++ b/spec/helpers/badges_helper_spec.rb @@ -89,16 +89,16 @@ RSpec.describe BadgesHelper do end describe 'icons' do - let(:spacing_class_regex) { %r{<svg .*class=".*gl-mr-2.*".*>.*</svg>} } + let(:spacing_class_regex) { %r{<svg .*class=".*my-icon-class gl-mr-2".*>.*</svg>} } describe 'with text' do - subject { helper.gl_badge_tag(label, icon: "question-o") } + subject { helper.gl_badge_tag(label, icon: "question-o", icon_classes: 'my-icon-class') } it 'renders an icon' do expect(subject).to match(%r{<svg .*#question-o".*>.*</svg>}) end - it 'adds a spacing class to the icon' do + it 'adds a spacing class and any custom classes to the icon' do expect(subject).to match(spacing_class_regex) end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index ee5b0145d13..2244aaee2dc 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -464,6 +464,41 @@ RSpec.describe IssuablesHelper do end end + describe '#state_name_with_icon' do + let_it_be(:project) { create(:project, :repository) } + + context 'for an issue' do + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue_closed) { create(:issue, :closed, project: project) } + + it 'returns the correct state name and icon when issue is open' do + expect(helper.state_name_with_icon(issue)).to match_array([_('Open'), 'issues']) + end + + it 'returns the correct state name and icon when issue is closed' do + expect(helper.state_name_with_icon(issue_closed)).to match_array([_('Closed'), 'issue-closed']) + end + end + + context 'for a merge request' do + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:merge_request_merged) { create(:merge_request, :merged, source_project: project) } + let_it_be(:merge_request_closed) { create(:merge_request, :closed, source_project: project) } + + it 'returns the correct state name and icon when merge request is open' do + expect(helper.state_name_with_icon(merge_request)).to match_array([_('Open'), 'merge-request-open']) + end + + it 'returns the correct state name and icon when merge request is merged' do + expect(helper.state_name_with_icon(merge_request_merged)).to match_array([_('Merged'), 'merge']) + end + + it 'returns the correct state name and icon when merge request is closed' do + expect(helper.state_name_with_icon(merge_request_closed)).to match_array([_('Closed'), 'merge-request-close']) + end + end + end + describe '#issuable_display_type' do using RSpec::Parameterized::TableSyntax diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index f7762069b68..97ad55d9df9 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -5,31 +5,6 @@ require 'spec_helper' RSpec.describe MergeRequestsHelper do include ProjectForksHelper - describe '#state_name_with_icon' do - using RSpec::Parameterized::TableSyntax - - let(:merge_request) { MergeRequest.new } - - where(:state, :expected_name, :expected_icon) do - :merged? | 'Merged' | 'git-merge' - :closed? | 'Closed' | 'close' - :opened? | 'Open' | 'issue-open-m' - end - - with_them do - before do - allow(merge_request).to receive(state).and_return(true) - end - - it 'returns name and icon' do - name, icon = helper.state_name_with_icon(merge_request) - - expect(name).to eq(expected_name) - expect(icon).to eq(expected_icon) - end - end - end - describe '#format_mr_branch_names' do describe 'within the same project' do let(:merge_request) { create(:merge_request) } diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 8e372ba795b..d4f96f1a37f 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -22,6 +22,8 @@ RSpec.describe Gitlab::UrlBuilder do :group_board | ->(board) { "/groups/#{board.group.full_path}/-/boards/#{board.id}" } :commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" } :issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" } + [:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.id}" } + :work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.id}" } :merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" } :project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" } :project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" } @@ -57,7 +59,7 @@ RSpec.describe Gitlab::UrlBuilder do end with_them do - let(:object) { build_stubbed(factory) } + let(:object) { build_stubbed(*Array(factory)) } let(:path) { path_generator.call(object) } it 'returns the full URL' do @@ -69,6 +71,18 @@ RSpec.describe Gitlab::UrlBuilder do end end + context 'when work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'returns an issue path for an issue of type task' do + task = create(:issue, :task) + + expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/issues/#{task.iid}") + end + end + context 'when passing a compare' do # NOTE: The Compare requires an actual repository, which isn't available # with the `build_stubbed` strategy used by the table tests above diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb index d22955718f8..3447ff83ed8 100644 --- a/spec/requests/projects/issue_links_controller_spec.rb +++ b/spec/requests/projects/issue_links_controller_spec.rb @@ -24,6 +24,17 @@ RSpec.describe Projects::IssueLinksController do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq(list_service_response.as_json) end + + context 'when linked issue is a task' do + let(:issue_b) { create :issue, :task, project: project } + + it 'returns a work item path for the linked task' do + get namespace_project_issue_links_path(issue_links_params) + + expect(json_response.count).to eq(1) + expect(json_response.first).to include('path' => project_work_items_path(issue_b.project, issue_b.id)) + end + end end describe 'POST /*namespace_id/:project_id/issues/:issue_id/links' do diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb index 30423ceba6d..b8e2bfeaa3d 100644 --- a/spec/serializers/issue_board_entity_spec.rb +++ b/spec/serializers/issue_board_entity_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe IssueBoardEntity do + include Gitlab::Routing.url_helpers + let_it_be(:project) { create(:project) } let_it_be(:resource) { create(:issue, project: project) } let_it_be(:user) { create(:user) } @@ -40,4 +42,18 @@ RSpec.describe IssueBoardEntity do expect(subject).to include(labels: array_including(hash_including(:id, :title, :color, :description, :text_color, :priority))) end + + describe 'real_path' do + it 'has an issue path' do + expect(subject[:real_path]).to eq(project_issue_path(project, resource.iid)) + end + + context 'when issue is of type task' do + let(:resource) { create(:issue, :task, project: project) } + + it 'has a work item path' do + expect(subject[:real_path]).to eq(project_work_items_path(project, resource.id)) + end + end + end end diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb index 76f8cf644c6..6ccb3dbc657 100644 --- a/spec/serializers/issue_entity_spec.rb +++ b/spec/serializers/issue_entity_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe IssueEntity do + include Gitlab::Routing.url_helpers + let(:project) { create(:project) } let(:resource) { create(:issue, project: project) } let(:user) { create(:user) } @@ -11,6 +13,17 @@ RSpec.describe IssueEntity do subject { described_class.new(resource, request: request).as_json } + describe 'web_url' do + context 'when issue is of type task' do + let(:resource) { create(:issue, :task, project: project) } + + # This was already a path and not a url when the work items change was introduced + it 'has a work item path' do + expect(subject[:web_url]).to eq(project_work_items_path(project, resource.id)) + end + end + end + it 'has Issuable attributes' do expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id, :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels) diff --git a/spec/serializers/linked_project_issue_entity_spec.rb b/spec/serializers/linked_project_issue_entity_spec.rb index 864b5c45599..b28b00bd8e1 100644 --- a/spec/serializers/linked_project_issue_entity_spec.rb +++ b/spec/serializers/linked_project_issue_entity_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe LinkedProjectIssueEntity do + include Gitlab::Routing.url_helpers + let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:issue_link) { create(:issue_link) } @@ -17,7 +19,25 @@ RSpec.describe LinkedProjectIssueEntity do issue_link.target.project.add_developer(user) end + subject(:serialized_entity) { entity.as_json } + describe 'issue_link_type' do - it { expect(entity.as_json).to include(link_type: 'relates_to') } + it { is_expected.to include(link_type: 'relates_to') } + end + + describe 'path' do + it 'returns an issue path' do + expect(serialized_entity).to include(path: project_issue_path(related_issue.project, related_issue.iid)) + end + + context 'when related issue is a task' do + before do + related_issue.update!(issue_type: :task, work_item_type: WorkItems::Type.default_by_type(:task)) + end + + it 'returns a work items path' do + expect(serialized_entity).to include(path: project_work_items_path(related_issue.project, related_issue.id)) + end + end end end diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb index 4427c4e7d9f..c3ba7c0374d 100644 --- a/spec/services/members/groups/creator_service_spec.rb +++ b/spec/services/members/groups/creator_service_spec.rb @@ -3,14 +3,28 @@ require 'spec_helper' RSpec.describe Members::Groups::CreatorService do - it_behaves_like 'member creation' do - let_it_be(:source, reload: true) { create(:group, :public) } - let_it_be(:member_type) { GroupMember } - end - describe '.access_levels' do it 'returns Gitlab::Access.options_with_owner' do expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner) end end + + describe '#execute' do + let_it_be(:source, reload: true) { create(:group, :public) } + let_it_be(:user) { create(:user) } + + it_behaves_like 'member creation' do + let_it_be(:member_type) { GroupMember } + end + + context 'authorized projects update' do + it 'schedules a single project authorization update job when called multiple times' do + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once + + 1.upto(3) do + described_class.new(source, user, :maintainer).execute + end + end + end + end end diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb index 7ba183759bc..7605238c3c5 100644 --- a/spec/services/members/projects/creator_service_spec.rb +++ b/spec/services/members/projects/creator_service_spec.rb @@ -3,14 +3,28 @@ require 'spec_helper' RSpec.describe Members::Projects::CreatorService do - it_behaves_like 'member creation' do - let_it_be(:source, reload: true) { create(:project, :public) } - let_it_be(:member_type) { ProjectMember } - end - describe '.access_levels' do it 'returns Gitlab::Access.sym_options_with_owner' do expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner) end end + + describe '#execute' do + let_it_be(:source, reload: true) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + it_behaves_like 'member creation' do + let_it_be(:member_type) { ProjectMember } + end + + context 'authorized projects update' do + it 'schedules a single project authorization update job when called multiple times' do + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once + + 1.upto(3) do + described_class.new(source, user, :maintainer).execute + end + end + end + end end diff --git a/spec/support/helpers/trial_status_widget_test_helper.rb b/spec/support/helpers/trial_status_widget_test_helper.rb new file mode 100644 index 00000000000..d75620d17ee --- /dev/null +++ b/spec/support/helpers/trial_status_widget_test_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module TrialStatusWidgetTestHelper + def purchase_href(group) + new_subscriptions_path(namespace_id: group.id, plan_id: 'ultimate-plan-id') + end +end + +TrialStatusWidgetTestHelper.prepend_mod diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index a329a6dca91..e293d10964b 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -77,312 +77,309 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name| end RSpec.shared_examples_for "member creation" do - let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } - describe '#execute' do - it 'returns a Member object', :aggregate_failures do - member = described_class.new(source, user, :maintainer).execute - - expect(member).to be_a member_type - expect(member).to be_persisted - end + it 'returns a Member object', :aggregate_failures do + member = described_class.new(source, user, :maintainer).execute - context 'when adding a project_bot' do - let_it_be(:project_bot) { create(:user, :project_bot) } - - before_all do - source.add_owner(user) - end + expect(member).to be_a member_type + expect(member).to be_persisted + end - context 'when project_bot is already a member' do - before do - source.add_developer(project_bot) - end + context 'when adding a project_bot' do + let_it_be(:project_bot) { create(:user, :project_bot) } - it 'does not update the member' do - member = described_class.new(source, project_bot, :maintainer, current_user: user).execute + before_all do + source.add_owner(user) + end - expect(source.users.reload).to include(project_bot) - expect(member).to be_persisted - expect(member.access_level).to eq(Gitlab::Access::DEVELOPER) - expect(member.errors.full_messages).to include(/not authorized to update member/) - end + context 'when project_bot is already a member' do + before do + source.add_developer(project_bot) end - context 'when project_bot is not already a member' do - it 'adds the member' do - member = described_class.new(source, project_bot, :maintainer, current_user: user).execute + it 'does not update the member' do + member = described_class.new(source, project_bot, :maintainer, current_user: user).execute - expect(source.users.reload).to include(project_bot) - expect(member).to be_persisted - end + expect(source.users.reload).to include(project_bot) + expect(member).to be_persisted + expect(member.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(member.errors.full_messages).to include(/not authorized to update member/) end end - context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do - it 'sets members.created_by to the given admin current_user' do - member = described_class.new(source, user, :maintainer, current_user: admin).execute + context 'when project_bot is not already a member' do + it 'adds the member' do + member = described_class.new(source, project_bot, :maintainer, current_user: user).execute + expect(source.users.reload).to include(project_bot) expect(member).to be_persisted - expect(source.users.reload).to include(user) - expect(member.created_by).to eq(admin) end end + end - context 'when admin mode is disabled' do - it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do - member = described_class.new(source, user, :maintainer, current_user: admin).execute + context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do + it 'sets members.created_by to the given admin current_user' do + member = described_class.new(source, user, :maintainer, current_user: admin).execute - expect(member).not_to be_persisted - expect(source.users.reload).not_to include(user) - expect(member.errors.full_messages).to include(/not authorized to create member/) - end + expect(member).to be_persisted + expect(source.users.reload).to include(user) + expect(member.created_by).to eq(admin) end + end - it 'sets members.expires_at to the given expires_at' do - member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute + context 'when admin mode is disabled' do + it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do + member = described_class.new(source, user, :maintainer, current_user: admin).execute - expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + expect(member).not_to be_persisted + expect(source.users.reload).not_to include(user) + expect(member.errors.full_messages).to include(/not authorized to create member/) end + end - described_class.access_levels.each do |sym_key, int_access_level| - it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do - expect(source.users).not_to include(user) + it 'sets members.expires_at to the given expires_at' do + member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute - member = described_class.new(source, user.id, sym_key).execute + expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + end - expect(member.access_level).to eq(int_access_level) - expect(source.users.reload).to include(user) - end + described_class.access_levels.each do |sym_key, int_access_level| + it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, sym_key).execute - it "accepts the #{int_access_level} integer as access level", :aggregate_failures do + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + + it "accepts the #{int_access_level} integer as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, int_access_level).execute + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + end + + context 'with no current_user' do + context 'when called with a known user id' do + it 'adds the user as a member' do expect(source.users).not_to include(user) - member = described_class.new(source, user.id, int_access_level).execute + described_class.new(source, user.id, :maintainer).execute - expect(member.access_level).to eq(int_access_level) expect(source.users.reload).to include(user) end end - context 'with no current_user' do - context 'when called with a known user id' do - it 'adds the user as a member' do - expect(source.users).not_to include(user) + context 'when called with an unknown user id' do + it 'does not add the user as a member' do + expect(source.users).not_to include(user) - described_class.new(source, user.id, :maintainer).execute + described_class.new(source, non_existing_record_id, :maintainer).execute - expect(source.users.reload).to include(user) - end + expect(source.users.reload).not_to include(user) end + end - context 'when called with an unknown user id' do - it 'does not add the user as a member' do - expect(source.users).not_to include(user) + context 'when called with a user object' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) - described_class.new(source, non_existing_record_id, :maintainer).execute + described_class.new(source, user, :maintainer).execute - expect(source.users.reload).not_to include(user) - end + expect(source.users.reload).to include(user) + end + end + + context 'when called with a requester user object' do + before do + source.request_access(user) end - context 'when called with a user object' do - it 'adds the user as a member' do - expect(source.users).not_to include(user) + it 'adds the requester as a member', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + expect do described_class.new(source, user, :maintainer).execute + end.to raise_error(Gitlab::Access::AccessDeniedError) - expect(source.users.reload).to include(user) - end + expect(source.users.reload).not_to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_truthy end + end - context 'when called with a requester user object' do - before do - source.request_access(user) - end - - it 'adds the requester as a member', :aggregate_failures do - expect(source.users).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy + context 'when called with a known user email' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) - expect do - described_class.new(source, user, :maintainer).execute - end.to raise_error(Gitlab::Access::AccessDeniedError) + described_class.new(source, user.email, :maintainer).execute - expect(source.users.reload).not_to include(user) - expect(source.requesters.reload.exists?(user_id: user)).to be_truthy - end + expect(source.users.reload).to include(user) end + end - context 'when called with a known user email' do - it 'adds the user as a member' do - expect(source.users).not_to include(user) + context 'when called with an unknown user email' do + it 'creates an invited member' do + expect(source.users).not_to include(user) - described_class.new(source, user.email, :maintainer).execute + described_class.new(source, 'user@example.com', :maintainer).execute - expect(source.users.reload).to include(user) - end + expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') end + end - context 'when called with an unknown user email' do - it 'creates an invited member' do - expect(source.users).not_to include(user) + context 'when called with an unknown user email starting with a number' do + it 'creates an invited member', :aggregate_failures do + email_starting_with_number = "#{user.id}_email@example.com" - described_class.new(source, 'user@example.com', :maintainer).execute + described_class.new(source, email_starting_with_number, :maintainer).execute - expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') - end + expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) + expect(source.users.reload).not_to include(user) end + end + end - context 'when called with an unknown user email starting with a number' do - it 'creates an invited member', :aggregate_failures do - email_starting_with_number = "#{user.id}_email@example.com" + context 'when current_user can update member', :enable_admin_mode do + it 'creates the member' do + expect(source.users).not_to include(user) - described_class.new(source, email_starting_with_number, :maintainer).execute + described_class.new(source, user, :maintainer, current_user: admin).execute - expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) - expect(source.users.reload).not_to include(user) - end - end + expect(source.users.reload).to include(user) end - context 'when current_user can update member', :enable_admin_mode do - it 'creates the member' do + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member', :aggregate_failures do expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy described_class.new(source, user, :maintainer, current_user: admin).execute expect(source.users.reload).to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_falsy end + end + end - context 'when called with a requester user object' do - before do - source.request_access(user) - end + context 'when current_user cannot update member' do + it 'does not create the member', :aggregate_failures do + expect(source.users).not_to include(user) - it 'adds the requester as a member', :aggregate_failures do - expect(source.users).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy + member = described_class.new(source, user, :maintainer, current_user: user).execute - described_class.new(source, user, :maintainer, current_user: admin).execute + expect(source.users.reload).not_to include(user) + expect(member).not_to be_persisted + end - expect(source.users.reload).to include(user) - expect(source.requesters.reload.exists?(user_id: user)).to be_falsy - end + context 'when called with a requester user object' do + before do + source.request_access(user) end - end - context 'when current_user cannot update member' do - it 'does not create the member', :aggregate_failures do + it 'does not destroy the requester', :aggregate_failures do expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy - member = described_class.new(source, user, :maintainer, current_user: user).execute + described_class.new(source, user, :maintainer, current_user: user).execute expect(source.users.reload).not_to include(user) - expect(member).not_to be_persisted + expect(source.requesters.exists?(user_id: user)).to be_truthy end + end + end - context 'when called with a requester user object' do - before do - source.request_access(user) - end + context 'when member already exists' do + before do + source.add_user(user, :developer) + end - it 'does not destroy the requester', :aggregate_failures do - expect(source.users).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy + context 'with no current_user' do + it 'updates the member' do + expect(source.users).to include(user) - described_class.new(source, user, :maintainer, current_user: user).execute + described_class.new(source, user, :maintainer).execute - expect(source.users.reload).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy - end + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) end end - context 'when member already exists' do - before do - source.add_user(user, :developer) - end - - context 'with no current_user' do - it 'updates the member' do - expect(source.users).to include(user) + context 'when current_user can update member', :enable_admin_mode do + it 'updates the member' do + expect(source.users).to include(user) - described_class.new(source, user, :maintainer).execute + described_class.new(source, user, :maintainer, current_user: admin).execute - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) - end + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) end + end - context 'when current_user can update member', :enable_admin_mode do - it 'updates the member' do - expect(source.users).to include(user) + context 'when current_user cannot update member' do + it 'does not update the member' do + expect(source.users).to include(user) - described_class.new(source, user, :maintainer, current_user: admin).execute + described_class.new(source, user, :maintainer, current_user: user).execute - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) - end + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) end + end + end - context 'when current_user cannot update member' do - it 'does not update the member' do - expect(source.users).to include(user) + context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do + let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source } - described_class.new(source, user, :maintainer, current_user: user).execute + it 'creates a member_task with the correct attributes', :aggregate_failures do + described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) - end - end - end + member = source.members.last - context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do - let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source } + expect(member.tasks_to_be_done).to match_array([:ci, :code]) + expect(member.member_task.project).to eq(task_project) + end - it 'creates a member_task with the correct attributes', :aggregate_failures do - described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute + context 'with an already existing member' do + before do + source.add_user(user, :developer) + end - member = source.members.last + it 'does not update tasks to be done if tasks already exist', :aggregate_failures do + member = source.members.find_by(user_id: user.id) + create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci)) - expect(member.tasks_to_be_done).to match_array([:ci, :code]) + expect do + described_class.new(source, + user, + :developer, + tasks_to_be_done: %w(issues), + tasks_project_id: task_project.id).execute + end.not_to change(MemberTask, :count) + + member.reset + expect(member.tasks_to_be_done).to match_array([:code, :ci]) expect(member.member_task.project).to eq(task_project) end - context 'with an already existing member' do - before do - source.add_user(user, :developer) - end - - it 'does not update tasks to be done if tasks already exist', :aggregate_failures do - member = source.members.find_by(user_id: user.id) - create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci)) - - expect do - described_class.new(source, - user, - :developer, - tasks_to_be_done: %w(issues), - tasks_project_id: task_project.id).execute - end.not_to change(MemberTask, :count) - - member.reset - expect(member.tasks_to_be_done).to match_array([:code, :ci]) - expect(member.member_task.project).to eq(task_project) - end - - it 'adds tasks to be done if they do not exist', :aggregate_failures do - expect do - described_class.new(source, - user, - :developer, - tasks_to_be_done: %w(issues), - tasks_project_id: task_project.id).execute - end.to change(MemberTask, :count).by(1) - - member = source.members.find_by(user_id: user.id) - expect(member.tasks_to_be_done).to match_array([:issues]) - expect(member.member_task.project).to eq(task_project) - end + it 'adds tasks to be done if they do not exist', :aggregate_failures do + expect do + described_class.new(source, + user, + :developer, + tasks_to_be_done: %w(issues), + tasks_project_id: task_project.id).execute + end.to change(MemberTask, :count).by(1) + + member = source.members.find_by(user_id: user.id) + expect(member.tasks_to_be_done).to match_array([:issues]) + expect(member.member_task.project).to eq(task_project) end end end diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb index b2d208f038a..3f1496a24ce 100644 --- a/spec/views/projects/issues/show.html.haml_spec.rb +++ b/spec/views/projects/issues/show.html.haml_spec.rb @@ -26,14 +26,14 @@ RSpec.describe 'projects/issues/show' do it 'shows "Closed (moved)" if an issue has been moved and closed' do render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)') end it 'shows "Closed (moved)" if an issue has been moved and discussion is locked' do allow(issue).to receive(:discussion_locked).and_return(true) render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)') end it 'links "moved" to the new issue the original issue was moved to' do @@ -47,7 +47,7 @@ RSpec.describe 'projects/issues/show' do render - expect(rendered).not_to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + expect(rendered).not_to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)') end end @@ -75,7 +75,7 @@ RSpec.describe 'projects/issues/show' do it 'shows "Closed (duplicated)" if an issue has been duplicated' do render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (duplicated)') + expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (duplicated)') end it 'links "duplicated" to the new issue the original issue was duplicated to' do @@ -97,14 +97,14 @@ RSpec.describe 'projects/issues/show' do it 'shows "Closed" if an issue has not been moved or duplicated' do render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed') + expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed') end it 'shows "Closed" if discussion is locked' do allow(issue).to receive(:discussion_locked).and_return(true) render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed') + expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed') end end @@ -117,14 +117,14 @@ RSpec.describe 'projects/issues/show' do it 'shows "Open" if an issue has been moved' do render - expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open') + expect(rendered).to have_selector('.issuable-status-badge-open:not(.hidden)', text: 'Open') end it 'shows "Open" if discussion is locked' do allow(issue).to receive(:discussion_locked).and_return(true) render - expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open') + expect(rendered).to have_selector('.issuable-status-badge-open:not(.hidden)', text: 'Open') end end |