diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-09 21:07:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-09 21:07:32 +0300 |
commit | cd22685717501ac6f3c81e16418270123a2cccee (patch) | |
tree | eb45b2ea1c3ac174aaa4fd54a38901f1f8420838 /spec | |
parent | 1f753bca2624be1e507424127fe0d48b9765da70 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |