From 998adcc422d4161515bf2960ef4dce71258f69a3 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 4 May 2021 12:10:04 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../oauth/authorizations_controller_spec.rb | 69 ++++-- spec/factories/projects.rb | 2 +- spec/features/admin/admin_dev_ops_report_spec.rb | 6 +- spec/features/groups_spec.rb | 29 +++ .../issues/user_toggles_subscription_spec.rb | 4 +- .../devops_score/components/devops_score_spec.js | 91 ++++++++ .../admin/analytics/devops_score/mock_data.js | 42 ++++ .../components/board_content_sidebar_spec.js | 4 +- .../issuable/components/status_box_spec.js | 71 ++++++ .../merge_request/components/status_box_spec.js | 87 -------- .../frontend/notes/components/comment_form_spec.js | 4 + .../projects/compare/components/app_spec.js | 77 +++++-- .../projects/compare/components/mock_data.js | 37 ++++ .../compare/components/repo_dropdown_spec.js | 56 ++--- .../compare/components/revision_card_spec.js | 8 +- .../compare/components/revision_dropdown_spec.js | 22 +- .../sidebar_subscriptions_widget_spec.js | 131 +++++++++++ spec/frontend/sidebar/mock_data.js | 14 ++ .../frontend/sidebar/sidebar_subscriptions_spec.js | 36 --- spec/graphql/types/ci/job_type_spec.rb | 2 + spec/graphql/types/ci/runner_type_spec.rb | 16 ++ spec/graphql/types/permission_types/ci/job_spec.rb | 13 ++ spec/graphql/types/query_type_spec.rb | 7 + spec/helpers/auth_helper_spec.rb | 33 +++ spec/helpers/dev_ops_report_helper_spec.rb | 35 +++ spec/helpers/users_helper_spec.rb | 2 +- spec/lib/api/entities/release_spec.rb | 39 +++- .../recalculate_project_authorizations_spec.rb | 241 --------------------- .../import_export/base/relation_factory_spec.rb | 1 + spec/lib/gitlab/import_export/config_spec.rb | 2 +- .../import_export/group/relation_factory_spec.rb | 1 + .../json/streaming_serializer_spec.rb | 58 ++++- .../import_export/project/relation_factory_spec.rb | 72 +++++- .../project/sample/relation_factory_spec.rb | 1 + spec/lib/gitlab/tree_summary_spec.rb | 2 +- spec/lib/gitlab/usage_data_spec.rb | 7 +- spec/models/note_spec.rb | 10 + spec/models/project_spec.rb | 31 +++ spec/requests/api/graphql/ci/runner_spec.rb | 144 ++++++++++++ spec/spec_helper.rb | 9 + .../features/sidebar_shared_examples.rb | 20 +- .../layouts/header/_new_dropdown.haml_spec.rb | 1 + 42 files changed, 1054 insertions(+), 483 deletions(-) create mode 100644 spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js create mode 100644 spec/frontend/admin/analytics/devops_score/mock_data.js create mode 100644 spec/frontend/issuable/components/status_box_spec.js delete mode 100644 spec/frontend/merge_request/components/status_box_spec.js create mode 100644 spec/frontend/projects/compare/components/mock_data.js create mode 100644 spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js delete mode 100644 spec/frontend/sidebar/sidebar_subscriptions_spec.js create mode 100644 spec/graphql/types/ci/runner_type_spec.rb create mode 100644 spec/graphql/types/permission_types/ci/job_spec.rb create mode 100644 spec/helpers/dev_ops_report_helper_spec.rb delete mode 100644 spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb create mode 100644 spec/requests/api/graphql/ci/runner_spec.rb (limited to 'spec') diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 21124299b25..5fc5cdfc9b9 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -73,39 +73,74 @@ RSpec.describe Oauth::AuthorizationsController do include_examples 'OAuth Authorizations require confirmed user' include_examples "Implicit grant can't be used in confidential application" - context 'when the user is confirmed' do - let(:confirmed_at) { 1.hour.ago } + context 'rendering of views based on the ownership of the application' do + shared_examples 'render views' do + render_views - context 'without valid params' do - it 'returns 200 code and renders error view' do - get :new + it 'returns 200 and renders view with correct info', :aggregate_failures do + subject expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template('doorkeeper/authorizations/error') + expect(response.body).to include(application.owner.name) + expect(response).to render_template('doorkeeper/authorizations/new') end end - context 'with valid params' do - render_views + subject { get :new, params: params } - it 'returns 200 code and renders view' do - subject + context 'when auth app owner is a user' do + context 'with valid params' do + it_behaves_like 'render views' + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template('doorkeeper/authorizations/new') + context 'when auth app owner is a group' do + let(:group) { create(:group) } + + context 'when auth app owner is a root group' do + let(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') } + + it_behaves_like 'render views' + end + + context 'when auth app owner is a subgroup' do + let(:subgroup) { create(:group, parent: group) } + let(:application) { create(:oauth_application, owner_id: subgroup.id, owner_type: 'Namespace') } + + it_behaves_like 'render views' end + end - it 'deletes session.user_return_to and redirects when skip authorization' do - application.update!(trusted: true) - request.session['user_return_to'] = 'http://example.com' + context 'when there is no owner associated' do + let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) } + it 'renders view' do subject - expect(request.session['user_return_to']).to be_nil - expect(response).to have_gitlab_http_status(:found) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/new') end end end + + context 'without valid params' do + it 'returns 200 code and renders error view' do + get :new + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/error') + end + end + + it 'deletes session.user_return_to and redirects when skip authorization' do + application.update!(trusted: true) + request.session['user_return_to'] = 'http://example.com' + + subject + + expect(request.session['user_return_to']).to be_nil + expect(response).to have_gitlab_http_status(:found) + end end describe 'POST #create' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 80392a2fece..f4f1e1bcbda 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -194,7 +194,7 @@ FactoryBot.define do filename, content, message: "Automatically created file #{filename}", - branch_name: project.default_branch_or_master + branch_name: project.default_branch || 'master' ) end end diff --git a/spec/features/admin/admin_dev_ops_report_spec.rb b/spec/features/admin/admin_dev_ops_report_spec.rb index a05fa0640d8..33f984af807 100644 --- a/spec/features/admin/admin_dev_ops_report_spec.rb +++ b/spec/features/admin/admin_dev_ops_report_spec.rb @@ -53,15 +53,13 @@ RSpec.describe 'DevOps Report page', :js do end context 'when there is data to display' do - it 'shows numbers for each metric' do + it 'shows the DevOps Score app' do stub_application_setting(usage_ping_enabled: true) create(:dev_ops_report_metric) visit admin_dev_ops_report_path - expect(page).to have_content( - 'Issues created per active user 1.2 You 9.3 Lead 13.3%' - ) + expect(page).to have_selector('[data-testid="devops-score-app"]') end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 27b75d15d3b..a43946925bf 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -439,6 +439,35 @@ RSpec.describe 'Group' do end end + describe 'new_repo experiment' do + let_it_be(:group) { create_default(:group) } + + it 'when in candidate renders "project/repository"' do + stub_experiments(new_repo: :candidate) + + visit group_path(group) + + find('li.header-new.dropdown').click + + page.within('li.header-new.dropdown') do + expect(page).to have_selector('a', text: 'New project/repository') + end + end + + it 'when in control renders "project/repository"' do + stub_experiments(new_repo: :control) + + visit group_path(group) + + find('li.header-new.dropdown').click + + page.within('li.header-new.dropdown') do + expect(page).to have_selector('a', text: 'New project') + expect(page).to have_no_selector('a', text: 'New project/repository') + end + end + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index d91c187c840..35f4b415463 100644 --- a/spec/features/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb @@ -32,8 +32,8 @@ RSpec.describe "User toggles subscription", :js do let(:project) { create(:project_empty_repo, :public, emails_disabled: true) } it 'is disabled' do - expect(page).to have_content('Notifications have been disabled by the project or group owner') - expect(page).not_to have_selector('[data-testid="subscription-toggle"]') + expect(page).to have_content('Disabled by project owner') + expect(page).to have_button('Notifications', class: 'is-disabled') end end end diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js new file mode 100644 index 00000000000..09979694e07 --- /dev/null +++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js @@ -0,0 +1,91 @@ +import { GlTable, GlBadge } from '@gitlab/ui'; +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import DevopsScore from '~/analytics/devops_report/components/devops_score.vue'; +import { createdAt, cards, averageScore, devopsScoreTableHeaders } from '../mock_data'; + +describe('DevopsScore', () => { + let wrapper; + + const createComponent = () => { + wrapper = extendedWrapper( + mount(DevopsScore, { + provide: { + devopsScoreMetrics: { + createdAt, + cards, + averageScore, + }, + }, + }), + ); + }; + + const findTable = () => wrapper.find(GlTable); + const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`); + const findUsageCol = () => findCol('usageCol'); + + beforeEach(() => { + createComponent(); + }); + + it('displays the title note', () => { + expect(wrapper.findByTestId('devops-score-note-text').text()).toBe( + 'DevOps score metrics are based on usage over the last 30 days. Last updated: 2020-06-29 08:16.', + ); + }); + + it('displays the single stat section', () => { + const component = wrapper.find(GlSingleStat); + + expect(component.exists()).toBe(true); + expect(component.props('value')).toBe(averageScore.value); + }); + + describe('devops score table', () => { + it('displays the table', () => { + expect(findTable().exists()).toBe(true); + }); + + describe('table headings', () => { + let headers; + + beforeEach(() => { + headers = findTable().findAll("[data-testid='header']"); + }); + + it('displays the correct number of headings', () => { + expect(headers).toHaveLength(devopsScoreTableHeaders.length); + }); + + describe.each(devopsScoreTableHeaders)('header fields', ({ label, index }) => { + let headerWrapper; + + beforeEach(() => { + headerWrapper = headers.at(index); + }); + + it(`displays the correct table heading text for "${label}"`, () => { + expect(headerWrapper.text()).toContain(label); + }); + }); + }); + + describe('table columns', () => { + describe('Your usage', () => { + it('displays the corrrect value', () => { + expect(findUsageCol().text()).toContain('3.2'); + }); + + it('displays the corrrect badge', () => { + const badge = findUsageCol().find(GlBadge); + + expect(badge.exists()).toBe(true); + expect(badge.props('variant')).toBe('muted'); + expect(badge.text()).toBe('Low'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/admin/analytics/devops_score/mock_data.js b/spec/frontend/admin/analytics/devops_score/mock_data.js new file mode 100644 index 00000000000..358568c6e39 --- /dev/null +++ b/spec/frontend/admin/analytics/devops_score/mock_data.js @@ -0,0 +1,42 @@ +export const averageScore = { + value: '10', + scoreLevel: { + label: 'High', + icon: 'check-circle', + variant: 'success', + }, +}; + +export const cards = [ + { + title: 'Issues created per active user', + usage: '3.2', + leadInstance: '10.2', + score: '0', + scoreLevel: { + label: 'Low', + variant: 'muted', + }, + }, +]; + +export const createdAt = '2020-06-29 08:16'; + +export const devopsScoreTableHeaders = [ + { + index: 0, + label: '', + }, + { + index: 1, + label: 'Your usage', + }, + { + index: 2, + label: 'Leader usage', + }, + { + index: 3, + label: 'Score', + }, +]; diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index 7f949739891..01c99a02db2 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -6,9 +6,9 @@ import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; -import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { ISSUABLE } from '~/boards/constants'; +import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; describe('BoardContentSidebar', () => { @@ -111,7 +111,7 @@ describe('BoardContentSidebar', () => { }); it('renders BoardSidebarSubscription', () => { - expect(wrapper.find(BoardSidebarSubscription).exists()).toBe(true); + expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true); }); it('renders BoardSidebarMilestoneSelect', () => { diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js new file mode 100644 index 00000000000..990fac67f7e --- /dev/null +++ b/spec/frontend/issuable/components/status_box_spec.js @@ -0,0 +1,71 @@ +import { GlSprintf } 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 } }); +} + +const testCases = [ + { + name: 'Open', + state: 'opened', + class: 'status-box-open', + icon: 'issue-open-m', + }, + { + name: 'Open', + state: 'locked', + class: 'status-box-open', + icon: 'issue-open-m', + }, + { + name: 'Closed', + state: 'closed', + class: 'status-box-mr-closed', + icon: 'issue-close', + }, + { + name: 'Merged', + state: 'merged', + class: 'status-box-mr-merged', + icon: 'git-merge', + }, +]; + +describe('Merge request status box component', () => { + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + testCases.forEach((testCase) => { + describe(`when merge request is ${testCase.name}`, () => { + it('renders human readable test', () => { + factory({ + initialState: testCase.state, + }); + + expect(wrapper.text()).toContain(testCase.name); + }); + + it('sets css class', () => { + factory({ + initialState: testCase.state, + }); + + expect(wrapper.classes()).toContain(testCase.class); + }); + + it('renders icon', () => { + factory({ + initialState: testCase.state, + }); + + expect(wrapper.find('[data-testid="status-icon"]').props('name')).toBe(testCase.icon); + }); + }); + }); +}); diff --git a/spec/frontend/merge_request/components/status_box_spec.js b/spec/frontend/merge_request/components/status_box_spec.js deleted file mode 100644 index de0f3574ab2..00000000000 --- a/spec/frontend/merge_request/components/status_box_spec.js +++ /dev/null @@ -1,87 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import StatusBox from '~/merge_request/components/status_box.vue'; -import mrEventHub from '~/merge_request/eventhub'; - -let wrapper; - -function factory(propsData) { - wrapper = shallowMount(StatusBox, { propsData, stubs: { GlSprintf } }); -} - -const testCases = [ - { - name: 'Open', - state: 'opened', - class: 'status-box-open', - icon: 'issue-open-m', - }, - { - name: 'Open', - state: 'locked', - class: 'status-box-open', - icon: 'issue-open-m', - }, - { - name: 'Closed', - state: 'closed', - class: 'status-box-mr-closed', - icon: 'issue-close', - }, - { - name: 'Merged', - state: 'merged', - class: 'status-box-mr-merged', - icon: 'git-merge', - }, -]; - -describe('Merge request status box component', () => { - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - testCases.forEach((testCase) => { - describe(`when merge request is ${testCase.name}`, () => { - it('renders human readable test', () => { - factory({ - initialState: testCase.state, - }); - - expect(wrapper.text()).toContain(testCase.name); - }); - - it('sets css class', () => { - factory({ - initialState: testCase.state, - }); - - expect(wrapper.classes()).toContain(testCase.class); - }); - - it('renders icon', () => { - factory({ - initialState: testCase.state, - }); - - expect(wrapper.find('[data-testid="status-icon"]').props('name')).toBe(testCase.icon); - }); - }); - }); - - it('updates with eventhub event', async () => { - factory({ - initialState: 'opened', - }); - - expect(wrapper.text()).toContain('Open'); - - mrEventHub.$emit('mr.state.updated', { state: 'closed' }); - - await nextTick(); - - expect(wrapper.text()).toContain('Closed'); - }); -}); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index b717bab7c3f..b140eea9439 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -436,6 +436,7 @@ describe('issue_comment_form component', () => { await findCloseReopenButton().trigger('click'); + await wrapper.vm.$nextTick; await wrapper.vm.$nextTick; expect(flash).toHaveBeenCalledWith( @@ -471,6 +472,7 @@ describe('issue_comment_form component', () => { await findCloseReopenButton().trigger('click'); + await wrapper.vm.$nextTick; await wrapper.vm.$nextTick; expect(flash).toHaveBeenCalledWith( @@ -489,6 +491,8 @@ describe('issue_comment_form component', () => { await findCloseReopenButton().trigger('click'); + await wrapper.vm.$nextTick(); + expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js index 6de06e4373c..7989a6f3d74 100644 --- a/spec/frontend/projects/compare/components/app_spec.js +++ b/spec/frontend/projects/compare/components/app_spec.js @@ -2,26 +2,19 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import CompareApp from '~/projects/compare/components/app.vue'; import RevisionCard from '~/projects/compare/components/revision_card.vue'; +import { appDefaultProps as defaultProps } from './mock_data'; jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); -const projectCompareIndexPath = 'some/path'; -const refsProjectPath = 'some/refs/path'; -const paramsFrom = 'master'; -const paramsTo = 'master'; - describe('CompareApp component', () => { let wrapper; + const findSourceRevisionCard = () => wrapper.find('[data-testid="sourceRevisionCard"]'); + const findTargetRevisionCard = () => wrapper.find('[data-testid="targetRevisionCard"]'); const createComponent = (props = {}) => { wrapper = shallowMount(CompareApp, { propsData: { - projectCompareIndexPath, - refsProjectPath, - paramsFrom, - paramsTo, - projectMergeRequestPath: '', - createMrPath: '', + ...defaultProps, ...props, }, }); @@ -39,16 +32,16 @@ describe('CompareApp component', () => { it('renders component with prop', () => { expect(wrapper.props()).toEqual( expect.objectContaining({ - projectCompareIndexPath, - refsProjectPath, - paramsFrom, - paramsTo, + projectCompareIndexPath: defaultProps.projectCompareIndexPath, + refsProjectPath: defaultProps.refsProjectPath, + paramsFrom: defaultProps.paramsFrom, + paramsTo: defaultProps.paramsTo, }), ); }); it('contains the correct form attributes', () => { - expect(wrapper.attributes('action')).toBe(projectCompareIndexPath); + expect(wrapper.attributes('action')).toBe(defaultProps.projectCompareIndexPath); expect(wrapper.attributes('method')).toBe('POST'); }); @@ -87,6 +80,58 @@ describe('CompareApp component', () => { }); }); + it('sets the selected project when the "selectProject" event is emitted', async () => { + const project = { + name: 'some-to-name', + id: '1', + }; + + findTargetRevisionCard().vm.$emit('selectProject', { + direction: 'to', + project, + }); + + await wrapper.vm.$nextTick(); + + expect(findTargetRevisionCard().props('selectedProject')).toEqual( + expect.objectContaining(project), + ); + }); + + it('sets the selected revision when the "selectRevision" event is emitted', async () => { + const revision = 'some-revision'; + + findTargetRevisionCard().vm.$emit('selectRevision', { + direction: 'to', + revision, + }); + + await wrapper.vm.$nextTick(); + + expect(findSourceRevisionCard().props('paramsBranch')).toBe(revision); + }); + + describe('swap revisions button', () => { + const findSwapRevisionsButton = () => wrapper.find('[data-testid="swapRevisionsButton"]'); + + it('renders the swap revisions button', () => { + expect(findSwapRevisionsButton().exists()).toBe(true); + }); + + it('has the correct text', () => { + expect(findSwapRevisionsButton().text()).toBe('Swap revisions'); + }); + + it('swaps revisions when clicked', async () => { + findSwapRevisionsButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(findTargetRevisionCard().props('paramsBranch')).toBe(defaultProps.paramsTo); + expect(findSourceRevisionCard().props('paramsBranch')).toBe(defaultProps.paramsFrom); + }); + }); + describe('merge request buttons', () => { const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]'); const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]'); diff --git a/spec/frontend/projects/compare/components/mock_data.js b/spec/frontend/projects/compare/components/mock_data.js new file mode 100644 index 00000000000..61309928c26 --- /dev/null +++ b/spec/frontend/projects/compare/components/mock_data.js @@ -0,0 +1,37 @@ +const refsProjectPath = 'some/refs/path'; +const paramsName = 'to'; +const paramsBranch = 'main'; +const defaultProject = { + name: 'some-to-name', + id: '1', +}; + +export const appDefaultProps = { + projectCompareIndexPath: 'some/path', + projectMergeRequestPath: '', + projects: [defaultProject], + paramsFrom: 'main', + paramsTo: 'target/branch', + createMrPath: '', + refsProjectPath, + defaultProject, +}; + +export const revisionCardDefaultProps = { + selectedProject: defaultProject, + paramsBranch, + revisionText: 'Source', + refsProjectPath, + paramsName, +}; + +export const repoDropdownDefaultProps = { + selectedProject: defaultProject, + paramsName, +}; + +export const revisionDropdownDefaultProps = { + refsProjectPath, + paramsBranch, + paramsName, +}; diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js index df8fea8fd32..27a7a32ebca 100644 --- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js @@ -1,37 +1,17 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue'; - -const defaultProps = { - paramsName: 'to', -}; - -const projectToId = '1'; -const projectToName = 'some-to-name'; -const projectFromId = '2'; -const projectFromName = 'some-from-name'; - -const defaultProvide = { - projectTo: { id: projectToId, name: projectToName }, - projectsFrom: [ - { id: projectFromId, name: projectFromName }, - { id: 3, name: 'some-from-another-name' }, - ], -}; +import { revisionCardDefaultProps as defaultProps } from './mock_data'; describe('RepoDropdown component', () => { let wrapper; - const createComponent = (props = {}, provide = {}) => { + const createComponent = (props = {}) => { wrapper = shallowMount(RepoDropdown, { propsData: { ...defaultProps, ...props, }, - provide: { - ...defaultProvide, - ...provide, - }, }); }; @@ -49,11 +29,11 @@ describe('RepoDropdown component', () => { }); it('set hidden input', () => { - expect(findHiddenInput().attributes('value')).toBe(projectToId); + expect(findHiddenInput().attributes('value')).toBe(defaultProps.selectedProject.id); }); it('displays the project name in the disabled dropdown', () => { - expect(findGlDropdown().props('text')).toBe(projectToName); + expect(findGlDropdown().props('text')).toBe(defaultProps.selectedProject.name); expect(findGlDropdown().props('disabled')).toBe(true); }); @@ -66,31 +46,39 @@ describe('RepoDropdown component', () => { describe('Target Revision', () => { beforeEach(() => { - createComponent({ paramsName: 'from' }); + const projects = [ + { + name: 'some-to-name', + id: '1', + }, + ]; + + createComponent({ paramsName: 'from', projects }); }); it('set hidden input of the selected project', () => { - expect(findHiddenInput().attributes('value')).toBe(projectToId); + expect(findHiddenInput().attributes('value')).toBe(defaultProps.selectedProject.id); }); it('displays matching project name of the source revision initially in the dropdown', () => { - expect(findGlDropdown().props('text')).toBe(projectToName); + expect(findGlDropdown().props('text')).toBe(defaultProps.selectedProject.name); }); - it('updates the hiddin input value when onClick method is triggered', async () => { - const repoId = '100'; + it('updates the hidden input value when onClick method is triggered', async () => { + const repoId = '1'; wrapper.vm.onClick({ id: repoId }); await wrapper.vm.$nextTick(); expect(findHiddenInput().attributes('value')).toBe(repoId); }); - it('emits `changeTargetProject` event when another target project is selected', async () => { - const index = 1; - const { projectsFrom } = defaultProvide; - findGlDropdown().findAll(GlDropdownItem).at(index).vm.$emit('click'); + it('emits `selectProject` event when another target project is selected', async () => { + findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click'); await wrapper.vm.$nextTick(); - expect(wrapper.emitted('changeTargetProject')[0][0]).toEqual(projectsFrom[index].name); + expect(wrapper.emitted('selectProject')[0][0]).toEqual({ + direction: 'from', + project: { id: '1', name: 'some-to-name' }, + }); }); }); }); diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js index 83f858f4454..57906045337 100644 --- a/spec/frontend/projects/compare/components/revision_card_spec.js +++ b/spec/frontend/projects/compare/components/revision_card_spec.js @@ -3,13 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue'; import RevisionCard from '~/projects/compare/components/revision_card.vue'; import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; - -const defaultProps = { - refsProjectPath: 'some/refs/path', - revisionText: 'Source', - paramsName: 'to', - paramsBranch: 'master', -}; +import { revisionCardDefaultProps as defaultProps } from './mock_data'; describe('RepoDropdown component', () => { let wrapper; diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js index aab9607ceae..118bb68585e 100644 --- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js @@ -1,15 +1,10 @@ -import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; - -const defaultProps = { - refsProjectPath: 'some/refs/path', - paramsName: 'from', - paramsBranch: 'master', -}; +import { revisionDropdownDefaultProps as defaultProps } from './mock_data'; jest.mock('~/flash'); @@ -142,4 +137,17 @@ describe('RevisionDropdown component', () => { expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch); }); }); + + it('emits `selectRevision` event when another revision is selected', async () => { + createComponent(); + wrapper.vm.branches = ['some-branch']; + await wrapper.vm.$nextTick(); + + findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click'); + + expect(wrapper.emitted('selectRevision')[0][0]).toEqual({ + direction: 'to', + revision: 'some-branch', + }); + }); }); diff --git a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js new file mode 100644 index 00000000000..549ab99c6af --- /dev/null +++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js @@ -0,0 +1,131 @@ +import { GlIcon, GlToggle } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; +import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql'; +import { issueSubscriptionsResponse } from '../../mock_data'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +describe('Sidebar Subscriptions Widget', () => { + let wrapper; + let fakeApollo; + + const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findToggle = () => wrapper.findComponent(GlToggle); + const findIcon = () => wrapper.findComponent(GlIcon); + + const createComponent = ({ + subscriptionsQueryHandler = jest.fn().mockResolvedValue(issueSubscriptionsResponse()), + } = {}) => { + fakeApollo = createMockApollo([[issueSubscribedQuery, subscriptionsQueryHandler]]); + + wrapper = shallowMount(SidebarSubscriptionWidget, { + apolloProvider: fakeApollo, + provide: { + canUpdate: true, + }, + propsData: { + fullPath: 'group/project', + iid: '1', + issuableType: 'issue', + }, + stubs: { + SidebarEditableItem, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + it('passes a `loading` prop as true to editable item when query is loading', () => { + createComponent(); + + expect(findEditableItem().props('loading')).toBe(true); + }); + + describe('when user is not subscribed to the issue', () => { + beforeEach(() => { + createComponent(); + return waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findEditableItem().props('loading')).toBe(false); + }); + + it('toggle is unchecked', () => { + expect(findToggle().props('value')).toBe(false); + }); + + it('emits `subscribedUpdated` event with a `false` payload', () => { + expect(wrapper.emitted('subscribedUpdated')).toEqual([[false]]); + }); + }); + + describe('when user is subscribed to the issue', () => { + beforeEach(() => { + createComponent({ + subscriptionsQueryHandler: jest.fn().mockResolvedValue(issueSubscriptionsResponse(true)), + }); + return waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findEditableItem().props('loading')).toBe(false); + }); + + it('toggle is checked', () => { + expect(findToggle().props('value')).toBe(true); + }); + + it('emits `subscribedUpdated` event with a `true` payload', () => { + expect(wrapper.emitted('subscribedUpdated')).toEqual([[true]]); + }); + }); + + describe('when emails are disabled', () => { + it('toggle is disabled and off when user is subscribed', async () => { + createComponent({ + subscriptionsQueryHandler: jest + .fn() + .mockResolvedValue(issueSubscriptionsResponse(true, true)), + }); + await waitForPromises(); + + expect(findIcon().props('name')).toBe('notifications-off'); + expect(findToggle().props('disabled')).toBe(true); + }); + + it('toggle is disabled and off when user is not subscribed', async () => { + createComponent({ + subscriptionsQueryHandler: jest + .fn() + .mockResolvedValue(issueSubscriptionsResponse(false, true)), + }); + await waitForPromises(); + + expect(findIcon().props('name')).toBe('notifications-off'); + expect(findToggle().props('disabled')).toBe(true); + }); + }); + + it('displays a flash message when query is rejected', async () => { + createComponent({ + subscriptionsQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'), + }); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 38fce53e398..f51d2f3d459 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -275,6 +275,20 @@ export const issueReferenceResponse = (reference) => ({ }, }); +export const issueSubscriptionsResponse = (subscribed = false, emailsDisabled = false) => ({ + data: { + workspace: { + __typename: 'Project', + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/4', + subscribed, + emailsDisabled, + }, + }, + }, +}); + export const issuableQueryResponse = { data: { workspace: { diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js deleted file mode 100644 index d900fde7e70..00000000000 --- a/spec/frontend/sidebar/sidebar_subscriptions_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue'; -import SidebarService from '~/sidebar/services/sidebar_service'; -import SidebarMediator from '~/sidebar/sidebar_mediator'; -import SidebarStore from '~/sidebar/stores/sidebar_store'; -import Mock from './mock_data'; - -describe('Sidebar Subscriptions', () => { - let wrapper; - let mediator; - - beforeEach(() => { - mediator = new SidebarMediator(Mock.mediator); - wrapper = shallowMount(SidebarSubscriptions, { - propsData: { - mediator, - }, - }); - }); - - afterEach(() => { - wrapper.destroy(); - SidebarService.singleton = null; - SidebarStore.singleton = null; - SidebarMediator.singleton = null; - }); - - it('calls the mediator toggleSubscription on event', () => { - const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve()); - - wrapper.vm.onToggleSubscription(); - - expect(spy).toHaveBeenCalled(); - spy.mockRestore(); - }); -}); diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index a1107bae630..8b092584efe 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Types::Ci::JobType do specify { expect(described_class.graphql_name).to eq('CiJob') } specify { expect(described_class).to require_graphql_authorizations(:read_commit_status) } + specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Job) } it 'exposes the expected fields' do expected_fields = %i[ @@ -38,6 +39,7 @@ RSpec.describe Types::Ci::JobType do status tags triggered + userPermissions ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb new file mode 100644 index 00000000000..dfe4a30c5b7 --- /dev/null +++ b/spec/graphql/types/ci/runner_type_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::RunnerType do + specify { expect(described_class.graphql_name).to eq('CiRunner') } + + it 'contains attributes related to a runner' do + expected_fields = %w[ + id description contacted_at maximum_timeout access_level active status + version short_sha revision locked run_untagged ip_address runner_type tag_list + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/permission_types/ci/job_spec.rb b/spec/graphql/types/permission_types/ci/job_spec.rb new file mode 100644 index 00000000000..e4bc5419070 --- /dev/null +++ b/spec/graphql/types/permission_types/ci/job_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::PermissionTypes::Ci::Job do + it 'has expected permission fields' do + expected_permissions = [ + :read_job_artifacts, :read_build, :update_build + ] + + expect(described_class).to have_graphql_fields(expected_permissions).only + end +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index a877e19c069..f0e80fa8f14 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -24,6 +24,7 @@ RSpec.describe GitlabSchema.types['Query'] do merge_request usage_trends_measurements runner_platforms + runner ] expect(described_class).to have_graphql_fields(*expected_fields).at_least @@ -84,6 +85,12 @@ RSpec.describe GitlabSchema.types['Query'] do end end + describe 'runner field' do + subject { described_class.fields['runner'] } + + it { is_expected.to have_graphql_type(Types::Ci::RunnerType) } + end + describe 'runner_platforms field' do subject { described_class.fields['runnerPlatforms'] } diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index beffa4cf60e..5194285b965 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -313,4 +313,37 @@ RSpec.describe AuthHelper do it { is_expected.to be_falsey } end end + + describe '#auth_app_owner_text' do + shared_examples 'generates text with the correct info' do + it 'includes the name of the application owner' do + auth_app_owner_text = helper.auth_app_owner_text(owner) + + expect(auth_app_owner_text).to include(owner.name) + expect(auth_app_owner_text).to include(path_to_owner) + end + end + + context 'when owner is a user' do + let_it_be(:owner) { create(:user) } + + let(:path_to_owner) { user_path(owner) } + + it_behaves_like 'generates text with the correct info' + end + + context 'when owner is a group' do + let_it_be(:owner) { create(:group) } + + let(:path_to_owner) { user_path(owner) } + + it_behaves_like 'generates text with the correct info' + end + + context 'when the user is missing' do + it 'returns nil' do + expect(helper.auth_app_owner_text(nil)).to be(nil) + end + end + end end diff --git a/spec/helpers/dev_ops_report_helper_spec.rb b/spec/helpers/dev_ops_report_helper_spec.rb new file mode 100644 index 00000000000..769400521f8 --- /dev/null +++ b/spec/helpers/dev_ops_report_helper_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DevOpsReportHelper do + subject { DevOpsReport::MetricPresenter.new(metric) } + + let(:metric) { build(:dev_ops_report_metric, created_at: DateTime.new(2021, 4, 3, 2, 1, 0) ) } + + describe '#devops_score_metrics' do + let(:devops_score_metrics) { helper.devops_score_metrics(subject) } + + it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status-alert", label: "Moderate", variant: "warning" }, value: "55.9" } ) } + + it { expect(devops_score_metrics[:cards].first).to eq({ leadInstance: "9.3", score: "13.3", scoreLevel: { label: "Low", variant: "muted" }, title: "Issues created per active user", usage: "1.2" } ) } + it { expect(devops_score_metrics[:cards].second).to eq({ leadInstance: "30.3", score: "92.7", scoreLevel: { label: "High", variant: "success" }, title: "Comments created per active user", usage: "28.1" } ) } + it { expect(devops_score_metrics[:cards].fourth).to eq({ leadInstance: "5.2", score: "62.4", scoreLevel: { label: "Moderate", variant: "neutral" }, title: "Boards created per active user", usage: "3.3" } ) } + + it { expect(devops_score_metrics[:createdAt]).to eq("2021-04-03 02:01") } + + describe 'with low average score' do + let(:low_metric) { double(average_percentage_score: 2, cards: subject.cards, created_at: subject.created_at) } + let(:devops_score_metrics) { helper.devops_score_metrics(low_metric) } + + it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status-failed", label: "Low", variant: "danger" }, value: "2.0" } ) } + end + + describe 'with high average score' do + let(:high_metric) { double(average_percentage_score: 82, cards: subject.cards, created_at: subject.created_at) } + let(:devops_score_metrics) { helper.devops_score_metrics(high_metric) } + + it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status_success_solid", label: "High", variant: "success" }, value: "82.0" } ) } + end + end +end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index f0f09408249..46073b37a3b 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -160,7 +160,7 @@ RSpec.describe UsersHelper do it 'returns the "It\'s You" badge' do badges = helper.user_badges_in_admin_section(user) - expect(filter_ee_badges(badges)).to eq([text: "It's you!", variant: nil]) + expect(filter_ee_badges(badges)).to eq([text: "It's you!", variant: "muted"]) end end diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb index 06062634015..4f40830a15c 100644 --- a/spec/lib/api/entities/release_spec.rb +++ b/spec/lib/api/entities/release_spec.rb @@ -54,18 +54,41 @@ RSpec.describe API::Entities::Release do subject(:description_html) { entity.as_json['description_html'] } - it 'renders special references if current user has access' do - project.add_reporter(user) + it 'is inexistent' do + expect(description_html).to be_nil + end + + context 'when remove_description_html_in_release_api feature flag is disabled' do + before do + stub_feature_flags(remove_description_html_in_release_api: false) + end + + it 'renders special references if current user has access' do + project.add_reporter(user) + + expect(description_html).to include(issue_path) + expect(description_html).to include(issue_title) + end - expect(description_html).to include(issue_path) - expect(description_html).to include(issue_title) + it 'does not render special references if current user has no access' do + project.add_guest(user) + + expect(description_html).not_to include(issue_path) + expect(description_html).not_to include(issue_title) + end end - it 'does not render special references if current user has no access' do - project.add_guest(user) + context 'when remove_description_html_in_release_api_override feature flag is enabled' do + before do + stub_feature_flags(remove_description_html_in_release_api_override: project) + end - expect(description_html).not_to include(issue_path) - expect(description_html).not_to include(issue_title) + it 'renders special references if current user has access' do + project.add_reporter(user) + + expect(description_html).to include(issue_path) + expect(description_html).to include(issue_title) + end end end end diff --git a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb deleted file mode 100644 index 1c55b50ea3f..00000000000 --- a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb +++ /dev/null @@ -1,241 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, schema: 20200204113223 do - let(:users_table) { table(:users) } - let(:namespaces_table) { table(:namespaces) } - let(:projects_table) { table(:projects) } - let(:project_authorizations_table) { table(:project_authorizations) } - let(:members_table) { table(:members) } - let(:group_group_links) { table(:group_group_links) } - let(:project_group_links) { table(:project_group_links) } - - let(:user) { users_table.create!(id: 1, email: 'user@example.com', projects_limit: 10) } - let(:group) { namespaces_table.create!(type: 'Group', name: 'group', path: 'group') } - - subject { described_class.new.perform([user.id]) } - - context 'missing authorization' do - context 'personal project' do - before do - user_namespace = namespaces_table.create!(owner_id: user.id, name: 'User', path: 'user') - projects_table.create!(id: 1, - name: 'personal-project', - path: 'personal-project', - visibility_level: 0, - namespace_id: user_namespace.id) - end - - it 'creates correct authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 40)])) - end - end - - context 'group membership' do - before do - projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: group.id) - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 20, notification_level: 3) - end - - it 'creates correct authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) - end - end - - context 'inherited group membership' do - before do - sub_group = namespaces_table.create!(type: 'Group', name: 'subgroup', - path: 'subgroup', parent_id: group.id) - projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: sub_group.id) - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 20, notification_level: 3) - end - - it 'creates correct authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) - end - end - - context 'project membership' do - before do - project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: group.id) - members_table.create!(user_id: user.id, source_id: project.id, source_type: 'Project', - type: 'ProjectMember', access_level: 20, notification_level: 3) - end - - it 'creates correct authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) - end - end - - context 'shared group' do - before do - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 30, notification_level: 3) - - shared_group = namespaces_table.create!(type: 'Group', name: 'shared group', - path: 'shared-group') - projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0, - namespace_id: shared_group.id) - - group_group_links.create!(shared_group_id: shared_group.id, shared_with_group_id: group.id, - group_access: 20) - end - - it 'creates correct authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) - end - end - - context 'shared project' do - before do - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 30, notification_level: 3) - - another_group = namespaces_table.create!(type: 'Group', name: 'another group', path: 'another-group') - shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project', - visibility_level: 0, namespace_id: another_group.id) - - project_group_links.create!(project_id: shared_project.id, group_id: group.id, group_access: 20) - end - - it 'creates correct authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) - end - end - end - - context 'unapproved access requests' do - context 'group membership' do - before do - projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: group.id) - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 20, requested_at: Time.now, notification_level: 3) - end - - it 'does not create authorization' do - expect { subject }.not_to change { project_authorizations_table.count }.from(0) - end - end - - context 'inherited group membership' do - before do - sub_group = namespaces_table.create!(type: 'Group', name: 'subgroup', path: 'subgroup', - parent_id: group.id) - projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: sub_group.id) - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 20, requested_at: Time.now, notification_level: 3) - end - - it 'does not create authorization' do - expect { subject }.not_to change { project_authorizations_table.count }.from(0) - end - end - - context 'project membership' do - before do - project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: group.id) - members_table.create!(user_id: user.id, source_id: project.id, source_type: 'Project', - type: 'ProjectMember', access_level: 20, requested_at: Time.now, notification_level: 3) - end - - it 'does not create authorization' do - expect { subject }.not_to change { project_authorizations_table.count }.from(0) - end - end - - context 'shared group' do - before do - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 30, requested_at: Time.now, notification_level: 3) - - shared_group = namespaces_table.create!(type: 'Group', name: 'shared group', - path: 'shared-group') - projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0, - namespace_id: shared_group.id) - - group_group_links.create!(shared_group_id: shared_group.id, shared_with_group_id: group.id, - group_access: 20) - end - - it 'does not create authorization' do - expect { subject }.not_to change { project_authorizations_table.count }.from(0) - end - end - - context 'shared project' do - before do - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 30, requested_at: Time.now, notification_level: 3) - - another_group = namespaces_table.create!(type: 'Group', name: 'another group', path: 'another-group') - shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project', - visibility_level: 0, namespace_id: another_group.id) - - project_group_links.create!(project_id: shared_project.id, group_id: group.id, group_access: 20) - end - - it 'does not create authorization' do - expect { subject }.not_to change { project_authorizations_table.count }.from(0) - end - end - end - - context 'incorrect authorization' do - before do - project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: group.id) - members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', - type: 'GroupMember', access_level: 30, notification_level: 3) - - project_authorizations_table.create!(user_id: user.id, project_id: project.id, - access_level: 10) - end - - it 'fixes authorization' do - expect { subject }.not_to change { project_authorizations_table.count }.from(1) - expect(project_authorizations_table.all).to( - match_array([have_attributes(user_id: 1, project_id: 1, access_level: 30)])) - end - end - - context 'unwanted authorization' do - before do - project = projects_table.create!(name: 'group-project', path: 'group-project', - visibility_level: 0, namespace_id: group.id) - - project_authorizations_table.create!(user_id: user.id, project_id: project.id, - access_level: 10) - end - - it 'deletes authorization' do - expect { subject }.to change { project_authorizations_table.count }.from(1).to(0) - end - end - - context 'deleted user' do - it 'does not fail' do - expect { described_class.new.perform([non_existing_record_id]) }.not_to raise_error - end - end -end diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb index 09e6e5a03bb..df33b4896a4 100644 --- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb @@ -13,6 +13,7 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do subject do described_class.create(relation_sym: relation_sym, relation_hash: relation_hash, + relation_index: 1, object_builder: Gitlab::ImportExport::Project::ObjectBuilder, members_mapper: members_mapper, user: user, diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb index 40cf75779b6..7ad5d3d846c 100644 --- a/spec/lib/gitlab/import_export/config_spec.rb +++ b/spec/lib/gitlab/import_export/config_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::ImportExport::Config do expect { subject }.not_to raise_error expect(subject).to be_a(Hash) expect(subject.keys).to contain_exactly( - :tree, :excluded_attributes, :included_attributes, :methods, :preloads) + :tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders) end end end diff --git a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb index 6b2f80cc80a..63286fc0719 100644 --- a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb @@ -12,6 +12,7 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do described_class.create( relation_sym: relation_sym, relation_hash: relation_hash, + relation_index: 1, members_mapper: members_mapper, object_builder: Gitlab::ImportExport::Group::ObjectBuilder, user: importer_user, diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb index 762687beedb..a0b2faaecfe 100644 --- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb @@ -30,12 +30,14 @@ RSpec.describe Gitlab::ImportExport::JSON::StreamingSerializer do let(:json_writer) { instance_double('Gitlab::ImportExport::JSON::LegacyWriter') } let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys } let(:include) { [] } + let(:custom_orderer) { nil } let(:relations_schema) do { only: [:name, :description], include: include, - preload: { issues: nil } + preload: { issues: nil }, + export_reorder: custom_orderer } end @@ -57,19 +59,63 @@ RSpec.describe Gitlab::ImportExport::JSON::StreamingSerializer do [{ issues: { include: [] } }] end + before do + create_list(:issue, 3, project: exportable, relative_position: 10000) # ascending ids, same position positive + create_list(:issue, 3, project: exportable, relative_position: -5000) # ascending ids, same position negative + create_list(:issue, 3, project: exportable, relative_position: 0) # ascending ids, duplicate positions + create_list(:issue, 3, project: exportable, relative_position: nil) # no position + create_list(:issue, 3, :with_desc_relative_position, project: exportable ) # ascending ids, descending position + end + it 'calls json_writer.write_relation_array with proper params' do expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json)) subject.execute end - context 'relation ordering' do - before do - create_list(:issue, 5, project: exportable) + context 'default relation ordering' do + it 'orders exported issues by primary key(:id)' do + expected_issues = exportable.issues.reorder(:id).map(&:to_json) + + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, expected_issues) + + subject.execute end + end - it 'orders exported issues by primary key' do - expected_issues = exportable.issues.reorder(:id).map(&:to_json) + context 'custom relation ordering ascending' do + let(:custom_orderer) do + { + issues: { + column: :relative_position, + direction: :asc, + nulls_position: :nulls_last + } + } + end + + it 'orders exported issues by custom column(relative_position)' do + expected_issues = exportable.issues.reorder(:relative_position, :id).map(&:to_json) + + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, expected_issues) + + subject.execute + end + end + + context 'custom relation ordering descending' do + let(:custom_orderer) do + { + issues: { + column: :relative_position, + direction: :desc, + nulls_position: :nulls_first + } + } + end + + it 'orders exported issues by custom column(relative_position)' do + expected_issues = exportable.issues.order_relative_position_desc.order(id: :desc).map(&:to_json) expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, expected_issues) diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb index 56ba730e893..38e700e8f9e 100644 --- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ImportExport::Project::RelationFactory do +RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_memory_store_caching do let(:group) { create(:group) } let(:project) { create(:project, :repository, group: group) } let(:members_mapper) { double('members_mapper').as_null_object } @@ -13,6 +13,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do described_class.create( relation_sym: relation_sym, relation_hash: relation_hash, + relation_index: 1, object_builder: Gitlab::ImportExport::Project::ObjectBuilder, members_mapper: members_mapper, user: importer_user, @@ -171,6 +172,75 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do end end + context 'issue object' do + let(:relation_sym) { :issues } + + let(:exported_member) do + { + "id" => 111, + "access_level" => 30, + "source_id" => 1, + "source_type" => "Project", + "user_id" => 3, + "notification_level" => 3, + "created_at" => "2016-11-18T09:29:42.634Z", + "updated_at" => "2016-11-18T09:29:42.634Z", + "user" => { + "id" => admin.id, + "email" => admin.email, + "username" => admin.username + } + } + end + + let(:members_mapper) do + Gitlab::ImportExport::MembersMapper.new( + exported_members: [exported_member], + user: importer_user, + importable: project) + end + + let(:relation_hash) do + { + 'id' => 20, + 'target_branch' => "feature", + 'source_branch' => "feature_conflict", + 'project_id' => project.id, + 'author_id' => admin.id, + 'assignee_id' => admin.id, + 'updated_by_id' => admin.id, + 'title' => "Issue 1", + 'created_at' => "2016-06-14T15:02:36.568Z", + 'updated_at' => "2016-06-14T15:02:56.815Z", + 'state' => "opened", + 'description' => "Description", + "relative_position" => 25111 # just a random position + } + end + + it 'has preloaded project' do + expect(created_object.project).to equal(project) + end + + context 'computing relative position' do + context 'when max relative position in the hierarchy is not cached' do + it 'has computed new relative_position' do + expect(created_object.relative_position).to equal(1026) # 513*2 - ideal distance + end + end + + context 'when max relative position in the hierarchy is cached' do + before do + Rails.cache.write("import:#{project.model_name.plural}:#{project.id}:hierarchy_max_issues_relative_position", 10000) + end + + it 'has computed new relative_position' do + expect(created_object.relative_position).to equal(10000 + 1026) # 513*2 - ideal distance + end + end + end + end + context 'label object' do let(:relation_sym) { :labels } let(:relation_hash) do diff --git a/spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb index 86d5f2402f8..9dde09a7602 100644 --- a/spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationFactory do described_class.create( # rubocop:disable Rails/SaveBang relation_sym: relation_sym, relation_hash: relation_hash, + relation_index: 1, object_builder: Gitlab::ImportExport::Project::ObjectBuilder, members_mapper: members_mapper, user: importer_user, diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index a86afa9cba5..3021d92244e 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -85,7 +85,7 @@ RSpec.describe Gitlab::TreeSummary do 'long.txt', '', message: message, - branch_name: project.default_branch_or_master + branch_name: project.default_branch ) end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 8e0c79b3842..9be0ca71bcb 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -172,6 +172,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do another_project = create(:project, :repository, creator: another_user) create(:remote_mirror, project: another_project, enabled: false) create(:snippet, author: user) + create(:suggestion, note: create(:note, project: project)) end expect(described_class.usage_activity_by_stage_create({})).to include( @@ -181,7 +182,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do projects_with_disable_overriding_approvers_per_merge_request: 2, projects_without_disable_overriding_approvers_per_merge_request: 6, remote_mirrors: 2, - snippets: 2 + snippets: 2, + suggestions: 2 ) expect(described_class.usage_activity_by_stage_create(described_class.last_28_days_time_period)).to include( deploy_keys: 1, @@ -190,7 +192,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do projects_with_disable_overriding_approvers_per_merge_request: 1, projects_without_disable_overriding_approvers_per_merge_request: 3, remote_mirrors: 1, - snippets: 1 + snippets: 1, + suggestions: 1 ) end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 992b2246f01..4eabc266b40 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1384,6 +1384,16 @@ RSpec.describe Note do expect(notes.second.id).to eq(note2.id) end end + + describe '.with_suggestions' do + it 'returns the correct note' do + note_with_suggestion = create(:note, suggestions: [create(:suggestion)]) + note_without_suggestion = create(:note) + + expect(described_class.with_suggestions).to include(note_with_suggestion) + expect(described_class.with_suggestions).not_to include(note_without_suggestion) + end + end end describe '#noteable_assignee_or_author?' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5e12161f47b..55b13128c17 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6892,6 +6892,37 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#default_branch_or_main' do + let(:project) { create(:project, :repository) } + + before do + # Stubbing it as true since the FF disabled for tests globally + stub_feature_flags(main_branch_over_master: true) + end + + it 'returns default branch' do + expect(project.default_branch_or_main).to eq(project.default_branch) + end + + context 'when default branch is nil' do + let(:project) { create(:project, :empty_repo) } + + it 'returns main' do + expect(project.default_branch_or_main).to eq('main') + end + + context 'main_branch_over_master is disabled' do + before do + stub_feature_flags(main_branch_over_master: false) + end + + it 'returns master' do + expect(project.default_branch_or_main).to eq('master') + end + end + end + end + def finish_job(export_job) export_job.start export_job.finish diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb new file mode 100644 index 00000000000..e1f84d23209 --- /dev/null +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.runner(id)' do + include GraphqlHelpers + + let_it_be(:user) { create_default(:user, :admin) } + + let_it_be(:active_runner) do + create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago, + active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, + access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true) + end + + let_it_be(:inactive_runner) do + create(:ci_runner, :instance, description: 'Runner 2', contacted_at: 1.day.ago, active: false, + version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + end + + def get_runner(id) + case id + when :active_runner + active_runner + when :inactive_runner + inactive_runner + end + end + + shared_examples 'runner details fetch' do |runner_id| + let(:query) do + wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner'))) + end + + let(:query_path) do + [ + [:runner, { id: get_runner(runner_id).to_global_id.to_s }] + ] + end + + it 'retrieves expected fields' do + post_graphql(query, current_user: user) + + runner_data = graphql_data_at(:runner) + expect(runner_data).not_to be_nil + + runner = get_runner(runner_id) + expect(runner_data).to match a_hash_including( + 'id' => "gid://gitlab/Ci::Runner/#{runner.id}", + 'description' => runner.description, + 'contactedAt' => runner.contacted_at&.iso8601, + 'version' => runner.version, + 'shortSha' => runner.short_sha, + 'revision' => runner.revision, + 'locked' => runner.locked, + 'active' => runner.active, + 'status' => runner.status.to_s.upcase, + 'maximumTimeout' => runner.maximum_timeout, + 'accessLevel' => runner.access_level.to_s.upcase, + 'runUntagged' => runner.run_untagged, + 'ipAddress' => runner.ip_address, + 'runnerType' => 'INSTANCE_TYPE' + ) + expect(runner_data['tagList']).to match_array runner.tag_list + end + end + + shared_examples 'retrieval by unauthorized user' do |runner_id| + let(:query) do + wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner'))) + end + + let(:query_path) do + [ + [:runner, { id: get_runner(runner_id).to_global_id.to_s }] + ] + end + + it 'returns null runner' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(:runner)).to be_nil + end + end + + describe 'for active runner' do + it_behaves_like 'runner details fetch', :active_runner + end + + describe 'for inactive runner' do + it_behaves_like 'runner details fetch', :inactive_runner + end + + describe 'by regular user' do + let(:user) { create_default(:user) } + + it_behaves_like 'retrieval by unauthorized user', :active_runner + end + + describe 'by unauthenticated user' do + let(:user) { nil } + + it_behaves_like 'retrieval by unauthorized user', :active_runner + end + + describe 'Query limits' do + def runner_query(runner) + <<~SINGLE + runner(id: "#{runner.to_global_id}") { + #{all_graphql_fields_for('CiRunner')} + } + SINGLE + end + + let(:single_query) do + <<~QUERY + { + active: #{runner_query(active_runner)} + } + QUERY + end + + let(:double_query) do + <<~QUERY + { + active: #{runner_query(active_runner)} + inactive: #{runner_query(inactive_runner)} + } + QUERY + end + + it 'does not execute more queries per runner', :aggregate_failures do + # warm-up license cache and so on: + post_graphql(single_query, current_user: user) + + control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) } + + expect { post_graphql(double_query, current_user: user) } + .not_to exceed_query_limit(control) + expect(graphql_data_at(:active)).not_to be_nil + expect(graphql_data_at(:inactive)).not_to be_nil + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4179e6f7e91..4a3227cc9f5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -275,6 +275,15 @@ RSpec.configure do |config| # https://gitlab.com/groups/gitlab-org/-/epics/5531 stub_feature_flags(refactor_blob_viewer: false) + # Disable `main_branch_over_master` as we migrate + # from `master` to `main` accross our codebase. + # It's done in order to preserve the concistency in tests + # As we're ready to change `master` usages to `main`, let's enable it + stub_feature_flags(main_branch_over_master: false) + + # Selectively disable by actor https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor + stub_feature_flags(remove_description_html_in_release_api_override: false) + allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged) else unstub_all_feature_flags diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index ca891669e31..c9508818f74 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -44,22 +44,24 @@ RSpec.shared_examples 'issue boards sidebar' do context 'in notifications subscription' do it 'displays notifications toggle', :aggregate_failures do page.within('[data-testid="sidebar-notifications"]') do - expect(page).to have_selector('[data-testid="notification-subscribe-toggle"]') + expect(page).to have_selector('[data-testid="subscription-toggle"]') expect(page).to have_content('Notifications') - expect(page).not_to have_content('Notifications have been disabled by the project or group owner') + expect(page).not_to have_content('Disabled by project owner') end end it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do - toggle = find('[data-testid="notification-subscribe-toggle"]') + wait_for_requests - toggle.click + click_button 'Notifications' - expect(toggle).to have_css("button.is-checked") + expect(page).to have_button('Notifications', class: 'is-checked') - toggle.click + click_button 'Notifications' - expect(toggle).not_to have_css("button.is-checked") + wait_for_requests + + expect(page).not_to have_button('Notifications', class: 'is-checked') end context 'when notifications have been disabled' do @@ -71,8 +73,8 @@ RSpec.shared_examples 'issue boards sidebar' do it 'displays a message that notifications have been disabled' do page.within('[data-testid="sidebar-notifications"]') do - expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]') - expect(page).to have_content('Notifications have been disabled by the project or group owner') + expect(page).to have_button('Notifications', class: 'is-disabled') + expect(page).to have_content('Disabled by project owner') end end end diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb index cec095f93ad..bf81ab577f7 100644 --- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -52,6 +52,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do end it 'has a "New project" link' do + render('layouts/header/new_repo_experiment') render expect(rendered).to have_link('New project', href: new_project_path(namespace_id: group.id)) -- cgit v1.2.3