summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 09:08:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 09:08:36 +0000
commit19e00b948726c0f7ca27dd92200493803499a4e1 (patch)
tree0df898db4ba20af4b4de2baf39285fe4d113d148 /spec
parentca5ebd2044ce696cc1aafc8a80a606e20f2c9e4b (diff)
downloadgitlab-ce-19e00b948726c0f7ca27dd92200493803499a4e1.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/features/groups/members/manage_members_spec.rb2
-rw-r--r--spec/features/projects/navbar_spec.rb2
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json131
-rw-r--r--spec/fixtures/api/schemas/conflicts.json130
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_poll_widget.json5
-rw-r--r--spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json64
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js46
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js47
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js60
-rw-r--r--spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js54
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js21
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js17
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js7
-rw-r--r--spec/lib/gitlab/counters/buffered_counter_spec.rb25
-rw-r--r--spec/lib/gitlab/counters/legacy_counter_spec.rb21
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb4
-rw-r--r--spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb3
-rw-r--r--spec/models/ci/job_artifact_spec.rb4
-rw-r--r--spec/models/project_spec.rb26
-rw-r--r--spec/models/project_statistics_spec.rb136
-rw-r--r--spec/requests/groups/usage_quotas_controller_spec.rb2
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb9
-rw-r--r--spec/services/ci/job_artifacts/destroy_associations_service_spec.rb28
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb46
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb6
-rw-r--r--spec/support/prometheus/additional_metrics_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/inviting_members_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb7
-rw-r--r--spec/tooling/danger/user_types_spec.rb2
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) }