diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-16 18:10:18 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-16 18:10:18 +0300 |
commit | 6364c14cc1f445d471bca118dca5af5a85b2c5dc (patch) | |
tree | 2579c5592f207e86ff7a0c5c7499caad723cdec1 /spec | |
parent | 5a2284f3500088e04cf3a5854fb06dc9db2b6077 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
22 files changed, 398 insertions, 124 deletions
diff --git a/spec/features/file_uploads/git_lfs_spec.rb b/spec/features/file_uploads/git_lfs_spec.rb index 3824c04ada5..239afb1a1bb 100644 --- a/spec/features/file_uploads/git_lfs_spec.rb +++ b/spec/features/file_uploads/git_lfs_spec.rb @@ -19,7 +19,7 @@ RSpec.describe 'Upload a git lfs object', :js do HTTParty.put( url, headers: headers, - basic_auth: { user: user.username, password: personal_access_token.token }, + basic_auth: { username: user.username, password: personal_access_token.token }, body: file.read ) end diff --git a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb index b3ace2e30ff..91c8e100e6a 100644 --- a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb +++ b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do subject do HTTParty.put( url, - basic_auth: { user: user.username, password: personal_access_token.token }, + basic_auth: { username: user.username, password: personal_access_token.token }, body: body ) end diff --git a/spec/features/file_uploads/nuget_package_spec.rb b/spec/features/file_uploads/nuget_package_spec.rb index fb1e0a54744..6e05e5d1a6e 100644 --- a/spec/features/file_uploads/nuget_package_spec.rb +++ b/spec/features/file_uploads/nuget_package_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'Upload a nuget package', :api, :js do subject do HTTParty.put( url, - basic_auth: { user: user.username, password: personal_access_token.token }, + basic_auth: { username: user.username, password: personal_access_token.token }, body: { package: file } ) end diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb index dec07eb3783..a4c450c9a2c 100644 --- a/spec/features/groups/navbar_spec.rb +++ b/spec/features/groups/navbar_spec.rb @@ -33,6 +33,7 @@ RSpec.describe 'Group navbar' do nav_item: _('Merge Requests'), nav_sub_items: [] }, + (security_and_compliance_nav_item if Gitlab.ee?), (push_rules_nav_item if Gitlab.ee?), { nav_item: _('Kubernetes'), diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index ca54c97d2bb..14d6b03645c 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -51,9 +51,15 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { const createComponent = ({ props = {}, - loading = false, + blobLoading = false, + lintLoading = false, options = {}, mountFn = shallowMount, + provide = { + glFeatures: { + ciConfigVisualizationTab: true, + }, + }, } = {}) => { mockMutate = jest.fn().mockResolvedValue({ data: { @@ -73,6 +79,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { newMergeRequestPath: mockNewMergeRequestPath, ...props, }, + provide, stubs: { GlTabs, GlButton, @@ -86,7 +93,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { $apollo: { queries: { content: { - loading, + loading: blobLoading, + }, + ciConfigData: { + loading: lintLoading, }, }, mutate: mockMutate, @@ -124,9 +134,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findAlert = () => wrapper.find(GlAlert); + const findBlobFailureAlert = () => wrapper.find(GlAlert); const findTabAt = i => wrapper.findAll(GlTab).at(i); + const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); const findTextEditor = () => wrapper.find(TextEditor); const findCommitForm = () => wrapper.find(CommitForm); + const findPipelineGraph = () => wrapper.find(PipelineGraph); const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon); beforeEach(() => { @@ -145,39 +158,65 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { wrapper = null; }); - it('displays a loading icon if the query is loading', () => { - createComponent({ loading: true }); + it('displays a loading icon if the blob query is loading', () => { + createComponent({ blobLoading: true }); expect(findLoadingIcon().exists()).toBe(true); expect(findTextEditor().exists()).toBe(false); }); describe('tabs', () => { - beforeEach(() => { - createComponent(); - }); + describe('editor tab', () => { + beforeEach(() => { + createComponent(); + }); + + it('displays the tab and its content', async () => { + expect( + findTabAt(0) + .find(TextEditor) + .exists(), + ).toBe(true); + }); - it('displays tabs and their content', async () => { - expect( - findTabAt(0) - .find(TextEditor) - .exists(), - ).toBe(true); - expect( - findTabAt(1) - .find(PipelineGraph) - .exists(), - ).toBe(true); + it('displays tab lazily, until editor is ready', async () => { + expect(findTabAt(0).attributes('lazy')).toBe('true'); + + findTextEditor().vm.$emit('editor-ready'); + + await nextTick(); + + expect(findTabAt(0).attributes('lazy')).toBe(undefined); + }); }); - it('displays editor tab lazily, until editor is ready', async () => { - expect(findTabAt(0).attributes('lazy')).toBe('true'); + describe('visualization tab', () => { + describe('with feature flag on', () => { + beforeEach(() => { + createComponent(); + }); - findTextEditor().vm.$emit('editor-ready'); + it('display the tab', () => { + expect(findVisualizationTab().exists()).toBe(true); + }); + + it('displays a loading icon if the lint query is loading', () => { + createComponent({ lintLoading: true }); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); - await nextTick(); + describe('with feature flag off', () => { + beforeEach(() => { + createComponent({ provide: { glFeatures: { ciConfigVisualizationTab: false } } }); + }); - expect(findTabAt(0).attributes('lazy')).toBe(undefined); + it('does not display the tab', () => { + expect(findVisualizationTab().exists()).toBe(false); + }); + }); }); }); @@ -359,7 +398,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { await waitForPromises(); - expect(findAlert().exists()).toBe(false); + expect(findBlobFailureAlert().exists()).toBe(false); expect(findTextEditor().attributes('value')).toBe(mockCiYml); }); @@ -373,7 +412,9 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { await waitForPromises(); - expect(findAlert().text()).toMatch('No CI file found in this repository, please add one.'); + expect(findBlobFailureAlert().text()).toBe( + 'No CI file found in this repository, please add one.', + ); }); it('shows a 400 error message', async () => { @@ -386,7 +427,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { await waitForPromises(); - expect(findAlert().text()).toMatch( + expect(findBlobFailureAlert().text()).toBe( 'Repository does not have a default branch, please set one.', ); }); @@ -396,7 +437,9 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { createComponentWithApollo(); await waitForPromises(); - expect(findAlert().text()).toMatch('The CI configuration was not loaded, please try again.'); + expect(findBlobFailureAlert().text()).toBe( + 'The CI configuration was not loaded, please try again.', + ); }); }); }); diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js index 4f55fdd6b28..a77973b293c 100644 --- a/spec/frontend/pipelines/pipeline_graph/mock_data.js +++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js @@ -1,4 +1,4 @@ -import { createUniqueJobId } from '~/pipelines/utils'; +import { createUniqueLinkId } from '~/pipelines/utils'; export const yamlString = `stages: - empty @@ -41,10 +41,10 @@ deploy_a: script: echo hello `; -const jobId1 = createUniqueJobId('build', 'build_1'); -const jobId2 = createUniqueJobId('test', 'test_1'); -const jobId3 = createUniqueJobId('test', 'test_2'); -const jobId4 = createUniqueJobId('deploy', 'deploy_1'); +const jobId1 = createUniqueLinkId('build', 'build_1'); +const jobId2 = createUniqueLinkId('test', 'test_1'); +const jobId3 = createUniqueLinkId('test', 'test_2'); +const jobId4 = createUniqueLinkId('deploy', 'deploy_1'); export const pipelineData = { stages: [ diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js index 7c8ebc27974..6704ee06c1a 100644 --- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js +++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js @@ -1,5 +1,8 @@ import { shallowMount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; import { pipelineData, singleStageData } from './mock_data'; +import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; +import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue'; import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue'; @@ -8,15 +11,16 @@ describe('pipeline graph component', () => { const defaultProps = { pipelineData }; let wrapper; - const createComponent = props => { + const createComponent = (props = defaultProps) => { return shallowMount(PipelineGraph, { propsData: { - ...defaultProps, ...props, }, }); }; + const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]'); + const findAlert = () => wrapper.find(GlAlert); const findAllStagePills = () => wrapper.findAll(StagePill); const findAllStageBackgroundElements = () => wrapper.findAll('[data-testid="stage-background"]'); const findStageBackgroundElementAt = index => findAllStageBackgroundElements().at(index); @@ -33,54 +37,92 @@ describe('pipeline graph component', () => { }); it('renders an empty section', () => { - expect(wrapper.text()).toContain( - 'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.', - ); + expect(wrapper.text()).toBe(wrapper.vm.$options.warningTexts[EMPTY_PIPELINE_DATA]); + expect(findPipelineGraph().exists()).toBe(false); expect(findAllStagePills()).toHaveLength(0); expect(findAllJobPills()).toHaveLength(0); }); }); - describe('with data', () => { + describe('with `INVALID` status', () => { + beforeEach(() => { + wrapper = createComponent({ pipelineData: { status: CI_CONFIG_STATUS_INVALID } }); + }); + + it('renders an error message and does not render the graph', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.warningTexts[INVALID_CI_CONFIG]); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); + + describe('without `INVALID` status', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('renders the graph with no status error', () => { + expect(findAlert().text()).not.toBe(wrapper.vm.$options.warningTexts[INVALID_CI_CONFIG]); + expect(findPipelineGraph().exists()).toBe(true); + }); + }); + + describe('with error while rendering the links', () => { beforeEach(() => { wrapper = createComponent(); }); + it('renders the error that link could not be drawn', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[DRAW_FAILURE]); + }); + }); + + describe('with only one stage', () => { + beforeEach(() => { + wrapper = createComponent({ pipelineData: singleStageData }); + }); + it('renders the right number of stage pills', () => { - const expectedStagesLength = pipelineData.stages.length; + const expectedStagesLength = singleStageData.stages.length; expect(findAllStagePills()).toHaveLength(expectedStagesLength); }); - it.each` - cssClass | expectedState - ${'gl-rounded-bottom-left-6'} | ${true} - ${'gl-rounded-top-left-6'} | ${true} - ${'gl-rounded-top-right-6'} | ${false} - ${'gl-rounded-bottom-right-6'} | ${false} - `( - 'rounds corner: $class should be $expectedState on the first element', - ({ cssClass, expectedState }) => { + it('renders the right number of job pills', () => { + // We count the number of jobs in the mock data + const expectedJobsLength = singleStageData.stages.reduce((acc, val) => { + return acc + val.groups.length; + }, 0); + + expect(findAllJobPills()).toHaveLength(expectedJobsLength); + }); + + describe('rounds corner', () => { + it.each` + cssClass | expectedState + ${'gl-rounded-bottom-left-6'} | ${true} + ${'gl-rounded-top-left-6'} | ${true} + ${'gl-rounded-top-right-6'} | ${true} + ${'gl-rounded-bottom-right-6'} | ${true} + `('$cssClass should be $expectedState on the only element', ({ cssClass, expectedState }) => { const classes = findStageBackgroundElementAt(0).classes(); expect(classes.includes(cssClass)).toBe(expectedState); - }, - ); - - it.each` - cssClass | expectedState - ${'gl-rounded-bottom-left-6'} | ${false} - ${'gl-rounded-top-left-6'} | ${false} - ${'gl-rounded-top-right-6'} | ${true} - ${'gl-rounded-bottom-right-6'} | ${true} - `( - 'rounds corner: $class should be $expectedState on the last element', - ({ cssClass, expectedState }) => { - const classes = findStageBackgroundElementAt(pipelineData.stages.length - 1).classes(); + }); + }); + }); - expect(classes.includes(cssClass)).toBe(expectedState); - }, - ); + describe('with multiple stages and jobs', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('renders the right number of stage pills', () => { + const expectedStagesLength = pipelineData.stages.length; + + expect(findAllStagePills()).toHaveLength(expectedStagesLength); + }); it('renders the right number of job pills', () => { // We count the number of jobs in the mock data @@ -90,26 +132,34 @@ describe('pipeline graph component', () => { expect(findAllJobPills()).toHaveLength(expectedJobsLength); }); - }); - describe('with only one stage', () => { - beforeEach(() => { - wrapper = createComponent({ pipelineData: singleStageData }); - }); + describe('rounds corner', () => { + it.each` + cssClass | expectedState + ${'gl-rounded-bottom-left-6'} | ${true} + ${'gl-rounded-top-left-6'} | ${true} + ${'gl-rounded-top-right-6'} | ${false} + ${'gl-rounded-bottom-right-6'} | ${false} + `( + '$cssClass should be $expectedState on the first element', + ({ cssClass, expectedState }) => { + const classes = findStageBackgroundElementAt(0).classes(); + + expect(classes.includes(cssClass)).toBe(expectedState); + }, + ); - it.each` - cssClass | expectedState - ${'gl-rounded-bottom-left-6'} | ${true} - ${'gl-rounded-top-left-6'} | ${true} - ${'gl-rounded-top-right-6'} | ${true} - ${'gl-rounded-bottom-right-6'} | ${true} - `( - 'rounds corner: $class should be $expectedState on the only element', - ({ cssClass, expectedState }) => { - const classes = findStageBackgroundElementAt(0).classes(); + it.each` + cssClass | expectedState + ${'gl-rounded-bottom-left-6'} | ${false} + ${'gl-rounded-top-left-6'} | ${false} + ${'gl-rounded-top-right-6'} | ${true} + ${'gl-rounded-bottom-right-6'} | ${true} + `('$cssClass should be $expectedState on the last element', ({ cssClass, expectedState }) => { + const classes = findStageBackgroundElementAt(pipelineData.stages.length - 1).classes(); expect(classes.includes(cssClass)).toBe(expectedState); - }, - ); + }); + }); }); }); diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js index b073ad7647e..12154df6fcf 100644 --- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js +++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js @@ -1,14 +1,24 @@ -import { createUniqueJobId, generateJobNeedsDict } from '~/pipelines/utils'; +import { createJobsHash, generateJobNeedsDict } from '~/pipelines/utils'; describe('utils functions', () => { const jobName1 = 'build_1'; const jobName2 = 'build_2'; const jobName3 = 'test_1'; const jobName4 = 'deploy_1'; - const job1 = { script: 'echo hello', stage: 'build' }; - const job2 = { script: 'echo build', stage: 'build' }; - const job3 = { script: 'echo test', stage: 'test', needs: [jobName1, jobName2] }; - const job4 = { script: 'echo deploy', stage: 'deploy', needs: [jobName3] }; + const job1 = { name: jobName1, script: 'echo hello', stage: 'build' }; + const job2 = { name: jobName2, script: 'echo build', stage: 'build' }; + const job3 = { + name: jobName3, + script: 'echo test', + stage: 'test', + needs: [jobName1, jobName2], + }; + const job4 = { + name: jobName4, + script: 'echo deploy', + stage: 'deploy', + needs: [jobName3], + }; const userDefinedStage = 'myStage'; const pipelineGraphData = { @@ -23,7 +33,6 @@ describe('utils functions', () => { { name: jobName4, jobs: [{ ...job4 }], - id: createUniqueJobId(job4.stage, jobName4), }, ], }, @@ -33,12 +42,10 @@ describe('utils functions', () => { { name: jobName1, jobs: [{ ...job1 }], - id: createUniqueJobId(job1.stage, jobName1), }, { name: jobName2, jobs: [{ ...job2 }], - id: createUniqueJobId(job2.stage, jobName2), }, ], }, @@ -48,49 +55,59 @@ describe('utils functions', () => { { name: jobName3, jobs: [{ ...job3 }], - id: createUniqueJobId(job3.stage, jobName3), }, ], }, ], - jobs: { - [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) }, - [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) }, - [jobName3]: { ...job3, id: createUniqueJobId(job3.stage, jobName3) }, - [jobName4]: { ...job4, id: createUniqueJobId(job4.stage, jobName4) }, - }, }; + describe('createJobsHash', () => { + it('returns an empty object if there are no jobs received as argument', () => { + expect(createJobsHash([])).toEqual({}); + }); + + it('returns a hash with the jobname as key and all its data as value', () => { + const jobs = { + [jobName1]: job1, + [jobName2]: job2, + [jobName3]: job3, + [jobName4]: job4, + }; + + expect(createJobsHash(pipelineGraphData.stages)).toEqual(jobs); + }); + }); + describe('generateJobNeedsDict', () => { it('generates an empty object if it receives no jobs', () => { - expect(generateJobNeedsDict({ jobs: {} })).toEqual({}); + expect(generateJobNeedsDict({})).toEqual({}); }); it('generates a dict with empty needs if there are no dependencies', () => { const smallGraph = { - jobs: { - [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) }, - [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) }, - }, + [jobName1]: job1, + [jobName2]: job2, }; expect(generateJobNeedsDict(smallGraph)).toEqual({ - [pipelineGraphData.jobs[jobName1].id]: [], - [pipelineGraphData.jobs[jobName2].id]: [], + [jobName1]: [], + [jobName2]: [], }); }); it('generates a dict where key is the a job and its value is an array of all its needs', () => { - const uniqueJobName1 = pipelineGraphData.jobs[jobName1].id; - const uniqueJobName2 = pipelineGraphData.jobs[jobName2].id; - const uniqueJobName3 = pipelineGraphData.jobs[jobName3].id; - const uniqueJobName4 = pipelineGraphData.jobs[jobName4].id; + const jobsWithNeeds = { + [jobName1]: job1, + [jobName2]: job2, + [jobName3]: job3, + [jobName4]: job4, + }; - expect(generateJobNeedsDict(pipelineGraphData)).toEqual({ - [uniqueJobName1]: [], - [uniqueJobName2]: [], - [uniqueJobName3]: [uniqueJobName1, uniqueJobName2], - [uniqueJobName4]: [uniqueJobName3, uniqueJobName1, uniqueJobName2], + expect(generateJobNeedsDict(jobsWithNeeds)).toEqual({ + [jobName1]: [], + [jobName2]: [], + [jobName3]: [jobName1, jobName2], + [jobName4]: [jobName3, jobName1, jobName2], }); }); }); diff --git a/spec/frontend/pipelines/unwrapping_utils_spec.js b/spec/frontend/pipelines/unwrapping_utils_spec.js index 34413ad3ef3..3533599611f 100644 --- a/spec/frontend/pipelines/unwrapping_utils_spec.js +++ b/spec/frontend/pipelines/unwrapping_utils_spec.js @@ -1,4 +1,5 @@ import { + unwrapArrayOfJobs, unwrapGroups, unwrapNodesWithName, unwrapStagesWithNeeds, @@ -94,6 +95,29 @@ const completeMock = [ ]; describe('Shared pipeline unwrapping utils', () => { + describe('unwrapArrayOfJobs', () => { + it('returns an empty array if the input is an empty undefined', () => { + expect(unwrapArrayOfJobs(undefined)).toEqual([]); + }); + + it('returns an empty array if the input is an empty array', () => { + expect(unwrapArrayOfJobs([])).toEqual([]); + }); + + it('returns a flatten array of each job with their data and stage name', () => { + expect( + unwrapArrayOfJobs([ + { name: 'build', groups: [{ name: 'job_a_1' }, { name: 'job_a_2' }] }, + { name: 'test', groups: [{ name: 'job_b' }] }, + ]), + ).toMatchObject([ + { category: 'build', name: 'job_a_1' }, + { category: 'build', name: 'job_a_2' }, + { category: 'test', name: 'job_b' }, + ]); + }); + }); + describe('unwrapGroups', () => { it('takes stages without nodes and returns the unwrapped groups', () => { expect(unwrapGroups(stagesAndGroups)[0].groups).toEqual(groupsArray); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index e67e8cf3dff..d28d5ecda1b 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -416,6 +416,7 @@ RSpec.describe ProjectsHelper do describe '#get_project_nav_tabs' do before do + allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:can?) { true } end diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb index db961d5a4e6..adeccd999f3 100644 --- a/spec/models/label_priority_spec.rb +++ b/spec/models/label_priority_spec.rb @@ -18,5 +18,11 @@ RSpec.describe LabelPriority do expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id) end + + describe 'when importing' do + subject { create(:label_priority, importing: true) } + + it { is_expected.not_to validate_presence_of(:label) } + end end end diff --git a/spec/models/sentry_issue_spec.rb b/spec/models/sentry_issue_spec.rb index 33654bf5e1a..c24350d7067 100644 --- a/spec/models/sentry_issue_spec.rb +++ b/spec/models/sentry_issue_spec.rb @@ -27,6 +27,12 @@ RSpec.describe SentryIssue do expect(duplicate_sentry_issue).to be_invalid expect(duplicate_sentry_issue.errors.full_messages.first).to include('is already associated') end + + describe 'when importing' do + subject { create(:sentry_issue, importing: true) } + + it { is_expected.not_to validate_presence_of(:issue) } + end end describe 'callbacks' do diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb index e88fc13ecee..9a7624c253a 100644 --- a/spec/models/suggestion_spec.rb +++ b/spec/models/suggestion_spec.rb @@ -12,6 +12,12 @@ RSpec.describe Suggestion do describe 'validations' do it { is_expected.to validate_presence_of(:note) } + context 'when importing' do + subject { create(:suggestion, importing: true) } + + it { is_expected.not_to validate_presence_of(:note) } + end + context 'when suggestion is applied' do before do allow(subject).to receive(:applied?).and_return(true) diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb index ae1697fb7e6..e9019b55635 100644 --- a/spec/models/timelog_spec.rb +++ b/spec/models/timelog_spec.rb @@ -40,6 +40,14 @@ RSpec.describe Timelog do expect(subject).to be_valid end + + describe 'when importing' do + it 'is valid if issue_id and merge_request_id are missing' do + subject.attributes = { issue: nil, merge_request: nil, importing: true } + + expect(subject).to be_valid + end + end end describe 'scopes' do diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb index 00a0f92e848..2b45533035d 100644 --- a/spec/models/zoom_meeting_spec.rb +++ b/spec/models/zoom_meeting_spec.rb @@ -12,8 +12,8 @@ RSpec.describe ZoomMeeting do end describe 'Associations' do - it { is_expected.to belong_to(:project).required } - it { is_expected.to belong_to(:issue).required } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:issue) } end describe 'scopes' do @@ -40,6 +40,16 @@ RSpec.describe ZoomMeeting do end describe 'Validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:issue) } + + describe 'when importing' do + subject { build(:zoom_meeting, importing: true) } + + it { is_expected.not_to validate_presence_of(:project) } + it { is_expected.not_to validate_presence_of(:issue) } + end + describe 'url' do it { is_expected.to validate_presence_of(:url) } it { is_expected.to validate_length_of(:url).is_at_most(255) } diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index acc49768545..0e163ec2154 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -117,6 +117,12 @@ RSpec.describe API::Features, stub_feature_flags: false do ) end + it 'logs the event' do + expect(Feature.logger).to receive(:info).once + + post api("/features/#{feature_name}", admin), params: { value: 'true' } + end + it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' } @@ -444,6 +450,12 @@ RSpec.describe API::Features, stub_feature_flags: false do expect(response).to have_gitlab_http_status(:no_content) end + + it 'logs the event' do + expect(Feature.logger).to receive(:info).once + + delete api("/features/#{feature_name}", admin) + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 83b32d9b3fd..c50fe2c7e1d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -883,7 +883,9 @@ RSpec.describe API::Projects do only_allow_merge_if_all_discussions_are_resolved: false, ci_config_path: 'a/custom/path', merge_method: 'ff' - }) + }).tap do |attrs| + attrs[:operations_access_level] = 'disabled' + end post api('/projects', user), params: project @@ -900,6 +902,7 @@ RSpec.describe API::Projects do expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) + expect(project.operations_access_level).to eq(ProjectFeature::DISABLED) end it 'creates a project using a template' do @@ -1579,6 +1582,7 @@ RSpec.describe API::Projects do expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) + expect(json_response['operations_access_level']).to be_present end end @@ -1621,6 +1625,7 @@ RSpec.describe API::Projects do expect(json_response['forking_access_level']).to be_present expect(json_response['wiki_access_level']).to be_present expect(json_response['builds_access_level']).to be_present + expect(json_response['operations_access_level']).to be_present expect(json_response).to have_key('emails_disabled') expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['remove_source_branch_after_merge']).to be_truthy diff --git a/spec/controllers/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index 39d92846863..805c1f1d82b 100644 --- a/spec/controllers/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -12,6 +12,6 @@ RSpec.describe IdeController do it 'increases the views counter' do expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count) - get :index + get ide_url end end diff --git a/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb new file mode 100644 index 00000000000..8c3703a488a --- /dev/null +++ b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +require_relative '../../../../rubocop/cop/rspec/httparty_basic_auth' + +RSpec.describe RuboCop::Cop::RSpec::HTTPartyBasicAuth, type: :rubocop do + include CopHelper + + subject(:cop) { described_class.new } + + context 'when passing `basic_auth: { user: ... }`' do + it 'registers an offence' do + expect_offense(<<~SOURCE, 'spec/foo.rb') + HTTParty.put( + url, + basic_auth: { user: user, password: token }, + ^^^^ #{described_class::MESSAGE} + body: body + ) + SOURCE + end + + it 'can autocorrect the source' do + bad = 'HTTParty.put(url, basic_auth: { user: user, password: token })' + good = 'HTTParty.put(url, basic_auth: { username: user, password: token })' + expect(autocorrect_source(bad)).to eq(good) + end + end + + context 'when passing `basic_auth: { username: ... }`' do + it 'does not register an offence' do + expect_no_offenses(<<~SOURCE, 'spec/frontend/fixtures/foo.rb') + HTTParty.put( + url, + basic_auth: { username: user, password: token }, + body: body + ) + SOURCE + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 93867ef2990..c19c26f9a0b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -142,7 +142,9 @@ RSpec.configure do |config| config.include TestEnv config.include FileReadHelpers config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::IntegrationHelpers, type: :feature + config.include Devise::Test::IntegrationHelpers, type: :request config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature config.include WaitHelpers, type: :feature @@ -150,7 +152,6 @@ RSpec.configure do |config| config.include EmailHelpers, :mailer, type: :mailer config.include Warden::Test::Helpers, type: :request config.include Gitlab::Routing, type: :routing - config.include Devise::Test::ControllerHelpers, type: :view config.include ApiHelpers, :api config.include CookieHelper, :js config.include InputHelper, :js diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index ed74c3f179f..549dc1cff1d 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -14,6 +14,15 @@ RSpec.shared_context 'project navbar structure' do } end + let(:security_and_compliance_nav_item) do + { + nav_item: _('Security & Compliance'), + nav_sub_items: [ + _('Audit Events') + ] + } + end + let(:structure) do [ { @@ -62,6 +71,7 @@ RSpec.shared_context 'project navbar structure' do _('Schedules') ] }, + (security_and_compliance_nav_item if Gitlab.ee?), { nav_item: _('Operations'), nav_sub_items: [ @@ -101,8 +111,7 @@ RSpec.shared_context 'project navbar structure' do _('Access Tokens'), _('Repository'), _('CI / CD'), - _('Operations'), - (_('Audit Events') if Gitlab.ee?) + _('Operations') ].compact } ].compact @@ -128,8 +137,7 @@ RSpec.shared_context 'group navbar structure' do _('Projects'), _('Repository'), _('CI / CD'), - _('Webhooks'), - _('Audit Events') + _('Webhooks') ] } end @@ -143,6 +151,15 @@ RSpec.shared_context 'group navbar structure' do } end + let(:security_and_compliance_nav_item) do + { + nav_item: _('Security & Compliance'), + nav_sub_items: [ + _('Audit Events') + ] + } + end + let(:push_rules_nav_item) do { nav_item: _('Push Rules'), @@ -172,6 +189,7 @@ RSpec.shared_context 'group navbar structure' do nav_item: _('Merge Requests'), nav_sub_items: [] }, + (security_and_compliance_nav_item if Gitlab.ee?), (push_rules_nav_item if Gitlab.ee?), { nav_item: _('Kubernetes'), diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb index 9c0b398a5c1..2b93d174653 100644 --- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb @@ -40,6 +40,30 @@ RSpec.shared_examples 'boards create mutation' do end end + context 'when hide_backlog_list parameter is true' do + before do + params[:hide_backlog_list] = true + end + + it 'returns the board with correct hide_backlog_list field' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['board']['hideBacklogList']).to eq(true) + end + end + + context 'when hide_closed_list parameter is true' do + before do + params[:hide_closed_list] = true + end + + it 'returns the board with correct hide_closed_list field' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['board']['hideClosedList']).to eq(true) + end + end + context 'when the Boards::CreateService returns an error response' do before do allow_next_instance_of(Boards::CreateService) do |service| |