diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-15 18:13:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-15 18:13:49 +0300 |
commit | 229395d3af51cd46a9179f2eba142c027d08b208 (patch) | |
tree | 56efbeeb1bb9bf8e6f68174436cacd5c2d20fca4 /spec | |
parent | 3107fe7203685580829c7d6ca56161e13acb83eb (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
21 files changed, 457 insertions, 358 deletions
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 46fd524adbb..9b721d8cfca 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -164,4 +164,16 @@ RSpec.describe Settings do end end end + + describe '.microsoft_graph_mailer' do + it 'defaults' do + expect(described_class.microsoft_graph_mailer.enabled).to be false + expect(described_class.microsoft_graph_mailer.user_id).to be_nil + expect(described_class.microsoft_graph_mailer.tenant).to be_nil + expect(described_class.microsoft_graph_mailer.client_id).to be_nil + expect(described_class.microsoft_graph_mailer.client_secret).to be_nil + expect(described_class.microsoft_graph_mailer.azure_ad_endpoint).to eq('https://login.microsoftonline.com') + expect(described_class.microsoft_graph_mailer.graph_endpoint).to eq('https://graph.microsoft.com') + end + end end diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 83c11f5d356..5133c02b190 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -29,7 +29,6 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue'; - import { CREATED_DESC, RELATIVE_POSITION, @@ -58,9 +57,11 @@ import { WORK_ITEM_TYPE_ENUM_TASK, WORK_ITEM_TYPE_ENUM_TEST_CASE, } from '~/work_items/constants'; - import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import('~/issuable/bulk_update_sidebar'); +import('~/users_select'); + jest.mock('@sentry/browser'); jest.mock('~/flash'); jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() })); diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js deleted file mode 100644 index f17d66c7ef4..00000000000 --- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { __ } from '~/locale'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue'; -import RunnerTags from '~/runner/components/runner_tags.vue'; -import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -const mockId = '1'; -const mockShortSha = '2P6oDVDm'; -const mockDescription = 'runner-1'; -const mockIpAddress = '0.0.0.0'; -const mockTagList = ['shell', 'linux']; - -describe('RunnerTypeCell', () => { - let wrapper; - - const findLockIcon = () => wrapper.findByTestId('lock-icon'); - const findRunnerTags = () => wrapper.findComponent(RunnerTags); - - const createComponent = (runner, options) => { - wrapper = mountExtended(RunnerSummaryCell, { - propsData: { - runner: { - id: `gid://gitlab/Ci::Runner/${mockId}`, - shortSha: mockShortSha, - description: mockDescription, - ipAddress: mockIpAddress, - runnerType: INSTANCE_TYPE, - tagList: mockTagList, - ...runner, - }, - }, - ...options, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays the runner name as id and short token', () => { - expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`); - }); - - it('Displays the runner type', () => { - expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE); - }); - - it('Does not display the locked icon', () => { - expect(findLockIcon().exists()).toBe(false); - }); - - it('Displays the locked icon for locked runners', () => { - createComponent({ - runnerType: PROJECT_TYPE, - locked: true, - }); - - expect(findLockIcon().exists()).toBe(true); - }); - - it('Displays the runner description', () => { - expect(wrapper.text()).toContain(mockDescription); - }); - - it('Displays ip address', () => { - expect(wrapper.text()).toContain(`${__('IP Address')} ${mockIpAddress}`); - }); - - it('Displays no ip address', () => { - createComponent({ - ipAddress: null, - }); - - expect(wrapper.text()).not.toContain(__('IP Address')); - }); - - it('Displays tag list', () => { - expect(findRunnerTags().props('tagList')).toEqual(mockTagList); - }); - - it('Displays a custom slot', () => { - const slotContent = 'My custom runner summary'; - - createComponent( - {}, - { - slots: { - 'runner-name': slotContent, - }, - }, - ); - - expect(wrapper.text()).toContain(slotContent); - }); -}); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 1e434ae683f..54a9e713721 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -65,9 +65,6 @@ describe('RunnerList', () => { expect(headerLabels).toEqual([ 'Status', 'Runner', - 'Version', - 'Jobs', - 'Last contact', '', // actions has no label ]); }); @@ -87,23 +84,28 @@ describe('RunnerList', () => { }); it('Displays details of a runner', () => { - const { id, description, version, shortSha } = mockRunners[0]; - createComponent({}, mountExtended); + const { id, description, version, shortSha } = mockRunners[0]; + const numericId = getIdFromGraphQLId(id); + // Badges - expect(findCell({ fieldKey: 'status' }).text()).toBe(I18N_STATUS_NEVER_CONTACTED); + expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( + I18N_STATUS_NEVER_CONTACTED, + ); // Runner summary - expect(findCell({ fieldKey: 'summary' }).text()).toContain( - `#${getIdFromGraphQLId(id)} (${shortSha})`, - ); - expect(findCell({ fieldKey: 'summary' }).text()).toContain(description); + const summary = findCell({ fieldKey: 'summary' }).text(); - // Other fields - expect(findCell({ fieldKey: 'version' }).text()).toBe(version); - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0'); - expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String)); + expect(summary).toContain(`#${numericId} (${shortSha})`); + expect(summary).toContain(I18N_PROJECT_TYPE); + + expect(summary).toContain(version); + expect(summary).toContain(description); + + expect(summary).toContain('Last contact'); + expect(summary).toContain('0'); // job count + expect(summary).toContain('Created'); // Actions expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); @@ -162,42 +164,6 @@ describe('RunnerList', () => { }); }); - describe('Table data formatting', () => { - let mockRunnersCopy; - - beforeEach(() => { - mockRunnersCopy = [ - { - ...mockRunners[0], - }, - ]; - }); - - it('Formats job counts', () => { - mockRunnersCopy[0].jobCount = 1; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1'); - }); - - it('Formats large job counts', () => { - mockRunnersCopy[0].jobCount = 1000; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000'); - }); - - it('Formats large job counts with a plus symbol', () => { - mockRunnersCopy[0].jobCount = 1001; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+'); - }); - }); - it('Shows runner identifier', () => { const { id, shortSha } = mockRunners[0]; const numericId = getIdFromGraphQLId(id); @@ -226,62 +192,4 @@ describe('RunnerList', () => { expect(findSkeletonLoader().exists()).toBe(false); }); }); - - describe.each` - glFeatures - ${{ runnerListStackedLayoutAdmin: true }} - ${{ runnerListStackedLayout: true }} - `('When glFeatures = $glFeatures', ({ glFeatures }) => { - beforeEach(() => { - createComponent( - { - stubs: { - RunnerStatusPopover: { - template: '<div/>', - }, - }, - provide: { - glFeatures, - }, - }, - mountExtended, - ); - }); - - it('Displays stacked list headers', () => { - const headerLabels = findHeaders().wrappers.map((w) => w.text()); - - expect(headerLabels).toEqual([ - 'Status', - 'Runner', - '', // actions has no label - ]); - }); - - it('Displays stacked details of a runner', () => { - const { id, description, version, shortSha } = mockRunners[0]; - const numericId = getIdFromGraphQLId(id); - - // Badges - expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( - I18N_STATUS_NEVER_CONTACTED, - ); - - // Runner summary - const summary = findCell({ fieldKey: 'summary' }).text(); - - expect(summary).toContain(`#${numericId} (${shortSha})`); - expect(summary).toContain(I18N_PROJECT_TYPE); - - expect(summary).toContain(version); - expect(summary).toContain(description); - - expect(summary).toContain('Last contact'); - expect(summary).toContain('0'); // job count - expect(summary).toContain('Created'); - - // Actions - expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); - }); - }); }); diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js index b892cfc7d3d..1a8aced9292 100644 --- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js +++ b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import { nextTick } from 'vue'; import { GlBanner } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue'; @@ -16,42 +16,24 @@ describe('RunnerStackedLayoutBanner', () => { }); }; - it('Does not display a banner', () => { + it('Displays a banner', () => { createComponent(); - expect(findBanner().exists()).toBe(false); - }); - - describe.each` - glFeatures - ${{ runnerListStackedLayoutAdmin: true }} - ${{ runnerListStackedLayout: true }} - `('When glFeatures = $glFeatures', ({ glFeatures }) => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures, - }, - }); - }); - - it('Displays a banner', () => { - expect(findBanner().props()).toMatchObject({ - svgPath: expect.stringContaining('data:image/svg+xml;utf8,'), - title: expect.any(String), - buttonText: expect.any(String), - buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'), - }); - expect(findLocalStorageSync().exists()).toBe(true); + expect(findBanner().props()).toMatchObject({ + svgPath: expect.stringContaining('data:image/svg+xml;utf8,'), + title: expect.any(String), + buttonText: expect.any(String), + buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'), }); + expect(findLocalStorageSync().exists()).toBe(true); + }); - it('Does not display a banner when dismissed', async () => { - findLocalStorageSync().vm.$emit('input', true); + it('Does not display a banner when dismissed', async () => { + findLocalStorageSync().vm.$emit('input', true); - await Vue.nextTick(); + await nextTick(); - expect(findBanner().exists()).toBe(false); - expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal - }); + expect(findBanner().exists()).toBe(false); + expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal }); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index d843da4da5b..e5594b6d37e 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -164,8 +164,7 @@ describe('IssuableEditForm', () => { const titleInputEl = wrapper.findComponent(GlFormInput); titleInputEl.vm.$emit('keydown', eventObj, 'title'); - - expect(wrapper.emitted('keydown-title')).toBeTruthy(); + expect(wrapper.emitted('keydown-title')).toHaveLength(1); expect(wrapper.emitted('keydown-title')[0]).toMatchObject([ eventObj, { @@ -179,8 +178,7 @@ describe('IssuableEditForm', () => { const descriptionInputEl = wrapper.find('[data-testid="description"] textarea'); descriptionInputEl.trigger('keydown', eventObj, 'description'); - - expect(wrapper.emitted('keydown-description')).toBeTruthy(); + expect(wrapper.emitted('keydown-description')).toHaveLength(1); expect(wrapper.emitted('keydown-description')[0]).toMatchObject([ eventObj, { diff --git a/spec/initializers/microsoft_graph_mailer_spec.rb b/spec/initializers/microsoft_graph_mailer_spec.rb new file mode 100644 index 00000000000..fbe667e34fe --- /dev/null +++ b/spec/initializers/microsoft_graph_mailer_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'microsoft_graph_mailer initializer for GitLab' do + let(:microsoft_graph_setting) do + { + user_id: SecureRandom.hex, + tenant: SecureRandom.hex, + client_id: SecureRandom.hex, + client_secret: SecureRandom.hex, + azure_ad_endpoint: 'https://test-azure_ad_endpoint', + graph_endpoint: 'https://test-graph_endpoint' + } + end + + def load_microsoft_graph_mailer_initializer + load Rails.root.join('config/initializers/microsoft_graph_mailer.rb') + end + + context 'when microsoft_graph_mailer is enabled' do + before do + stub_microsoft_graph_mailer_setting(microsoft_graph_setting.merge(enabled: true)) + end + + it 'configures ActionMailer' do + previous_delivery_method = ActionMailer::Base.delivery_method + previous_microsoft_graph_settings = ActionMailer::Base.microsoft_graph_settings + + load_microsoft_graph_mailer_initializer + + expect(ActionMailer::Base.delivery_method).to eq(:microsoft_graph) + expect(ActionMailer::Base.microsoft_graph_settings).to eq(microsoft_graph_setting) + ensure + ActionMailer::Base.delivery_method = previous_delivery_method + ActionMailer::Base.microsoft_graph_settings = previous_microsoft_graph_settings + end + end + + context 'when microsoft_graph_mailer is disabled' do + before do + stub_microsoft_graph_mailer_setting(microsoft_graph_setting.merge(enabled: false)) + end + + it 'does not configure ActionMailer' do + previous_delivery_method = ActionMailer::Base.delivery_method + previous_microsoft_graph_settings = ActionMailer::Base.microsoft_graph_settings + + load_microsoft_graph_mailer_initializer + + expect(previous_microsoft_graph_settings).not_to eq(:microsoft_graph) + expect(ActionMailer::Base.delivery_method).to eq(previous_delivery_method) + expect(ActionMailer::Base.microsoft_graph_settings).to eq(previous_microsoft_graph_settings) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb new file mode 100644 index 00000000000..29833074109 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +# todo: this will need to specify schema version once we introduce the not null constraint on issues#namespace_id +# https://gitlab.com/gitlab-org/gitlab/-/issues/367835 +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + + let(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') } + let(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'space2') } + + let(:proj_namespace1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id) } + let(:proj_namespace2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace2.id) } + + # rubocop:disable Layout/LineLength + let(:proj1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id) } + let(:proj2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: namespace2.id, project_namespace_id: proj_namespace2.id) } + + let!(:proj1_issue_with_namespace) { issues.create!(title: 'issue1', project_id: proj1.id, namespace_id: proj_namespace1.id) } + let!(:proj1_issue_without_namespace1) { issues.create!(title: 'issue2', project_id: proj1.id) } + let!(:proj1_issue_without_namespace2) { issues.create!(title: 'issue3', project_id: proj1.id) } + let!(:proj2_issue_with_namespace) { issues.create!(title: 'issue4', project_id: proj2.id, namespace_id: proj_namespace2.id) } + let!(:proj2_issue_without_namespace1) { issues.create!(title: 'issue5', project_id: proj2.id) } + let!(:proj2_issue_without_namespace2) { issues.create!(title: 'issue6', project_id: proj2.id) } + # rubocop:enable Layout/LineLength + + let(:migration) do + described_class.new( + start_id: proj1_issue_with_namespace.id, + end_id: proj2_issue_without_namespace2.id, + batch_table: :issues, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 2, + connection: ApplicationRecord.connection + ) + end + + subject(:perform_migration) { migration.perform } + + it 'backfills namespace_id for the selected records', :aggregate_failures do + perform_migration + + expected_namespaces = [proj_namespace1.id, proj_namespace2.id] + + expect(issues.where.not(namespace_id: nil).count).to eq(6) + expect(issues.where.not(namespace_id: nil).pluck(:namespace_id).uniq).to match_array(expected_namespaces) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e1ea5c2d825..9a87911b6e8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -420,6 +420,26 @@ RSpec.describe Gitlab::Git::Repository do end end + describe '#delete_branch' do + let(:repository) { mutable_repository } + + it 'deletes a branch' do + expect(repository.find_branch('feature')).not_to be_nil + + repository.delete_branch('feature') + + expect(repository.find_branch('feature')).to be_nil + end + + it 'deletes a fully qualified branch' do + expect(repository.find_branch('feature')).not_to be_nil + + repository.delete_branch('refs/heads/feature') + + expect(repository.find_branch('feature')).to be_nil + end + end + describe '#delete_refs' do let(:repository) { mutable_repository } diff --git a/spec/migrations/backfill_namespace_id_on_issues_spec.rb b/spec/migrations/backfill_namespace_id_on_issues_spec.rb new file mode 100644 index 00000000000..2721d7ce8f1 --- /dev/null +++ b/spec/migrations/backfill_namespace_id_on_issues_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillNamespaceIdOnIssues, :migration do + let(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules background jobs for each batch of issues' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :issues, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + max_batch_size: described_class::MAX_BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index bc4ba33067f..ec03030a4b8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1599,7 +1599,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'track artifact report' do - let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, status: :running) } + let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, status: :running, user: create(:user)) } context 'when transitioning to completed status' do %i[drop! skip! succeed! cancel!].each do |command| diff --git a/spec/requests/api/resource_state_events_spec.rb b/spec/requests/api/resource_state_events_spec.rb index 46ca9874395..5f756bc6c63 100644 --- a/spec/requests/api/resource_state_events_spec.rb +++ b/spec/requests/api/resource_state_events_spec.rb @@ -6,87 +6,8 @@ RSpec.describe API::ResourceStateEvents do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) } - before_all do - project.add_developer(user) - end - - shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name| - describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do - let!(:event) { create_event } - - it "returns an array of resource state events" do - url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events" - get api(url, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(event.id) - expect(json_response.first['state']).to eq(event.state.to_s) - end - - it "returns a 404 error when eventable id not found" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns 404 when not authorized" do - parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - private_user = create(:user) - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do - let!(:event) { create_event } - - it "returns a resource state event by id" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['id']).to eq(event.id) - expect(json_response['state']).to eq(event.state.to_s) - end - - it "returns 404 when not authorized" do - parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - private_user = create(:user) - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 error if resource state event not found" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe 'pagination' do - # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 - it 'returns the second page' do - create_event - event2 = create_event - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq '2' - expect(json_response.count).to eq(1) - expect(json_response.first['id']).to eq(event2.id) - end - end - - def create_event(state: :opened) - create(:resource_state_event, eventable.class.name.underscore => eventable, state: state) - end + before do + parent.add_developer(user) end context 'when eventable is an Issue' do diff --git a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb index 1abf8eb562c..6d9fc4c8e34 100644 --- a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb +++ b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb @@ -6,10 +6,10 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do describe '#execute', :clean_gitlab_redis_shared_state do let_it_be(:group) { create(:group, :private) } let_it_be(:project) { create(:project, group: group) } - let_it_be(:user) { create(:user) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } let(:test_event_name) { 'i_testing_test_report_uploaded' } - let(:values_delimiter) { '_' } let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter } let(:start_time) { 1.week.ago } let(:end_time) { 1.week.from_now } @@ -17,7 +17,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do subject(:track_artifact_report) { described_class.new.execute(pipeline) } context 'when pipeline has test reports' do - let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) } before do 2.times do @@ -28,7 +28,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do it 'tracks the event using HLLRedisCounter' do allow(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(test_event_name, values: [pipeline.id, user.id].join(values_delimiter)) + .with(test_event_name, values: user1.id) .and_call_original expect { track_artifact_report } @@ -53,19 +53,57 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do end end - context 'when multiple pipelines have test reports' do - let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user) } - let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user) } + context 'when a single user started multiple pipelines with test reports' do + let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) } + let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) } - it 'tracks all pipelines using HLLRedisCounter' do + it 'tracks all pipelines using HLLRedisCounter by one user_id' do allow(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(test_event_name, values: [pipeline1.id, user.id].join(values_delimiter)) + .with(test_event_name, values: user1.id) .and_call_original allow(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(test_event_name, values: [pipeline2.id, user.id].join(values_delimiter)) + .with(test_event_name, values: user1.id) + .and_call_original + + expect do + described_class.new.execute(pipeline1) + described_class.new.execute(pipeline2) + end + .to change { + counter.unique_events(event_names: test_event_name, + start_date: start_time, + end_date: end_time) + } + .by 1 + end + end + + context 'when multiple users started multiple pipelines with test reports' do + let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) } + let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user2) } + + it 'tracks all pipelines using HLLRedisCounter by multiple users' do + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user1.id) + .and_call_original + + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user1.id) + .and_call_original + + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user2.id) + .and_call_original + + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user2.id) .and_call_original expect do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a40f19ecf7c..c75f651fb92 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -93,25 +93,6 @@ RSpec.configure do |config| config.full_backtrace = true end - # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/297359 - if ENV['CI'] - config.after do |example| - if example.exception.is_a?(GRPC::Unavailable) - warn "=== gRPC unavailable detected, process list:" - processes = `ps -ef | grep toml` - warn processes - warn "=== free memory" - warn `free -m` - warn "=== uptime" - warn `uptime` - warn "=== Prometheus metrics:" - warn `curl -s -o log/gitaly-metrics.log http://localhost:9236/metrics` - warn "=== Taking goroutine dump in log/goroutines.log..." - warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2` - end - end - end - # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531 config.after do |example| if example.exception.is_a?(Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError) diff --git a/spec/support/helpers/html_escaped_helpers.rb b/spec/support/helpers/html_escaped_helpers.rb new file mode 100644 index 00000000000..7f6825e9598 --- /dev/null +++ b/spec/support/helpers/html_escaped_helpers.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module HtmlEscapedHelpers + extend self + + # Checks if +content+ contains HTML escaped tags and returns its match. + # + # It matches escaped opening and closing tags `<<name>` and + # `</<name>`. The match is discarded if the tag is inside a quoted + # attribute value. + # Foor example, `<div title="We allow # <b>bold</b>">`. + # + # @return [MatchData, nil] Returns the match or +nil+ if no match was found. + def match_html_escaped_tags(content) + match_data = %r{<\s*(?:/\s*)?\w+}.match(content) + return unless match_data + + # Escaped HTML tags are allowed inside quoted attribute values like: + # `title="Press <back>"` + return if %r{=\s*["'][^>]*\z}.match?(match_data.pre_match) + + match_data + end +end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 20f46396424..c08e35912c3 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -104,6 +104,10 @@ module StubConfiguration .to receive(:sentry_clientside_dsn) { clientside_dsn } end + def stub_microsoft_graph_mailer_setting(messages) + allow(Gitlab.config.microsoft_graph_mailer).to receive_messages(to_settings(messages)) + end + def stub_kerberos_setting(messages) allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages)) end diff --git a/spec/support/shared_contexts/views/html_safe_render_shared_context.rb b/spec/support/shared_contexts/views/html_safe_render_shared_context.rb new file mode 100644 index 00000000000..3acca60c901 --- /dev/null +++ b/spec/support/shared_contexts/views/html_safe_render_shared_context.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_context 'when rendered view has no HTML escapes', type: :view do + # Check once per example if `rendered` contains HTML escapes. + let(:rendered) do |example| + super().tap do |rendered| + next if example.metadata[:skip_html_escaped_tags_check] + + ensure_no_html_escaped_tags!(rendered, example) + end + end + + def ensure_no_html_escaped_tags!(content, example) + match_data = HtmlEscapedHelpers.match_html_escaped_tags(content) + return unless match_data + + # Truncate + pre_match = match_data.pre_match.last(50) + match = match_data[0] + post_match = match_data.post_match.first(50) + + string = "#{pre_match}«#{match}»#{post_match}" + + raise <<~MESSAGE + The following string contains HTML escaped tags: + + #{string} + + Please consider using `.html_safe`. + + This check can be disabled via: + + it #{example.description.inspect}, :skip_html_escaped_tags_check do + ... + end + + MESSAGE + end +end diff --git a/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb new file mode 100644 index 00000000000..c1850a0d0c9 --- /dev/null +++ b/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name| + let(:base_path) { "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}" } + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do + let!(:event) { create_event } + + it "returns an array of resource state events" do + url = "#{base_path}/resource_state_events" + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + expect(json_response.first['state']).to eq(event.state.to_s) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("#{base_path}/resource_state_events", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do + let!(:event) { create_event } + + it "returns a resource state event by id" do + get api("#{base_path}/resource_state_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(event.id) + expect(json_response['state']).to eq(event.state.to_s) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("#{base_path}/resource_state_events/#{event.id}", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if resource state event not found" do + get api("#{base_path}/resource_state_events/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'pagination' do + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it 'returns the second page' do + create_event + event2 = create_event + + get api("#{base_path}/resource_state_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq '2' + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + + def create_event(state: :opened) + create(:resource_state_event, eventable.class.name.underscore => eventable, state: state) + end +end diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb index 716bee39fca..a7e51408032 100644 --- a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'filters by paginated notes' do |event_type| - let(:event) { create(event_type) } # rubocop:disable Rails/SaveBang + let(:event) { create(event_type, issue: create(:issue)) } before do create(event_type, issue: event.issue) diff --git a/spec/support_specs/helpers/html_escaped_helpers_spec.rb b/spec/support_specs/helpers/html_escaped_helpers_spec.rb new file mode 100644 index 00000000000..337f7ecc659 --- /dev/null +++ b/spec/support_specs/helpers/html_escaped_helpers_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +require_relative '../../support/helpers/html_escaped_helpers' + +RSpec.describe HtmlEscapedHelpers do + using RSpec::Parameterized::TableSyntax + + describe '#match_html_escaped_tags' do + let(:actual_match) { actual_match_data && actual_match_data[0] } + + subject(:actual_match_data) { described_class.match_html_escaped_tags(content) } + + where(:content, :expected_match) do + nil | nil + '' | nil + '<a href' | nil + '<span href' | nil + '</a>' | nil + '<a href' | '<a' + '<span href' | '<span' + '< span' | '< span' + 'some text <a href' | '<a' + 'some text "<a href' | '<a' + '</a&glt;' | '</a' + '</span>' | '</span' + '< / span>' | '< / span' + 'title="<a href' | nil + 'title= "<a href' | nil + "title= '<a href" | nil + "title= '</a" | nil + "title= '</span" | nil + 'title="foo"><a' | '<a' + "title='foo'>\n<a" | '<a' + end + + with_them do + specify { expect(actual_match).to eq(expected_match) } + end + end +end diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb index 7c171ee65b9..7f537022445 100644 --- a/spec/views/projects/imports/new.html.haml_spec.rb +++ b/spec/views/projects/imports/new.html.haml_spec.rb @@ -14,7 +14,7 @@ RSpec.describe "projects/imports/new.html.haml" do project.add_maintainer(user) end - it "escapes HTML in import errors" do + it "escapes HTML in import errors", :skip_html_escaped_tags_check do assign(:project, project) render |