diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /spec/support/shared_examples | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) | |
download | gitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/support/shared_examples')
51 files changed, 2094 insertions, 259 deletions
diff --git a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb index e4d59463d93..17087456720 100644 --- a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb +++ b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb @@ -27,12 +27,24 @@ RSpec.shared_examples 'instance statistics availability' do context 'for admins' do let(:user) { create(:admin) } - it 'allows access when the feature is not available publicly' do - stub_application_setting(instance_statistics_visibility_private: true) + context 'when admin mode disabled' do + it 'forbids access when the feature is not available publicly' do + stub_application_setting(instance_statistics_visibility_private: true) - get :index + get :index - expect(response).to have_gitlab_http_status(:success) + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when admin mode enabled', :enable_admin_mode do + it 'allows access when the feature is not available publicly' do + stub_application_setting(instance_statistics_visibility_private: true) + + get :index + + expect(response).to have_gitlab_http_status(:success) + end end end end diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb new file mode 100644 index 00000000000..60abb76acec --- /dev/null +++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'known sign in' do + def stub_remote_ip(ip) + request.remote_ip = ip + end + + def stub_user_ip(ip) + user.update!(current_sign_in_ip: ip) + end + + context 'with a valid post' do + context 'when remote IP does not match user last sign in IP' do + before do + stub_user_ip('127.0.0.1') + stub_remote_ip('169.0.0.1') + end + + it 'notifies the user' do + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:unknown_sign_in) + end + + post_action + end + end + + context 'when remote IP matches an active session' do + before do + existing_sessions = ActiveSession.session_ids_for_user(user.id) + existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) } + + stub_user_ip('169.0.0.1') + stub_remote_ip('127.0.0.1') + + ActiveSession.set(user, request) + end + + it 'does not notify the user' do + expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) + + post_action + end + end + + context 'when remote IP address matches last sign in IP' do + before do + stub_user_ip('127.0.0.1') + stub_remote_ip('127.0.0.1') + end + + it 'does not notify the user' do + expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) + + post_action + end + end + end +end diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb index 752bdc47851..9ff0bc3d217 100644 --- a/spec/support/shared_examples/controllers/variables_shared_examples.rb +++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb @@ -27,6 +27,9 @@ RSpec.shared_examples 'PATCH #update updates variables' do protected: 'false' } end + let(:variables_scope) { owner.variables } + let(:file_variables_scope) { owner.variables.file } + context 'with invalid new variable parameters' do let(:variables_attributes) do [ @@ -40,7 +43,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do end it 'does not create the new variable' do - expect { subject }.not_to change { owner.variables.count } + expect { subject }.not_to change { variables_scope.count } end it 'returns a bad request response' do @@ -63,7 +66,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do end it 'does not create the new variable' do - expect { subject }.not_to change { owner.variables.count } + expect { subject }.not_to change { variables_scope.count } end it 'returns a bad request response' do @@ -86,7 +89,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do end it 'creates the new variable' do - expect { subject }.to change { owner.variables.count }.by(1) + expect { subject }.to change { variables_scope.count }.by(1) end it 'returns a successful response' do @@ -106,7 +109,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] } it 'destroys the variable' do - expect { subject }.to change { owner.variables.count }.by(-1) + expect { subject }.to change { variables_scope.count }.by(-1) expect { variable.reload }.to raise_error ActiveRecord::RecordNotFound end @@ -123,6 +126,18 @@ RSpec.shared_examples 'PATCH #update updates variables' do end end + context 'with missing variable' do + let(:variables_attributes) do + [variable_attributes.merge(_destroy: 'true', id: 'some-id')] + end + + it 'returns not found response' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + context 'for variables of type file' do let(:variables_attributes) do [ @@ -131,7 +146,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do end it 'creates new variable of type file' do - expect { subject }.to change { owner.variables.file.count }.by(1) + expect { subject }.to change { file_variables_scope.count }.by(1) end end end diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb index 922d2627bce..1cd05b22ae9 100644 --- a/spec/support/shared_examples/features/error_tracking_shared_example.rb +++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true shared_examples 'error tracking index page' do - it 'renders the error index page' do + it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.js-title-container') do expect(page).to have_content(project.namespace.name) expect(page).to have_content(project.name) @@ -15,7 +15,7 @@ shared_examples 'error tracking index page' do end end - it 'loads the error show page on click' do + it 'loads the error show page on click', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do click_on issues_response[0]['title'] wait_for_requests @@ -23,7 +23,7 @@ shared_examples 'error tracking index page' do expect(page).to have_content('Error Details') end - it 'renders the error index data' do + it 'renders the error index data', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.error-list') do expect(page).to have_content(issues_response[0]['title']) expect(page).to have_content(issues_response[0]['count'].to_s) @@ -34,7 +34,7 @@ shared_examples 'error tracking index page' do end shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| - it 'expands the stack trace context' do + it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.stacktrace') do find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line @@ -49,7 +49,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_ end shared_examples 'error tracking show page' do - it 'renders the error details' do + it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do content = page.find(".content") nav = page.find("nav.breadcrumbs") header = page.find(".error-details-header") @@ -67,11 +67,11 @@ shared_examples 'error tracking show page' do expect(content).to have_content('Users: 0') end - it 'renders the stack trace heading' do + it 'renders the stack trace heading', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do expect(page).to have_content('Stack trace') end - it 'renders the stack trace' do + it 'renders the stack trace', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame| expect(frame['filename']).not_to be_nil expect(page).to have_content(frame['filename']) diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 4fd4d42003f..218ef070221 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -29,7 +29,6 @@ RSpec.shared_examples 'variable list' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') find('.js-ci-variable-input-value').set('key_value') - find('.ci-variable-protected-item .js-project-feature-toggle').click expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') end @@ -173,6 +172,7 @@ RSpec.shared_examples 'variable list' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('unprotected_key') find('.js-ci-variable-input-value').set('unprotected_value') + find('.ci-variable-protected-item .js-project-feature-toggle').click expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') end @@ -207,7 +207,6 @@ RSpec.shared_examples 'variable list' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('protected_key') find('.js-ci-variable-input-value').set('protected_value') - find('.ci-variable-protected-item .js-project-feature-toggle').click expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') end diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb new file mode 100644 index 00000000000..029d7e677da --- /dev/null +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# To use these shared examples, you may define a value in scope named +# `extra_design_fields`, to pass any extra fields in addition to the +# standard design fields. +RSpec.shared_examples 'a GraphQL type with design fields' do + let(:extra_design_fields) { [] } + + it { expect(described_class).to require_graphql_authorizations(:read_design) } + + it 'exposes the expected design fields' do + expected_fields = %i[ + id + project + issue + filename + full_path + image + image_v432x230 + diff_refs + event + notes_count + ] + extra_design_fields + + expect(described_class).to have_graphql_fields(*expected_fields).only + end + + describe '#image' do + let(:schema) { GitlabSchema } + let(:query) { GraphQL::Query.new(schema) } + let(:context) { double('Context', schema: schema, query: query, parent: nil) } + let(:field) { described_class.fields['image'] } + let(:args) { GraphQL::Query::Arguments::NO_ARGS } + let(:instance) do + object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id)) + object_type.authorized_new(object, query.context) + end + let(:instance_b) do + object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b)) + object_type.authorized_new(object_b, query.context) + end + + it 'resolves to the design image URL' do + image = field.resolve_field(instance, args, context) + sha = design.versions.first.sha + url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha) + + expect(image).to eq(url) + end + + it 'has better than O(N) peformance', :request_store do + # Assuming designs have been loaded (as they must be), the following + # queries are required: + # For each distinct version: + # - design_management_versions + # (Request store is needed so that each version is fetched only once.) + # For each distinct issue + # - issues + # For each distinct project + # - projects + # - routes + # - namespaces + # Here that total is: + # - 2 x issues + # - 2 x versions + # - 2 x (projects + routes + namespaces) + # = 10 + expect(instance).not_to eq(instance_b) # preload designs themselves. + expect do + image_a = field.resolve_field(instance, args, context) + image_b = field.resolve_field(instance, args, context) + image_c = field.resolve_field(instance_b, args, context) + image_d = field.resolve_field(instance_b, args, context) + expect(image_a).to eq(image_b) + expect(image_c).not_to eq(image_b) + expect(image_c).to eq(image_d) + end.not_to exceed_query_limit(10) + end + end +end diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb index 3d97fe10a47..2b96010477c 100644 --- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -shared_examples 'no jira import data present' do +shared_examples 'no Jira import data present' do it 'returns none' do expect(resolve_imports).to eq JiraImportState.none end end -shared_examples 'no jira import access' do +shared_examples 'no Jira import access' do it 'raises error' do expect do resolve_imports diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb new file mode 100644 index 00000000000..fb7e24eecf2 --- /dev/null +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +# Use this for testing how a GraphQL query handles sorting and pagination. +# This is particularly important when using keyset pagination connection, +# which is the default for ActiveRecord relations, as certain sort keys +# might not be supportable. +# +# sort_param: the value to specify the sort +# data_path: the keys necessary to dig into the return GraphQL data to get the +# returned results +# first_param: number of items expected (like a page size) +# expected_results: array of comparison data of all items sorted correctly +# pagination_query: method that specifies the GraphQL query +# pagination_results_data: method that extracts the sorted data used to compare against +# the expected results +# +# Example: +# describe 'sorting and pagination' do +# let(:sort_project) { create(:project, :public) } +# let(:data_path) { [:project, :issues] } +# +# def pagination_query(params, page_info) +# graphql_query_for( +# 'project', +# { 'fullPath' => sort_project.full_path }, +# "issues(#{params}) { #{page_info} edges { node { iid weight } } }" +# ) +# end +# +# def pagination_results_data(data) +# data.map { |issue| issue.dig('node', 'iid').to_i } +# end +# +# context 'when sorting by weight' do +# ... +# context 'when ascending' do +# it_behaves_like 'sorted paginated query' do +# let(:sort_param) { 'WEIGHT_ASC' } +# let(:first_param) { 2 } +# let(:expected_results) { [weight_issue3.iid, weight_issue5.iid, weight_issue1.iid, weight_issue4.iid, weight_issue2.iid] } +# end +# end +# +RSpec.shared_examples 'sorted paginated query' do + it_behaves_like 'requires variables' do + let(:required_variables) { [:sort_param, :first_param, :expected_results, :data_path, :current_user] } + end + + describe do + let(:params) { "sort: #{sort_param}" } + let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) } + let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) } + let(:sorted_edges) { graphql_data_at(*data_path, :edges) } + let(:page_info) { "pageInfo { startCursor endCursor }" } + + def pagination_query(params, page_info) + raise('pagination_query(params, page_info) must be defined in the test, see example in comment') unless defined?(super) + + super + end + + def pagination_results_data(data) + raise('pagination_results_data(data) must be defined in the test, see example in comment') unless defined?(super) + + super(data) + end + + before do + post_graphql(pagination_query(params, page_info), current_user: current_user) + end + + context 'when sorting' do + it 'sorts correctly' do + expect(pagination_results_data(sorted_edges)).to eq expected_results + end + + context 'when paginating' do + let(:params) { "sort: #{sort_param}, first: #{first_param}" } + + it 'paginates correctly' do + expect(pagination_results_data(sorted_edges)).to eq expected_results.first(first_param) + + cursored_query = pagination_query("sort: #{sort_param}, after: \"#{end_cursor}\"", page_info) + post_graphql(cursored_query, current_user: current_user) + response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) + + expect(pagination_results_data(response_data)).to eq expected_results.drop(first_param) + end + end + end + end +end diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb new file mode 100644 index 00000000000..f0624fbf29f --- /dev/null +++ b/spec/support/shared_examples/helm_commands_shared_examples.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +shared_examples 'helm command generator' do + describe '#generate_script' do + let(:helm_setup) do + <<~EOS + set -xeo pipefail + EOS + end + + it 'returns appropriate command' do + expect(subject.generate_script.strip).to eq((helm_setup + commands).strip) + end + end +end + +shared_examples 'helm command' do + describe '#rbac?' do + subject { command.rbac? } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_truthy } + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_falsey } + end + end + + describe '#pod_resource' do + subject { command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_an_instance_of ::Kubeclient::Resource } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_an_instance_of ::Kubeclient::Resource } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + + describe '#config_map_resource' do + subject { command.config_map_resource } + + let(:metadata) do + { + name: "values-content-configuration-#{command.name}", + namespace: 'gitlab-managed-apps', + labels: { name: "values-content-configuration-#{command.name}" } + } + end + + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: command.files) } + + it 'returns a KubeClient resource with config map content for the application' do + is_expected.to eq(resource) + end + end + + describe '#service_account_resource' do + let(:resource) do + Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) + end + + subject { command.service_account_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the tiller ServiceAccount' do + is_expected.to eq(resource) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end + + describe '#cluster_role_binding_resource' do + let(:resource) do + Kubeclient::Resource.new( + metadata: { name: 'tiller-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] + ) + end + + subject(:cluster_role_binding_resource) { command.cluster_role_binding_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do + is_expected.to eq(resource) + end + + it 'binds the account in #service_account_resource' do + expect(cluster_role_binding_resource.subjects.first.name).to eq(command.service_account_resource.metadata.name) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb index 851ed9c65a3..14292f70228 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb @@ -63,7 +63,7 @@ shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend exampl context 'provides the same results as the old implementation' do it 'for the median' do - expect(data_collector.median.seconds).to eq(ISSUES_MEDIAN) + expect(data_collector.median.seconds).to be_within(0.5).of(ISSUES_MEDIAN) end it 'for the list of event records' do diff --git a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb deleted file mode 100644 index bbf8a946f8b..00000000000 --- a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'helm commands' do - describe '#generate_script' do - let(:helm_setup) do - <<~EOS - set -xeo pipefail - EOS - end - - it 'returns appropriate command' do - expect(subject.generate_script.strip).to eq((helm_setup + commands).strip) - end - end -end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb new file mode 100644 index 00000000000..d6dc89a2c15 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rake task with disabled object_storage' do |service_class, method| + it 'disables direct & background upload only for service call' do + expect_next_instance_of(service_class) do |service| + expect(service).to receive(:execute).and_wrap_original do |m| + expect(Settings.uploads.object_store['enabled']).to eq(false) + + m.call + end + end + + expect(rake_task).to receive(method).and_wrap_original do |m, *args| + expect(Settings.uploads.object_store['enabled']).to eq(true) + expect(Settings.uploads.object_store).not_to receive(:[]=).with('enabled', false) + + m.call(*args) + end + + subject + end +end diff --git a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb index 8893ed5504b..72d672fd36c 100644 --- a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb @@ -13,10 +13,11 @@ end RSpec.shared_examples 'performs validation' do |validation_option| it 'performs validation' do expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) model.add_concurrent_foreign_key(*args, **options.merge(validation_option)) end diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb index 1cc1a1c8176..0a1c27b32db 100644 --- a/spec/support/shared_examples/models/chat_service_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb @@ -198,7 +198,7 @@ RSpec.shared_examples "chat service" do |service_name| message: "user created page: Awesome wiki_page" } end - let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) } + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) } let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") } it_behaves_like "triggered #{service_name} service" diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 37f1b33d455..c2fd04d648b 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -194,6 +194,66 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| end end + describe '#make_externally_installed' do + subject { create(application_name, :installing) } + + it 'is installed' do + subject.make_externally_installed + + expect(subject).to be_installed + end + + context 'application is updated' do + subject { create(application_name, :updated) } + + it 'is installed' do + subject.make_externally_installed + + expect(subject).to be_installed + end + end + + context 'application is errored' do + subject { create(application_name, :errored) } + + it 'is installed' do + subject.make_externally_installed + + expect(subject).to be_installed + end + end + end + + describe '#make_externally_uninstalled' do + subject { create(application_name, :installed) } + + it 'is uninstalled' do + subject.make_externally_uninstalled + + expect(subject).to be_uninstalled + end + + context 'application is updated' do + subject { create(application_name, :updated) } + + it 'is uninstalled' do + subject.make_externally_uninstalled + + expect(subject).to be_uninstalled + end + end + + context 'application is errored' do + subject { create(application_name, :errored) } + + it 'is uninstalled' do + subject.make_externally_uninstalled + + expect(subject).to be_uninstalled + end + end + end + describe '#make_scheduled' do subject { create(application_name, :installable) } @@ -278,6 +338,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| :update_errored | false :uninstalling | false :uninstall_errored | false + :uninstalled | false :timed_out | false end diff --git a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb index 8372ee9ac4a..76339837351 100644 --- a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb @@ -76,7 +76,7 @@ RSpec.shared_examples 'a blob replicator' do expect(service).to receive(:execute) expect(::Geo::BlobDownloadService).to receive(:new).with(replicator: replicator).and_return(service) - replicator.consume_created_event + replicator.consume_event_created end end @@ -89,7 +89,7 @@ RSpec.shared_examples 'a blob replicator' do end describe '#model' do - let(:invoke_model) { replicator.send(:model) } + let(:invoke_model) { replicator.class.model } it 'is implemented' do expect do diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb index 41de499f590..30c8c7d0fe5 100644 --- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb @@ -5,6 +5,7 @@ RSpec.shared_examples 'model with repository' do let(:stubbed_container) { raise NotImplementedError } let(:expected_full_path) { raise NotImplementedError } let(:expected_web_url_path) { expected_full_path } + let(:expected_repo_url_path) { expected_full_path } describe '#commits_by' do let(:commits) { container.repository.commits('HEAD', limit: 3).commits } @@ -53,19 +54,19 @@ RSpec.shared_examples 'model with repository' do describe '#url_to_repo' do it 'returns the SSH URL to the repository' do - expect(container.url_to_repo).to eq("#{Gitlab.config.gitlab_shell.ssh_path_prefix}#{expected_web_url_path}.git") + expect(container.url_to_repo).to eq(container.ssh_url_to_repo) end end describe '#ssh_url_to_repo' do it 'returns the SSH URL to the repository' do - expect(container.ssh_url_to_repo).to eq(container.url_to_repo) + expect(container.ssh_url_to_repo).to eq("#{Gitlab.config.gitlab_shell.ssh_path_prefix}#{expected_repo_url_path}.git") end end describe '#http_url_to_repo' do it 'returns the HTTP URL to the repository' do - expect(container.http_url_to_repo).to eq("#{Gitlab.config.gitlab.url}/#{expected_web_url_path}.git") + expect(container.http_url_to_repo).to eq("#{Gitlab.config.gitlab.url}/#{expected_repo_url_path}.git") end end @@ -95,12 +96,8 @@ RSpec.shared_examples 'model with repository' do end context 'when the repo exists' do - it { expect(container.empty_repo?).to be(false) } - - it 'returns true when repository is empty' do - allow(container.repository).to receive(:empty?).and_return(true) - - expect(container.empty_repo?).to be(true) + it 'returns the empty state of the repository' do + expect(container.empty_repo?).to be(container.repository.empty?) end end end @@ -146,15 +143,14 @@ RSpec.shared_examples 'model with repository' do end it 'picks storage from ApplicationSetting' do - expect_next_instance_of(ApplicationSetting) do |instance| - expect(instance).to receive(:pick_repository_storage).and_return('picked') - end + expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked') expect(subject).to eq('picked') end it 'picks from the latest available storage', :request_store do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + Gitlab::CurrentSettings.expire_current_application_settings Gitlab::CurrentSettings.current_application_settings settings = ApplicationSetting.last diff --git a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb new file mode 100644 index 00000000000..0357b7462fb --- /dev/null +++ b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'model with wiki' do + describe '#create_wiki' do + it 'returns true if the wiki repository already exists' do + expect(container.wiki_repository_exists?).to be(true) + expect(container.create_wiki).to be(true) + end + + it 'returns true if the wiki repository was created' do + expect(container_without_wiki.wiki_repository_exists?).to be(false) + expect(container_without_wiki.create_wiki).to be(true) + expect(container_without_wiki.wiki_repository_exists?).to be(true) + end + + context 'when the repository cannot be created' do + before do + expect(container.wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError } + end + + it 'returns false and adds a validation error' do + expect(container.create_wiki).to be(false) + expect(container.errors[:base]).to contain_exactly('Failed to create wiki') + end + end + end + + describe '#wiki_repository_exists?' do + it 'returns true when the wiki repository exists' do + expect(container.wiki_repository_exists?).to eq(true) + end + + it 'returns false when the wiki repository does not exist' do + expect(container_without_wiki.wiki_repository_exists?).to eq(false) + end + end + + describe 'wiki path conflict' do + context 'when the new path has been used by the wiki of other Project' do + it 'has an error on the name attribute' do + create(:project, namespace: container.parent, path: 'existing') + container.path = 'existing.wiki' + + expect(container).not_to be_valid + expect(container.errors[:name].first).to eq(_('has already been taken')) + end + end + + context 'when the new wiki path has been used by the path of other Project' do + it 'has an error on the name attribute' do + create(:project, namespace: container.parent, path: 'existing.wiki') + container.path = 'existing' + + expect(container).not_to be_valid + expect(container.errors[:name].first).to eq(_('has already been taken')) + end + end + + context 'when the new path has been used by the wiki of other Group' do + it 'has an error on the name attribute' do + create(:group, parent: container.parent, path: 'existing') + container.path = 'existing.wiki' + + expect(container).not_to be_valid + expect(container.errors[:name].first).to eq(_('has already been taken')) + end + end + + context 'when the new wiki path has been used by the path of other Group' do + it 'has an error on the name attribute' do + create(:group, parent: container.parent, path: 'existing.wiki') + container.path = 'existing' + + expect(container).not_to be_valid + expect(container.errors[:name].first).to eq(_('has already been taken')) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb new file mode 100644 index 00000000000..4bcea36fd42 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'includes Limitable concern' do + describe 'validations' do + let(:plan_limits) { create(:plan_limits, :default_plan) } + + it { is_expected.to be_a(Limitable) } + + context 'without plan limits configured' do + it 'can create new models' do + expect { subject.save }.to change { described_class.count } + end + end + + context 'with plan limits configured' do + before do + plan_limits.update(subject.class.limit_name => 1) + end + + it 'can create new models' do + expect { subject.save }.to change { described_class.count } + end + + context 'with an existing model' do + before do + subject.dup.save + end + + it 'cannot create new models exceding the plan limits' do + expect { subject.save }.not_to change { described_class.count } + expect(subject.errors[:base]).to contain_exactly("Maximum number of #{subject.class.limit_name.humanize(capitalize: false)} (1) exceeded") + end + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb new file mode 100644 index 00000000000..32d502af5a2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a timebox' do |timebox_type| + let(:project) { create(:project, :public) } + let(:group) { create(:group) } + let(:timebox) { create(timebox_type, project: project) } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } + + describe 'modules' do + context 'with a project' do + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(timebox_type, project: build(:project), group: nil) } + let(:scope) { :project } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { timebox_table_name } + end + end + + context 'with a group' do + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(timebox_type, project: nil, group: build(:group)) } + let(:scope) { :group } + let(:scope_attrs) { { namespace: instance.group } } + let(:usage) { timebox_table_name } + end + end + end + + describe "Validation" do + before do + allow(subject).to receive(:set_iid).and_return(false) + end + + describe 'start_date' do + it 'adds an error when start_date is greater then due_date' do + timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday) + + expect(timebox).not_to be_valid + expect(timebox.errors[:due_date]).to include("must be greater than start date") + end + + it 'adds an error when start_date is greater than 9999-12-31' do + timebox = build(timebox_type, start_date: Date.new(10000, 1, 1)) + + expect(timebox).not_to be_valid + expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31") + end + end + + describe 'due_date' do + it 'adds an error when due_date is greater than 9999-12-31' do + timebox = build(timebox_type, due_date: Date.new(10000, 1, 1)) + + expect(timebox).not_to be_valid + expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31") + end + end + + describe 'title' do + it { is_expected.to validate_presence_of(:title) } + + it 'is invalid if title would be empty after sanitation' do + timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>') + + expect(timebox).not_to be_valid + expect(timebox.errors[:title]).to include("can't be blank") + end + end + + describe '#timebox_type_check' do + it 'is invalid if it has both project_id and group_id' do + timebox = build(timebox_type, group: group) + timebox.project = project + + expect(timebox).not_to be_valid + expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.") + end + end + + describe "#uniqueness_of_title" do + context "per project" do + it "does not accept the same title in a project twice" do + new_timebox = timebox.dup + expect(new_timebox).not_to be_valid + end + + it "accepts the same title in another project" do + project = create(:project) + new_timebox = timebox.dup + new_timebox.project = project + + expect(new_timebox).to be_valid + end + end + + context "per group" do + let(:timebox) { create(timebox_type, group: group) } + + before do + project.update(group: group) + end + + it "does not accept the same title in a group twice" do + new_timebox = described_class.new(group: group, title: timebox.title) + + expect(new_timebox).not_to be_valid + end + + it "does not accept the same title of a child project timebox" do + create(timebox_type, project: group.projects.first) + + new_timebox = described_class.new(group: group, title: timebox.title) + + expect(new_timebox).not_to be_valid + end + end + end + end + + describe "Associations" do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:group) } + it { is_expected.to have_many(:issues) } + it { is_expected.to have_many(:merge_requests) } + it { is_expected.to have_many(:labels) } + end + + describe '#timebox_name' do + it 'returns the name of the model' do + expect(timebox.timebox_name).to eq(timebox_type.to_s) + end + end + + describe '#project_timebox?' do + context 'when project_id is present' do + it 'returns true' do + expect(timebox.project_timebox?).to be_truthy + end + end + + context 'when project_id is not present' do + let(:timebox) { build(timebox_type, group: group) } + + it 'returns false' do + expect(timebox.project_timebox?).to be_falsey + end + end + end + + describe '#group_timebox?' do + context 'when group_id is present' do + let(:timebox) { build(timebox_type, group: group) } + + it 'returns true' do + expect(timebox.group_timebox?).to be_truthy + end + end + + context 'when group_id is not present' do + it 'returns false' do + expect(timebox.group_timebox?).to be_falsey + end + end + end + + describe '#safe_title' do + let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") } + + it 'normalizes the title for use as a slug' do + expect(timebox.safe_title).to eq('foo-bar-22') + end + end + + describe '#resource_parent' do + context 'when group is present' do + let(:timebox) { build(timebox_type, group: group) } + + it 'returns the group' do + expect(timebox.resource_parent).to eq(group) + end + end + + context 'when project is present' do + it 'returns the project' do + expect(timebox.resource_parent).to eq(project) + end + end + end + + describe "#title" do + let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") } + + it "sanitizes title" do + expect(timebox.title).to eq("foo & bar -> 2.2") + end + end + + describe '#merge_requests_enabled?' do + context "per project" do + it "is true for projects with MRs enabled" do + project = create(:project, :merge_requests_enabled) + timebox = create(timebox_type, project: project) + + expect(timebox.merge_requests_enabled?).to be_truthy + end + + it "is false for projects with MRs disabled" do + project = create(:project, :repository_enabled, :merge_requests_disabled) + timebox = create(timebox_type, project: project) + + expect(timebox.merge_requests_enabled?).to be_falsey + end + + it "is false for projects with repository disabled" do + project = create(:project, :repository_disabled) + timebox = create(timebox_type, project: project) + + expect(timebox.merge_requests_enabled?).to be_falsey + end + end + + context "per group" do + let(:timebox) { create(timebox_type, group: group) } + + it "is always true for groups, for performance reasons" do + expect(timebox.merge_requests_enabled?).to be_truthy + end + end + end + + describe '#to_ability_name' do + it 'returns timebox' do + timebox = build(timebox_type) + + expect(timebox.to_ability_name).to eq(timebox_type.to_s) + end + end +end diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb index aa8979603b6..b0cdc77a378 100644 --- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb +++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb @@ -49,5 +49,29 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit| expect(subject.errors).to have_key(:commit_id) end end + + %i(original_position position change_position).each do |method| + describe "#{method}=" do + it "doesn't accept non-hash JSON passed as a string" do + subject.send(:"#{method}=", "true") + expect(subject.attributes_before_type_cast[method.to_s]).to be(nil) + end + + it "does accept a position hash as a string" do + subject.send(:"#{method}=", position.to_json) + expect(subject.position).to eq(position) + end + + it "doesn't accept an array" do + subject.send(:"#{method}=", ["test"]) + expect(subject.attributes_before_type_cast[method.to_s]).to be(nil) + end + + it "does accept a hash" do + subject.send(:"#{method}=", position.to_h) + expect(subject.position).to eq(position) + end + end + end end end diff --git a/spec/support/shared_examples/models/email_format_shared_examples.rb b/spec/support/shared_examples/models/email_format_shared_examples.rb index 6797836e383..a8115e440a4 100644 --- a/spec/support/shared_examples/models/email_format_shared_examples.rb +++ b/spec/support/shared_examples/models/email_format_shared_examples.rb @@ -44,3 +44,44 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes end end end + +RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes| + attributes.each do |attribute| + describe "specifically its :#{attribute} attribute" do + %w[ + info@example.com + info+test@example.com + o'reilly@example.com + ].each do |valid_email| + context "with a value of '#{valid_email}'" do + let(:email_value) { valid_email } + + it 'is valid' do + subject.send("#{attribute}=", valid_email) + + expect(subject).to be_valid + end + end + end + + %w[ + foobar + test@test@example.com + test.test.@example.com + .test.test@example.com + mailto:test@example.com + lol!'+=?><#$%^&*()@gmail.com + ].each do |invalid_email| + context "with a value of '#{invalid_email}'" do + let(:email_value) { invalid_email } + + it 'is invalid' do + subject.send("#{attribute}=", invalid_email) + + expect(subject).to be_invalid + end + end + end + end + end +end diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb index ecf1640ef5d..21ab9b06c33 100644 --- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb +++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb @@ -7,6 +7,7 @@ RSpec.shared_examples 'issuable hook data' do |kind| include_examples 'project hook data' do let(:project) { builder.issuable.project } end + include_examples 'deprecated repository hook data' context "with a #{kind}" do diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb index ba6aa4e1d89..d3e9035393f 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -210,6 +210,10 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type| it 'stores no mentions' do expect(mentionable.user_mentions.count).to eq 0 end + + it 'renders description_html correctly' do + expect(mentionable.description_html).to include("<a href=\"/#{user.username}\" data-user=\"#{user.id}\"") + end end end diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb index 24ff57c8517..a5228c43f6f 100644 --- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb @@ -112,7 +112,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| message: "user created page: Awesome wiki_page" } - @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts) + @wiki_page = create(:wiki_page, wiki: project.wiki, **opts) @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create') end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb new file mode 100644 index 00000000000..84569e95e11 --- /dev/null +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -0,0 +1,423 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki model' do + let_it_be(:user) { create(:user, :commit_email) } + let(:wiki_container) { raise NotImplementedError } + let(:wiki_container_without_repo) { raise NotImplementedError } + let(:wiki) { described_class.new(wiki_container, user) } + let(:commit) { subject.repository.head_commit } + + subject { wiki } + + it_behaves_like 'model with repository' do + let(:container) { wiki } + let(:stubbed_container) { described_class.new(wiki_container_without_repo, user) } + let(:expected_full_path) { "#{container.container.full_path}.wiki" } + let(:expected_web_url_path) { "#{container.container.web_url(only_path: true).sub(%r{^/}, '')}/-/wikis/home" } + end + + describe '#repository' do + it 'returns a wiki repository' do + expect(subject.repository.repo_type).to be_wiki + end + end + + describe '#full_path' do + it 'returns the container path with the .wiki extension' do + expect(subject.full_path).to eq(wiki_container.full_path + '.wiki') + end + end + + describe '#wiki_base_path' do + it 'returns the wiki base path' do + expect(subject.wiki_base_path).to eq("#{wiki_container.web_url(only_path: true)}/-/wikis") + end + end + + describe '#wiki' do + it 'contains a Gitlab::Git::Wiki instance' do + expect(subject.wiki).to be_a Gitlab::Git::Wiki + end + + it 'creates a new wiki repo if one does not yet exist' do + expect(subject.create_page('index', 'test content')).to be_truthy + end + + it 'creates a new wiki repo with a default commit message' do + expect(subject.create_page('index', 'test content', :markdown, '')).to be_truthy + + page = subject.find_page('index') + + expect(page.last_version.message).to eq("#{user.username} created page: index") + end + + context 'when the repository cannot be created' do + let(:wiki_container) { wiki_container_without_repo } + + before do + expect(subject.repository).to receive(:create_if_not_exists) { false } + end + + it 'raises CouldNotCreateWikiError' do + expect { subject.wiki }.to raise_exception(Wiki::CouldNotCreateWikiError) + end + end + end + + describe '#empty?' do + context 'when the wiki repository is empty' do + it 'returns true' do + expect(subject.empty?).to be(true) + end + end + + context 'when the wiki has pages' do + before do + subject.create_page('index', 'This is an awesome new Gollum Wiki') + subject.create_page('another-page', 'This is another page') + end + + describe '#empty?' do + it 'returns false' do + expect(subject.empty?).to be(false) + end + + it 'only instantiates a Wiki page once' do + expect(WikiPage).to receive(:new).once.and_call_original + + subject.empty? + end + end + end + end + + describe '#list_pages' do + let(:wiki_pages) { subject.list_pages } + + before do + subject.create_page('index', 'This is an index') + subject.create_page('index2', 'This is an index2') + subject.create_page('an index3', 'This is an index3') + end + + it 'returns an array of WikiPage instances' do + expect(wiki_pages).to be_present + expect(wiki_pages).to all(be_a(WikiPage)) + end + + it 'does not load WikiPage content by default' do + wiki_pages.each do |page| + expect(page.content).to be_empty + end + end + + it 'returns all pages by default' do + expect(wiki_pages.count).to eq(3) + end + + context 'with limit option' do + it 'returns limited set of pages' do + expect(subject.list_pages(limit: 1).count).to eq(1) + end + end + + context 'with sorting options' do + it 'returns pages sorted by title by default' do + pages = ['an index3', 'index', 'index2'] + + expect(subject.list_pages.map(&:title)).to eq(pages) + expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse) + end + + it 'returns pages sorted by created_at' do + pages = ['index', 'index2', 'an index3'] + + expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages) + expect(subject.list_pages(sort: 'created_at', direction: 'desc').map(&:title)).to eq(pages.reverse) + end + end + + context 'with load_content option' do + let(:pages) { subject.list_pages(load_content: true) } + + it 'loads WikiPage content' do + expect(pages.first.content).to eq('This is an index3') + expect(pages.second.content).to eq('This is an index') + expect(pages.third.content).to eq('This is an index2') + end + end + end + + describe '#sidebar_entries' do + before do + (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") } + (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") } + (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") } + end + + def total_pages(entries) + entries.sum do |entry| + entry.is_a?(WikiDirectory) ? entry.pages.size : 1 + end + end + + context 'when the number of pages does not exceed the limit' do + it 'returns all pages grouped by directory and limited is false' do + entries, limited = subject.sidebar_entries + + expect(entries.size).to be(7) + expect(total_pages(entries)).to be(15) + expect(limited).to be(false) + end + end + + context 'when the number of pages exceeds the limit' do + before do + create(:wiki_page, wiki: wiki, title: 'my page 16') + end + + it 'returns 15 pages grouped by directory and limited is true' do + entries, limited = subject.sidebar_entries + + expect(entries.size).to be(8) + expect(total_pages(entries)).to be(15) + expect(limited).to be(true) + end + end + end + + describe '#find_page' do + before do + subject.create_page('index page', 'This is an awesome Gollum Wiki') + end + + it 'returns the latest version of the page if it exists' do + page = subject.find_page('index page') + + expect(page.title).to eq('index page') + end + + it 'returns nil if the page does not exist' do + expect(subject.find_page('non-existent')).to eq(nil) + end + + it 'can find a page by slug' do + page = subject.find_page('index-page') + + expect(page.title).to eq('index page') + end + + it 'returns a WikiPage instance' do + page = subject.find_page('index page') + + expect(page).to be_a WikiPage + end + + context 'pages with multibyte-character title' do + before do + subject.create_page('autre pagé', "C'est un génial Gollum Wiki") + end + + it 'can find a page by slug' do + page = subject.find_page('autre pagé') + + expect(page.title).to eq('autre pagé') + end + end + + context 'pages with invalidly-encoded content' do + before do + subject.create_page('encoding is fun', "f\xFCr".b) + end + + it 'can find the page' do + page = subject.find_page('encoding is fun') + + expect(page.content).to eq('fr') + end + end + end + + describe '#find_sidebar' do + before do + subject.create_page(described_class::SIDEBAR, 'This is an awesome Sidebar') + end + + it 'finds the page defined as _sidebar' do + page = subject.find_sidebar + + expect(page.content).to eq('This is an awesome Sidebar') + end + end + + describe '#find_file' do + let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) } + + before do + subject.wiki # Make sure the wiki repo exists + + subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image') + end + + it 'returns the latest version of the file if it exists' do + file = subject.find_file('image.png') + + expect(file.mime_type).to eq('image/png') + end + + it 'returns nil if the page does not exist' do + expect(subject.find_file('non-existent')).to eq(nil) + end + + it 'returns a Gitlab::Git::WikiFile instance' do + file = subject.find_file('image.png') + + expect(file).to be_a Gitlab::Git::WikiFile + end + + it 'returns the whole file' do + file = subject.find_file('image.png') + image.rewind + + expect(file.raw_data.b).to eq(image.read.b) + end + end + + describe '#create_page' do + it 'creates a new wiki page' do + expect(subject.create_page('test page', 'this is content')).not_to eq(false) + expect(subject.list_pages.count).to eq(1) + end + + it 'returns false when a duplicate page exists' do + subject.create_page('test page', 'content') + + expect(subject.create_page('test page', 'content')).to eq(false) + end + + it 'stores an error message when a duplicate page exists' do + 2.times { subject.create_page('test page', 'content') } + + expect(subject.error_message).to match(/Duplicate page:/) + end + + it 'sets the correct commit message' do + subject.create_page('test page', 'some content', :markdown, 'commit message') + + expect(subject.list_pages.first.page.version.message).to eq('commit message') + end + + it 'sets the correct commit email' do + subject.create_page('test page', 'content') + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + + it 'updates container activity' do + expect(subject).to receive(:update_container_activity) + + subject.create_page('Test Page', 'This is content') + end + end + + describe '#update_page' do + let(:page) { create(:wiki_page, wiki: subject, title: 'update-page') } + + def update_page + subject.update_page( + page.page, + content: 'some other content', + format: :markdown, + message: 'updated page' + ) + end + + it 'updates the content of the page' do + update_page + page = subject.find_page('update-page') + + expect(page.raw_content).to eq('some other content') + end + + it 'sets the correct commit message' do + update_page + page = subject.find_page('update-page') + + expect(page.version.message).to eq('updated page') + end + + it 'sets the correct commit email' do + update_page + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + + it 'updates container activity' do + page + + expect(subject).to receive(:update_container_activity) + + update_page + end + end + + describe '#delete_page' do + let(:page) { create(:wiki_page, wiki: wiki) } + + it 'deletes the page' do + subject.delete_page(page) + + expect(subject.list_pages.count).to eq(0) + end + + it 'sets the correct commit email' do + subject.delete_page(page) + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + + it 'updates container activity' do + page + + expect(subject).to receive(:update_container_activity) + + subject.delete_page(page) + end + end + + describe '#ensure_repository' do + context 'if the repository exists' do + it 'does not create the repository' do + expect(subject.repository.exists?).to eq(true) + expect(subject.repository.raw).not_to receive(:create_repository) + + subject.ensure_repository + end + end + + context 'if the repository does not exist' do + let(:wiki_container) { wiki_container_without_repo } + + it 'creates the repository' do + expect(subject.repository.exists?).to eq(false) + + subject.ensure_repository + + expect(subject.repository.exists?).to eq(true) + end + end + end + + describe '#hook_attrs' do + it 'returns a hash with values' do + expect(subject.hook_attrs).to be_a Hash + expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch) + end + end +end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index 1831fc10628..4dd0152e3d1 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -212,8 +212,8 @@ RSpec.shared_examples 'project policies as owner' do end end -RSpec.shared_examples 'project policies as admin' do - context 'abilities for non-public projects' do +RSpec.shared_examples 'project policies as admin with admin mode' do + context 'abilities for non-public projects', :enable_admin_mode do let(:project) { create(:project, namespace: owner.namespace) } subject { described_class.new(admin, project) } @@ -232,3 +232,13 @@ RSpec.shared_examples 'project policies as admin' do end end end + +RSpec.shared_examples 'project policies as admin without admin mode' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(admin, project) } + + it { is_expected.to be_banned } + end +end diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb index b91500ffd9c..58822f4309b 100644 --- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb +++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb @@ -1,152 +1,116 @@ # frozen_string_literal: true RSpec.shared_examples 'model with wiki policies' do - let(:container) { raise NotImplementedError } - let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) } - - # TODO: Remove this helper once we implement group features - # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 - def set_access_level(access_level) - raise NotImplementedError - end - - subject { described_class.new(owner, container) } - - context 'when the feature is disabled' do - before do - set_access_level(ProjectFeature::DISABLED) - end + include ProjectHelpers + include AdminModeHelper - it 'does not include the wiki permissions' do - expect_disallowed(*permissions) - end + let(:container) { raise NotImplementedError } + let(:user) { raise NotImplementedError } - context 'when there is an external wiki' do - it 'does not include the wiki permissions' do - allow(container).to receive(:has_external_wiki?).and_return(true) + subject { described_class.new(user, container) } - expect_disallowed(*permissions) - end + let_it_be(:wiki_permissions) do + {}.tap do |permissions| + permissions[:guest] = %i[read_wiki] + permissions[:reporter] = permissions[:guest] + %i[download_wiki_code] + permissions[:developer] = permissions[:reporter] + %i[create_wiki] + permissions[:maintainer] = permissions[:developer] + %i[admin_wiki] + permissions[:all] = permissions[:maintainer] end end - describe 'read_wiki' do - subject { described_class.new(user, container) } - - member_roles = %i[guest developer] - stranger_roles = %i[anonymous non_member] - - user_roles = stranger_roles + member_roles + using RSpec::Parameterized::TableSyntax + + where(:container_level, :access_level, :membership, :access) do + :public | :enabled | :admin | :all + :public | :enabled | :maintainer | :maintainer + :public | :enabled | :developer | :developer + :public | :enabled | :reporter | :reporter + :public | :enabled | :guest | :guest + :public | :enabled | :non_member | :guest + :public | :enabled | :anonymous | :guest + + :public | :private | :admin | :all + :public | :private | :maintainer | :maintainer + :public | :private | :developer | :developer + :public | :private | :reporter | :reporter + :public | :private | :guest | :guest + :public | :private | :non_member | nil + :public | :private | :anonymous | nil + + :public | :disabled | :admin | nil + :public | :disabled | :maintainer | nil + :public | :disabled | :developer | nil + :public | :disabled | :reporter | nil + :public | :disabled | :guest | nil + :public | :disabled | :non_member | nil + :public | :disabled | :anonymous | nil + + :internal | :enabled | :admin | :all + :internal | :enabled | :maintainer | :maintainer + :internal | :enabled | :developer | :developer + :internal | :enabled | :reporter | :reporter + :internal | :enabled | :guest | :guest + :internal | :enabled | :non_member | :guest + :internal | :enabled | :anonymous | nil + + :internal | :private | :admin | :all + :internal | :private | :maintainer | :maintainer + :internal | :private | :developer | :developer + :internal | :private | :reporter | :reporter + :internal | :private | :guest | :guest + :internal | :private | :non_member | nil + :internal | :private | :anonymous | nil + + :internal | :disabled | :admin | nil + :internal | :disabled | :maintainer | nil + :internal | :disabled | :developer | nil + :internal | :disabled | :reporter | nil + :internal | :disabled | :guest | nil + :internal | :disabled | :non_member | nil + :internal | :disabled | :anonymous | nil + + :private | :private | :admin | :all + :private | :private | :maintainer | :maintainer + :private | :private | :developer | :developer + :private | :private | :reporter | :reporter + :private | :private | :guest | :guest + :private | :private | :non_member | nil + :private | :private | :anonymous | nil + + :private | :disabled | :admin | nil + :private | :disabled | :maintainer | nil + :private | :disabled | :developer | nil + :private | :disabled | :reporter | nil + :private | :disabled | :guest | nil + :private | :disabled | :non_member | nil + :private | :disabled | :anonymous | nil + end - # When a user is anonymous, their `current_user == nil` - let(:user) { create(:user) unless user_role == :anonymous } + with_them do + let(:user) { create_user_from_membership(container, membership) } + let(:allowed_permissions) { wiki_permissions[access].dup || [] } + let(:disallowed_permissions) { wiki_permissions[:all] - allowed_permissions } before do - container.visibility = container_visibility - set_access_level(wiki_access_level) - container.add_user(user, user_role) if member_roles.include?(user_role) - end - - title = ->(container_visibility, wiki_access_level, user_role) do - [ - "container is #{Gitlab::VisibilityLevel.level_name container_visibility}", - "wiki is #{ProjectFeature.str_from_access_level wiki_access_level}", - "user is #{user_role}" - ].join(', ') - end - - describe 'Situations where :read_wiki is always false' do - where(case_names: title, - container_visibility: Gitlab::VisibilityLevel.options.values, - wiki_access_level: [ProjectFeature::DISABLED], - user_role: user_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - - describe 'Situations where :read_wiki is always true' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: user_roles) + container.visibility = container_level.to_s + set_access_level(ProjectFeature.access_level_from_str(access_level.to_s)) + enable_admin_mode!(user) if user&.admin? - with_them do - it { is_expected.to be_allowed(:read_wiki) } + if allowed_permissions.any? && [container_level, access_level, membership] != [:private, :private, :guest] + allowed_permissions << :download_wiki_code end end - describe 'Situations where :read_wiki requires membership' do - context 'the wiki is private, and the user is a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC, - Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::PRIVATE], - user_role: member_roles) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the wiki is private, and the user is not member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC, - Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::PRIVATE], - user_role: stranger_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - - context 'the wiki is enabled, and the user is a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PRIVATE], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: member_roles) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the wiki is enabled, and the user is not a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PRIVATE], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: stranger_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end + it 'allows actions based on membership' do + expect_allowed(*allowed_permissions) + expect_disallowed(*disallowed_permissions) end + end - describe 'Situations where :read_wiki prohibits anonymous access' do - context 'the user is not anonymous' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC], - user_role: user_roles.reject { |u| u == :anonymous }) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the user is anonymous' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC], - user_role: %i[anonymous]) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - end + # TODO: Remove this helper once we implement group features + # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 + def set_access_level(access_level) + raise NotImplementedError end end diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb index b03da4471bc..50a8b81b518 100644 --- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb @@ -18,6 +18,16 @@ RSpec.shared_examples 'issuable quick actions' do end end + let(:unlabel_expectation) do + ->(noteable, can_use_quick_action) { + if can_use_quick_action + expect(noteable.labels).to be_empty + else + expect(noteable.labels).not_to be_empty + end + } + end + # Quick actions shared by issues and merge requests let(:issuable_quick_actions) do [ @@ -136,13 +146,11 @@ RSpec.shared_examples 'issuable quick actions' do ), QuickAction.new( action_text: "/unlabel", - expectation: ->(noteable, can_use_quick_action) { - if can_use_quick_action - expect(noteable.labels).to be_empty - else - expect(noteable.labels).not_to be_empty - end - } + expectation: unlabel_expectation + ), + QuickAction.new( + action_text: "/remove_label", + expectation: unlabel_expectation ), QuickAction.new( action_text: "/award :100:", diff --git a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb index 88ad37d232f..9bfd1e6faa0 100644 --- a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb @@ -25,7 +25,7 @@ RSpec.shared_examples 'creating award emojis marks Todos as done' do let(:awardable) { create(type) } let!(:todo) { create(:todo, target: awardable, project: project, user: user) } - it do + specify do subject expect(todo.reload.done?).to eq(expectation) diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index 90ac60a6fe7..feb3ba46353 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -80,7 +80,7 @@ RSpec.shared_examples 'group and project boards query' do cursored_query = query("after: \"#{end_cursor}\"") post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data'][board_parent_type]['boards']['edges'] + response_data = Gitlab::Json.parse(response.body)['data'][board_parent_type]['boards']['edges'] expect(grab_names(response_data)).to eq expected_boards.drop(2).first(2).map(&:name) end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb new file mode 100644 index 00000000000..48824a4b0d2 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'when the snippet is not found' do + let(:snippet_gid) do + "gid://gitlab/#{snippet.class.name}/#{non_existing_record_id}" + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] +end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index aa7f57ae903..f830f957174 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -23,21 +23,53 @@ RSpec.shared_examples 'update with repository actions' do context 'when the repository does not exist' do let(:snippet) { snippet_without_repo } - it 'creates the repository' do - update_snippet(snippet_id: snippet.id, params: { title: 'foo' }) + context 'when update attributes does not include file_name or content' do + it 'does not create the repository' do + update_snippet(snippet_id: snippet.id, params: { title: 'foo' }) - expect(snippet.repository).to exist + expect(snippet.repository).not_to exist + end end - it 'commits the file to the repository' do - content = 'New Content' - file_name = 'file_name.rb' + context 'when update attributes include file_name or content' do + it 'creates the repository' do + update_snippet(snippet_id: snippet.id, params: { title: 'foo', file_name: 'foo' }) + + expect(snippet.repository).to exist + end + + it 'commits the file to the repository' do + content = 'New Content' + file_name = 'file_name.rb' + + update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name }) + + blob = snippet.repository.blob_at('master', file_name) + expect(blob).not_to be_nil + expect(blob.data).to eq content + end + + context 'when save fails due to a repository creation error' do + let(:content) { 'File content' } + let(:file_name) { 'test.md' } + + before do + allow_next_instance_of(Snippets::UpdateService) do |instance| + allow(instance).to receive(:create_repository_for).with(snippet).and_raise(Snippets::UpdateService::CreateRepositoryError) + end + + update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name }) + end - update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name }) + it 'returns 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end - blob = snippet.repository.blob_at('master', file_name) - expect(blob).not_to be_nil - expect(blob.data).to eq content + it 'does not save the changes to the snippet object' do + expect(snippet.content).not_to eq(content) + expect(snippet.file_name).not_to eq(file_name) + end + end end end end @@ -48,3 +80,21 @@ RSpec.shared_examples 'snippet response without repository URLs' do expect(json_response).not_to have_key('http_url_to_repo') end end + +RSpec.shared_examples 'snippet blob content' do + it 'returns content from repository' do + subject + + expect(response.body).to eq(snippet.blobs.first.data) + end + + context 'when snippet repository is empty' do + let(:snippet) { snippet_with_empty_repo } + + it 'returns content from database' do + subject + + expect(response.body).to eq(snippet.content) + end + end +end diff --git a/spec/support/shared_examples/requires_variables_shared_example.rb b/spec/support/shared_examples/requires_variables_shared_example.rb new file mode 100644 index 00000000000..2921fccf87a --- /dev/null +++ b/spec/support/shared_examples/requires_variables_shared_example.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'requires variables' do + it 'shared example requires variables to be set', :aggregate_failures do + variables = Array.wrap(required_variables) + + variables.each do |variable_name| + expect { send(variable_name) }.not_to( + raise_error, "The following variable must be set to use this shared example: #{variable_name}" + ) + end + end +end diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb index 963453666c9..66f5e760c37 100644 --- a/spec/support/shared_examples/resource_events.rb +++ b/spec/support/shared_examples/resource_events.rb @@ -83,6 +83,24 @@ shared_examples 'a resource event for issues' do expect(events).to be_empty end end + + describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') } + let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') } + let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') } + + it 'returns the expected records for an issue with events' do + events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59') + + expect(events).to contain_exactly(event1, event2) + end + + it 'returns the expected records for an issue with no events' do + events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12') + + expect(events).to be_empty + end + end end shared_examples 'a resource event for merge requests' do diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb index b6c4841dbd4..db5c4b45b70 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb @@ -60,7 +60,7 @@ RSpec.shared_examples 'diff file entity' do context 'when the `single_mr_diff_view` feature is disabled' do before do - stub_feature_flags(single_mr_diff_view: { enabled: false, thing: project }) + stub_feature_flags(single_mr_diff_view: false) end it 'contains both kinds of diffs' do diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb index 1b7fe626aea..07a6353296d 100644 --- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb @@ -18,6 +18,10 @@ RSpec.shared_examples 'lists list service' do expect { service.execute(board) }.to change(board.lists, :count).by(1) end + it 'does not create a backlog list when create_default_lists is false' do + expect { service.execute(board, create_default_lists: false) }.not_to change(board.lists, :count) + end + it "returns board's lists" do expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] end diff --git a/spec/support/shared_examples/services/measurable_service_shared_examples.rb b/spec/support/shared_examples/services/measurable_service_shared_examples.rb new file mode 100644 index 00000000000..206c25e49af --- /dev/null +++ b/spec/support/shared_examples/services/measurable_service_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'measurable service' do + context 'when measurement is enabled' do + let!(:measuring) { Gitlab::Utils::Measuring.new(base_log_data) } + + before do + stub_feature_flags(feature_flag => true) + end + + it 'measure service execution with Gitlab::Utils::Measuring', :aggregate_failures do + expect(Gitlab::Utils::Measuring).to receive(:new).with(base_log_data).and_return(measuring) + expect(measuring).to receive(:with_measuring).and_call_original + end + end + + context 'when measurement is disabled' do + it 'does not measure service execution' do + stub_feature_flags(feature_flag => false) + + expect(Gitlab::Utils::Measuring).not_to receive(:new) + end + end + + def feature_flag + "gitlab_service_measuring_#{described_class_name}" + end + + def described_class_name + described_class.name.underscore.tr('/', '_') + end +end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index 90fcac0e55c..5dd1badbefc 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'valid dashboard service response for schema' do end RSpec.shared_examples 'valid dashboard service response' do - let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) } + let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) } it_behaves_like 'valid dashboard service response for schema' end @@ -38,7 +38,7 @@ RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do end RSpec.shared_examples 'valid embedded dashboard service response' do - let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) } + let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) } it_behaves_like 'valid dashboard service response for schema' end diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index d6166ac8188..0e6ecf49cd0 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -47,9 +47,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| old_repository_path = repository.full_path - result = subject.execute('test_second_storage') + result = subject.execute - expect(result[:status]).to eq(:success) + expect(result).to be_success expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('test_second_storage') expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false) @@ -62,7 +62,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| end it 'does not enqueue a GC run' do - expect { subject.execute('test_second_storage') } + expect { subject.execute } .not_to change(GitGarbageCollectWorker.jobs, :count) end end @@ -75,23 +75,25 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| it 'does not enqueue a GC run if housekeeping is disabled' do stub_application_setting(housekeeping_enabled: false) - expect { subject.execute('test_second_storage') } + expect { subject.execute } .not_to change(GitGarbageCollectWorker.jobs, :count) end it 'enqueues a GC run' do - expect { subject.execute('test_second_storage') } + expect { subject.execute } .to change(GitGarbageCollectWorker.jobs, :count).by(1) end end end context 'when the filesystems are the same' do + let(:destination) { project.repository_storage } + it 'bails out and does nothing' do - result = subject.execute(project.repository_storage) + result = subject.execute - expect(result[:status]).to eq(:error) - expect(result[:message]).to match(/SameFilesystemError/) + expect(result).to be_error + expect(result.message).to match(/SameFilesystemError/) end end @@ -114,9 +116,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(GitlabShellWorker).not_to receive(:perform_async) - result = subject.execute('test_second_storage') + result = subject.execute - expect(result[:status]).to eq(:error) + expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') end @@ -142,9 +144,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(GitlabShellWorker).not_to receive(:perform_async) - result = subject.execute('test_second_storage') + result = subject.execute - expect(result[:status]).to eq(:error) + expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') end diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb index 77f64e5e8f8..c5f84e205cf 100644 --- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb @@ -4,7 +4,7 @@ shared_examples 'a milestone events creator' do let_it_be(:user) { create(:user) } let(:created_at_time) { Time.utc(2019, 12, 30) } - let(:service) { described_class.new(resource, user, created_at: created_at_time) } + let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: nil) } context 'when milestone is present' do let_it_be(:milestone) { create(:milestone) } @@ -25,10 +25,13 @@ shared_examples 'a milestone events creator' do resource.milestone = nil end + let(:old_milestone) { create(:milestone, project: resource.project) } + let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: old_milestone) } + it 'creates the expected event records' do expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1) - expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: nil, state: 'opened') + expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: old_milestone, state: 'opened') end end diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb new file mode 100644 index 00000000000..51a4a8b1cd9 --- /dev/null +++ b/spec/support/shared_examples/services/snippets_shared_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'snippets spam check is performed' do + shared_examples 'marked as spam' do + it 'marks a snippet as spam' do + expect(snippet).to be_spam + end + + it 'invalidates the snippet' do + expect(snippet).to be_invalid + end + + it 'creates a new spam_log' do + expect { snippet } + .to have_spam_log(title: snippet.title, noteable_type: snippet.class.name) + end + + it 'assigns a spam_log to an issue' do + expect(snippet.spam_log).to eq(SpamLog.last) + end + end + + let(:extra_opts) do + { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) } + end + + before do + expect_next_instance_of(Spam::AkismetService) do |akismet_service| + expect(akismet_service).to receive_messages(spam?: true) + end + end + + [true, false, nil].each do |allow_possible_spam| + context "when allow_possible_spam flag is #{allow_possible_spam.inspect}" do + before do + stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil? + end + + it_behaves_like 'marked as spam' + end + end +end diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb new file mode 100644 index 00000000000..71bdd46572f --- /dev/null +++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| + let(:container) { create(container_type, :wiki_repo) } + let(:user) { create(:user) } + let(:page_title) { 'Title' } + + let(:opts) do + { + title: page_title, + content: 'Content for wiki page', + format: 'markdown' + } + end + + subject(:service) { described_class.new(container: container, current_user: user, params: opts) } + + it 'creates wiki page with valid attributes' do + page = service.execute + + expect(page).to be_valid + expect(page).to be_persisted + expect(page.title).to eq(opts[:title]) + expect(page.content).to eq(opts[:content]) + expect(page.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once.with(WikiPage) + + service.execute + end + + it 'counts wiki page creation' do + counter = Gitlab::UsageDataCounters::WikiPageCounter + + expect { service.execute }.to change { counter.read(:create) }.by 1 + end + + shared_examples 'correct event created' do + it 'creates appropriate events' do + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904 + pending('group wiki support') if container_type == :group + + expect { service.execute }.to change { Event.count }.by 1 + + expect(Event.recent.first).to have_attributes( + action: Event::CREATED, + target: have_attributes(canonical_slug: page_title) + ) + end + end + + context 'the new page is at the top level' do + let(:page_title) { 'root-level-page' } + + include_examples 'correct event created' + end + + context 'the new page is in a subsection' do + let(:page_title) { 'subsection/page' } + + include_examples 'correct event created' + end + + context 'the feature is disabled' do + before do + stub_feature_flags(wiki_events: false) + end + + it 'does not record the activity' do + expect { service.execute }.not_to change(Event, :count) + end + end + + context 'when the options are bad' do + let(:page_title) { '' } + + it 'does not count a creation event' do + counter = Gitlab::UsageDataCounters::WikiPageCounter + + expect { service.execute }.not_to change { counter.read(:create) } + end + + it 'does not record the activity' do + expect { service.execute }.not_to change(Event, :count) + end + + it 'reports the error' do + expect(service.execute).to be_invalid + .and have_attributes(errors: be_present) + end + end +end diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb new file mode 100644 index 00000000000..62541eb3da9 --- /dev/null +++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type| + let(:container) { create(container_type) } + + let(:user) { create(:user) } + let(:page) { create(:wiki_page) } + + subject(:service) { described_class.new(container: container, current_user: user) } + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once.with(page) + + service.execute(page) + end + + it 'increments the delete count' do + counter = Gitlab::UsageDataCounters::WikiPageCounter + + expect { service.execute(page) }.to change { counter.read(:delete) }.by 1 + end + + it 'creates a new wiki page deletion event' do + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904 + pending('group wiki support') if container_type == :group + + expect { service.execute(page) }.to change { Event.count }.by 1 + + expect(Event.recent.first).to have_attributes( + action: Event::DESTROYED, + target: have_attributes(canonical_slug: page.slug) + ) + end + + it 'does not increment the delete count if the deletion failed' do + counter = Gitlab::UsageDataCounters::WikiPageCounter + + expect { service.execute(nil) }.not_to change { counter.read(:delete) } + end + + context 'the feature is disabled' do + before do + stub_feature_flags(wiki_events: false) + end + + it 'does not record the activity' do + expect { service.execute(page) }.not_to change(Event, :count) + end + end +end diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb new file mode 100644 index 00000000000..0dfc99d043b --- /dev/null +++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| + let(:container) { create(container_type, :wiki_repo) } + + let(:user) { create(:user) } + let(:page) { create(:wiki_page) } + let(:page_title) { 'New Title' } + + let(:opts) do + { + content: 'New content for wiki page', + format: 'markdown', + message: 'New wiki message', + title: page_title + } + end + + subject(:service) { described_class.new(container: container, current_user: user, params: opts) } + + it 'updates the wiki page' do + updated_page = service.execute(page) + + expect(updated_page).to be_valid + expect(updated_page.message).to eq(opts[:message]) + expect(updated_page.content).to eq(opts[:content]) + expect(updated_page.format).to eq(opts[:format].to_sym) + expect(updated_page.title).to eq(page_title) + end + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once.with(WikiPage) + + service.execute(page) + end + + it 'counts edit events' do + counter = Gitlab::UsageDataCounters::WikiPageCounter + + expect { service.execute page }.to change { counter.read(:update) }.by 1 + end + + shared_examples 'adds activity event' do + it 'adds a new wiki page activity event' do + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904 + pending('group wiki support') if container_type == :group + + expect { service.execute(page) }.to change { Event.count }.by 1 + + expect(Event.recent.first).to have_attributes( + action: Event::UPDATED, + wiki_page: page, + target_title: page.title + ) + end + end + + context 'the page is at the top level' do + let(:page_title) { 'Top level page' } + + include_examples 'adds activity event' + end + + context 'the page is in a subsection' do + let(:page_title) { 'Subsection / secondary page' } + + include_examples 'adds activity event' + end + + context 'the feature is disabled' do + before do + stub_feature_flags(wiki_events: false) + end + + it 'does not record the activity' do + expect { service.execute(page) }.not_to change(Event, :count) + end + end + + context 'when the options are bad' do + let(:page_title) { '' } + + it 'does not count an edit event' do + counter = Gitlab::UsageDataCounters::WikiPageCounter + + expect { service.execute page }.not_to change { counter.read(:update) } + end + + it 'does not record the activity' do + expect { service.execute page }.not_to change(Event, :count) + end + + it 'reports the error' do + expect(service.execute(page)).to be_invalid + .and have_attributes(errors: be_present) + end + end +end diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb new file mode 100644 index 00000000000..541e332e3a1 --- /dev/null +++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_type| + let(:container) { create(container_type, :wiki_repo) } + let(:wiki) { container.wiki } + + let(:user) { create(:user) } + let(:file_name) { 'filename.txt' } + let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} } + + let(:file_opts) do + { + file_name: file_name, + file_content: 'Content of attachment' + } + end + let(:opts) { file_opts } + + let(:service) { Wikis::CreateAttachmentService.new(container: container, current_user: user, params: opts) } + + subject(:service_execute) { service.execute[:result] } + + before do + container.add_developer(user) + end + + context 'creates branch if it does not exists' do + let(:branch_name) { 'new_branch' } + let(:opts) { file_opts.merge(branch_name: branch_name) } + + it do + expect(wiki.repository.branches).to be_empty + expect { service.execute }.to change { wiki.repository.branches.count }.by(1) + expect(wiki.repository.branches.first.name).to eq branch_name + end + end + + it 'adds file to the repository' do + expect(wiki.repository.ls_files('HEAD')).to be_empty + + service.execute + + files = wiki.repository.ls_files('HEAD') + expect(files.count).to eq 1 + expect(files.first).to match(file_path_regex) + end + + context 'returns' do + before do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + service_execute + end + + it 'returns related information', :aggregate_failures do + expect(service_execute[:file_name]).to eq file_name + expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt' + expect(service_execute[:branch]).to eq wiki.default_branch + expect(service_execute[:commit]).not_to be_empty + end + end +end diff --git a/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb b/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb deleted file mode 100644 index 5950a1a53e2..00000000000 --- a/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'measurable' do - context 'when measurement is enabled' do - let(:measurement_enabled) { true } - - it 'prints measurement results' do - expect { subject }.to output(including('Measuring enabled...', 'Number of sql calls:', 'GC stats:')).to_stdout - end - end - - context 'when measurement is not enabled' do - let(:measurement_enabled) { false } - - it 'does not output measurement results' do - expect { subject }.not_to output(/Measuring enabled.../).to_stdout - end - end - - context 'when measurement is not provided' do - let(:measurement_enabled) { nil } - - it 'does not output measurement results' do - expect { subject }.not_to output(/Measuring enabled.../).to_stdout - end - - it 'does not raise any exception' do - expect { subject }.not_to raise_error - end - end -end diff --git a/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb new file mode 100644 index 00000000000..fba8b4aadbb --- /dev/null +++ b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "refreshes user's project authorizations" do + describe '#perform' do + let(:user) { create(:user) } + + subject(:job) { described_class.new } + + it "refreshes user's authorized projects" do + expect_any_instance_of(User).to receive(:refresh_authorized_projects) + + job.perform(user.id) + end + + context "when the user is not found" do + it "does nothing" do + expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) + + job.perform(-1) + end + end + + it_behaves_like "an idempotent worker" do + let(:job_args) { user.id } + + it "does not change authorizations when run twice" do + group = create(:group) + create(:project, namespace: group) + group.add_developer(user) + + # Delete the authorization created by the after save hook of the member + # created above. + user.project_authorizations.delete_all + + expect { job.perform(user.id) }.to change { user.project_authorizations.reload.size }.by(1) + expect { job.perform(user.id) }.not_to change { user.project_authorizations.reload.size } + end + end + end +end diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb index c0d17d6853d..ae8c82cb67c 100644 --- a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb +++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb @@ -20,7 +20,7 @@ shared_examples 'does not advance to next stage' do end end -shared_examples 'cannot do jira import' do +shared_examples 'cannot do Jira import' do it 'does not advance to next stage' do worker = described_class.new expect(worker).not_to receive(:import) diff --git a/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb b/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb index 9e8102aea53..c79e3ed7d21 100644 --- a/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb @@ -3,12 +3,14 @@ RSpec.shared_examples 'a pages cronjob scheduling jobs with context' do |scheduled_worker_class| let(:worker) { described_class.new } - it 'does not cause extra queries for multiple domains' do - control = ActiveRecord::QueryRecorder.new { worker.perform } + context 'with RequestStore enabled', :request_store do + it 'does not cause extra queries for multiple domains' do + control = ActiveRecord::QueryRecorder.new { worker.perform } - extra_domain + extra_domain - expect { worker.perform }.not_to exceed_query_limit(control) + expect { worker.perform }.not_to exceed_query_limit(control) + end end it 'schedules the renewal with a context' do diff --git a/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb b/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb new file mode 100644 index 00000000000..0bbd0e2a90d --- /dev/null +++ b/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'reactive cacheable worker' do + describe '#perform' do + context 'when reactive cache worker class is found' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + let!(:environment) { create(:environment, project: project) } + + it 'calls #exclusively_update_reactive_cache!' do + expect_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!) + + described_class.new.perform("Environment", environment.id) + end + + context 'when ReactiveCaching::ExceededReactiveCacheLimit is raised' do + it 'avoids failing the job and tracks via Gitlab::ErrorTracking' do + allow_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!) + .and_raise(ReactiveCaching::ExceededReactiveCacheLimit) + + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(kind_of(ReactiveCaching::ExceededReactiveCacheLimit)) + + described_class.new.perform("Environment", environment.id) + end + end + end + + context 'when reactive cache worker class is not found' do + it 'raises no error' do + expect { described_class.new.perform("Environment", -1) }.not_to raise_error + end + end + + context 'when reactive cache worker class is invalid' do + it 'raises no error' do + expect { described_class.new.perform("FooBarKux", -1) }.not_to raise_error + end + end + end + + describe 'worker context' do + it 'sets the related class on the job' do + described_class.perform_async('Environment', 1, 'other', 'argument') + + scheduled_job = described_class.jobs.first + + expect(scheduled_job).to include('meta.related_class' => 'Environment') + end + + it 'sets the related class on the job when it was passed as a class' do + described_class.perform_async(Project, 1, 'other', 'argument') + + scheduled_job = described_class.jobs.first + + expect(scheduled_job).to include('meta.related_class' => 'Project') + end + end +end |