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:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb121
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js392
-rw-r--r--spec/lib/gitlab/ci/pipeline/duration_spec.rb204
-rw-r--r--spec/lib/gitlab/pages/random_domain_spec.rb44
-rw-r--r--spec/lib/object_storage/config_spec.rb7
-rw-r--r--spec/models/concerns/has_user_type_spec.rb4
-rw-r--r--spec/models/project_setting_spec.rb38
-rw-r--r--spec/models/project_spec.rb185
-rw-r--r--spec/policies/global_policy_spec.rb31
-rw-r--r--spec/services/markup/rendering_service_spec.rb17
-rw-r--r--spec/services/projects/update_service_spec.rb106
13 files changed, 790 insertions, 367 deletions
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 136f98ac907..ded5dd57e3e 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PagesController do
+RSpec.describe Projects::PagesController, feature_category: :pages do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -14,7 +14,12 @@ RSpec.describe Projects::PagesController do
end
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_config(pages: {
+ enabled: true,
+ external_https: true,
+ access_control: false
+ })
+
sign_in(user)
project.add_maintainer(user)
end
@@ -123,49 +128,99 @@ RSpec.describe Projects::PagesController do
end
describe 'PATCH update' do
- let(:request_params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- project: { pages_https_only: 'false' }
- }
- end
+ context 'when updating pages_https_only' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: { pages_https_only: 'true' }
+ }
+ end
- let(:update_service) { double(execute: { status: :success }) }
+ it 'updates project field and redirects back to the pages settings' do
+ project.update!(pages_https_only: false)
- before do
- allow(Projects::UpdateService).to receive(:new) { update_service }
- end
+ expect { patch :update, params: request_params }
+ .to change { project.reload.pages_https_only }
+ .from(false).to(true)
- it 'returns 302 status' do
- patch :update, params: request_params
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(project_pages_path(project))
+ end
- expect(response).to have_gitlab_http_status(:found)
- end
+ context 'when it fails to update' do
+ it 'adds an error message' do
+ expect_next_instance_of(Projects::UpdateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return(status: :error, message: 'some error happened')
+ end
- it 'redirects back to the pages settings' do
- patch :update, params: request_params
+ expect { patch :update, params: request_params }
+ .not_to change { project.reload.pages_https_only }
- expect(response).to redirect_to(project_pages_path(project))
+ expect(response).to redirect_to(project_pages_path(project))
+ expect(flash[:alert]).to eq('some error happened')
+ end
+ end
end
- it 'calls the update service' do
- expect(Projects::UpdateService)
- .to receive(:new)
- .with(project, user, ActionController::Parameters.new(request_params[:project]).permit!)
- .and_return(update_service)
+ context 'when updating pages_unique_domain' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: {
+ project_setting_attributes: {
+ pages_unique_domain_enabled: 'true'
+ }
+ }
+ }
+ end
- patch :update, params: request_params
- end
+ before do
+ create(:project_setting, project: project, pages_unique_domain_enabled: false)
+ end
- context 'when update_service returns an error message' do
- let(:update_service) { double(execute: { status: :error, message: 'some error happened' }) }
+ context 'with pages_unique_domain feature flag disabled' do
+ it 'does not update pages unique domain' do
+ stub_feature_flags(pages_unique_domain: false)
- it 'adds an error message' do
- patch :update, params: request_params
+ expect { patch :update, params: request_params }
+ .not_to change { project.project_setting.reload.pages_unique_domain_enabled }
+ end
+ end
- expect(response).to redirect_to(project_pages_path(project))
- expect(flash[:alert]).to eq('some error happened')
+ context 'with pages_unique_domain feature flag enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ it 'updates pages_https_only and pages_unique_domain and redirects back to pages settings' do
+ expect { patch :update, params: request_params }
+ .to change { project.project_setting.reload.pages_unique_domain_enabled }
+ .from(false).to(true)
+
+ expect(project.project_setting.pages_unique_domain).not_to be_nil
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(project_pages_path(project))
+ end
+
+ context 'when it fails to update' do
+ it 'adds an error message' do
+ expect_next_instance_of(Projects::UpdateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return(status: :error, message: 'some error happened')
+ end
+
+ expect { patch :update, params: request_params }
+ .not_to change { project.project_setting.reload.pages_unique_domain_enabled }
+
+ expect(response).to redirect_to(project_pages_path(project))
+ expect(flash[:alert]).to eq('some error happened')
+ end
+ end
end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e641f925758..a3a2af52807 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -59,6 +59,10 @@ FactoryBot.define do
user_type { :project_bot }
end
+ trait :service_account do
+ user_type { :service_account }
+ end
+
trait :migration_bot do
user_type { :migration_bot }
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b5f640f1cca..e3c2402b2c9 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -695,7 +695,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
end
context 'when variables are specified' do
- it 'creates a new pipeline with variables', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a new pipeline with variables' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
find("[data-testid='pipeline-form-ci-variable-value']").set('value')
@@ -721,7 +721,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
it { expect(page).to have_content('Missing CI config file') }
- it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
stub_ci_pipeline_to_return_yaml_file
expect do
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 3f4513e6bfa..da51372dd3d 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -310,69 +310,58 @@ describe('Description component', () => {
});
});
- describe('with work_items_mvc feature flag enabled', () => {
- describe('empty description', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: '',
- },
- provide: {
- glFeatures: {
- workItemsMvc: true,
- },
- },
- });
- return nextTick();
+ describe('empty description', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: '',
+ },
});
+ return nextTick();
+ });
- it('renders without error', () => {
- expect(findTaskActionButtons()).toHaveLength(0);
- });
+ it('renders without error', () => {
+ expect(findTaskActionButtons()).toHaveLength(0);
});
+ });
- describe('description with checkboxes', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: {
- workItemsMvc: true,
- },
- },
- });
- return nextTick();
+ describe('description with checkboxes', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ },
});
+ return nextTick();
+ });
- it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
- expect(findTaskActionButtons()).toHaveLength(3);
- });
+ it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
+ expect(findTaskActionButtons()).toHaveLength(3);
+ });
- it('does not show a modal by default', () => {
- expect(findModal().exists()).toBe(false);
- });
+ it('does not show a modal by default', () => {
+ expect(findModal().exists()).toBe(false);
+ });
- it('shows toast after delete success', async () => {
- const newDesc = 'description';
- findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
+ it('shows toast after delete success', async () => {
+ const newDesc = 'description';
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
- expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
- expect($toast.show).toHaveBeenCalledWith('Task deleted');
- });
+ expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
+ expect($toast.show).toHaveBeenCalledWith('Task deleted');
});
+ });
- describe('task list item actions', () => {
- describe('converting the task list item to a task', () => {
- describe('when successful', () => {
- let createWorkItemMutationHandler;
+ describe('task list item actions', () => {
+ describe('converting the task list item to a task', () => {
+ describe('when successful', () => {
+ let createWorkItemMutationHandler;
- beforeEach(async () => {
- createWorkItemMutationHandler = jest
- .fn()
- .mockResolvedValue(createWorkItemMutationResponse);
- const descriptionText = `Tasks
+ beforeEach(async () => {
+ createWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationResponse);
+ const descriptionText = `Tasks
1. [ ] item 1
1. [ ] item 2
@@ -381,218 +370,207 @@ describe('Description component', () => {
1. [ ] item 3
1. [ ] item 4;`;
- createComponent({
- props: { descriptionText },
- provide: { glFeatures: { workItemsMvc: true } },
- createWorkItemMutationHandler,
- });
- await waitForPromises();
-
- eventHub.$emit('convert-task-list-item', '4:4-8:19');
- await waitForPromises();
+ createComponent({
+ props: { descriptionText },
+ createWorkItemMutationHandler,
});
+ await waitForPromises();
- it('emits an event to update the description with the deleted task list item omitted', () => {
- const newDescriptionText = `Tasks
+ eventHub.$emit('convert-task-list-item', '4:4-8:19');
+ await waitForPromises();
+ });
+
+ it('emits an event to update the description with the deleted task list item omitted', () => {
+ const newDescriptionText = `Tasks
1. [ ] item 1
1. [ ] item 3
1. [ ] item 4;`;
- expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
- });
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
- it('calls a mutation to create a task', () => {
- const {
+ it('calls a mutation to create a task', () => {
+ const {
+ confidential,
+ iteration,
+ milestone,
+ } = issueDetailsResponse.data.workspace.issuable;
+ expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
confidential,
- iteration,
- milestone,
- } = issueDetailsResponse.data.workspace.issuable;
- expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
- input: {
- confidential,
- description: '\nparagraph text\n',
- hierarchyWidget: {
- parentId: 'gid://gitlab/WorkItem/1',
- },
- iterationWidget: {
- iterationId: IS_EE ? iteration.id : null,
- },
- milestoneWidget: {
- milestoneId: milestone.id,
- },
- projectPath: 'gitlab-org/gitlab-test',
- title: 'item 2',
- workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ description: '\nparagraph text\n',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ iterationWidget: {
+ iterationId: IS_EE ? iteration.id : null,
+ },
+ milestoneWidget: {
+ milestoneId: milestone.id,
},
- });
+ projectPath: 'gitlab-org/gitlab-test',
+ title: 'item 2',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ },
});
+ });
- it('shows a toast to confirm the creation of the task', () => {
- expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
- });
+ it('shows a toast to confirm the creation of the task', () => {
+ expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
});
+ });
- describe('when unsuccessful', () => {
- beforeEach(async () => {
- createComponent({
- props: { descriptionText: 'description' },
- provide: { glFeatures: { workItemsMvc: true } },
- createWorkItemMutationHandler: jest
- .fn()
- .mockResolvedValue(createWorkItemMutationErrorResponse),
- });
- await waitForPromises();
-
- eventHub.$emit('convert-task-list-item', '1:1-1:11');
- await waitForPromises();
+ describe('when unsuccessful', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: { descriptionText: 'description' },
+ createWorkItemMutationHandler: jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationErrorResponse),
});
+ await waitForPromises();
- it('shows an alert with an error message', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Something went wrong when creating task. Please try again.',
- error: new Error('an error'),
- captureError: true,
- });
+ eventHub.$emit('convert-task-list-item', '1:1-1:11');
+ await waitForPromises();
+ });
+
+ it('shows an alert with an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong when creating task. Please try again.',
+ error: new Error('an error'),
+ captureError: true,
});
});
});
+ });
- describe('deleting the task list item', () => {
- it('emits an event to update the description with the deleted task list item', () => {
- const descriptionText = `Tasks
+ describe('deleting the task list item', () => {
+ it('emits an event to update the description with the deleted task list item', () => {
+ const descriptionText = `Tasks
1. [ ] item 1
1. [ ] item 2
1. [ ] item 3
1. [ ] item 4;`;
- const newDescriptionText = `Tasks
+ const newDescriptionText = `Tasks
1. [ ] item 1
1. [ ] item 3
1. [ ] item 4;`;
- createComponent({
- props: { descriptionText },
- provide: { glFeatures: { workItemsMvc: true } },
- });
+ createComponent({
+ props: { descriptionText },
+ });
- eventHub.$emit('delete-task-list-item', '4:4-5:19');
+ eventHub.$emit('delete-task-list-item', '4:4-5:19');
- expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
- });
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
});
});
+ });
- describe('work items detail', () => {
- describe('when opening and closing', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- provide: {
- glFeatures: { workItemsMvc: true },
- },
- });
- return nextTick();
+ describe('work items detail', () => {
+ describe('when opening and closing', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
});
+ return nextTick();
+ });
- it('opens when task button is clicked', async () => {
- await findTaskLink().trigger('click');
+ it('opens when task button is clicked', async () => {
+ await findTaskLink().trigger('click');
- expect(showDetailsModal).toHaveBeenCalled();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/?work_item_id=2`,
- replace: true,
- });
+ expect(showDetailsModal).toHaveBeenCalled();
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?work_item_id=2`,
+ replace: true,
});
+ });
- it('closes from an open state', async () => {
- await findTaskLink().trigger('click');
+ it('closes from an open state', async () => {
+ await findTaskLink().trigger('click');
- findWorkItemDetailModal().vm.$emit('close');
- await nextTick();
+ findWorkItemDetailModal().vm.$emit('close');
+ await nextTick();
- expect(updateHistory).toHaveBeenLastCalledWith({
- url: `${TEST_HOST}/`,
- replace: true,
- });
+ expect(updateHistory).toHaveBeenLastCalledWith({
+ url: `${TEST_HOST}/`,
+ replace: true,
});
+ });
- it('tracks when opened', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- await findTaskLink().trigger('click');
+ it('tracks when opened', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'viewed_work_item_from_modal',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'work_item_view',
- property: 'type_task',
- },
- );
- });
- });
+ await findTaskLink().trigger('click');
- describe('when url query `work_item_id` exists', () => {
- it.each`
- behavior | workItemId | modalOpened
- ${'opens'} | ${'2'} | ${1}
- ${'does not open'} | ${'123'} | ${0}
- ${'does not open'} | ${'123e'} | ${0}
- ${'does not open'} | ${'12e3'} | ${0}
- ${'does not open'} | ${'1e23'} | ${0}
- ${'does not open'} | ${'x'} | ${0}
- ${'does not open'} | ${'undefined'} | ${0}
- `(
- '$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, modalOpened }) => {
- setWindowLocation(`?work_item_id=${workItemId}`);
-
- createComponent({
- props: { descriptionHtml: descriptionHtmlWithTask },
- provide: { glFeatures: { workItemsMvc: true } },
- });
-
- expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
+ expect(trackingSpy).toHaveBeenCalledWith(
+ TRACKING_CATEGORY_SHOW,
+ 'viewed_work_item_from_modal',
+ {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_view',
+ property: 'type_task',
},
);
});
});
- describe('when hovering task links', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- provide: {
- glFeatures: { workItemsMvc: true },
- },
- });
- return nextTick();
- });
+ describe('when url query `work_item_id` exists', () => {
+ it.each`
+ behavior | workItemId | modalOpened
+ ${'opens'} | ${'2'} | ${1}
+ ${'does not open'} | ${'123'} | ${0}
+ ${'does not open'} | ${'123e'} | ${0}
+ ${'does not open'} | ${'12e3'} | ${0}
+ ${'does not open'} | ${'1e23'} | ${0}
+ ${'does not open'} | ${'x'} | ${0}
+ ${'does not open'} | ${'undefined'} | ${0}
+ `(
+ '$behavior when url contains `work_item_id=$workItemId`',
+ async ({ workItemId, modalOpened }) => {
+ setWindowLocation(`?work_item_id=${workItemId}`);
- it('prefetches work item detail after work item link is hovered for 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- jest.advanceTimersByTime(150);
- await waitForPromises();
+ createComponent({
+ props: { descriptionHtml: descriptionHtmlWithTask },
+ });
- expect(queryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
- });
+ expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
+ },
+ );
+ });
+ });
+
+ describe('when hovering task links', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
});
+ return nextTick();
+ });
- it('does not work item detail after work item link is hovered for less than 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- await findTaskLink().trigger('mouseout');
- jest.advanceTimersByTime(150);
- await waitForPromises();
+ it('prefetches work item detail after work item link is hovered for 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
- expect(queryHandler).not.toHaveBeenCalled();
+ expect(queryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
});
});
+
+ it('does not work item detail after work item link is hovered for less than 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ await findTaskLink().trigger('mouseout');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
index 36714413da6..088a901c80e 100644
--- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Duration do
+RSpec.describe Gitlab::Ci::Pipeline::Duration, feature_category: :continuous_integration do
describe '.from_periods' do
let(:calculated_duration) { calculate(data) }
@@ -113,16 +113,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
described_class::Period.new(first, last)
end
- described_class.from_periods(periods.sort_by(&:first))
+ described_class.send(:from_periods, periods.sort_by(&:first))
end
end
describe '.from_pipeline' do
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }
+
let_it_be(:start_time) { Time.current.change(usec: 0) }
let_it_be(:current) { start_time + 1000 }
- let_it_be(:pipeline) { create(:ci_pipeline) }
- let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 60) }
- let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 120) }
+ let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 50) }
+ let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 110) }
let_it_be(:canceled_build) { create_build(:canceled, started_at: start_time + 120, finished_at: start_time + 180) }
let_it_be(:skipped_build) { create_build(:skipped, started_at: start_time) }
let_it_be(:pending_build) { create_build(:pending) }
@@ -141,21 +142,55 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
end
context 'when there is no running build' do
- let(:running_build) { nil }
+ let!(:running_build) { nil }
it 'returns the duration for all the builds' do
travel_to(current) do
- expect(described_class.from_pipeline(pipeline)).to eq 180.seconds
+ # 160 = success (50) + failed (50) + canceled (60)
+ expect(described_class.from_pipeline(pipeline)).to eq 160.seconds
end
end
end
- context 'when there are bridge jobs' do
- let!(:success_bridge) { create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280) }
- let!(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) }
- let!(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) }
- let!(:created_bridge) { create_bridge(:created) }
- let!(:manual_bridge) { create_bridge(:manual) }
+ context 'when there are direct bridge jobs' do
+ let_it_be(:success_bridge) do
+ create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280)
+ end
+
+ let_it_be(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) }
+ # NOTE: bridge won't be `canceled` as it will be marked as failed when downstream pipeline is canceled
+ # @see Ci::Bridge#inherit_status_from_downstream
+ let_it_be(:canceled_bridge) do
+ create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 210)
+ end
+
+ let_it_be(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) }
+ let_it_be(:created_bridge) { create_bridge(:created) }
+ let_it_be(:manual_bridge) { create_bridge(:manual) }
+
+ let_it_be(:success_bridge_pipeline) do
+ create(:ci_pipeline, :success, started_at: start_time + 230, finished_at: start_time + 280).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_bridge, pipeline: p)
+ create_build(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 280)
+ create_bridge(:success, pipeline: p, started_at: start_time + 240, finished_at: start_time + 280)
+ end
+ end
+
+ let_it_be(:failed_bridge_pipeline) do
+ create(:ci_pipeline, :failed, started_at: start_time + 225, finished_at: start_time + 240).tap do |p|
+ create(:ci_sources_pipeline, source_job: failed_bridge, pipeline: p)
+ create_build(:failed, pipeline: p, started_at: start_time + 230, finished_at: start_time + 240)
+ create_bridge(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 240)
+ end
+ end
+
+ let_it_be(:canceled_bridge_pipeline) do
+ create(:ci_pipeline, :canceled, started_at: start_time + 190, finished_at: start_time + 210).tap do |p|
+ create(:ci_sources_pipeline, source_job: canceled_bridge, pipeline: p)
+ create_build(:canceled, pipeline: p, started_at: start_time + 200, finished_at: start_time + 210)
+ create_bridge(:success, pipeline: p, started_at: start_time + 205, finished_at: start_time + 210)
+ end
+ end
it 'returns the duration of the running build' do
travel_to(current) do
@@ -166,12 +201,147 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
context 'when there is no running build' do
let!(:running_build) { nil }
- it 'returns the duration for all the builds and bridge jobs' do
+ it 'returns the duration for all the builds (including self and downstreams)' do
+ travel_to(current) do
+ # 220 = 160 (see above)
+ # + success build (45) + failed (10) + canceled (10) - overlapping (success & failed) (5)
+ expect(described_class.from_pipeline(pipeline)).to eq 220.seconds
+ end
+ end
+ end
+
+ context 'when feature flag ci_use_downstream_pipeline_duration_for_calculation is disabled' do
+ before do
+ stub_feature_flags(ci_use_downstream_pipeline_duration_for_calculation: false)
+ end
+
+ it 'returns the duration of the running build' do
+ travel_to(current) do
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for builds and bridges' do
+ travel_to(current) do
+ # 260 = 160 (see above)
+ # + success bridge build (60) + failed (60) + canceled (30)
+ # - overlapping (success & failed) (20) - overlapping (failed & canceled) (30)
+ expect(described_class.from_pipeline(pipeline)).to eq 260.seconds
+ end
+ end
+ end
+ end
+
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
+ context 'when there are downstream bridge jobs' do
+ let_it_be(:success_direct_bridge) do
+ create_bridge(:success, started_at: start_time + 280, finished_at: start_time + 400)
+ end
+
+ let_it_be(:success_downstream_pipeline) do
+ create(:ci_pipeline, :success, started_at: start_time + 285, finished_at: start_time + 300).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:success, pipeline: p, started_at: start_time + 290, finished_at: start_time + 296)
+ create_bridge(:success, pipeline: p, started_at: start_time + 285, finished_at: start_time + 288)
+ end
+ end
+
+ let_it_be(:failed_downstream_pipeline) do
+ create(:ci_pipeline, :failed, started_at: start_time + 305, finished_at: start_time + 350).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:failed, pipeline: p, started_at: start_time + 320, finished_at: start_time + 327)
+ create_bridge(:success, pipeline: p, started_at: start_time + 305, finished_at: start_time + 350)
+ end
+ end
+
+ let_it_be(:canceled_downstream_pipeline) do
+ create(:ci_pipeline, :canceled, started_at: start_time + 360, finished_at: start_time + 400).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:canceled, pipeline: p, started_at: start_time + 390, finished_at: start_time + 398)
+ create_bridge(:success, pipeline: p, started_at: start_time + 360, finished_at: start_time + 378)
+ end
+ end
+
+ it 'returns the duration of the running build' do
travel_to(current) do
- expect(described_class.from_pipeline(pipeline)).to eq 280.seconds
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for all the builds (including self and downstreams)' do
+ travel_to(current) do
+ # 241 = 220 (see above)
+ # + success downstream build (6) + failed (7) + canceled (8)
+ expect(described_class.from_pipeline(pipeline)).to eq 241.seconds
+ end
+ end
+ end
+
+ context 'when feature flag ci_use_downstream_pipeline_duration_for_calculation is disabled' do
+ before do
+ stub_feature_flags(ci_use_downstream_pipeline_duration_for_calculation: false)
+ end
+
+ it 'returns the duration of the running build' do
+ travel_to(current) do
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for builds and bridges' do
+ travel_to(current) do
+ # 380 = 260 (see above) + success direct bridge (120)
+ expect(described_class.from_pipeline(pipeline)).to eq 380.seconds
+ end
+ end
end
end
end
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
+ end
+
+ it 'does not generate N+1 queries if more builds are added' do
+ travel_to(current) do
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+
+ create_list(:ci_build, 2, :success, pipeline: pipeline, started_at: start_time, finished_at: start_time + 50)
+
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ it 'does not generate N+1 queries if more bridges and their pipeline builds are added' do
+ travel_to(current) do
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+
+ create_list(
+ :ci_bridge, 2, :success,
+ pipeline: pipeline, started_at: start_time + 220, finished_at: start_time + 280).each do |bridge|
+ create(:ci_pipeline, :success, started_at: start_time + 235, finished_at: start_time + 280).tap do |p|
+ create(:ci_sources_pipeline, source_job: bridge, pipeline: p)
+ create_builds(3, :success)
+ end
+ end
+
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+ end
end
private
@@ -180,6 +350,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
create(:ci_build, trait, pipeline: pipeline, **opts)
end
+ def create_builds(counts, trait, **opts)
+ create_list(:ci_build, counts, trait, pipeline: pipeline, **opts)
+ end
+
def create_bridge(trait, **opts)
create(:ci_bridge, trait, pipeline: pipeline, **opts)
end
diff --git a/spec/lib/gitlab/pages/random_domain_spec.rb b/spec/lib/gitlab/pages/random_domain_spec.rb
new file mode 100644
index 00000000000..978412bb72c
--- /dev/null
+++ b/spec/lib/gitlab/pages/random_domain_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pages::RandomDomain, feature_category: :pages do
+ let(:namespace_path) { 'namespace' }
+
+ subject(:generator) do
+ described_class.new(project_path: project_path, namespace_path: namespace_path)
+ end
+
+ RSpec.shared_examples 'random domain' do |domain|
+ it do
+ expect(SecureRandom)
+ .to receive(:hex)
+ .and_wrap_original do |_, size, _|
+ ('h' * size)
+ end
+
+ generated = generator.generate
+
+ expect(generated).to eq(domain)
+ expect(generated.length).to eq(63)
+ end
+ end
+
+ context 'when project path is less than 48 chars' do
+ let(:project_path) { 'p' }
+
+ it_behaves_like 'random domain', 'p-namespace-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh'
+ end
+
+ context 'when project path is close to 48 chars' do
+ let(:project_path) { 'p' * 45 }
+
+ it_behaves_like 'random domain', 'ppppppppppppppppppppppppppppppppppppppppppppp-na-hhhhhhhhhhhhhh'
+ end
+
+ context 'when project path is larger than 48 chars' do
+ let(:project_path) { 'p' * 49 }
+
+ it_behaves_like 'random domain', 'pppppppppppppppppppppppppppppppppppppppppppppppp-hhhhhhhhhhhhhh'
+ end
+end
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 2a81142ea44..3099468c07d 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
require 'fog/core'
@@ -130,6 +130,11 @@ RSpec.describe ObjectStorage::Config do
it { expect(subject.provider).to eq('AWS') }
it { expect(subject.aws?).to be true }
it { expect(subject.google?).to be false }
+ it { expect(subject.credentials).to eq(credentials) }
+
+ context 'with FIPS enabled', :fips_mode do
+ it { expect(subject.credentials).to eq(credentials.merge(disable_content_md5_validation: true)) }
+ end
end
context 'with Google credentials' do
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index bd128112113..0f90bbcda4e 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe User do
+RSpec.describe User, feature_category: :authentication_and_authorization do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES.keys)
.to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot
- migration_bot automation_bot admin_bot suggested_reviewers_bot])
+ migration_bot automation_bot admin_bot suggested_reviewers_bot service_account])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index feb5985818b..42433a2a84a 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -39,6 +39,44 @@ RSpec.describe ProjectSetting, type: :model do
[nil, 'not_allowed', :invalid].each do |invalid_value|
it { is_expected.not_to allow_value([invalid_value]).for(:target_platforms) }
end
+
+ context "when pages_unique_domain is required", feature_category: :pages do
+ it "is not required if pages_unique_domain_enabled is false" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: false)
+
+ expect(project_setting).to be_valid
+ expect(project_setting.errors.full_messages).not_to include("Pages unique domain can't be blank")
+ end
+
+ it "is required when pages_unique_domain_enabled is true" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: true)
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+
+ it "is required if it is already saved in the database" do
+ project_setting = create(
+ :project_setting,
+ pages_unique_domain: "random-unique-domain-here",
+ pages_unique_domain_enabled: true
+ )
+
+ project_setting.pages_unique_domain = nil
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+ end
+
+ it "validates uniqueness of pages_unique_domain", feature_category: :pages do
+ create(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ project_setting = build(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain has already been taken")
+ end
end
describe 'target_platforms=' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index dfc8919e19d..5304fec506e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2666,7 +2666,11 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#pages_url', feature_category: :pages do
+ let(:group_name) { 'group' }
+ let(:project_name) { 'project' }
+
let(:group) { create(:group, name: group_name) }
+ let(:nested_group) { create(:group, parent: group) }
let(:project_path) { project_name.downcase }
let(:project) do
@@ -2689,101 +2693,114 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
.and_return(['http://example.com', port].compact.join(':'))
end
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'when pages_unique_domain feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
- it { is_expected.to eq("http://group.example.com") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
- it { is_expected.to eq("http://group.example.com") }
+ project.project_setting.update!(
+ pages_unique_domain_enabled: pages_unique_domain_enabled,
+ pages_unique_domain: 'unique-domain'
+ )
end
- end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { false }
- it { is_expected.to eq("http://group.example.com/project") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { true }
- it { is_expected.to eq("http://group.example.com/Project") }
+ it { is_expected.to eq('http://unique-domain.example.com') }
end
end
- context 'when there is an explicit port' do
- let(:port) { 3000 }
-
- context 'when not in dev mode' do
- before do
- stub_rails_env('production')
- end
+ context 'with nested group' do
+ let(:project) { create(:project, namespace: nested_group, name: project_name) }
+ let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'group page' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ it { is_expected.to eq(expected_url) }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'project page' do
+ let(:project_name) { 'Project' }
- it { is_expected.to eq('http://group.example.com:3000/Group.example.com') }
- end
- end
+ it { is_expected.to eq(expected_url) }
+ end
+ end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when the project matches its namespace url' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ it { is_expected.to eq('http://group.example.com') }
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'with different group name capitalization' do
+ let(:group_name) { 'Group' }
- it { is_expected.to eq("http://group.example.com:3000/Project") }
- end
- end
+ it { is_expected.to eq("http://group.example.com") }
end
- context 'when in dev mode' do
- before do
- stub_rails_env('development')
- end
-
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'with different project path capitalization' do
+ let(:project_path) { 'Group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000') }
+ it { is_expected.to eq("http://group.example.com") }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'with different project name capitalization' do
+ let(:project_name) { 'Project' }
- it { is_expected.to eq('http://group.example.com:3000') }
- end
- end
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when there is an explicit port' do
+ let(:port) { 3000 }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ context 'when not in dev mode' do
+ before do
+ stub_rails_env('production')
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ end
- it { is_expected.to eq("http://group.example.com:3000/Project") }
+ context 'when in dev mode' do
+ before do
+ stub_rails_env('development')
end
+
+ it { is_expected.to eq('http://group.example.com:3000') }
end
end
end
end
+ describe '#pages_unique_url', feature_category: :pages do
+ let(:project_settings) { create(:project_setting, pages_unique_domain: 'unique-domain') }
+ let(:project) { build(:project, project_setting: project_settings) }
+ let(:domain) { 'example.com' }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://#{domain}")
+ end
+
+ it 'returns the pages unique url' do
+ expect(project.pages_unique_url).to eq('http://unique-domain.example.com')
+ end
+ end
+
describe '#pages_namespace_url', feature_category: :pages do
let(:group) { create(:group, name: group_name) }
let(:project) { create(:project, namespace: group, name: project_name) }
@@ -4643,52 +4660,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#pages_url' do
- let(:group) { create(:group, name: 'Group') }
- let(:nested_group) { create(:group, parent: group) }
- let(:domain) { 'Example.com' }
-
- subject { project.pages_url }
-
- before do
- allow(Settings.pages).to receive(:host).and_return(domain)
- allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
- end
-
- context 'top-level group' do
- let(:project) { create(:project, namespace: group, name: project_name) }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq("http://group.example.com") }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq("http://group.example.com/project") }
- end
- end
-
- context 'nested group' do
- let(:project) { create(:project, namespace: nested_group, name: project_name) }
- let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq(expected_url) }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq(expected_url) }
- end
- end
- end
-
describe '#lfs_http_url_to_repo' do
let(:project) { create(:project) }
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 0575ba3237b..78bbff57572 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
let_it_be(:admin_user) { create(:admin) }
let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:service_account) { create(:user, :service_account) }
let_it_be(:migration_bot) { create(:user, :migration_bot) }
let_it_be(:security_bot) { create(:user, :security_bot) }
let_it_be_with_reload(:current_user) { create(:user) }
@@ -219,6 +220,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_api) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -345,6 +352,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:receive_notifications) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_disallowed(:receive_notifications) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -433,6 +446,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_git) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
context 'user blocked pending approval' do
before do
current_user.block_pending_approval
@@ -517,6 +536,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:use_slash_commands) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:use_slash_commands) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -571,6 +596,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:log_in) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_disallowed(:log_in) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
diff --git a/spec/services/markup/rendering_service_spec.rb b/spec/services/markup/rendering_service_spec.rb
index 99ab87f2072..ca70e983714 100644
--- a/spec/services/markup/rendering_service_spec.rb
+++ b/spec/services/markup/rendering_service_spec.rb
@@ -111,5 +111,22 @@ RSpec.describe Markup::RenderingService do
is_expected.to eq(expected_html)
end
end
+
+ context 'with reStructuredText' do
+ let(:file_name) { 'foo.rst' }
+ let(:text) { "####\nPART\n####" }
+
+ it 'returns rendered html' do
+ is_expected.to eq("<h1>PART</h1>\n\n")
+ end
+
+ context 'when input has an invalid syntax' do
+ let(:text) { "####\nPART\n##" }
+
+ it 'uses a simple formatter for html' do
+ is_expected.to eq("<p>####\n<br>PART\n<br>##</p>")
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 3cda6bc2627..57249bcd562 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -794,6 +794,112 @@ RSpec.describe Projects::UpdateService do
expect(project.topic_list).to eq(%w[tag_list])
end
end
+
+ describe 'when updating pages unique domain', feature_category: :pages do
+ let(:group) { create(:group, path: 'group') }
+ let(:project) { create(:project, path: 'project', group: group) }
+
+ context 'with pages_unique_domain feature flag disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
+
+ it 'does not change pages unique domain' do
+ expect(project)
+ .to receive(:update)
+ .with({ project_setting_attributes: { has_confluence: true } })
+ .and_call_original
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ has_confluence: true,
+ pages_unique_domain_enabled: true
+ })
+ end.not_to change { project.project_setting.pages_unique_domain_enabled }
+ end
+
+ it 'does not remove other attributes' do
+ expect(project)
+ .to receive(:update)
+ .with({ name: 'True' })
+ .and_call_original
+
+ update_project(project, user, name: 'True')
+ end
+ end
+
+ context 'with pages_unique_domain feature flag enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ it 'updates project pages unique domain' do
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+ end.to change { project.project_setting.pages_unique_domain_enabled }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq true
+ expect(project.project_setting.pages_unique_domain).to match %r{project-group-\w+}
+ end
+
+ it 'does not changes unique domain when it already exists' do
+ project.project_setting.update!(
+ pages_unique_domain_enabled: false,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+ end.to change { project.project_setting.pages_unique_domain_enabled }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq true
+ expect(project.project_setting.pages_unique_domain).to eq 'unique-domain'
+ end
+
+ it 'does not changes unique domain when it disabling unique domain' do
+ project.project_setting.update!(
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: false
+ })
+ end.not_to change { project.project_setting.pages_unique_domain }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq false
+ expect(project.project_setting.pages_unique_domain).to eq 'unique-domain'
+ end
+
+ context 'when there is another project with the unique domain' do
+ it 'fails pages unique domain already exists' do
+ create(
+ :project_setting,
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ allow(Gitlab::Pages::RandomDomain)
+ .to receive(:generate)
+ .and_return('unique-domain')
+
+ result = update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+
+ expect(result).to eq(
+ status: :error,
+ message: 'Project setting pages unique domain has already been taken'
+ )
+ end
+ end
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do