Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-15 18:13:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-15 18:13:49 +0300
commit229395d3af51cd46a9179f2eba142c027d08b208 (patch)
tree56efbeeb1bb9bf8e6f68174436cacd5c2d20fca4 /spec
parent3107fe7203685580829c7d6ca56161e13acb83eb (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/config/settings_spec.rb12
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js5
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js99
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js124
-rw-r--r--spec/frontend/runner/components/runner_stacked_layout_banner_spec.js46
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js6
-rw-r--r--spec/initializers/microsoft_graph_mailer_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb57
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb20
-rw-r--r--spec/migrations/backfill_namespace_id_on_issues_spec.rb32
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/requests/api/resource_state_events_spec.rb83
-rw-r--r--spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb58
-rw-r--r--spec/spec_helper.rb19
-rw-r--r--spec/support/helpers/html_escaped_helpers.rb24
-rw-r--r--spec/support/helpers/stub_configuration.rb4
-rw-r--r--spec/support/shared_contexts/views/html_safe_render_shared_context.rb39
-rw-r--r--spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb2
-rw-r--r--spec/support_specs/helpers/html_escaped_helpers_spec.rb43
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb2
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 `&lt;<name>` and
+ # `&lt;/<name>`. The match is discarded if the tag is inside a quoted
+ # attribute value.
+ # Foor example, `<div title="We allow # &lt;b&gt;bold&lt;/b&gt;">`.
+ #
+ # @return [MatchData, nil] Returns the match or +nil+ if no match was found.
+ def match_html_escaped_tags(content)
+ match_data = %r{&lt;\s*(?:/\s*)?\w+}.match(content)
+ return unless match_data
+
+ # Escaped HTML tags are allowed inside quoted attribute values like:
+ # `title="Press &lt;back&gt;"`
+ 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
+ '&lt;a href' | '&lt;a'
+ '&lt;span href' | '&lt;span'
+ '&lt; span' | '&lt; span'
+ 'some text &lt;a href' | '&lt;a'
+ 'some text "&lt;a href' | '&lt;a'
+ '&lt;/a&glt;' | '&lt;/a'
+ '&lt;/span&gt;' | '&lt;/span'
+ '&lt; / span&gt;' | '&lt; / span'
+ 'title="&lt;a href' | nil
+ 'title= "&lt;a href' | nil
+ "title= '&lt;a href" | nil
+ "title= '&lt;/a" | nil
+ "title= '&lt;/span" | nil
+ 'title="foo">&lt;a' | '&lt;a'
+ "title='foo'>\n&lt;a" | '&lt;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