diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 09:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 09:08:36 +0000 |
commit | 19e00b948726c0f7ca27dd92200493803499a4e1 (patch) | |
tree | 0df898db4ba20af4b4de2baf39285fe4d113d148 /spec | |
parent | ca5ebd2044ce696cc1aafc8a80a606e20f2c9e4b (diff) | |
download | gitlab-ce-19e00b948726c0f7ca27dd92200493803499a4e1.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
34 files changed, 787 insertions, 193 deletions
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 9e23cca7c3f..65d5da00087 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -166,7 +166,7 @@ RSpec.describe 'Database schema' do context 'columns ending with _id' do let(:column_names) { columns.map(&:name) } let(:column_names_with_id) { column_names.select { |column_name| column_name.ends_with?('_id') } } - let(:foreign_keys_columns) { all_foreign_keys.map(&:column).uniq } # we can have FK and loose FK present at the same time + let(:foreign_keys_columns) { all_foreign_keys.reject { |fk| fk.name&.end_with?("_p") }.map(&:column).uniq } # we can have FK and loose FK present at the same time let(:ignored_columns) { ignored_fk_columns(table) } it 'do have the foreign keys' do diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index a49bd48d51e..4211f2b6265 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -72,7 +72,7 @@ RSpec.describe 'Groups > Members > Manage members', feature_category: :subgroups visit group_group_members_path(group) - invite_member(user1.name, role: 'Reporter', refresh: false) + invite_member(user1.name, role: 'Reporter') invite_modal = page.find(invite_modal_selector) expect(invite_modal).to have_content("not authorized to update member") diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 4d85b5cfb2e..22aae2b67c2 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'Project navbar', feature_category: :projects do before do sign_in(user) + Feature.disable(:show_pages_in_deployments_menu, :project) + stub_config(registry: { enabled: false }) stub_feature_flags(harbor_registry_integration: false) insert_package_nav(_('Deployments')) diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb index 7ceefdecbae..4e51d8a615c 100644 --- a/spec/features/projects/pages/user_edits_settings_spec.rb +++ b/spec/features/projects/pages/user_edits_settings_spec.rb @@ -10,6 +10,8 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do before do allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + Feature.disable(:show_pages_in_deployments_menu, :project) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index 661cb6dabb1..ee832da48d9 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -51,8 +51,6 @@ RSpec.describe 'Projects > Settings > User manages project members', feature_cat click_button 'Import project members' wait_for_requests - page.refresh - expect(find_member_row(user_mike)).to have_content('Reporter') end diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 6f9535286ed..efc609b3c3f 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -1,55 +1,118 @@ { "type": "object", - "required" : [ + "required": [ "status", "applications" ], - "properties" : { - "status": { "type": "string" }, - "status_reason": { "type": ["string", "null"] }, + "properties": { + "status": { + "type": "string" + }, + "status_reason": { + "$ref": "types/nullable_string.json" + }, "applications": { "type": "array", - "items": { "$ref": "#/definitions/application_status" } + "items": { + "$ref": "#/definitions/application_status" + } } }, "additionalProperties": false, "definitions": { "application_status": { "type": "object", + "required": [ + "name", + "status" + ], "additionalProperties": false, - "properties" : { - "name": { "type": "string" }, + "properties": { + "name": { + "type": "string" + }, "status": { - "type": { - "enum": [ - "installable", - "scheduled", - "installing", - "installed", - "errored" - ] - } + "type": "string", + "enum": [ + "installable", + "scheduled", + "installing", + "installed", + "errored", + "not_installable" + ] + }, + "version": { + "type": "string" + }, + "status_reason": { + "$ref": "types/nullable_string.json" + }, + "external_ip": { + "$ref": "types/nullable_string.json" + }, + "external_hostname": { + "$ref": "types/nullable_string.json" + }, + "hostname": { + "$ref": "types/nullable_string.json" + }, + "email": { + "$ref": "types/nullable_string.json" + }, + "stack": { + "$ref": "types/nullable_string.json" + }, + "host": { + "$ref": "types/nullable_string.json" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "integer" + }, + "update_available": { + "type": [ + "boolean", + "null" + ] + }, + "can_uninstall": { + "type": "boolean" }, - "version": { "type": "string" }, - "status_reason": { "type": ["string", "null"] }, - "external_ip": { "type": ["string", "null"] }, - "external_hostname": { "type": ["string", "null"] }, - "hostname": { "type": ["string", "null"] }, - "email": { "type": ["string", "null"] }, - "stack": { "type": ["string", "null"] }, - "host": {"type": ["string", "null"]}, - "port": {"type": ["integer", "514"]}, - "protocol": {"type": ["integer", "0"]}, - "update_available": { "type": ["boolean", "null"] }, - "can_uninstall": { "type": "boolean" }, "available_domains": { "type": "array", - "items": { "$ref": "#/definitions/domain" } + "items": { + "$ref": "#/definitions/domain" + } }, - "pages_domain": { "type": [ { "$ref": "#/definitions/domain" }, "null"] } - }, - "required" : [ "name", "status" ] + "pages_domain": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/domain" + } + ] + } + } }, - "domain": { "id": "integer", "domain": "string" } + "domain": { + "type": "object", + "required": [ + "id", + "domain" + ], + "properties": { + "id": { + "type": "integer" + }, + "domain": { + "type": "string" + } + } + } } -} +}
\ No newline at end of file diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json index a947783d505..f8acac9f074 100644 --- a/spec/fixtures/api/schemas/conflicts.json +++ b/spec/fixtures/api/schemas/conflicts.json @@ -8,16 +8,29 @@ "files" ], "properties": { - "commit_message": {"type": "string"}, - "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, - "source_branch": {"type": "string"}, - "target_branch": {"type": "string"}, + "commit_message": { + "type": "string" + }, + "commit_sha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "source_branch": { + "type": "string" + }, + "target_branch": { + "type": "string" + }, "files": { "type": "array", "items": { "oneOf": [ - { "$ref": "#/definitions/conflict-text-with-sections" }, - { "$ref": "#/definitions/conflict-text-for-editor" } + { + "$ref": "#/definitions/conflict-text-with-sections" + }, + { + "$ref": "#/definitions/conflict-text-for-editor" + } ] } } @@ -32,15 +45,25 @@ "blob_path" ], "properties": { - "old_path": {"type": "string"}, - "new_path": {"type": "string"}, - "blob_icon": {"type": "string"}, - "blob_path": {"type": "string"} + "old_path": { + "type": "string" + }, + "new_path": { + "type": "string" + }, + "blob_icon": { + "type": "string" + }, + "blob_path": { + "type": "string" + } } }, "conflict-text-for-editor": { "allOf": [ - {"$ref": "#/definitions/conflict-base"}, + { + "$ref": "#/definitions/conflict-base" + }, { "type": "object", "required": [ @@ -48,15 +71,25 @@ "content_path" ], "properties": { - "type": {"type": {"enum": ["text-editor"]}}, - "content_path": {"type": "string"} + "type": { + "type": "string", + "enum": [ + "text", + "text-editor" + ] + }, + "content_path": { + "type": "string" + } } } ] }, "conflict-text-with-sections": { "allOf": [ - {"$ref": "#/definitions/conflict-base"}, + { + "$ref": "#/definitions/conflict-base" + }, { "type": "object", "required": [ @@ -65,14 +98,25 @@ "sections" ], "properties": { - "type": {"type": {"enum": ["text"]}}, - "content_path": {"type": "string"}, + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "content_path": { + "type": "string" + }, "sections": { "type": "array", "items": { "oneOf": [ - { "$ref": "#/definitions/section-context" }, - { "$ref": "#/definitions/section-conflict" } + { + "$ref": "#/definitions/section-context" + }, + { + "$ref": "#/definitions/section-conflict" + } ] } } @@ -87,7 +131,9 @@ "lines" ], "properties": { - "conflict": {"type": "boolean"}, + "conflict": { + "type": "boolean" + }, "lines": { "type": "array", "items": { @@ -99,11 +145,21 @@ "rich_text" ], "properties": { - "type": {"type": "string"}, - "old_line": {"type": "string"}, - "new_line": {"type": "string"}, - "text": {"type": "string"}, - "rich_text": {"type": "string"} + "type": { + "type": "string" + }, + "old_line": { + "type": "string" + }, + "new_line": { + "type": "string" + }, + "text": { + "type": "string" + }, + "rich_text": { + "type": "string" + } } } } @@ -111,27 +167,39 @@ }, "section-context": { "allOf": [ - {"$ref": "#/definitions/section-base"}, + { + "$ref": "#/definitions/section-base" + }, { "type": "object", "properties": { - "conflict": {"enum": [false]} + "conflict": { + "type": "boolean" + } } } ] }, "section-conflict": { "allOf": [ - {"$ref": "#/definitions/section-base"}, + { + "$ref": "#/definitions/section-base" + }, { "type": "object", - "required": ["id"], + "required": [ + "id" + ], "properties": { - "conflict": {"enum": [true]}, - "id": {"type": "string"} + "conflict": { + "type": "boolean" + }, + "id": { + "type": "string" + } } } ] } } -} +}
\ No newline at end of file diff --git a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json index d7e5a80df61..f0509f7a76f 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json @@ -35,10 +35,7 @@ "type": "boolean" }, "ff_only_enabled": { - "type": [ - "boolean", - false - ] + "type": "boolean" }, "merge_user": { "type": [ diff --git a/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json index 47b5d283b8c..f32ae3d334c 100644 --- a/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json +++ b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json @@ -1,13 +1,29 @@ { + "type": "array", "items": { + "type": "object", + "required": [ + "group", + "metrics", + "priority" + ], "properties": { "group": { "type": "string" }, "metrics": { + "type": "array", "items": { + "type": "object", + "required": [ + "queries", + "title", + "weight" + ], "properties": { "queries": { + "type": "array", + "required": [], "items": { "properties": { "query_range": { @@ -16,13 +32,33 @@ "query": { "type": "string" }, + "label": { + "type": "string" + }, + "unit": { + "type": "string" + }, "result": { - "type": "any" + "type": "array", + "items": { + "type": "object", + "required": [ + "metric", + "values" + ], + "properties": { + "metric": { + "type": "object" + }, + "values": { + "type": "array" + } + } + } } }, "type": "object" - }, - "type": "array" + } }, "title": { "type": "string" @@ -33,26 +69,12 @@ "y_label": { "type": "string" } - }, - "type": "object" - }, - "required": [ - "metrics", - "title", - "weight" - ], - "type": "array" + } + } }, "priority": { "type": "integer" } - }, - "type": "object" - }, - "required": [ - "group", - "priority", - "metrics" - ], - "type": "array" + } + } }
\ No newline at end of file diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js index 8b2d13be309..d839cde163c 100644 --- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js +++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js @@ -8,6 +8,12 @@ import * as ProjectsApi from '~/api/projects_api'; import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue'; import ProjectSelect from '~/invite_members/components/project_select.vue'; import axios from '~/lib/utils/axios_utils'; +import { + displaySuccessfulInvitationAlert, + reloadOnInvitationSuccess, +} from '~/invite_members/utils/trigger_successful_invite_alert'; + +jest.mock('~/invite_members/utils/trigger_successful_invite_alert'); let wrapper; let mock; @@ -19,11 +25,12 @@ const $toast = { show: jest.fn(), }; -const createComponent = () => { +const createComponent = ({ props = {} } = {}) => { wrapper = shallowMountExtended(ImportProjectMembersModal, { propsData: { projectId, projectName, + ...props, }, stubs: { GlModal: stubComponent(GlModal, { @@ -101,6 +108,35 @@ describe('ImportProjectMembersModal', () => { }); describe('submitting the import', () => { + describe('when the import is successful with reloadPageOnSubmit', () => { + beforeEach(() => { + createComponent({ + props: { reloadPageOnSubmit: true }, + }); + + findProjectSelect().vm.$emit('input', projectToBeImported); + + jest.spyOn(ProjectsApi, 'importProjectMembers').mockResolvedValue(); + + clickImportButton(); + }); + + it('calls displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).toHaveBeenCalled(); + }); + + it('calls reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).toHaveBeenCalled(); + }); + + it('does not display the successful toastMessage', () => { + expect($toast.show).not.toHaveBeenCalledWith( + 'Successfully imported', + wrapper.vm.$options.toastOptions, + ); + }); + }); + describe('when the import is successful', () => { beforeEach(() => { createComponent(); @@ -126,6 +162,14 @@ describe('ImportProjectMembersModal', () => { ); }); + it('does not call displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); + }); + + it('does not call reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); + }); + it('sets isLoading to false after success', () => { expect(findGlModal().props('actionPrimary').attributes.loading).toBe(false); }); diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js index 49914f7351c..c2a55517405 100644 --- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js @@ -8,8 +8,14 @@ import ContentTransition from '~/vue_shared/components/content_transition.vue'; import GroupSelect from '~/invite_members/components/group_select.vue'; import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue'; import { stubComponent } from 'helpers/stub_component'; +import { + displaySuccessfulInvitationAlert, + reloadOnInvitationSuccess, +} from '~/invite_members/utils/trigger_successful_invite_alert'; import { propsData, sharedGroup } from '../mock_data/group_modal'; +jest.mock('~/invite_members/utils/trigger_successful_invite_alert'); + describe('InviteGroupsModal', () => { let wrapper; @@ -142,6 +148,14 @@ describe('InviteGroupsModal', () => { onComplete: expect.any(Function), }); }); + + it('does not call displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); + }); + + it('does not call reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); + }); }); describe('when fails', () => { @@ -172,4 +186,37 @@ describe('InviteGroupsModal', () => { }); }); }); + + describe('submitting the invite form with reloadPageOnSubmit set true', () => { + const groupPostData = { + group_id: sharedGroup.id, + group_access: propsData.defaultAccessLevel, + expires_at: undefined, + format: 'json', + }; + + beforeEach(() => { + createComponent({ reloadPageOnSubmit: true }); + triggerGroupSelect(sharedGroup); + + wrapper.vm.$toast = { show: jest.fn() }; + jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData }); + + clickInviteButton(); + }); + + describe('when succeeds', () => { + it('calls displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).toHaveBeenCalled(); + }); + + it('calls reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).toHaveBeenCalled(); + }); + + it('does not show the toast message on failure', () => { + expect(wrapper.vm.$toast.show).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 834e4f1577b..22fcedb2eaf 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -26,6 +26,10 @@ import ContentTransition from '~/vue_shared/components/content_transition.vue'; import axios from '~/lib/utils/axios_utils'; import httpStatus, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status'; import { getParameterValues } from '~/lib/utils/url_utility'; +import { + displaySuccessfulInvitationAlert, + reloadOnInvitationSuccess, +} from '~/invite_members/utils/trigger_successful_invite_alert'; import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses'; import { propsData, @@ -40,6 +44,7 @@ import { GlEmoji, } from '../mock_data/member_modal'; +jest.mock('~/invite_members/utils/trigger_successful_invite_alert'); jest.mock('~/experimentation/experiment_tracking'); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), @@ -420,6 +425,29 @@ describe('InviteMembersModal', () => { tasks_project_id: '', }; + describe('when reloadOnSubmit is true', () => { + beforeEach(async () => { + createComponent({ reloadPageOnSubmit: true }); + await triggerMembersTokenSelect([user1, user2]); + + wrapper.vm.$toast = { show: jest.fn() }; + jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData }); + clickInviteButton(); + }); + + it('calls displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).toHaveBeenCalled(); + }); + + it('calls reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).toHaveBeenCalled(); + }); + + it('does not show the toast message', () => { + expect(wrapper.vm.$toast.show).not.toHaveBeenCalled(); + }); + }); + describe('when member is added successfully', () => { beforeEach(async () => { createComponent(); @@ -441,6 +469,14 @@ describe('InviteMembersModal', () => { it('displays the successful toastMessage', () => { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added'); }); + + it('does not call displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); + }); + + it('does not call reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); + }); }); describe('when opened from a Learn GitLab page', () => { @@ -593,6 +629,14 @@ describe('InviteMembersModal', () => { it('displays the successful toastMessage', () => { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added'); }); + + it('does not call displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); + }); + + it('does not call reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); + }); }); }); @@ -680,6 +724,14 @@ describe('InviteMembersModal', () => { expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); }); + + it('does not call displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); + }); + + it('does not call reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); + }); }); describe('when multiple emails are invited at the same time', () => { @@ -794,6 +846,14 @@ describe('InviteMembersModal', () => { it('displays the successful toastMessage', () => { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added'); }); + + it('does not call displaySuccessfulInvitationAlert on mount', () => { + expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); + }); + + it('does not call reloadOnInvitationSuccess', () => { + expect(reloadOnInvitationSuccess).not.toHaveBeenCalled(); + }); }); it('calls Apis with the invite source passed through to openModal', async () => { diff --git a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js new file mode 100644 index 00000000000..38b16dd0c2c --- /dev/null +++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js @@ -0,0 +1,54 @@ +import { + displaySuccessfulInvitationAlert, + reloadOnInvitationSuccess, +} from '~/invite_members/utils/trigger_successful_invite_alert'; +import { + TOAST_MESSAGE_LOCALSTORAGE_KEY, + TOAST_MESSAGE_SUCCESSFUL, +} from '~/invite_members/constants'; +import { createAlert } from '~/flash'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; + +jest.mock('~/flash'); +useLocalStorageSpy(); + +describe('Display Successful Invitation Alert', () => { + it('does not show alert if localStorage key not present', () => { + localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY); + + displaySuccessfulInvitationAlert(); + + expect(createAlert).not.toHaveBeenCalled(); + }); + + it('shows alert when localStorage key is present', () => { + localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true'); + + displaySuccessfulInvitationAlert(); + + expect(createAlert).toHaveBeenCalledWith({ + message: TOAST_MESSAGE_SUCCESSFUL, + variant: 'info', + }); + }); +}); + +describe('Reload On Invitation Success', () => { + const { location } = window; + + beforeAll(() => { + delete window.location; + window.location = { reload: jest.fn() }; + }); + + afterAll(() => { + window.location = location; + }); + + it('sets localStorage value and calls window.location.reload', () => { + reloadOnInvitationSuccess(); + + expect(localStorage.setItem).toHaveBeenCalledWith(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true'); + expect(window.location.reload).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js index adea2b6df59..9accfcea791 100644 --- a/spec/frontend/issues/show/components/incidents/mock_data.js +++ b/spec/frontend/issues/show/components/incidents/mock_data.js @@ -13,6 +13,9 @@ export const mockEvents = [ noteHtml: '<p>Dummy event 1</p>', occurredAt: '2022-03-22T15:59:00Z', updatedAt: '2022-03-22T15:59:08Z', + timelineEventTags: { + nodes: [], + }, __typename: 'TimelineEventType', }, { @@ -29,6 +32,18 @@ export const mockEvents = [ noteHtml: '<p>Dummy event 2</p>', occurredAt: '2022-03-23T14:57:00Z', updatedAt: '2022-03-23T14:57:08Z', + timelineEventTags: { + nodes: [ + { + id: 'gid://gitlab/IncidentManagement::TimelineEvent/132', + name: 'Start time', + }, + { + id: 'gid://gitlab/IncidentManagement::TimelineEvent/132', + name: 'End time', + }, + ], + }, __typename: 'TimelineEventType', }, { @@ -45,6 +60,9 @@ export const mockEvents = [ noteHtml: '<p>Dummy event 3</p>', occurredAt: '2022-03-23T15:59:00Z', updatedAt: '2022-03-23T15:59:08Z', + timelineEventTags: { + nodes: [], + }, __typename: 'TimelineEventType', }, ]; @@ -152,6 +170,9 @@ export const mockGetTimelineData = { action: 'comment', occurredAt: '2022-07-01T12:47:00Z', createdAt: '2022-07-20T12:47:40Z', + timelineEventTags: { + nodes: [], + }, }, ], }, diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js index dff1c429d07..a7250e8ad0d 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js @@ -92,6 +92,9 @@ describe('IncidentTimelineEventList', () => { expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt); expect(findItems().at(1).props('action')).toBe(mockEvents[1].action); expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml); + expect(findItems().at(1).props('eventTag')).toBe( + mockEvents[1].timelineEventTags.nodes[0].name, + ); }); it('formats dates correctly', () => { @@ -120,6 +123,20 @@ describe('IncidentTimelineEventList', () => { }); }); + describe('getFirstTag', () => { + it('returns undefined, when timelineEventTags contains an empty array', () => { + const returnedTag = wrapper.vm.getFirstTag(mockEvents[0].timelineEventTags); + + expect(returnedTag).toEqual(undefined); + }); + + it('returns the first string, when timelineEventTags contains array with at least one tag', () => { + const returnedTag = wrapper.vm.getFirstTag(mockEvents[1].timelineEventTags); + + expect(returnedTag).toBe(mockEvents[1].timelineEventTags.nodes[0].name); + }); + }); + describe('delete functionality', () => { beforeEach(() => { mockConfirmAction({ confirmed: true }); diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js index d5b78cebcb3..33c6394eb41 100644 --- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js +++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js @@ -364,6 +364,7 @@ describe('Pipeline Wizard - wrapper.vue', () => { extra: { fromStep: 0, toStep: 1, + features: expect.any(Object), }, }); }); @@ -386,6 +387,7 @@ describe('Pipeline Wizard - wrapper.vue', () => { extra: { fromStep: 1, toStep: 0, + features: expect.any(Object), }, }); }); @@ -409,6 +411,7 @@ describe('Pipeline Wizard - wrapper.vue', () => { extra: { fromStep: 2, toStep: 1, + features: expect.any(Object), }, }); }); @@ -429,6 +432,9 @@ describe('Pipeline Wizard - wrapper.vue', () => { category: trackingCategory, label: 'pipeline_wizard_commit', property: 'commit', + extra: { + features: expect.any(Object), + }, }); }); @@ -443,6 +449,7 @@ describe('Pipeline Wizard - wrapper.vue', () => { label: 'pipeline_wizard_editor_interaction', extra: { currentStep: 0, + features: expect.any(Object), }, }); }); diff --git a/spec/lib/gitlab/counters/buffered_counter_spec.rb b/spec/lib/gitlab/counters/buffered_counter_spec.rb index a1fd97768ea..cd07a04cc89 100644 --- a/spec/lib/gitlab/counters/buffered_counter_spec.rb +++ b/spec/lib/gitlab/counters/buffered_counter_spec.rb @@ -52,6 +52,31 @@ RSpec.describe Gitlab::Counters::BufferedCounter, :clean_gitlab_redis_shared_sta end end + describe '#bulk_increment' do + let(:increments) { [123, 456] } + + it 'increments the key by the given values' do + counter.bulk_increment(increments) + + expect(counter.get).to eq(increments.sum) + end + + it 'returns the value of the key after the increment' do + counter.increment(100) + + result = counter.bulk_increment(increments) + + expect(result).to eq(100 + increments.sum) + end + + it 'schedules a worker to commit the counter into database' do + expect(FlushCounterIncrementsWorker).to receive(:perform_in) + .with(described_class::WORKER_DELAY, counter_record.class.to_s, counter_record.id, attribute) + + counter.bulk_increment(increments) + end + end + describe '#reset!' do before do allow(counter_record).to receive(:update!) diff --git a/spec/lib/gitlab/counters/legacy_counter_spec.rb b/spec/lib/gitlab/counters/legacy_counter_spec.rb index e66b1ce08c4..5b2b1b51215 100644 --- a/spec/lib/gitlab/counters/legacy_counter_spec.rb +++ b/spec/lib/gitlab/counters/legacy_counter_spec.rb @@ -27,6 +27,27 @@ RSpec.describe Gitlab::Counters::LegacyCounter do end end + describe '#bulk_increment' do + let(:increments) { [123, 456] } + + it 'increments the attribute in the counter record' do + expect { counter.bulk_increment(increments) } + .to change { counter_record.reload.method(attribute).call }.by(increments.sum) + end + + it 'returns the value after the increment' do + counter.increment(100) + + expect(counter.bulk_increment(increments)).to eq(100 + increments.sum) + end + + it 'executes after counter_record after commit callback' do + expect(counter_record).to receive(:execute_after_commit_callbacks).and_call_original + + counter.bulk_increment(increments) + end + end + describe '#reset!' do before do allow(counter_record).to receive(:update!) diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb index 1f00f7bbec3..10e336e9235 100644 --- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb +++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb @@ -12,6 +12,12 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi describe '.calculate_count_for_aggregation' do using RSpec::Parameterized::TableSyntax + before do + %w[event1 event2].each do |event_name| + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_event?).with(event_name).and_return(true) + end + end + context 'with valid configuration' do where(:number_of_days, :operator, :datasource, :expected_method) do 28 | 'AND' | 'redis_hll' | :calculate_metrics_intersections diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index 0733e0c6521..e31eeccca7a 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -10,6 +10,10 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do subject { described_class.new(context) } + before do + Feature.disable(:show_pages_in_deployments_menu, :project) + end + describe '#render?' do it 'returns false when menu does not have any menu items' do allow(subject).to receive(:has_renderable_items?).and_return(false) diff --git a/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb b/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb index cc807951d3e..d86720365c4 100644 --- a/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb +++ b/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' require_migration! -RSpec.describe BackfillProjectStatisticsStorageSizeWithoutUploadsSize, feature_category: :subscription_usage_reports do +RSpec.describe BackfillProjectStatisticsStorageSizeWithoutUploadsSize, + feature_category: :subscription_cost_management do let!(:batched_migration) { described_class::MIGRATION_CLASS } it 'does not schedule background jobs when Gitlab.org_or_com? is false' do diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 18aaab1d1f3..5477eb9fb5d 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -690,8 +690,8 @@ RSpec.describe Ci::JobArtifact do end it 'updates project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(project, :build_artifacts_size, -job_artifact.file.size) + expect(ProjectStatistics).to receive(:bulk_increment_statistic).once + .with(project, :build_artifacts_size, [-job_artifact.file.size]) pipeline.destroy! end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f33001b9c5b..2ecd438274f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7612,32 +7612,6 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#increment_statistic_value' do - let(:project) { build_stubbed(:project) } - - subject(:increment) do - project.increment_statistic_value(:build_artifacts_size, -10) - end - - it 'increments the value' do - expect(ProjectStatistics) - .to receive(:increment_statistic) - .with(project, :build_artifacts_size, -10) - - increment - end - - context 'when the project is scheduled for removal' do - let(:project) { build_stubbed(:project, pending_delete: true) } - - it 'does not increment the value' do - expect(ProjectStatistics).not_to receive(:increment_statistic) - - increment - end - end - end - describe 'topics' do let_it_be(:project) { create(:project, name: 'topic-project', topic_list: 'topic1, topic2, topic3') } diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index a6e2bcf1525..e6eda77f7ae 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -455,7 +455,7 @@ RSpec.describe ProjectStatistics do end describe '.increment_statistic' do - shared_examples 'a statistic that increases storage_size' do + shared_examples 'a statistic that increases storage_size synchronously' do it 'increases the statistic by that amount' do expect { described_class.increment_statistic(project, stat, 13) } .to change { statistics.reload.send(stat) || 0 } @@ -474,6 +474,17 @@ RSpec.describe ProjectStatistics do described_class.increment_statistic(project, stat, 20) end + + context 'when the project is pending delete' do + before do + project.update_attribute(:pending_delete, true) + end + + it 'does not change the statistics' do + expect { described_class.increment_statistic(project, stat, 13) } + .not_to change { statistics.reload.send(stat) } + end + end end shared_examples 'a statistic that increases storage_size asynchronously' do @@ -497,6 +508,17 @@ RSpec.describe ProjectStatistics do .to change { statistics.reload.send(stat) }.by(20) .and change { statistics.reload.send(:storage_size) }.by(20) end + + context 'when the project is pending delete' do + before do + project.update_attribute(:pending_delete, true) + end + + it 'does not change the statistics' do + expect { described_class.increment_statistic(project, stat, 13) } + .not_to change { [statistics.reload.send(stat), statistics.reload.send(:storage_size)] } + end + end end context 'when adjusting :build_artifacts_size' do @@ -508,7 +530,7 @@ RSpec.describe ProjectStatistics do context 'when adjusting :pipeline_artifacts_size' do let(:stat) { :pipeline_artifacts_size } - it_behaves_like 'a statistic that increases storage_size' + it_behaves_like 'a statistic that increases storage_size synchronously' end context 'when adjusting :packages_size' do @@ -532,4 +554,114 @@ RSpec.describe ProjectStatistics do end end end + + describe '.bulk_increment_statistic' do + let(:increments) { [10, 3] } + let(:total_amount) { increments.sum } + + shared_examples 'a statistic that increases storage_size synchronously' do + it 'increases the statistic by that amount' do + expect { described_class.bulk_increment_statistic(project, stat, increments) } + .to change { statistics.reload.send(stat) || 0 } + .by(total_amount) + end + + it 'increases also storage size by that amount' do + expect { described_class.bulk_increment_statistic(project, stat, increments) } + .to change { statistics.reload.storage_size } + .by(total_amount) + end + + it 'schedules a namespace aggregation worker' do + expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + .with(statistics.project.namespace.id) + + described_class.bulk_increment_statistic(project, stat, increments) + end + + context 'when the project is pending delete' do + before do + project.update_attribute(:pending_delete, true) + end + + it 'does not change the statistics' do + expect { described_class.bulk_increment_statistic(project, stat, increments) } + .not_to change { statistics.reload.send(stat) } + end + end + end + + shared_examples 'a statistic that increases storage_size asynchronously' do + it 'stores the increment temporarily in Redis', :clean_gitlab_redis_shared_state do + described_class.bulk_increment_statistic(project, stat, increments) + + Gitlab::Redis::SharedState.with do |redis| + key = statistics.counter(stat).key + increment = redis.get(key) + expect(increment.to_i).to eq(total_amount) + end + end + + it 'schedules a worker to update the statistic and storage_size async', :sidekiq_inline do + expect(FlushCounterIncrementsWorker) + .to receive(:perform_in) + .with(Gitlab::Counters::BufferedCounter::WORKER_DELAY, described_class.name, statistics.id, stat) + .and_call_original + + expect { described_class.bulk_increment_statistic(project, stat, increments) } + .to change { statistics.reload.send(stat) }.by(total_amount) + .and change { statistics.reload.send(:storage_size) }.by(total_amount) + end + + context 'when the project is pending delete' do + before do + project.update_attribute(:pending_delete, true) + end + + it 'does not change the statistics' do + expect { described_class.bulk_increment_statistic(project, stat, increments) } + .not_to change { [statistics.reload.send(stat), statistics.reload.send(:storage_size)] } + end + end + end + + context 'when adjusting :build_artifacts_size' do + let(:stat) { :build_artifacts_size } + + it_behaves_like 'a statistic that increases storage_size asynchronously' + + context 'when :project_statistics_bulk_increment flag is disabled' do + before do + stub_feature_flags(project_statistics_bulk_increment: false) + end + + it 'calls increment_statistic on once with the sum of the increments' do + expect(statistics).to receive(:increment_statistic).with(stat, increments.sum).and_call_original + + described_class.bulk_increment_statistic(project, stat, increments) + end + + it_behaves_like 'a statistic that increases storage_size asynchronously' + end + end + + context 'when adjusting :pipeline_artifacts_size' do + let(:stat) { :pipeline_artifacts_size } + + it_behaves_like 'a statistic that increases storage_size synchronously' + end + + context 'when adjusting :packages_size' do + let(:stat) { :packages_size } + + it_behaves_like 'a statistic that increases storage_size asynchronously' + end + + context 'when using an invalid column' do + it 'raises an error' do + expect { described_class.bulk_increment_statistic(project, :id, increments) } + .to raise_error(ArgumentError, "Cannot increment attribute: id") + end + end + end end diff --git a/spec/requests/groups/usage_quotas_controller_spec.rb b/spec/requests/groups/usage_quotas_controller_spec.rb index 3772fc2ba3b..bddc95434ce 100644 --- a/spec/requests/groups/usage_quotas_controller_spec.rb +++ b/spec/requests/groups/usage_quotas_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Groups::UsageQuotasController, feature_category: :subscription_usage_reports do +RSpec.describe Groups::UsageQuotasController, feature_category: :subscription_cost_management do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:user) { create(:user) } diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb index 4f7663d7996..dd10c0df374 100644 --- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb @@ -87,12 +87,9 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s expect { subject }.to change { Ci::DeletedObject.count }.by(1) end - it 'resets project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact.project, :build_artifacts_size, -artifact.file.size) - .and_call_original - - subject + it 'resets project statistics', :sidekiq_inline do + expect { subject } + .to change { artifact.project.statistics.reload.build_artifacts_size }.by(-artifact.file.size) end it 'does not remove the files' do diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb index b1a4741851b..700e44ee703 100644 --- a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb @@ -3,23 +3,20 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do - let(:artifacts) { Ci::JobArtifact.all } - let(:service) { described_class.new(artifacts) } - - let_it_be(:artifact, refind: true) do - create(:ci_job_artifact) - end + let_it_be(:artifact_1, refind: true) { create(:ci_job_artifact, :zip) } + let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :zip) } + let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :zip, project: artifact_1.project) } - before do - artifact.file = fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') - artifact.save! - end + let(:artifacts) { Ci::JobArtifact.where(id: [artifact_1.id, artifact_2.id, artifact_3.id]) } + let(:service) { described_class.new(artifacts) } describe '#destroy_records' do it 'removes artifacts without updating statistics' do - expect(ProjectStatistics).not_to receive(:increment_statistic) + expect_next_instance_of(Ci::JobArtifacts::DestroyBatchService) do |service| + expect(service).to receive(:execute).with(update_stats: false).and_call_original + end - expect { service.destroy_records }.to change { Ci::JobArtifact.count } + expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-3) end context 'when there are no artifacts' do @@ -33,12 +30,15 @@ RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do describe '#update_statistics' do before do + stub_const("#{described_class}::BATCH_SIZE", 2) service.destroy_records end it 'updates project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact.project, :build_artifacts_size, -artifact.file.size) + expect(ProjectStatistics).to receive(:bulk_increment_statistic).once + .with(artifact_1.project, :build_artifacts_size, match_array([-artifact_1.size, -artifact_3.size])) + expect(ProjectStatistics).to receive(:bulk_increment_statistic).once + .with(artifact_2.project, :build_artifacts_size, match_array([-artifact_2.size])) service.update_statistics end diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 79920dcb2c7..aab45d21b83 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do create(:ci_job_artifact, :trace, :expired) end - describe '.execute' do + describe '#execute' do subject(:execute) { service.execute } it 'creates a deleted object for artifact with attached file' do @@ -207,44 +207,38 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do end end - context 'ProjectStatistics' do - it 'resets project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact_with_file.project, :build_artifacts_size, -artifact_with_file.file.size) - .and_call_original - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact_without_file.project, :build_artifacts_size, 0) - .and_call_original + context 'ProjectStatistics', :sidekiq_inline do + let(:artifact_with_file) { create(:ci_job_artifact, :zip) } + let(:artifact_with_file_2) { create(:ci_job_artifact, :zip, project: artifact_with_file.project) } + let(:artifact_without_file) { create(:ci_job_artifact) } + let(:affected_statistics) { artifact_with_file.project.statistics } + let(:unaffected_statistics) { artifact_without_file.project.statistics } + let!(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id, artifact_with_file_2.id]) } - execute + it 'updates project statistics by the relevant amount' do + expected_amount = -(artifact_with_file.size + artifact_with_file_2.size) + + expect { execute } + .to change { affected_statistics.reload.build_artifacts_size }.by(expected_amount) + .and change { unaffected_statistics.reload.build_artifacts_size }.by(0) end context 'with update_stats: false' do - let_it_be(:extra_artifact_with_file) do - create(:ci_job_artifact, :zip, project: artifact_with_file.project) - end - - let(:artifacts) do - Ci::JobArtifact.where(id: [artifact_with_file.id, extra_artifact_with_file.id, - artifact_without_file.id, trace_artifact.id]) - end + subject(:execute) { service.execute(update_stats: false) } it 'does not update project statistics' do - expect(ProjectStatistics).not_to receive(:increment_statistic) - - service.execute(update_stats: false) + expect { execute }.not_to change { [affected_statistics.reload.build_artifacts_size, unaffected_statistics.reload.build_artifacts_size] } end - it 'returns size statistics' do + it 'returns statistic updates per project' do expected_updates = { statistics_updates: { - artifact_with_file.project => -(artifact_with_file.file.size + extra_artifact_with_file.file.size), - artifact_without_file.project => 0 + artifact_with_file.project => match_array([-artifact_with_file.file.size, -artifact_with_file_2.file.size]), + artifact_without_file.project => [0] } } - expect(service.execute(update_stats: false)).to match( - a_hash_including(expected_updates)) + expect(execute).to match(a_hash_including(expected_updates)) end end end diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 8df73d9db90..47cbd6b5208 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -5,7 +5,7 @@ module Spec module Helpers module Features module InviteMembersModalHelper - def invite_member(names, role: 'Guest', expires_at: nil, refresh: true) + def invite_member(names, role: 'Guest', expires_at: nil) click_on 'Invite members' page.within invite_modal_selector do @@ -15,8 +15,6 @@ module Spec end wait_for_requests - - page.refresh if refresh end def invite_member_by_email(role) @@ -61,8 +59,6 @@ module Spec choose_options(role, expires_at) submit_invites - - page.refresh end def submit_invites diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb index 6aba9b16313..e589baf0909 100644 --- a/spec/support/prometheus/additional_metrics_shared_examples.rb +++ b/spec/support/prometheus/additional_metrics_shared_examples.rb @@ -100,7 +100,7 @@ RSpec.shared_examples 'additional metrics query' do } ] - expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result') expect(query_result).to eq(expected) end end @@ -128,7 +128,7 @@ RSpec.shared_examples 'additional metrics query' do queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] } - expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result') expect(query_result.count).to eq(2) expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) @@ -147,7 +147,7 @@ RSpec.shared_examples 'additional metrics query' do it 'return group data only for query with results' do queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } - expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result') expect(query_result.count).to eq(1) expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb index 277ec6a7fa7..2eca2a72997 100644 --- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb +++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb @@ -81,7 +81,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| invite_member(user2.name, role: 'Developer') - invite_member(user2.name, role: 'Reporter', refresh: false) + invite_member(user2.name, role: 'Reporter') expect(page).not_to have_selector(invite_modal_selector) @@ -101,7 +101,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| invite_member(email, role: 'Developer') - invite_member(email, role: 'Reporter', refresh: false) + invite_member(email, role: 'Reporter') expect(page).not_to have_selector(invite_modal_selector) @@ -127,7 +127,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| it 'adds the user as a member on sub-entity with higher access level', :js do visit subentity_members_page_path - invite_member(user2.name, role: role, refresh: false) + invite_member(user2.name, role: role) expect(page).not_to have_selector(invite_modal_selector) @@ -145,7 +145,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| it 'fails with an error', :js do visit subentity_members_page_path - invite_member(user2.name, role: role, refresh: false) + invite_member(user2.name, role: role) invite_modal = page.find(invite_modal_selector) expect(invite_modal).to have_content "#{user2.name}: Access level should be greater than or equal to " \ @@ -177,7 +177,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| visit subentity_members_page_path - invite_member([user2.name, user3.name, user4.name, user6.name, user7.name], role: role, refresh: false) + invite_member([user2.name, user3.name, user4.name, user6.name, user7.name], role: role) # we have more than 2 errors, so one will be hidden invite_modal = page.find(invite_modal_selector) @@ -266,7 +266,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| it 'only shows the error for an invalid formatted email and does not display other member errors', :js do visit subentity_members_page_path - invite_member([user2.name, user3.name, 'bad@email'], role: role, refresh: false) + invite_member([user2.name, user3.name, 'bad@email'], role: role) invite_modal = page.find(invite_modal_selector) expect(invite_modal).to have_text('email contains an invalid email address') diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb index a20bb794095..00dd1f0b244 100644 --- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -71,6 +71,51 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end end end + + describe '#bulk_increment_counter', :redis do + let(:increments) { [10, 5] } + let(:total_amount) { increments.sum } + let(:counter_key) { model.counter(attribute).key } + + subject { model.bulk_increment_counter(attribute, increments) } + + context 'when attribute is a counter attribute' do + it 'increments the counter in Redis and logs it' do + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Increment counter attribute', + attribute: attribute, + project_id: model.project_id, + increment: total_amount, + new_counter_value: 0 + total_amount, + current_db_value: model.read_attribute(attribute), + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) + ) + + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(counter_key) + expect(counter).to eq(total_amount.to_s) + end + end + + it 'does not increment the counter for the record' do + expect { subject }.not_to change { model.reset.read_attribute(attribute) } + end + + it 'schedules a worker to flush counter increments asynchronously' do + expect(FlushCounterIncrementsWorker).to receive(:perform_in) + .with(Gitlab::Counters::BufferedCounter::WORKER_DELAY, model.class.name, model.id, attribute) + .and_call_original + + subject + end + end + end end end diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb index eb742921d35..3d2d78344f0 100644 --- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb +++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb @@ -108,11 +108,8 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute| end context 'when it is destroyed from the project level' do - it 'does not update the project statistics' do - expect(ProjectStatistics) - .not_to receive(:increment_statistic) - - expect(Projects::DestroyService.new(project, project.first_owner).execute).to eq(true) + it 'does not store pending increments for async update' do + expect { Projects::DestroyService.new(project, project.first_owner).execute }.not_to change { read_pending_increment } end it 'does not schedule a namespace statistics worker' do diff --git a/spec/tooling/danger/user_types_spec.rb b/spec/tooling/danger/user_types_spec.rb index 4b87f649760..53556601212 100644 --- a/spec/tooling/danger/user_types_spec.rb +++ b/spec/tooling/danger/user_types_spec.rb @@ -4,7 +4,7 @@ require 'gitlab-dangerfiles' require 'gitlab/dangerfiles/spec_helper' require_relative '../../../tooling/danger/user_types' -RSpec.describe Tooling::Danger::UserTypes, feature_category: :subscription_usage_reports do +RSpec.describe Tooling::Danger::UserTypes, feature_category: :subscription_cost_management do include_context 'with dangerfile' let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } |