Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb2
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js33
-rw-r--r--spec/frontend/ci/runner/components/runner_managers_detail_spec.js27
-rw-r--r--spec/frontend/ci/runner/components/runner_managers_table_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js6
-rw-r--r--spec/graphql/mutations/achievements/delete_user_achievement_spec.rb55
-rw-r--r--spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/compressed_json_spec.rb24
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb158
-rw-r--r--spec/migrations/20230302811133_re_migrate_redis_slot_keys_spec.rb8
-rw-r--r--spec/migrations/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation_spec.rb19
-rw-r--r--spec/policies/group_policy_spec.rb8
-rw-r--r--spec/requests/api/admin/migrations_spec.rb89
-rw-r--r--spec/requests/api/error_tracking/collector_spec.rb233
-rw-r--r--spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb85
-rw-r--r--spec/services/achievements/destroy_user_achievement_service_spec.rb40
-rw-r--r--spec/services/database/mark_migration_service_spec.rb71
-rw-r--r--spec/services/personal_access_tokens/last_used_service_spec.rb52
-rw-r--r--spec/support/rspec_order_todo.yml1
19 files changed, 498 insertions, 447 deletions
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
index e15336f586e..54d0bfef9dd 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountFooMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountFooMetric, feature_category: :service_ping do
let(:expected_value) { 1 }
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
index f11667ee415..c452e32b0e4 100644
--- a/spec/frontend/ci/runner/components/runner_create_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -11,6 +11,7 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ I18N_CREATE_ERROR,
} from '~/ci/runner/constants';
import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -188,5 +189,37 @@ describe('RunnerCreateForm', () => {
expect(captureException).not.toHaveBeenCalled();
});
});
+
+ describe('when no runner information is returned', () => {
+ beforeEach(async () => {
+ runnerCreateHandler.mockResolvedValue({
+ data: {
+ runnerCreate: {
+ errors: [],
+ runner: null,
+ },
+ },
+ });
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" result', () => {
+ expect(wrapper.emitted('error')[0]).toEqual([new TypeError(I18N_CREATE_ERROR)]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('reports error', () => {
+ expect(captureException).toHaveBeenCalledTimes(1);
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerCreateForm',
+ error: new Error(I18N_CREATE_ERROR),
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/ci/runner/components/runner_managers_detail_spec.js b/spec/frontend/ci/runner/components/runner_managers_detail_spec.js
index b2551e67396..3435292394f 100644
--- a/spec/frontend/ci/runner/components/runner_managers_detail_spec.js
+++ b/spec/frontend/ci/runner/components/runner_managers_detail_spec.js
@@ -2,16 +2,12 @@ import { GlCollapse, GlSkeletonLoader, GlTableLite } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { __ } from '~/locale';
-import {
- shallowMountExtended,
- mountExtended,
- extendedWrapper,
-} from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RunnerManagersDetail from '~/ci/runner/components/runner_managers_detail.vue';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerManagersTable from '~/ci/runner/components/runner_managers_table.vue';
import runnerManagersQuery from '~/ci/runner/graphql/show/runner_managers.query.graphql';
import { runnerData, runnerManagersData } from '../mock_data';
@@ -33,8 +29,7 @@ describe('RunnerJobs', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findCollapse = () => wrapper.findComponent(GlCollapse);
- const findRows = () => wrapper.findAll('tbody tr');
- const findCell = ({ field, i }) => extendedWrapper(findRows().at(i)).findByTestId(`td-${field}`);
+ const findRunnerManagersTable = () => wrapper.findComponent(RunnerManagersTable);
const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerManagersDetail, {
@@ -162,21 +157,7 @@ describe('RunnerJobs', () => {
it('shows rows', () => {
expect(findCollapse().attributes('visible')).toBe('true');
- expect(findRows()).toHaveLength(mockRunnerManagers.length);
- });
-
- it('shows system id', () => {
- expect(findCell({ field: 'systemId', i: 0 }).text()).toBe(mockRunnerManagers[0].systemId);
- expect(findCell({ field: 'systemId', i: 1 }).text()).toBe(mockRunnerManagers[1].systemId);
- });
-
- it('shows contacted at', () => {
- expect(findCell({ field: 'contactedAt', i: 0 }).findComponent(TimeAgo).props('time')).toBe(
- mockRunnerManagers[0].contactedAt,
- );
- expect(findCell({ field: 'contactedAt', i: 1 }).findComponent(TimeAgo).props('time')).toBe(
- mockRunnerManagers[1].contactedAt,
- );
+ expect(findRunnerManagersTable().props('items')).toEqual(mockRunnerManagers);
});
it('collapses when clicked', async () => {
diff --git a/spec/frontend/ci/runner/components/runner_managers_table_spec.js b/spec/frontend/ci/runner/components/runner_managers_table_spec.js
index a0ebf3b2578..e72ea60cdbd 100644
--- a/spec/frontend/ci/runner/components/runner_managers_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_managers_table_spec.js
@@ -1,20 +1,17 @@
import { GlTableLite } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
import { s__ } from '~/locale';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import RunnerManagersTable from '~/ci/runner/components/runner_managers_table.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { I18N_STATUS_NEVER_CONTACTED } from '~/ci/runner/constants';
import { runnerManagersData } from '../mock_data';
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
-const [runnerManager1, runnerManager2] = runnerManagersData.data.runner.managers.nodes;
-
-Vue.use(VueApollo);
+const mockItems = runnerManagersData.data.runner.managers.nodes;
describe('RunnerJobs', () => {
let wrapper;
@@ -25,9 +22,11 @@ describe('RunnerJobs', () => {
const findCellText = (opts) => findCell(opts).text().replace(/\s+/g, ' ');
const createComponent = ({ item } = {}) => {
+ const [mockItem, ...otherItems] = mockItems;
+
wrapper = mountExtended(RunnerManagersTable, {
propsData: {
- items: [{ ...runnerManager1, ...item }, runnerManager2],
+ items: [{ ...mockItem, ...item }, ...otherItems],
},
stubs: {
GlTableLite,
@@ -54,8 +53,8 @@ describe('RunnerJobs', () => {
it('shows system id', () => {
createComponent();
- expect(findCellText({ field: 'systemId', i: 0 })).toBe(runnerManager1.systemId);
- expect(findCellText({ field: 'systemId', i: 1 })).toBe(runnerManager2.systemId);
+ expect(findCellText({ field: 'systemId', i: 0 })).toBe(mockItems[0].systemId);
+ expect(findCellText({ field: 'systemId', i: 1 })).toBe(mockItems[1].systemId);
});
it('shows version', () => {
@@ -74,6 +73,14 @@ describe('RunnerJobs', () => {
expect(findCellText({ field: 'version', i: 0 })).toBe('1.0 (123456)');
});
+ it('shows revision without version', () => {
+ createComponent({
+ item: { version: null, revision: '123456' },
+ });
+
+ expect(findCellText({ field: 'version', i: 0 })).toBe('(123456)');
+ });
+
it('shows ip address', () => {
createComponent({
item: { ipAddress: '127.0.0.1' },
@@ -117,7 +124,14 @@ describe('RunnerJobs', () => {
it('shows contacted at', () => {
createComponent();
expect(findCell({ field: 'contactedAt', i: 0 }).findComponent(TimeAgo).props('time')).toBe(
- runnerManager1.contactedAt,
+ mockItems[0].contactedAt,
);
});
+
+ it('shows missing contacted at', () => {
+ createComponent({
+ item: { contactedAt: null },
+ });
+ expect(findCellText({ field: 'contactedAt', i: 0 })).toBe(I18N_STATUS_NEVER_CONTACTED);
+ });
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
index c3e0818fc11..ca65d87f86c 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlButton } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue/';
import stubChildren from 'helpers/stub_children';
@@ -19,7 +19,7 @@ describe('Package Files', () => {
const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]');
const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon);
const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip);
- const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown);
+ const findFirstActionMenu = () => findFirstRow().findComponent(GlDisclosureDropdown);
const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]');
const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
const findFirstRowShaComponent = (id) => wrapper.find(`[data-testid="${id}"]`);
@@ -159,7 +159,7 @@ describe('Package Files', () => {
it('emits a delete event when clicked', () => {
createComponent();
- findActionMenuDelete().vm.$emit('click');
+ findActionMenuDelete().vm.$emit('action');
const [[{ id }]] = wrapper.emitted('delete-file');
expect(id).toBe(npmFiles[0].id);
diff --git a/spec/graphql/mutations/achievements/delete_user_achievement_spec.rb b/spec/graphql/mutations/achievements/delete_user_achievement_spec.rb
new file mode 100644
index 00000000000..d36b93bd3ea
--- /dev/null
+++ b/spec/graphql/mutations/achievements/delete_user_achievement_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::DeleteUserAchievement, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ describe '#resolve' do
+ subject(:resolve_mutation) do
+ described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
+ user_achievement_id: user_achievement&.to_global_id
+ )
+ end
+
+ before_all do
+ group.add_maintainer(maintainer)
+ group.add_owner(owner)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { maintainer }
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { owner }
+
+ context 'when the params are invalid' do
+ let(:user_achievement) { nil }
+
+ it 'returns the validation error' do
+ expect { resolve_mutation }.to raise_error { Gitlab::Graphql::Errors::ArgumentError }
+ end
+ end
+
+ it 'deletes user_achievement' do
+ resolve_mutation
+
+ expect(Achievements::UserAchievement.find_by(id: user_achievement.id)).to be_nil
+ end
+ end
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:destroy_user_achievement) }
+end
diff --git a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
index 0afd3201853..517ba4d7699 100644
--- a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
+++ b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
@@ -280,7 +280,7 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
describe 'Creating known event entry' do
let(:time_frames) { %w[7d 28d] }
- let(:expected_known_events) { [{ "name" => event, "aggregation" => "weekly" }] }
+ let(:expected_known_events) { [{ "name" => event }] }
it 'creates a metric definition file using the template' do
described_class.new([], options).invoke_all
diff --git a/spec/lib/gitlab/middleware/compressed_json_spec.rb b/spec/lib/gitlab/middleware/compressed_json_spec.rb
index 5978b2422e0..c0e54c89222 100644
--- a/spec/lib/gitlab/middleware/compressed_json_spec.rb
+++ b/spec/lib/gitlab/middleware/compressed_json_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Middleware::CompressedJson do
+RSpec.describe Gitlab::Middleware::CompressedJson, feature_category: :shared do
let_it_be(:decompressed_input) { '{"foo": "bar"}' }
let_it_be(:input) { ActiveSupport::Gzip.compress(decompressed_input) }
@@ -70,24 +70,6 @@ RSpec.describe Gitlab::Middleware::CompressedJson do
end
describe '#call' do
- context 'with collector route' do
- let(:path) { '/api/v4/error_tracking/collector/1/store' }
-
- it_behaves_like 'decompress middleware'
-
- context 'with no Content-Type' do
- let(:content_type) { nil }
-
- it_behaves_like 'decompress middleware'
- end
-
- include_context 'with relative url' do
- let(:path) { "#{relative_url_root}/api/v4/error_tracking/collector/1/store" }
-
- it_behaves_like 'decompress middleware'
- end
- end
-
context 'with packages route' do
context 'with instance level endpoint' do
context 'with npm advisory bulk url' do
@@ -192,11 +174,11 @@ RSpec.describe Gitlab::Middleware::CompressedJson do
it_behaves_like 'passes input'
end
- context 'payload is too large' do
+ context 'when payload is too large' do
let(:body_limit) { Gitlab::Middleware::CompressedJson::MAXIMUM_BODY_SIZE }
let(:decompressed_input) { 'a' * (body_limit + 100) }
let(:input) { ActiveSupport::Gzip.compress(decompressed_input) }
- let(:path) { '/api/v4/error_tracking/collector/1/envelope' }
+ let(:path) { '/api/v4/packages/npm/-/npm/v1/security/advisories/bulk' }
it 'reads only limited size' do
expect(middleware.call(env))
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index b962757c35b..50fb9f9df6e 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -23,91 +23,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
described_class.clear_memoization(:known_events)
end
- describe '.track_event' do
- # ToDo: remove during https://gitlab.com/groups/gitlab-org/-/epics/9542 cleanup
- describe 'daily to weekly key migration precautions' do
- let(:event_a_name) { 'example_event_a' }
- let(:event_b_name) { 'example_event_b' }
- let(:known_events) do
- [
- { name: event_a_name, aggregation: 'daily' },
- { name: event_b_name, aggregation: 'weekly' }
- ].map(&:with_indifferent_access)
- end
-
- let(:start_date) { (Date.current - 1.week).beginning_of_week }
- let(:end_date) { Date.current }
-
- let(:daily_event) { known_events.first }
- let(:daily_key) { described_class.send(:redis_key, daily_event, start_date) }
- let(:weekly_key) do
- weekly_event = known_events.first.merge(aggregation: 'weekly')
- described_class.send(:redis_key, weekly_event, start_date)
- end
-
- before do
- allow(described_class).to receive(:load_events).with(described_class::KNOWN_EVENTS_PATH).and_return(known_events)
- allow(described_class).to receive(:load_events).with(/ee/).and_return([])
- end
-
- shared_examples 'writes daily events to daily and weekly keys' do
- it :aggregate_failures do
- expect(Gitlab::Redis::HLL).to receive(:add).with(expiry: 29.days, key: daily_key, value: 1).and_call_original
- expect(Gitlab::Redis::HLL).to receive(:add).with(expiry: 6.weeks, key: weekly_key, value: 1).and_call_original
-
- described_class.track_event(event_a_name, values: 1, time: start_date)
- end
- end
-
- context 'when revert_daily_hll_events_to_weekly_aggregation FF is disabled' do
- before do
- stub_feature_flags(revert_daily_hll_events_to_weekly_aggregation: false)
- end
-
- it_behaves_like 'writes daily events to daily and weekly keys'
-
- it 'aggregates weekly for daily keys', :aggregate_failures do
- expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [weekly_key]).and_call_original
- expect(Gitlab::Redis::HLL).not_to receive(:count).with(keys: [daily_key]).and_call_original
-
- described_class.unique_events(event_names: [event_a_name], start_date: start_date, end_date: end_date)
- end
-
- it 'does not persists changes to event aggregation attribute' do
- described_class.unique_events(event_names: [event_a_name], start_date: start_date, end_date: end_date)
-
- expect(described_class.known_events.find { |e| e[:name] == event_a_name }[:aggregation])
- .to eql 'daily'
- end
- end
-
- context 'when revert_daily_hll_events_to_weekly_aggregation FF is enabled' do
- before do
- stub_feature_flags(revert_daily_hll_events_to_weekly_aggregation: true)
- end
-
- # we want to write events no matter of the feature state
- it_behaves_like 'writes daily events to daily and weekly keys'
-
- it 'aggregates daily for daily keys', :aggregate_failures do
- expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [daily_key]).and_call_original
- expect(Gitlab::Redis::HLL).not_to receive(:count).with(keys: [weekly_key]).and_call_original
-
- described_class.unique_events(event_names: [event_a_name], start_date: start_date, end_date: start_date)
- end
- end
- end
- end
-
describe '.known_events' do
let(:ce_temp_dir) { Dir.mktmpdir }
let(:ce_temp_file) { Tempfile.new(%w[common .yml], ce_temp_dir) }
- let(:ce_event) do
- {
- "name" => "ce_event",
- "aggregation" => "weekly"
- }
- end
+ let(:ce_event) { { "name" => "ce_event" } }
before do
stub_const("#{described_class}::KNOWN_EVENTS_PATH", File.expand_path('*.yml', ce_temp_dir))
@@ -144,13 +63,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: weekly_event, aggregation: "weekly" },
- { name: daily_event, aggregation: "daily" },
- { name: category_productivity_event, aggregation: "weekly" },
- { name: compliance_slot_event, aggregation: "weekly" },
- { name: no_slot, aggregation: "daily" },
- { name: different_aggregation, aggregation: "monthly" },
- { name: context_event, aggregation: 'weekly' }
+ { name: weekly_event },
+ { name: daily_event },
+ { name: category_productivity_event },
+ { name: compliance_slot_event },
+ { name: no_slot },
+ { name: different_aggregation },
+ { name: context_event }
].map(&:with_indifferent_access)
end
@@ -203,15 +122,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it 'tracks events with multiple values' do
values = [entity1, entity2]
expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, value: values,
- expiry: described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
+ expiry: described_class::KEY_EXPIRY_LENGTH)
described_class.track_event(:g_analytics_contribution, values: values)
end
- it "raise error if metrics don't have same aggregation" do
- expect { described_class.track_event(different_aggregation, values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation)
- end
-
it 'raise error if metrics of unknown event' do
expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
end
@@ -248,22 +163,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect(keys).not_to be_empty
keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
- end
- end
- end
- end
-
- context 'for daily events' do
- it 'sets the keys in Redis to expire' do
- described_class.track_event("no_slot", values: entity1)
-
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "*_no_slot").to_a
- expect(keys).not_to be_empty
-
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::DEFAULT_DAILY_KEY_EXPIRY_LENGTH)
+ expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::KEY_EXPIRY_LENGTH)
end
end
end
@@ -285,7 +185,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
values = [entity1, entity2]
expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/,
value: values,
- expiry: described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
+ expiry: described_class::KEY_EXPIRY_LENGTH)
described_class.track_event_in_context(:g_analytics_contribution, values: values, context: default_context)
end
@@ -347,12 +247,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect(described_class.unique_events(event_names: [weekly_event], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1)
end
- it "raise error if metrics don't have same aggregation" do
- expect do
- described_class.unique_events(event_names: [daily_event, weekly_event], start_date: 4.weeks.ago, end_date: Date.current)
- end.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::AggregationMismatch)
- end
-
context 'when data for the last complete week' do
it { expect(described_class.unique_events(event_names: [weekly_event], start_date: 1.week.ago, end_date: Date.current)).to eq(1) }
end
@@ -369,12 +263,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it { expect(described_class.unique_events(event_names: [weekly_event.to_sym], start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) }
end
- context 'when using daily aggregation' do
- it { expect(described_class.unique_events(event_names: [daily_event], start_date: 7.days.ago, end_date: Date.current)).to eq(2) }
- it { expect(described_class.unique_events(event_names: [daily_event], start_date: 28.days.ago, end_date: Date.current)).to eq(3) }
- it { expect(described_class.unique_events(event_names: [daily_event], start_date: 28.days.ago, end_date: 21.days.ago)).to eq(1) }
- end
-
context 'when no slot is set' do
it { expect(described_class.unique_events(event_names: [no_slot], start_date: 7.days.ago, end_date: Date.current)).to eq(1) }
end
@@ -388,7 +276,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- describe '.weekly_redis_keys' do
+ describe '.keys_for_aggregation' do
using RSpec::Parameterized::TableSyntax
let(:weekly_event) { 'i_search_total' }
@@ -398,7 +286,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:week_three) { "{#{described_class::REDIS_SLOT}}_i_search_total-2021-01" }
let(:week_four) { "{#{described_class::REDIS_SLOT}}_i_search_total-2021-02" }
- subject(:weekly_redis_keys) { described_class.send(:weekly_redis_keys, events: [redis_event], start_date: DateTime.parse(start_date), end_date: DateTime.parse(end_date)) }
+ subject(:keys_for_aggregation) { described_class.send(:keys_for_aggregation, events: [redis_event], start_date: DateTime.parse(start_date), end_date: DateTime.parse(end_date)) }
where(:start_date, :end_date, :keys) do
'2020-12-21' | '2020-12-21' | []
@@ -421,11 +309,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
it 'returns 1 key for last for week' do
- expect(described_class.send(:weekly_redis_keys, events: [redis_event], start_date: 7.days.ago.to_date, end_date: Date.current).size).to eq 1
+ expect(described_class.send(:keys_for_aggregation, events: [redis_event], start_date: 7.days.ago.to_date, end_date: Date.current).size).to eq 1
end
it 'returns 4 key for last for weeks' do
- expect(described_class.send(:weekly_redis_keys, events: [redis_event], start_date: 4.weeks.ago.to_date, end_date: Date.current).size).to eq 4
+ expect(described_class.send(:keys_for_aggregation, events: [redis_event], start_date: 4.weeks.ago.to_date, end_date: Date.current).size).to eq 4
end
end
@@ -434,9 +322,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: 'event_name_1', aggregation: "weekly" },
- { name: 'event_name_2', aggregation: "weekly" },
- { name: 'event_name_3', aggregation: "weekly" }
+ { name: 'event_name_1' },
+ { name: 'event_name_2' },
+ { name: 'event_name_3' }
].map(&:with_indifferent_access)
end
@@ -475,11 +363,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } }
let(:known_events) do
[
- { name: 'event1_slot', aggregation: "weekly" },
- { name: 'event2_slot', aggregation: "weekly" },
- { name: 'event3_slot', aggregation: "weekly" },
- { name: 'event5_slot', aggregation: "daily" },
- { name: 'event4', aggregation: "weekly" }
+ { name: 'event1_slot' },
+ { name: 'event2_slot' },
+ { name: 'event3_slot' },
+ { name: 'event5_slot' },
+ { name: 'event4' }
].map(&:with_indifferent_access)
end
diff --git a/spec/migrations/20230302811133_re_migrate_redis_slot_keys_spec.rb b/spec/migrations/20230302811133_re_migrate_redis_slot_keys_spec.rb
index d33a80b5e64..b4146761aa2 100644
--- a/spec/migrations/20230302811133_re_migrate_redis_slot_keys_spec.rb
+++ b/spec/migrations/20230302811133_re_migrate_redis_slot_keys_spec.rb
@@ -10,17 +10,13 @@ RSpec.describe ReMigrateRedisSlotKeys, :migration, feature_category: :service_pi
[
{
redis_slot: 'management',
- aggregation: 'daily',
name: 'g_project_management_epic_closed'
}, {
- aggregation: 'weekly',
- name: 'incident_management_incident_assigned'
+ name: 'incident_management_incident_assigned' # weekly event
},
{
- aggregation: 'weekly',
name: 'non_existing_event'
}, {
- aggregation: 'weekly',
name: 'event_without_expiry'
}
]
@@ -32,7 +28,7 @@ RSpec.describe ReMigrateRedisSlotKeys, :migration, feature_category: :service_pi
.and_return(known_events)
expiry_daily = 29.days
- expiry_weekly = Gitlab::UsageDataCounters::HLLRedisCounter::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH
+ expiry_weekly = described_class::KEY_EXPIRY_LENGTH
default_slot = Gitlab::UsageDataCounters::HLLRedisCounter::REDIS_SLOT
diff --git a/spec/migrations/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation_spec.rb b/spec/migrations/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation_spec.rb
index 21cdb6afa24..b5bf55f0d86 100644
--- a/spec/migrations/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation_spec.rb
+++ b/spec/migrations/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation_spec.rb
@@ -15,29 +15,29 @@ RSpec.describe MigrateDailyRedisHllEventsToWeeklyAggregation, :migration, :clean
context 'with daily aggregation' do
let(:date_formatted) { date.strftime('%G-%j') }
- let(:event) { { aggregation: 'daily', name: 'wiki_action' } }
+ let(:event) { { name: 'g_edit_by_web_ide' } }
it 'returns correct key' do
- existing_key = "#{date_formatted}-{hll_counters}_wiki_action"
+ existing_key = "#{date_formatted}-{hll_counters}_g_edit_by_web_ide"
- expect(described_class.new.redis_key(event, date, event[:aggregation])).to eq(existing_key)
+ expect(described_class.new.redis_key(event, date, :daily)).to eq(existing_key)
end
end
context 'with weekly aggregation' do
let(:date_formatted) { date.strftime('%G-%V') }
- let(:event) { { aggregation: 'weekly', name: 'weekly_action' } }
+ let(:event) { { name: 'weekly_action' } }
it 'returns correct key' do
existing_key = "{hll_counters}_weekly_action-#{date_formatted}"
- expect(described_class.new.redis_key(event, date, event[:aggregation])).to eq(existing_key)
+ expect(described_class.new.redis_key(event, date, :weekly)).to eq(existing_key)
end
end
end
context 'with weekly events' do
- let(:events) { [{ aggregation: 'weekly', name: 'weekly_action' }] }
+ let(:events) { [{ name: 'weekly_action' }] }
before do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_events).and_return(events)
@@ -55,13 +55,12 @@ RSpec.describe MigrateDailyRedisHllEventsToWeeklyAggregation, :migration, :clean
context 'with daily events' do
let(:daily_expiry) { 29.days }
- let(:weekly_expiry) { Gitlab::UsageDataCounters::HLLRedisCounter::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH }
+ let(:weekly_expiry) { Gitlab::UsageDataCounters::HLLRedisCounter::KEY_EXPIRY_LENGTH }
it 'migrates with correct parameters', :aggregate_failures do
- events = [{ aggregation: 'daily', name: 'g_project_management_epic_blocked_removed' }]
- allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_events).and_return(events)
+ event = { name: 'g_project_management_epic_blocked_removed' }
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_events).and_return([event])
- event = events.first.dup.tap { |e| e[:aggregation] = 'weekly' }
# For every day in the last 30 days, add a value to the daily key with daily expiry (including today)
31.times do |i|
key = described_class.new.send(:redis_key, event, Date.today - i.days, :weekly)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index bca38fa5638..fcde094939a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1762,6 +1762,7 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
specify { is_expected.to be_allowed(:read_achievement) }
specify { is_expected.to be_allowed(:admin_achievement) }
specify { is_expected.to be_allowed(:award_achievement) }
+ specify { is_expected.to be_allowed(:destroy_user_achievement) }
context 'with feature flag disabled' do
before do
@@ -1771,6 +1772,7 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
specify { is_expected.to be_disallowed(:read_achievement) }
specify { is_expected.to be_disallowed(:admin_achievement) }
specify { is_expected.to be_disallowed(:award_achievement) }
+ specify { is_expected.to be_disallowed(:destroy_user_achievement) }
end
context 'when current user can not see the group' do
@@ -1778,6 +1780,12 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
specify { is_expected.to be_allowed(:read_achievement) }
end
+
+ context 'when current user is not an owner' do
+ let(:current_user) { maintainer }
+
+ specify { is_expected.to be_disallowed(:destroy_user_achievement) }
+ end
end
describe 'admin_package ability' do
diff --git a/spec/requests/api/admin/migrations_spec.rb b/spec/requests/api/admin/migrations_spec.rb
new file mode 100644
index 00000000000..fc464300b56
--- /dev/null
+++ b/spec/requests/api/admin/migrations_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Admin::Migrations, feature_category: :database do
+ let(:admin) { create(:admin) }
+
+ describe 'POST /admin/migrations/:version/mark' do
+ let(:database) { :main }
+ let(:params) { { database: database } }
+ let(:connection) { ApplicationRecord.connection }
+ let(:path) { "/admin/migrations/#{version}/mark" }
+ let(:version) { 1 }
+
+ subject(:mark) do
+ post api(path, admin, admin_mode: true), params: params
+ end
+
+ context 'when the migration exists' do
+ before do
+ double = instance_double(
+ Database::MarkMigrationService,
+ execute: ServiceResponse.success)
+
+ allow(Database::MarkMigrationService)
+ .to receive(:new)
+ .with(connection: connection, version: version)
+ .and_return(double)
+ end
+
+ it_behaves_like "POST request permissions for admin mode"
+
+ it 'marks the migration as successful' do
+ mark
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'when the migration does not exist' do
+ let(:version) { 123 }
+
+ it 'returns 404' do
+ mark
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the migration was already executed' do
+ let(:version) { connection.migration_context.current_version }
+
+ it 'returns 422' do
+ mark
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ let(:ci_model) { Ci::ApplicationRecord }
+ let(:database) { :ci }
+
+ before do
+ skip_if_multiple_databases_not_setup(:ci)
+ end
+
+ it 'uses the correct connection' do
+ expect(Database::MarkMigrationService)
+ .to receive(:new)
+ .with(connection: ci_model.connection, version: version)
+ .and_call_original
+
+ mark
+ end
+
+ context 'when the database name does not exist' do
+ let(:database) { :wrong_database }
+
+ it 'returns bad request', :aggregate_failures do
+ mark
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('database does not have a valid value')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb
deleted file mode 100644
index 6a3e71bc859..00000000000
--- a/spec/requests/api/error_tracking/collector_spec.rb
+++ /dev/null
@@ -1,233 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::ErrorTracking::Collector, feature_category: :error_tracking do
- let_it_be(:project) { create(:project, :private) }
- let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) }
- let_it_be(:client_key) { create(:error_tracking_client_key, project: project) }
-
- RSpec.shared_examples 'not found' do
- it 'reponds with 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- RSpec.shared_examples 'bad request' do
- it 'responds with 400' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
-
- RSpec.shared_examples 'successful request' do
- it 'writes to the database and returns OK', :aggregate_failures do
- expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1)
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- describe "POST /error_tracking/collector/api/:id/envelope" do
- let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') }
- let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/envelope" }
-
- let(:params) { raw_event }
- let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}" } }
-
- subject { post api(url), params: params, headers: headers }
-
- it_behaves_like 'successful request'
-
- context 'intergrated error tracking feature flag is disabled' do
- before do
- stub_feature_flags(integrated_error_tracking: false)
- end
-
- it_behaves_like 'not found'
- end
-
- context 'error tracking feature is disabled' do
- before do
- setting.update!(enabled: false)
- end
-
- it_behaves_like 'not found'
- end
-
- context 'integrated error tracking is disabled' do
- before do
- setting.update!(integrated: false)
- end
-
- it_behaves_like 'not found'
- end
-
- context 'auth headers are missing' do
- let(:headers) { {} }
-
- it_behaves_like 'bad request'
- end
-
- context 'public key is wrong' do
- let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=glet_1fedb514e17f4b958435093deb02048c" } }
-
- it_behaves_like 'not found'
- end
-
- context 'public key is inactive' do
- let(:client_key) { create(:error_tracking_client_key, :disabled, project: project) }
-
- it_behaves_like 'not found'
- end
-
- context 'empty body' do
- let(:params) { '' }
-
- it_behaves_like 'bad request'
- end
-
- context 'unknown request type' do
- let(:params) { fixture_file('error_tracking/unknown.txt') }
-
- it_behaves_like 'bad request'
- end
-
- context 'transaction request type' do
- let(:params) { fixture_file('error_tracking/transaction.txt') }
-
- it 'does nothing and returns ok' do
- expect { subject }.not_to change { ErrorTracking::ErrorEvent.count }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'gzip body' do
- let(:standard_headers) do
- {
- 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}",
- 'HTTP_CONTENT_ENCODING' => 'gzip'
- }
- end
-
- let(:params) { ActiveSupport::Gzip.compress(raw_event) }
-
- context 'with application/x-sentry-envelope Content-Type' do
- let(:headers) { standard_headers.merge({ 'CONTENT_TYPE' => 'application/x-sentry-envelope' }) }
-
- it_behaves_like 'successful request'
- end
-
- context 'with unexpected Content-Type' do
- let(:headers) { standard_headers.merge({ 'CONTENT_TYPE' => 'application/gzip' }) }
-
- it 'responds with 415' do
- subject
-
- expect(response).to have_gitlab_http_status(:unsupported_media_type)
- end
- end
- end
- end
-
- describe "POST /error_tracking/collector/api/:id/store" do
- let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event.json') }
- let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/store" }
-
- let(:params) { raw_event }
- let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}" } }
-
- subject { post api(url), params: params, headers: headers }
-
- it_behaves_like 'successful request'
-
- context 'empty headers' do
- let(:headers) { {} }
-
- it_behaves_like 'bad request'
- end
-
- context 'empty body' do
- let(:params) { '' }
-
- it_behaves_like 'bad request'
- end
-
- context 'body with string instead of json' do
- let(:params) { '"********"' }
-
- it_behaves_like 'bad request'
- end
-
- context 'collector fails with validation error' do
- before do
- allow(::ErrorTracking::CollectErrorService)
- .to receive(:new).and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError)
- end
-
- it_behaves_like 'bad request'
- end
-
- context 'with platform field too long' do
- let(:params) do
- event = Gitlab::Json.parse(raw_event)
- event['platform'] = 'a' * 256
- Gitlab::Json.dump(event)
- end
-
- it_behaves_like 'bad request'
- end
-
- context 'gzip body' do
- let(:headers) do
- {
- 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}",
- 'HTTP_CONTENT_ENCODING' => 'gzip',
- 'CONTENT_TYPE' => 'application/json'
- }
- end
-
- let(:params) { ActiveSupport::Gzip.compress(raw_event) }
-
- it_behaves_like 'successful request'
- end
-
- context 'body contains nullbytes' do
- let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event_nullbytes.json') }
-
- it_behaves_like 'successful request'
- end
-
- context 'when JSON key transaction is empty string' do
- let_it_be(:raw_event) { fixture_file('error_tracking/php_empty_transaction.json') }
-
- it_behaves_like 'successful request'
- end
-
- context 'sentry_key as param and empty headers' do
- let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" }
- let(:headers) { {} }
-
- context 'key is wrong' do
- let(:sentry_key) { 'glet_1fedb514e17f4b958435093deb02048c' }
-
- it_behaves_like 'not found'
- end
-
- context 'key is empty' do
- let(:sentry_key) { '' }
-
- it_behaves_like 'bad request'
- end
-
- context 'key is correct' do
- let(:sentry_key) { client_key.public_key }
-
- it_behaves_like 'successful request'
- end
- end
- end
-end
diff --git a/spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb b/spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb
new file mode 100644
index 00000000000..f759e6dce08
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::DeleteUserAchievement, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ let(:mutation) { graphql_mutation(:user_achievements_delete, params) }
+ let(:user_achievement_id) { user_achievement&.to_global_id }
+ let(:params) { { user_achievement_id: user_achievement_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ before_all do
+ group.add_maintainer(maintainer)
+ group.add_owner(owner)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { maintainer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not delete any user achievements' do
+ expect { subject }.not_to change { Achievements::UserAchievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { owner }
+
+ context 'when the params are invalid' do
+ let(:user_achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for userAchievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the user_achievement_id is invalid' do
+ let(:user_achievement_id) { "gid://gitlab/Achievements::UserAchievement/#{non_existing_record_id}" }
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when everything is ok' do
+ it 'deletes an user achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.count }.by(-1)
+ end
+
+ it 'returns the deleted user achievement' do
+ subject
+
+ expect(graphql_data_at(:user_achievements_delete, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/services/achievements/destroy_user_achievement_service_spec.rb b/spec/services/achievements/destroy_user_achievement_service_spec.rb
new file mode 100644
index 00000000000..c5ff43fa1b2
--- /dev/null
+++ b/spec/services/achievements/destroy_user_achievement_service_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Achievements::DestroyUserAchievementService, feature_category: :user_profile do
+ describe '#execute' do
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ subject(:response) { described_class.new(current_user, user_achievement).execute }
+
+ before_all do
+ group.add_maintainer(maintainer)
+ group.add_owner(owner)
+ end
+
+ context 'when user does not have permission' do
+ let(:current_user) { maintainer }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ expect(response.message).to match_array(
+ ['You have insufficient permissions to delete this user achievement'])
+ end
+ end
+
+ context 'when user has permission' do
+ let(:current_user) { owner }
+
+ it 'deletes the achievement' do
+ expect(response).to be_success
+ expect(Achievements::UserAchievement.find_by(id: user_achievement.id)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/database/mark_migration_service_spec.rb b/spec/services/database/mark_migration_service_spec.rb
new file mode 100644
index 00000000000..5fd2268484e
--- /dev/null
+++ b/spec/services/database/mark_migration_service_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Database::MarkMigrationService, feature_category: :database do
+ let(:service) { described_class.new(connection: connection, version: version) }
+ let(:version) { 1 }
+ let(:connection) { ApplicationRecord.connection }
+
+ let(:migrations) do
+ [
+ instance_double(
+ ActiveRecord::MigrationProxy,
+ version: 1,
+ name: 'migration_pending',
+ filename: 'db/migrate/1_migration_pending.rb'
+ )
+ ]
+ end
+
+ before do
+ ctx = instance_double(ActiveRecord::MigrationContext, migrations: migrations)
+ allow(connection).to receive(:migration_context).and_return(ctx)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute }
+
+ it 'marks the migration as successful' do
+ expect { execute }
+ .to change { ActiveRecord::SchemaMigration.where(version: version).count }
+ .by(1)
+
+ is_expected.to be_success
+ end
+
+ context 'when the migration does not exist' do
+ let(:version) { 123 }
+
+ it { is_expected.to be_error }
+ it { expect(execute.reason).to eq(:not_found) }
+
+ it 'does not insert records' do
+ expect { execute }
+ .not_to change { ActiveRecord::SchemaMigration.where(version: version).count }
+ end
+ end
+
+ context 'when the migration was already executed' do
+ before do
+ allow(service).to receive(:all_versions).and_return([version])
+ end
+
+ it { is_expected.to be_error }
+ it { expect(execute.reason).to eq(:invalid) }
+
+ it 'does not insert records' do
+ expect { execute }
+ .not_to change { ActiveRecord::SchemaMigration.where(version: version).count }
+ end
+ end
+
+ context 'when the insert fails' do
+ it 'returns an error response' do
+ expect(service).to receive(:create_version).with(version).and_return(false)
+
+ is_expected.to be_error
+ end
+ end
+ end
+end
diff --git a/spec/services/personal_access_tokens/last_used_service_spec.rb b/spec/services/personal_access_tokens/last_used_service_spec.rb
index 20eabc20338..77ea5e10379 100644
--- a/spec/services/personal_access_tokens/last_used_service_spec.rb
+++ b/spec/services/personal_access_tokens/last_used_service_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe PersonalAccessTokens::LastUsedService, feature_category: :system_
describe '#execute' do
subject { described_class.new(personal_access_token).execute }
- context 'when the personal access token has not been used recently' do
- let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: 1.year.ago) }
+ context 'when the personal access token was used 10 minutes ago', :freeze_time do
+ let(:personal_access_token) { create(:personal_access_token, last_used_at: 10.minutes.ago) }
it 'updates the last_used_at timestamp' do
expect { subject }.to change { personal_access_token.last_used_at }
@@ -20,8 +20,8 @@ RSpec.describe PersonalAccessTokens::LastUsedService, feature_category: :system_
end
end
- context 'when the personal access token has been used recently' do
- let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: 1.minute.ago) }
+ context 'when the personal access token was used less than 10 minutes ago', :freeze_time do
+ let(:personal_access_token) { create(:personal_access_token, last_used_at: (10.minutes - 1.second).ago) }
it 'does not update the last_used_at timestamp' do
expect { subject }.not_to change { personal_access_token.last_used_at }
@@ -43,5 +43,49 @@ RSpec.describe PersonalAccessTokens::LastUsedService, feature_category: :system_
expect(subject).to be_nil
end
end
+
+ context 'when update_personal_access_token_usage_information_every_10_minutes is disabled' do
+ before do
+ stub_feature_flags(update_personal_access_token_usage_information_every_10_minutes: false)
+ end
+
+ context 'when the personal access token was used 1 day ago', :freeze_time do
+ let(:personal_access_token) { create(:personal_access_token, last_used_at: 1.day.ago) }
+
+ it 'updates the last_used_at timestamp' do
+ expect { subject }.to change { personal_access_token.last_used_at }
+ end
+
+ it 'does not run on read-only GitLab instances' do
+ allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ expect { subject }.not_to change { personal_access_token.last_used_at }
+ end
+ end
+
+ context 'when the personal access token was used less than 1 day ago', :freeze_time do
+ let(:personal_access_token) { create(:personal_access_token, last_used_at: (1.day - 1.second).ago) }
+
+ it 'does not update the last_used_at timestamp' do
+ expect { subject }.not_to change { personal_access_token.last_used_at }
+ end
+ end
+
+ context 'when the last_used_at timestamp is nil' do
+ let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: nil) }
+
+ it 'updates the last_used_at timestamp' do
+ expect { subject }.to change { personal_access_token.last_used_at }
+ end
+ end
+
+ context 'when not a personal access token' do
+ let_it_be(:personal_access_token) { create(:oauth_access_token) }
+
+ it 'does not execute' do
+ expect(subject).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 354d175d825..b3b6854481f 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -8228,7 +8228,6 @@
- './spec/requests/api/doorkeeper_access_spec.rb'
- './spec/requests/api/environments_spec.rb'
- './spec/requests/api/error_tracking/client_keys_spec.rb'
-- './spec/requests/api/error_tracking/collector_spec.rb'
- './spec/requests/api/events_spec.rb'
- './spec/requests/api/feature_flags_spec.rb'
- './spec/requests/api/feature_flags_user_lists_spec.rb'