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>2024-01-09 21:07:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-09 21:07:32 +0300
commitcd22685717501ac6f3c81e16418270123a2cccee (patch)
treeeb45b2ea1c3ac174aaa4fd54a38901f1f8420838 /spec
parent1f753bca2624be1e507424127fe0d48b9765da70 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/ci/reports/security/findings.rb1
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/features/groups_spec.rb19
-rw-r--r--spec/frontend/environments/helpers/k8s_integration_helper_spec.js30
-rw-r--r--spec/frontend/kubernetes_dashboard/graphql/mock_data.js84
-rw-r--r--spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js84
-rw-r--r--spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js30
-rw-r--r--spec/frontend/kubernetes_dashboard/pages/services_page_spec.js104
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js4
-rw-r--r--spec/helpers/ci/catalog/resources_helper_spec.rb12
-rw-r--r--spec/lib/click_house/iterator_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/reports/security/report_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb12
-rw-r--r--spec/models/member_spec.rb104
-rw-r--r--spec/models/users/phone_number_validation_spec.rb41
-rw-r--r--spec/policies/organizations/organization_policy_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/organizations/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb4
-rw-r--r--spec/requests/organizations/settings_controller_spec.rb19
-rw-r--r--spec/services/ci/catalog/resources/create_service_spec.rb4
-rw-r--r--spec/services/ci/catalog/resources/destroy_service_spec.rb4
-rw-r--r--spec/services/organizations/update_service_spec.rb2
-rw-r--r--spec/services/security/merge_reports_service_spec.rb56
-rw-r--r--spec/validators/ip_cidr_array_validator_spec.rb1
-rw-r--r--spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb104
27 files changed, 670 insertions, 102 deletions
diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb
index 670d833c1f8..2de17115934 100644
--- a/spec/factories/ci/reports/security/findings.rb
+++ b/spec/factories/ci/reports/security/findings.rb
@@ -2,7 +2,6 @@
FactoryBot.define do
factory :ci_reports_security_finding, class: '::Gitlab::Ci::Reports::Security::Finding' do
- compare_key { "#{identifiers.first&.external_type}:#{identifiers.first&.external_id}:#{location.fingerprint}" }
confidence { :medium }
identifiers { Array.new(1) { association(:ci_reports_security_identifier) } }
location factory: :ci_reports_security_locations_sast
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 807df94e115..2f55c3ab567 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -18,6 +18,10 @@ FactoryBot.define do
create(:namespace_settings, namespace: group) unless group.namespace_settings
end
+ trait :with_organization do
+ association :organization
+ end
+
trait :public do
visibility_level { Gitlab::VisibilityLevel::PUBLIC }
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c0aaa7f818a..aac2e3c4e46 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -205,7 +205,10 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
describe 'not showing personalization questions on group creation when it is enabled' do
before do
stub_application_setting(hide_third_party_offers: true)
- visit new_group_path(anchor: 'create-group-pane')
+
+ # If visiting directly via path, personalization setting is not being picked up correctly
+ visit new_group_path
+ click_link 'Create group'
end
it 'does not render personalization questions' do
@@ -350,10 +353,16 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
visit path
end
- it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="group[name]"]', submit: 'button[type="submit"]' },
- { form: '.js-general-settings-form', input: '#group_visibility_level_0', submit: 'button[type="submit"]' },
- { form: '.js-general-permissions-form', input: '#group_request_access_enabled', submit: 'button[type="submit"]' },
- { form: '.js-general-permissions-form', input: 'input[name="group[two_factor_grace_period]"]', submit: 'button[type="submit"]' }]
+ it_behaves_like 'dirty submit form', [
+ { form: '.js-general-settings-form', input: 'input[name="group[name]"]', submit: 'button[type="submit"]' },
+ { form: '.js-general-settings-form', input: '#group_visibility_level_0', submit: 'button[type="submit"]' },
+ { form: '.js-general-permissions-form', input: '#group_request_access_enabled', submit: 'button[type="submit"]' },
+ {
+ form: '.js-general-permissions-form',
+ input: 'input[name="group[two_factor_grace_period]"]',
+ submit: 'button[type="submit"]'
+ }
+ ]
it 'saves new settings' do
page.within('.gs-general') do
diff --git a/spec/frontend/environments/helpers/k8s_integration_helper_spec.js b/spec/frontend/environments/helpers/k8s_integration_helper_spec.js
index 97100557ef3..852b5318c77 100644
--- a/spec/frontend/environments/helpers/k8s_integration_helper_spec.js
+++ b/spec/frontend/environments/helpers/k8s_integration_helper_spec.js
@@ -1,5 +1,4 @@
import {
- generateServicePortsString,
getDeploymentsStatuses,
getDaemonSetStatuses,
getStatefulSetStatuses,
@@ -12,35 +11,6 @@ import {
import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
describe('k8s_integration_helper', () => {
- describe('generateServicePortsString', () => {
- const port = '8080';
- const protocol = 'TCP';
- const nodePort = '31732';
-
- it('returns empty string if no ports provided', () => {
- expect(generateServicePortsString([])).toBe('');
- });
-
- it('returns port and protocol when provided', () => {
- expect(generateServicePortsString([{ port, protocol }])).toBe(`${port}/${protocol}`);
- });
-
- it('returns port, protocol and nodePort when provided', () => {
- expect(generateServicePortsString([{ port, protocol, nodePort }])).toBe(
- `${port}:${nodePort}/${protocol}`,
- );
- });
-
- it('returns joined strings of ports if multiple are provided', () => {
- expect(
- generateServicePortsString([
- { port, protocol },
- { port, protocol, nodePort },
- ]),
- ).toBe(`${port}/${protocol}, ${port}:${nodePort}/${protocol}`);
- });
- });
-
describe('getDeploymentsStatuses', () => {
const pending = {
status: {
diff --git a/spec/frontend/kubernetes_dashboard/graphql/mock_data.js b/spec/frontend/kubernetes_dashboard/graphql/mock_data.js
index 63224950a1e..8f733d382b2 100644
--- a/spec/frontend/kubernetes_dashboard/graphql/mock_data.js
+++ b/spec/frontend/kubernetes_dashboard/graphql/mock_data.js
@@ -513,3 +513,87 @@ export const mockCronJobsTableItems = [
];
export const k8sCronJobsMock = [readyCronJob, suspendedCronJob, failedCronJob];
+
+export const k8sServicesMock = [
+ {
+ metadata: {
+ name: 'my-first-service',
+ namespace: 'default',
+ creationTimestamp: '2023-07-31T11:50:17Z',
+ labels: {},
+ annotations: {},
+ },
+ spec: {
+ ports: [
+ {
+ name: 'https',
+ protocol: 'TCP',
+ port: 443,
+ targetPort: 8443,
+ },
+ ],
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ type: 'ClusterIP',
+ },
+ },
+ {
+ metadata: {
+ name: 'my-second-service',
+ namespace: 'default',
+ creationTimestamp: '2023-11-21T11:50:59Z',
+ labels: {},
+ annotations: {},
+ },
+ spec: {
+ ports: [
+ {
+ name: 'http',
+ protocol: 'TCP',
+ appProtocol: 'http',
+ port: 80,
+ targetPort: 'http',
+ nodePort: 31989,
+ },
+ {
+ name: 'https',
+ protocol: 'TCP',
+ appProtocol: 'https',
+ port: 443,
+ targetPort: 'https',
+ nodePort: 32679,
+ },
+ ],
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ type: 'NodePort',
+ },
+ },
+];
+
+export const mockServicesTableItems = [
+ {
+ name: 'my-first-service',
+ namespace: 'default',
+ type: 'ClusterIP',
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ ports: '443/TCP',
+ age: '114d',
+ labels: {},
+ annotations: {},
+ kind: 'Service',
+ },
+ {
+ name: 'my-second-service',
+ namespace: 'default',
+ type: 'NodePort',
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ ports: '80:31989/TCP, 443:32679/TCP',
+ age: '1d',
+ labels: {},
+ annotations: {},
+ kind: 'Service',
+ },
+];
diff --git a/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js b/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js
index 9be7ca2877b..01e2c3d2716 100644
--- a/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js
@@ -7,6 +7,7 @@ import k8sDashboardReplicaSetsQuery from '~/kubernetes_dashboard/graphql/queries
import k8sDashboardDaemonSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql';
import k8sDashboardJobsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql';
import k8sDashboardCronJobsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql';
+import k8sDashboardServicesQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql';
import {
k8sPodsMock,
k8sDeploymentsMock,
@@ -15,6 +16,7 @@ import {
k8sDaemonSetsMock,
k8sJobsMock,
k8sCronJobsMock,
+ k8sServicesMock,
} from '../mock_data';
describe('~/frontend/environments/graphql/resolvers', () => {
@@ -624,4 +626,86 @@ describe('~/frontend/environments/graphql/resolvers', () => {
).rejects.toThrow('API error');
});
});
+
+ describe('k8sServices', () => {
+ const client = { writeQuery: jest.fn() };
+
+ const mockWatcher = WatchApi.prototype;
+ const mockServicesListWatcherFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
+ });
+
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback([]);
+ }
+ });
+
+ const mockServicesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ items: k8sServicesMock,
+ });
+ });
+
+ const mockAllServicesListFn = jest.fn().mockImplementation(mockServicesListFn);
+
+ describe('when the Services data is present', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockImplementation(mockAllServicesListFn);
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockServicesListWatcherFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ it('should request all Services from the cluster_client library and watch the events', async () => {
+ const Services = await mockResolvers.Query.k8sServices(
+ null,
+ {
+ configuration,
+ },
+ { client },
+ );
+
+ expect(mockAllServicesListFn).toHaveBeenCalled();
+ expect(mockServicesListWatcherFn).toHaveBeenCalled();
+
+ expect(Services).toEqual(k8sServicesMock);
+ });
+
+ it('should update cache with the new data when received from the library', async () => {
+ await mockResolvers.Query.k8sServices(null, { configuration, namespace: '' }, { client });
+
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: k8sDashboardServicesQuery,
+ variables: { configuration, namespace: '' },
+ data: { k8sServices: [] },
+ });
+ });
+ });
+
+ it('should not watch Services from the cluster_client library when the Services data is not present', async () => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces').mockImplementation(
+ jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ items: [],
+ });
+ }),
+ );
+
+ await mockResolvers.Query.k8sServices(null, { configuration }, { client });
+
+ expect(mockServicesListWatcherFn).not.toHaveBeenCalled();
+ });
+
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(
+ mockResolvers.Query.k8sServices(null, { configuration }, { client }),
+ ).rejects.toThrow('API error');
+ });
+ });
});
diff --git a/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js b/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js
index 4d6ea2742f2..1fd89e67e79 100644
--- a/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js
+++ b/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js
@@ -5,6 +5,7 @@ import {
calculateDaemonSetStatus,
calculateJobStatus,
calculateCronJobStatus,
+ generateServicePortsString,
} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
import { useFakeDate } from 'helpers/fake_date';
@@ -140,4 +141,33 @@ describe('k8s_integration_helper', () => {
expect(calculateCronJobStatus(item)).toBe(expected);
});
});
+
+ describe('generateServicePortsString', () => {
+ const port = '8080';
+ const protocol = 'TCP';
+ const nodePort = '31732';
+
+ it('returns empty string if no ports provided', () => {
+ expect(generateServicePortsString([])).toBe('');
+ });
+
+ it('returns port and protocol when provided', () => {
+ expect(generateServicePortsString([{ port, protocol }])).toBe(`${port}/${protocol}`);
+ });
+
+ it('returns port, protocol and nodePort when provided', () => {
+ expect(generateServicePortsString([{ port, protocol, nodePort }])).toBe(
+ `${port}:${nodePort}/${protocol}`,
+ );
+ });
+
+ it('returns joined strings of ports if multiple are provided', () => {
+ expect(
+ generateServicePortsString([
+ { port, protocol },
+ { port, protocol, nodePort },
+ ]),
+ ).toBe(`${port}/${protocol}, ${port}:${nodePort}/${protocol}`);
+ });
+ });
});
diff --git a/spec/frontend/kubernetes_dashboard/pages/services_page_spec.js b/spec/frontend/kubernetes_dashboard/pages/services_page_spec.js
new file mode 100644
index 00000000000..c76f4330cd6
--- /dev/null
+++ b/spec/frontend/kubernetes_dashboard/pages/services_page_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import ServicesPage from '~/kubernetes_dashboard/pages/services_page.vue';
+import WorkloadLayout from '~/kubernetes_dashboard/components/workload_layout.vue';
+import { SERVICES_TABLE_FIELDS } from '~/kubernetes_dashboard/constants';
+import { useFakeDate } from 'helpers/fake_date';
+import { k8sServicesMock, mockServicesTableItems } from '../graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('Kubernetes dashboard services page', () => {
+ let wrapper;
+
+ const configuration = {
+ basePath: 'kas/tunnel/url',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findWorkloadLayout = () => wrapper.findComponent(WorkloadLayout);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockReturnValue(k8sServicesMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMount(ServicesPage, {
+ provide: { configuration },
+ apolloProvider,
+ });
+ };
+
+ describe('mounted', () => {
+ it('renders WorkloadLayout component', () => {
+ createWrapper();
+
+ expect(findWorkloadLayout().exists()).toBe(true);
+ });
+
+ it('sets loading prop for the WorkloadLayout', () => {
+ createWrapper();
+
+ expect(findWorkloadLayout().props('loading')).toBe(true);
+ });
+
+ it('removes loading prop from the WorkloadLayout when the list of services loaded', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findWorkloadLayout().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when gets services data', () => {
+ useFakeDate(2023, 10, 23, 10, 10);
+
+ it('sets correct stats object for the WorkloadLayout', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findWorkloadLayout().props('stats')).toEqual([]);
+ });
+
+ it('sets correct table items object for the WorkloadLayout', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findWorkloadLayout().props('items')).toMatchObject(mockServicesTableItems);
+ expect(findWorkloadLayout().props('fields')).toMatchObject(SERVICES_TABLE_FIELDS);
+ });
+ });
+
+ describe('when gets an error from the cluster_client API', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+ });
+
+ it('sets errorMessage prop for the WorkloadLayout', () => {
+ expect(findWorkloadLayout().props('errorMessage')).toBe(error.message);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 9296e548081..85166549771 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -264,7 +264,7 @@ describe('MrWidgetOptions', () => {
expect(findMergePipelineForkAlert().exists()).toBe(false);
});
- it('hides the alert when merge pipelines are not enabled', async () => {
+ it('hides the alert when merged results pipelines are not enabled', async () => {
createComponent({
updatedMrData: {
source_project_id: 1,
@@ -275,7 +275,7 @@ describe('MrWidgetOptions', () => {
expect(findMergePipelineForkAlert().exists()).toBe(false);
});
- it('shows the alert when merge pipelines are enabled and the source project and target project are different', async () => {
+ it('shows the alert when merged results pipelines are enabled and the source project and target project are different', async () => {
createComponent({
updatedMrData: {
source_project_id: 1,
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 38d54eff872..a755f35332f 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -44,6 +44,10 @@ describe('GlCountdown', () => {
it('displays 00:00:00', () => {
expect(wrapper.text()).toContain('00:00:00');
});
+
+ it('emits `timer-expired` event', () => {
+ expect(wrapper.emitted('timer-expired')).toStrictEqual([[]]);
+ });
});
describe('when an invalid date is passed', () => {
diff --git a/spec/helpers/ci/catalog/resources_helper_spec.rb b/spec/helpers/ci/catalog/resources_helper_spec.rb
index 5c5d02ce6d8..68d56437249 100644
--- a/spec/helpers/ci/catalog/resources_helper_spec.rb
+++ b/spec/helpers/ci/catalog/resources_helper_spec.rb
@@ -36,18 +36,6 @@ RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composi
end
end
- describe '#can_view_namespace_catalog?' do
- subject { helper.can_view_namespace_catalog?(project) }
-
- before do
- stub_licensed_features(ci_namespace_catalog: false)
- end
-
- it 'user cannot view the Catalog in CE regardless of permissions' do
- expect(subject).to be false
- end
- end
-
describe '#js_ci_catalog_data' do
let(:project) { build(:project, :repository) }
diff --git a/spec/lib/click_house/iterator_spec.rb b/spec/lib/click_house/iterator_spec.rb
index fd054c0afe5..962ccc6d884 100644
--- a/spec/lib/click_house/iterator_spec.rb
+++ b/spec/lib/click_house/iterator_spec.rb
@@ -29,6 +29,16 @@ RSpec.describe ClickHouse::Iterator, :click_house, feature_category: :database d
expect(collect_ids_with_batch_size(15)).to match_array(expected_values)
end
+ context 'when min value is given' do
+ let(:iterator) { described_class.new(query_builder: query_builder, connection: connection, min_value: 5) }
+
+ it 'iterates from the given min value' do
+ expected_values = (5..10).to_a
+
+ expect(collect_ids_with_batch_size(5)).to match_array(expected_values)
+ end
+ end
+
context 'when there are no records for the given query' do
let(:query_builder) do
ClickHouse::QueryBuilder
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index bfcc87179af..6aa526c1829 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -185,7 +185,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
context 'when name is provided' do
it 'sets name from the report as a name' do
- finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
+ finding = report.findings.second
expected_name = Gitlab::Json.parse(finding.raw_metadata)['name']
expect(finding.name).to eq(expected_name)
@@ -197,7 +197,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
let(:location) { nil }
it 'returns only identifier name' do
- finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
+ finding = report.findings.third
+
expect(finding.name).to eq("CVE-2017-11429")
end
end
@@ -205,21 +206,24 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
context 'when location exists' do
context 'when CVE identifier exists' do
it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
+ finding = report.findings.third
+
expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
end
end
context 'when CWE identifier exists' do
it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
+ finding = report.findings.fourth
+
expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
end
end
context 'when neither CVE nor CWE identifier exist' do
it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
+ finding = report.findings.fifth
+
expect(finding.name).to eq("other-2017-11429 in yarn.lock")
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb
index d7f967f1c55..dabee0f32de 100644
--- a/spec/lib/gitlab/ci/reports/security/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Reports::Security::Report do
+RSpec.describe Gitlab::Ci::Reports::Security::Report, feature_category: :vulnerability_management do
let_it_be(:pipeline) { create(:ci_pipeline) }
let(:created_at) { 2.weeks.ago }
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Report do
let(:other_report) do
create(
:ci_reports_security_report,
- findings: [create(:ci_reports_security_finding, compare_key: 'other_finding')],
+ findings: [create(:ci_reports_security_finding)],
scanners: [create(:ci_reports_security_scanner, external_id: 'other_scanner', name: 'Other Scanner')],
identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')]
)
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index 2c9506dd498..05938fa08cd 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -50,18 +50,6 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter, feature_categ
end
end
- context 'with implicit includes', :snowplow do
- let(:config_source) { :auto_devops_source }
-
- described_class.all_included_templates('Auto-DevOps.gitlab-ci.yml').each do |template_name|
- context "for #{template_name}" do
- let(:template_path) { Gitlab::Template::GitlabCiYmlTemplate.find(template_name.delete_suffix('.gitlab-ci.yml')).full_name }
-
- include_examples 'tracks template'
- end
- end
- end
-
it 'expands short template names' do
expect do
described_class.track_unique_project_event(project: project, template: 'Dependency-Scanning.gitlab-ci.yml', config_source: :repository_source, user: user)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 9419bfa76a7..bd74af9b7e8 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -1084,6 +1084,110 @@ RSpec.describe Member, feature_category: :groups_and_projects do
end
end
+ context 'for updating organization_users' do
+ let_it_be(:group) { create(:group, :with_organization) }
+ let(:member) { create(:group_member, source: group) }
+ let(:update_organization_users_enabled) { true }
+
+ before do
+ stub_feature_flags(update_organization_users: update_organization_users_enabled)
+ end
+
+ context 'when update_organization_users is enabled' do
+ it 'inserts new record on member creation' do
+ expect { member }.to change { Organizations::OrganizationUser.count }.by(1)
+ record_attrs = { organization: group.organization, user: member.user, access_level: :default }
+ expect(Organizations::OrganizationUser.exists?(record_attrs)).to be(true)
+ end
+
+ context 'when user already exists in the organization_users' do
+ context 'for an already existing default organization_user' do
+ let_it_be(:project) { create(:project, group: group, organization: group.organization) }
+
+ before do
+ member
+ end
+
+ it 'does not insert a new record in organization_users' do
+ expect do
+ create(:project_member, :owner, source: project, user: member.user)
+ end.not_to change { Organizations::OrganizationUser.count }
+
+ expect(
+ Organizations::OrganizationUser.exists?(
+ organization: project.organization, user: member.user, access_level: :default
+ )
+ ).to be(true)
+ end
+
+ it 'does not update timestamps' do
+ travel_to(1.day.from_now) do
+ expect do
+ create(:project_member, :owner, source: project, user: member.user)
+ end.not_to change { Organizations::OrganizationUser.last.updated_at }
+ end
+ end
+ end
+
+ context 'for an already existing owner organization_user' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:common_attrs) { { organization: group.organization, user: user } }
+
+ before_all do
+ create(:organization_user, :owner, common_attrs)
+ end
+
+ it 'does not insert a new record in organization_users nor update the access_level' do
+ expect do
+ create(:group_member, :owner, source: group, user: user)
+ end.not_to change { Organizations::OrganizationUser.count }
+
+ expect(
+ Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :default))
+ ).to be(false)
+ expect(
+ Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :owner))
+ ).to be(true)
+ end
+ end
+ end
+
+ context 'when updating the organization_users is not successful' do
+ it 'rolls back the member creation' do
+ allow(Organizations::OrganizationUser).to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout)
+
+ expect { member }.to raise_error(ActiveRecord::StatementTimeout)
+ expect(Organizations::OrganizationUser.exists?(organization: group.organization)).to be(false)
+ expect(group.group_members).to be_empty
+ end
+ end
+ end
+
+ shared_examples_for 'does not create an organization_user entry' do
+ specify do
+ expect { member }.not_to change { Organizations::OrganizationUser.count }
+ end
+ end
+
+ context 'when update_organization_users is disabled' do
+ let(:update_organization_users_enabled) { false }
+
+ it_behaves_like 'does not create an organization_user entry'
+ end
+
+ context 'when member is an invite' do
+ let(:member) { create(:group_member, :invited, source: group) }
+
+ it_behaves_like 'does not create an organization_user entry'
+ end
+
+ context 'when organization does not exist' do
+ let(:member) { create(:group_member) }
+
+ it_behaves_like 'does not create an organization_user entry'
+ end
+ end
+
context 'when after_commit :update_highest_role' do
let_it_be(:user) { create(:user) }
diff --git a/spec/models/users/phone_number_validation_spec.rb b/spec/models/users/phone_number_validation_spec.rb
index 788df05763c..eb73fc31dac 100644
--- a/spec/models/users/phone_number_validation_spec.rb
+++ b/spec/models/users/phone_number_validation_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resiliency do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:user) { create(:user) }
let_it_be(:banned_user) { create(:user, :banned) }
@@ -250,4 +252,43 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
it { is_expected.to be_nil }
end
end
+
+ describe '.sms_send_allowed_after' do
+ let_it_be(:record) { create(:phone_number_validation, sms_send_count: 0) }
+
+ subject(:result) { record.sms_send_allowed_after }
+
+ context 'when there are no attempts yet' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when sms_send_wait_time feature flag is disabled' do
+ let_it_be(:record) { create(:phone_number_validation, sms_send_count: 1) }
+
+ before do
+ stub_feature_flags(sms_send_wait_time: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ where(:attempt_number, :expected_delay) do
+ 2 | 1.minute
+ 3 | 3.minutes
+ 4 | 5.minutes
+ 5 | 10.minutes
+ 6 | 10.minutes
+ end
+
+ with_them do
+ it 'returns the correct delayed timestamp value' do
+ freeze_time do
+ record.update!(sms_send_count: attempt_number - 1, sms_sent_at: Time.current)
+
+ expected_result = Time.current + expected_delay
+ expect(result).to eq expected_result
+ end
+ end
+ end
+ end
end
diff --git a/spec/policies/organizations/organization_policy_spec.rb b/spec/policies/organizations/organization_policy_spec.rb
index a1a2f1db305..9660ed578f7 100644
--- a/spec/policies/organizations/organization_policy_spec.rb
+++ b/spec/policies/organizations/organization_policy_spec.rb
@@ -32,8 +32,19 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do
end
context 'when the user is part of the organization' do
- before do
- create :organization_user, organization: organization, user: current_user
+ before_all do
+ create(:organization_user, organization: organization, user: current_user)
+ end
+
+ it { is_expected.to be_disallowed(:admin_organization) }
+ it { is_expected.to be_allowed(:create_group) }
+ it { is_expected.to be_allowed(:read_organization) }
+ it { is_expected.to be_allowed(:read_organization_user) }
+ end
+
+ context 'when the user is an owner of the organization' do
+ before_all do
+ create(:organization_user, :owner, organization: organization, user: current_user)
end
it { is_expected.to be_allowed(:admin_organization) }
diff --git a/spec/requests/api/graphql/mutations/organizations/update_spec.rb b/spec/requests/api/graphql/mutations/organizations/update_spec.rb
index 4e819c280d0..33890ae4592 100644
--- a/spec/requests/api/graphql/mutations/organizations/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/organizations/update_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Mutations::Organizations::Update, feature_category: :cell do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:organization) do
- create(:organization) { |org| create(:organization_user, organization: org, user: user) }
+ create(:organization) { |org| create(:organization_user, :owner, organization: org, user: user) }
end
let(:mutation) { graphql_mutation(:organization_update, params) }
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
index 73bcfe81d76..14becd52e93 100644
--- a/spec/requests/api/graphql/organizations/organization_query_spec.rb
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -67,13 +67,13 @@ RSpec.describe 'getting organization information', feature_category: :cell do
it 'returns correct organization user fields' do
request_organization
- organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first
+ organization_user_nodes = graphql_data_at(:organization, :organizationUsers, :nodes)
expected_attributes = {
"badges" => [{ "text" => "It's you!", "variant" => 'muted' }],
"id" => organization_user.to_global_id.to_s,
"user" => { "id" => user.to_global_id.to_s }
}
- expect(organization_user_node).to match(expected_attributes)
+ expect(organization_user_nodes).to include(expected_attributes)
end
it 'avoids N+1 queries for all the fields' do
diff --git a/spec/requests/organizations/settings_controller_spec.rb b/spec/requests/organizations/settings_controller_spec.rb
index 1d98e598159..0177187e3a3 100644
--- a/spec/requests/organizations/settings_controller_spec.rb
+++ b/spec/requests/organizations/settings_controller_spec.rb
@@ -21,13 +21,13 @@ RSpec.describe Organizations::SettingsController, feature_category: :cell do
end
context 'when the user is signed in' do
+ let_it_be(:user) { create(:user) }
+
before do
sign_in(user)
end
context 'with no association to an organization' do
- let_it_be(:user) { create(:user) }
-
it_behaves_like 'organization - not found response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
end
@@ -39,11 +39,18 @@ RSpec.describe Organizations::SettingsController, feature_category: :cell do
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
end
- context 'as an organization user' do
- let_it_be(:user) { create :user }
+ context 'as a default organization user' do
+ before_all do
+ create(:organization_user, organization: organization, user: user)
+ end
- before do
- create :organization_user, organization: organization, user: user
+ it_behaves_like 'organization - not found response'
+ it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
+ end
+
+ context 'as an owner of an organization' do
+ before_all do
+ create(:organization_user, :owner, organization: organization, user: user)
end
it_behaves_like 'organization - successful response'
diff --git a/spec/services/ci/catalog/resources/create_service_spec.rb b/spec/services/ci/catalog/resources/create_service_spec.rb
index 202c76acaec..5839b9ac2fe 100644
--- a/spec/services/ci/catalog/resources/create_service_spec.rb
+++ b/spec/services/ci/catalog/resources/create_service_spec.rb
@@ -8,10 +8,6 @@ RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipelin
let(:service) { described_class.new(project, user) }
- before do
- stub_licensed_features(ci_namespace_catalog: true)
- end
-
describe '#execute' do
context 'with an unauthorized user' do
it 'raises an AccessDeniedError' do
diff --git a/spec/services/ci/catalog/resources/destroy_service_spec.rb b/spec/services/ci/catalog/resources/destroy_service_spec.rb
index da5ba7ad0bc..4783506416d 100644
--- a/spec/services/ci/catalog/resources/destroy_service_spec.rb
+++ b/spec/services/ci/catalog/resources/destroy_service_spec.rb
@@ -9,10 +9,6 @@ RSpec.describe Ci::Catalog::Resources::DestroyService, feature_category: :pipeli
let(:service) { described_class.new(project, user) }
- before do
- stub_licensed_features(ci_namespace_catalog: true)
- end
-
describe '#execute' do
context 'with an unauthorized user' do
it 'raises an AccessDeniedError' do
diff --git a/spec/services/organizations/update_service_spec.rb b/spec/services/organizations/update_service_spec.rb
index 630bfdfe1d7..30c07ae1d13 100644
--- a/spec/services/organizations/update_service_spec.rb
+++ b/spec/services/organizations/update_service_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Organizations::UpdateService, feature_category: :cell do
context 'when user has permission' do
before_all do
- create(:organization_user, organization: organization, user: current_user)
+ create(:organization_user, :owner, organization: organization, user: current_user)
end
shared_examples 'updating an organization' do
diff --git a/spec/services/security/merge_reports_service_spec.rb b/spec/services/security/merge_reports_service_spec.rb
index c141bbe5b5a..a65e73bd8ce 100644
--- a/spec/services/security/merge_reports_service_spec.rb
+++ b/spec/services/security/merge_reports_service_spec.rb
@@ -20,7 +20,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
- severity: :low
+ severity: :low,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94610'
)
end
@@ -29,7 +30,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
- severity: :low
+ severity: :low,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94611'
)
end
@@ -39,7 +41,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94614'
)
end
@@ -49,7 +52,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94613'
)
end
@@ -59,7 +63,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 42, end_line: 44),
scanner: scanner_2,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94612'
)
end
@@ -68,7 +73,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_3,
- severity: :high
+ severity: :high,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94615'
)
end
@@ -77,7 +83,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_1,
- severity: :critical
+ severity: :critical,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94616'
)
end
@@ -86,7 +93,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_1,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94617'
)
end
@@ -95,7 +103,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_2,
- severity: :critical
+ severity: :critical,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94618'
)
end
@@ -226,9 +235,32 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
let(:identifier_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-123', external_type: 'cve') }
let(:identifier_semgrep) { build(:ci_reports_security_identifier, external_id: 'rules.bandit.B105', external_type: 'semgrep_id') }
- let(:finding_id_1) { build(:ci_reports_security_finding, identifiers: [identifier_bandit, identifier_cve], scanner: bandit_scanner, report_type: :sast) }
- let(:finding_id_2) { build(:ci_reports_security_finding, identifiers: [identifier_cve], scanner: semgrep_scanner, report_type: :sast) }
- let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast) }
+ let(:finding_id_1) do
+ build(
+ :ci_reports_security_finding,
+ identifiers: [identifier_bandit, identifier_cve],
+ scanner: bandit_scanner,
+ report_type: :sast,
+ uuid: '21ab978a-7052-5428-af0b-c7a4b3fe5020')
+ end
+
+ let(:finding_id_2) do
+ build(
+ :ci_reports_security_finding,
+ identifiers: [identifier_cve],
+ scanner: semgrep_scanner,
+ report_type: :sast,
+ uuid: '21ab978a-7052-5428-af0b-c7a4b3fe5021')
+ end
+
+ let(:finding_id_3) do
+ build(
+ :ci_reports_security_finding,
+ identifiers: [identifier_semgrep],
+ scanner: semgrep_scanner,
+ report_type: :sast,
+ uuid: '21ab978a-7052-5428-af0b-c7a4b3fe5022')
+ end
let(:bandit_report) do
build(:ci_reports_security_report,
diff --git a/spec/validators/ip_cidr_array_validator_spec.rb b/spec/validators/ip_cidr_array_validator_spec.rb
index 6adb0bc70db..f18005054b5 100644
--- a/spec/validators/ip_cidr_array_validator_spec.rb
+++ b/spec/validators/ip_cidr_array_validator_spec.rb
@@ -17,7 +17,6 @@ RSpec.describe IpCidrArrayValidator, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
- # noinspection RubyMismatchedArgumentType - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-32041
where(:cidr_array, :validity, :errors) do
# rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors
nil | false | { cidr_array: ["must be an array of CIDR values"] }
diff --git a/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb b/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb
new file mode 100644
index 00000000000..d4fa35b9b82
--- /dev/null
+++ b/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClickHouse::EventAuthorsConsistencyCronWorker, feature_category: :value_stream_management do
+ let(:worker) { described_class.new }
+
+ context 'when ClickHouse is disabled' do
+ it 'does nothing' do
+ allow(ClickHouse::Client).to receive(:database_configured?).and_return(false)
+
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ worker.perform
+ end
+ end
+
+ context 'when the event_sync_worker_for_click_house feature flag is off' do
+ it 'does nothing' do
+ allow(ClickHouse::Client).to receive(:database_configured?).and_return(true)
+ stub_feature_flags(event_sync_worker_for_click_house: false)
+
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ worker.perform
+ end
+ end
+
+ context 'when ClickHouse is available', :click_house do
+ let_it_be(:connection) { ClickHouse::Connection.new(:main) }
+ let_it_be_with_reload(:user1) { create(:user) }
+ let_it_be_with_reload(:user2) { create(:user) }
+
+ let(:leftover_author_ids) { connection.select('SELECT DISTINCT author_id FROM events FINAL').pluck('author_id') }
+ let(:deleted_user_id1) { user2.id + 1 }
+ let(:deleted_user_id2) { user2.id + 2 }
+
+ before do
+ insert_query = <<~SQL
+ INSERT INTO events (id, author_id) VALUES
+ (1, #{user1.id}),
+ (2, #{user2.id}),
+ (3, #{deleted_user_id1}),
+ (4, #{deleted_user_id1}),
+ (5, #{deleted_user_id2})
+ SQL
+
+ connection.execute(insert_query)
+ end
+
+ it 'cleans up all inconsistent records in ClickHouse' do
+ worker.perform
+
+ expect(leftover_author_ids).to contain_exactly(user1.id, user2.id)
+
+ # the next job starts from the beginning of the table
+ expect(ClickHouse::SyncCursor.cursor_for(:event_authors_consistency_check)).to eq(0)
+ end
+
+ context 'when the previous job was not finished' do
+ it 'continues the processing from the cursor' do
+ ClickHouse::SyncCursor.update_cursor_for(:event_authors_consistency_check, deleted_user_id1)
+
+ worker.perform
+
+ # the previous records should remain
+ expect(leftover_author_ids).to contain_exactly(user1.id, user2.id)
+ end
+ end
+
+ context 'when processing stops due to the record clean up limit' do
+ it 'stores the last processed id value' do
+ User.where(id: [user1.id, user2.id]).delete_all
+
+ stub_const("#{described_class}::MAX_AUTHOR_DELETIONS", 2)
+ stub_const("#{described_class}::POSTGRESQL_BATCH_SIZE", 1)
+
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:result,
+ { status: :deletion_limit_reached, deletions: 2 })
+
+ worker.perform
+
+ expect(leftover_author_ids).to contain_exactly(deleted_user_id1, deleted_user_id2)
+ expect(ClickHouse::SyncCursor.cursor_for(:event_authors_consistency_check)).to eq(user2.id)
+ end
+ end
+
+ context 'when time limit is reached' do
+ it 'stops the processing earlier' do
+ stub_const("#{described_class}::POSTGRESQL_BATCH_SIZE", 1)
+
+ # stop at the third author_id
+ allow_next_instance_of(Analytics::CycleAnalytics::RuntimeLimiter) do |runtime_limiter|
+ allow(runtime_limiter).to receive(:over_time?).and_return(false, false, true)
+ end
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :over_time, deletions: 1 })
+
+ worker.perform
+
+ expect(leftover_author_ids).to contain_exactly(user1.id, user2.id, deleted_user_id2)
+ end
+ end
+ end
+end