diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-05 00:09:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-05 00:09:06 +0300 |
commit | f4726e9f5029931fc74aee9d5eff93d6a762dcff (patch) | |
tree | bc6d47ea3d39afdf46c5df3d8328f3f266c38ae5 /spec | |
parent | 7c221ba5ce130ca50b892e6dd32783e1327718df (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
25 files changed, 922 insertions, 225 deletions
diff --git a/spec/controllers/projects/security/configuration_controller_spec.rb b/spec/controllers/projects/security/configuration_controller_spec.rb new file mode 100644 index 00000000000..afbebbad3d1 --- /dev/null +++ b/spec/controllers/projects/security/configuration_controller_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Security::ConfigurationController do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET show' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: false) + end + + it 'renders not found' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when feature flag is enabled' do + context 'when user has guest access' do + before do + project.add_guest(user) + end + + it 'denies access' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when user has developer access' do + before do + project.add_developer(user) + end + + it 'grants access' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end + end +end diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb index 7017b0ee9e7..40ad221415c 100644 --- a/spec/factories/u2f_registrations.rb +++ b/spec/factories/u2f_registrations.rb @@ -2,6 +2,8 @@ FactoryBot.define do factory :u2f_registration do + user + certificate { FFaker::BaconIpsum.characters(728) } key_handle { FFaker::BaconIpsum.characters(86) } public_key { FFaker::BaconIpsum.characters(88) } diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb index 87bd02c3684..07c87f98eb6 100644 --- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb +++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'Alert integrations settings form', :js do describe 'when viewing alert integrations as a maintainer' do context 'with the default page permissions' do before do + stub_feature_flags(multiple_http_integrations_custom_mapping: false) visit project_settings_operations_path(project, anchor: 'js-alert-management-settings') wait_for_requests end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 59fba5f65e0..ca44978d223 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -11,6 +11,11 @@ RSpec.describe 'Issue Sidebar' do let!(:label) { create(:label, project: project, title: 'bug') } let(:issue) { create(:labeled_issue, project: project, labels: [label]) } let!(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } + let!(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) } + let!(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } + let!(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } + let!(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } + let!(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") @@ -134,6 +139,36 @@ RSpec.describe 'Issue Sidebar' do end end + context 'editing issue milestone', :js do + before do + page.within('.block.milestone > .title') do + click_on 'Edit' + end + end + + it 'shows milestons list in the dropdown' do + page.within('.block.milestone .dropdown-content') do + # 5 milestones + "No milestone" = 6 items + expect(page.find('ul')).to have_selector('li[data-milestone-id]', count: 6) + end + end + + it 'shows expired milestone at the bottom of the list' do + page.within('.block.milestone .dropdown-content ul') do + expect(page.find('li:last-child')).to have_content milestone_expired.title + end + end + + it 'shows milestone due earliest at the top of the list' do + page.within('.block.milestone .dropdown-content ul') do + expect(page.all('li[data-milestone-id]')[1]).to have_content milestone3.title + expect(page.all('li[data-milestone-id]')[2]).to have_content milestone2.title + expect(page.all('li[data-milestone-id]')[3]).to have_content milestone1.title + expect(page.all('li[data-milestone-id]')[4]).to have_content milestone_no_duedate.title + end + end + end + context 'editing issue labels', :js do before do issue.update(labels: [label]) diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index be6205598d8..1ef6d2a1068 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -9,142 +9,158 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) } let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test') } - dropdown_toggle_selector = '[data-testid="mini-pipeline-graph-dropdown-toggle"]' - - before do - build.run - build.trace.set('hello') - sign_in(user) - visit_merge_request - end - - def visit_merge_request(format: :html, serializer: nil) - visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) - end + shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled| + before do + build.run + build.trace.set('hello') + sign_in(user) + stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled) + visit_merge_request + end - it 'displays a mini pipeline graph' do - expect(page).to have_selector('.mr-widget-pipeline-graph') - end + let_it_be(:dropdown_toggle_selector) do + if ci_mini_pipeline_gl_dropdown_enabled + '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle' + else + '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + end + end - context 'as json' do - let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } - let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } + def visit_merge_request(format: :html, serializer: nil) + visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) + end - before do - job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) - create(:ci_job_artifact, :archive, file: artifacts_file1, job: job) - create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + it 'displays a mini pipeline graph' do + expect(page).to have_selector('.mr-widget-pipeline-graph') end - # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034 - xit 'avoids repeated database queries' do - before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + context 'as json' do + let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } - job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) - create(:ci_job_artifact, :archive, file: artifacts_file2, job: job) - create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + before do + job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) + create(:ci_job_artifact, :archive, file: artifacts_file1, job: job) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + end - after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034 + xit 'avoids repeated database queries' do + before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } - expect(before.count).to eq(after.count) - expect(before.cached_count).to eq(after.cached_count) - end - end + job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) + create(:ci_job_artifact, :archive, file: artifacts_file2, job: job) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') - describe 'build list toggle' do - let(:toggle) do - find(dropdown_toggle_selector) - first(dropdown_toggle_selector) + after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + + expect(before.count).to eq(after.count) + expect(before.cached_count).to eq(after.cached_count) + end end - # Status icon button styles should update as described in - # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769 - it 'has unique styles for default, :hover, :active, and :focus states' do - default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector) + describe 'build list toggle' do + let(:toggle) do + find(dropdown_toggle_selector) + first(dropdown_toggle_selector) + end - toggle.hover - hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector) + # Status icon button styles should update as described in + # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769 + it 'has unique styles for default, :hover, :active, and :focus states' do + default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector) - page.driver.browser.action.click_and_hold(toggle.native).perform - active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector) + toggle.hover + hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector) - page.driver.browser.action.release(toggle.native) - .move_by(100, 100) - .perform - focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector) + page.driver.browser.action.click_and_hold(toggle.native).perform + active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector) + page.driver.browser.action.release(toggle.native).perform - expect(default_background_color).not_to eq(hover_background_color) - expect(hover_background_color).not_to eq(active_background_color) - expect(default_background_color).not_to eq(active_background_color) + page.driver.browser.action.click(toggle.native).move_by(100, 100).perform + focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector) - expect(default_foreground_color).not_to eq(hover_foreground_color) - expect(hover_foreground_color).not_to eq(active_foreground_color) - expect(default_foreground_color).not_to eq(active_foreground_color) + expect(default_background_color).not_to eq(hover_background_color) + expect(hover_background_color).not_to eq(active_background_color) + expect(default_background_color).not_to eq(active_background_color) - expect(focus_background_color).to eq(hover_background_color) - expect(focus_foreground_color).to eq(hover_foreground_color) + expect(default_foreground_color).not_to eq(hover_foreground_color) + expect(hover_foreground_color).not_to eq(active_foreground_color) + expect(default_foreground_color).not_to eq(active_foreground_color) - expect(default_box_shadow).to eq('none') - expect(hover_box_shadow).to eq('none') - expect(active_box_shadow).not_to eq('none') - expect(focus_box_shadow).not_to eq('none') - end + expect(focus_background_color).to eq(hover_background_color) + expect(focus_foreground_color).to eq(hover_foreground_color) - it 'shows tooltip when hovered' do - toggle.hover + expect(default_box_shadow).to eq('none') + expect(hover_box_shadow).to eq('none') + expect(active_box_shadow).not_to eq('none') + expect(focus_box_shadow).not_to eq('none') + end - expect(page).to have_selector('.tooltip') - end - end + it 'shows tooltip when hovered' do + toggle.hover - describe 'builds list menu' do - let(:toggle) do - find(dropdown_toggle_selector) - first(dropdown_toggle_selector) + expect(page).to have_selector('.tooltip') + end end - before do - toggle.click - wait_for_requests - end + describe 'builds list menu' do + let(:toggle) do + find(dropdown_toggle_selector) + first(dropdown_toggle_selector) + end - it 'pens when toggle is clicked' do - expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') - end + before do + toggle.click + wait_for_requests + end - it 'closes when toggle is clicked again' do - toggle.click + it 'pens when toggle is clicked' do + expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') + end - expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') - end + it 'closes when toggle is clicked again' do + toggle.click - it 'closes when clicking somewhere else' do - find('body').click + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') + end - expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') - end + it 'closes when clicking somewhere else' do + find('body').click - describe 'build list build item' do - let(:build_item) do - find('.mini-pipeline-graph-dropdown-item') - first('.mini-pipeline-graph-dropdown-item') + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end - it 'visits the build page when clicked' do - build_item.click - find('.build-page') + describe 'build list build item' do + let(:build_item) do + find('.mini-pipeline-graph-dropdown-item') + first('.mini-pipeline-graph-dropdown-item') + end - expect(current_path).to eql(project_job_path(project, build)) - end + it 'visits the build page when clicked' do + build_item.click + find('.build-page') - it 'shows tooltip when hovered' do - build_item.hover + expect(current_path).to eql(project_job_path(project, build)) + end - expect(page).to have_selector('.tooltip') + it 'shows tooltip when hovered' do + build_item.hover + + expect(page).to have_selector('.tooltip') + end end end end + context 'with ci_mini_pipeline_gl_dropdown disabled' do + it_behaves_like "mini pipeline renders", false + end + + context 'with ci_mini_pipeline_gl_dropdown enabled' do + it_behaves_like "mini pipeline renders", true + end + private def get_toggle_colors(selector) diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index d9a23edc03c..e0a0591fe6b 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -518,58 +518,75 @@ RSpec.describe 'Pipelines', :js do end end - context 'mini pipeline graph' do - let!(:build) do - create(:ci_build, :pending, pipeline: pipeline, - stage: 'build', - name: 'build') - end + shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled| + context 'mini pipeline graph' do + let!(:build) do + create(:ci_build, :pending, pipeline: pipeline, + stage: 'build', + name: 'build') + end - dropdown_toggle_selector = '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + before do + stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled) + visit_project_pipelines + end - before do - visit_project_pipelines - end + let_it_be(:dropdown_toggle_selector) do + if ci_mini_pipeline_gl_dropdown_enabled + '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle' + else + '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + end + end - it 'renders a mini pipeline graph' do - expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]') - expect(page).to have_selector(dropdown_toggle_selector) - end + it 'renders a mini pipeline graph' do + expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]') + expect(page).to have_selector(dropdown_toggle_selector) + end - context 'when clicking a stage badge' do - it 'opens a dropdown' do - find(dropdown_toggle_selector).click + context 'when clicking a stage badge' do + it 'opens a dropdown' do + find(dropdown_toggle_selector).click - expect(page).to have_link build.name - end + expect(page).to have_link build.name + end - it 'is possible to cancel pending build' do - find(dropdown_toggle_selector).click - find('.js-ci-action').click - wait_for_requests + it 'is possible to cancel pending build' do + find(dropdown_toggle_selector).click + find('.js-ci-action').click + wait_for_requests - expect(build.reload).to be_canceled + expect(build.reload).to be_canceled + end end - end - context 'for a failed pipeline' do - let!(:build) do - create(:ci_build, :failed, pipeline: pipeline, - stage: 'build', - name: 'build') - end + context 'for a failed pipeline' do + let!(:build) do + create(:ci_build, :failed, pipeline: pipeline, + stage: 'build', + name: 'build') + end - it 'displays the failure reason' do - find(dropdown_toggle_selector).click + it 'displays the failure reason' do + find(dropdown_toggle_selector).click - within('.js-builds-dropdown-list') do - build_element = page.find('.mini-pipeline-graph-dropdown-item') - expect(build_element['title']).to eq('build - failed - (unknown failure)') + within('.js-builds-dropdown-list') do + build_element = page.find('.mini-pipeline-graph-dropdown-item') + expect(build_element['title']).to eq('build - failed - (unknown failure)') + end end end end end + context 'with ci_mini_pipeline_gl_dropdown disabled' do + it_behaves_like "mini pipeline renders", false + end + + context 'with ci_mini_pipeline_gl_dropdown enabled' do + it_behaves_like "mini pipeline renders", true + end + context 'with pagination' do before do allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 5762a54a717..eed67e3ac78 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -39,6 +39,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j end it 'allows the same device to be registered for multiple users' do + # U2f specs will be removed after WebAuthn migration completed + pending('FakeU2fDevice has static key handle, '\ + 'leading to duplicate credential_xid for WebAuthn during migration, '\ + 'resulting in unique constraint violation') + # First user visit profile_account_path manage_two_factor_authentication @@ -148,6 +153,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j describe "and also the current user" do it "allows logging in with that particular device" do + # U2f specs will be removed after WebAuthn migration completed + pending('FakeU2fDevice has static key handle, '\ + 'leading to duplicate credential_xid for WebAuthn during migration, '\ + 'resulting in unique constraint violation') + # Register current user with the same U2F device current_user = gitlab_sign_in(:user) current_user.update_attribute(:otp_required_for_login, true) diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb index 2ffb6bb3477..4eebc9d2c1e 100644 --- a/spec/features/webauthn_spec.rb +++ b/spec/features/webauthn_spec.rb @@ -129,6 +129,10 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js do end it 'falls back to U2F' do + # WebAuthn registration is automatically created with the U2fRegistration because of the after_create callback + # so we need to delete it + WebauthnRegistration.delete_all + gitlab_sign_in(user) u2f_device.respond_to_u2f_authentication diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap index 66e3f180d92..eb2b82a0211 100644 --- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = ` +exports[`AlertsSettingsForm with default values renders the initial template 1`] = ` <form class="gl-mt-6" > diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js index 92ea8b2f33c..1ec7b5c18f6 100644 --- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js +++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js @@ -1,10 +1,10 @@ import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue'; -import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json'; import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations'; +import alertFields from '../mocks/alertFields.json'; describe('AlertMappingBuilder', () => { let wrapper; @@ -14,6 +14,7 @@ describe('AlertMappingBuilder', () => { propsData: { parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes, savedMapping: parsedMapping.storedMapping.nodes, + alertFields, }, }); } @@ -44,28 +45,28 @@ describe('AlertMappingBuilder', () => { }); it('renders disabled form input for each mapped field', () => { - gitlabFields.forEach((field, index) => { + alertFields.forEach((field, index) => { const input = findColumnInRow(index + 1, 0).find(GlFormInput); - const types = field.type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or '); + const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or '); expect(input.attributes('value')).toBe(`${field.label} (${types})`); expect(input.attributes('disabled')).toBe(''); }); }); it('renders right arrow next to each input', () => { - gitlabFields.forEach((field, index) => { + alertFields.forEach((field, index) => { const arrow = findColumnInRow(index + 1, 1).find('.right-arrow'); expect(arrow.exists()).toBe(true); }); }); it('renders mapping dropdown for each field', () => { - gitlabFields.forEach(({ compatibleTypes }, index) => { + alertFields.forEach(({ types }, index) => { const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); const searchBox = dropdown.findComponent(GlSearchBoxByType); const dropdownItems = dropdown.findAllComponents(GlDropdownItem); const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type)); + const mappingOptions = nodes.filter(({ type }) => types.includes(type)); expect(dropdown.exists()).toBe(true); expect(searchBox.exists()).toBe(true); @@ -74,7 +75,7 @@ describe('AlertMappingBuilder', () => { }); it('renders fallback dropdown only for the fields that have fallback', () => { - gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => { + alertFields.forEach(({ types, numberOfFallbacks }, index) => { const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown); expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks)); @@ -82,7 +83,7 @@ describe('AlertMappingBuilder', () => { const searchBox = dropdown.findComponent(GlSearchBoxByType); const dropdownItems = dropdown.findAllComponents(GlDropdownItem); const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type)); + const mappingOptions = nodes.filter(({ type }) => types.includes(type)); expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks)); expect(dropdownItems).toHaveLength(mappingOptions.length); diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js index 2afa714fcf8..b43c1318a86 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -11,9 +11,10 @@ import waitForPromises from 'helpers/wait_for_promises'; import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue'; import { typeSet } from '~/alerts_settings/constants'; +import alertFields from '../mocks/alertFields.json'; import { defaultAlertSettingsConfig } from './util'; -describe('AlertsSettingsFormNew', () => { +describe('AlertsSettingsForm', () => { let wrapper; const mockToastShow = jest.fn(); @@ -21,6 +22,7 @@ describe('AlertsSettingsFormNew', () => { data = {}, props = {}, multipleHttpIntegrationsCustomMapping = false, + multiIntegrations = true, } = {}) => { wrapper = mount(AlertsSettingsForm, { data() { @@ -32,8 +34,9 @@ describe('AlertsSettingsFormNew', () => { ...props, }, provide: { - glFeatures: { multipleHttpIntegrationsCustomMapping }, ...defaultAlertSettingsConfig, + glFeatures: { multipleHttpIntegrationsCustomMapping }, + multiIntegrations, }, mocks: { $toast: { @@ -132,7 +135,11 @@ describe('AlertsSettingsFormNew', () => { }); it('create with custom mapping', async () => { - createComponent({ multipleHttpIntegrationsCustomMapping: true }); + createComponent({ + multipleHttpIntegrationsCustomMapping: true, + multiIntegrations: true, + props: { alertFields }, + }); const integrationName = 'Test integration'; await selectOptionAtIndex(1); @@ -275,6 +282,7 @@ describe('AlertsSettingsFormNew', () => { currentIntegration: { type: typeSet.http, }, + alertFields, }, }); }); @@ -347,18 +355,27 @@ describe('AlertsSettingsFormNew', () => { describe('Mapping builder section', () => { describe.each` - featureFlag | integrationOption | visible - ${true} | ${1} | ${true} - ${true} | ${2} | ${false} - ${false} | ${1} | ${false} - ${false} | ${2} | ${false} - `('', ({ featureFlag, integrationOption, visible }) => { + alertFieldsProvided | multiIntegrations | featureFlag | integrationOption | visible + ${true} | ${true} | ${true} | ${1} | ${true} + ${true} | ${true} | ${true} | ${2} | ${false} + ${true} | ${true} | ${false} | ${1} | ${false} + ${true} | ${true} | ${false} | ${2} | ${false} + ${true} | ${false} | ${true} | ${1} | ${false} + ${false} | ${true} | ${true} | ${1} | ${false} + `('', ({ alertFieldsProvided, multiIntegrations, featureFlag, integrationOption, visible }) => { const visibleMsg = visible ? 'is rendered' : 'is not rendered'; const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled'; + const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided'; const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus; - it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => { - createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag }); + it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => { + createComponent({ + multipleHttpIntegrationsCustomMapping: featureFlag, + multiIntegrations, + props: { + alertFields: alertFieldsProvided ? alertFields : [], + }, + }); await selectOptionAtIndex(integrationOption); expect(findMappingBuilderSection().exists()).toBe(visible); diff --git a/spec/frontend/alerts_settings/mocks/alertFields.json b/spec/frontend/alerts_settings/mocks/alertFields.json new file mode 100644 index 00000000000..ffe59dd0c05 --- /dev/null +++ b/spec/frontend/alerts_settings/mocks/alertFields.json @@ -0,0 +1,123 @@ +[ + { + "name": "title", + "label": "Title", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ], + "numberOfFallbacks": 1 + }, + { + "name": "description", + "label": "Description", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "start_time", + "label": "Start time", + "type": [ + "datetime" + ], + "types": [ + "number", + "datetime" + ] + }, + { + "name": "end_time", + "label": "End time", + "type": [ + "datetime" + ], + "types": [ + "number", + "datetime" + ] + }, + { + "name": "service", + "label": "Service", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "monitoring_tool", + "label": "Monitoring tool", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "hosts", + "label": "Hosts", + "type": [ + "string", + "ARRAY" + ], + "types": [ + "string", + "ARRAY", + "number", + "datetime" + ] + }, + { + "name": "severity", + "label": "Severity", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "fingerprint", + "label": "Fingerprint", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + }, + { + "name": "gitlab_environment_name", + "label": "Environment", + "type": [ + "string" + ], + "types": [ + "string", + "number", + "datetime" + ] + } +] diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js index f725712fdd2..f8d2a7aa23b 100644 --- a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js +++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js @@ -3,24 +3,23 @@ import { getPayloadFields, transformForSave, } from '~/alerts_settings/utils/mapping_transformations'; -import gitlabFieldsMock from '~/alerts_settings/components/mocks/gitlabFields.json'; import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; +import alertFields from '../mocks/alertFields.json'; describe('Mapping Transformation Utilities', () => { const nameField = { label: 'Name', path: ['alert', 'name'], - type: 'STRING', + type: 'string', }; const dashboardField = { label: 'Dashboard Id', path: ['alert', 'dashboardId'], - type: 'STRING', + type: 'string', }; describe('getMappingData', () => { it('should return mapping data', () => { - const alertFields = gitlabFieldsMock.slice(0, 3); const result = getMappingData( alertFields, getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)), @@ -51,7 +50,9 @@ describe('Mapping Transformation Utilities', () => { ]; const result = transformForSave(mockMappingData); const { path, type, label } = nameField; - expect(result).toEqual([{ fieldName, path, type, label }]); + expect(result).toEqual([ + { fieldName: fieldName.toUpperCase(), path, type: type.toUpperCase(), label }, + ]); }); it('should return empty array if no mapping provided', () => { diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js index e4782a1dab1..8c914705d97 100644 --- a/spec/frontend/pipelines/stage_spec.js +++ b/spec/frontend/pipelines/stage_spec.js @@ -1,4 +1,6 @@ import 'bootstrap/js/dist/dropdown'; +import $ from 'jquery'; +import { GlDropdown } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; @@ -9,6 +11,7 @@ import { stageReply } from './mock_data'; describe('Pipelines stage component', () => { let wrapper; let mock; + let glFeatures; const defaultProps = { stage: { @@ -22,8 +25,6 @@ describe('Pipelines stage component', () => { updateDropdown: false, }; - const isDropdownOpen = () => wrapper.classes('show'); - const createComponent = (props = {}) => { wrapper = mount(StageComponent, { attachTo: document.body, @@ -31,110 +32,265 @@ describe('Pipelines stage component', () => { ...defaultProps, ...props, }, + provide: { + glFeatures, + }, }); }; beforeEach(() => { mock = new MockAdapter(axios); + jest.spyOn(eventHub, '$emit'); + glFeatures = {}; }); afterEach(() => { wrapper.destroy(); wrapper = null; + eventHub.$emit.mockRestore(); mock.restore(); }); - describe('default', () => { - beforeEach(() => { - createComponent(); + describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => { + const isDropdownOpen = () => wrapper.classes('show'); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a dropdown with the status icon', () => { + expect(wrapper.attributes('class')).toEqual('dropdown'); + expect(wrapper.find('svg').exists()).toBe(true); + expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown'); + }); }); - it('should render a dropdown with the status icon', () => { - expect(wrapper.attributes('class')).toEqual('dropdown'); - expect(wrapper.find('svg').exists()).toBe(true); - expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown'); + describe('with successful request', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + createComponent(); + }); + + it('should render the received data and emit `clickedDropdown` event', async () => { + wrapper.find('button').trigger('click'); + + await axios.waitForAll(); + expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( + stageReply.latest_statuses[0].name, + ); + + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + }); }); - }); - describe('with successful request', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); + it('when request fails should close the dropdown', async () => { + mock.onGet('path.json').reply(500); createComponent(); - }); + wrapper.find({ ref: 'dropdown' }).trigger('click'); - it('should render the received data and emit `clickedDropdown` event', async () => { - jest.spyOn(eventHub, '$emit'); - wrapper.find('button').trigger('click'); + expect(isDropdownOpen()).toBe(true); + wrapper.find('button').trigger('click'); await axios.waitForAll(); - expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( - stageReply.latest_statuses[0].name, - ); - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + expect(isDropdownOpen()).toBe(false); }); - }); - it('when request fails should close the dropdown', async () => { - mock.onGet('path.json').reply(500); - createComponent(); - wrapper.find({ ref: 'dropdown' }).trigger('click'); - expect(isDropdownOpen()).toBe(true); + describe('update endpoint correctly', () => { + beforeEach(() => { + const copyStage = { ...stageReply }; + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + return axios.waitForAll(); + }); + + it('should update the stage to request the new endpoint provided', async () => { + wrapper.find('button').trigger('click'); + await axios.waitForAll(); + + expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( + 'this is the updated content', + ); + }); + }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + }); + + const clickCiAction = async () => { + wrapper.find('button').trigger('click'); + await axios.waitForAll(); + + wrapper.find('.js-ci-action').trigger('click'); + await axios.waitForAll(); + }; + + describe('within pipeline table', () => { + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { + createComponent({ type: 'PIPELINES_TABLE' }); + + await clickCiAction(); + + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }); + }); + + describe('in MR widget', () => { + beforeEach(() => { + jest.spyOn($.fn, 'dropdown'); + }); - wrapper.find('button').trigger('click'); - await axios.waitForAll(); + it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { + createComponent(); - expect(isDropdownOpen()).toBe(false); + await clickCiAction(); + + expect($.fn.dropdown).toHaveBeenCalledWith('toggle'); + }); + }); + }); }); - describe('update endpoint correctly', () => { + describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => { + const findDropdown = () => wrapper.find(GlDropdown); + const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle'); + const findDropdownMenu = () => + wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); + const findCiActionBtn = () => wrapper.find('.js-ci-action'); + + const openGlDropdown = () => { + findDropdownToggle().trigger('click'); + return new Promise((resolve) => { + wrapper.vm.$root.$on('bv::dropdown::show', resolve); + }); + }; + beforeEach(() => { - const copyStage = { ...stageReply }; - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(200, copyStage); - createComponent({ - stage: { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }, + glFeatures = { ciMiniPipelineGlDropdown: true }; + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should render a dropdown with the status icon', () => { + expect(findDropdown().exists()).toBe(true); + expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true); + expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true); }); - return axios.waitForAll(); }); - it('should update the stage to request the new endpoint provided', async () => { - wrapper.find('button').trigger('click'); + describe('with successful request', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + createComponent(); + }); + + it('should render the received data and emit `clickedDropdown` event', async () => { + await openGlDropdown(); + await axios.waitForAll(); + + expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + }); + }); + + it('when request fails should close the dropdown', async () => { + mock.onGet('path.json').reply(500); + + createComponent(); + + await openGlDropdown(); await axios.waitForAll(); - expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain( - 'this is the updated content', - ); + expect(findDropdown().classes('show')).toBe(false); }); - }); - describe('pipelineActionRequestComplete', () => { - beforeEach(() => { - mock.onGet('path.json').reply(200, stageReply); - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + describe('update endpoint correctly', () => { + beforeEach(async () => { + const copyStage = { ...stageReply }; + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + await axios.waitForAll(); + }); - createComponent({ type: 'PIPELINES_TABLE' }); + it('should update the stage to request the new endpoint provided', async () => { + await openGlDropdown(); + await axios.waitForAll(); + + expect(findDropdownMenu().text()).toContain('this is the updated content'); + }); }); - describe('within pipeline table', () => { - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { - jest.spyOn(eventHub, '$emit'); + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + }); - wrapper.find('button').trigger('click'); + const clickCiAction = async () => { + await openGlDropdown(); await axios.waitForAll(); - wrapper.find('.js-ci-action').trigger('click'); + findCiActionBtn().trigger('click'); await axios.waitForAll(); + }; + + describe('within pipeline table', () => { + beforeEach(() => { + createComponent({ type: 'PIPELINES_TABLE' }); + }); + + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { + await clickCiAction(); + + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }); + }); + + describe('in MR widget', () => { + beforeEach(() => { + jest.spyOn($.fn, 'dropdown'); + createComponent(); + }); + + it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { + const hidden = jest.fn(); + + wrapper.vm.$root.$on('bv::dropdown::hide', hidden); + + expect(hidden).toHaveBeenCalledTimes(0); + + await clickCiAction(); - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + expect(hidden).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 8e4da5a317d..b61db537159 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -398,6 +398,45 @@ RSpec.describe ProjectsHelper do helper.send(:get_project_nav_tabs, project, user) end + context 'Security & Compliance tabs' do + before do + stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: feature_flag_enabled) + allow(helper).to receive(:can?).with(user, :read_security_configuration, project).and_return(can_read_security_configuration) + end + + context 'when user cannot read security configuration' do + let(:can_read_security_configuration) { false } + + context 'when feature flag is disabled' do + let(:feature_flag_enabled) { false } + + it { is_expected.not_to include(:security_configuration) } + end + + context 'when feature flag is enabled' do + let(:feature_flag_enabled) { true } + + it { is_expected.not_to include(:security_configuration) } + end + end + + context 'when user can read security configuration' do + let(:can_read_security_configuration) { true } + + context 'when feature flag is disabled' do + let(:feature_flag_enabled) { false } + + it { is_expected.not_to include(:security_configuration) } + end + + context 'when feature flag is enabled' do + let(:feature_flag_enabled) { true } + + it { is_expected.to include(:security_configuration) } + end + end + end + context 'when builds feature is enabled' do before do allow(project).to receive(:builds_enabled?).and_return(true) diff --git a/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb new file mode 100644 index 00000000000..deddc7f5294 --- /dev/null +++ b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::U2fWebauthnConverter do + let_it_be(:u2f_registration) do + device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) + create(:u2f_registration, name: 'u2f_device', + certificate: Base64.strict_encode64(device.cert_raw), + key_handle: U2F.urlsafe_encode64(device.key_handle_raw), + public_key: Base64.strict_encode64(device.origin_public_key_raw)) + end + + it 'converts u2f registration' do + webauthn_credential = WebAuthn::U2fMigrator.new( + app_id: Gitlab.config.gitlab.url, + certificate: u2f_registration.certificate, + key_handle: u2f_registration.key_handle, + public_key: u2f_registration.public_key, + counter: u2f_registration.counter + ).credential + + converted_webauthn = described_class.new(u2f_registration).convert + + expect(converted_webauthn).to( + include(user_id: u2f_registration.user_id, + credential_xid: Base64.strict_encode64(webauthn_credential.id))) + end +end diff --git a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb new file mode 100644 index 00000000000..89e5dffd7b4 --- /dev/null +++ b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::SubgroupBuilder do + let_it_be(:parent_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: parent_group) } + + describe '#build' do + let(:data) { described_class.new(subgroup).build(event) } + let(:event_name) { data[:event_name] } + let(:attributes) do + [ + :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id, + :parent_group_id, :parent_name, :parent_path, :parent_full_path + ] + end + + context 'data' do + shared_examples_for 'includes the required attributes' do + it 'includes the required attributes' do + expect(data).to include(*attributes) + + expect(data[:name]).to eq(subgroup.name) + expect(data[:path]).to eq(subgroup.path) + expect(data[:full_path]).to eq(subgroup.full_path) + expect(data[:group_id]).to eq(subgroup.id) + expect(data[:created_at]).to eq(subgroup.created_at.xmlschema) + expect(data[:updated_at]).to eq(subgroup.updated_at.xmlschema) + expect(data[:parent_name]).to eq(parent_group.name) + expect(data[:parent_path]).to eq(parent_group.path) + expect(data[:parent_full_path]).to eq(parent_group.full_path) + expect(data[:parent_group_id]).to eq(parent_group.id) + end + end + + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('subgroup_create') } + it_behaves_like 'includes the required attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('subgroup_destroy') } + it_behaves_like 'includes the required attributes' + end + end + end +end diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb index 77a1377c138..476d99364b6 100644 --- a/spec/models/project_services/chat_notification_service_spec.rb +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -75,6 +75,39 @@ RSpec.describe ChatNotificationService do end end + context 'when the data object has a label' do + let(:label) { create(:label, project: project, name: 'Bug')} + let(:issue) { create(:labeled_issue, project: project, labels: [label]) } + let(:note) { create(:note, noteable: issue, project: project)} + let(:data) { Gitlab::DataBuilder::Note.build(note, user) } + + it 'notifies the chat service' do + expect(chat_service).to receive(:notify).with(any_args) + + chat_service.execute(data) + end + + context 'and the chat_service has a label filter that does not matches the label' do + subject(:chat_service) { described_class.new(labels_to_be_notified: '~some random label') } + + it 'does not notify the chat service' do + expect(chat_service).not_to receive(:notify) + + chat_service.execute(data) + end + end + + context 'and the chat_service has a label filter that matches the label' do + subject(:chat_service) { described_class.new(labels_to_be_notified: '~Backend, ~Bug') } + + it 'notifies the chat service' do + expect(chat_service).to receive(:notify).with(any_args) + + chat_service.execute(data) + end + end + end + context 'with "channel" property' do before do allow(chat_service).to receive(:channel).and_return(channel) diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb new file mode 100644 index 00000000000..1f2e4d1e447 --- /dev/null +++ b/spec/models/u2f_registration_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe U2fRegistration do + let_it_be(:user) { create(:user) } + let(:u2f_registration) do + device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) + create(:u2f_registration, name: 'u2f_device', + user: user, + certificate: Base64.strict_encode64(device.cert_raw), + key_handle: U2F.urlsafe_encode64(device.key_handle_raw), + public_key: Base64.strict_encode64(device.origin_public_key_raw)) + end + + describe 'callbacks' do + describe '#create_webauthn_registration' do + it 'creates webauthn registration' do + u2f_registration.save! + + webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id) + expect(webauthn_registration).to exist + end + + it 'logs error' do + allow(Gitlab::Auth::U2fWebauthnConverter).to receive(:new).and_raise('boom!') + expect(Gitlab::AppJsonLogger).to( + receive(:error).with(a_hash_including(event: 'u2f_migration', + error: 'RuntimeError', + message: 'U2F to WebAuthn conversion failed')) + ) + + u2f_registration.save! + end + end + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 6a4a7ecd13e..efa1afb758f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -865,6 +865,28 @@ RSpec.describe ProjectPolicy do end end + context 'security configuration feature' do + %w(guest reporter).each do |role| + context role do + let(:current_user) { send(role) } + + it 'prevents reading security configuration' do + expect_disallowed(:read_security_configuration) + end + end + end + + %w(developer maintainer owner).each do |role| + context role do + let(:current_user) { send(role) } + + it 'allows reading security configuration' do + expect_allowed(:read_security_configuration) + end + end + end + end + describe 'design permissions' do let(:current_user) { guest } diff --git a/spec/routing/projects/security/configuration_controller_routing_spec.rb b/spec/routing/projects/security/configuration_controller_routing_spec.rb new file mode 100644 index 00000000000..c2b10a49dea --- /dev/null +++ b/spec/routing/projects/security/configuration_controller_routing_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Security::ConfigurationController, 'routing' do + let(:base_params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } } + + before do + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true) + end + + it 'routes to #show' do + expect(get('/gitlab/gitlabhq/-/security/configuration')).to route_to('projects/security/configuration#show', namespace_id: 'gitlab', project_id: 'gitlabhq') + end +end diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb index a2131c5c1b0..ed234376381 100644 --- a/spec/services/resource_events/change_milestone_service_spec.rb +++ b/spec/services/resource_events/change_milestone_service_spec.rb @@ -6,8 +6,8 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do let_it_be(:timebox) { create(:milestone) } let(:created_at_time) { Time.utc(2019, 12, 30) } - let(:add_timebox_args) { { created_at: created_at_time, old_milestone: nil } } - let(:remove_timebox_args) { { created_at: created_at_time, old_milestone: timebox } } + let(:add_timebox_args) { { old_milestone: nil } } + let(:remove_timebox_args) { { old_milestone: timebox } } [:issue, :merge_request].each do |issuable| it_behaves_like 'timebox(milestone or iteration) resource events creator', ResourceMilestoneEvent do diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 57d8320b76a..3fd4f2698e9 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -18,7 +18,8 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Security & Compliance'), nav_sub_items: [ - _('Audit Events') + _('Configuration'), + (_('Audit Events') if Gitlab.ee?) ] } end @@ -71,7 +72,7 @@ RSpec.shared_context 'project navbar structure' do _('Schedules') ] }, - (security_and_compliance_nav_item if Gitlab.ee?), + security_and_compliance_nav_item, { nav_item: _('Operations'), nav_sub_items: [ @@ -190,7 +191,7 @@ RSpec.shared_context 'group navbar structure' do nav_item: _('Merge Requests'), nav_sub_items: [] }, - (security_and_compliance_nav_item if Gitlab.ee?), + security_and_compliance_nav_item, (push_rules_nav_item if Gitlab.ee?), { nav_item: _('Kubernetes'), diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb index 7d42bda7090..fac9f1d6253 100644 --- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb @@ -3,6 +3,10 @@ RSpec.shared_examples 'timebox(milestone or iteration) resource events creator' do |timebox_event_class| let_it_be(:user) { create(:user) } + before do + resource.system_note_timestamp = created_at_time + end + context 'when milestone/iteration is added' do let(:service) { described_class.new(resource, user, **add_timebox_args) } diff --git a/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb new file mode 100644 index 00000000000..d3fb35bff6d --- /dev/null +++ b/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'layouts/nav/sidebar/_project_security_link' do + let_it_be_with_reload(:project) { create(:project) } + context 'on security configuration' do + before do + assign(:project, project) + allow(controller).to receive(:controller_name).and_return('configuration') + allow(controller).to receive(:controller_path).and_return('projects/security/configuration') + allow(controller).to receive(:action_name).and_return('show') + allow(view).to receive(:any_project_nav_tab?).and_return(true) + allow(view).to receive(:project_nav_tab?).and_return(true) + end + + it 'activates Security & Compliance tab' do + render + + expect(rendered).to have_css('li.active', text: 'Security & Compliance') + end + + it 'activates Configuration sub tab' do + render + + expect(rendered).to have_css('.sidebar-sub-level-items > li.active', text: 'Configuration') + end + end +end |