From 6438df3a1e0fb944485cebf07976160184697d72 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 20 Jan 2021 13:34:23 -0600 Subject: Add latest changes from gitlab-org/gitlab@13-8-stable-ee --- .../snippet_repository_storage_move_spec.rb | 25 ++ spec/lib/api/helpers/authentication_spec.rb | 207 +++++++++++++ spec/lib/api/helpers_spec.rb | 47 ++- spec/lib/atlassian/jira_connect/client_spec.rb | 300 +++++++++++++++++- .../jira_connect/serializers/build_entity_spec.rb | 4 +- .../serializers/deployment_entity_spec.rb | 95 ++++++ .../serializers/feature_flag_entity_spec.rb | 85 +++++ spec/lib/banzai/filter/asset_proxy_filter_spec.rb | 10 +- .../broadcast_message_sanitization_filter_spec.rb | 12 +- .../filter/reference_redactor_filter_spec.rb | 31 +- spec/lib/banzai/filter/sanitization_filter_spec.rb | 24 +- .../banzai/filter/truncate_source_filter_spec.rb | 31 ++ .../banzai/pipeline/description_pipeline_spec.rb | 2 +- spec/lib/banzai/pipeline/gfm_pipeline_spec.rb | 4 +- .../banzai/pipeline/pre_process_pipeline_spec.rb | 8 + .../common/extractors/graphql_extractor_spec.rb | 7 +- .../groups/pipelines/group_pipeline_spec.rb | 16 +- .../pipelines/subgroup_entities_pipeline_spec.rb | 10 +- .../bulk_imports/importers/group_importer_spec.rb | 5 +- .../bulk_imports/importers/groups_importer_spec.rb | 36 --- spec/lib/bulk_imports/pipeline_spec.rb | 14 +- spec/lib/constraints/admin_constrainer_spec.rb | 2 +- spec/lib/container_registry/client_spec.rb | 100 +++++- spec/lib/declarative_enum_spec.rb | 147 +++++++++ spec/lib/expand_variables_spec.rb | 37 +++ .../issue_deployed_to_production_spec.rb | 7 + spec/lib/gitlab/api_authentication/builder_spec.rb | 76 +++++ .../sent_through_builder_spec.rb | 17 + .../api_authentication/token_locator_spec.rb | 55 ++++ .../api_authentication/token_resolver_spec.rb | 117 +++++++ .../api_authentication/token_type_builder_spec.rb | 16 + spec/lib/gitlab/asset_proxy_spec.rb | 4 +- spec/lib/gitlab/auth/auth_finders_spec.rb | 58 ++-- spec/lib/gitlab/auth/current_user_mode_spec.rb | 2 +- spec/lib/gitlab/auth/ldap/config_spec.rb | 22 +- spec/lib/gitlab/auth/request_authenticator_spec.rb | 5 +- spec/lib/gitlab/auth_spec.rb | 5 + .../backfill_artifact_expiry_date_spec.rb | 82 +++++ ...y_column_using_background_migration_job_spec.rb | 91 ++++++ ...finding_uuid_for_vulnerability_feedback_spec.rb | 113 +++++++ .../remove_duplicate_services_spec.rb | 121 ++++++++ spec/lib/gitlab/checks/diff_check_spec.rb | 97 ++---- spec/lib/gitlab/ci/config/entry/artifacts_spec.rb | 17 + spec/lib/gitlab/ci/config/entry/variables_spec.rb | 2 +- .../gitlab/ci/config/external/file/local_spec.rb | 6 +- .../gitlab/ci/config/external/file/project_spec.rb | 6 +- spec/lib/gitlab/ci/config/external/mapper_spec.rb | 126 +++++++- .../gitlab/ci/config/external/processor_spec.rb | 13 - spec/lib/gitlab/ci/config_spec.rb | 24 ++ spec/lib/gitlab/ci/lint_spec.rb | 2 +- .../gitlab/ci/parsers/coverage/cobertura_spec.rb | 6 + spec/lib/gitlab/ci/pipeline/chain/build_spec.rb | 114 ++++++- spec/lib/gitlab/ci/pipeline/chain/command_spec.rb | 26 ++ .../gitlab/ci/pipeline/chain/seed_block_spec.rb | 12 - spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb | 39 +-- .../ci/pipeline/chain/template_usage_spec.rb | 37 +++ spec/lib/gitlab/ci/pipeline/seed/build_spec.rb | 2 +- spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb | 3 +- .../gitlab/ci/reports/test_failure_history_spec.rb | 13 - spec/lib/gitlab/ci/status/group/factory_spec.rb | 5 + spec/lib/gitlab/ci/syntax_templates_spec.rb | 25 ++ .../5_minute_production_app_ci_yaml_spec.rb | 49 +++ .../AWS/deploy_ecs_gitlab_ci_yaml_spec.rb | 2 +- .../ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb | 2 +- .../Jobs/code_quality_gitlab_ci_yaml_spec.rb | 2 +- .../templates/Jobs/deploy_gitlab_ci_yaml_spec.rb | 2 +- .../ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb | 2 +- .../Terraform/base_gitlab_ci_yaml_spec.rb | 2 +- ...load_performance_testing_gitlab_ci_yaml_spec.rb | 2 +- .../templates/auto_devops_gitlab_ci_yaml_spec.rb | 4 +- .../ci/templates/flutter_gitlab_ci_yaml_spec.rb | 25 ++ spec/lib/gitlab/ci/templates/npm_spec.rb | 3 +- .../terraform_latest_gitlab_ci_yaml_spec.rb | 3 +- .../gitlab/ci/variables/collection/sorted_spec.rb | 251 +++++++++++++++ spec/lib/gitlab/ci/yaml_processor_spec.rb | 34 -- spec/lib/gitlab/composer/version_index_spec.rb | 49 +++ .../gitlab/config/entry/composable_hash_spec.rb | 2 +- spec/lib/gitlab/conflict/file_spec.rb | 32 +- .../cycle_analytics/base_event_fetcher_spec.rb | 49 --- .../cycle_analytics/code_event_fetcher_spec.rb | 13 - spec/lib/gitlab/cycle_analytics/code_stage_spec.rb | 129 -------- spec/lib/gitlab/cycle_analytics/events_spec.rb | 182 ----------- .../cycle_analytics/issue_event_fetcher_spec.rb | 9 - .../lib/gitlab/cycle_analytics/issue_stage_spec.rb | 136 -------- .../cycle_analytics/plan_event_fetcher_spec.rb | 17 - spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb | 116 ------- .../production_event_fetcher_spec.rb | 9 - .../cycle_analytics/review_event_fetcher_spec.rb | 9 - .../gitlab/cycle_analytics/review_stage_spec.rb | 90 ------ .../cycle_analytics/staging_event_fetcher_spec.rb | 13 - .../gitlab/cycle_analytics/staging_stage_spec.rb | 99 ------ .../cycle_analytics/test_event_fetcher_spec.rb | 13 - spec/lib/gitlab/cycle_analytics/test_stage_spec.rb | 57 ---- spec/lib/gitlab/danger/base_linter_spec.rb | 53 +++- spec/lib/gitlab/danger/changelog_spec.rb | 71 ++++- spec/lib/gitlab/danger/helper_spec.rb | 56 +--- spec/lib/gitlab/danger/roulette_spec.rb | 63 ---- spec/lib/gitlab/danger/teammate_spec.rb | 8 + spec/lib/gitlab/danger/title_linting_spec.rb | 56 ++++ .../gitlab/danger/weightage/maintainers_spec.rb | 34 ++ spec/lib/gitlab/danger/weightage/reviewers_spec.rb | 63 ++++ spec/lib/gitlab/data_builder/build_spec.rb | 1 + spec/lib/gitlab/data_builder/pipeline_spec.rb | 1 + spec/lib/gitlab/database/migration_helpers_spec.rb | 190 ++++++++++++ .../partitioning/partition_creator_spec.rb | 2 +- .../database/partitioning/replace_table_spec.rb | 2 +- .../foreign_key_helpers_spec.rb | 2 +- .../index_helpers_spec.rb | 2 +- .../table_management_helpers_spec.rb | 139 +++++---- .../postgres_hll/batch_distinct_counter_spec.rb | 83 +---- .../gitlab/database/postgres_hll/buckets_spec.rb | 33 ++ .../gitlab/database/reindexing/coordinator_spec.rb | 82 +++-- .../database/reindexing/grafana_notifier_spec.rb | 139 +++++++++ .../database/reindexing/index_selection_spec.rb | 2 +- .../database/reindexing/reindex_action_spec.rb | 112 ++++--- spec/lib/gitlab/database/reindexing_spec.rb | 9 +- .../self_monitoring/project/create_service_spec.rb | 30 +- spec/lib/gitlab/diff/position_spec.rb | 58 ++++ .../email/handler/create_note_handler_spec.rb | 234 ++------------ .../create_note_on_issuable_handler_spec.rb | 61 ++++ .../email/handler/service_desk_handler_spec.rb | 10 - spec/lib/gitlab/email/handler_spec.rb | 5 +- spec/lib/gitlab/error_tracking_spec.rb | 21 +- .../experimentation/controller_concern_spec.rb | 10 + spec/lib/gitlab/experimentation/experiment_spec.rb | 6 +- spec/lib/gitlab/experimentation_spec.rb | 2 + spec/lib/gitlab/faraday/error_callback_spec.rb | 59 ++++ spec/lib/gitlab/git/changed_path_spec.rb | 29 ++ spec/lib/gitlab/git/diff_spec.rb | 2 +- spec/lib/gitlab/git/repository_spec.rb | 31 +- spec/lib/gitlab/git/wiki_page_version_spec.rb | 28 ++ spec/lib/gitlab/git_access_snippet_spec.rb | 89 +++++- spec/lib/gitlab/git_access_spec.rb | 60 +++- .../gitlab/gitaly_client/commit_service_spec.rb | 6 +- .../gitaly_client/repository_service_spec.rb | 5 +- .../importer/repository_importer_spec.rb | 2 +- spec/lib/gitlab/gitpod_spec.rb | 73 ----- spec/lib/gitlab/graphql/batch_key_spec.rb | 78 +++++ .../graphql/pagination/keyset/connection_spec.rb | 41 +++ spec/lib/gitlab/graphql/queries_spec.rb | 343 +++++++++++++++++++++ spec/lib/gitlab/import_export/all_models.yml | 4 +- .../kubernetes/cilium_network_policy_spec.rb | 24 +- spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb | 5 +- spec/lib/gitlab/kubernetes/pod_cmd_spec.rb | 14 + .../metrics/samplers/action_cable_sampler_spec.rb | 10 +- .../metrics/samplers/database_sampler_spec.rb | 10 +- .../gitlab/metrics/samplers/puma_sampler_spec.rb | 10 +- .../gitlab/metrics/samplers/ruby_sampler_spec.rb | 12 +- .../metrics/samplers/threads_sampler_spec.rb | 10 +- .../metrics/samplers/unicorn_sampler_spec.rb | 2 + spec/lib/gitlab/metrics/system_spec.rb | 34 ++ .../multipart/handler_for_jwt_params_spec.rb | 53 ---- spec/lib/gitlab/middleware/multipart_spec.rb | 198 ++++++++++++ .../multipart_with_handler_for_jwt_params_spec.rb | 202 ------------ .../middleware/multipart_with_handler_spec.rb | 196 ------------ spec/lib/gitlab/path_regex_spec.rb | 6 + .../redis_adapter_when_peek_enabled_spec.rb | 1 + spec/lib/gitlab/project_template_spec.rb | 1 + spec/lib/gitlab/prometheus/internal_spec.rb | 48 +-- spec/lib/gitlab/rack_attack_spec.rb | 212 ++++++++++++- spec/lib/gitlab/search_results_spec.rb | 39 ++- .../sidekiq_middleware/admin_mode/client_spec.rb | 2 +- .../sidekiq_middleware/admin_mode/server_spec.rb | 2 +- spec/lib/gitlab/sidekiq_middleware_spec.rb | 2 +- .../slash_commands/presenters/issue_move_spec.rb | 9 +- .../template/gitlab_ci_syntax_yml_template_spec.rb | 17 + spec/lib/gitlab/throttle_spec.rb | 28 ++ spec/lib/gitlab/tracking/standard_context_spec.rb | 55 ++++ spec/lib/gitlab/tracking_spec.rb | 43 ++- spec/lib/gitlab/url_builder_spec.rb | 2 + spec/lib/gitlab/usage/metric_definition_spec.rb | 123 ++++++++ spec/lib/gitlab/usage/metric_spec.rb | 29 ++ .../ci_template_unique_counter_spec.rb | 31 ++ .../guest_package_event_counter_spec.rb | 31 -- .../usage_data_counters/hll_redis_counter_spec.rb | 156 +++++----- .../merge_request_activity_unique_counter_spec.rb | 151 +++++++++ .../package_event_counter_spec.rb | 31 ++ spec/lib/gitlab/usage_data_spec.rb | 6 +- spec/lib/gitlab/user_access_spec.rb | 32 +- spec/lib/gitlab/utils/usage_data_spec.rb | 112 ++++++- spec/lib/gitlab/utils_spec.rb | 17 + spec/lib/gitlab/uuid_spec.rb | 19 ++ spec/lib/gitlab/visibility_level_spec.rb | 48 ++- .../lib/release_highlights/validator/entry_spec.rb | 87 ++++++ spec/lib/release_highlights/validator_spec.rb | 85 +++++ spec/lib/uploaded_file_spec.rb | 194 +----------- 186 files changed, 5947 insertions(+), 2974 deletions(-) create mode 100644 spec/lib/api/entities/snippet_repository_storage_move_spec.rb create mode 100644 spec/lib/api/helpers/authentication_spec.rb create mode 100644 spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb create mode 100644 spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb create mode 100644 spec/lib/banzai/filter/truncate_source_filter_spec.rb delete mode 100644 spec/lib/bulk_imports/importers/groups_importer_spec.rb create mode 100644 spec/lib/declarative_enum_spec.rb create mode 100644 spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_deployed_to_production_spec.rb create mode 100644 spec/lib/gitlab/api_authentication/builder_spec.rb create mode 100644 spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb create mode 100644 spec/lib/gitlab/api_authentication/token_locator_spec.rb create mode 100644 spec/lib/gitlab/api_authentication/token_resolver_spec.rb create mode 100644 spec/lib/gitlab/api_authentication/token_type_builder_spec.rb create mode 100644 spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb create mode 100644 spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb create mode 100644 spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb create mode 100644 spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb create mode 100644 spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb create mode 100644 spec/lib/gitlab/ci/syntax_templates_spec.rb create mode 100644 spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb create mode 100644 spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb create mode 100644 spec/lib/gitlab/ci/variables/collection/sorted_spec.rb create mode 100644 spec/lib/gitlab/composer/version_index_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/code_stage_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/events_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/review_stage_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/test_stage_spec.rb create mode 100644 spec/lib/gitlab/danger/title_linting_spec.rb create mode 100644 spec/lib/gitlab/danger/weightage/maintainers_spec.rb create mode 100644 spec/lib/gitlab/danger/weightage/reviewers_spec.rb create mode 100644 spec/lib/gitlab/database/postgres_hll/buckets_spec.rb create mode 100644 spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb create mode 100644 spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb create mode 100644 spec/lib/gitlab/faraday/error_callback_spec.rb create mode 100644 spec/lib/gitlab/git/changed_path_spec.rb create mode 100644 spec/lib/gitlab/git/wiki_page_version_spec.rb delete mode 100644 spec/lib/gitlab/gitpod_spec.rb create mode 100644 spec/lib/gitlab/graphql/batch_key_spec.rb create mode 100644 spec/lib/gitlab/graphql/queries_spec.rb create mode 100644 spec/lib/gitlab/kubernetes/pod_cmd_spec.rb delete mode 100644 spec/lib/gitlab/middleware/multipart/handler_for_jwt_params_spec.rb create mode 100644 spec/lib/gitlab/middleware/multipart_spec.rb delete mode 100644 spec/lib/gitlab/middleware/multipart_with_handler_for_jwt_params_spec.rb delete mode 100644 spec/lib/gitlab/middleware/multipart_with_handler_spec.rb create mode 100644 spec/lib/gitlab/template/gitlab_ci_syntax_yml_template_spec.rb create mode 100644 spec/lib/gitlab/tracking/standard_context_spec.rb create mode 100644 spec/lib/gitlab/usage/metric_definition_spec.rb create mode 100644 spec/lib/gitlab/usage/metric_spec.rb create mode 100644 spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb delete mode 100644 spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb create mode 100644 spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb create mode 100644 spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb create mode 100644 spec/lib/release_highlights/validator/entry_spec.rb create mode 100644 spec/lib/release_highlights/validator_spec.rb (limited to 'spec/lib') diff --git a/spec/lib/api/entities/snippet_repository_storage_move_spec.rb b/spec/lib/api/entities/snippet_repository_storage_move_spec.rb new file mode 100644 index 00000000000..8086be3ffa7 --- /dev/null +++ b/spec/lib/api/entities/snippet_repository_storage_move_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::SnippetRepositoryStorageMove do + describe '#as_json' do + subject { entity.as_json } + + let(:default_storage) { 'default' } + let(:second_storage) { 'test_second_storage' } + let(:storage_move) { create(:snippet_repository_storage_move, :scheduled, destination_storage_name: second_storage) } + let(:entity) { described_class.new(storage_move) } + + it 'includes basic fields' do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%W[#{default_storage} #{second_storage}]) + + is_expected.to include( + state: 'scheduled', + source_storage_name: default_storage, + destination_storage_name: second_storage, + snippet: a_kind_of(Hash) + ) + end + end +end diff --git a/spec/lib/api/helpers/authentication_spec.rb b/spec/lib/api/helpers/authentication_spec.rb new file mode 100644 index 00000000000..461b0d2f6f9 --- /dev/null +++ b/spec/lib/api/helpers/authentication_spec.rb @@ -0,0 +1,207 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Helpers::Authentication do + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + + describe 'class methods' do + subject { Class.new.include(described_class::ClassMethods).new } + + describe '.authenticate_with' do + it 'sets namespace_inheritable :authentication to correctly when body is empty' do + expect(subject).to receive(:namespace_inheritable).with(:authentication, {}) + + subject.authenticate_with { |allow| } + end + + it 'sets namespace_inheritable :authentication to correctly when body is not empty' do + expect(subject).to receive(:namespace_inheritable).with(:authentication, { basic: [:pat, :job], oauth: [:pat, :job] }) + + subject.authenticate_with { |allow| allow.token_type(:pat, :job).sent_through(:basic, :oauth) } + end + end + end + + describe 'helper methods' do + let(:object) do + cls = Class.new + + class << cls + def helpers(*modules, &block) + modules.each { |m| include m } + include Module.new.tap { |m| m.class_eval(&block) } if block_given? + end + end + + cls.define_method(:unauthorized!) { raise '401' } + cls.define_method(:bad_request!) { |m| raise "400 - #{m}" } + + # Include the helper class methods, as instance methods + cls.include described_class::ClassMethods + + # Include the methods under test + cls.include described_class + + cls.new + end + + describe '#token_from_namespace_inheritable' do + let(:object) do + o = super() + + o.instance_eval do + # It doesn't matter what this returns as long as the method is defined + def current_request + nil + end + + # Spoof Grape's namespace inheritable system + def namespace_inheritable(key, value = nil) + return unless key == :authentication + + if value + @authentication = value + else + @authentication + end + end + end + + o + end + + let(:authentication) do + object.authenticate_with { |allow| allow.token_types(*resolvers).sent_through(*locators) } + end + + subject { object.token_from_namespace_inheritable } + + before do + # Skip validation of token transports and types to simplify testing + allow(Gitlab::APIAuthentication::TokenLocator).to receive(:new) { |type| type } + allow(Gitlab::APIAuthentication::TokenResolver).to receive(:new) { |type| type } + + authentication + end + + shared_examples 'stops early' do |response_method| + it "calls ##{response_method}" do + errcls = Class.new(StandardError) + expect(object).to receive(response_method).and_raise(errcls) + expect { subject }.to raise_error(errcls) + end + end + + shared_examples 'an anonymous request' do + it 'returns nil' do + expect(subject).to be(nil) + end + end + + shared_examples 'an authenticated request' do + it 'returns the token' do + expect(subject).to be(token) + end + end + + shared_examples 'an unauthorized request' do + it_behaves_like 'stops early', :unauthorized! + end + + context 'with no allowed authentication strategies' do + let(:authentication) { nil } + + it_behaves_like 'an anonymous request' + end + + context 'with no located credentials' do + let(:locators) { [double(extract: nil)] } + let(:resolvers) { [] } + + it_behaves_like 'an anonymous request' + end + + context 'with one set of located credentials' do + let(:locators) { [double(extract: true)] } + + context 'when the credentials contain a valid token' do + let(:token) { double } + let(:resolvers) { [double(resolve: token)] } + + it_behaves_like 'an authenticated request' + end + + context 'when the credentials do not contain a valid token' do + let(:resolvers) { [double(resolve: nil)] } + + it_behaves_like 'an unauthorized request' + end + end + + context 'with multiple located credentials' do + let(:locators) { [double(extract: true), double(extract: true)] } + let(:resolvers) { [] } + + it_behaves_like 'stops early', :bad_request! + end + + context 'when a resolver raises UnauthorizedError' do + let(:locators) { [double(extract: true)] } + let(:resolvers) do + r = double + expect(r).to receive(:resolve).and_raise(Gitlab::Auth::UnauthorizedError) + r + end + + it_behaves_like 'an unauthorized request' + end + end + + describe '#access_token_from_namespace_inheritable' do + subject { object.access_token_from_namespace_inheritable } + + it 'returns #token_from_namespace_inheritable if it is a personal access token' do + expect(object).to receive(:token_from_namespace_inheritable).and_return(personal_access_token) + expect(subject).to be(personal_access_token) + end + + it 'returns nil if #token_from_namespace_inheritable is not a personal access token' do + token = double + expect(object).to receive(:token_from_namespace_inheritable).and_return(token) + expect(subject).to be(nil) + end + end + + describe '#user_from_namespace_inheritable' do + subject { object.user_from_namespace_inheritable } + + it 'returns #token_from_namespace_inheritable if it is a deploy token' do + expect(object).to receive(:token_from_namespace_inheritable).and_return(deploy_token) + expect(subject).to be(deploy_token) + end + + it 'returns #token_from_namespace_inheritable.user if the token is not a deploy token' do + user = double + token = double(user: user) + expect(object).to receive(:token_from_namespace_inheritable).and_return(token) + + expect(subject).to be(user) + end + + it 'falls back to #find_user_from_warden if #token_from_namespace_inheritable.user is nil' do + token = double(user: nil) + expect(object).to receive(:token_from_namespace_inheritable).and_return(token) + subject + end + + it 'falls back to #find_user_from_warden if #token_from_namespace_inheritable is nil' do + expect(object).to receive(:token_from_namespace_inheritable).and_return(nil) + subject + end + end + end +end diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index be5f0cc9f9a..bdf04fafaae 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -205,7 +205,7 @@ RSpec.describe API::Helpers do end it 'tracks redis hll event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(value, event_name) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(event_name, values: value) subject.increment_unique_values(event_name, value) end @@ -363,4 +363,49 @@ RSpec.describe API::Helpers do end end end + + describe '#present_disk_file!' do + let_it_be(:dummy_class) do + Class.new do + attr_reader :headers + alias_method :header, :headers + + def initialize + @headers = {} + end + end + end + + let(:dummy_instance) { dummy_class.include(described_class).new } + let(:path) { '/tmp/file.txt' } + let(:filename) { 'file.txt' } + + subject { dummy_instance.present_disk_file!(path, filename) } + + before do + expect(dummy_instance).to receive(:content_type).with('application/octet-stream') + end + + context 'with X-Sendfile supported' do + before do + dummy_instance.headers['X-Sendfile-Type'] = 'X-Sendfile' + end + + it 'sends the file using X-Sendfile' do + expect(dummy_instance).to receive(:body).with('') + + subject + + expect(dummy_instance.headers['X-Sendfile']).to eq(path) + end + end + + context 'without X-Sendfile supported' do + it 'sends the file' do + expect(dummy_instance).to receive(:sendfile).with(path) + + subject + end + end + end end diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb index 6a161854dfb..21ee40f22fe 100644 --- a/spec/lib/atlassian/jira_connect/client_spec.rb +++ b/spec/lib/atlassian/jira_connect/client_spec.rb @@ -8,6 +8,15 @@ RSpec.describe Atlassian::JiraConnect::Client do subject { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') } let_it_be(:project) { create_default(:project, :repository) } + let_it_be(:mrs_by_title) { create_list(:merge_request, 4, :unique_branches, :jira_title) } + let_it_be(:mrs_by_branch) { create_list(:merge_request, 2, :jira_branch) } + let_it_be(:red_herrings) { create_list(:merge_request, 1, :unique_branches) } + + let_it_be(:pipelines) do + (red_herrings + mrs_by_branch + mrs_by_title).map do |mr| + create(:ci_pipeline, merge_request: mr) + end + end around do |example| freeze_time { example.run } @@ -22,13 +31,25 @@ RSpec.describe Atlassian::JiraConnect::Client do end describe '#send_info' do - it 'calls store_build_info and store_dev_info as appropriate' do + it 'calls more specific methods as appropriate' do + expect(subject).to receive(:store_ff_info).with( + project: project, + update_sequence_id: :x, + feature_flags: :r + ).and_return(:ff_stored) + expect(subject).to receive(:store_build_info).with( project: project, update_sequence_id: :x, pipelines: :y ).and_return(:build_stored) + expect(subject).to receive(:store_deploy_info).with( + project: project, + update_sequence_id: :x, + deployments: :q + ).and_return(:deploys_stored) + expect(subject).to receive(:store_dev_info).with( project: project, update_sequence_id: :x, @@ -43,10 +64,13 @@ RSpec.describe Atlassian::JiraConnect::Client do commits: :a, branches: :b, merge_requests: :c, - pipelines: :y + pipelines: :y, + deployments: :q, + feature_flags: :r } - expect(subject.send_info(**args)).to contain_exactly(:dev_stored, :build_stored) + expect(subject.send_info(**args)) + .to contain_exactly(:dev_stored, :build_stored, :deploys_stored, :ff_stored) end it 'only calls methods that we need to call' do @@ -83,31 +107,263 @@ RSpec.describe Atlassian::JiraConnect::Client do } end - describe '#store_build_info' do - let_it_be(:mrs_by_title) { create_list(:merge_request, 4, :unique_branches, :jira_title) } - let_it_be(:mrs_by_branch) { create_list(:merge_request, 2, :jira_branch) } - let_it_be(:red_herrings) { create_list(:merge_request, 1, :unique_branches) } + describe '#handle_response' do + let(:errors) { [{ 'message' => 'X' }, { 'message' => 'Y' }] } + let(:processed) { subject.send(:handle_response, response, 'foo') { |x| [:data, x] } } + + context 'the response is 200 OK' do + let(:response) { double(code: 200, parsed_response: :foo) } + + it 'yields to the block' do + expect(processed).to eq [:data, :foo] + end + end + + context 'the response is 400 bad request' do + let(:response) { double(code: 400, parsed_response: errors) } + + it 'extracts the errors messages' do + expect(processed).to eq('errorMessages' => %w(X Y)) + end + end + + context 'the response is 401 forbidden' do + let(:response) { double(code: 401, parsed_response: nil) } + + it 'reports that our JWT is wrong' do + expect(processed).to eq('errorMessages' => ['Invalid JWT']) + end + end + + context 'the response is 403' do + let(:response) { double(code: 403, parsed_response: nil) } + + it 'reports that the App is misconfigured' do + expect(processed).to eq('errorMessages' => ['App does not support foo']) + end + end + + context 'the response is 413' do + let(:response) { double(code: 413, parsed_response: errors) } + + it 'extracts the errors messages' do + expect(processed).to eq('errorMessages' => ['Data too large', 'X', 'Y']) + end + end + + context 'the response is 429' do + let(:response) { double(code: 429, parsed_response: nil) } + + it 'reports that we exceeded the rate limit' do + expect(processed).to eq('errorMessages' => ['Rate limit exceeded']) + end + end + + context 'the response is 503' do + let(:response) { double(code: 503, parsed_response: nil) } - let_it_be(:pipelines) do - (red_herrings + mrs_by_branch + mrs_by_title).map do |mr| - create(:ci_pipeline, merge_request: mr) + it 'reports that the service is unavailable' do + expect(processed).to eq('errorMessages' => ['Service unavailable']) end end + context 'the response is anything else' do + let(:response) { double(code: 1000, parsed_response: :something) } + + it 'reports that this was unanticipated' do + expect(processed).to eq('errorMessages' => ['Unknown error'], 'response' => :something) + end + end + end + + describe '#store_deploy_info' do + let_it_be(:environment) { create(:environment, name: 'DEV', project: project) } + let_it_be(:deployments) do + pipelines.map do |p| + build = create(:ci_build, environment: environment.name, pipeline: p, project: project) + create(:deployment, deployable: build, environment: environment) + end + end + + let(:schema) do + Atlassian::Schemata.deploy_info_payload + end + + let(:body) do + matcher = be_valid_json.and match_schema(schema) + + ->(text) { matcher.matches?(text) } + end + + let(:rejections) { [] } + let(:response_body) do + { + acceptedDeployments: [], + rejectedDeployments: rejections, + unknownIssueKeys: [] + }.to_json + end + + before do + path = '/rest/deployments/0.1/bulk' + stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post) + .with(body: body, headers: expected_headers(path)) + .to_return(body: response_body, headers: { 'Content-Type': 'application/json' }) + end + + it "calls the API with auth headers" do + subject.send(:store_deploy_info, project: project, deployments: deployments) + end + + it 'only sends information about relevant MRs' do + expect(subject).to receive(:post).with('/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 6) }).and_call_original + + subject.send(:store_deploy_info, project: project, deployments: deployments) + end + + it 'does not call the API if there is nothing to report' do + expect(subject).not_to receive(:post) + + subject.send(:store_deploy_info, project: project, deployments: deployments.take(1)) + end + + context 'there are errors' do + let(:rejections) do + [{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }] + end + + it 'reports the errors' do + response = subject.send(:store_deploy_info, project: project, deployments: deployments) + + expect(response['errorMessages']).to eq(%w(X Y Z)) + end + end + + it 'does not call the API if the feature flag is not enabled' do + stub_feature_flags(jira_sync_deployments: false) + + expect(subject).not_to receive(:post) + + subject.send(:store_deploy_info, project: project, deployments: deployments) + end + + it 'does call the API if the feature flag enabled for the project' do + stub_feature_flags(jira_sync_deployments: project) + + expect(subject).to receive(:post).with('/rest/deployments/0.1/bulk', { deployments: Array }).and_call_original + + subject.send(:store_deploy_info, project: project, deployments: deployments) + end + end + + describe '#store_ff_info' do + let_it_be(:feature_flags) { create_list(:operations_feature_flag, 3, project: project) } + + let(:schema) do + Atlassian::Schemata.ff_info_payload + end + + let(:body) do + matcher = be_valid_json.and match_schema(schema) + + ->(text) { matcher.matches?(text) } + end + + let(:failures) { {} } + let(:response_body) do + { + acceptedFeatureFlags: [], + failedFeatureFlags: failures, + unknownIssueKeys: [] + }.to_json + end + + before do + feature_flags.first.update!(description: 'RELEVANT-123') + feature_flags.second.update!(description: 'RELEVANT-123') + path = '/rest/featureflags/0.1/bulk' + stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post) + .with(body: body, headers: expected_headers(path)) + .to_return(body: response_body, headers: { 'Content-Type': 'application/json' }) + end + + it "calls the API with auth headers" do + subject.send(:store_ff_info, project: project, feature_flags: feature_flags) + end + + it 'only sends information about relevant MRs' do + expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', { + flags: have_attributes(size: 2), properties: Hash + }).and_call_original + + subject.send(:store_ff_info, project: project, feature_flags: feature_flags) + end + + it 'does not call the API if there is nothing to report' do + expect(subject).not_to receive(:post) + + subject.send(:store_ff_info, project: project, feature_flags: [feature_flags.last]) + end + + context 'there are errors' do + let(:failures) do + { + a: [{ message: 'X' }, { message: 'Y' }], + b: [{ message: 'Z' }] + } + end + + it 'reports the errors' do + response = subject.send(:store_ff_info, project: project, feature_flags: feature_flags) + + expect(response['errorMessages']).to eq(['a: X', 'a: Y', 'b: Z']) + end + end + + it 'does not call the API if the feature flag is not enabled' do + stub_feature_flags(jira_sync_feature_flags: false) + + expect(subject).not_to receive(:post) + + subject.send(:store_ff_info, project: project, feature_flags: feature_flags) + end + + it 'does call the API if the feature flag enabled for the project' do + stub_feature_flags(jira_sync_feature_flags: project) + + expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', { + flags: Array, properties: Hash + }).and_call_original + + subject.send(:store_ff_info, project: project, feature_flags: feature_flags) + end + end + + describe '#store_build_info' do let(:build_info_payload_schema) do Atlassian::Schemata.build_info_payload end let(:body) do - matcher = be_valid_json.according_to_schema(build_info_payload_schema) + matcher = be_valid_json.and match_schema(build_info_payload_schema) ->(text) { matcher.matches?(text) } end + let(:failures) { [] } + let(:response_body) do + { + acceptedBuilds: [], + rejectedBuilds: failures, + unknownIssueKeys: [] + }.to_json + end + before do path = '/rest/builds/0.1/bulk' stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post) .with(body: body, headers: expected_headers(path)) + .to_return(body: response_body, headers: { 'Content-Type': 'application/json' }) end it "calls the API with auth headers" do @@ -115,7 +371,9 @@ RSpec.describe Atlassian::JiraConnect::Client do end it 'only sends information about relevant MRs' do - expect(subject).to receive(:post).with('/rest/builds/0.1/bulk', { builds: have_attributes(size: 6) }) + expect(subject).to receive(:post) + .with('/rest/builds/0.1/bulk', { builds: have_attributes(size: 6) }) + .and_call_original subject.send(:store_build_info, project: project, pipelines: pipelines) end @@ -137,12 +395,28 @@ RSpec.describe Atlassian::JiraConnect::Client do it 'does call the API if the feature flag enabled for the project' do stub_feature_flags(jira_sync_builds: project) - expect(subject).to receive(:post).with('/rest/builds/0.1/bulk', { builds: Array }) + expect(subject).to receive(:post) + .with('/rest/builds/0.1/bulk', { builds: Array }) + .and_call_original subject.send(:store_build_info, project: project, pipelines: pipelines) end + context 'there are errors' do + let(:failures) do + [{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }] + end + + it 'reports the errors' do + response = subject.send(:store_build_info, project: project, pipelines: pipelines) + + expect(response['errorMessages']).to eq(%w(X Y Z)) + end + end + it 'avoids N+1 database queries' do + pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/292818' + baseline = ActiveRecord::QueryRecorder.new do subject.send(:store_build_info, project: project, pipelines: pipelines) end diff --git a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb index 52e475d20ca..4bbd654655d 100644 --- a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb +++ b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do end it 'is invalid, since it has no issue keys' do - expect(subject.to_json).not_to be_valid_json.according_to_schema(Atlassian::Schemata.build_info) + expect(subject.to_json).not_to match_schema(Atlassian::Schemata.build_info) end end end @@ -43,7 +43,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do describe '#to_json' do it 'is valid according to the build info schema' do - expect(subject.to_json).to be_valid_json.according_to_schema(Atlassian::Schemata.build_info) + expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.build_info) end end end diff --git a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb new file mode 100644 index 00000000000..82bcbdc4561 --- /dev/null +++ b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity do + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create_default(:project, :repository) } + let_it_be(:environment) { create(:environment, name: 'prod', project: project) } + let_it_be_with_reload(:deployment) { create(:deployment, environment: environment) } + + subject { described_class.represent(deployment) } + + context 'when the deployment does not belong to any Jira issue' do + describe '#issue_keys' do + it 'is empty' do + expect(subject.issue_keys).to be_empty + end + end + + describe '#to_json' do + it 'can encode the object' do + expect(subject.to_json).to be_valid_json + end + + it 'is invalid, since it has no issue keys' do + expect(subject.to_json).not_to match_schema(Atlassian::Schemata.deployment_info) + end + end + end + + context 'this is an external deployment' do + before do + deployment.update!(deployable: nil) + end + + it 'does not raise errors when serializing' do + expect { subject.to_json }.not_to raise_error + end + + it 'returns an empty list of issue keys' do + expect(subject.issue_keys).to be_empty + end + end + + describe 'environment type' do + using RSpec::Parameterized::TableSyntax + + where(:env_name, :env_type) do + 'prod' | 'production' + 'test' | 'testing' + 'staging' | 'staging' + 'dev' | 'development' + 'review/app' | 'development' + 'something-else' | 'unmapped' + end + + with_them do + before do + environment.update!(name: env_name) + end + + let(:exposed_type) { subject.send(:environment_entity).send(:type) } + + it 'has the correct environment type' do + expect(exposed_type).to eq(env_type) + end + end + end + + context 'when the deployment can be linked to a Jira issue' do + let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) } + + before do + subject.deployable.update!(pipeline: pipeline) + end + + %i[jira_branch jira_title].each do |trait| + context "because it belongs to an MR with a #{trait}" do + let(:merge_request) { create(:merge_request, trait) } + + describe '#issue_keys' do + it 'is not empty' do + expect(subject.issue_keys).not_to be_empty + end + end + + describe '#to_json' do + it 'is valid according to the deployment info schema' do + expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info) + end + end + end + end + end +end diff --git a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb new file mode 100644 index 00000000000..964801338cf --- /dev/null +++ b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create_default(:project) } + + subject { described_class.represent(feature_flag) } + + context 'when the feature flag does not belong to any Jira issue' do + let_it_be(:feature_flag) { create(:operations_feature_flag) } + + describe '#issue_keys' do + it 'is empty' do + expect(subject.issue_keys).to be_empty + end + end + + describe '#to_json' do + it 'can encode the object' do + expect(subject.to_json).to be_valid_json + end + + it 'is invalid, since it has no issue keys' do + expect(subject.to_json).not_to match_schema(Atlassian::Schemata.feature_flag_info) + end + end + end + + context 'when the feature flag does belong to a Jira issue' do + let(:feature_flag) do + create(:operations_feature_flag, description: 'THING-123') + end + + describe '#issue_keys' do + it 'is not empty' do + expect(subject.issue_keys).not_to be_empty + end + end + + describe '#to_json' do + it 'is valid according to the feature flag info schema' do + expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.feature_flag_info) + end + end + + context 'it has a percentage strategy' do + let!(:scopes) do + strat = create(:operations_strategy, + feature_flag: feature_flag, + name: ::Operations::FeatureFlags::Strategy::STRATEGY_GRADUALROLLOUTUSERID, + parameters: { 'percentage' => '50', 'groupId' => 'abcde' }) + + [ + create(:operations_scope, strategy: strat, environment_scope: 'production in live'), + create(:operations_scope, strategy: strat, environment_scope: 'staging'), + create(:operations_scope, strategy: strat) + ] + end + + let(:entity) { Gitlab::Json.parse(subject.to_json) } + + it 'is valid according to the feature flag info schema' do + expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.feature_flag_info) + end + + it 'has the correct summary' do + expect(entity.dig('summary', 'status')).to eq( + 'enabled' => true, + 'defaultValue' => '', + 'rollout' => { 'percentage' => 50.0, 'text' => 'Percent of users' } + ) + end + + it 'includes the correct environments' do + expect(entity['details']).to contain_exactly( + include('environment' => { 'name' => 'production in live', 'type' => 'production' }), + include('environment' => { 'name' => 'staging', 'type' => 'staging' }), + include('environment' => { 'name' => scopes.last.environment_scope }) + ) + end + end + end +end diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb index 2a4ee28130b..1f886059bf6 100644 --- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb +++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb @@ -35,8 +35,8 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do expect(Gitlab.config.asset_proxy.enabled).to be_truthy expect(Gitlab.config.asset_proxy.secret_key).to eq 'shared-secret' expect(Gitlab.config.asset_proxy.url).to eq 'https://assets.example.com' - expect(Gitlab.config.asset_proxy.whitelist).to eq %w(gitlab.com *.mydomain.com) - expect(Gitlab.config.asset_proxy.domain_regexp).to eq /^(gitlab\.com|.*?\.mydomain\.com)$/i + expect(Gitlab.config.asset_proxy.allowlist).to eq %w(gitlab.com *.mydomain.com) + expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i) end context 'when whitelist is empty' do @@ -46,7 +46,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do described_class.initialize_settings - expect(Gitlab.config.asset_proxy.whitelist).to eq [Gitlab.config.gitlab.host] + expect(Gitlab.config.asset_proxy.allowlist).to eq [Gitlab.config.gitlab.host] end end end @@ -56,8 +56,8 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do stub_asset_proxy_setting(enabled: true) stub_asset_proxy_setting(secret_key: 'shared-secret') stub_asset_proxy_setting(url: 'https://assets.example.com') - stub_asset_proxy_setting(whitelist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host})) - stub_asset_proxy_setting(domain_regexp: described_class.compile_whitelist(Gitlab.config.asset_proxy.whitelist)) + stub_asset_proxy_setting(allowlist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host})) + stub_asset_proxy_setting(domain_regexp: described_class.compile_allowlist(Gitlab.config.asset_proxy.allowlist)) @context = described_class.transform_context({}) end diff --git a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb index 1f65268bd3c..67b480f8973 100644 --- a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter do include FilterSpecHelper - it_behaves_like 'default whitelist' + it_behaves_like 'default allowlist' - describe 'custom whitelist' do + describe 'custom allowlist' do it_behaves_like 'XSS prevention' it_behaves_like 'sanitize link' @@ -26,19 +26,19 @@ RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter do end context 'when `a` elements have `style` attribute' do - let(:whitelisted_style) { 'color: red; border: blue; background: green; padding: 10px; margin: 10px; text-decoration: underline;' } + let(:allowed_style) { 'color: red; border: blue; background: green; padding: 10px; margin: 10px; text-decoration: underline;' } context 'allows specific properties' do - let(:exp) { %{Stylish Link} } + let(:exp) { %{Stylish Link} } it { is_expected.to eq(exp) } end it 'disallows other properties in `style` attribute on `a` elements' do - style = [whitelisted_style, 'position: fixed'].join(';') + style = [allowed_style, 'position: fixed'].join(';') doc = filter(%{Stylish Link}) - expect(doc.at_css('a')['style']).to eq(whitelisted_style) + expect(doc.at_css('a')['style']).to eq(allowed_style) end end diff --git a/spec/lib/banzai/filter/reference_redactor_filter_spec.rb b/spec/lib/banzai/filter/reference_redactor_filter_spec.rb index ac1cabb34cc..d0336e9e059 100644 --- a/spec/lib/banzai/filter/reference_redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/reference_redactor_filter_spec.rb @@ -143,15 +143,32 @@ RSpec.describe Banzai::Filter::ReferenceRedactorFilter do expect(doc.css('a').length).to eq 1 end - it 'allows references for admin' do - admin = create(:admin) - project = create(:project, :public) - issue = create(:issue, :confidential, project: project) - link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') + context 'for admin' do + context 'when admin mode is enabled', :enable_admin_mode do + it 'allows references' do + admin = create(:admin) + project = create(:project, :public) + issue = create(:issue, :confidential, project: project) + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') + + doc = filter(link, current_user: admin) + + expect(doc.css('a').length).to eq 1 + end + end - doc = filter(link, current_user: admin) + context 'when admin mode is disabled' do + it 'removes references' do + admin = create(:admin) + project = create(:project, :public) + issue = create(:issue, :confidential, project: project) + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') - expect(doc.css('a').length).to eq 1 + doc = filter(link, current_user: admin) + + expect(doc.css('a').length).to eq 0 + end + end end context "when a confidential issue is moved from a public project to a private one" do diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 09dcd5518ff..bc4b60dfe60 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -5,31 +5,31 @@ require 'spec_helper' RSpec.describe Banzai::Filter::SanitizationFilter do include FilterSpecHelper - it_behaves_like 'default whitelist' + it_behaves_like 'default allowlist' - describe 'custom whitelist' do + describe 'custom allowlist' do it_behaves_like 'XSS prevention' it_behaves_like 'sanitize link' - it 'customizes the whitelist only once' do + it 'customizes the allowlist only once' do instance = described_class.new('Foo') - control_count = instance.whitelist[:transformers].size + control_count = instance.allowlist[:transformers].size - 3.times { instance.whitelist } + 3.times { instance.allowlist } - expect(instance.whitelist[:transformers].size).to eq control_count + expect(instance.allowlist[:transformers].size).to eq control_count end - it 'customizes the whitelist only once for different instances' do + it 'customizes the allowlist only once for different instances' do instance1 = described_class.new('Foo1') instance2 = described_class.new('Foo2') - control_count = instance1.whitelist[:transformers].size + control_count = instance1.allowlist[:transformers].size - instance1.whitelist - instance2.whitelist + instance1.allowlist + instance2.allowlist - expect(instance1.whitelist[:transformers].size).to eq control_count - expect(instance2.whitelist[:transformers].size).to eq control_count + expect(instance1.allowlist[:transformers].size).to eq control_count + expect(instance2.allowlist[:transformers].size).to eq control_count end it 'sanitizes `class` attribute from all elements' do diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb new file mode 100644 index 00000000000..b0c6d91daa8 --- /dev/null +++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::TruncateSourceFilter do + include FilterSpecHelper + + let(:short_text) { 'foo' * 10 } + let(:long_text) { ([short_text] * 10).join(' ') } + + it 'does nothing when limit is unspecified' do + output = filter(long_text) + + expect(output).to eq(long_text) + end + + it 'does nothing to a short-enough text' do + output = filter(short_text, limit: short_text.bytesize) + + expect(output).to eq(short_text) + end + + it 'truncates UTF-8 text by bytes, on a character boundary' do + utf8_text = '日本語の文字が大きい' + truncated = '日…' + + expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated) + expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text) + expect(filter(utf8_text, limit: utf8_text.mb_chars.size)).not_to eq(utf8_text) + end +end diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb index 82d4f883e0d..be553433e9e 100644 --- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Banzai::Pipeline::DescriptionPipeline do stub_commonmark_sourcepos_disabled end - it 'uses a limited whitelist' do + it 'uses a limited allowlist' do doc = parse('# Description') expect(doc.strip).to eq 'Description' diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index 247f4591632..31047b9494a 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -176,8 +176,8 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do stub_asset_proxy_setting(enabled: true) stub_asset_proxy_setting(secret_key: 'shared-secret') stub_asset_proxy_setting(url: 'https://assets.example.com') - stub_asset_proxy_setting(whitelist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host})) - stub_asset_proxy_setting(domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist)) + stub_asset_proxy_setting(allowlist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host})) + stub_asset_proxy_setting(domain_regexp: Banzai::Filter::AssetProxyFilter.compile_allowlist(Gitlab.config.asset_proxy.allowlist)) end it 'replaces a lazy loaded img src' do diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb index fc74c592867..f0498f41b61 100644 --- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb @@ -24,4 +24,12 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do expect(result[:output]).to include "> blockquote\n" end end + + it 'truncates the text if requested' do + text = (['foo'] * 10).join(' ') + + result = described_class.call(text, limit: 12) + + expect(result[:output]).to eq('foo foo f…') + end end diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb index a7a19fb73fc..2abd3df20fd 100644 --- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb +++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb @@ -27,11 +27,8 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do allow(graphql_client).to receive(:execute).and_return(response) end - it 'returns an enumerator with fetched results' do - response = subject.extract(context) - - expect(response).to be_instance_of(Enumerator) - expect(response.first).to eq({ foo: :bar }) + it 'returns original hash' do + expect(subject.extract(context)).to eq({ foo: :bar }) end end diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb index c9b481388c3..1a91f3d7a78 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -75,13 +75,11 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } it 'has extractors' do - expect(described_class.extractors) - .to contain_exactly( - { - klass: BulkImports::Common::Extractors::GraphqlExtractor, - options: { - query: BulkImports::Groups::Graphql::GetGroupQuery - } + expect(described_class.get_extractor) + .to eq( + klass: BulkImports::Common::Extractors::GraphqlExtractor, + options: { + query: BulkImports::Groups::Graphql::GetGroupQuery } ) end @@ -97,9 +95,7 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do end it 'has loaders' do - expect(described_class.loaders).to contain_exactly({ - klass: BulkImports::Groups::Loaders::GroupLoader, options: nil - }) + expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::GroupLoader, options: nil) end end end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index 788a6e98c45..e5a8ed7f47d 100644 --- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -58,10 +58,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } it 'has extractors' do - expect(described_class.extractors).to contain_exactly( - klass: BulkImports::Groups::Extractors::SubgroupsExtractor, - options: nil - ) + expect(described_class.get_extractor).to eq(klass: BulkImports::Groups::Extractors::SubgroupsExtractor, options: nil) end it 'has transformers' do @@ -72,10 +69,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do end it 'has loaders' do - expect(described_class.loaders).to contain_exactly( - klass: BulkImports::Common::Loaders::EntityLoader, - options: nil - ) + expect(described_class.get_loader).to eq(klass: BulkImports::Common::Loaders::EntityLoader, options: nil) end end end diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb index 95dca7fc486..87baf1b8026 100644 --- a/spec/lib/bulk_imports/importers/group_importer_spec.rb +++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe BulkImports::Importers::GroupImporter do let(:user) { create(:user) } let(:bulk_import) { create(:bulk_import) } - let(:bulk_import_entity) { create(:bulk_import_entity, bulk_import: bulk_import) } + let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) } let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } let(:context) do BulkImports::Pipeline::Context.new( @@ -18,14 +18,13 @@ RSpec.describe BulkImports::Importers::GroupImporter do subject { described_class.new(bulk_import_entity) } before do - allow(Gitlab).to receive(:ee?).and_return(false) allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context) end describe '#execute' do it 'starts the entity and run its pipelines' do - expect(bulk_import_entity).to receive(:start).and_call_original expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context + expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee? expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context subject.execute diff --git a/spec/lib/bulk_imports/importers/groups_importer_spec.rb b/spec/lib/bulk_imports/importers/groups_importer_spec.rb deleted file mode 100644 index 4865034b0cd..00000000000 --- a/spec/lib/bulk_imports/importers/groups_importer_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Importers::GroupsImporter do - let_it_be(:bulk_import) { create(:bulk_import) } - - subject { described_class.new(bulk_import.id) } - - describe '#execute' do - context "when there is entities to be imported" do - let!(:bulk_import_entity) { create(:bulk_import_entity, bulk_import: bulk_import) } - - it "starts the bulk_import and imports its entities" do - expect(BulkImports::Importers::GroupImporter).to receive(:new) - .with(bulk_import_entity).and_return(double(execute: true)) - expect(BulkImportWorker).to receive(:perform_async).with(bulk_import.id) - - subject.execute - - expect(bulk_import.reload).to be_started - end - end - - context "when there is no entities to be imported" do - it "starts the bulk_import and imports its entities" do - expect(BulkImports::Importers::GroupImporter).not_to receive(:new) - expect(BulkImportWorker).not_to receive(:perform_async) - - subject.execute - - expect(bulk_import.reload).to be_finished - end - end - end -end diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb index 94052be7df2..3811a02a7fd 100644 --- a/spec/lib/bulk_imports/pipeline_spec.rb +++ b/spec/lib/bulk_imports/pipeline_spec.rb @@ -24,9 +24,9 @@ RSpec.describe BulkImports::Pipeline do describe 'getters' do it 'retrieves class attributes' do - expect(BulkImports::MyPipeline.extractors).to contain_exactly({ klass: BulkImports::Extractor, options: { foo: :bar } }) + expect(BulkImports::MyPipeline.get_extractor).to eq({ klass: BulkImports::Extractor, options: { foo: :bar } }) expect(BulkImports::MyPipeline.transformers).to contain_exactly({ klass: BulkImports::Transformer, options: { foo: :bar } }) - expect(BulkImports::MyPipeline.loaders).to contain_exactly({ klass: BulkImports::Loader, options: { foo: :bar } }) + expect(BulkImports::MyPipeline.get_loader).to eq({ klass: BulkImports::Loader, options: { foo: :bar } }) expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true) end end @@ -41,20 +41,14 @@ RSpec.describe BulkImports::Pipeline do BulkImports::MyPipeline.loader(klass, options) BulkImports::MyPipeline.abort_on_failure! - expect(BulkImports::MyPipeline.extractors) - .to contain_exactly( - { klass: BulkImports::Extractor, options: { foo: :bar } }, - { klass: klass, options: options }) + expect(BulkImports::MyPipeline.get_extractor).to eq({ klass: klass, options: options }) expect(BulkImports::MyPipeline.transformers) .to contain_exactly( { klass: BulkImports::Transformer, options: { foo: :bar } }, { klass: klass, options: options }) - expect(BulkImports::MyPipeline.loaders) - .to contain_exactly( - { klass: BulkImports::Loader, options: { foo: :bar } }, - { klass: klass, options: options }) + expect(BulkImports::MyPipeline.get_loader).to eq({ klass: klass, options: options }) expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true) end diff --git a/spec/lib/constraints/admin_constrainer_spec.rb b/spec/lib/constraints/admin_constrainer_spec.rb index 3efe683177c..ac6ad31120e 100644 --- a/spec/lib/constraints/admin_constrainer_spec.rb +++ b/spec/lib/constraints/admin_constrainer_spec.rb @@ -2,7 +2,7 @@ # require 'spec_helper' -RSpec.describe Constraints::AdminConstrainer, :do_not_mock_admin_mode do +RSpec.describe Constraints::AdminConstrainer do let(:user) { create(:user) } let(:session) { {} } diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index 2c08fdc1e75..9d6f4db537d 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -26,7 +26,54 @@ RSpec.describe ContainerRegistry::Client do } end - shared_examples '#repository_manifest' do |manifest_type| + let(:expected_faraday_headers) { { user_agent: "GitLab/#{Gitlab::VERSION}" } } + let(:expected_faraday_request_options) { Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS } + + shared_examples 'handling timeouts' do + let(:retry_options) do + ContainerRegistry::Client::RETRY_OPTIONS.merge( + interval: 0.1, + interval_randomness: 0, + backoff_factor: 0 + ) + end + + before do + stub_request(method, url).to_timeout + end + + it 'handles network timeouts' do + actual_retries = 0 + retry_options_with_block = retry_options.merge( + retry_block: -> (_, _, _, _) { actual_retries += 1 } + ) + + stub_const('ContainerRegistry::Client::RETRY_OPTIONS', retry_options_with_block) + + expect { subject }.to raise_error(Faraday::ConnectionFailed) + expect(actual_retries).to eq(retry_options_with_block[:max]) + end + + it 'logs the error' do + stub_const('ContainerRegistry::Client::RETRY_OPTIONS', retry_options) + + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .exactly(retry_options[:max] + 1) + .times + .with( + an_instance_of(Faraday::ConnectionFailed), + class: described_class.name, + url: URI(url) + ) + + expect { subject }.to raise_error(Faraday::ConnectionFailed) + end + end + + shared_examples 'handling repository manifest' do |manifest_type| + let(:method) { :get } + let(:url) { 'http://container-registry/v2/group/test/manifests/mytag' } let(:manifest) do { "schemaVersion" => 2, @@ -48,7 +95,7 @@ RSpec.describe ContainerRegistry::Client do end it 'GET /v2/:name/manifests/mytag' do - stub_request(:get, "http://container-registry/v2/group/test/manifests/mytag") + stub_request(method, url) .with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json', 'Authorization' => "bearer #{token}", @@ -56,14 +103,24 @@ RSpec.describe ContainerRegistry::Client do }) .to_return(status: 200, body: manifest.to_json, headers: { content_type: manifest_type }) - expect(client.repository_manifest('group/test', 'mytag')).to eq(manifest) + expect_new_faraday + + expect(subject).to eq(manifest) end + + it_behaves_like 'handling timeouts' end - it_behaves_like '#repository_manifest', described_class::DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE - it_behaves_like '#repository_manifest', described_class::OCI_MANIFEST_V1_TYPE + describe '#repository_manifest' do + subject { client.repository_manifest('group/test', 'mytag') } + + it_behaves_like 'handling repository manifest', described_class::DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE + it_behaves_like 'handling repository manifest', described_class::OCI_MANIFEST_V1_TYPE + end describe '#blob' do + let(:method) { :get } + let(:url) { 'http://container-registry/v2/group/test/blobs/sha256:0123456789012345' } let(:blob_headers) do { 'Accept' => 'application/octet-stream', @@ -78,16 +135,20 @@ RSpec.describe ContainerRegistry::Client do } end + subject { client.blob('group/test', 'sha256:0123456789012345') } + it 'GET /v2/:name/blobs/:digest' do - stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345") + stub_request(method, url) .with(headers: blob_headers) .to_return(status: 200, body: "Blob") - expect(client.blob('group/test', 'sha256:0123456789012345')).to eq('Blob') + expect_new_faraday + + expect(subject).to eq('Blob') end it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do - stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345") + stub_request(method, url) .with(headers: blob_headers) .to_return(status: 307, body: '', headers: { Location: 'http://redirected' }) # We should probably use hash_excluding here, but that requires an update to WebMock: @@ -98,10 +159,12 @@ RSpec.describe ContainerRegistry::Client do end .to_return(status: 200, body: "Successfully redirected") - response = client.blob('group/test', 'sha256:0123456789012345') + expect_new_faraday(times: 2) - expect(response).to eq('Successfully redirected') + expect(subject).to eq('Successfully redirected') end + + it_behaves_like 'handling timeouts' end describe '#upload_blob' do @@ -111,6 +174,8 @@ RSpec.describe ContainerRegistry::Client do it 'starts the upload and posts the blob' do stub_upload('path', 'content', 'sha256:123') + expect_new_faraday(timeout: false) + expect(subject).to be_success end end @@ -173,6 +238,8 @@ RSpec.describe ContainerRegistry::Client do .with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers) .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' }) + expect_new_faraday(timeout: false) + expect(subject).to eq 'sha256:123' end end @@ -375,4 +442,17 @@ RSpec.describe ContainerRegistry::Client do headers: { 'Allow' => 'DELETE' } ) end + + def expect_new_faraday(times: 1, timeout: true) + request_options = timeout ? expected_faraday_request_options : nil + expect(Faraday) + .to receive(:new) + .with( + 'http://container-registry', + headers: expected_faraday_headers, + request: request_options + ).and_call_original + .exactly(times) + .times + end end diff --git a/spec/lib/declarative_enum_spec.rb b/spec/lib/declarative_enum_spec.rb new file mode 100644 index 00000000000..66cda9fc3a8 --- /dev/null +++ b/spec/lib/declarative_enum_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DeclarativeEnum do + let(:enum_module) do + Module.new do + extend DeclarativeEnum + + key :my_enum + name 'MyEnumName' + + description "Enum description" + + define do + foo value: 0, description: 'description of foo' + bar value: 1, description: 'description of bar' + end + end + end + + let(:original_definition) do + { + foo: { description: 'description of foo', value: 0 }, + bar: { description: 'description of bar', value: 1 } + } + end + + describe '.key' do + subject(:key) { enum_module.key(new_key) } + + context 'when the argument is set' do + let(:new_key) { :new_enum_key } + + it 'changes the key' do + expect { key }.to change { enum_module.key }.from(:my_enum).to(:new_enum_key) + end + end + + context 'when the argument is `nil`' do + let(:new_key) { nil } + + it { is_expected.to eq(:my_enum) } + end + end + + describe '.name' do + subject(:name) { enum_module.name(new_name) } + + context 'when the argument is set' do + let(:new_name) { 'NewMyEnumName' } + + it 'changes the name' do + expect { name }.to change { enum_module.name }.from('MyEnumName').to('NewMyEnumName') + end + end + + context 'when the argument is `nil`' do + let(:new_name) { nil } + + it { is_expected.to eq('MyEnumName') } + end + end + + describe '.description' do + subject(:description) { enum_module.description(new_description) } + + context 'when the argument is set' do + let(:new_description) { 'New enum description' } + + it 'changes the description' do + expect { description }.to change { enum_module.description }.from('Enum description').to('New enum description') + end + end + + context 'when the argument is `nil`' do + let(:new_description) { nil } + + it { is_expected.to eq('Enum description') } + end + end + + describe '.define' do + subject(:define) { enum_module.define(&block) } + + context 'when there is a block given' do + context 'when the given block tries to register the same key' do + let(:block) do + proc do + foo value: 2, description: 'description of foo' + end + end + + it 'raises a `KeyCollisionError`' do + expect { define }.to raise_error(DeclarativeEnum::Builder::KeyCollisionError) + end + end + + context 'when the given block does not try to register the same key' do + let(:expected_new_definition) { original_definition.merge(zoo: { description: 'description of zoo', value: 0 }) } + let(:block) do + proc do + zoo value: 0, description: 'description of zoo' + end + end + + it 'appends the new definition' do + expect { define }.to change { enum_module.definition }.from(original_definition).to(expected_new_definition) + end + end + end + + context 'when there is no block given' do + let(:block) { nil } + + it 'raises a LocalJumpError' do + expect { define }.to raise_error(LocalJumpError) + end + end + end + + describe '.definition' do + subject { enum_module.definition } + + it { is_expected.to eq(original_definition) } + end + + describe 'extending the enum module' do + let(:extended_definition) { original_definition.merge(zoo: { value: 2, description: 'description of zoo' }) } + let(:new_enum_module) do + Module.new do + extend DeclarativeEnum + + define do + zoo value: 2, description: 'description of zoo' + end + end + end + + subject(:prepend_new_enum_module) { enum_module.prepend(new_enum_module) } + + it 'extends the values of the base enum module' do + expect { prepend_new_enum_module }.to change { enum_module.definition }.from(original_definition) + .to(extended_definition) + end + end +end diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index a994b4b92a6..b603325cdb8 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -224,4 +224,41 @@ RSpec.describe ExpandVariables do end end end + + describe '#possible_var_reference?' do + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty value": { + value: '', + result: false + }, + "normal value": { + value: 'some value', + result: false + }, + "simple expansions": { + value: 'key$variable', + result: true + }, + "complex expansions": { + value: 'key${variable}${variable2}', + result: true + }, + "complex expansions for Windows": { + value: 'key%variable%%variable2%', + result: true + } + } + end + + with_them do + subject { ExpandVariables.possible_var_reference?(value) } + + it { is_expected.to eq(result) } + end + end + end end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_deployed_to_production_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_deployed_to_production_spec.rb new file mode 100644 index 00000000000..93e588675d3 --- /dev/null +++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_deployed_to_production_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::IssueDeployedToProduction do + it_behaves_like 'value stream analytics event' +end diff --git a/spec/lib/gitlab/api_authentication/builder_spec.rb b/spec/lib/gitlab/api_authentication/builder_spec.rb new file mode 100644 index 00000000000..e241aa77805 --- /dev/null +++ b/spec/lib/gitlab/api_authentication/builder_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::APIAuthentication::Builder do + describe '#build' do + shared_examples 'builds the correct result' do |token_type:, sent_through:, builds:| + context "with #{token_type.size} token type(s) and #{sent_through.size} sent through(s)" do + it 'works when passed together' do + strategies = described_class.new.build { |allow| allow.token_types(*token_type).sent_through(*sent_through) } + + expect(strategies).to eq(builds) + end + + it 'works when token types are passed separately' do + strategies = described_class.new.build { |allow| token_type.each { |t| allow.token_types(t).sent_through(*sent_through) } } + + expect(strategies).to eq(builds) + end + + it 'works when sent throughs are passed separately' do + strategies = described_class.new.build { |allow| sent_through.each { |s| allow.token_types(*token_type).sent_through(s) } } + + expect(strategies).to eq(builds) + end + + it 'works when token types and sent throughs are passed separately' do + strategies = described_class.new.build { |allow| token_type.each { |t| sent_through.each { |s| allow.token_types(t).sent_through(s) } } } + + expect(strategies).to eq(builds) + end + end + end + + it_behaves_like 'builds the correct result', + token_type: [:pat], + sent_through: [:basic], + builds: { basic: [:pat] } + + it_behaves_like 'builds the correct result', + token_type: [:pat], + sent_through: [:basic, :oauth], + builds: { basic: [:pat], oauth: [:pat] } + + it_behaves_like 'builds the correct result', + token_type: [:pat, :job], + sent_through: [:basic], + builds: { basic: [:pat, :job] } + + it_behaves_like 'builds the correct result', + token_type: [:pat, :job], + sent_through: [:basic, :oauth], + builds: { basic: [:pat, :job], oauth: [:pat, :job] } + + context 'with a complex auth strategy' do + it 'builds the correct result' do + strategies = described_class.new.build do |allow| + allow.token_types(:pat, :job, :deploy).sent_through(:http_basic, :oauth) + allow.token_types(:pat).sent_through(:http_private, :query_private) + allow.token_types(:oauth2).sent_through(:http_bearer, :query_access) + end + + expect(strategies).to eq({ + http_basic: [:pat, :job, :deploy], + oauth: [:pat, :job, :deploy], + + http_private: [:pat], + query_private: [:pat], + + http_bearer: [:oauth2], + query_access: [:oauth2] + }) + end + end + end +end diff --git a/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb b/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb new file mode 100644 index 00000000000..845e317f3aa --- /dev/null +++ b/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::APIAuthentication::SentThroughBuilder do + describe '#sent_through' do + let(:resolvers) { Array.new(3) { double } } + let(:locators) { Array.new(3) { double } } + + it 'adds a strategy for each of locators x resolvers' do + strategies = locators.to_h { |l| [l, []] } + described_class.new(strategies, resolvers).sent_through(*locators) + + expect(strategies).to eq(locators.to_h { |l| [l, resolvers] }) + end + end +end diff --git a/spec/lib/gitlab/api_authentication/token_locator_spec.rb b/spec/lib/gitlab/api_authentication/token_locator_spec.rb new file mode 100644 index 00000000000..68ce48a70ea --- /dev/null +++ b/spec/lib/gitlab/api_authentication/token_locator_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::APIAuthentication::TokenLocator do + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:ci_job) { create(:ci_build, project: project, user: user, status: :running) } + let_it_be(:ci_job_done) { create(:ci_build, project: project, user: user, status: :success) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + + describe '.new' do + context 'with a valid type' do + it 'creates a new instance' do + expect(described_class.new(:http_basic_auth)).to be_a(described_class) + end + end + + context 'with an invalid type' do + it 'raises ActiveModel::ValidationError' do + expect { described_class.new(:not_a_real_locator) }.to raise_error(ActiveModel::ValidationError) + end + end + end + + describe '#extract' do + let(:locator) { described_class.new(type) } + + subject { locator.extract(request) } + + context 'with :http_basic_auth' do + let(:type) { :http_basic_auth } + + context 'without credentials' do + let(:request) { double(authorization: nil) } + + it 'returns nil' do + expect(subject).to be(nil) + end + end + + context 'with credentials' do + let(:username) { 'foo' } + let(:password) { 'bar' } + let(:request) { double(authorization: "Basic #{::Base64.strict_encode64("#{username}:#{password}")}") } + + it 'returns the credentials' do + expect(subject.username).to eq(username) + expect(subject.password).to eq(password) + end + end + end + end +end diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb new file mode 100644 index 00000000000..0028fb080ac --- /dev/null +++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::APIAuthentication::TokenResolver do + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:ci_job) { create(:ci_build, project: project, user: user, status: :running) } + let_it_be(:ci_job_done) { create(:ci_build, project: project, user: user, status: :success) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + + shared_examples 'an authorized request' do + it 'returns the correct token' do + expect(subject).to eq(token) + end + end + + shared_examples 'an unauthorized request' do + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + shared_examples 'an anoymous request' do + it 'returns nil' do + expect(subject).to eq(nil) + end + end + + describe '.new' do + context 'with a valid type' do + it 'creates a new instance' do + expect(described_class.new(:personal_access_token)).to be_a(described_class) + end + end + + context 'with an invalid type' do + it 'raises a validation error' do + expect { described_class.new(:not_a_real_locator) }.to raise_error(ActiveModel::ValidationError) + end + end + end + + describe '#resolve' do + let(:resolver) { described_class.new(type) } + + subject { resolver.resolve(raw) } + + context 'with :personal_access_token' do + let(:type) { :personal_access_token } + let(:token) { personal_access_token } + + context 'with valid credentials' do + let(:raw) { username_and_password(user.username, token.token) } + + it_behaves_like 'an authorized request' + end + + context 'with an invalid username' do + let(:raw) { username_and_password("not-my-#{user.username}", token.token) } + + it_behaves_like 'an unauthorized request' + end + end + + context 'with :job_token' do + let(:type) { :job_token } + let(:token) { ci_job } + + context 'with valid credentials' do + let(:raw) { username_and_password(Gitlab::Auth::CI_JOB_USER, token.token) } + + it_behaves_like 'an authorized request' + end + + context 'when the job is not running' do + let(:raw) { username_and_password(Gitlab::Auth::CI_JOB_USER, ci_job_done.token) } + + it_behaves_like 'an unauthorized request' + end + + context 'with the wrong username' do + let(:raw) { username_and_password("not-#{Gitlab::Auth::CI_JOB_USER}", nil) } + + it_behaves_like 'an anoymous request' + end + + context 'with an invalid job token' do + let(:raw) { username_and_password(Gitlab::Auth::CI_JOB_USER, "not a valid CI job token") } + + it_behaves_like 'an unauthorized request' + end + end + + context 'with :deploy_token' do + let(:type) { :deploy_token } + let(:token) { deploy_token } + + context 'with a valid deploy token' do + let(:raw) { username_and_password(token.username, token.token) } + + it_behaves_like 'an authorized request' + end + + context 'with an invalid username' do + let(:raw) { username_and_password("not-my-#{token.username}", token.token) } + + it_behaves_like 'an unauthorized request' + end + end + end + + def username_and_password(username, password) + ::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword.new(username, password) + end +end diff --git a/spec/lib/gitlab/api_authentication/token_type_builder_spec.rb b/spec/lib/gitlab/api_authentication/token_type_builder_spec.rb new file mode 100644 index 00000000000..fbca62c9a42 --- /dev/null +++ b/spec/lib/gitlab/api_authentication/token_type_builder_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::APIAuthentication::TokenTypeBuilder do + describe '#token_types' do + it 'passes strategies and resolvers to SentThroughBuilder' do + strategies = double + resolvers = Array.new(3) { double } + retval = double + expect(Gitlab::APIAuthentication::SentThroughBuilder).to receive(:new).with(strategies, resolvers).and_return(retval) + + expect(described_class.new(strategies).token_types(*resolvers)).to be(retval) + end + end +end diff --git a/spec/lib/gitlab/asset_proxy_spec.rb b/spec/lib/gitlab/asset_proxy_spec.rb index 73b101c0dd8..7d7952d5741 100644 --- a/spec/lib/gitlab/asset_proxy_spec.rb +++ b/spec/lib/gitlab/asset_proxy_spec.rb @@ -17,12 +17,12 @@ RSpec.describe Gitlab::AssetProxy do context 'when asset proxy is enabled' do before do - stub_asset_proxy_setting(whitelist: %w(gitlab.com *.mydomain.com)) + stub_asset_proxy_setting(allowlist: %w(gitlab.com *.mydomain.com)) stub_asset_proxy_setting( enabled: true, url: 'https://assets.example.com', secret_key: 'shared-secret', - domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist) + domain_regexp: Banzai::Filter::AssetProxyFilter.compile_allowlist(Gitlab.config.asset_proxy.allowlist) ) end diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index f927d5912bb..775f8f056b5 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do include described_class include HttpBasicAuthHelpers - let(:user) { create(:user) } + # Create the feed_token and static_object_token for the user + let_it_be(:user) { create(:user).tap(&:feed_token).tap(&:static_object_token) } let(:env) do { 'rack.input' => '' @@ -65,7 +66,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_bearer_token' do - let(:job) { create(:ci_build, user: user) } + let_it_be_with_reload(:job) { create(:ci_build, user: user) } subject { find_user_from_bearer_token } @@ -91,7 +92,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'with a personal access token' do - let(:pat) { create(:personal_access_token, user: user) } + let_it_be(:pat) { create(:personal_access_token, user: user) } let(:token) { pat.token } before do @@ -148,7 +149,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end it 'returns nil if valid feed_token and disabled' do - allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true) + stub_application_setting(disable_feed_token: true) set_param(:feed_token, user.feed_token) expect(find_user_from_feed_token(:rss)).to be_nil @@ -166,7 +167,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'when rss_token param is provided' do - it 'returns user if valid rssd_token' do + it 'returns user if valid rss_token' do set_param(:rss_token, user.feed_token) expect(find_user_from_feed_token(:rss)).to eq user @@ -347,7 +348,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_access_token' do - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } before do set_header('SCRIPT_NAME', 'url.atom') @@ -386,7 +387,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'when using a non-prefixed access token' do - let(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) } it 'returns user' do set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}") @@ -398,7 +399,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_web_access_token' do - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) } before do set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token) @@ -449,6 +450,22 @@ RSpec.describe Gitlab::Auth::AuthFinders do expect(find_user_from_web_access_token(:api)).to be_nil end + context 'when the token has read_api scope' do + before do + personal_access_token.update!(scopes: ['read_api']) + + set_header('SCRIPT_NAME', '/api/endpoint') + end + + it 'raises InsufficientScopeError by default' do + expect { find_user_from_web_access_token(:api) }.to raise_error(Gitlab::Auth::InsufficientScopeError) + end + + it 'finds the user when the read_api scope is passed' do + expect(find_user_from_web_access_token(:api, scopes: [:api, :read_api])).to eq(user) + end + end + context 'when relative_url_root is set' do before do stub_config_setting(relative_url_root: '/relative_root') @@ -464,7 +481,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_personal_access_token' do - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } before do set_header('SCRIPT_NAME', 'url.atom') @@ -534,7 +551,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'access token is valid' do - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let(:route_authentication_setting) { { basic_auth_personal_access_token: true } } it 'finds the token from basic auth' do @@ -555,7 +572,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'route_setting is not set' do - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } it 'returns nil' do auth_header_with(personal_access_token.token) @@ -565,7 +582,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'route_setting is not correct' do - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let(:route_authentication_setting) { { basic_auth_personal_access_token: false } } it 'returns nil' do @@ -611,8 +628,9 @@ RSpec.describe Gitlab::Auth::AuthFinders do context 'with CI username' do let(:username) { ::Gitlab::Auth::CI_JOB_USER } - let(:user) { create(:user) } - let(:build) { create(:ci_build, user: user, status: :running) } + + let_it_be(:user) { create(:user) } + let_it_be(:build) { create(:ci_build, user: user, status: :running) } it 'returns nil without password' do set_basic_auth_header(username, nil) @@ -645,11 +663,11 @@ RSpec.describe Gitlab::Auth::AuthFinders do describe '#validate_access_token!' do subject { validate_access_token! } - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) } context 'with a job token' do + let_it_be(:job) { create(:ci_build, user: user, status: :running) } let(:route_authentication_setting) { { job_token_allowed: true } } - let(:job) { create(:ci_build, user: user, status: :running) } before do env['HTTP_AUTHORIZATION'] = "Bearer #{job.token}" @@ -671,7 +689,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end it 'returns Gitlab::Auth::ExpiredError if token expired' do - personal_access_token.expires_at = 1.day.ago + personal_access_token.update!(expires_at: 1.day.ago) expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError) end @@ -688,7 +706,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'with impersonation token' do - let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) } context 'when impersonation is disabled' do before do @@ -704,7 +722,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_job_token' do - let(:job) { create(:ci_build, user: user, status: :running) } + let_it_be(:job) { create(:ci_build, user: user, status: :running) } let(:route_authentication_setting) { { job_token_allowed: true } } subject { find_user_from_job_token } @@ -866,7 +884,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_runner_from_token' do - let(:runner) { create(:ci_runner) } + let_it_be(:runner) { create(:ci_runner) } context 'with API requests' do before do diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb index ffd7813190a..a21f0931b78 100644 --- a/spec/lib/gitlab/auth/current_user_mode_spec.rb +++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store do +RSpec.describe Gitlab::Auth::CurrentUserMode, :request_store do let(:user) { build_stubbed(:user) } subject { described_class.new(user) } diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index e4c87a54365..7a657cce597 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::Ldap::Config do include LdapHelpers + before do + stub_ldap_setting(enabled: true) + end + let(:config) { described_class.new('ldapmain') } def raw_cert @@ -68,12 +72,28 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK describe '.servers' do it 'returns empty array if no server information is available' do - allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false) + stub_ldap_setting(servers: {}) expect(described_class.servers).to eq [] end end + describe '.available_providers' do + before do + stub_licensed_features(multiple_ldap_servers: false) + stub_ldap_setting( + 'servers' => { + 'main' => { 'provider_name' => 'ldapmain' }, + 'secondary' => { 'provider_name' => 'ldapsecondary' } + } + ) + end + + it 'returns one provider' do + expect(described_class.available_providers).to match_array(%w(ldapmain)) + end + end + describe '#initialize' do it 'requires a provider' do expect { described_class.new }.to raise_error ArgumentError diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index ef6b1d72712..93e9cb06786 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -47,7 +47,10 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do let!(:job_token_user) { build(:user) } it 'returns access_token user first' do - allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token) + .with(anything, scopes: [:api, :read_api]) + .and_return(access_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) expect(subject.find_sessionless_user(:api)).to eq access_token_user diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index dfd21983682..4e4bbd1bb60 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -372,6 +372,11 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities)) end + + it 'successfully authenticates the project bot with a nil project' do + expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities)) + end end context 'with invalid project access token' do diff --git a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb new file mode 100644 index 00000000000..49fa7b41916 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20201111152859 do + subject(:perform) { migration.perform(1, 99) } + + let(:migration) { described_class.new } + let(:artifact_outside_id_range) { create_artifact!(id: 100, created_at: 1.year.ago, expire_at: nil) } + let(:artifact_outside_date_range) { create_artifact!(id: 40, created_at: Time.current, expire_at: nil) } + let(:old_artifact) { create_artifact!(id: 10, created_at: 16.months.ago, expire_at: nil) } + let(:recent_artifact) { create_artifact!(id: 20, created_at: 1.year.ago, expire_at: nil) } + let(:artifact_with_expiry) { create_artifact!(id: 30, created_at: 1.year.ago, expire_at: Time.current + 1.day) } + + before do + table(:namespaces).create!(id: 1, name: 'the-namespace', path: 'the-path') + table(:projects).create!(id: 1, name: 'the-project', namespace_id: 1) + table(:ci_builds).create!(id: 1, allow_failure: false) + end + + context 'when current date is before the 22nd' do + before do + travel_to(Time.zone.local(2020, 1, 1, 0, 0, 0)) + end + + it 'backfills the expiry date for old artifacts' do + expect(old_artifact.reload.expire_at).to eq(nil) + + perform + + expect(old_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2020, 4, 22, 0, 0, 0)) + end + + it 'backfills the expiry date for recent artifacts' do + expect(recent_artifact.reload.expire_at).to eq(nil) + + perform + + expect(recent_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2021, 1, 22, 0, 0, 0)) + end + end + + context 'when current date is after the 22nd' do + before do + travel_to(Time.zone.local(2020, 1, 23, 0, 0, 0)) + end + + it 'backfills the expiry date for old artifacts' do + expect(old_artifact.reload.expire_at).to eq(nil) + + perform + + expect(old_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2020, 5, 22, 0, 0, 0)) + end + + it 'backfills the expiry date for recent artifacts' do + expect(recent_artifact.reload.expire_at).to eq(nil) + + perform + + expect(recent_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2021, 2, 22, 0, 0, 0)) + end + end + + it 'does not touch artifacts with expiry date' do + expect { perform }.not_to change { artifact_with_expiry.reload.expire_at } + end + + it 'does not touch artifacts outside id range' do + expect { perform }.not_to change { artifact_outside_id_range.reload.expire_at } + end + + it 'does not touch artifacts outside date range' do + expect { perform }.not_to change { artifact_outside_date_range.reload.expire_at } + end + + private + + def create_artifact!(**args) + table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 1) + end +end diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb new file mode 100644 index 00000000000..110a1ff8a08 --- /dev/null +++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob do + let(:table_name) { :copy_primary_key_test } + let(:test_table) { table(table_name) } + let(:sub_batch_size) { 1000 } + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE #{table_name} + ( + id integer NOT NULL, + name character varying, + fk integer NOT NULL, + id_convert_to_bigint bigint DEFAULT 0 NOT NULL, + fk_convert_to_bigint bigint DEFAULT 0 NOT NULL, + name_convert_to_text text DEFAULT 'no name' + ); + SQL + + # Insert some data, it doesn't make a difference + test_table.create!(id: 11, name: 'test1', fk: 1) + test_table.create!(id: 12, name: 'test2', fk: 2) + test_table.create!(id: 15, name: nil, fk: 3) + test_table.create!(id: 19, name: 'test4', fk: 4) + end + + after do + # Make sure that the temp table we created is dropped (it is not removed by the database_cleaner) + ActiveRecord::Base.connection.execute(<<~SQL) + DROP TABLE IF EXISTS #{table_name}; + SQL + end + + subject { described_class.new } + + describe '#perform' do + let(:migration_class) { described_class.name } + let!(:job1) do + table(:background_migration_jobs).create!( + class_name: migration_class, + arguments: [1, 10, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size] + ) + end + + let!(:job2) do + table(:background_migration_jobs).create!( + class_name: migration_class, + arguments: [11, 20, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size] + ) + end + + it 'copies all primary keys in range' do + subject.perform(12, 15, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size) + + expect(test_table.where('id = id_convert_to_bigint').pluck(:id)).to contain_exactly(12, 15) + expect(test_table.where(id_convert_to_bigint: 0).pluck(:id)).to contain_exactly(11, 19) + expect(test_table.all.count).to eq(4) + end + + it 'copies all foreign keys in range' do + subject.perform(10, 14, table_name, 'id', 'fk', 'fk_convert_to_bigint', sub_batch_size) + + expect(test_table.where('fk = fk_convert_to_bigint').pluck(:id)).to contain_exactly(11, 12) + expect(test_table.where(fk_convert_to_bigint: 0).pluck(:id)).to contain_exactly(15, 19) + expect(test_table.all.count).to eq(4) + end + + it 'copies columns with NULLs' do + expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(4) + + subject.perform(10, 20, table_name, 'id', 'name', 'name_convert_to_text', sub_batch_size) + + expect(test_table.where('name = name_convert_to_text').pluck(:id)).to contain_exactly(11, 12, 19) + expect(test_table.where('name is NULL and name_convert_to_text is NULL').pluck(:id)).to contain_exactly(15) + expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(0) + end + + it 'tracks completion with BackgroundMigrationJob' do + expect do + subject.perform(11, 20, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size) + end.to change { Gitlab::Database::BackgroundMigrationJob.succeeded.count }.from(0).to(1) + + expect(job1.reload.status).to eq(0) + expect(job2.reload.status).to eq(1) + expect(test_table.where('id = id_convert_to_bigint').count).to eq(4) + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb new file mode 100644 index 00000000000..8e74935e127 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityFeedback, schema: 20201211090634 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:scanners) { table(:vulnerability_scanners) } + let(:identifiers) { table(:vulnerability_identifiers) } + let(:findings) { table(:vulnerability_occurrences) } + let(:vulnerability_feedback) { table(:vulnerability_feedback) } + + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') } + let(:user) { users.create!(username: 'john.doe', projects_limit: 5) } + let(:scanner) { scanners.create!(project_id: project.id, external_id: 'foo', name: 'bar') } + let(:identifier) { identifiers.create!(project_id: project.id, fingerprint: 'foo', external_type: 'bar', external_id: 'zoo', name: 'baz') } + let(:sast_report) { 0 } + let(:dependency_scanning_report) { 1 } + let(:dast_report) { 3 } + let(:secret_detection_report) { 4 } + let(:project_fingerprint) { Digest::SHA1.hexdigest(SecureRandom.uuid) } + let(:location_fingerprint_1) { Digest::SHA1.hexdigest(SecureRandom.uuid) } + let(:location_fingerprint_2) { Digest::SHA1.hexdigest(SecureRandom.uuid) } + let(:location_fingerprint_3) { Digest::SHA1.hexdigest(SecureRandom.uuid) } + let(:finding_1) { finding_creator.call(sast_report, location_fingerprint_1) } + let(:finding_2) { finding_creator.call(dast_report, location_fingerprint_2) } + let(:finding_3) { finding_creator.call(secret_detection_report, location_fingerprint_3) } + let(:uuid_1_components) { ['sast', identifier.fingerprint, location_fingerprint_1, project.id].join('-') } + let(:uuid_2_components) { ['dast', identifier.fingerprint, location_fingerprint_2, project.id].join('-') } + let(:uuid_3_components) { ['secret_detection', identifier.fingerprint, location_fingerprint_3, project.id].join('-') } + let(:expected_uuid_1) { Gitlab::UUID.v5(uuid_1_components) } + let(:expected_uuid_2) { Gitlab::UUID.v5(uuid_2_components) } + let(:expected_uuid_3) { Gitlab::UUID.v5(uuid_3_components) } + let(:finding_creator) do + -> (report_type, location_fingerprint) do + findings.create!( + project_id: project.id, + primary_identifier_id: identifier.id, + scanner_id: scanner.id, + report_type: report_type, + uuid: SecureRandom.uuid, + name: 'Foo', + location_fingerprint: Gitlab::Database::ShaAttribute.serialize(location_fingerprint), + project_fingerprint: Gitlab::Database::ShaAttribute.serialize(project_fingerprint), + metadata_version: '1', + severity: 0, + confidence: 5, + raw_metadata: '{}' + ) + end + end + + let(:feedback_creator) do + -> (category, project_fingerprint) do + vulnerability_feedback.create!( + project_id: project.id, + author_id: user.id, + feedback_type: 0, + category: category, + project_fingerprint: project_fingerprint + ) + end + end + + let!(:feedback_1) { feedback_creator.call(finding_1.report_type, project_fingerprint) } + let!(:feedback_2) { feedback_creator.call(finding_2.report_type, project_fingerprint) } + let!(:feedback_3) { feedback_creator.call(finding_3.report_type, project_fingerprint) } + let!(:feedback_4) { feedback_creator.call(finding_1.report_type, 'foo') } + let!(:feedback_5) { feedback_creator.call(dependency_scanning_report, project_fingerprint) } + + subject(:populate_finding_uuids) { described_class.new.perform(feedback_1.id, feedback_5.id) } + + before do + allow(Gitlab::BackgroundMigration::Logger).to receive(:info) + end + + describe '#perform' do + it 'updates the `finding_uuid` attributes of the feedback records' do + expect { populate_finding_uuids }.to change { feedback_1.reload.finding_uuid }.from(nil).to(expected_uuid_1) + .and change { feedback_2.reload.finding_uuid }.from(nil).to(expected_uuid_2) + .and change { feedback_3.reload.finding_uuid }.from(nil).to(expected_uuid_3) + .and not_change { feedback_4.reload.finding_uuid } + .and not_change { feedback_5.reload.finding_uuid } + + expect(Gitlab::BackgroundMigration::Logger).to have_received(:info).once + end + + it 'preloads the finding and identifier records to prevent N+1 queries' do + # Load feedback records(1), load findings(2), load identifiers(3) and finally update feedback records one by one(6) + expect { populate_finding_uuids }.not_to exceed_query_limit(6) + end + + context 'when setting the `finding_uuid` attribute of a feedback record fails' do + let(:expected_error) { RuntimeError.new } + + before do + allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + + allow_next_found_instance_of(described_class::VulnerabilityFeedback) do |feedback| + allow(feedback).to receive(:update_column).and_raise(expected_error) + end + end + + it 'captures the errors and does not crash entirely' do + expect { populate_finding_uuids }.not_to raise_error + + expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(expected_error).exactly(3).times + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb new file mode 100644 index 00000000000..391b27b28e6 --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateServices, :migration, schema: 20201207165956 do + let_it_be(:users) { table(:users) } + let_it_be(:namespaces) { table(:namespaces) } + let_it_be(:projects) { table(:projects) } + let_it_be(:services) { table(:services) } + + let_it_be(:alerts_service_data) { table(:alerts_service_data) } + let_it_be(:chat_names) { table(:chat_names) } + let_it_be(:issue_tracker_data) { table(:issue_tracker_data) } + let_it_be(:jira_tracker_data) { table(:jira_tracker_data) } + let_it_be(:open_project_tracker_data) { table(:open_project_tracker_data) } + let_it_be(:slack_integrations) { table(:slack_integrations) } + let_it_be(:web_hooks) { table(:web_hooks) } + + let_it_be(:data_tables) do + [alerts_service_data, chat_names, issue_tracker_data, jira_tracker_data, open_project_tracker_data, slack_integrations, web_hooks] + end + + let!(:user) { users.create!(id: 1, projects_limit: 100) } + let!(:namespace) { namespaces.create!(id: 1, name: 'group', path: 'group') } + + # project without duplicate services + let!(:project1) { projects.create!(id: 1, namespace_id: namespace.id) } + let!(:service1) { services.create!(id: 1, project_id: project1.id, type: 'AsanaService') } + let!(:service2) { services.create!(id: 2, project_id: project1.id, type: 'JiraService') } + let!(:service3) { services.create!(id: 3, project_id: project1.id, type: 'SlackService') } + + # project with duplicate services + let!(:project2) { projects.create!(id: 2, namespace_id: namespace.id) } + let!(:service4) { services.create!(id: 4, project_id: project2.id, type: 'AsanaService') } + let!(:service5) { services.create!(id: 5, project_id: project2.id, type: 'JiraService') } + let!(:service6) { services.create!(id: 6, project_id: project2.id, type: 'JiraService') } + let!(:service7) { services.create!(id: 7, project_id: project2.id, type: 'SlackService') } + let!(:service8) { services.create!(id: 8, project_id: project2.id, type: 'SlackService') } + let!(:service9) { services.create!(id: 9, project_id: project2.id, type: 'SlackService') } + + # project with duplicate services and dependant records + let!(:project3) { projects.create!(id: 3, namespace_id: namespace.id) } + let!(:service10) { services.create!(id: 10, project_id: project3.id, type: 'AlertsService') } + let!(:service11) { services.create!(id: 11, project_id: project3.id, type: 'AlertsService') } + let!(:service12) { services.create!(id: 12, project_id: project3.id, type: 'SlashCommandsService') } + let!(:service13) { services.create!(id: 13, project_id: project3.id, type: 'SlashCommandsService') } + let!(:service14) { services.create!(id: 14, project_id: project3.id, type: 'IssueTrackerService') } + let!(:service15) { services.create!(id: 15, project_id: project3.id, type: 'IssueTrackerService') } + let!(:service16) { services.create!(id: 16, project_id: project3.id, type: 'JiraService') } + let!(:service17) { services.create!(id: 17, project_id: project3.id, type: 'JiraService') } + let!(:service18) { services.create!(id: 18, project_id: project3.id, type: 'OpenProjectService') } + let!(:service19) { services.create!(id: 19, project_id: project3.id, type: 'OpenProjectService') } + let!(:service20) { services.create!(id: 20, project_id: project3.id, type: 'SlackService') } + let!(:service21) { services.create!(id: 21, project_id: project3.id, type: 'SlackService') } + let!(:dependant_records) do + alerts_service_data.create!(id: 1, service_id: service10.id) + alerts_service_data.create!(id: 2, service_id: service11.id) + chat_names.create!(id: 1, service_id: service12.id, user_id: user.id, team_id: 'team1', chat_id: 'chat1') + chat_names.create!(id: 2, service_id: service13.id, user_id: user.id, team_id: 'team2', chat_id: 'chat2') + issue_tracker_data.create!(id: 1, service_id: service14.id) + issue_tracker_data.create!(id: 2, service_id: service15.id) + jira_tracker_data.create!(id: 1, service_id: service16.id) + jira_tracker_data.create!(id: 2, service_id: service17.id) + open_project_tracker_data.create!(id: 1, service_id: service18.id) + open_project_tracker_data.create!(id: 2, service_id: service19.id) + slack_integrations.create!(id: 1, service_id: service20.id, user_id: user.id, team_id: 'team1', team_name: 'team1', alias: 'alias1') + slack_integrations.create!(id: 2, service_id: service21.id, user_id: user.id, team_id: 'team2', team_name: 'team2', alias: 'alias2') + web_hooks.create!(id: 1, service_id: service20.id) + web_hooks.create!(id: 2, service_id: service21.id) + end + + # project without services + let!(:project4) { projects.create!(id: 4, namespace_id: namespace.id) } + + it 'removes duplicate services and dependant records' do + # Determine which services we expect to keep + expected_services = projects.pluck(:id).each_with_object({}) do |project_id, map| + project_services = services.where(project_id: project_id) + types = project_services.distinct.pluck(:type) + + map[project_id] = types.map { |type| project_services.where(type: type).take!.id } + end + + expect do + subject.perform(project2.id, project3.id) + end.to change { services.count }.from(21).to(12) + + services1 = services.where(project_id: project1.id) + expect(services1.count).to be(3) + expect(services1.pluck(:type)).to contain_exactly('AsanaService', 'JiraService', 'SlackService') + expect(services1.pluck(:id)).to contain_exactly(*expected_services[project1.id]) + + services2 = services.where(project_id: project2.id) + expect(services2.count).to be(3) + expect(services2.pluck(:type)).to contain_exactly('AsanaService', 'JiraService', 'SlackService') + expect(services2.pluck(:id)).to contain_exactly(*expected_services[project2.id]) + + services3 = services.where(project_id: project3.id) + expect(services3.count).to be(6) + expect(services3.pluck(:type)).to contain_exactly('AlertsService', 'SlashCommandsService', 'IssueTrackerService', 'JiraService', 'OpenProjectService', 'SlackService') + expect(services3.pluck(:id)).to contain_exactly(*expected_services[project3.id]) + + kept_services = expected_services.values.flatten + data_tables.each do |table| + expect(table.count).to be(1) + expect(kept_services).to include(table.pluck(:service_id).first) + end + end + + it 'does not delete services without duplicates' do + expect do + subject.perform(project1.id, project4.id) + end.not_to change { services.count } + end + + it 'only deletes duplicate services for the current batch' do + expect do + subject.perform(project2.id) + end.to change { services.count }.by(-3) + end +end diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb index f4daafb1d0e..6b45b8d4628 100644 --- a/spec/lib/gitlab/checks/diff_check_spec.rb +++ b/spec/lib/gitlab/checks/diff_check_spec.rb @@ -6,96 +6,63 @@ RSpec.describe Gitlab::Checks::DiffCheck do include_context 'change access checks context' describe '#validate!' do - let(:owner) { create(:user) } - - before do - allow(project.repository).to receive(:new_commits).and_return( - project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') - ) - end - - context 'with LFS not enabled' do - before do - allow(project).to receive(:lfs_enabled?).and_return(false) - end - - it 'does not invoke :lfs_file_locks_validation' do - expect(subject).not_to receive(:lfs_file_locks_validation) + context 'when commits is empty' do + it 'does not call find_changed_paths' do + expect(project.repository).not_to receive(:find_changed_paths) subject.validate! end end - context 'with LFS enabled' do - let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } - + context 'when commits is not empty' do before do - allow(project).to receive(:lfs_enabled?).and_return(true) + allow(project.repository).to receive(:new_commits).and_return( + project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') + ) end - context 'when change is sent by a different user' do - context 'when diff check with paths rpc feature flag is true' do - it 'raises an error if the user is not allowed to update the file' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") - end - end + context 'when deletion is true' do + let(:newrev) { Gitlab::Git::BLANK_SHA } - context 'when diff check with paths rpc feature flag is false' do - before do - stub_feature_flags(diff_check_with_paths_changed_rpc: false) - end + it 'does not call find_changed_paths' do + expect(project.repository).not_to receive(:find_changed_paths) - it 'raises an error if the user is not allowed to update the file' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") - end + subject.validate! end end - context 'when change is sent by the author of the lock' do - let(:user) { owner } - - it "doesn't raise any error" do - expect { subject.validate! }.not_to raise_error + context 'with LFS not enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(false) end - end - end - - context 'commit diff validations' do - before do - allow(subject).to receive(:validations_for_diff).and_return([lambda { |diff| return }]) - - expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original - - stub_feature_flags(diff_check_with_paths_changed_rpc: false) - - subject.validate! - end - context 'when request store is inactive' do - it 'are run for every commit' do - expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original + it 'does not invoke :lfs_file_locks_validation' do + expect(subject).not_to receive(:lfs_file_locks_validation) subject.validate! end end - context 'when request store is active', :request_store do - it 'are cached for every commit' do - expect_any_instance_of(Commit).not_to receive(:raw_deltas) + context 'with LFS enabled' do + let(:owner) { create(:user) } + let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } - subject.validate! + before do + allow(project).to receive(:lfs_enabled?).and_return(true) end - it 'are run for not cached commits' do - allow(project.repository).to receive(:new_commits).and_return( - project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', 'a5391128b0ef5d21df5dd23d98557f4ef12fae20') - ) - change_access.instance_variable_set(:@commits, project.repository.new_commits) + context 'when change is sent by a different user' do + it 'raises an error if the user is not allowed to update the file' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + end + end - expect(project.repository.new_commits.first).not_to receive(:raw_deltas).and_call_original - expect(project.repository.new_commits.last).to receive(:raw_deltas).and_call_original + context 'when change is sent by the author of the lock' do + let(:user) { owner } - subject.validate! + it "doesn't raise any error" do + expect { subject.validate! }.not_to raise_error + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb index 028dcd3e1e6..0e6d5b6c311 100644 --- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb @@ -36,6 +36,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do expect(entry.value).to eq config end end + + context "when value includes 'public' keyword" do + let(:config) { { paths: %w[results.txt], public: false } } + + it 'returns general artifact and report-type artifacts configuration' do + expect(entry.value).to eq config + end + end end context 'when entry value is not correct' do @@ -67,6 +75,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do end end + context "when 'public' is not a boolean" do + let(:config) { { paths: %w[results.txt], public: 'false' } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts public should be a boolean value' + end + end + context "when 'expose_as' is not a string" do let(:config) { { paths: %w[results.txt], expose_as: 1 } } diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index 426a38e2ef7..78d37e228df 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Variables do let(:metadata) { {} } - subject { described_class.new(config, metadata) } + subject { described_class.new(config, **metadata) } shared_examples 'valid config' do describe '#value' do diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index fdd29afe2d6..7e39fae7b9b 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -16,7 +16,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local do project: project, sha: sha, user: user, - parent_pipeline: parent_pipeline + parent_pipeline: parent_pipeline, + variables: project.predefined_variables.to_runner_variables } end @@ -131,7 +132,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local do user: user, project: project, sha: sha, - parent_pipeline: parent_pipeline) + parent_pipeline: parent_pipeline, + variables: project.predefined_variables.to_runner_variables) end end diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index a5e4e27df6f..0e8851ba915 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -16,7 +16,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do project: context_project, sha: '12345', user: context_user, - parent_pipeline: parent_pipeline + parent_pipeline: parent_pipeline, + variables: project.predefined_variables.to_runner_variables } end @@ -165,7 +166,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do user: user, project: project, sha: project.commit('master').id, - parent_pipeline: parent_pipeline) + parent_pipeline: parent_pipeline, + variables: project.predefined_variables.to_runner_variables) end end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 7ad57827e30..4fdaaca8316 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' } let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' } - let(:context_params) { { project: project, sha: '123456', user: user } } + let(:context_params) { { project: project, sha: '123456', user: user, variables: project.predefined_variables.to_runner_variables } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } let(:file_content) do @@ -124,17 +124,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do an_instance_of(Gitlab::Ci::Config::External::File::Project), an_instance_of(Gitlab::Ci::Config::External::File::Project)) end - - context 'when FF ci_include_multiple_files_from_project is disabled' do - before do - stub_feature_flags(ci_include_multiple_files_from_project: false) - end - - it 'returns a File instance' do - expect(subject).to contain_exactly( - an_instance_of(Gitlab::Ci::Config::External::File::Project)) - end - end end end @@ -236,5 +225,118 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do end end end + + context "when 'include' section uses project variable" do + let(:full_local_file_path) { '$CI_PROJECT_PATH' + local_file } + + context 'when local file is included as a single string' do + let(:values) do + { include: full_local_file_path } + end + + it 'expands the variable', :aggregate_failures do + expect(subject[0].location).to eq(project.full_path + local_file) + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local)) + end + end + + context 'when remote file is included as a single string' do + let(:remote_url) { "#{Gitlab.config.gitlab.url}/radio/.gitlab-ci.yml" } + + let(:values) do + { include: '$CI_SERVER_URL/radio/.gitlab-ci.yml' } + end + + it 'expands the variable', :aggregate_failures do + expect(subject[0].location).to eq(remote_url) + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote)) + end + end + + context 'defined as an array' do + let(:values) do + { include: [full_local_file_path, remote_url], + image: 'ruby:2.7' } + end + + it 'expands the variable' do + expect(subject[0].location).to eq(project.full_path + local_file) + expect(subject[1].location).to eq(remote_url) + end + end + + context 'defined as an array of hashes' do + let(:values) do + { include: [{ local: full_local_file_path }, { remote: remote_url }], + image: 'ruby:2.7' } + end + + it 'expands the variable' do + expect(subject[0].location).to eq(project.full_path + local_file) + expect(subject[1].location).to eq(remote_url) + end + end + + context 'local file hash' do + let(:values) do + { include: { 'local' => full_local_file_path } } + end + + it 'expands the variable' do + expect(subject[0].location).to eq(project.full_path + local_file) + end + end + + context 'project name' do + let(:values) do + { include: { project: '$CI_PROJECT_PATH', file: local_file }, + image: 'ruby:2.7' } + end + + it 'expands the variable', :aggregate_failures do + expect(subject[0].project_name).to eq(project.full_path) + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Project)) + end + end + + context 'with multiple files' do + let(:values) do + { include: { project: project.full_path, file: [full_local_file_path, 'another_file_path.yml'] }, + image: 'ruby:2.7' } + end + + it 'expands the variable' do + expect(subject[0].location).to eq(project.full_path + local_file) + expect(subject[1].location).to eq('another_file_path.yml') + end + end + + context 'when include variable has an unsupported type for variable expansion' do + let(:values) do + { include: { project: project.id, file: local_file }, + image: 'ruby:2.7' } + end + + it 'does not invoke expansion for the variable', :aggregate_failures do + expect(ExpandVariables).not_to receive(:expand).with(project.id, context_params[:variables]) + + expect { subject }.to raise_error(described_class::AmbigiousSpecificationError) + end + end + + context 'when feature flag is turned off' do + let(:values) do + { include: full_local_file_path } + end + + before do + stub_feature_flags(variables_in_include_section_ci: false) + end + + it 'does not expand the variables' do + expect(subject[0].location).to eq('$CI_PROJECT_PATH' + local_file) + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 150a2ec2929..d2d7116bb12 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -365,19 +365,6 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do output = processor.perform expect(output.keys).to match_array([:image, :my_build, :my_test]) end - - context 'when FF ci_include_multiple_files_from_project is disabled' do - before do - stub_feature_flags(ci_include_multiple_files_from_project: false) - end - - it 'raises an error' do - expect { processor.perform }.to raise_error( - described_class::IncludeError, - 'Included file `["/templates/my-build.yml", "/templates/my-test.yml"]` needs to be a string' - ) - end - end end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index b5a0f0e3fd7..dc03d2f80fe 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -82,6 +82,30 @@ RSpec.describe Gitlab::Ci::Config do end end + describe '#included_templates' do + let(:yml) do + <<-EOS + include: + - template: Jobs/Deploy.gitlab-ci.yml + - template: Jobs/Build.gitlab-ci.yml + - remote: https://example.com/gitlab-ci.yml + EOS + end + + before do + stub_request(:get, 'https://example.com/gitlab-ci.yml').to_return(status: 200, body: <<-EOS) + test: + script: [ 'echo hello world' ] + EOS + end + + subject(:included_templates) do + config.included_templates + end + + it { is_expected.to contain_exactly('Jobs/Deploy.gitlab-ci.yml', 'Jobs/Build.gitlab-ci.yml') } + end + context 'when using extendable hash' do let(:yml) do <<-EOS diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index c67f8464123..67324c09d86 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -247,7 +247,7 @@ RSpec.describe Gitlab::Ci::Lint do include_context 'advanced validations' do it 'runs advanced logical validations' do expect(subject).not_to be_valid - expect(subject.errors).to eq(["test: needs 'build'"]) + expect(subject.errors).to eq(["'test' job needs 'build' job, but it was not added to the pipeline"]) end end diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb index 2313378d1e9..546de2bee5c 100644 --- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb +++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb @@ -224,6 +224,12 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do it_behaves_like 'ignoring sources, project_path, and worktree_paths' end + context 'when there is an empty ' do + let(:sources_xml) { '' } + + it_behaves_like 'ignoring sources, project_path, and worktree_paths' + end + context 'when there is a ' do context 'and has a single source with a pattern for Go projects' do let(:project_path) { 'local/go' } # Make sure we're not making false positives diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index 6da565a2bf6..20406acb658 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do - let_it_be(:project) { create(:project, :repository) } + let_it_be(:project, reload: true) { create(:project, :repository) } let_it_be(:user) { create(:user, developer_projects: [project]) } let(:pipeline) { Ci::Pipeline.new } @@ -29,29 +29,96 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do let(:step) { described_class.new(pipeline, command) } - before do - stub_ci_pipeline_yaml_file(gitlab_ci_yaml) + shared_examples 'builds pipeline' do + it 'builds a pipeline with the expected attributes' do + step.perform! + + expect(pipeline.sha).not_to be_empty + expect(pipeline.sha).to eq project.commit.id + expect(pipeline.ref).to eq 'master' + expect(pipeline.tag).to be false + expect(pipeline.user).to eq user + expect(pipeline.project).to eq project + end end - it 'never breaks the chain' do - step.perform! + shared_examples 'breaks the chain' do + it 'returns true' do + step.perform! + + expect(step.break?).to be true + end + end + + shared_examples 'does not break the chain' do + it 'returns false' do + step.perform! + + expect(step.break?).to be false + end + end - expect(step.break?).to be false + before do + stub_ci_pipeline_yaml_file(gitlab_ci_yaml) end - it 'fills pipeline object with data' do + it_behaves_like 'does not break the chain' + it_behaves_like 'builds pipeline' + + it 'sets pipeline variables' do step.perform! - expect(pipeline.sha).not_to be_empty - expect(pipeline.sha).to eq project.commit.id - expect(pipeline.ref).to eq 'master' - expect(pipeline.tag).to be false - expect(pipeline.user).to eq user - expect(pipeline.project).to eq project expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) }) .to eq variables_attributes.map(&:with_indifferent_access) end + context 'when project setting restrict_user_defined_variables is enabled' do + before do + project.update!(restrict_user_defined_variables: true) + end + + context 'when user is developer' do + it_behaves_like 'breaks the chain' + it_behaves_like 'builds pipeline' + + it 'returns an error on variables_attributes', :aggregate_failures do + step.perform! + + expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables']) + expect(pipeline.variables).to be_empty + end + + context 'when variables_attributes is not specified' do + let(:variables_attributes) { nil } + + it_behaves_like 'does not break the chain' + it_behaves_like 'builds pipeline' + + it 'assigns empty variables' do + step.perform! + + expect(pipeline.variables).to be_empty + end + end + end + + context 'when user is maintainer' do + before do + project.add_maintainer(user) + end + + it_behaves_like 'does not break the chain' + it_behaves_like 'builds pipeline' + + it 'assigns variables_attributes' do + step.perform! + + expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) }) + .to eq variables_attributes.map(&:with_indifferent_access) + end + end + end + it 'returns a valid pipeline' do step.perform! @@ -157,4 +224,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do expect(pipeline.target_sha).to eq(external_pull_request.target_sha) end end + + context 'when keep_latest_artifact is set' do + using RSpec::Parameterized::TableSyntax + + where(:keep_latest_artifact, :locking_result) do + true | 'artifacts_locked' + false | 'unlocked' + end + + with_them do + before do + project.update!(ci_keep_latest_artifact: keep_latest_artifact) + end + + it 'builds a pipeline with appropriate locked value' do + step.perform! + + expect(pipeline.locked).to eq(locking_result) + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index bc2012e83bd..9ca5aeeea58 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -295,4 +295,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do it { is_expected.to eq(false) } end end + + describe '#creates_child_pipeline?' do + let(:command) { described_class.new(bridge: bridge) } + + subject { command.creates_child_pipeline? } + + context 'when bridge is present' do + context 'when bridge triggers a child pipeline' do + let(:bridge) { double(:bridge, triggers_child_pipeline?: true) } + + it { is_expected.to be_truthy } + end + + context 'when bridge triggers a multi-project pipeline' do + let(:bridge) { double(:bridge, triggers_child_pipeline?: false) } + + it { is_expected.to be_falsey } + end + end + + context 'when bridge is not present' do + let(:bridge) { nil } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb index 85c8e20767f..fabfbd779f3 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb @@ -51,18 +51,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::SeedBlock do expect(pipeline.variables.size).to eq(1) end - - context 'when FF ci_seed_block_run_before_workflow_rules is disabled' do - before do - stub_feature_flags(ci_seed_block_run_before_workflow_rules: false) - end - - it 'does not execute the block' do - run_chain - - expect(pipeline.variables.size).to eq(0) - end - end end context 'when the seeds_block tries to save the pipelie' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index 0ce8b80902e..80013cab6ee 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -20,6 +20,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do describe '#perform!' do before do stub_ci_pipeline_yaml_file(YAML.dump(config)) + run_chain end let(:config) do @@ -36,20 +37,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'allocates next IID' do - run_chain - expect(pipeline.iid).to be_present end it 'ensures ci_ref' do - run_chain - expect(pipeline.ci_ref).to be_present end it 'sets the seeds in the command object' do - run_chain - expect(command.pipeline_seed).to be_a(Gitlab::Ci::Pipeline::Seed::Pipeline) expect(command.pipeline_seed.size).to eq 1 end @@ -64,8 +59,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'correctly fabricates stages and builds' do - run_chain - seed = command.pipeline_seed expect(seed.stages.size).to eq 2 @@ -91,8 +84,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'returns pipeline seed with jobs only assigned to master' do - run_chain - seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -112,8 +103,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'returns pipeline seed with jobs only assigned to schedules' do - run_chain - seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -141,8 +130,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do let(:pipeline) { build(:ci_pipeline, project: project) } it 'returns seeds for kubernetes dependent job' do - run_chain - seed = command.pipeline_seed expect(seed.size).to eq 2 @@ -154,8 +141,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do context 'when kubernetes is not active' do it 'does not return seeds for kubernetes dependent job' do - run_chain - seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -173,8 +158,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'returns stage seeds only when variables expression is truthy' do - run_chain - seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -187,24 +170,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do ->(pipeline) { pipeline.variables.build(key: 'VAR', value: '123') } end - context 'when FF ci_seed_block_run_before_workflow_rules is enabled' do - it 'does not execute the block' do - run_chain - - expect(pipeline.variables.size).to eq(0) - end - end - - context 'when FF ci_seed_block_run_before_workflow_rules is disabled' do - before do - stub_feature_flags(ci_seed_block_run_before_workflow_rules: false) - end - - it 'executes the block' do - run_chain - - expect(pipeline.variables.size).to eq(1) - end + it 'does not execute the block' do + expect(pipeline.variables.size).to eq(0) end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb new file mode 100644 index 00000000000..3616461d94f --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + describe '#perform!' do + subject(:perform) { step.perform! } + + it 'tracks the included templates' do + expect(command).to( + receive(:yaml_processor_result) + .and_return( + double(included_templates: %w(Template-1 Template-2)) + ) + ) + + %w(Template-1 Template-2).each do |expected_template| + expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to( + receive(:track_unique_project_event) + .with(project_id: project.id, template: expected_template) + ) + end + + perform + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index bc10e94c81d..cf020fc343c 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -966,7 +966,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it "returns an error" do expect(subject.errors).to contain_exactly( - "rspec: needs 'build'") + "'rspec' job needs 'build' job, but it was not added to the pipeline") end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb index 1790388da03..860b07647bd 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb @@ -62,7 +62,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do needs_attributes: [{ name: 'non-existent', artifacts: true }] } - expect(seed.errors).to contain_exactly("invalid_job: needs 'non-existent'") + expect(seed.errors).to contain_exactly( + "'invalid_job' job needs 'non-existent' job, but it was not added to the pipeline") end end end diff --git a/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb b/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb index 8df34eddffd..831bc5e9f37 100644 --- a/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb @@ -28,18 +28,5 @@ RSpec.describe Gitlab::Ci::Reports::TestFailureHistory, :aggregate_failures do expect(failed_rspec.recent_failures).to eq(count: 2, base_branch: 'master') expect(failed_java.recent_failures).to eq(count: 1, base_branch: 'master') end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(test_failure_history: false) - end - - it 'does not set recent failures' do - load_history - - expect(failed_rspec.recent_failures).to be_nil - expect(failed_java.recent_failures).to be_nil - end - end end end diff --git a/spec/lib/gitlab/ci/status/group/factory_spec.rb b/spec/lib/gitlab/ci/status/group/factory_spec.rb index 6267b26aa78..c67c7ff8271 100644 --- a/spec/lib/gitlab/ci/status/group/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/group/factory_spec.rb @@ -12,4 +12,9 @@ RSpec.describe Gitlab::Ci::Status::Group::Factory do expect(described_class.common_helpers) .to eq Gitlab::Ci::Status::Group::Common end + + it 'exposes extended statuses' do + expect(described_class.extended_statuses) + .to eq([[Gitlab::Ci::Status::SuccessWarning]]) + end end diff --git a/spec/lib/gitlab/ci/syntax_templates_spec.rb b/spec/lib/gitlab/ci/syntax_templates_spec.rb new file mode 100644 index 00000000000..ce3169e17ec --- /dev/null +++ b/spec/lib/gitlab/ci/syntax_templates_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ci/syntax_templates' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let(:lint) { Gitlab::Ci::Lint.new(project: project, current_user: user) } + + before do + project.add_developer(user) + end + + subject(:lint_result) { lint.validate(content) } + + Dir.glob('lib/gitlab/ci/syntax_templates/**/*.yml').each do |template| + describe template do + let(:content) { File.read(template) } + + it 'validates the template' do + expect(lint_result).to be_valid, "got errors: #{lint_result.errors.join(', ')}" + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb new file mode 100644 index 00000000000..6bc8e261640 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('5-Minute-Production-App') } + + describe 'the created pipeline' do + let_it_be(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + + let(:user) { project.owner } + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + end + + it 'creates only build job' do + expect(build_names).to match_array('build') + end + + context 'when AWS variables are set' do + before do + create(:ci_variable, project: project, key: 'AWS_ACCESS_KEY_ID', value: 'AKIAIOSFODNN7EXAMPLE') + create(:ci_variable, project: project, key: 'AWS_SECRET_ACCESS_KEY', value: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY') + create(:ci_variable, project: project, key: 'AWS_DEFAULT_REGION', value: 'us-west-2') + end + + it 'creates all jobs' do + expect(build_names).to match_array(%w(build terraform_apply deploy terraform_destroy)) + end + + context 'pipeline branch is protected' do + before do + create(:protected_branch, project: project, name: pipeline_branch) + project.reload + end + + it 'does not create a destroy job' do + expect(build_names).to match_array(%w(build terraform_apply deploy)) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb index 4be92e8608e..653b3be0b2a 100644 --- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb @@ -6,10 +6,10 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('AWS/Deploy-ECS') } describe 'the created pipeline' do - let_it_be(:user) { create(:admin) } let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } let(:pipeline) { service.execute!(:push) } let(:build_names) { pipeline.builds.pluck(:name) } diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb index 4f8faa5ddb1..1f278048ad5 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Build') } describe 'the created pipeline' do - let_it_be(:user) { create(:admin) } let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb index e685ad3b46e..0a76de82421 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Code-Quality') } describe 'the created pipeline' do - let_it_be(:user) { create(:admin) } let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb index ea9bd5bd02c..25c88c161ea 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb @@ -27,8 +27,8 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do end describe 'the created pipeline' do - let(:user) { create(:admin) } let(:project) { create(:project, :repository) } + let(:user) { project.owner } let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb index f475785be98..b64959a9917 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Test') } describe 'the created pipeline' do - let_it_be(:user) { create(:admin) } let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb index 8df739d9245..0811c07e896 100644 --- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb @@ -6,10 +6,10 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') } describe 'the created pipeline' do - let(:user) { create(:admin) } let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } let(:pipeline) { service.execute!(:push) } let(:build_names) { pipeline.builds.pluck(:name) } diff --git a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb index 9711df55226..03fa45fe0a1 100644 --- a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb @@ -19,8 +19,8 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do end describe 'the created pipeline' do - let(:user) { create(:admin) } let(:project) { create(:project, :repository) } + let(:user) { project.owner } let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 793df55f45d..f9d6fe24e70 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -6,10 +6,10 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') } describe 'the created pipeline' do - let(:user) { create(:admin) } let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } let(:pipeline) { service.execute!(:push) } let(:build_names) { pipeline.builds.pluck(:name) } @@ -232,8 +232,8 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do end with_them do - let(:user) { create(:admin) } let(:project) { create(:project, :custom_repo, files: files) } + let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } let(:pipeline) { service.execute(:push) } let(:build_names) { pipeline.builds.pluck(:name) } diff --git a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..4e5fe622648 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Flutter.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Flutter') } + + describe 'the created pipeline' do + let(:pipeline_branch) { 'master' } + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + end + + it 'creates test and code_quality jobs' do + expect(build_names).to include('test', 'code_quality') + end + end +end diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb index 1f8e32ce019..b10e2b0e057 100644 --- a/spec/lib/gitlab/ci/templates/npm_spec.rb +++ b/spec/lib/gitlab/ci/templates/npm_spec.rb @@ -6,11 +6,10 @@ RSpec.describe 'npm.latest.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('npm.latest') } describe 'the created pipeline' do - let_it_be(:user) { create(:admin) } - let(:repo_files) { { 'package.json' => '{}', 'README.md' => '' } } let(:modified_files) { %w[package.json] } let(:project) { create(:project, :custom_repo, files: repo_files) } + let(:user) { project.owner } let(:pipeline_branch) { project.default_branch } let(:pipeline_tag) { 'v1.2.1' } let(:pipeline_ref) { pipeline_branch } diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb index 5eec021b9d7..4377f155d34 100644 --- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb @@ -10,11 +10,10 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform.latest') } describe 'the created pipeline' do - let_it_be(:user) { create(:admin) } - let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } let(:pipeline) { service.execute!(:push) } let(:build_names) { pipeline.builds.pluck(:name) } diff --git a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb new file mode 100644 index 00000000000..d85bf29f77f --- /dev/null +++ b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do + describe '#errors' do + context 'when FF :variable_inside_variable is disabled' do + before do + stub_feature_flags(variable_inside_variable: false) + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [] + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ] + }, + "complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'key${variable}' } + ] + }, + "complex expansions with missing variable for Windows": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'key%variable%%variable2%' } + ] + }, + "out-of-order variable reference": { + variables: [ + { key: 'variable2', value: 'key${variable}' }, + { key: 'variable', value: 'value' } + ] + }, + "array with cyclic dependency": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ] + } + } + end + + with_them do + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + + it 'does not report error' do + expect(subject.errors).to eq(nil) + end + + it 'valid? reports true' do + expect(subject.valid?).to eq(true) + end + end + end + end + + context 'when FF :variable_inside_variable is enabled' do + before do + stub_feature_flags(variable_inside_variable: true) + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [], + validation_result: nil + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + validation_result: nil + }, + "cyclic dependency": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + validation_result: 'circular variable reference detected: ["variable", "variable2", "variable3"]' + } + } + end + + with_them do + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + + it 'errors matches expected validation result' do + expect(subject.errors).to eq(validation_result) + end + + it 'valid? matches expected validation result' do + expect(subject.valid?).to eq(validation_result.nil?) + end + end + end + end + end + + describe '#sort' do + context 'when FF :variable_inside_variable is disabled' do + before do + stub_feature_flags(variable_inside_variable: false) + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [] + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ] + }, + "complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'key${variable}' } + ] + }, + "complex expansions with missing variable for Windows": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'key%variable%%variable2%' } + ] + }, + "out-of-order variable reference": { + variables: [ + { key: 'variable2', value: 'key${variable}' }, + { key: 'variable', value: 'value' } + ] + }, + "array with cyclic dependency": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ] + } + } + end + + with_them do + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + + it 'does not expand variables' do + expect(subject.sort).to eq(variables) + end + end + end + end + + context 'when FF :variable_inside_variable is enabled' do + before do + stub_licensed_features(group_saml_group_sync: true) + stub_feature_flags(saml_group_links: true) + stub_feature_flags(variable_inside_variable: true) + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [], + result: [] + }, + "simple expansions, no reordering needed": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + result: %w[variable variable2 variable3] + }, + "complex expansion, reordering needed": { + variables: [ + { key: 'variable2', value: 'key${variable}' }, + { key: 'variable', value: 'value' } + ], + result: %w[variable variable2] + }, + "unused variables": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable4', value: 'key$variable$variable3' }, + { key: 'variable2', value: 'result2' }, + { key: 'variable3', value: 'result3' } + ], + result: %w[variable variable3 variable4 variable2] + }, + "missing variable": { + variables: [ + { key: 'variable2', value: 'key$variable' } + ], + result: %w[variable2] + }, + "complex expansions with missing variable": { + variables: [ + { key: 'variable4', value: 'key${variable}${variable2}${variable3}' }, + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'value3' } + ], + result: %w[variable variable3 variable4] + }, + "cyclic dependency causes original array to be returned": { + variables: [ + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' }, + { key: 'variable', value: '$variable2' } + ], + result: %w[variable2 variable3 variable] + } + } + end + + with_them do + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + + it 'sort returns correctly sorted variables' do + expect(subject.sort.map { |var| var[:key] }).to eq(result) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5ad1b3dd241..9498453852a 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2711,40 +2711,6 @@ module Gitlab end end - describe "#validation_message" do - subject { Gitlab::Ci::YamlProcessor.validation_message(content) } - - context "when the YAML could not be parsed" do - let(:content) { YAML.dump("invalid: yaml: test") } - - it { is_expected.to eq "Invalid configuration format" } - end - - context "when the tags parameter is invalid" do - let(:content) { YAML.dump({ rspec: { script: "test", tags: "mysql" } }) } - - it { is_expected.to eq "jobs:rspec:tags config should be an array of strings" } - end - - context "when YAML content is empty" do - let(:content) { '' } - - it { is_expected.to eq "Please provide content of .gitlab-ci.yml" } - end - - context 'when the YAML contains an unknown alias' do - let(:content) { 'steps: *bad_alias' } - - it { is_expected.to eq "Unknown alias: bad_alias" } - end - - context "when the YAML is valid" do - let(:content) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } - - it { is_expected.to be_nil } - end - end - describe '#execute' do subject { Gitlab::Ci::YamlProcessor.new(content).execute } diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb new file mode 100644 index 00000000000..4c4742d9f59 --- /dev/null +++ b/spec/lib/gitlab/composer/version_index_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Composer::VersionIndex do + let_it_be(:package_name) { 'sample-project' } + let_it_be(:json) { { 'name' => package_name } } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) } + let_it_be(:package1) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let_it_be(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + + let(:branch) { project.repository.find_branch('master') } + + let(:packages) { [package1, package2] } + + describe '#as_json' do + subject(:index) { described_class.new(packages).as_json } + + def expected_json(package) + { + 'dist' => { + 'reference' => branch.target, + 'shasum' => '', + 'type' => 'zip', + 'url' => "http://localhost/api/v4/projects/#{project.id}/packages/composer/archives/#{package.name}.zip?sha=#{branch.target}" + }, + 'name' => package.name, + 'uid' => package.id, + 'version' => package.version + } + end + + it 'returns the packages json' do + packages = index['packages'][package_name] + + expect(packages['1.0.0']).to eq(expected_json(package1)) + expect(packages['2.0.0']).to eq(expected_json(package2)) + end + end + + describe '#sha' do + subject(:sha) { described_class.new(packages).sha } + + it 'returns the json SHA' do + expect(sha).to match /^[A-Fa-f0-9]{64}$/ + end + end +end diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb index 15bbf2047c5..f64b39231a3 100644 --- a/spec/lib/gitlab/config/entry/composable_hash_spec.rb +++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do end let(:entry) do - parent_entry = composable_hash_parent_class.new(secrets: config) + parent_entry = composable_hash_parent_class.new({ secrets: config }) parent_entry.compose! parent_entry[:secrets] diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 0de944d3f8a..bb9bee763d8 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -97,19 +97,27 @@ RSpec.describe Gitlab::Conflict::File do let(:diff_line_types) { conflict_file.diff_lines_for_serializer.map(&:type) } it 'assigns conflict types to the diff lines' do - expect(diff_line_types[4]).to eq('conflict_marker') - expect(diff_line_types[5..10]).to eq(['conflict_marker_our'] * 6) + expect(diff_line_types[4]).to eq('conflict_marker_our') + expect(diff_line_types[5..10]).to eq(['conflict_our'] * 6) expect(diff_line_types[11]).to eq('conflict_marker') - expect(diff_line_types[12..17]).to eq(['conflict_marker_their'] * 6) - expect(diff_line_types[18]).to eq('conflict_marker') + expect(diff_line_types[12..17]).to eq(['conflict_their'] * 6) + expect(diff_line_types[18]).to eq('conflict_marker_their') expect(diff_line_types[19..24]).to eq([nil] * 6) - expect(diff_line_types[25]).to eq('conflict_marker') - expect(diff_line_types[26..27]).to eq(['conflict_marker_our'] * 2) + expect(diff_line_types[25]).to eq('conflict_marker_our') + expect(diff_line_types[26..27]).to eq(['conflict_our'] * 2) expect(diff_line_types[28]).to eq('conflict_marker') - expect(diff_line_types[29..30]).to eq(['conflict_marker_their'] * 2) - expect(diff_line_types[31]).to eq('conflict_marker') + expect(diff_line_types[29..30]).to eq(['conflict_their'] * 2) + expect(diff_line_types[31]).to eq('conflict_marker_their') + end + + # Swap the positions around due to conflicts/diffs display inconsistency + # https://gitlab.com/gitlab-org/gitlab/-/issues/291989 + it 'swaps the new and old positions around' do + lines = conflict_file.diff_lines_for_serializer + expect(lines.map(&:old_pos)[26..27]).to eq([21, 22]) + expect(lines.map(&:new_pos)[29..30]).to eq([21, 22]) end it 'does not add a match line to the end of the section' do @@ -124,13 +132,13 @@ RSpec.describe Gitlab::Conflict::File do expect(diff_line_types).to eq([ 'match', nil, nil, nil, - "conflict_marker", "conflict_marker_our", + "conflict_our", "conflict_marker", + "conflict_their", + "conflict_their", + "conflict_their", "conflict_marker_their", - "conflict_marker_their", - "conflict_marker_their", - "conflict_marker", nil, nil, nil, "match" ]) diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb deleted file mode 100644 index 056c1b5bc9f..00000000000 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::BaseEventFetcher do - let(:max_events) { 2 } - let(:project) { create(:project, :repository) } - let(:user) { create(:user, :admin) } - let(:start_time_attrs) { Issue.arel_table[:created_at] } - let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] } - let(:options) do - { start_time_attrs: start_time_attrs, - end_time_attrs: end_time_attrs, - from: 30.days.ago, - project: project } - end - - subject do - described_class.new(stage: :issue, - options: options).fetch - end - - before do - allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all) - allow_any_instance_of(described_class).to receive(:serialize) do |event| - event - end - allow_any_instance_of(described_class) - .to receive(:allowed_ids).and_return(nil) - - stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events) - - setup_events(count: 3) - end - - it 'limits the rows to the max number' do - expect(subject.count).to eq(max_events) - end - - def setup_events(count:) - count.times do - issue = create(:issue, project: project, created_at: 2.days.ago) - milestone = create(:milestone, project: project) - - issue.update(milestone: milestone) - create_merge_request_closing_issue(user, project, issue) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb deleted file mode 100644 index a1a173abe57..00000000000 --- a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::CodeEventFetcher do - let(:stage_name) { :code } - - it_behaves_like 'default query config' do - it 'has a default order' do - expect(event.order).not_to be_nil - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb deleted file mode 100644 index 17104715580..00000000000 --- a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::CodeStage do - let(:stage_name) { :code } - - let(:project) { create(:project) } - let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } - let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:mr_1) { create(:merge_request, source_project: project, created_at: 15.minutes.ago) } - let(:mr_2) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'A') } - let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } } - let(:stage) { described_class.new(options: stage_options) } - - before do - issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago) - issue_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) - issue_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) - create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') - create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) - create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) - end - - it_behaves_like 'base stage' - - context 'when using the new query backend' do - include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do - let(:expected_record_count) { 2 } - let(:expected_ordered_attribute_values) { [mr_2.title, mr_1.title] } - end - end - - describe '#project_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.project_median).to eq(ISSUES_MEDIAN) - end - - include_examples 'calculate #median with date range' - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that closes issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) - end - end - - context 'when group is given' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project_2) { create(:project, group: group) } - let(:project_3) { create(:project, group: group) } - let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) } - let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) } - let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) } - let(:mr_2_1) { create(:merge_request, source_project: project_2, created_at: 15.minutes.ago) } - let(:mr_2_2) { create(:merge_request, source_project: project_3, created_at: 10.minutes.ago, source_branch: 'A') } - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) } - - before do - group.add_owner(user) - issue_2_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago) - issue_2_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) - issue_2_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) - create(:merge_requests_closing_issues, merge_request: mr_2_1, issue: issue_2_1) - create(:merge_requests_closing_issues, merge_request: mr_2_2, issue: issue_2_2) - end - - describe '#group_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.group_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(mr_2_1.title, mr_2_2.title) - end - end - - context 'when subgroup is given' do - let(:subgroup) { create(:group, parent: group) } - let(:project_4) { create(:project, group: subgroup) } - let(:project_5) { create(:project, group: subgroup) } - let(:issue_3_1) { create(:issue, project: project_4, created_at: 90.minutes.ago) } - let(:issue_3_2) { create(:issue, project: project_5, created_at: 60.minutes.ago) } - let(:issue_3_3) { create(:issue, project: project_5, created_at: 60.minutes.ago) } - let(:mr_3_1) { create(:merge_request, source_project: project_4, created_at: 15.minutes.ago) } - let(:mr_3_2) { create(:merge_request, source_project: project_5, created_at: 10.minutes.ago, source_branch: 'A') } - - before do - issue_3_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago) - issue_3_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) - issue_3_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) - create(:merge_requests_closing_issues, merge_request: mr_3_1, issue: issue_3_1) - create(:merge_requests_closing_issues, merge_request: mr_3_2, issue: issue_3_2) - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(4) - expect(subject.map { |event| event[:title] }).to contain_exactly(mr_2_1.title, mr_2_2.title, mr_3_1.title, mr_3_2.title) - end - - it 'exposes merge requests that close issues with full path for subgroup' do - expect(subject.count).to eq(4) - expect(subject.find { |event| event[:title] == mr_3_1.title }[:url]).to include("#{subgroup.full_path}") - end - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb deleted file mode 100644 index 553f33a66c4..00000000000 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'value stream analytics events', :aggregate_failures do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user, :admin) } - let(:from_date) { 10.days.ago } - let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - - let(:events) do - CycleAnalytics::ProjectLevel - .new(project, options: { from: from_date, current_user: user })[stage] - .events - end - - let(:event) { events.first } - - before do - setup(context) - end - - describe '#issue_events' do - let(:stage) { :issue } - - it 'has correct attributes' do - expect(event[:total_time]).not_to be_empty - expect(event[:title]).to eq(context.title) - expect(event[:url]).not_to be_nil - expect(event[:iid]).to eq(context.iid.to_s) - expect(event[:created_at]).to end_with('ago') - expect(event[:author][:web_url]).not_to be_nil - expect(event[:author][:avatar_url]).not_to be_nil - expect(event[:author][:name]).to eq(context.author.name) - end - end - - describe '#plan_events' do - let(:stage) { :plan } - - before do - create_commit_referencing_issue(context) - - # Adding extra duration because the new VSA backend filters out 0 durations between these columns - context.metrics.update!(first_mentioned_in_commit_at: context.metrics.first_associated_with_milestone_at + 1.day) - end - - it 'has correct attributes' do - expect(event[:total_time]).not_to be_empty - expect(event[:title]).to eq(context.title) - expect(event[:url]).not_to be_nil - expect(event[:iid]).to eq(context.iid.to_s) - expect(event[:created_at]).to end_with('ago') - expect(event[:author][:web_url]).not_to be_nil - expect(event[:author][:avatar_url]).not_to be_nil - expect(event[:author][:name]).to eq(context.author.name) - end - end - - describe '#code_events' do - let(:stage) { :code } - let!(:merge_request) { MergeRequest.first } - - before do - create_commit_referencing_issue(context) - end - - it 'has correct attributes' do - expect(event[:total_time]).not_to be_empty - expect(event[:title]).to eq('Awesome merge_request') - expect(event[:iid]).to eq(context.iid.to_s) - expect(event[:created_at]).to end_with('ago') - expect(event[:author][:web_url]).not_to be_nil - expect(event[:author][:avatar_url]).not_to be_nil - expect(event[:author][:name]).to eq(MergeRequest.first.author.name) - end - end - - describe '#test_events', :sidekiq_might_not_need_inline do - let(:stage) { :test } - - let(:merge_request) { MergeRequest.first } - let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - - let!(:pipeline) do - create(:ci_pipeline, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha, - project: project, - head_pipeline_of: merge_request) - end - - before do - create(:ci_build, :success, pipeline: pipeline, author: user) - create(:ci_build, :success, pipeline: pipeline, author: user) - - pipeline.run! - pipeline.succeed! - merge_merge_requests_closing_issue(user, project, context) - end - - it 'has correct attributes' do - expect(event[:name]).not_to be_nil - expect(event[:id]).not_to be_nil - expect(event[:url]).not_to be_nil - expect(event[:branch]).not_to be_nil - expect(event[:branch][:url]).not_to be_nil - expect(event[:short_sha]).not_to be_nil - expect(event[:commit_url]).not_to be_nil - expect(event[:date]).not_to be_nil - expect(event[:total_time]).not_to be_empty - end - end - - describe '#review_events' do - let(:stage) { :review } - let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - - before do - merge_merge_requests_closing_issue(user, project, context) - end - - it 'has correct attributes' do - expect(event[:total_time]).not_to be_empty - expect(event[:title]).to eq('Awesome merge_request') - expect(event[:iid]).to eq(context.iid.to_s) - expect(event[:url]).not_to be_nil - expect(event[:state]).not_to be_nil - expect(event[:created_at]).not_to be_nil - expect(event[:author][:web_url]).not_to be_nil - expect(event[:author][:avatar_url]).not_to be_nil - expect(event[:author][:name]).to eq(MergeRequest.first.author.name) - end - end - - describe '#staging_events', :sidekiq_might_not_need_inline do - let(:stage) { :staging } - let(:merge_request) { MergeRequest.first } - - let!(:pipeline) do - create(:ci_pipeline, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha, - project: project, - head_pipeline_of: merge_request) - end - - before do - create(:ci_build, :success, pipeline: pipeline, author: user) - create(:ci_build, :success, pipeline: pipeline, author: user) - - pipeline.run! - pipeline.succeed! - - merge_merge_requests_closing_issue(user, project, context) - deploy_master(user, project) - end - - it 'has correct attributes' do - expect(event[:name]).not_to be_nil - expect(event[:id]).not_to be_nil - expect(event[:url]).not_to be_nil - expect(event[:branch]).not_to be_nil - expect(event[:branch][:url]).not_to be_nil - expect(event[:short_sha]).not_to be_nil - expect(event[:commit_url]).not_to be_nil - expect(event[:date]).not_to be_nil - expect(event[:total_time]).not_to be_empty - expect(event[:author][:web_url]).not_to be_nil - expect(event[:author][:avatar_url]).not_to be_nil - expect(event[:author][:name]).to eq(MergeRequest.first.author.name) - end - end - - def setup(context) - milestone = create(:milestone, project: project) - context.update!(milestone: milestone) - mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}") - - ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) - end -end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb deleted file mode 100644 index 7a49ee53e8f..00000000000 --- a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::IssueEventFetcher do - let(:stage_name) { :issue } - - it_behaves_like 'default query config' -end diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb deleted file mode 100644 index c7ab2b9b84b..00000000000 --- a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::IssueStage do - let(:stage_name) { :issue } - let(:project) { create(:project) } - let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } - let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) } - let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) } - let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } } - let(:stage) { described_class.new(options: stage_options) } - - before do - issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago ) - issue_2.metrics.update!(first_added_to_board_at: 30.minutes.ago) - issue_3.metrics.update!(first_added_to_board_at: 15.minutes.ago) - end - - it_behaves_like 'base stage' - - context 'when using the new query backend' do - include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do - let(:expected_record_count) { 3 } - let(:expected_ordered_attribute_values) { [issue_3.title, issue_2.title, issue_1.title] } - end - end - - describe '#median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.project_median).to eq(ISSUES_MEDIAN) - end - - include_examples 'calculate #median with date range' - end - - describe '#events' do - it 'exposes issues with metrics' do - result = stage.events - - expect(result.count).to eq(3) - expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title, issue_3.title) - end - end - context 'when group is given' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project_2) { create(:project, group: group) } - let(:project_3) { create(:project, group: group) } - let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) } - let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) } - let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) } - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) } - - before do - group.add_owner(user) - issue_2_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago) - issue_2_2.metrics.update!(first_added_to_board_at: 30.minutes.ago) - end - - describe '#group_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.group_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title) - end - end - - context 'when only part of projects is chosen' do - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group, projects: [project_2.id] }) } - - describe '#group_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.group_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(1) - expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title) - end - end - end - - context 'when subgroup is given' do - let(:subgroup) { create(:group, parent: group) } - let(:project_4) { create(:project, group: subgroup) } - let(:project_5) { create(:project, group: subgroup) } - let(:issue_3_1) { create(:issue, project: project_4, created_at: 90.minutes.ago) } - let(:issue_3_2) { create(:issue, project: project_5, created_at: 60.minutes.ago) } - let(:issue_3_3) { create(:issue, project: project_5, created_at: 60.minutes.ago) } - - before do - issue_3_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago) - issue_3_2.metrics.update!(first_added_to_board_at: 30.minutes.ago) - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(4) - expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title, issue_3_1.title, issue_3_2.title) - end - - it 'exposes merge requests that close issues with full path for subgroup' do - expect(subject.count).to eq(4) - expect(subject.find { |event| event[:title] == issue_3_1.title }[:url]).to include("#{subgroup.full_path}") - end - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb deleted file mode 100644 index bc14a772d34..00000000000 --- a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::PlanEventFetcher do - let(:stage_name) { :plan } - - it_behaves_like 'default query config' do - context 'no commits' do - it 'does not blow up if there are no commits' do - allow(event).to receive(:event_result).and_return([{}]) - - expect { event.fetch }.not_to raise_error - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb deleted file mode 100644 index 2547c05c025..00000000000 --- a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::PlanStage do - let(:stage_name) { :plan } - let(:project) { create(:project) } - let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } - let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } - let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) } - let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) } - let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } } - let(:stage) { described_class.new(options: stage_options) } - - before do - issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago) - issue_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago) - issue_3.metrics.update!(first_added_to_board_at: 15.minutes.ago) - end - - it_behaves_like 'base stage' - - context 'when using the new query backend' do - include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do - let(:expected_record_count) { 2 } - let(:expected_ordered_attribute_values) { [issue_1.title, issue_2.title] } - end - end - - describe '#project_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.project_median).to eq(ISSUES_MEDIAN) - end - - include_examples 'calculate #median with date range' - end - - describe '#events' do - subject { stage.events } - - it 'exposes issues with metrics' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title) - end - end - - context 'when group is given' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project_2) { create(:project, group: group) } - let(:project_3) { create(:project, group: group) } - let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) } - let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) } - let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) } - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) } - - before do - group.add_owner(user) - issue_2_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago) - issue_2_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago) - issue_2_3.metrics.update!(first_added_to_board_at: 15.minutes.ago) - end - - describe '#group_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.group_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title) - end - end - - context 'when subgroup is given' do - let(:subgroup) { create(:group, parent: group) } - let(:project_4) { create(:project, group: subgroup) } - let(:project_5) { create(:project, group: subgroup) } - let(:issue_3_1) { create(:issue, project: project_4, created_at: 90.minutes.ago) } - let(:issue_3_2) { create(:issue, project: project_5, created_at: 60.minutes.ago) } - let(:issue_3_3) { create(:issue, project: project_5, created_at: 60.minutes.ago) } - - before do - issue_3_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago) - issue_3_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago) - issue_3_3.metrics.update!(first_added_to_board_at: 15.minutes.ago) - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(4) - expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title, issue_3_1.title, issue_3_2.title) - end - - it 'exposes merge requests that close issues with full path for subgroup' do - expect(subject.count).to eq(4) - expect(subject.find { |event| event[:title] == issue_3_1.title }[:url]).to include("#{subgroup.full_path}") - end - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb deleted file mode 100644 index 86b07a95cbb..00000000000 --- a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::ProductionEventFetcher do - let(:stage_name) { :production } - - it_behaves_like 'default query config' -end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb deleted file mode 100644 index fe13cc6b065..00000000000 --- a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::ReviewEventFetcher do - let(:stage_name) { :review } - - it_behaves_like 'default query config' -end diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb deleted file mode 100644 index 5593013740e..00000000000 --- a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::ReviewStage do - let(:stage_name) { :review } - let(:project) { create(:project) } - let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } - let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) } - let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') } - let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') } - let!(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') } - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) } - - before do - mr_1.metrics.update!(merged_at: 30.minutes.ago) - mr_2.metrics.update!(merged_at: 10.minutes.ago) - - create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) - create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) - create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3) - end - - it_behaves_like 'base stage' - - describe '#project_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.project_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) - end - end - - context 'when group is given' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project_2) { create(:project, group: group) } - let(:project_3) { create(:project, group: group) } - let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) } - let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) } - let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) } - let(:mr_2_1) { create(:merge_request, :closed, source_project: project_2, created_at: 60.minutes.ago) } - let(:mr_2_2) { create(:merge_request, :closed, source_project: project_3, created_at: 40.minutes.ago, source_branch: 'A') } - let(:mr_2_3) { create(:merge_request, source_project: project_2, created_at: 10.minutes.ago, source_branch: 'B') } - let!(:mr_2_4) { create(:merge_request, source_project: project_3, created_at: 10.minutes.ago, source_branch: 'C') } - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) } - - before do - group.add_owner(user) - mr_2_1.metrics.update!(merged_at: 30.minutes.ago) - mr_2_2.metrics.update!(merged_at: 10.minutes.ago) - - create(:merge_requests_closing_issues, merge_request: mr_2_1, issue: issue_2_1) - create(:merge_requests_closing_issues, merge_request: mr_2_2, issue: issue_2_2) - create(:merge_requests_closing_issues, merge_request: mr_2_3, issue: issue_2_3) - end - - describe '#group_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.group_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:title] }).to contain_exactly(mr_2_1.title, mr_2_2.title) - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb deleted file mode 100644 index bdf1b99c4c9..00000000000 --- a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::StagingEventFetcher do - let(:stage_name) { :staging } - - it_behaves_like 'default query config' do - it 'has a default order' do - expect(event.order).not_to be_nil - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb deleted file mode 100644 index 852f7041dc6..00000000000 --- a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::StagingStage do - let(:stage_name) { :staging } - - let(:project) { create(:project) } - let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } - let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) } - let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) } - let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') } - let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') } - let(:build_1) { create(:ci_build, project: project) } - let(:build_2) { create(:ci_build, project: project) } - - let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } } - let(:stage) { described_class.new(options: stage_options) } - - before do - mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id) - mr_2.metrics.update!(merged_at: 60.minutes.ago, first_deployed_to_production_at: 30.minutes.ago, pipeline_id: build_2.commit_id) - mr_3.metrics.update!(merged_at: 10.minutes.ago, first_deployed_to_production_at: 3.days.ago, pipeline_id: create(:ci_build, project: project).commit_id) - - create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) - create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) - create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3) - end - - it_behaves_like 'base stage' - - describe '#project_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.project_median).to eq(ISSUES_MEDIAN) - end - - it_behaves_like 'calculate #median with date range' - end - - describe '#events' do - subject { stage.events } - - it 'exposes builds connected to merge request' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name) - end - end - - context 'when group is given' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project_2) { create(:project, group: group) } - let(:project_3) { create(:project, group: group) } - let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) } - let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) } - let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) } - let(:mr_1) { create(:merge_request, :closed, source_project: project_2, created_at: 60.minutes.ago) } - let(:mr_2) { create(:merge_request, :closed, source_project: project_3, created_at: 40.minutes.ago, source_branch: 'A') } - let(:mr_3) { create(:merge_request, source_project: project_2, created_at: 10.minutes.ago, source_branch: 'B') } - let(:build_1) { create(:ci_build, project: project_2) } - let(:build_2) { create(:ci_build, project: project_3) } - let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) } - - before do - group.add_owner(user) - mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id) - mr_2.metrics.update!(merged_at: 60.minutes.ago, first_deployed_to_production_at: 30.minutes.ago, pipeline_id: build_2.commit_id) - mr_3.metrics.update!(merged_at: 10.minutes.ago, first_deployed_to_production_at: 3.days.ago, pipeline_id: create(:ci_build, project: project_2).commit_id) - - create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_2_1) - create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2_2) - create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_2_3) - end - - describe '#group_median' do - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.group_median).to eq(ISSUES_MEDIAN) - end - end - - describe '#events' do - subject { stage.events } - - it 'exposes merge requests that close issues' do - expect(subject.count).to eq(2) - expect(subject.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name) - end - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb deleted file mode 100644 index 1277385d0b4..00000000000 --- a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::TestEventFetcher do - let(:stage_name) { :test } - - it_behaves_like 'default query config' do - it 'has a default order' do - expect(event.order).not_to be_nil - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb deleted file mode 100644 index 49ee6624260..00000000000 --- a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::TestStage do - let(:stage_name) { :test } - let(:project) { create(:project) } - let(:stage_options) { { from: 2.days.ago, current_user: project.creator, project: project } } - let(:stage) { described_class.new(options: stage_options) } - - it_behaves_like 'base stage' - - describe '#median' do - let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) } - let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') } - let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') } - let(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') } - let(:mr_5) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D') } - let(:ci_build1) { create(:ci_build, project: project) } - let(:ci_build2) { create(:ci_build, project: project) } - - before do - issue_1 = create(:issue, project: project, created_at: 90.minutes.ago) - issue_2 = create(:issue, project: project, created_at: 60.minutes.ago) - issue_3 = create(:issue, project: project, created_at: 60.minutes.ago) - mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago, pipeline_id: ci_build1.commit_id) - mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago, pipeline_id: ci_build2.commit_id) - mr_3.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) - mr_4.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) - mr_5.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) - - create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) - create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) - create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3) - create(:merge_requests_closing_issues, merge_request: mr_4, issue: issue_3) - create(:merge_requests_closing_issues, merge_request: mr_5, issue: issue_3) - end - - around do |example| - freeze_time { example.run } - end - - it 'counts median from issues with metrics' do - expect(stage.project_median).to eq(ISSUES_MEDIAN) - end - - include_examples 'calculate #median with date range' - - context 'when using the new query backend' do - include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do - let(:expected_record_count) { 2 } - let(:attribute_to_verify) { :id } - let(:expected_ordered_attribute_values) { [mr_1.metrics.pipeline.builds.first.id, mr_2.metrics.pipeline.builds.first.id] } - end - end - end -end diff --git a/spec/lib/gitlab/danger/base_linter_spec.rb b/spec/lib/gitlab/danger/base_linter_spec.rb index bd0ceb5a125..0136a0278ae 100644 --- a/spec/lib/gitlab/danger/base_linter_spec.rb +++ b/spec/lib/gitlab/danger/base_linter_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'rspec-parameterized' require_relative 'danger_spec_helper' require 'gitlab/danger/base_linter' @@ -70,19 +71,57 @@ RSpec.describe Gitlab::Danger::BaseLinter do end end - context 'when subject is a WIP' do + context 'when ignoring length issues for subject having not-ready wording' do + using RSpec::Parameterized::TableSyntax + let(:final_message) { 'A B C' } - # commit message with prefix will be over max length. commit message without prefix will be of maximum size - let(:commit_message) { described_class::WIP_PREFIX + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) } - it 'does not have any problems' do - commit_linter.lint_subject + context 'when used as prefix' do + where(prefix: [ + 'WIP: ', + 'WIP:', + 'wIp:', + '[WIP] ', + '[WIP]', + '[draft]', + '[draft] ', + '(draft)', + '(draft) ', + 'draft - ', + 'draft: ', + 'draft:', + 'DRAFT:' + ]) + + with_them do + it 'does not have any problems' do + commit_message = prefix + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) + commit = commit_class.new(commit_message, anything, anything) + + linter = described_class.new(commit).lint_subject + + expect(linter.problems).to be_empty + end + end + end - expect(commit_linter.problems).to be_empty + context 'when used as suffix' do + where(suffix: %w[WIP draft]) + + with_them do + it 'does not have any problems' do + commit_message = final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) + suffix + commit = commit_class.new(commit_message, anything, anything) + + linter = described_class.new(commit).lint_subject + + expect(linter.problems).to be_empty + end + end end end - context 'when subject is too short and too long' do + context 'when subject does not have enough words and is too long' do let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH } it 'adds a problem' do diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb index 2da60f4f8bd..04c515f1205 100644 --- a/spec/lib/gitlab/danger/changelog_spec.rb +++ b/spec/lib/gitlab/danger/changelog_spec.rb @@ -150,41 +150,80 @@ RSpec.describe Gitlab::Danger::Changelog do end describe '#modified_text' do - let(:sanitize_mr_title) { 'Fake Title' } let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } subject { changelog.modified_text } - it do - expect(subject).to include('CHANGELOG.md was edited') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') + context "when title is not changed from sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'Fake Title' } + + specify do + expect(subject).to include('CHANGELOG.md was edited') + expect(subject).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') + end + end + + context "when title needs sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'DRAFT: Fake Title' } + + specify do + expect(subject).to include('CHANGELOG.md was edited') + expect(subject).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') + end end end describe '#required_text' do - let(:sanitize_mr_title) { 'Fake Title' } let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } subject { changelog.required_text } - it do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).not_to include('--ee') + context "when title is not changed from sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'Fake Title' } + + specify do + expect(subject).to include('CHANGELOG missing') + expect(subject).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject).not_to include('--ee') + end + end + + context "when title needs sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'DRAFT: Fake Title' } + + specify do + expect(subject).to include('CHANGELOG missing') + expect(subject).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject).not_to include('--ee') + end end end - describe 'optional_text' do - let(:sanitize_mr_title) { 'Fake Title' } + describe '#optional_text' do let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } subject { changelog.optional_text } - it do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') + context "when title is not changed from sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'Fake Title' } + + specify do + expect(subject).to include('CHANGELOG missing') + expect(subject).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') + end + end + + context "when title needs sanitization", :aggregate_failures do + let(:sanitize_mr_title) { 'DRAFT: Fake Title' } + + specify do + expect(subject).to include('CHANGELOG missing') + expect(subject).to include('bin/changelog -m 1234 "Fake Title"') + expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') + end end end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index a8f113a8cd1..bd5c746dd54 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -351,33 +351,23 @@ RSpec.describe Gitlab::Danger::Helper do end context 'having specific changes' do - it 'has database and backend categories' do - changed_files = ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb'] - - changed_files.each do |file| - allow(fake_git).to receive(:diff_for_file).with(file) { double(:diff, patch: "+ count(User.active)") } - - expect(helper.categories_for_file(file)).to eq([:database, :backend]) - end - end - - it 'has backend category' do - allow(fake_git).to receive(:diff_for_file).with('usage_data.rb') { double(:diff, patch: "+ alt_usage_data(User.active)") } - - expect(helper.categories_for_file('usage_data.rb')).to eq([:backend]) - end - - it 'has backend category for changes outside usage_data files' do - allow(fake_git).to receive(:diff_for_file).with('user.rb') { double(:diff, patch: "+ count(User.active)") } - - expect(helper.categories_for_file('user.rb')).to eq([:backend]) + where(:expected_categories, :patch, :changed_files) do + [:database, :backend] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb'] + [:database, :backend] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb'] + [:backend] | '+ alt_usage_data(User.active)' | ['usage_data.rb'] + [:backend] | '+ count(User.active)' | ['user.rb'] + [:backend] | '+ count(User.active)' | ['usage_data/topology.rb'] + [:backend] | '+ foo_count(User.active)' | ['usage_data.rb'] end - it 'has backend category for files that are not usage_data.rb' do - changed_file = 'usage_data/topology.rb' - allow(fake_git).to receive(:diff_for_file).with(changed_file) { double(:diff, patch: "+ count(User.active)") } + with_them do + it 'has the correct categories' do + changed_files.each do |file| + allow(fake_git).to receive(:diff_for_file).with(file) { double(:diff, patch: patch) } - expect(helper.categories_for_file(changed_file)).to eq([:backend]) + expect(helper.categories_for_file(file)).to eq(expected_categories) + end + end end end end @@ -412,24 +402,6 @@ RSpec.describe Gitlab::Danger::Helper do end end - describe '#sanitize_mr_title' do - where(:mr_title, :expected_mr_title) do - 'My MR title' | 'My MR title' - 'WIP: My MR title' | 'My MR title' - 'Draft: My MR title' | 'My MR title' - '(Draft) My MR title' | 'My MR title' - '[Draft] My MR title' | 'My MR title' - '[DRAFT] My MR title' | 'My MR title' - 'DRAFT: My MR title' | 'My MR title' - end - - with_them do - subject { helper.sanitize_mr_title(mr_title) } - - it { is_expected.to eq(expected_mr_title) } - end - end - describe '#security_mr?' do it 'returns false when `gitlab_helper` is unavailable' do expect(helper).to receive(:gitlab_helper).and_return(nil) diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb index 561e108bf31..59ac3b12b6b 100644 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -245,69 +245,6 @@ RSpec.describe Gitlab::Danger::Roulette do end end end - - describe 'reviewer suggestion probability' do - let(:reviewer) { teammate_with_capability('reviewer', 'reviewer backend') } - let(:hungry_reviewer) { teammate_with_capability('hungry_reviewer', 'reviewer backend', hungry: true) } - let(:traintainer) { teammate_with_capability('traintainer', 'trainee_maintainer backend') } - let(:hungry_traintainer) { teammate_with_capability('hungry_traintainer', 'trainee_maintainer backend', hungry: true) } - let(:teammates) do - [ - reviewer.to_h, - hungry_reviewer.to_h, - traintainer.to_h, - hungry_traintainer.to_h - ] - end - - let(:categories) { [:backend] } - - # This test is testing probability with inherent randomness. - # The variance is inversely related to sample size - # Given large enough sample size, the variance would be smaller, - # but the test would take longer. - # Given smaller sample size, the variance would be larger, - # but the test would take less time. - let!(:sample_size) { 500 } - let!(:variance) { 0.1 } - - before do - # This test needs actual randomness to simulate probabilities - allow(subject).to receive(:new_random).and_return(Random.new) - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(body: teammate_json) - end - - it 'has 1:2:3:4 probability of picking reviewer, hungry_reviewer, traintainer, hungry_traintainer' do - picks = Array.new(sample_size).map do - spins = subject.spin(project, categories, timezone_experiment: timezone_experiment) - spins.first.reviewer.name - end - - expect(probability(picks, 'reviewer')).to be_within(variance).of(0.1) - expect(probability(picks, 'hungry_reviewer')).to be_within(variance).of(0.2) - expect(probability(picks, 'traintainer')).to be_within(variance).of(0.3) - expect(probability(picks, 'hungry_traintainer')).to be_within(variance).of(0.4) - end - - def probability(picks, role) - picks.count(role).to_f / picks.length - end - - def teammate_with_capability(name, capability, hungry: false) - Gitlab::Danger::Teammate.new( - { - 'name' => name, - 'projects' => { - 'gitlab' => capability - }, - 'available' => true, - 'hungry' => hungry - } - ) - end - end end RSpec::Matchers.define :match_teammates do |expected| diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb index eebe14ed5e1..9c066ba4c1b 100644 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -121,6 +121,14 @@ RSpec.describe Gitlab::Danger::Teammate do end end + context 'when capabilities include maintainer engineering productivity' do + let(:capabilities) { ['maintainer engineering_productivity'] } + + it '#maintainer? returns true' do + expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_truthy + end + end + context 'when capabilities include trainee_maintainer backend' do let(:capabilities) { ['trainee_maintainer backend'] } diff --git a/spec/lib/gitlab/danger/title_linting_spec.rb b/spec/lib/gitlab/danger/title_linting_spec.rb new file mode 100644 index 00000000000..b48d2c5e53d --- /dev/null +++ b/spec/lib/gitlab/danger/title_linting_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +require 'gitlab/danger/title_linting' + +RSpec.describe Gitlab::Danger::TitleLinting do + using RSpec::Parameterized::TableSyntax + + describe '#sanitize_mr_title' do + where(:mr_title, :expected_mr_title) do + '`My MR title`' | "\\`My MR title\\`" + 'WIP: My MR title' | 'My MR title' + 'Draft: My MR title' | 'My MR title' + '(Draft) My MR title' | 'My MR title' + '[Draft] My MR title' | 'My MR title' + '[DRAFT] My MR title' | 'My MR title' + 'DRAFT: My MR title' | 'My MR title' + 'DRAFT: `My MR title`' | "\\`My MR title\\`" + end + + with_them do + subject { described_class.sanitize_mr_title(mr_title) } + + it { is_expected.to eq(expected_mr_title) } + end + end + + describe '#remove_draft_flag' do + where(:mr_title, :expected_mr_title) do + 'WIP: My MR title' | 'My MR title' + 'Draft: My MR title' | 'My MR title' + '(Draft) My MR title' | 'My MR title' + '[Draft] My MR title' | 'My MR title' + '[DRAFT] My MR title' | 'My MR title' + 'DRAFT: My MR title' | 'My MR title' + end + + with_them do + subject { described_class.remove_draft_flag(mr_title) } + + it { is_expected.to eq(expected_mr_title) } + end + end + + describe '#has_draft_flag?' do + it 'returns true for a draft title' do + expect(described_class.has_draft_flag?('Draft: My MR title')).to be true + end + + it 'returns false for non draft title' do + expect(described_class.has_draft_flag?('My MR title')).to be false + end + end +end diff --git a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb b/spec/lib/gitlab/danger/weightage/maintainers_spec.rb new file mode 100644 index 00000000000..066bb487fa2 --- /dev/null +++ b/spec/lib/gitlab/danger/weightage/maintainers_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'gitlab/danger/weightage/maintainers' + +RSpec.describe Gitlab::Danger::Weightage::Maintainers do + let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER } + let(:regular_maintainer) { double('Teammate', reduced_capacity: false) } + let(:reduced_capacity_maintainer) { double('Teammate', reduced_capacity: true) } + let(:maintainers) do + [ + regular_maintainer, + reduced_capacity_maintainer + ] + end + + let(:maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier } + let(:reduced_capacity_maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT } + + subject(:weighted_maintainers) { described_class.new(maintainers).execute } + + describe '#execute' do + it 'weights the maintainers overall' do + expect(weighted_maintainers.count).to eq maintainer_count + reduced_capacity_maintainer_count + end + + it 'has total count of regular maintainers' do + expect(weighted_maintainers.count { |r| r.object_id == regular_maintainer.object_id }).to eq maintainer_count + end + + it 'has count of reduced capacity maintainers' do + expect(weighted_maintainers.count { |r| r.object_id == reduced_capacity_maintainer.object_id }).to eq reduced_capacity_maintainer_count + end + end +end diff --git a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb b/spec/lib/gitlab/danger/weightage/reviewers_spec.rb new file mode 100644 index 00000000000..cca81f4d9b5 --- /dev/null +++ b/spec/lib/gitlab/danger/weightage/reviewers_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'gitlab/danger/weightage/reviewers' + +RSpec.describe Gitlab::Danger::Weightage::Reviewers do + let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER } + let(:regular_reviewer) { double('Teammate', hungry: false, reduced_capacity: false) } + let(:hungry_reviewer) { double('Teammate', hungry: true, reduced_capacity: false) } + let(:reduced_capacity_reviewer) { double('Teammate', hungry: false, reduced_capacity: true) } + let(:reviewers) do + [ + hungry_reviewer, + regular_reviewer, + reduced_capacity_reviewer + ] + end + + let(:regular_traintainer) { double('Teammate', hungry: false, reduced_capacity: false) } + let(:hungry_traintainer) { double('Teammate', hungry: true, reduced_capacity: false) } + let(:reduced_capacity_traintainer) { double('Teammate', hungry: false, reduced_capacity: true) } + let(:traintainers) do + [ + hungry_traintainer, + regular_traintainer, + reduced_capacity_traintainer + ] + end + + let(:hungry_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT } + let(:hungry_traintainer_count) { described_class::TRAINTAINER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT } + let(:reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier } + let(:traintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * described_class::TRAINTAINER_WEIGHT * multiplier } + let(:reduced_capacity_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT } + let(:reduced_capacity_traintainer_count) { described_class::TRAINTAINER_WEIGHT } + + subject(:weighted_reviewers) { described_class.new(reviewers, traintainers).execute } + + describe '#execute', :aggregate_failures do + it 'weights the reviewers overall' do + reviewers_count = hungry_reviewer_count + reviewer_count + reduced_capacity_reviewer_count + traintainers_count = hungry_traintainer_count + traintainer_count + reduced_capacity_traintainer_count + + expect(weighted_reviewers.count).to eq reviewers_count + traintainers_count + end + + it 'has total count of hungry reviewers and traintainers' do + expect(weighted_reviewers.count(&:hungry)).to eq hungry_reviewer_count + hungry_traintainer_count + expect(weighted_reviewers.count { |r| r.object_id == hungry_reviewer.object_id }).to eq hungry_reviewer_count + expect(weighted_reviewers.count { |r| r.object_id == hungry_traintainer.object_id }).to eq hungry_traintainer_count + end + + it 'has total count of regular reviewers and traintainers' do + expect(weighted_reviewers.count { |r| r.object_id == regular_reviewer.object_id }).to eq reviewer_count + expect(weighted_reviewers.count { |r| r.object_id == regular_traintainer.object_id }).to eq traintainer_count + end + + it 'has count of reduced capacity reviewers' do + expect(weighted_reviewers.count(&:reduced_capacity)).to eq reduced_capacity_reviewer_count + reduced_capacity_traintainer_count + expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_reviewer.object_id }).to eq reduced_capacity_reviewer_count + expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_traintainer.object_id }).to eq reduced_capacity_traintainer_count + end + end +end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index cfaaf849b09..2f74e766a11 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -26,6 +26,7 @@ RSpec.describe Gitlab::DataBuilder::Build do it { expect(data[:user]).to eq( { + id: user.id, name: user.name, username: user.username, avatar_url: user.avatar_url(only_path: false), diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index e5dfff33a2a..297d87708d8 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -41,6 +41,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do expect(project_data).to eq(project.hook_attrs(backward: false)) expect(data[:merge_request]).to be_nil expect(data[:user]).to eq({ + id: user.id, name: user.name, username: user.username, avatar_url: user.avatar_url(only_path: false), diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index a763dc08b73..6b709cba5b3 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Database::MigrationHelpers do + include Database::TableSchemaHelpers + let(:model) do ActiveRecord::Migration.new.extend(described_class) end @@ -96,6 +98,131 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#create_table_with_constraints' do + let(:table_name) { :test_table } + let(:column_attributes) do + [ + { name: 'id', sql_type: 'bigint', null: false, default: nil }, + { name: 'created_at', sql_type: 'timestamp with time zone', null: false, default: nil }, + { name: 'updated_at', sql_type: 'timestamp with time zone', null: false, default: nil }, + { name: 'some_id', sql_type: 'integer', null: false, default: nil }, + { name: 'active', sql_type: 'boolean', null: false, default: 'true' }, + { name: 'name', sql_type: 'text', null: true, default: nil } + ] + end + + before do + allow(model).to receive(:transaction_open?).and_return(true) + end + + context 'when no check constraints are defined' do + it 'creates the table as expected' do + model.create_table_with_constraints table_name do |t| + t.timestamps_with_timezone + t.integer :some_id, null: false + t.boolean :active, null: false, default: true + t.text :name + end + + expect_table_columns_to_match(column_attributes, table_name) + end + end + + context 'when check constraints are defined' do + context 'when the text_limit is explicity named' do + it 'creates the table as expected' do + model.create_table_with_constraints table_name do |t| + t.timestamps_with_timezone + t.integer :some_id, null: false + t.boolean :active, null: false, default: true + t.text :name + + t.text_limit :name, 255, name: 'check_name_length' + t.check_constraint :some_id_is_positive, 'some_id > 0' + end + + expect_table_columns_to_match(column_attributes, table_name) + + expect_check_constraint(table_name, 'check_name_length', 'char_length(name) <= 255') + expect_check_constraint(table_name, 'some_id_is_positive', 'some_id > 0') + end + end + + context 'when the text_limit is not named' do + it 'creates the table as expected, naming the text limit' do + model.create_table_with_constraints table_name do |t| + t.timestamps_with_timezone + t.integer :some_id, null: false + t.boolean :active, null: false, default: true + t.text :name + + t.text_limit :name, 255 + t.check_constraint :some_id_is_positive, 'some_id > 0' + end + + expect_table_columns_to_match(column_attributes, table_name) + + expect_check_constraint(table_name, 'check_cda6f69506', 'char_length(name) <= 255') + expect_check_constraint(table_name, 'some_id_is_positive', 'some_id > 0') + end + end + + it 'runs the change within a with_lock_retries' do + expect(model).to receive(:with_lock_retries).ordered.and_yield + expect(model).to receive(:create_table).ordered.and_call_original + expect(model).to receive(:execute).with(<<~SQL).ordered + ALTER TABLE "#{table_name}"\nADD CONSTRAINT "check_cda6f69506" CHECK (char_length("name") <= 255) + SQL + + model.create_table_with_constraints table_name do |t| + t.text :name + t.text_limit :name, 255 + end + end + + context 'when constraints are given invalid names' do + let(:expected_max_length) { described_class::MAX_IDENTIFIER_NAME_LENGTH } + let(:expected_error_message) { "The maximum allowed constraint name is #{expected_max_length} characters" } + + context 'when the explicit text limit name is not valid' do + it 'raises an error' do + too_long_length = expected_max_length + 1 + + expect do + model.create_table_with_constraints table_name do |t| + t.timestamps_with_timezone + t.integer :some_id, null: false + t.boolean :active, null: false, default: true + t.text :name + + t.text_limit :name, 255, name: ('a' * too_long_length) + t.check_constraint :some_id_is_positive, 'some_id > 0' + end + end.to raise_error(expected_error_message) + end + end + + context 'when a check constraint name is not valid' do + it 'raises an error' do + too_long_length = expected_max_length + 1 + + expect do + model.create_table_with_constraints table_name do |t| + t.timestamps_with_timezone + t.integer :some_id, null: false + t.boolean :active, null: false, default: true + t.text :name + + t.text_limit :name, 255 + t.check_constraint ('a' * too_long_length), 'some_id > 0' + end + end.to raise_error(expected_error_message) + end + end + end + end + end + describe '#add_concurrent_index' do context 'outside a transaction' do before do @@ -1548,6 +1675,69 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#initialize_conversion_of_integer_to_bigint' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:issue) { create(:issue, project: project) } + let!(:event) do + create(:event, :created, project: project, target: issue, author: user) + end + + context 'in a transaction' do + it 'raises RuntimeError' do + allow(model).to receive(:transaction_open?).and_return(true) + + expect { model.initialize_conversion_of_integer_to_bigint(:events, :id) } + .to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + it 'creates a bigint column and starts backfilling it' do + expect(model) + .to receive(:add_column) + .with( + :events, + 'id_convert_to_bigint', + :bigint, + default: 0, + null: false + ) + + expect(model) + .to receive(:install_rename_triggers) + .with(:events, :id, 'id_convert_to_bigint') + + expect(model).to receive(:queue_background_migration_jobs_by_range_at_intervals).and_call_original + + expect(BackgroundMigrationWorker) + .to receive(:perform_in) + .ordered + .with( + 2.minutes, + 'CopyColumnUsingBackgroundMigrationJob', + [event.id, event.id, :events, :id, :id, 'id_convert_to_bigint', 100] + ) + + expect(Gitlab::BackgroundMigration) + .to receive(:steal) + .ordered + .with('CopyColumnUsingBackgroundMigrationJob') + + model.initialize_conversion_of_integer_to_bigint( + :events, + :id, + batch_size: 300, + sub_batch_size: 100 + ) + end + end + end + describe '#index_exists_by_name?' do it 'returns true if an index exists' do ActiveRecord::Base.connection.execute( diff --git a/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb b/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb index 56399941662..ec89f2ed61c 100644 --- a/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb +++ b/spec/lib/gitlab/database/partitioning/partition_creator_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Partitioning::PartitionCreator do - include PartitioningHelpers + include Database::PartitioningHelpers include ExclusiveLeaseHelpers describe '.register' do diff --git a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb index d47666eeffd..8e27797208c 100644 --- a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do - include TableSchemaHelpers + include Database::TableSchemaHelpers subject(:replace_table) { described_class.new(original_table, replacement_table, archived_table, 'id').perform } diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb index 7d88c17c9b3..93dbd9d7c30 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers do - include TriggerHelpers + include Database::TriggerHelpers let(:model) do ActiveRecord::Migration.new.extend(described_class) diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb index 7f61ff759fc..603f3dc41af 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do - include TableSchemaHelpers + include Database::TableSchemaHelpers let(:migration) do ActiveRecord::Migration.new.extend(described_class) diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index f10ff704c17..b50e02c7043 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -3,25 +3,36 @@ require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers do - include PartitioningHelpers - include TriggerHelpers - include TableSchemaHelpers + include Database::PartitioningHelpers + include Database::TriggerHelpers + include Database::TableSchemaHelpers let(:migration) do ActiveRecord::Migration.new.extend(described_class) end let_it_be(:connection) { ActiveRecord::Base.connection } - let(:source_table) { :audit_events } + let(:source_table) { :_test_original_table } let(:partitioned_table) { '_test_migration_partitioned_table' } let(:function_name) { '_test_migration_function_name' } let(:trigger_name) { '_test_migration_trigger_name' } let(:partition_column) { 'created_at' } let(:min_date) { Date.new(2019, 12) } let(:max_date) { Date.new(2020, 3) } + let(:source_model) { Class.new(ActiveRecord::Base) } before do allow(migration).to receive(:puts) + + migration.create_table source_table do |t| + t.string :name, null: false + t.integer :age, null: false + t.datetime partition_column + t.datetime :updated_at + end + + source_model.table_name = source_table + allow(migration).to receive(:transaction_open?).and_return(false) allow(migration).to receive(:make_partitioned_table_name).and_return(partitioned_table) allow(migration).to receive(:make_sync_function_name).and_return(function_name) @@ -81,14 +92,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when the given table does not have a primary key' do - let(:source_table) { :_partitioning_migration_helper_test_table } - let(:partition_column) { :some_field } - it 'raises an error' do - migration.create_table source_table, id: false do |t| - t.integer :id - t.datetime partition_column - end + migration.execute(<<~SQL) + ALTER TABLE #{source_table} + DROP CONSTRAINT #{source_table}_pkey + SQL expect do migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date @@ -97,12 +105,12 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when an invalid partition column is given' do - let(:partition_column) { :_this_is_not_real } + let(:invalid_column) { :_this_is_not_real } it 'raises an error' do expect do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - end.to raise_error(/partition column #{partition_column} does not exist/) + migration.partition_table_by_date source_table, invalid_column, min_date: min_date, max_date: max_date + end.to raise_error(/partition column #{invalid_column} does not exist/) end end @@ -126,19 +134,19 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe context 'with a non-integer primary key datatype' do before do - connection.create_table :another_example, id: false do |t| + connection.create_table non_int_table, id: false do |t| t.string :identifier, primary_key: true t.timestamp :created_at end end - let(:source_table) { :another_example } + let(:non_int_table) { :another_example } let(:old_primary_key) { 'identifier' } it 'does not change the primary key datatype' do - migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date non_int_table, partition_column, min_date: min_date, max_date: max_date - original_pk_column = connection.columns(source_table).find { |c| c.name == old_primary_key } + original_pk_column = connection.columns(non_int_table).find { |c| c.name == old_primary_key } pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } expect(pk_column).not_to be_nil @@ -176,11 +184,9 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when min_date is not given' do - let(:source_table) { :todos } - context 'with records present already' do before do - create(:todo, created_at: Date.parse('2019-11-05')) + source_model.create!(name: 'Test', age: 10, created_at: Date.parse('2019-11-05')) end it 'creates a partition spanning over each month from the first record' do @@ -248,13 +254,12 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end describe 'keeping data in sync with the partitioned table' do - let(:source_table) { :todos } - let(:model) { Class.new(ActiveRecord::Base) } + let(:partitioned_model) { Class.new(ActiveRecord::Base) } let(:timestamp) { Time.utc(2019, 12, 1, 12).round } before do - model.primary_key = :id - model.table_name = partitioned_table + partitioned_model.primary_key = :id + partitioned_model.table_name = partitioned_table end it 'creates a trigger function on the original table' do @@ -270,50 +275,50 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'syncs inserts to the partitioned tables' do migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - expect(model.count).to eq(0) + expect(partitioned_model.count).to eq(0) - first_todo = create(:todo, created_at: timestamp, updated_at: timestamp) - second_todo = create(:todo, created_at: timestamp, updated_at: timestamp) + first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) + second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) - expect(model.count).to eq(2) - expect(model.find(first_todo.id).attributes).to eq(first_todo.attributes) - expect(model.find(second_todo.id).attributes).to eq(second_todo.attributes) + expect(partitioned_model.count).to eq(2) + expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes) + expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes) end it 'syncs updates to the partitioned tables' do migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - first_todo = create(:todo, :pending, commit_id: nil, created_at: timestamp, updated_at: timestamp) - second_todo = create(:todo, created_at: timestamp, updated_at: timestamp) + first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) + second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) - expect(model.count).to eq(2) + expect(partitioned_model.count).to eq(2) - first_copy = model.find(first_todo.id) - second_copy = model.find(second_todo.id) + first_copy = partitioned_model.find(first_record.id) + second_copy = partitioned_model.find(second_record.id) - expect(first_copy.attributes).to eq(first_todo.attributes) - expect(second_copy.attributes).to eq(second_todo.attributes) + expect(first_copy.attributes).to eq(first_record.attributes) + expect(second_copy.attributes).to eq(second_record.attributes) - first_todo.update(state_event: 'done', commit_id: 'abc123', updated_at: timestamp + 1.second) + first_record.update!(age: 21, updated_at: timestamp + 1.hour) - expect(model.count).to eq(2) - expect(first_copy.reload.attributes).to eq(first_todo.attributes) - expect(second_copy.reload.attributes).to eq(second_todo.attributes) + expect(partitioned_model.count).to eq(2) + expect(first_copy.reload.attributes).to eq(first_record.attributes) + expect(second_copy.reload.attributes).to eq(second_record.attributes) end it 'syncs deletes to the partitioned tables' do migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - first_todo = create(:todo, created_at: timestamp, updated_at: timestamp) - second_todo = create(:todo, created_at: timestamp, updated_at: timestamp) + first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) + second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) - expect(model.count).to eq(2) + expect(partitioned_model.count).to eq(2) - first_todo.destroy + first_record.destroy! - expect(model.count).to eq(1) - expect(model.find_by_id(first_todo.id)).to be_nil - expect(model.find(second_todo.id).attributes).to eq(second_todo.attributes) + expect(partitioned_model.count).to eq(1) + expect(partitioned_model.find_by_id(first_record.id)).to be_nil + expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes) end end end @@ -388,13 +393,12 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when records exist in the source table' do - let(:source_table) { 'todos' } let(:migration_class) { '::Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable' } let(:sub_batch_size) { described_class::SUB_BATCH_SIZE } let(:pause_seconds) { described_class::PAUSE_SECONDS } - let!(:first_id) { create(:todo).id } - let!(:second_id) { create(:todo).id } - let!(:third_id) { create(:todo).id } + let!(:first_id) { source_model.create!(name: 'Bob', age: 20).id } + let!(:second_id) { source_model.create!(name: 'Alice', age: 30).id } + let!(:third_id) { source_model.create!(name: 'Sam', age: 40).id } before do stub_const("#{described_class.name}::BATCH_SIZE", 2) @@ -410,10 +414,10 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe expect(BackgroundMigrationWorker.jobs.size).to eq(2) - first_job_arguments = [first_id, second_id, source_table, partitioned_table, 'id'] + first_job_arguments = [first_id, second_id, source_table.to_s, partitioned_table, 'id'] expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([migration_class, first_job_arguments]) - second_job_arguments = [third_id, third_id, source_table, partitioned_table, 'id'] + second_job_arguments = [third_id, third_id, source_table.to_s, partitioned_table, 'id'] expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([migration_class, second_job_arguments]) end end @@ -482,7 +486,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end describe '#finalize_backfilling_partitioned_table' do - let(:source_table) { 'todos' } let(:source_column) { 'id' } context 'when the table is not allowed' do @@ -536,27 +539,27 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe context 'when there is missed data' do let(:partitioned_model) { Class.new(ActiveRecord::Base) } let(:timestamp) { Time.utc(2019, 12, 1, 12).round } - let!(:todo1) { create(:todo, created_at: timestamp, updated_at: timestamp) } - let!(:todo2) { create(:todo, created_at: timestamp, updated_at: timestamp) } - let!(:todo3) { create(:todo, created_at: timestamp, updated_at: timestamp) } - let!(:todo4) { create(:todo, created_at: timestamp, updated_at: timestamp) } + let!(:record1) { source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp) } + let!(:record2) { source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp) } + let!(:record3) { source_model.create!(name: 'Sam', age: 40, created_at: timestamp, updated_at: timestamp) } + let!(:record4) { source_model.create!(name: 'Sue', age: 50, created_at: timestamp, updated_at: timestamp) } let!(:pending_job1) do create(:background_migration_job, class_name: described_class::MIGRATION_CLASS_NAME, - arguments: [todo1.id, todo2.id, source_table, partitioned_table, source_column]) + arguments: [record1.id, record2.id, source_table, partitioned_table, source_column]) end let!(:pending_job2) do create(:background_migration_job, class_name: described_class::MIGRATION_CLASS_NAME, - arguments: [todo3.id, todo3.id, source_table, partitioned_table, source_column]) + arguments: [record3.id, record3.id, source_table, partitioned_table, source_column]) end let!(:succeeded_job) do create(:background_migration_job, :succeeded, class_name: described_class::MIGRATION_CLASS_NAME, - arguments: [todo4.id, todo4.id, source_table, partitioned_table, source_column]) + arguments: [record4.id, record4.id, source_table, partitioned_table, source_column]) end before do @@ -575,17 +578,17 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'idempotently cleans up after failed background migrations' do expect(partitioned_model.count).to eq(0) - partitioned_model.insert!(todo2.attributes) + partitioned_model.insert!(record2.attributes) expect_next_instance_of(Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable) do |backfill| allow(backfill).to receive(:transaction_open?).and_return(false) expect(backfill).to receive(:perform) - .with(todo1.id, todo2.id, source_table, partitioned_table, source_column) + .with(record1.id, record2.id, source_table, partitioned_table, source_column) .and_call_original expect(backfill).to receive(:perform) - .with(todo3.id, todo3.id, source_table, partitioned_table, source_column) + .with(record3.id, record3.id, source_table, partitioned_table, source_column) .and_call_original end @@ -593,12 +596,12 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe expect(partitioned_model.count).to eq(3) - [todo1, todo2, todo3].each do |original| + [record1, record2, record3].each do |original| copy = partitioned_model.find(original.id) expect(copy.attributes).to eq(original.attributes) end - expect(partitioned_model.find_by_id(todo4.id)).to be_nil + expect(partitioned_model.find_by_id(record4.id)).to be_nil [pending_job1, pending_job2].each do |job| expect(job.reload).to be_succeeded diff --git a/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb index 934e2274358..2c550f14a08 100644 --- a/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb +++ b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb @@ -24,107 +24,48 @@ RSpec.describe Gitlab::Database::PostgresHll::BatchDistinctCounter do allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction) end - context 'different distribution of relation records' do - [10, 100, 100_000].each do |spread| - context "records are spread within #{spread}" do - before do - ids = (1..spread).to_a.sample(10) - create_list(:issue, 10).each_with_index do |issue, i| - issue.id = ids[i] - end - end - - it 'counts table' do - expect(described_class.new(model).estimate_distinct_count).to be_within(error_rate).percent_of(10) - end - end - end - end - context 'unit test for different counting parameters' do before_all do create_list(:issue, 3, author: user) create_list(:issue, 2, author: another_user) end - describe '#estimate_distinct_count' do - it 'counts table' do - expect(described_class.new(model).estimate_distinct_count).to be_within(error_rate).percent_of(5) - end - - it 'counts with column field' do - expect(described_class.new(model, column).estimate_distinct_count).to be_within(error_rate).percent_of(2) - end - - it 'counts with :id field' do - expect(described_class.new(model, :id).estimate_distinct_count).to be_within(error_rate).percent_of(5) - end - - it 'counts with "id" field' do - expect(described_class.new(model, "id").estimate_distinct_count).to be_within(error_rate).percent_of(5) - end - - it 'counts with table.column field' do - expect(described_class.new(model, "#{model.table_name}.#{column}").estimate_distinct_count).to be_within(error_rate).percent_of(2) - end - - it 'counts with Arel column' do - expect(described_class.new(model, model.arel_table[column]).estimate_distinct_count).to be_within(error_rate).percent_of(2) - end - - it 'counts over joined relations' do - expect(described_class.new(model.joins(:author), "users.email").estimate_distinct_count).to be_within(error_rate).percent_of(2) - end - - it 'counts with :column field with batch_size of 50K' do - expect(described_class.new(model, column).estimate_distinct_count(batch_size: 50_000)).to be_within(error_rate).percent_of(2) - end - - it 'will not count table with a batch size less than allowed' do - expect(described_class.new(model, column).estimate_distinct_count(batch_size: small_batch_size)).to eq(fallback) - end - - it 'counts with different number of batches and aggregates total result' do - stub_const('Gitlab::Database::PostgresHll::BatchDistinctCounter::MIN_REQUIRED_BATCH_SIZE', 0) - - [1, 2, 4, 5, 6].each { |i| expect(described_class.new(model).estimate_distinct_count(batch_size: i)).to be_within(error_rate).percent_of(5) } - end - - it 'counts with a start and finish' do - expect(described_class.new(model, column).estimate_distinct_count(start: model.minimum(:id), finish: model.maximum(:id))).to be_within(error_rate).percent_of(2) + describe '#execute' do + it 'builds hll buckets' do + expect(described_class.new(model).execute).to be_an_instance_of(Gitlab::Database::PostgresHll::Buckets) end - it "defaults the batch size to #{Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE}" do + it "defaults batch size to #{Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE}" do min_id = model.minimum(:id) batch_end_id = min_id + calculate_batch_size(Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE) expect(model).to receive(:where).with("id" => min_id..batch_end_id).and_call_original - described_class.new(model).estimate_distinct_count + described_class.new(model).execute end context 'when a transaction is open' do let(:in_transaction) { true } it 'raises an error' do - expect { described_class.new(model, column).estimate_distinct_count }.to raise_error('BatchCount can not be run inside a transaction') + expect { described_class.new(model, column).execute }.to raise_error('BatchCount can not be run inside a transaction') end end context 'disallowed configurations' do let(:default_batch_size) { Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE } - it 'returns fallback if start is bigger than finish' do - expect(described_class.new(model, column).estimate_distinct_count(start: 1, finish: 0)).to eq(fallback) + it 'raises WRONG_CONFIGURATION_ERROR if start is bigger than finish' do + expect { described_class.new(model, column).execute(start: 1, finish: 0) }.to raise_error(described_class::WRONG_CONFIGURATION_ERROR) end - it 'returns fallback if data volume exceeds upper limit' do + it 'raises WRONG_CONFIGURATION_ERROR if data volume exceeds upper limit' do large_finish = Gitlab::Database::PostgresHll::BatchDistinctCounter::MAX_DATA_VOLUME + 1 - expect(described_class.new(model, column).estimate_distinct_count(start: 1, finish: large_finish)).to eq(fallback) + expect { described_class.new(model, column).execute(start: 1, finish: large_finish) }.to raise_error(described_class::WRONG_CONFIGURATION_ERROR) end - it 'returns fallback if batch size is less than min required' do - expect(described_class.new(model, column).estimate_distinct_count(batch_size: small_batch_size)).to eq(fallback) + it 'raises WRONG_CONFIGURATION_ERROR if batch size is less than min required' do + expect { described_class.new(model, column).execute(batch_size: small_batch_size) }.to raise_error(described_class::WRONG_CONFIGURATION_ERROR) end end end diff --git a/spec/lib/gitlab/database/postgres_hll/buckets_spec.rb b/spec/lib/gitlab/database/postgres_hll/buckets_spec.rb new file mode 100644 index 00000000000..b4d8fd4a449 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_hll/buckets_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresHll::Buckets do + let(:error_rate) { Gitlab::Database::PostgresHll::BatchDistinctCounter::ERROR_RATE } # HyperLogLog is a probabilistic algorithm, which provides estimated data, with given error margin + let(:buckets_hash_5) { { 121 => 2, 126 => 1, 141 => 1, 383 => 1, 56 => 1 } } + let(:buckets_hash_2) { { 141 => 1, 56 => 1 } } + + describe '#estimated_distinct_count' do + it 'provides estimated cardinality', :aggregate_failures do + expect(described_class.new(buckets_hash_5).estimated_distinct_count).to be_within(error_rate).percent_of(5) + expect(described_class.new(buckets_hash_2).estimated_distinct_count).to be_within(error_rate).percent_of(2) + expect(described_class.new({}).estimated_distinct_count).to eq 0 + expect(described_class.new.estimated_distinct_count).to eq 0 + end + end + + describe '#merge_hash!' do + let(:hash_a) { { 1 => 1, 2 => 3 } } + let(:hash_b) { { 1 => 2, 2 => 1 } } + + it 'merges two hashes together into union of two sets' do + expect(described_class.new(hash_a).merge_hash!(hash_b).to_json).to eq described_class.new(1 => 2, 2 => 3).to_json + end + end + + describe '#to_json' do + it 'serialize HyperLogLog buckets as hash' do + expect(described_class.new(1 => 5).to_json).to eq '{"1":5}' + end + end +end diff --git a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb index f45d959c0de..ae6362ba812 100644 --- a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb +++ b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb @@ -3,65 +3,79 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Reindexing::Coordinator do + include Database::DatabaseHelpers include ExclusiveLeaseHelpers describe '.perform' do - subject { described_class.new(indexes).perform } + subject { described_class.new(index, notifier).perform } - let(:indexes) { [instance_double(Gitlab::Database::PostgresIndex), instance_double(Gitlab::Database::PostgresIndex)] } - let(:reindexers) { [instance_double(Gitlab::Database::Reindexing::ConcurrentReindex), instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)] } + before do + swapout_view_for_table(:postgres_indexes) + + allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer) + allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:create_for).with(index).and_return(action) + end + + let(:index) { create(:postgres_index) } + let(:notifier) { instance_double(Gitlab::Database::Reindexing::GrafanaNotifier, notify_start: nil, notify_end: nil) } + let(:reindexer) { instance_double(Gitlab::Database::Reindexing::ConcurrentReindex, perform: nil) } + let(:action) { create(:reindex_action, index: index) } let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) } let(:lease_key) { 'gitlab/database/reindexing/coordinator' } let(:lease_timeout) { 1.day } let(:uuid) { 'uuid' } - before do - allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield + context 'locking' do + it 'acquires a lock while reindexing' do + expect(lease).to receive(:try_obtain).ordered.and_return(uuid) - indexes.zip(reindexers).each do |index, reindexer| - allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer) - allow(reindexer).to receive(:perform) - end - end + expect(reindexer).to receive(:perform).ordered - it 'performs concurrent reindexing for each index' do - indexes.zip(reindexers).each do |index, reindexer| - expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer) - expect(reindexer).to receive(:perform) + expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid) + + subject end - subject + it 'does not perform reindexing actions if lease is not granted' do + expect(lease).to receive(:try_obtain).ordered.and_return(false) + expect(Gitlab::Database::Reindexing::ConcurrentReindex).not_to receive(:new) + + subject + end end - it 'keeps track of actions and creates ReindexAction records' do - indexes.each do |index| - expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield + context 'notifications' do + it 'sends #notify_start before reindexing' do + expect(notifier).to receive(:notify_start).with(action).ordered + expect(reindexer).to receive(:perform).ordered + + subject end - subject + it 'sends #notify_end after reindexing and updating the action is done' do + expect(action).to receive(:finish).ordered + expect(notifier).to receive(:notify_end).with(action).ordered + + subject + end end - context 'locking' do - it 'acquires a lock while reindexing' do - indexes.each do |index| - expect(lease).to receive(:try_obtain).ordered.and_return(uuid) - action = instance_double(Gitlab::Database::Reindexing::ConcurrentReindex) - expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).ordered.with(index).and_return(action) - expect(action).to receive(:perform).ordered - expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid) - end + context 'action tracking' do + it 'calls #finish on the action' do + expect(reindexer).to receive(:perform).ordered + expect(action).to receive(:finish).ordered subject end - it 'does does not perform reindexing actions if lease is not granted' do - indexes.each do |index| - expect(lease).to receive(:try_obtain).ordered.and_return(false) - expect(Gitlab::Database::Reindexing::ConcurrentReindex).not_to receive(:new) - end + it 'upon error, it still calls finish and raises the error' do + expect(reindexer).to receive(:perform).ordered.and_raise('something went wrong') + expect(action).to receive(:finish).ordered - subject + expect { subject }.to raise_error(/something went wrong/) + + expect(action).to be_failed end end end diff --git a/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb b/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb new file mode 100644 index 00000000000..e76718fe48a --- /dev/null +++ b/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Reindexing::GrafanaNotifier do + include Database::DatabaseHelpers + + let(:api_key) { "foo" } + let(:api_url) { "http://bar"} + let(:additional_tag) { "some-tag" } + + let(:action) { create(:reindex_action) } + + before do + swapout_view_for_table(:postgres_indexes) + end + + let(:headers) do + { + 'Content-Type': 'application/json', + 'Authorization': "Bearer #{api_key}" + } + end + + let(:response) { double('response', success?: true) } + + def expect_api_call(payload) + expect(Gitlab::HTTP).to receive(:post).with("#{api_url}/api/annotations", body: payload.to_json, headers: headers, allow_local_requests: true).and_return(response) + end + + shared_examples_for 'interacting with Grafana annotations API' do + it 'POSTs a JSON payload' do + expect_api_call(payload) + + expect(subject).to be_truthy + end + + context 'on error' do + it 'does not raise the error and returns false' do + allow(Gitlab::HTTP).to receive(:post).and_raise('something went wrong') + + expect(subject).to be_falsey + end + + context 'when request was not successful' do + it 'returns false' do + expect_api_call(payload) + allow(response).to receive(:success?).and_return(false) + + expect(subject).to be_falsey + end + end + end + + context 'without api_key' do + let(:api_key) { '' } + + it 'does not post anything' do + expect(Gitlab::HTTP).not_to receive(:post) + + expect(subject).to be_falsey + end + end + + context 'without api_url' do + let(:api_url) { '' } + + it 'does not post anything' do + expect(Gitlab::HTTP).not_to receive(:post) + + expect(subject).to be_falsey + end + end + end + + describe '#notify_start' do + context 'additional tag is nil' do + subject { described_class.new(api_key, api_url, nil).notify_start(action) } + + let(:payload) do + { + time: (action.action_start.utc.to_f * 1000).to_i, + tags: ['reindex', action.index.tablename, action.index.name], + text: "Started reindexing of #{action.index.name} on #{action.index.tablename}" + } + end + + it_behaves_like 'interacting with Grafana annotations API' + end + + context 'additional tag is not nil' do + subject { described_class.new(api_key, api_url, additional_tag).notify_start(action) } + + let(:payload) do + { + time: (action.action_start.utc.to_f * 1000).to_i, + tags: ['reindex', additional_tag, action.index.tablename, action.index.name], + text: "Started reindexing of #{action.index.name} on #{action.index.tablename}" + } + end + + it_behaves_like 'interacting with Grafana annotations API' + end + end + + describe '#notify_end' do + context 'additional tag is nil' do + subject { described_class.new(api_key, api_url, nil).notify_end(action) } + + let(:payload) do + { + time: (action.action_start.utc.to_f * 1000).to_i, + tags: ['reindex', action.index.tablename, action.index.name], + text: "Finished reindexing of #{action.index.name} on #{action.index.tablename} (#{action.state})", + timeEnd: (action.action_end.utc.to_f * 1000).to_i, + isRegion: true + } + end + + it_behaves_like 'interacting with Grafana annotations API' + end + + context 'additional tag is not nil' do + subject { described_class.new(api_key, api_url, additional_tag).notify_end(action) } + + let(:payload) do + { + time: (action.action_start.utc.to_f * 1000).to_i, + tags: ['reindex', additional_tag, action.index.tablename, action.index.name], + text: "Finished reindexing of #{action.index.name} on #{action.index.tablename} (#{action.state})", + timeEnd: (action.action_end.utc.to_f * 1000).to_i, + isRegion: true + } + end + + it_behaves_like 'interacting with Grafana annotations API' + end + end +end diff --git a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb index a5e2f368f40..4466679a099 100644 --- a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb +++ b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Reindexing::IndexSelection do - include DatabaseHelpers + include Database::DatabaseHelpers subject { described_class.new(Gitlab::Database::PostgresIndex.all).to_a } diff --git a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb index 225f23d2135..a8f196d8f0e 100644 --- a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb +++ b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb @@ -2,91 +2,83 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::Reindexing::ReindexAction, '.keep_track_of' do - let(:index) { double('index', identifier: 'public.something', ondisk_size_bytes: 10240, reload: nil, bloat_size: 42) } - let(:size_after) { 512 } +RSpec.describe Gitlab::Database::Reindexing::ReindexAction do + include Database::DatabaseHelpers - it 'yields to the caller' do - expect { |b| described_class.keep_track_of(index, &b) }.to yield_control - end + let(:index) { create(:postgres_index) } - def find_record - described_class.find_by(index_identifier: index.identifier) + before_all do + swapout_view_for_table(:postgres_indexes) end - it 'creates the record with a start time and updates its end time' do - freeze_time do - described_class.keep_track_of(index) do - expect(find_record.action_start).to be_within(1.second).of(Time.zone.now) + describe '.create_for' do + subject { described_class.create_for(index) } - travel(10.seconds) - end + it 'creates a new record for the given index' do + freeze_time do + record = subject - duration = find_record.action_end - find_record.action_start + expect(record.index_identifier).to eq(index.identifier) + expect(record.action_start).to eq(Time.zone.now) + expect(record.ondisk_size_bytes_start).to eq(index.ondisk_size_bytes) + expect(subject.bloat_estimate_bytes_start).to eq(index.bloat_size) - expect(duration).to be_within(1.second).of(10.seconds) + expect(record).to be_persisted + end end end - it 'creates the record with its status set to :started and updates its state to :finished' do - described_class.keep_track_of(index) do - expect(find_record).to be_started - end + describe '#finish' do + subject { action.finish } - expect(find_record).to be_finished - end + let(:action) { build(:reindex_action, index: index) } - it 'creates the record with the indexes start size and updates its end size' do - described_class.keep_track_of(index) do - expect(find_record.ondisk_size_bytes_start).to eq(index.ondisk_size_bytes) + it 'sets #action_end' do + freeze_time do + subject - expect(index).to receive(:reload).once - allow(index).to receive(:ondisk_size_bytes).and_return(size_after) + expect(action.action_end).to eq(Time.zone.now) + end end - expect(find_record.ondisk_size_bytes_end).to eq(size_after) - end + it 'sets #ondisk_size_bytes_end after reloading the index record' do + new_size = 4711 + expect(action.index).to receive(:reload).ordered + expect(action.index).to receive(:ondisk_size_bytes).and_return(new_size).ordered + + subject - it 'creates the record with the indexes bloat estimate' do - described_class.keep_track_of(index) do - expect(find_record.bloat_estimate_bytes_start).to eq(index.bloat_size) + expect(action.ondisk_size_bytes_end).to eq(new_size) end - end - context 'in case of errors' do - it 'sets the state to failed' do - expect do - described_class.keep_track_of(index) do - raise 'something went wrong' - end - end.to raise_error(/something went wrong/) + context 'setting #state' do + it 'sets #state to finished if not given' do + action.state = nil - expect(find_record).to be_failed - end + subject - it 'records the end time' do - freeze_time do - expect do - described_class.keep_track_of(index) do - raise 'something went wrong' - end - end.to raise_error(/something went wrong/) + expect(action).to be_finished + end + + it 'sets #state to finished if not set to started' do + action.state = :started - expect(find_record.action_end).to be_within(1.second).of(Time.zone.now) + subject + + expect(action).to be_finished end - end - it 'records the resulting index size' do - expect(index).to receive(:reload).once - allow(index).to receive(:ondisk_size_bytes).and_return(size_after) + it 'does not change state if set to failed' do + action.state = :failed + + expect { subject }.not_to change { action.state } + end + end - expect do - described_class.keep_track_of(index) do - raise 'something went wrong' - end - end.to raise_error(/something went wrong/) + it 'saves the record' do + expect(action).to receive(:save!) - expect(find_record.ondisk_size_bytes_end).to eq(size_after) + subject end end end diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb index eb78a5fe8ea..b2f038e8b62 100644 --- a/spec/lib/gitlab/database/reindexing_spec.rb +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -11,13 +11,16 @@ RSpec.describe Gitlab::Database::Reindexing do let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) } let(:index_selection) { instance_double(Gitlab::Database::Reindexing::IndexSelection) } let(:candidate_indexes) { double } - let(:indexes) { double } + let(:indexes) { [double, double] } it 'delegates to Coordinator' do expect(Gitlab::Database::Reindexing::IndexSelection).to receive(:new).with(candidate_indexes).and_return(index_selection) expect(index_selection).to receive(:take).with(2).and_return(indexes) - expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator) - expect(coordinator).to receive(:perform) + + indexes.each do |index| + expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(index).and_return(coordinator) + expect(coordinator).to receive(:perform) + end subject end diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb index 4048fc69591..417bf3e363a 100644 --- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb +++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb @@ -8,8 +8,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService let(:prometheus_settings) do { - enable: true, - listen_address: 'localhost:9090' + enabled: true, + server_address: 'localhost:9090' } end @@ -63,13 +63,13 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService application_setting.update(allow_local_requests_from_web_hooks_and_services: true) end - shared_examples 'has prometheus service' do |listen_address| + shared_examples 'has prometheus service' do |server_address| it do expect(result[:status]).to eq(:success) prometheus = project.prometheus_service expect(prometheus).not_to eq(nil) - expect(prometheus.api_url).to eq(listen_address) + expect(prometheus.api_url).to eq(server_address) expect(prometheus.active).to eq(true) expect(prometheus.manual_configuration).to eq(true) end @@ -202,25 +202,25 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService end context 'with non default prometheus address' do - let(:listen_address) { 'https://localhost:9090' } + let(:server_address) { 'https://localhost:9090' } let(:prometheus_settings) do { - enable: true, - listen_address: listen_address + enabled: true, + server_address: server_address } end it_behaves_like 'has prometheus service', 'https://localhost:9090' context 'with :9090 symbol' do - let(:listen_address) { :':9090' } + let(:server_address) { :':9090' } it_behaves_like 'has prometheus service', 'http://localhost:9090' end context 'with 0.0.0.0:9090' do - let(:listen_address) { '0.0.0.0:9090' } + let(:server_address) { '0.0.0.0:9090' } it_behaves_like 'has prometheus service', 'http://localhost:9090' end @@ -251,8 +251,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService context 'when prometheus setting is disabled in gitlab.yml' do let(:prometheus_settings) do { - enable: false, - listen_address: 'http://localhost:9090' + enabled: false, + server_address: 'http://localhost:9090' } end @@ -262,8 +262,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService end end - context 'when prometheus listen address is blank in gitlab.yml' do - let(:prometheus_settings) { { enable: true, listen_address: '' } } + context 'when prometheus server address is blank in gitlab.yml' do + let(:prometheus_settings) { { enabled: true, server_address: '' } } it 'does not configure prometheus' do expect(result).to include(status: :success) @@ -296,8 +296,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService context 'when prometheus manual configuration cannot be saved' do let(:prometheus_settings) do { - enable: true, - listen_address: 'httpinvalid://localhost:9090' + enabled: true, + server_address: 'httpinvalid://localhost:9090' } end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index a7f6ea0cbfb..c9a20f40462 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -752,4 +752,62 @@ RSpec.describe Gitlab::Diff::Position do expect(subject.file_hash).to eq(Digest::SHA1.hexdigest(subject.file_path)) end end + + describe '#multiline?' do + let(:end_line_code) { "ab09011fa121d0a2bb9fa4ca76094f2482b902b7_#{end_old_line}_#{end_new_line}" } + + let(:line_range) do + { + "start" => { + "line_code" => "ab09011fa121d0a2bb9fa4ca76094f2482b902b7_18_18", + "type" => nil, + "old_line" => 18, + "new_line" => 18 + }, + "end" => { + "line_code" => end_line_code, + "type" => nil, + "old_line" => end_old_line, + "new_line" => end_new_line + } + } + end + + subject(:multiline) do + described_class.new( + line_range: line_range, + position_type: position_type + ) + end + + let(:end_old_line) { 20 } + let(:end_new_line) { 20 } + + context 'when the position type is text' do + let(:position_type) { "text" } + + context 'when the start lines equal the end lines' do + let(:end_old_line) { 18 } + let(:end_new_line) { 18 } + + it "returns true" do + expect(subject.multiline?).to be_falsey + end + end + + context 'when the start lines do not equal the end lines' do + it "returns true" do + expect(subject.multiline?).to be_truthy + end + end + end + + context 'when the position type is not text' do + let(:position_type) { "image" } + + it "returns false" do + expect(subject.multiline?).to be_falsey + end + end + end end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index ef448ee96a4..8872800069a 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -4,146 +4,50 @@ require 'spec_helper' RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do include_context :email_shared_context - let!(:sent_notification) do - SentNotification.record_note(note, user.id, mail_key) - end + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } let(:noteable) { note.noteable } let(:note) { create(:diff_note_on_merge_request, project: project) } - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } let(:email_raw) { fixture_file('emails/valid_reply.eml') } + let!(:sent_notification) do + SentNotification.record_note(note, user.id, mail_key) + end it_behaves_like :reply_processing_shared_examples + it_behaves_like :note_handler_shared_examples do + let(:recipient) { sent_notification.recipient } + + let(:update_commands_only) { fixture_file('emails/update_commands_only_reply.eml')} + let(:no_content) { fixture_file('emails/no_content_reply.eml') } + let(:commands_in_reply) { fixture_file('emails/commands_in_reply.eml') } + let(:with_quick_actions) { fixture_file('emails/valid_reply_with_quick_actions.eml') } + end + before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") stub_config_setting(host: 'localhost') end - context "when the recipient address doesn't include a mail key" do - let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } + context 'when the recipient address does not include a mail key' do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, '') } - it "raises a UnknownIncomingEmail" do + it 'raises a UnknownIncomingEmail' do expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end end - context "when no sent notification for the mail key could be found" do + context 'when no sent notification for the mail key could be found' do let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } - it "raises a SentNotificationNotFoundError" do + it 'raises a SentNotificationNotFoundError' do expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) end end - context "when the noteable could not be found" do - before do - noteable.destroy - end - - it "raises a NoteableNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) - end - end - - context "when the note could not be saved" do - before do - allow_any_instance_of(Note).to receive(:persisted?).and_return(false) - end - - it "raises an InvalidNoteError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) - end - - context 'because the note was update commands only' do - let!(:email_raw) { fixture_file("emails/update_commands_only_reply.eml") } - - context 'and current user cannot update noteable' do - it 'raises a CommandsOnlyNoteError' do - expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) - end - end - - context "and current user can update noteable" do - before do - project.add_developer(user) - end - - it 'does not raise an error' do - expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1) - - expect(noteable.reload).to be_closed - end - end - end - end - - context 'when the note contains quick actions' do - let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") } - - context 'and current user cannot update the noteable' do - it 'only executes the commands that the user can perform' do - expect { receiver.execute } - .to change { noteable.notes.user.count }.by(1) - .and change { user.todos_pending_count }.from(0).to(1) - - expect(noteable.reload).to be_open - end - end - - context 'and current user can update noteable' do - before do - project.add_developer(user) - end - - it 'posts a note and updates the noteable' do - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - - expect { receiver.execute } - .to change { noteable.notes.user.count }.by(1) - .and change { user.todos_pending_count }.from(0).to(1) - - expect(noteable.reload).to be_closed - end - end - end - - context "when the reply is blank" do - let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - - it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) - end - end - - shared_examples "checks permissions on noteable" do - context "when user has access" do - before do - project.add_reporter(user) - end - - it "creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - end - end - - context "when user does not have access" do - it "raises UserNotAuthorizedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError) - end - end - end - - context "when discussion is locked" do - before do - noteable.update_attribute(:discussion_locked, true) - end - - it_behaves_like "checks permissions on noteable" - end - - context "when issue is confidential" do + context 'when issue is confidential' do let(:issue) { create(:issue, project: project) } let(:note) { create(:note, noteable: issue, project: project) } @@ -151,17 +55,17 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do issue.update_attribute(:confidential, true) end - it_behaves_like "checks permissions on noteable" + it_behaves_like :checks_permissions_on_noteable_examples end shared_examples 'a reply to existing comment' do - it "creates a comment" do + it 'creates a comment' do expect { receiver.execute }.to change { noteable.notes.count }.by(1) new_note = noteable.notes.last expect(new_note.author).to eq(sent_notification.recipient) expect(new_note.position).to eq(note.position) - expect(new_note.note).to include("I could not disagree more.") + expect(new_note.note).to include('I could not disagree more.') expect(new_note.in_reply_to?(note)).to be_truthy if note.part_of_discussion? @@ -172,32 +76,14 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do end end - context "when everything is fine" do + # additional shared tests in :reply_processing_shared_examples + context 'when everything is fine' do before do setup_attachment end it_behaves_like 'a reply to existing comment' - it "adds all attachments" do - expect_next_instance_of(Gitlab::Email::AttachmentUploader) do |uploader| - expect(uploader).to receive(:execute).with(upload_parent: project, uploader_class: FileUploader).and_return( - [ - { - url: "uploads/image.png", - alt: "image", - markdown: markdown - } - ] - ) - end - - receiver.execute - - note = noteable.notes.last - expect(note.note).to include(markdown) - end - context 'when sub-addressing is not supported' do before do stub_incoming_email_setting(enabled: true, address: nil) @@ -228,75 +114,9 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do end end - context "when note is not a discussion" do + context 'when note is not a discussion' do let(:note) { create(:note_on_merge_request, project: project) } it_behaves_like 'a reply to existing comment' end - - context 'when the service desk' do - let(:project) { create(:project, :public, service_desk_enabled: true) } - let(:support_bot) { User.support_bot } - let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') } - let(:note) { create(:note, project: project, noteable: noteable) } - let(:email_raw) { fixture_file('emails/valid_reply_with_quick_actions.eml') } - - let!(:sent_notification) do - SentNotification.record_note(note, support_bot.id, mail_key) - end - - context 'is enabled' do - before do - allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true) - project.project_feature.update!(issues_access_level: issues_access_level) - end - - context 'when issues are enabled for everyone' do - let(:issues_access_level) { ProjectFeature::ENABLED } - - it 'creates a comment' do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - end - - context 'when quick actions are present' do - it 'encloses quick actions with code span markdown' do - receiver.execute - noteable.reload - - note = Note.last - expect(note.note).to include("Jake out\n\n`/close`\n`/title test`") - expect(noteable.title).to eq('service desk issue') - expect(noteable).to be_opened - end - end - end - - context 'when issues are protected members only' do - let(:issues_access_level) { ProjectFeature::PRIVATE } - - it 'creates a comment' do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - end - end - - context 'when issues are disabled' do - let(:issues_access_level) { ProjectFeature::DISABLED } - - it 'does not create a comment' do - expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError) - end - end - end - - context 'is disabled' do - before do - allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false) - allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(false) - end - - it 'does not create a comment' do - expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) - end - end - end end diff --git a/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb new file mode 100644 index 00000000000..94f28d3399a --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Email::Handler::CreateNoteOnIssuableHandler do + include_context :email_shared_context + + let_it_be(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') } + let_it_be(:namespace) { create(:namespace, path: 'gitlabhq') } + let_it_be(:project) { create(:project, :public, namespace: namespace, path: 'gitlabhq') } + + let!(:noteable) { create(:issue, project: project) } + let(:email_raw) { email_fixture('emails/valid_note_on_issuable.eml') } + + before do + stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + it_behaves_like :reply_processing_shared_examples + + it_behaves_like :note_handler_shared_examples, true do + let_it_be(:recipient) { user } + + let(:update_commands_only) { email_reply_fixture('emails/update_commands_only_reply.eml') } + let(:no_content) { email_reply_fixture('emails/no_content_reply.eml') } + let(:commands_in_reply) { email_reply_fixture('emails/commands_in_reply.eml') } + let(:with_quick_actions) { email_reply_fixture('emails/valid_reply_with_quick_actions.eml') } + end + + context 'when the recipient address does not include a mail key' do + let(:mail_key) { 'gitlabhq-gitlabhq-project_id-auth_token-issue-issue_iid' } + let(:email_raw) { fixture_file('emails/valid_note_on_issuable.eml').gsub(mail_key, '') } + + it 'raises an UnknownIncomingEmail' do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) + end + end + + context 'when issue is confidential' do + before do + noteable.update_attribute(:confidential, true) + end + + it_behaves_like :checks_permissions_on_noteable_examples + end + + def email_fixture(path) + fixture_file(path) + .gsub('project_id', project.project_id.to_s) + .gsub('issue_iid', noteable.iid.to_s) + end + + def email_reply_fixture(path) + reply_address = 'reply+59d8df8370b7e95c5a49fbf86aeb2c93' + note_address = "incoming+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue-#{noteable.iid}" + + fixture_file(path) + .gsub(reply_address, note_address) + end +end diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index 32b451f8329..b1ffbedc7bf 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -191,16 +191,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) end end - - context 'when service_desk_custom_address feature is disabled' do - before do - stub_feature_flags(service_desk_custom_address: false) - end - - it 'bounces the email' do - expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) - end - end end end diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb index 2cd8c31e6b2..eff6fb63a5f 100644 --- a/spec/lib/gitlab/email/handler_spec.rb +++ b/spec/lib/gitlab/email/handler_spec.rb @@ -60,8 +60,9 @@ RSpec.describe Gitlab::Email::Handler do describe 'regexps are set properly' do let(:addresses) do - %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request) + - %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path-to-project-123-user_email_token-issue) + + %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}) + + %w(sent_notification_key path-to-project-123-user_email_token-merge-request) + + %w(path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123) + %w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project) end diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index 68a46b11487..764478ad1d7 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -236,7 +236,7 @@ RSpec.describe Gitlab::ErrorTracking do context 'the exception implements :sentry_extra_data' do let(:extra_info) { { event: 'explosion', size: :massive } } - let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller) } + let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller, cause: nil) } it 'includes the extra data from the exception in the tracking information' do track_exception @@ -247,7 +247,7 @@ RSpec.describe Gitlab::ErrorTracking do end context 'the exception implements :sentry_extra_data, which returns nil' do - let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller) } + let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller, cause: nil) } let(:extra) { { issue_url: issue_url } } it 'just includes the other extra info' do @@ -287,10 +287,23 @@ RSpec.describe Gitlab::ErrorTracking do let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') } it 'injects the normalized sql query into extra' do + allow(Raven.client.transport).to receive(:send_event) do |event| + expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') + end + track_exception + end + end - expect(Raven).to have_received(:capture_exception) - .with(exception, a_hash_including(extra: a_hash_including(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1'))) + context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do + let(:exception) { RuntimeError.new(cause: ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1')) } + + it 'injects the normalized sql query into extra' do + allow(Raven.client.transport).to receive(:send_event) do |event| + expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') + end + + track_exception end end end diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index 03cb89ee033..c47f71c207d 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -156,6 +156,16 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do is_expected.to eq(true) end end + + context 'Cookie parameter to force enable experiment' do + it 'returns true unconditionally' do + cookies[:force_experiment] = 'test_experiment,another_experiment' + get :index + + expect(check_experiment(:test_experiment)).to eq(true) + expect(check_experiment(:another_experiment)).to eq(true) + end + end end describe '#track_experiment_event', :snowplow do diff --git a/spec/lib/gitlab/experimentation/experiment_spec.rb b/spec/lib/gitlab/experimentation/experiment_spec.rb index 7b1d1763010..008e6699597 100644 --- a/spec/lib/gitlab/experimentation/experiment_spec.rb +++ b/spec/lib/gitlab/experimentation/experiment_spec.rb @@ -14,8 +14,10 @@ RSpec.describe Gitlab::Experimentation::Experiment do end before do - feature = double('FeatureFlag', percentage_of_time_value: percentage ) - expect(Feature).to receive(:get).with(:experiment_key_experiment_percentage).and_return(feature) + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check + feature = double('FeatureFlag', percentage_of_time_value: percentage, enabled?: true) + allow(Feature).to receive(:get).with(:experiment_key_experiment_percentage).and_return(feature) end subject(:experiment) { described_class.new(:experiment_key, **params) } diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index a68c050d829..b503960b8c7 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -38,6 +38,8 @@ RSpec.describe Gitlab::Experimentation do } }) + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check Feature.enable_percentage_of_time(:backwards_compatible_test_experiment_experiment_percentage, enabled_percentage) Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage) allow(Gitlab).to receive(:com?).and_return(true) diff --git a/spec/lib/gitlab/faraday/error_callback_spec.rb b/spec/lib/gitlab/faraday/error_callback_spec.rb new file mode 100644 index 00000000000..5da4b8adf6a --- /dev/null +++ b/spec/lib/gitlab/faraday/error_callback_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Faraday::ErrorCallback do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app, {}) } + + describe '#call' do + let(:env) { { url: 'http://target.url' } } + + subject { middleware.call(env) } + + context 'with no errors' do + before do + expect(app).to receive(:call).with(env).and_return('success') + end + + it { is_expected.to eq('success') } + end + + context 'with errors' do + before do + expect(app).to receive(:call).and_raise(ArgumentError, 'Kaboom!') + end + + context 'with no callback' do + it 'uses the default callback' do + expect { subject }.to raise_error(ArgumentError, 'Kaboom!') + end + end + + context 'with a custom callback' do + let(:options) { { callback: callback } } + + it 'uses the custom callback' do + count = 0 + target_url = nil + exception_class = nil + + callback = proc do |env, exception| + count += 1 + target_url = env[:url].to_s + exception_class = exception.class.name + end + + options = { callback: callback } + middleware = described_class.new(app, options) + + expect(callback).to receive(:call).and_call_original + expect { middleware.call(env) }.to raise_error(ArgumentError, 'Kaboom!') + expect(count).to eq(1) + expect(target_url).to eq('http://target.url') + expect(exception_class).to eq(ArgumentError.name) + end + end + end + end +end diff --git a/spec/lib/gitlab/git/changed_path_spec.rb b/spec/lib/gitlab/git/changed_path_spec.rb new file mode 100644 index 00000000000..93db107ad5c --- /dev/null +++ b/spec/lib/gitlab/git/changed_path_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::Git::ChangedPath do + subject(:changed_path) { described_class.new(path: path, status: status) } + + let(:path) { 'test_path' } + + describe '#new_file?' do + subject(:new_file?) { changed_path.new_file? } + + context 'when it is a new file' do + let(:status) { :ADDED } + + it 'returns true' do + expect(new_file?).to eq(true) + end + end + + context 'when it is not a new file' do + let(:status) { :MODIFIED } + + it 'returns false' do + expect(new_file?).to eq(false) + end + end + end +end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index d4174a34433..783f0a9ccf7 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -58,7 +58,7 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - diff = described_class.new(diff: 'a' * 204800) + diff = described_class.new({ diff: 'a' * 204800 }) expect(diff.diff).to be_empty expect(diff).to be_too_large diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index c917945499c..ef9b5a30c86 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -520,12 +520,13 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do forced: true, no_tags: true, timeout: described_class::GITLAB_PROJECTS_TIMEOUT, - prune: false + prune: false, + check_tags_changed: false } expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts) - repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false) + repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false, check_tags_changed: false) end it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do @@ -1191,25 +1192,25 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:commit_1_files) do [ - OpenStruct.new(status: :ADDED, path: "files/executables/ls"), - OpenStruct.new(status: :ADDED, path: "files/executables/touch"), - OpenStruct.new(status: :ADDED, path: "files/links/regex.rb"), - OpenStruct.new(status: :ADDED, path: "files/links/ruby-style-guide.md"), - OpenStruct.new(status: :ADDED, path: "files/links/touch"), - OpenStruct.new(status: :MODIFIED, path: ".gitmodules"), - OpenStruct.new(status: :ADDED, path: "deeper/nested/six"), - OpenStruct.new(status: :ADDED, path: "nested/six") + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/touch"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/regex.rb"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/ruby-style-guide.md"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/touch"), + Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "deeper/nested/six"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "nested/six") ] end let(:commit_2_files) do - [OpenStruct.new(status: :ADDED, path: "bin/executable")] + [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bin/executable")] end let(:commit_3_files) do [ - OpenStruct.new(status: :MODIFIED, path: ".gitmodules"), - OpenStruct.new(status: :ADDED, path: "gitlab-shell") + Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "gitlab-shell") ] end @@ -1217,7 +1218,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do collection = repository.find_changed_paths([commit_1, commit_2, commit_3]) expect(collection).to be_a(Enumerable) - expect(collection.to_a).to eq(commit_1_files + commit_2_files + commit_3_files) + expect(collection.as_json).to eq((commit_1_files + commit_2_files + commit_3_files).as_json) end it 'returns no paths when SHAs are invalid' do @@ -1231,7 +1232,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do collection = repository.find_changed_paths([nil, commit_1]) expect(collection).to be_a(Enumerable) - expect(collection.to_a).to eq(commit_1_files) + expect(collection.as_json).to eq(commit_1_files.as_json) end it 'returns no paths when the commits are nil' do diff --git a/spec/lib/gitlab/git/wiki_page_version_spec.rb b/spec/lib/gitlab/git/wiki_page_version_spec.rb new file mode 100644 index 00000000000..836fa2449ec --- /dev/null +++ b/spec/lib/gitlab/git/wiki_page_version_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Git::WikiPageVersion do + let_it_be(:project) { create(:project, :public, :repository) } + let(:user) { create(:user, username: 'someone') } + + describe '#author_url' do + subject(:author_url) { described_class.new(commit, nil).author_url } + + context 'user exists in gitlab' do + let(:commit) { create(:commit, project: project, author: user) } + + it 'returns the profile link of the user' do + expect(author_url).to eq('http://localhost/someone') + end + end + + context 'user does not exist in gitlab' do + let(:commit) { create(:commit, project: project, author_email: "someone@somewebsite.com") } + + it 'returns a mailto: url' do + expect(author_url).to eq('mailto:someone@somewebsite.com') + end + end + end +end diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb index f5d8758a78a..777c94035d4 100644 --- a/spec/lib/gitlab/git_access_snippet_spec.rb +++ b/spec/lib/gitlab/git_access_snippet_spec.rb @@ -29,8 +29,17 @@ RSpec.describe Gitlab::GitAccessSnippet do let(:actor) { build(:deploy_key) } it 'does not allow push and pull access' do - expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism]) - expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism]) + expect { push_access_check }.to raise_forbidden(:authentication_mechanism) + expect { pull_access_check }.to raise_forbidden(:authentication_mechanism) + end + end + + describe 'when snippet repository is read-only' do + it 'does not allow push and allows pull access' do + allow(snippet).to receive(:repository_read_only?).and_return(true) + + expect { push_access_check }.to raise_forbidden(:read_only) + expect { pull_access_check }.not_to raise_error end end @@ -58,7 +67,7 @@ RSpec.describe Gitlab::GitAccessSnippet do let(:snippet) { nil } it 'blocks access with "not found"' do - expect { pull_access_check }.to raise_snippet_not_found + expect { pull_access_check }.to raise_not_found(:snippet_not_found) end end @@ -66,7 +75,7 @@ RSpec.describe Gitlab::GitAccessSnippet do let(:snippet) { build_stubbed(:personal_snippet) } it 'blocks access with "not found"' do - expect { pull_access_check }.to raise_snippet_not_found + expect { pull_access_check }.to raise_not_found(:no_repo) end end end @@ -81,8 +90,8 @@ RSpec.describe Gitlab::GitAccessSnippet do it 'blocks access when the user did not accept terms' do message = /must accept the Terms of Service in order to perform this action/ - expect { push_access_check }.to raise_forbidden(message) - expect { pull_access_check }.to raise_forbidden(message) + expect { push_access_check }.to raise_forbidden_with_message(message) + expect { pull_access_check }.to raise_forbidden_with_message(message) end it 'allows access when the user accepted the terms' do @@ -149,8 +158,8 @@ RSpec.describe Gitlab::GitAccessSnippet do let(:membership) { membership } it 'respects accessibility' do - expect { push_access_check }.to raise_snippet_not_found - expect { pull_access_check }.to raise_snippet_not_found + expect { push_access_check }.to raise_not_found(:project_not_found) + expect { pull_access_check }.to raise_not_found(:project_not_found) end end end @@ -172,7 +181,7 @@ RSpec.describe Gitlab::GitAccessSnippet do end end - [:guest, :reporter, :maintainer, :author, :admin].each do |membership| + [:guest, :reporter, :maintainer, :author].each do |membership| context membership.to_s do let(:membership) { membership } @@ -183,6 +192,24 @@ RSpec.describe Gitlab::GitAccessSnippet do end end + context 'admin' do + let(:membership) { :admin } + + context 'when admin mode is enabled', :enable_admin_mode do + it 'cannot perform git pushes' do + expect { push_access_check }.to raise_error(described_class::ForbiddenError) + expect { pull_access_check }.not_to raise_error + end + end + + context 'when admin mode is disabled' do + it 'cannot perform git operations' do + expect { push_access_check }.to raise_error(described_class::ForbiddenError) + expect { pull_access_check }.to raise_error(described_class::ForbiddenError) + end + end + end + it_behaves_like 'actor is migration bot' end @@ -255,7 +282,7 @@ RSpec.describe Gitlab::GitAccessSnippet do allow(check).to receive(:validate!).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo') end - expect { push_access_check }.to raise_forbidden('foo') + expect { push_access_check }.to raise_forbidden_with_message('foo') end it 'sets the file count limit from Snippet class' do @@ -372,17 +399,49 @@ RSpec.describe Gitlab::GitAccessSnippet do end end + describe 'HEAD realignment' do + let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project) } + + shared_examples 'HEAD is updated to the snippet default branch' do + let(:actor) { snippet.author } + + specify do + expect(snippet).to receive(:change_head_to_default_branch).and_call_original + + subject + end + + context 'when an error is raised' do + let(:actor) { nil } + + it 'does not realign HEAD' do + expect(snippet).not_to receive(:change_head_to_default_branch).and_call_original + + expect { subject }.to raise_error(described_class::ForbiddenError) + end + end + end + + it_behaves_like 'HEAD is updated to the snippet default branch' do + subject { push_access_check } + end + + it_behaves_like 'HEAD is updated to the snippet default branch' do + subject { pull_access_check } + end + end + private - def raise_snippet_not_found - raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:snippet_not_found]) + def raise_not_found(message_key) + raise_error(described_class::NotFoundError, described_class.error_message(message_key)) end - def raise_project_not_found - raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found]) + def raise_forbidden(message_key) + raise_error(Gitlab::GitAccess::ForbiddenError, described_class.error_message(message_key)) end - def raise_forbidden(message) + def raise_forbidden_with_message(message) raise_error(Gitlab::GitAccess::ForbiddenError, message) end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 780f4329bcc..a0cafe3d763 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::GitAccess do include TermsHelper include GitHelpers + include AdminModeHelper let(:user) { create(:user) } @@ -769,19 +770,39 @@ RSpec.describe Gitlab::GitAccess do describe 'admin user' do let(:user) { create(:admin) } - context 'when member of the project' do - before do - project.add_reporter(user) + context 'when admin mode enabled', :enable_admin_mode do + context 'when member of the project' do + before do + project.add_reporter(user) + end + + context 'pull code' do + it { expect { pull_access_check }.not_to raise_error } + end end - context 'pull code' do - it { expect { pull_access_check }.not_to raise_error } + context 'when is not member of the project' do + context 'pull code' do + it { expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download]) } + end end end - context 'when is not member of the project' do - context 'pull code' do - it { expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download]) } + context 'when admin mode disabled' do + context 'when member of the project' do + before do + project.add_reporter(user) + end + + context 'pull code' do + it { expect { pull_access_check }.not_to raise_error } + end + end + + context 'when is not member of the project' do + context 'pull code' do + it { expect { pull_access_check }.to raise_not_found } + end end end end @@ -870,8 +891,13 @@ RSpec.describe Gitlab::GitAccess do # Expectations are given a custom failure message proc so that it's # easier to identify which check(s) failed. it "has the correct permissions for #{role}s" do - if role == :admin + if role == :admin_without_admin_mode + skip("All admins are allowed to perform actions https://gitlab.com/gitlab-org/gitlab/-/issues/296509") + end + + if [:admin_with_admin_mode, :admin_without_admin_mode].include?(role) user.update_attribute(:admin, true) + enable_admin_mode!(user) if role == :admin_with_admin_mode project.add_guest(user) else project.add_role(user, role) @@ -897,7 +923,7 @@ RSpec.describe Gitlab::GitAccess do end permissions_matrix = { - admin: { + admin_with_admin_mode: { any: true, push_new_branch: true, push_master: true, @@ -909,6 +935,18 @@ RSpec.describe Gitlab::GitAccess do merge_into_protected_branch: true }, + admin_without_admin_mode: { + any: false, + push_new_branch: false, + push_master: false, + push_protected_branch: false, + push_remove_protected_branch: false, + push_tag: false, + push_new_tag: false, + push_all: false, + merge_into_protected_branch: false + }, + maintainer: { any: true, push_new_branch: true, @@ -1009,7 +1047,7 @@ RSpec.describe Gitlab::GitAccess do run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, maintainer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, - admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) + admin_with_admin_mode: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 157c2393ce1..ac4c42d57ee 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -162,11 +162,9 @@ RSpec.describe Gitlab::GitalyClient::CommitService do .with(request, kind_of(Hash)).and_return([changed_paths_response]) returned_value = described_class.new(repository).find_changed_paths(commits) + mapped_expected_value = changed_paths_response.paths.map { |path| Gitlab::Git::ChangedPath.new(status: path.status, path: path.path) } - mapped_returned_value = returned_value.map(&:to_h) - mapped_expected_value = changed_paths_response.paths.map(&:to_h) - - expect(mapped_returned_value).to eq(mapped_expected_value) + expect(returned_value.as_json).to eq(mapped_expected_value.as_json) end end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index f810a5c15a5..7a382df1248 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -131,7 +131,8 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do known_hosts: '', force: false, no_tags: false, - no_prune: false + no_prune: false, + check_tags_changed: false ) expect_any_instance_of(Gitaly::RepositoryService::Stub) @@ -139,7 +140,7 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do .with(expected_request, kind_of(Hash)) .and_return(double(value: true)) - client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1) + client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1, check_tags_changed: false) end context 'SSH auth' do diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 180c6d9e420..3839303b881 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -205,7 +205,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github') service = double - expect(Projects::HousekeepingService) + expect(Repositories::HousekeepingService) .to receive(:new).with(project, :gc).and_return(service) expect(service).to receive(:execute) diff --git a/spec/lib/gitlab/gitpod_spec.rb b/spec/lib/gitlab/gitpod_spec.rb deleted file mode 100644 index 717e396f942..00000000000 --- a/spec/lib/gitlab/gitpod_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Gitpod do - let_it_be(:user) { create(:user) } - - before do - stub_feature_flags(gitpod: feature_scope) - end - - describe '.feature_available?' do - subject { described_class.feature_available? } - - context 'when feature has not been set' do - let(:feature_scope) { nil } - - it { is_expected.to be_truthy } - end - - context 'when feature is disabled' do - let(:feature_scope) { false } - - it { is_expected.to be_falsey } - end - - context 'when feature is enabled globally' do - let(:feature_scope) { true } - - it { is_expected.to be_truthy } - end - - context 'when feature is enabled only to a resource' do - let(:feature_scope) { user } - - it { is_expected.to be_truthy } - end - end - - describe '.feature_enabled?' do - let(:current_user) { nil } - - subject { described_class.feature_enabled?(current_user) } - - context 'when feature has not been set' do - let(:feature_scope) { nil } - - it { is_expected.to be_truthy } - end - - context 'when feature is enabled globally' do - let(:feature_scope) { true } - - it { is_expected.to be_truthy } - end - - context 'when feature is enabled only to a resource' do - let(:feature_scope) { user } - - context 'for the same resource' do - let(:current_user) { user } - - it { is_expected.to be_truthy } - end - - context 'for a different resource' do - let(:current_user) { create(:user) } - - it { is_expected.to be_falsey } - end - end - end -end diff --git a/spec/lib/gitlab/graphql/batch_key_spec.rb b/spec/lib/gitlab/graphql/batch_key_spec.rb new file mode 100644 index 00000000000..881fba5c1be --- /dev/null +++ b/spec/lib/gitlab/graphql/batch_key_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'test_prof/recipes/rspec/let_it_be' + +RSpec.describe ::Gitlab::Graphql::BatchKey do + let_it_be(:rect) { Struct.new(:len, :width) } + let_it_be(:circle) { Struct.new(:radius) } + let(:lookahead) { nil } + let(:object) { rect.new(2, 3) } + + subject { described_class.new(object, lookahead, object_name: :rect) } + + it 'is equal to keys of the same object, regardless of lookahead or object name' do + expect(subject).to eq(described_class.new(rect.new(2, 3))) + expect(subject).to eq(described_class.new(rect.new(2, 3), :anything)) + expect(subject).to eq(described_class.new(rect.new(2, 3), lookahead, object_name: :does_not_matter)) + expect(subject).not_to eq(described_class.new(rect.new(2, 4))) + expect(subject).not_to eq(described_class.new(circle.new(10))) + end + + it 'delegates attribute lookup methods to the inner object' do + other = rect.new(2, 3) + + expect(subject.hash).to eq(other.hash) + expect(subject.len).to eq(other.len) + expect(subject.width).to eq(other.width) + end + + it 'allows the object to be named more meaningfully' do + expect(subject.object).to eq(object) + expect(subject.object).to eq(subject.rect) + end + + it 'works as a hash key' do + h = { subject => :foo } + + expect(h[described_class.new(object)]).to eq(:foo) + end + + describe '#requires?' do + it 'returns false if the lookahead was not provided' do + expect(subject.requires?([:foo])).to be(false) + end + + context 'lookahead was provided' do + let(:lookahead) { double(:Lookahead) } + + before do + allow(lookahead).to receive(:selection).with(Symbol).and_return(lookahead) + end + + it 'returns false if the path is empty' do + expect(subject.requires?([])).to be(false) + end + + context 'it selects the field' do + before do + allow(lookahead).to receive(:selects?).with(Symbol).once.and_return(true) + end + + it 'returns true' do + expect(subject.requires?(%i[foo bar baz])).to be(true) + end + end + + context 'it does not select the field' do + before do + allow(lookahead).to receive(:selects?).with(Symbol).once.and_return(false) + end + + it 'returns false' do + expect(subject.requires?(%i[foo bar baz])).to be(false) + end + end + end + end +end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index 0ac54a20fcc..02e67488d3f 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -21,6 +21,47 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) end + # see: https://gitlab.com/gitlab-org/gitlab/-/issues/297358 + context 'the relation has been preloaded' do + let(:projects) { Project.all.preload(:issues) } + let(:nodes) { projects.first.issues } + + before do + project = create(:project) + create_list(:issue, 3, project: project) + end + + it 'is loaded' do + expect(nodes).to be_loaded + end + + it 'does not error when accessing pagination information' do + connection.first = 2 + + expect(connection).to have_attributes( + has_previous_page: false, + has_next_page: true + ) + end + + it 'can generate cursors' do + connection.send(:ordered_items) # necessary to generate the order-list + + expect(connection.cursor_for(nodes.first)).to be_a(String) + end + + it 'can read the next page' do + connection.send(:ordered_items) # necessary to generate the order-list + ordered = nodes.reorder(id: :desc) + next_page = described_class.new(nodes, + context: context, + max_page_size: 3, + after: connection.cursor_for(ordered.second)) + + expect(next_page.sliced_nodes).to contain_exactly(ordered.third) + end + end + it_behaves_like 'a connection with collection methods' it_behaves_like 'a redactable connection' do diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb new file mode 100644 index 00000000000..6e08a87523f --- /dev/null +++ b/spec/lib/gitlab/graphql/queries_spec.rb @@ -0,0 +1,343 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require "test_prof/recipes/rspec/let_it_be" + +RSpec.describe Gitlab::Graphql::Queries do + shared_examples 'a valid GraphQL query for the blog schema' do + it 'is valid' do + expect(subject.validate(schema).second).to be_empty + end + end + + shared_examples 'an invalid GraphQL query for the blog schema' do + it 'is invalid' do + expect(subject.validate(schema).second).to match errors + end + end + + # Toy schema to validate queries against + let_it_be(:schema) do + author = Class.new(GraphQL::Schema::Object) do + graphql_name 'Author' + field :name, GraphQL::STRING_TYPE, null: true + field :handle, GraphQL::STRING_TYPE, null: false + field :verified, GraphQL::BOOLEAN_TYPE, null: false + end + + post = Class.new(GraphQL::Schema::Object) do + graphql_name 'Post' + field :name, GraphQL::STRING_TYPE, null: false + field :title, GraphQL::STRING_TYPE, null: false + field :content, GraphQL::STRING_TYPE, null: true + field :author, author, null: false + end + author.field :posts, [post], null: false do + argument :blog_title, GraphQL::STRING_TYPE, required: false + end + + blog = Class.new(GraphQL::Schema::Object) do + graphql_name 'Blog' + field :title, GraphQL::STRING_TYPE, null: false + field :description, GraphQL::STRING_TYPE, null: false + field :main_author, author, null: false + field :posts, [post], null: false + field :post, post, null: true do + argument :slug, GraphQL::STRING_TYPE, required: true + end + end + + Class.new(GraphQL::Schema) do + query(Class.new(GraphQL::Schema::Object) do + graphql_name 'Query' + field :blog, blog, null: true do + argument :title, GraphQL::STRING_TYPE, required: true + end + field :post, post, null: true do + argument :slug, GraphQL::STRING_TYPE, required: true + end + end) + end + end + + let(:root) do + Rails.root / 'fixtures/lib/gitlab/graphql/queries' + end + + describe Gitlab::Graphql::Queries::Fragments do + subject { described_class.new(root) } + + it 'has the right home' do + expect(subject.home).to eq (root / 'app/assets/javascripts').to_s + end + + it 'has the right EE home' do + expect(subject.home_ee).to eq (root / 'ee/app/assets/javascripts').to_s + end + + it 'caches query definitions' do + fragment = subject.get('foo') + + expect(fragment).to be_a(::Gitlab::Graphql::Queries::Definition) + expect(subject.get('foo')).to be fragment + end + end + + describe '.all' do + it 'is the combination of finding queries in CE and EE' do + expect(described_class) + .to receive(:find).with(Rails.root / 'app/assets/javascripts').and_return([:ce]) + expect(described_class) + .to receive(:find).with(Rails.root / 'ee/app/assets/javascripts').and_return([:ee]) + + expect(described_class.all).to eq([:ce, :ee]) + end + end + + describe '.find' do + def definition_of(path) + be_a(::Gitlab::Graphql::Queries::Definition) + .and(have_attributes(file: path.to_s)) + end + + it 'find a single specific file' do + path = root / 'post_by_slug.graphql' + + expect(described_class.find(path)).to contain_exactly(definition_of(path)) + end + + it 'ignores files that do not exist' do + path = root / 'not_there.graphql' + + expect(described_class.find(path)).to be_empty + end + + it 'ignores fragments' do + path = root / 'author.fragment.graphql' + + expect(described_class.find(path)).to be_empty + end + + it 'ignores typedefs' do + path = root / 'typedefs.graphql' + + expect(described_class.find(path)).to be_empty + end + + it 'finds all query definitions under a root directory' do + found = described_class.find(root) + + expect(found).to include( + definition_of(root / 'post_by_slug.graphql'), + definition_of(root / 'post_by_slug.with_import.graphql'), + definition_of(root / 'post_by_slug.with_import.misspelled.graphql'), + definition_of(root / 'duplicate_imports.graphql'), + definition_of(root / 'deeply/nested/query.graphql') + ) + + expect(found).not_to include( + definition_of(root / 'typedefs.graphql'), + definition_of(root / 'author.fragment.graphql') + ) + end + end + + describe Gitlab::Graphql::Queries::Definition do + let(:fragments) { Gitlab::Graphql::Queries::Fragments.new(root, '.') } + + subject { described_class.new(root / path, fragments) } + + context 'a simple query' do + let(:path) { 'post_by_slug.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a query with an import' do + let(:path) { 'post_by_slug.with_import.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a query with duplicate imports' do + let(:path) { 'duplicate_imports.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a query importing from ee_else_ce' do + let(:path) { 'ee_else_ce.import.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + + it 'can resolve the ee fields' do + expect(subject.text(mode: :ce)).not_to include('verified') + expect(subject.text(mode: :ee)).to include('verified') + end + end + + context 'a query refering to parent directories' do + let(:path) { 'deeply/nested/query.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a query refering to parent directories, incorrectly' do + let(:path) { 'deeply/nested/bad_import.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + be_a(::Gitlab::Graphql::Queries::FileNotFound) + .and(have_attributes(message: include('deeply/author.fragment.graphql'))) + ) + end + end + end + + context 'a query with a broken import' do + let(:path) { 'post_by_slug.with_import.misspelled.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + be_a(::Gitlab::Graphql::Queries::FileNotFound) + .and(have_attributes(message: include('auther.fragment.graphql'))) + ) + end + end + end + + context 'a query which imports a file with a broken import' do + let(:path) { 'transitive_bad_import.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + be_a(::Gitlab::Graphql::Queries::FileNotFound) + .and(have_attributes(message: include('does-not-exist.graphql'))) + ) + end + end + end + + context 'a query containing a client directive' do + let(:path) { 'client.query.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + + it 'is tagged as a client query' do + expect(subject.validate(schema).first).to eq :client_query + end + end + + context 'a mixed client query, valid' do + let(:path) { 'mixed_client.query.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + + it 'is not tagged as a client query' do + expect(subject.validate(schema).first).not_to eq :client_query + end + end + + context 'a mixed client query, with skipped argument' do + let(:path) { 'mixed_client_skipped_argument.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a mixed client query, with unused fragment' do + let(:path) { 'mixed_client_unused_fragment.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a client query, with unused fragment' do + let(:path) { 'client_unused_fragment.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + + it 'is tagged as a client query' do + expect(subject.validate(schema).first).to eq :client_query + end + end + + context 'a mixed client query, invalid' do + let(:path) { 'mixed_client_invalid.query.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly(have_attributes(message: include('titlz'))) + end + end + end + + context 'a query containing a connection directive' do + let(:path) { 'connection.query.graphql' } + + it_behaves_like 'a valid GraphQL query for the blog schema' + end + + context 'a query which mentions an incorrect field' do + let(:path) { 'wrong_field.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + have_attributes(message: /'createdAt' doesn't exist/), + have_attributes(message: /'categories' doesn't exist/) + ) + end + end + end + + context 'a query which has a missing argument' do + let(:path) { 'missing_argument.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + have_attributes(message: include('blog')) + ) + end + end + end + + context 'a query which has a bad argument' do + let(:path) { 'bad_argument.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + have_attributes(message: include('Nullability mismatch on variable $bad')) + ) + end + end + end + + context 'a query which has a syntax error' do + let(:path) { 'syntax-error.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + have_attributes(message: include('Parse error')) + ) + end + end + end + + context 'a query which has an unused import' do + let(:path) { 'unused_import.graphql' } + + it_behaves_like 'an invalid GraphQL query for the blog schema' do + let(:errors) do + contain_exactly( + have_attributes(message: include('AuthorF was defined, but not used')) + ) + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index fba32ae0673..825513bdfc5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -87,6 +87,7 @@ label: - merge_requests - priorities - epic_board_labels +- epic_lists milestone: - group - project @@ -524,7 +525,6 @@ project: - designs - project_aliases - external_pull_requests -- alerts_service - grafana_integration - remove_source_branch_after_merge - deleting_user @@ -560,6 +560,7 @@ project: - alert_management_http_integrations - exported_protected_branches - incident_management_oncall_schedules +- debian_distributions award_emoji: - awardable - user @@ -722,6 +723,7 @@ epic: - user_mentions - note_authors - boards_epic_user_preferences +- epic_board_positions epic_issue: - epic - issue diff --git a/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb b/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb index 3f5661d4ca6..0092c69d0bb 100644 --- a/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb +++ b/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb @@ -12,7 +12,8 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do ingress: ingress, egress: egress, labels: labels, - resource_version: resource_version + resource_version: resource_version, + annotations: annotations ) end @@ -20,7 +21,7 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do ::Kubeclient::Resource.new( apiVersion: Gitlab::Kubernetes::CiliumNetworkPolicy::API_VERSION, kind: Gitlab::Kubernetes::CiliumNetworkPolicy::KIND, - metadata: { name: name, namespace: namespace, resourceVersion: resource_version }, + metadata: { name: name, namespace: namespace, resourceVersion: resource_version, annotations: annotations }, spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: egress }, description: description ) @@ -34,6 +35,7 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do let(:description) { 'example-description' } let(:partial_class_name) { described_class.name.split('::').last } let(:resource_version) { 101 } + let(:annotations) { { 'app.gitlab.com/alert': 'true' } } let(:ingress) do [ { @@ -64,6 +66,8 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do name: example-name namespace: example-namespace resourceVersion: 101 + annotations: + app.gitlab.com/alert: "true" spec: endpointSelector: matchLabels: @@ -157,7 +161,7 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do description: description, metadata: { name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', - labels: { app: 'foo' }, resourceVersion: resource_version + labels: { app: 'foo' }, resourceVersion: resource_version, annotations: annotations }, spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: nil, labels: nil } ) @@ -168,7 +172,7 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do apiVersion: Gitlab::Kubernetes::CiliumNetworkPolicy::API_VERSION, kind: Gitlab::Kubernetes::CiliumNetworkPolicy::KIND, description: description, - metadata: { name: name, namespace: namespace, resourceVersion: resource_version, labels: { app: 'foo' } }, + metadata: { name: name, namespace: namespace, resourceVersion: resource_version, labels: { app: 'foo' }, annotations: annotations }, spec: { endpointSelector: endpoint_selector, ingress: ingress } ) end @@ -211,7 +215,7 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do { apiVersion: Gitlab::Kubernetes::CiliumNetworkPolicy::API_VERSION, kind: Gitlab::Kubernetes::CiliumNetworkPolicy::KIND, - metadata: { name: name, namespace: namespace, resourceVersion: resource_version }, + metadata: { name: name, namespace: namespace, resourceVersion: resource_version, annotations: annotations }, spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: egress }, description: description } @@ -248,5 +252,15 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do it { is_expected.to eq(resource) } end + + context 'without annotations' do + let(:annotations) { nil } + + before do + resource[:metadata].delete(:annotations) + end + + it { is_expected.to eq(resource) } + end end end diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb index e80bb3dfb07..2e373613269 100644 --- a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb +++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb @@ -56,9 +56,10 @@ RSpec.describe Gitlab::Kubernetes::KubectlCmd do describe '.delete_crds_from_group' do it 'constructs string properly' do - expected_command = 'kubectl api-resources -o name --api-group foo | xargs kubectl delete --ignore-not-found crd' + command = 'kubectl api-resources -o name --api-group foo | xargs -r kubectl delete --ignore-not-found crd' + command_with_retries = "for i in $(seq 1 3); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)" - expect(described_class.delete_crds_from_group("foo")).to eq expected_command + expect(described_class.delete_crds_from_group("foo")).to eq command_with_retries end end end diff --git a/spec/lib/gitlab/kubernetes/pod_cmd_spec.rb b/spec/lib/gitlab/kubernetes/pod_cmd_spec.rb new file mode 100644 index 00000000000..51bdbf64741 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/pod_cmd_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Kubernetes::PodCmd do + describe '.retry_command' do + it 'constructs string properly' do + command = 'my command' + command_with_retries = "for i in $(seq 1 3); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)" + + expect(described_class.retry_command(command)).to eq command_with_retries + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb index 7f05f35c941..f751416f4ec 100644 --- a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb @@ -7,15 +7,7 @@ RSpec.describe Gitlab::Metrics::Samplers::ActionCableSampler do subject { described_class.new(action_cable: action_cable) } - describe '#interval' do - it 'samples every five seconds by default' do - expect(subject.interval).to eq(5) - end - - it 'samples at other intervals if requested' do - expect(described_class.new(11).interval).to eq(11) - end - end + it_behaves_like 'metrics sampler', 'ACTION_CABLE_SAMPLER' describe '#sample' do let(:pool) { instance_double(Concurrent::ThreadPoolExecutor) } diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb index b94d19ff227..9572e9f50be 100644 --- a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb @@ -5,15 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do subject { described_class.new } - describe '#interval' do - it 'samples every five seconds by default' do - expect(subject.interval).to eq(5) - end - - it 'samples at other intervals if requested' do - expect(described_class.new(11).interval).to eq(11) - end - end + it_behaves_like 'metrics sampler', 'DATABASE_SAMPLER' describe '#sample' do before do diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb index 214649d3e7e..2013435a074 100644 --- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb @@ -11,15 +11,7 @@ RSpec.describe Gitlab::Metrics::Samplers::PumaSampler do allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) end - describe '#interval' do - it 'samples every five seconds by default' do - expect(subject.interval).to eq(5) - end - - it 'samples at other intervals if requested' do - expect(described_class.new(11).interval).to eq(11) - end - end + it_behaves_like 'metrics sampler', 'PUMA_SAMPLER' describe '#sample' do before do diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index eb6c83096b9..6f1e0480197 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -10,6 +10,8 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) end + it_behaves_like 'metrics sampler', 'RUBY_SAMPLER' + describe '#initialize' do it 'sets process_start_time_seconds' do freeze_time do @@ -18,16 +20,6 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do end end - describe '#interval' do - it 'samples every sixty seconds by default' do - expect(subject.interval).to eq(60) - end - - it 'samples at other intervals if requested' do - expect(described_class.new(11).interval).to eq(11) - end - end - describe '#sample' do it 'adds a metric containing the process resident memory bytes' do expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return(9000) diff --git a/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb index 19477589289..5dabafb7c0b 100644 --- a/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb @@ -5,15 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Metrics::Samplers::ThreadsSampler do subject { described_class.new } - describe '#interval' do - it 'samples every five seconds by default' do - expect(subject.interval).to eq(5) - end - - it 'samples at other intervals if requested' do - expect(described_class.new(11).interval).to eq(11) - end - end + it_behaves_like 'metrics sampler', 'THREADS_SAMPLER' describe '#sample' do before do diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb index 9f2180c4170..7971a7cabd5 100644 --- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe Gitlab::Metrics::Samplers::UnicornSampler do subject { described_class.new(1.second) } + it_behaves_like 'metrics sampler', 'UNICORN_SAMPLER' + describe '#sample' do let(:unicorn) { Module.new } let(:raindrops) { double('raindrops') } diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index 720bd5d79b3..732aa553737 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -96,6 +96,25 @@ RSpec.describe Gitlab::Metrics::System do expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072) end end + + describe '.summary' do + it 'contains a selection of the available fields' do + stub_const('RUBY_DESCRIPTION', 'ruby-3.0-patch1') + mock_existing_proc_file('/proc/self/status', proc_status) + mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup) + + summary = described_class.summary + + expect(summary[:version]).to eq('ruby-3.0-patch1') + expect(summary[:gc_stat].keys).to eq(GC.stat.keys) + expect(summary[:memory_rss]).to eq(2527232) + expect(summary[:memory_uss]).to eq(475136) + expect(summary[:memory_pss]).to eq(515072) + expect(summary[:time_cputime]).to be_a(Float) + expect(summary[:time_realtime]).to be_a(Float) + expect(summary[:time_monotonic]).to be_a(Float) + end + end end context 'when /proc files do not exist' do @@ -128,6 +147,21 @@ RSpec.describe Gitlab::Metrics::System do expect(described_class.max_open_file_descriptors).to eq(0) end end + + describe '.summary' do + it 'returns only available fields' do + summary = described_class.summary + + expect(summary[:version]).to be_a(String) + expect(summary[:gc_stat].keys).to eq(GC.stat.keys) + expect(summary[:memory_rss]).to eq(0) + expect(summary[:memory_uss]).to eq(0) + expect(summary[:memory_pss]).to eq(0) + expect(summary[:time_cputime]).to be_a(Float) + expect(summary[:time_realtime]).to be_a(Float) + expect(summary[:time_monotonic]).to be_a(Float) + end + end end describe '.cpu_time' do diff --git a/spec/lib/gitlab/middleware/multipart/handler_for_jwt_params_spec.rb b/spec/lib/gitlab/middleware/multipart/handler_for_jwt_params_spec.rb deleted file mode 100644 index 59ec743f6ca..00000000000 --- a/spec/lib/gitlab/middleware/multipart/handler_for_jwt_params_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Middleware::Multipart::HandlerForJWTParams do - using RSpec::Parameterized::TableSyntax - - let_it_be(:env) { Rack::MockRequest.env_for('/', method: 'post', params: {}) } - let_it_be(:message) { { 'rewritten_fields' => {} } } - - describe '#allowed_paths' do - let_it_be(:expected_allowed_paths) do - [ - Dir.tmpdir, - ::FileUploader.root, - ::Gitlab.config.uploads.storage_path, - ::JobArtifactUploader.workhorse_upload_path, - ::LfsObjectUploader.workhorse_upload_path, - File.join(Rails.root, 'public/uploads/tmp') - ] - end - - let_it_be(:expected_with_packages_path) { expected_allowed_paths + [::Packages::PackageFileUploader.workhorse_upload_path] } - - subject { described_class.new(env, message).send(:allowed_paths) } - - where(:package_features_enabled, :object_storage_enabled, :direct_upload_enabled, :expected_paths) do - false | false | true | :expected_allowed_paths - false | false | false | :expected_allowed_paths - false | true | true | :expected_allowed_paths - false | true | false | :expected_allowed_paths - true | false | true | :expected_with_packages_path - true | false | false | :expected_with_packages_path - true | true | true | :expected_allowed_paths - true | true | false | :expected_with_packages_path - end - - with_them do - before do - stub_config(packages: { - enabled: package_features_enabled, - object_store: { - enabled: object_storage_enabled, - direct_upload: direct_upload_enabled - }, - storage_path: '/any/dir' - }) - end - - it { is_expected.to eq(send(expected_paths)) } - end - end -end diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb new file mode 100644 index 00000000000..65ec3535271 --- /dev/null +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Middleware::Multipart do + include MultipartHelpers + + describe '#call' do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + let(:secret) { Gitlab::Workhorse.secret } + let(:issuer) { 'gitlab-workhorse' } + + subject do + env = post_env( + rewritten_fields: rewritten_fields, + params: params, + secret: secret, + issuer: issuer + ) + middleware.call(env) + end + + context 'remote file mode' do + let(:mode) { :remote } + + it_behaves_like 'handling all upload parameters conditions' + + context 'and a path set' do + include_context 'with one temporary file for multipart' + + let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } + let(:params) { upload_parameters_for(key: 'file', mode: mode, filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') } + + it 'builds an UploadedFile' do + expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file)) + + subject + end + end + end + + context 'local file mode' do + let(:mode) { :local } + + it_behaves_like 'handling all upload parameters conditions' + + context 'when file is' do + include_context 'with one temporary file for multipart' + + let(:allowed_paths) { [Dir.tmpdir] } + + before do + expect_next_instance_of(::Gitlab::Middleware::Multipart::Handler) do |handler| + expect(handler).to receive(:allowed_paths).and_return(allowed_paths) + end + end + + context 'in allowed paths' do + let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } + let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename) } + + it 'builds an UploadedFile' do + expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file)) + + subject + end + end + + context 'not in allowed paths' do + let(:allowed_paths) { [] } + + let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } + let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode) } + + it 'returns an error' do + result = subject + + expect(result[0]).to eq(400) + expect(result[2]).to include('insecure path used') + end + end + end + end + + context 'with dummy params in remote mode' do + let(:rewritten_fields) { { 'file' => 'should/not/be/read' } } + let(:params) { upload_parameters_for(key: 'file', mode: mode) } + let(:mode) { :remote } + + context 'with an invalid secret' do + let(:secret) { 'INVALID_SECRET' } + + it { expect { subject }.to raise_error(JWT::VerificationError) } + end + + context 'with an invalid issuer' do + let(:issuer) { 'INVALID_ISSUER' } + + it { expect { subject }.to raise_error(JWT::InvalidIssuerError) } + end + + context 'with invalid rewritten field key' do + invalid_keys = [ + '[file]', + ';file', + 'file]', + ';file]', + 'file]]', + 'file;;' + ] + + invalid_keys.each do |invalid_key| + context invalid_key do + let(:rewritten_fields) { { invalid_key => 'should/not/be/read' } } + + it { expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"") } + end + end + end + + context 'with an invalid upload key' do + include_context 'with one temporary file for multipart' + + RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:| + let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) } + let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, mode: mode, filename: filename, remote_id: remote_id) } + + it 'raises an error' do + expect { subject }.to raise_error(RuntimeError, error_message) + end + end + + it_behaves_like 'rejecting the invalid key', + key_in_header: 'file', + key_in_upload_params: 'wrong_key', + error_message: 'Empty JWT param: file.gitlab-workhorse-upload' + it_behaves_like 'rejecting the invalid key', + key_in_header: 'user[avatar', + key_in_upload_params: 'user[avatar]', + error_message: 'invalid field: "user[avatar"' + it_behaves_like 'rejecting the invalid key', + key_in_header: '[user]avatar', + key_in_upload_params: 'user[avatar]', + error_message: 'invalid field: "[user]avatar"' + it_behaves_like 'rejecting the invalid key', + key_in_header: 'user[]avatar', + key_in_upload_params: 'user[avatar]', + error_message: 'invalid field: "user[]avatar"' + it_behaves_like 'rejecting the invalid key', + key_in_header: 'user[avatar[image[url]]]', + key_in_upload_params: 'user[avatar]', + error_message: 'invalid field: "user[avatar[image[url]]]"' + it_behaves_like 'rejecting the invalid key', + key_in_header: '[]', + key_in_upload_params: 'user[avatar]', + error_message: 'invalid field: "[]"' + it_behaves_like 'rejecting the invalid key', + key_in_header: 'x' * 11000, + key_in_upload_params: 'user[avatar]', + error_message: "invalid field: \"#{'x' * 11000}\"" + end + + context 'with a modified JWT payload' do + include_context 'with one temporary file for multipart' + + let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } + let(:crafted_payload) { Base64.urlsafe_encode64({ 'path' => 'test' }.to_json) } + let(:params) do + upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id).tap do |params| + header, _, sig = params['file.gitlab-workhorse-upload'].split('.') + params['file.gitlab-workhorse-upload'] = [header, crafted_payload, sig].join('.') + end + end + + it 'raises an error' do + expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised') + end + end + + context 'with a modified JWT sig' do + include_context 'with one temporary file for multipart' + + let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } + let(:params) do + upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id).tap do |params| + header, payload, sig = params['file.gitlab-workhorse-upload'].split('.') + params['file.gitlab-workhorse-upload'] = [header, payload, "#{sig}modified"].join('.') + end + end + + it 'raises an error' do + expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised') + end + end + end + end +end diff --git a/spec/lib/gitlab/middleware/multipart_with_handler_for_jwt_params_spec.rb b/spec/lib/gitlab/middleware/multipart_with_handler_for_jwt_params_spec.rb deleted file mode 100644 index a1e9ac6e425..00000000000 --- a/spec/lib/gitlab/middleware/multipart_with_handler_for_jwt_params_spec.rb +++ /dev/null @@ -1,202 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Middleware::Multipart do - include MultipartHelpers - - describe '#call' do - let(:app) { double(:app) } - let(:middleware) { described_class.new(app) } - let(:secret) { Gitlab::Workhorse.secret } - let(:issuer) { 'gitlab-workhorse' } - - subject do - env = post_env( - rewritten_fields: rewritten_fields, - params: params, - secret: secret, - issuer: issuer - ) - middleware.call(env) - end - - before do - stub_feature_flags(upload_middleware_jwt_params_handler: true) - end - - context 'remote file mode' do - let(:mode) { :remote } - - it_behaves_like 'handling all upload parameters conditions' - - context 'and a path set' do - include_context 'with one temporary file for multipart' - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(key: 'file', filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') } - - it 'builds an UploadedFile' do - expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file)) - - subject - end - end - end - - context 'local file mode' do - let(:mode) { :local } - - it_behaves_like 'handling all upload parameters conditions' - - context 'when file is' do - include_context 'with one temporary file for multipart' - - let(:allowed_paths) { [Dir.tmpdir] } - - before do - expect_next_instance_of(::Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler| - expect(handler).to receive(:allowed_paths).and_return(allowed_paths) - end - end - - context 'in allowed paths' do - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename) } - - it 'builds an UploadedFile' do - expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file)) - - subject - end - end - - context 'not in allowed paths' do - let(:allowed_paths) { [] } - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file') } - - it 'returns an error' do - result = subject - - expect(result[0]).to eq(400) - expect(result[2]).to include('insecure path used') - end - end - end - end - - context 'with dummy params in remote mode' do - let(:rewritten_fields) { { 'file' => 'should/not/be/read' } } - let(:params) { upload_parameters_for(key: 'file') } - let(:mode) { :remote } - - context 'with an invalid secret' do - let(:secret) { 'INVALID_SECRET' } - - it { expect { subject }.to raise_error(JWT::VerificationError) } - end - - context 'with an invalid issuer' do - let(:issuer) { 'INVALID_ISSUER' } - - it { expect { subject }.to raise_error(JWT::InvalidIssuerError) } - end - - context 'with invalid rewritten field key' do - invalid_keys = [ - '[file]', - ';file', - 'file]', - ';file]', - 'file]]', - 'file;;' - ] - - invalid_keys.each do |invalid_key| - context invalid_key do - let(:rewritten_fields) { { invalid_key => 'should/not/be/read' } } - - it { expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"") } - end - end - end - - context 'with an invalid upload key' do - include_context 'with one temporary file for multipart' - - RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:| - let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) } - - it 'raises an error' do - expect { subject }.to raise_error(RuntimeError, error_message) - end - end - - it_behaves_like 'rejecting the invalid key', - key_in_header: 'file', - key_in_upload_params: 'wrong_key', - error_message: 'Empty JWT param: file.gitlab-workhorse-upload' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'user[avatar', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "user[avatar"' - it_behaves_like 'rejecting the invalid key', - key_in_header: '[user]avatar', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "[user]avatar"' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'user[]avatar', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "user[]avatar"' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'user[avatar[image[url]]]', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "user[avatar[image[url]]]"' - it_behaves_like 'rejecting the invalid key', - key_in_header: '[]', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "[]"' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'x' * 11000, - key_in_upload_params: 'user[avatar]', - error_message: "invalid field: \"#{'x' * 11000}\"" - end - - context 'with a modified JWT payload' do - include_context 'with one temporary file for multipart' - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:crafted_payload) { Base64.urlsafe_encode64({ 'path' => 'test' }.to_json) } - let(:params) do - upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id).tap do |params| - header, _, sig = params['file.gitlab-workhorse-upload'].split('.') - params['file.gitlab-workhorse-upload'] = [header, crafted_payload, sig].join('.') - end - end - - it 'raises an error' do - expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised') - end - end - - context 'with a modified JWT sig' do - include_context 'with one temporary file for multipart' - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) do - upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id).tap do |params| - header, payload, sig = params['file.gitlab-workhorse-upload'].split('.') - params['file.gitlab-workhorse-upload'] = [header, payload, "#{sig}modified"].join('.') - end - end - - it 'raises an error' do - expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised') - end - end - end - end -end diff --git a/spec/lib/gitlab/middleware/multipart_with_handler_spec.rb b/spec/lib/gitlab/middleware/multipart_with_handler_spec.rb deleted file mode 100644 index 8c2af775574..00000000000 --- a/spec/lib/gitlab/middleware/multipart_with_handler_spec.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Middleware::Multipart do - include MultipartHelpers - - describe '#call' do - let(:app) { double(:app) } - let(:middleware) { described_class.new(app) } - let(:secret) { Gitlab::Workhorse.secret } - let(:issuer) { 'gitlab-workhorse' } - - subject do - env = post_env( - rewritten_fields: rewritten_fields, - params: params, - secret: secret, - issuer: issuer - ) - middleware.call(env) - end - - before do - stub_feature_flags(upload_middleware_jwt_params_handler: false) - end - - context 'remote file mode' do - let(:mode) { :remote } - - it_behaves_like 'handling all upload parameters conditions' - - context 'and a path set' do - include_context 'with one temporary file for multipart' - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(key: 'file', filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') } - - it 'builds an UploadedFile' do - expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file)) - - subject - end - end - end - - context 'local file mode' do - let(:mode) { :local } - - it_behaves_like 'handling all upload parameters conditions' - - context 'when file is' do - include_context 'with one temporary file for multipart' - - let(:allowed_paths) { [Dir.tmpdir] } - - before do - expect_next_instance_of(::Gitlab::Middleware::Multipart::Handler) do |handler| - expect(handler).to receive(:allowed_paths).and_return(allowed_paths) - end - end - - context 'in allowed paths' do - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename) } - - it 'builds an UploadedFile' do - expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file)) - - subject - end - end - - context 'not in allowed paths' do - let(:allowed_paths) { [] } - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file') } - - it 'returns an error' do - result = subject - - expect(result[0]).to eq(400) - expect(result[2]).to include('insecure path used') - end - end - end - end - - context 'with dummy params in remote mode' do - let(:rewritten_fields) { { 'file' => 'should/not/be/read' } } - let(:params) { upload_parameters_for(key: 'file') } - let(:mode) { :remote } - - context 'with an invalid secret' do - let(:secret) { 'INVALID_SECRET' } - - it { expect { subject }.to raise_error(JWT::VerificationError) } - end - - context 'with an invalid issuer' do - let(:issuer) { 'INVALID_ISSUER' } - - it { expect { subject }.to raise_error(JWT::InvalidIssuerError) } - end - - context 'with invalid rewritten field key' do - invalid_keys = [ - '[file]', - ';file', - 'file]', - ';file]', - 'file]]', - 'file;;' - ] - - invalid_keys.each do |invalid_key| - context invalid_key do - let(:rewritten_fields) { { invalid_key => 'should/not/be/read' } } - - it { expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"") } - end - end - end - - context 'with invalid key in parameters' do - include_context 'with one temporary file for multipart' - - let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'wrong_key', filename: filename, remote_id: remote_id) } - - it 'builds no UploadedFile' do - expect(app).to receive(:call) do |env| - received_params = get_params(env) - expect(received_params['file']).to be_nil - expect(received_params['wrong_key']).to be_nil - end - - subject - end - end - - context 'with invalid key in header' do - include_context 'with one temporary file for multipart' - - RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:| - let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) } - - it 'raises an error' do - expect { subject }.to raise_error(RuntimeError, error_message) - end - end - - it_behaves_like 'rejecting the invalid key', - key_in_header: 'user[avatar', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "user[avatar"' - it_behaves_like 'rejecting the invalid key', - key_in_header: '[user]avatar', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "[user]avatar"' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'user[]avatar', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "user[]avatar"' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'user[avatar[image[url]]]', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "user[avatar[image[url]]]"' - it_behaves_like 'rejecting the invalid key', - key_in_header: '[]', - key_in_upload_params: 'user[avatar]', - error_message: 'invalid field: "[]"' - it_behaves_like 'rejecting the invalid key', - key_in_header: 'x' * 11000, - key_in_upload_params: 'user[avatar]', - error_message: "invalid field: \"#{'x' * 11000}\"" - end - - context 'with key with unbalanced brackets in header' do - include_context 'with one temporary file for multipart' - - let(:invalid_key) { 'user[avatar' } - let(:rewritten_fields) { rewritten_fields_hash( invalid_key => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'user[avatar]', filename: filename, remote_id: remote_id) } - - it 'builds no UploadedFile' do - expect(app).not_to receive(:call) - - expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"") - end - end - end - end -end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 8e9f7e372c5..cd89674af0f 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -102,6 +102,7 @@ RSpec.describe Gitlab::PathRegex do .concat(files_in_public) .concat(Array(API::API.prefix.to_s)) .concat(sitemap_words) + .concat(deprecated_routes) .compact .uniq end @@ -110,6 +111,11 @@ RSpec.describe Gitlab::PathRegex do %w(sitemap sitemap.xml sitemap.xml.gz) end + let(:deprecated_routes) do + # profile was deprecated in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51646 + %w(profile) + end + let(:ee_top_level_words) do %w(unsubscribes v2) end diff --git a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb index bbc8b0d67e0..05cdc5bb79b 100644 --- a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb +++ b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb @@ -31,6 +31,7 @@ RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid) expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid) expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + expect(client).to receive(:expire).with(GitlabPerformanceBarStatsWorker::STATS_KEY, GitlabPerformanceBarStatsWorker::STATS_KEY_EXPIRE) peek_adapter.new(client).save('foo') end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 98bd2efdbc6..4eb13e63b46 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Gitlab::ProjectTemplate do hexo sse_middleman gitpod_spring_petclinic nfhugo nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx serverless_framework jsonnet cluster_management + kotlin_native_linux ] expect(described_class.all).to be_an(Array) diff --git a/spec/lib/gitlab/prometheus/internal_spec.rb b/spec/lib/gitlab/prometheus/internal_spec.rb index 7771d85222a..b08b8813470 100644 --- a/spec/lib/gitlab/prometheus/internal_spec.rb +++ b/spec/lib/gitlab/prometheus/internal_spec.rb @@ -3,12 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::Prometheus::Internal do - let(:listen_address) { 'localhost:9090' } + let(:server_address) { 'localhost:9090' } let(:prometheus_settings) do { - enable: true, - listen_address: listen_address + enabled: true, + server_address: server_address } end @@ -27,25 +27,25 @@ RSpec.describe Gitlab::Prometheus::Internal do it_behaves_like 'returns valid uri', 'http://localhost:9090' context 'with non default prometheus address' do - let(:listen_address) { 'https://localhost:9090' } + let(:server_address) { 'https://localhost:9090' } it_behaves_like 'returns valid uri', 'https://localhost:9090' context 'with :9090 symbol' do - let(:listen_address) { :':9090' } + let(:server_address) { :':9090' } it_behaves_like 'returns valid uri', 'http://localhost:9090' end context 'with 0.0.0.0:9090' do - let(:listen_address) { '0.0.0.0:9090' } + let(:server_address) { '0.0.0.0:9090' } it_behaves_like 'returns valid uri', 'http://localhost:9090' end end - context 'when listen_address is nil' do - let(:listen_address) { nil } + context 'when server_address is nil' do + let(:server_address) { nil } it 'does not fail' do expect(described_class.uri).to be_nil @@ -53,7 +53,7 @@ RSpec.describe Gitlab::Prometheus::Internal do end context 'when prometheus listen address is blank in gitlab.yml' do - let(:listen_address) { '' } + let(:server_address) { '' } it 'does not configure prometheus' do expect(described_class.uri).to be_nil @@ -61,26 +61,6 @@ RSpec.describe Gitlab::Prometheus::Internal do end end - describe '.server_address' do - context 'self.uri returns valid uri' do - ['http://localhost:9090', 'https://localhost:9090 '].each do |valid_uri| - it 'returns correct server address' do - expect(described_class).to receive(:uri).and_return(valid_uri) - - expect(described_class.server_address).to eq('localhost:9090') - end - end - end - - context 'self.uri returns nil' do - it 'returns nil' do - expect(described_class).to receive(:uri).and_return(nil) - - expect(described_class.server_address).to be_nil - end - end - end - describe '.prometheus_enabled?' do it 'returns correct value' do expect(described_class.prometheus_enabled?).to eq(true) @@ -89,8 +69,8 @@ RSpec.describe Gitlab::Prometheus::Internal do context 'when prometheus setting is disabled in gitlab.yml' do let(:prometheus_settings) do { - enable: false, - listen_address: listen_address + enabled: false, + server_address: server_address } end @@ -110,9 +90,9 @@ RSpec.describe Gitlab::Prometheus::Internal do end end - describe '.listen_address' do + describe '.server_address' do it 'returns correct value' do - expect(described_class.listen_address).to eq(listen_address) + expect(described_class.server_address).to eq(server_address) end context 'when prometheus setting is not present in gitlab.yml' do @@ -121,7 +101,7 @@ RSpec.describe Gitlab::Prometheus::Internal do end it 'does not fail' do - expect(described_class.listen_address).to be_nil + expect(described_class.server_address).to be_nil end end end diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb index d72863b0103..5748e1e49e5 100644 --- a/spec/lib/gitlab/rack_attack_spec.rb +++ b/spec/lib/gitlab/rack_attack_spec.rb @@ -22,8 +22,7 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do stub_const("Rack::Attack", fake_rack_attack) stub_const("Rack::Attack::Request", fake_rack_attack_request) - # Expect rather than just allow, because this is actually fairly important functionality - expect(fake_rack_attack).to receive(:throttled_response_retry_after_header=).with(true) + allow(fake_rack_attack).to receive(:throttled_response=) allow(fake_rack_attack).to receive(:throttle) allow(fake_rack_attack).to receive(:track) allow(fake_rack_attack).to receive(:safelist) @@ -36,6 +35,12 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do expect(fake_rack_attack_request).to include(described_class::Request) end + it 'configures the throttle response' do + described_class.configure(fake_rack_attack) + + expect(fake_rack_attack).to have_received(:throttled_response=).with(an_instance_of(Proc)) + end + it 'configures the safelist' do described_class.configure(fake_rack_attack) @@ -93,4 +98,207 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do end end end + + describe '.throttled_response_headers' do + where(:matched, :match_data, :headers) do + [ + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 3600, + epoch_time: Time.utc(2021, 1, 5, 10, 29, 30).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '60', + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '1830' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 3600, + epoch_time: Time.utc(2021, 1, 5, 10, 59, 59).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '60', + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '1' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 3600, + epoch_time: Time.utc(2021, 1, 5, 10, 0, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '60', + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '3600' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 3600, + epoch_time: Time.utc(2021, 1, 5, 23, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '60', + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609891200', # Time.utc(2021, 1, 6, 0, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Wed, 06 Jan 2021 00:00:00 GMT', # Next day + 'Retry-After' => '1800' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 3400, + epoch_time: Time.utc(2021, 1, 5, 10, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '57', # 56.66 requests per minute + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '1800' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 3700, + epoch_time: Time.utc(2021, 1, 5, 10, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '62', # 61.66 requests per minute + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '1800' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 59, + epoch_time: Time.utc(2021, 1, 5, 10, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '1', # 0.9833 requests per minute + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '1800' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 1.hour, + limit: 61, + epoch_time: Time.utc(2021, 1, 5, 10, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '2', # 1.016 requests per minute + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609844400', # Time.utc(2021, 1, 5, 11, 0, 0).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 11:00:00 GMT', + 'Retry-After' => '1800' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 15.seconds, + limit: 10, + epoch_time: Time.utc(2021, 1, 5, 10, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '40', + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609842615', # Time.utc(2021, 1, 5, 10, 30, 15).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 10:30:15 GMT', + 'Retry-After' => '15' + } + ], + [ + 'throttle_unauthenticated', + { + discriminator: '127.0.0.1', + count: 3700, + period: 27.seconds, + limit: 10, + epoch_time: Time.utc(2021, 1, 5, 10, 30, 0).to_i + }, + { + 'RateLimit-Name' => 'throttle_unauthenticated', + 'RateLimit-Limit' => '23', + 'RateLimit-Observed' => '3700', + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => '1609842627', # Time.utc(2021, 1, 5, 10, 30, 27).to_i.to_s + 'RateLimit-ResetTime' => 'Tue, 05 Jan 2021 10:30:27 GMT', + 'Retry-After' => '27' + } + ] + ] + end + + with_them do + it 'generates accurate throttled headers' do + expect(described_class.throttled_response_headers(matched, match_data)).to eql(headers) + end + end + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 57be9e93af2..c437b6bcceb 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -342,17 +342,36 @@ RSpec.describe Gitlab::SearchResults do expect(results.limited_issues_count).to eq 4 end - it 'lists all issues for admin' do - results = described_class.new(admin, query, limit_projects) - issues = results.objects('issues') + context 'with admin user' do + context 'when admin mode enabled', :enable_admin_mode do + it 'lists all issues' do + results = described_class.new(admin, query, limit_projects) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.limited_issues_count).to eq 5 + end + end - expect(issues).to include issue - expect(issues).to include security_issue_1 - expect(issues).to include security_issue_2 - expect(issues).to include security_issue_3 - expect(issues).to include security_issue_4 - expect(issues).not_to include security_issue_5 - expect(results.limited_issues_count).to eq 5 + context 'when admin mode disabled' do + it 'does not list confidential issues' do + results = described_class.new(admin, query, limit_projects) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(issues).not_to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.limited_issues_count).to eq 1 + end + end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb index 3d9ffb11ae2..3ba08455d01 100644 --- a/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::SidekiqMiddleware::AdminMode::Client, :do_not_mock_admin_mode, :request_store do +RSpec.describe Gitlab::SidekiqMiddleware::AdminMode::Client, :request_store do include AdminModeHelper let(:worker) do diff --git a/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb index 20f1e88bcf4..e8322b11875 100644 --- a/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::SidekiqMiddleware::AdminMode::Server, :do_not_mock_admin_mode, :request_store do +RSpec.describe Gitlab::SidekiqMiddleware::AdminMode::Server, :request_store do include AdminModeHelper let(:worker) do diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb index 4ee9569a0cf..b632fc8bad2 100644 --- a/spec/lib/gitlab/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb @@ -100,7 +100,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do "subject", "body" ], - "_aj_symbol_keys" => ["args"] + ActiveJob::Arguments.const_get('RUBY2_KEYWORDS_KEY', false) => ["args"] } ], "executions" => 0, diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb index df949154d4c..a4d8e3957cf 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb @@ -3,15 +3,20 @@ require 'spec_helper' RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove do - let_it_be(:admin) { create(:admin) } + let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project) } let_it_be(:other_project) { create(:project) } let_it_be(:old_issue, reload: true) { create(:issue, project: project) } - let(:new_issue) { Issues::MoveService.new(project, admin).execute(old_issue, other_project) } + let(:new_issue) { Issues::MoveService.new(project, user).execute(old_issue, other_project) } let(:attachment) { subject[:attachments].first } subject { described_class.new(new_issue).present(old_issue) } + before do + project.add_developer(user) + other_project.add_developer(user) + end + it { is_expected.to be_a(Hash) } it 'shows the new issue' do diff --git a/spec/lib/gitlab/template/gitlab_ci_syntax_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_syntax_yml_template_spec.rb new file mode 100644 index 00000000000..d1024019a9f --- /dev/null +++ b/spec/lib/gitlab/template/gitlab_ci_syntax_yml_template_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Template::GitlabCiSyntaxYmlTemplate do + subject { described_class } + + describe '#content' do + it 'loads the full file' do + template = subject.new(Rails.root.join('lib/gitlab/ci/syntax_templates/Artifacts example.gitlab-ci.yml')) + + expect(template.content).to start_with('#') + end + end + + it_behaves_like 'file template shared examples', 'Artifacts example', '.gitlab-ci.yml' +end diff --git a/spec/lib/gitlab/throttle_spec.rb b/spec/lib/gitlab/throttle_spec.rb index 7462b2e1c38..50d723193ac 100644 --- a/spec/lib/gitlab/throttle_spec.rb +++ b/spec/lib/gitlab/throttle_spec.rb @@ -30,4 +30,32 @@ RSpec.describe Gitlab::Throttle do end end end + + describe '.rate_limiting_response_text' do + subject { described_class.rate_limiting_response_text } + + context 'when the setting is not present' do + before do + stub_application_setting(rate_limiting_response_text: '') + end + + it 'returns the default value with a trailing newline' do + expect(subject).to eq(described_class::DEFAULT_RATE_LIMITING_RESPONSE_TEXT + "\n") + end + end + + context 'when the setting is present' do + let(:response_text) do + 'Rate limit exceeded; see https://docs.gitlab.com/ee/user/gitlab_com/#gitlabcom-specific-rate-limits for more details' + end + + before do + stub_application_setting(rate_limiting_response_text: response_text) + end + + it 'returns the default value with a trailing newline' do + expect(subject).to eq(response_text + "\n") + end + end + end end diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb new file mode 100644 index 00000000000..acf7aeb303a --- /dev/null +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Tracking::StandardContext do + let_it_be(:project) { create(:project) } + let_it_be(:namespace) { create(:namespace) } + + let(:snowplow_context) { subject.to_context } + + describe '#to_context' do + context 'with no arguments' do + it 'creates a Snowplow context with no data' do + snowplow_context.to_json[:data].each do |_, v| + expect(v).to be_nil + end + end + end + + context 'with extra data' do + subject { described_class.new(foo: 'bar') } + + it 'creates a Snowplow context with the given data' do + expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar') + end + end + + context 'with namespace' do + subject { described_class.new(namespace: namespace) } + + it 'creates a Snowplow context using the given data' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id) + expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil + end + end + + context 'with project' do + subject { described_class.new(project: project) } + + it 'creates a Snowplow context using the given data' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(project.namespace.id) + expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id) + end + end + + context 'with project and namespace' do + subject { described_class.new(namespace: namespace, project: project) } + + it 'creates a Snowplow context using the given data' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id) + expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id) + end + end + end +end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index 57882de0974..8f1fd49f4c5 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -41,21 +41,42 @@ RSpec.describe Gitlab::Tracking do allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event) end - it 'delegates to snowplow destination' do - expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow) - .to receive(:event) - .with('category', 'action', label: 'label', property: 'property', value: 1.5, context: nil) + shared_examples 'delegates to destination' do |klass| + context 'with standard context' do + it "delegates to #{klass} destination" do + expect_any_instance_of(klass).to receive(:event) do |_, category, action, args| + expect(category).to eq('category') + expect(action).to eq('action') + expect(args[:label]).to eq('label') + expect(args[:property]).to eq('property') + expect(args[:value]).to eq(1.5) + expect(args[:context].length).to eq(1) + expect(args[:context].first.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL) + expect(args[:context].first.to_json[:data]).to include(foo: 'bar') + end - described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5) - end + described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5, + standard_context: Gitlab::Tracking::StandardContext.new(foo: 'bar')) + end + end - it 'delegates to ProductAnalytics destination' do - expect_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics) - .to receive(:event) - .with('category', 'action', label: 'label', property: 'property', value: 1.5, context: nil) + context 'without standard context' do + it "delegates to #{klass} destination" do + expect_any_instance_of(klass).to receive(:event) do |_, category, action, args| + expect(category).to eq('category') + expect(action).to eq('action') + expect(args[:label]).to eq('label') + expect(args[:property]).to eq('property') + expect(args[:value]).to eq(1.5) + end - described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5) + described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5) + end + end end + + include_examples 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow + include_examples 'delegates to destination', Gitlab::Tracking::Destinations::ProductAnalytics end describe '.self_describing_event' do diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index c892f1f0410..6d055fe3643 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -18,6 +18,8 @@ RSpec.describe Gitlab::UrlBuilder do where(:factory, :path_generator) do :project | ->(project) { "/#{project.full_path}" } + :board | ->(board) { "/#{board.project.full_path}/-/boards/#{board.id}" } + :group_board | ->(board) { "/groups/#{board.group.full_path}/-/boards/#{board.id}" } :commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" } :issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" } :merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" } diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb new file mode 100644 index 00000000000..e101f837324 --- /dev/null +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::MetricDefinition do + let(:attributes) do + { + name: 'uuid', + description: 'GitLab instance unique identifier', + value_type: 'string', + product_category: 'collection', + stage: 'growth', + status: 'data_available', + default_generation: 'generation_1', + full_path: { + generation_1: 'uuid', + generation_2: 'license.uuid' + }, + group: 'group::product analytics', + time_frame: 'none', + data_source: 'database', + distribution: %w(ee ce), + tier: %w(free starter premium ultimate bronze silver gold) + } + end + + let(:path) { File.join('metrics', 'uuid.yml') } + let(:definition) { described_class.new(path, attributes) } + let(:yaml_content) { attributes.deep_stringify_keys.to_yaml } + + it 'has all definitons valid' do + expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError) + end + + describe '#key' do + subject { definition.key } + + it 'returns a symbol from name' do + is_expected.to eq('uuid') + end + end + + describe '#validate' do + using RSpec::Parameterized::TableSyntax + + where(:attribute, :value) do + :name | nil + :description | nil + :value_type | nil + :value_type | 'test' + :status | nil + :default_generation | nil + :group | nil + :time_frame | nil + :time_frame | '29d' + :data_source | 'other' + :data_source | nil + :distribution | nil + :distribution | 'test' + :tier | %w(test ee) + end + + with_them do + before do + attributes[attribute] = value + end + + it 'raise exception' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + + described_class.new(path, attributes).validate! + end + end + end + + describe '.load_all!' do + let(:metric1) { Dir.mktmpdir('metric1') } + let(:metric2) { Dir.mktmpdir('metric2') } + let(:definitions) { {} } + + before do + allow(described_class).to receive(:paths).and_return( + [ + File.join(metric1, '**', '*.yml'), + File.join(metric2, '**', '*.yml') + ] + ) + end + + subject { described_class.send(:load_all!) } + + it 'has empty list when there are no definition files' do + is_expected.to be_empty + end + + it 'has one metric when there is one file' do + write_metric(metric1, path, yaml_content) + + is_expected.to be_one + end + + it 'when the same meric is defined multiple times raises exception' do + write_metric(metric1, path, yaml_content) + write_metric(metric2, path, yaml_content) + + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + + subject + end + + after do + FileUtils.rm_rf(metric1) + FileUtils.rm_rf(metric2) + end + + def write_metric(metric, path, content) + path = File.join(metric, path) + dir = File.dirname(path) + FileUtils.mkdir_p(dir) + File.write(path, content) + end + end +end diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb new file mode 100644 index 00000000000..40671d980d6 --- /dev/null +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metric do + describe '#definition' do + it 'returns generation_1 metric definiton' do + expect(described_class.new(default_generation_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition) + end + end + + describe '#unflatten_default_path' do + using RSpec::Parameterized::TableSyntax + + where(:default_generation_path, :value, :expected_hash) do + 'uuid' | nil | { uuid: nil } + 'uuid' | '1111' | { uuid: '1111' } + 'counts.issues' | nil | { counts: { issues: nil } } + 'counts.issues' | 100 | { counts: { issues: 100 } } + 'usage_activity_by_stage.verify.ci_builds' | 100 | { usage_activity_by_stage: { verify: { ci_builds: 100 } } } + end + + with_them do + subject { described_class.new(default_generation_path: default_generation_path, value: value).unflatten_default_path } + + it { is_expected.to eq(expected_hash) } + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb new file mode 100644 index 00000000000..ba7bfe47bc9 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do + let(:project_id) { 1 } + + describe '.track_unique_project_event' do + described_class::TEMPLATE_TO_EVENT.keys.each do |template| + context "when given template #{template}" do + it_behaves_like 'tracking unique hll events', :usage_data_track_ci_templates_unique_projects do + subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) } + + let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" } + let(:expected_type) { instance_of(Integer) } + end + end + end + + it 'does not track templates outside of TEMPLATE_TO_EVENT' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to( + receive(:track_event) + ) + Dir.glob(File.join('lib', 'gitlab', 'ci', 'templates', '**'), base: Rails.root) do |template| + next if described_class::TEMPLATE_TO_EVENT.key?(template) + + described_class.track_unique_project_event(project_id: 1, template: template) + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb deleted file mode 100644 index d018100b041..00000000000 --- a/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::UsageDataCounters::GuestPackageEventCounter, :clean_gitlab_redis_shared_state do - shared_examples_for 'usage counter with totals' do |counter| - it 'increments counter and returns total count' do - expect(described_class.read(counter)).to eq(0) - - 2.times { described_class.count(counter) } - - expect(described_class.read(counter)).to eq(2) - end - end - - it 'includes the right events' do - expect(described_class::KNOWN_EVENTS.size).to eq 33 - end - - described_class::KNOWN_EVENTS.each do |event| - it_behaves_like 'usage counter with totals', event - end - - describe '.fetch_supported_event' do - subject { described_class.fetch_supported_event(event_name) } - - let(:event_name) { 'package_guest_i_package_composer_guest_push' } - - it { is_expected.to eq 'i_package_composer_guest_push' } - end -end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index b6a60c09d3d..b8eddc0ca7f 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -24,6 +24,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s describe '.categories' do it 'gets all unique category names' do expect(described_class.categories).to contain_exactly( + 'deploy_token_packages', + 'user_packages', 'compliance', 'analytics', 'ide_edit', @@ -34,18 +36,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'testing', 'issues_edit', 'ci_secrets_management', - 'maven_packages', - 'npm_packages', - 'conan_packages', - 'nuget_packages', - 'pypi_packages', - 'composer_packages', - 'generic_packages', - 'golang_packages', - 'debian_packages', - 'container_packages', - 'tag_packages', - 'snippets' + 'snippets', + 'code_review', + 'terraform', + 'ci_templates' ) end end @@ -95,7 +89,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it 'does not track the event' do stub_application_setting(usage_ping_enabled: false) - described_class.track_event(entity1, weekly_event, Date.current) + described_class.track_event(weekly_event, values: entity1, time: Date.current) expect(Gitlab::Redis::HLL).not_to receive(:add) end @@ -109,20 +103,27 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it 'tracks event when using symbol' do expect(Gitlab::Redis::HLL).to receive(:add) - described_class.track_event(entity1, :g_analytics_contribution) + described_class.track_event(:g_analytics_contribution, values: entity1) + end + + it 'tracks events with multiple values' do + values = [entity1, entity2] + expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/, value: values, expiry: 84.days) + + described_class.track_event(:g_analytics_contribution, values: values) end it "raise error if metrics don't have same aggregation" do - expect { described_class.track_event(entity1, different_aggregation, Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation) + expect { described_class.track_event(different_aggregation, values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation) end it 'raise error if metrics of unknown aggregation' do - expect { described_class.track_event(entity1, 'unknown', Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) + expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) end context 'for weekly events' do it 'sets the keys in Redis to expire automatically after the given expiry time' do - described_class.track_event(entity1, "g_analytics_contribution") + described_class.track_event("g_analytics_contribution", values: entity1) Gitlab::Redis::SharedState.with do |redis| keys = redis.scan_each(match: "g_{analytics}_contribution-*").to_a @@ -135,7 +136,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end it 'sets the keys in Redis to expire automatically after 6 weeks by default' do - described_class.track_event(entity1, "g_compliance_dashboard") + described_class.track_event("g_compliance_dashboard", values: entity1) Gitlab::Redis::SharedState.with do |redis| keys = redis.scan_each(match: "g_{compliance}_dashboard-*").to_a @@ -150,7 +151,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s context 'for daily events' do it 'sets the keys in Redis to expire after the given expiry time' do - described_class.track_event(entity1, "g_analytics_search") + described_class.track_event("g_analytics_search", values: entity1) Gitlab::Redis::SharedState.with do |redis| keys = redis.scan_each(match: "*-g_{analytics}_search").to_a @@ -163,7 +164,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end it 'sets the keys in Redis to expire after 29 days by default' do - described_class.track_event(entity1, "no_slot") + described_class.track_event("no_slot", values: entity1) Gitlab::Redis::SharedState.with do |redis| keys = redis.scan_each(match: "*-{no_slot}").to_a @@ -180,12 +181,19 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s describe '.track_event_in_context' do context 'with valid contex' do - it 'increments conext event counte' do + it 'increments context event counter' do expect(Gitlab::Redis::HLL).to receive(:add) do |kwargs| expect(kwargs[:key]).to match(/^#{default_context}\_.*/) end - described_class.track_event_in_context(entity1, context_event, default_context) + described_class.track_event_in_context(context_event, values: entity1, context: default_context) + end + + it 'tracks events with multiple values' do + values = [entity1, entity2] + expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/, value: values, expiry: 84.days) + + described_class.track_event_in_context(:g_analytics_contribution, values: values, context: default_context) end end @@ -193,7 +201,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it 'does not increment a counter' do expect(Gitlab::Redis::HLL).not_to receive(:add) - described_class.track_event_in_context(entity1, context_event, '') + described_class.track_event_in_context(context_event, values: entity1, context: '') end end @@ -201,7 +209,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it 'does not increment a counter' do expect(Gitlab::Redis::HLL).not_to receive(:add) - described_class.track_event_in_context(entity1, context_event, invalid_context) + described_class.track_event_in_context(context_event, values: entity1, context: invalid_context) end end end @@ -209,35 +217,35 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s describe '.unique_events' do before do # events in current week, should not be counted as week is not complete - described_class.track_event(entity1, weekly_event, Date.current) - described_class.track_event(entity2, weekly_event, Date.current) + described_class.track_event(weekly_event, values: entity1, time: Date.current) + described_class.track_event(weekly_event, values: entity2, time: Date.current) # Events last week - described_class.track_event(entity1, weekly_event, 2.days.ago) - described_class.track_event(entity1, weekly_event, 2.days.ago) - described_class.track_event(entity1, no_slot, 2.days.ago) + described_class.track_event(weekly_event, values: entity1, time: 2.days.ago) + described_class.track_event(weekly_event, values: entity1, time: 2.days.ago) + described_class.track_event(no_slot, values: entity1, time: 2.days.ago) # Events 2 weeks ago - described_class.track_event(entity1, weekly_event, 2.weeks.ago) + described_class.track_event(weekly_event, values: entity1, time: 2.weeks.ago) # Events 4 weeks ago - described_class.track_event(entity3, weekly_event, 4.weeks.ago) - described_class.track_event(entity4, weekly_event, 29.days.ago) + described_class.track_event(weekly_event, values: entity3, time: 4.weeks.ago) + described_class.track_event(weekly_event, values: entity4, time: 29.days.ago) # events in current day should be counted in daily aggregation - described_class.track_event(entity1, daily_event, Date.current) - described_class.track_event(entity2, daily_event, Date.current) + described_class.track_event(daily_event, values: entity1, time: Date.current) + described_class.track_event(daily_event, values: entity2, time: Date.current) # Events last week - described_class.track_event(entity1, daily_event, 2.days.ago) - described_class.track_event(entity1, daily_event, 2.days.ago) + described_class.track_event(daily_event, values: entity1, time: 2.days.ago) + described_class.track_event(daily_event, values: entity1, time: 2.days.ago) # Events 2 weeks ago - described_class.track_event(entity1, daily_event, 14.days.ago) + described_class.track_event(daily_event, values: entity1, time: 14.days.ago) # Events 4 weeks ago - described_class.track_event(entity3, daily_event, 28.days.ago) - described_class.track_event(entity4, daily_event, 29.days.ago) + described_class.track_event(daily_event, values: entity3, time: 28.days.ago) + described_class.track_event(daily_event, values: entity4, time: 29.days.ago) end it 'raise error if metrics are not in the same slot' do @@ -345,10 +353,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s allow(described_class).to receive(:known_events).and_return(known_events) allow(described_class).to receive(:categories).and_return(%w(category1 category2)) - described_class.track_event_in_context([entity1, entity3], 'event_name_1', default_context, 2.days.ago) - described_class.track_event_in_context(entity3, 'event_name_1', default_context, 2.days.ago) - described_class.track_event_in_context(entity3, 'event_name_1', invalid_context, 2.days.ago) - described_class.track_event_in_context([entity1, entity2], 'event_name_2', '', 2.weeks.ago) + described_class.track_event_in_context('event_name_1', values: [entity1, entity3], context: default_context, time: 2.days.ago) + described_class.track_event_in_context('event_name_1', values: entity3, context: default_context, time: 2.days.ago) + described_class.track_event_in_context('event_name_1', values: entity3, context: invalid_context, time: 2.days.ago) + described_class.track_event_in_context('event_name_2', values: [entity1, entity2], context: '', time: 2.weeks.ago) end subject(:unique_events) { described_class.unique_events(event_names: event_names, start_date: 4.weeks.ago, end_date: Date.current, context: context) } @@ -386,13 +394,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s allow(described_class).to receive(:known_events).and_return(known_events) allow(described_class).to receive(:categories).and_return(%w(category1 category2)) - described_class.track_event(entity1, 'event1_slot', 2.days.ago) - described_class.track_event(entity2, 'event2_slot', 2.days.ago) - described_class.track_event(entity3, 'event2_slot', 2.weeks.ago) + described_class.track_event('event1_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity2, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity3, time: 2.weeks.ago) # events in different slots - described_class.track_event(entity2, 'event3', 2.days.ago) - described_class.track_event(entity2, 'event4', 2.days.ago) + described_class.track_event('event3', values: entity2, time: 2.days.ago) + described_class.track_event('event4', values: entity2, time: 2.days.ago) end it 'returns the number of unique events for all known events' do @@ -516,23 +524,23 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s subject(:aggregated_metrics_data) { described_class.aggregated_metrics_weekly_data } before do - described_class.track_event(entity1, 'event1_slot', 2.days.ago) - described_class.track_event(entity2, 'event1_slot', 2.days.ago) - described_class.track_event(entity3, 'event1_slot', 2.days.ago) - described_class.track_event(entity1, 'event2_slot', 2.days.ago) - described_class.track_event(entity2, 'event2_slot', 3.days.ago) - described_class.track_event(entity3, 'event2_slot', 3.days.ago) - described_class.track_event(entity1, 'event3_slot', 3.days.ago) - described_class.track_event(entity2, 'event3_slot', 3.days.ago) - described_class.track_event(entity2, 'event5_slot', 3.days.ago) + described_class.track_event('event1_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event1_slot', values: entity2, time: 2.days.ago) + described_class.track_event('event1_slot', values: entity3, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity2, time: 3.days.ago) + described_class.track_event('event2_slot', values: entity3, time: 3.days.ago) + described_class.track_event('event3_slot', values: entity1, time: 3.days.ago) + described_class.track_event('event3_slot', values: entity2, time: 3.days.ago) + described_class.track_event('event5_slot', values: entity2, time: 3.days.ago) # events out of time scope - described_class.track_event(entity3, 'event2_slot', 8.days.ago) + described_class.track_event('event2_slot', values: entity3, time: 8.days.ago) # events in different slots - described_class.track_event(entity1, 'event4', 2.days.ago) - described_class.track_event(entity2, 'event4', 2.days.ago) - described_class.track_event(entity4, 'event4', 2.days.ago) + described_class.track_event('event4', values: entity1, time: 2.days.ago) + described_class.track_event('event4', values: entity2, time: 2.days.ago) + described_class.track_event('event4', values: entity4, time: 2.days.ago) end it_behaves_like 'aggregated_metrics_data' @@ -543,23 +551,23 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it_behaves_like 'aggregated_metrics_data' do before do - described_class.track_event(entity1, 'event1_slot', 2.days.ago) - described_class.track_event(entity2, 'event1_slot', 2.days.ago) - described_class.track_event(entity3, 'event1_slot', 2.days.ago) - described_class.track_event(entity1, 'event2_slot', 2.days.ago) - described_class.track_event(entity2, 'event2_slot', 3.days.ago) - described_class.track_event(entity3, 'event2_slot', 3.days.ago) - described_class.track_event(entity1, 'event3_slot', 3.days.ago) - described_class.track_event(entity2, 'event3_slot', 10.days.ago) - described_class.track_event(entity2, 'event5_slot', 4.weeks.ago.advance(days: 1)) + described_class.track_event('event1_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event1_slot', values: entity2, time: 2.days.ago) + described_class.track_event('event1_slot', values: entity3, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity2, time: 3.days.ago) + described_class.track_event('event2_slot', values: entity3, time: 3.days.ago) + described_class.track_event('event3_slot', values: entity1, time: 3.days.ago) + described_class.track_event('event3_slot', values: entity2, time: 10.days.ago) + described_class.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1)) # events out of time scope - described_class.track_event(entity1, 'event5_slot', 4.weeks.ago.advance(days: -1)) + described_class.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1)) # events in different slots - described_class.track_event(entity1, 'event4', 2.days.ago) - described_class.track_event(entity2, 'event4', 2.days.ago) - described_class.track_event(entity4, 'event4', 2.days.ago) + described_class.track_event('event4', values: entity1, time: 2.days.ago) + described_class.track_event('event4', values: entity2, time: 2.days.ago) + described_class.track_event('event4', values: entity4, time: 2.days.ago) end end diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb new file mode 100644 index 00000000000..c7b208cfb31 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :clean_gitlab_redis_shared_state do + let(:merge_request) { build(:merge_request, id: 1) } + let(:user) { build(:user, id: 1) } + let(:note) { build(:note, author: user) } + + shared_examples_for 'a tracked merge request unique event' do + specify do + expect { 3.times { subject } } + .to change { + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( + event_names: action, + start_date: 2.weeks.ago, + end_date: 2.weeks.from_now + ) + } + .by(1) + end + end + + describe '.track_mr_diffs_action' do + subject { described_class.track_mr_diffs_action(merge_request: merge_request) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_DIFFS_ACTION } + end + end + + describe '.track_mr_diffs_single_file_action' do + subject { described_class.track_mr_diffs_single_file_action(merge_request: merge_request, user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_DIFFS_SINGLE_FILE_ACTION } + end + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_DIFFS_USER_SINGLE_FILE_ACTION } + end + end + + describe '.track_create_mr_action' do + subject { described_class.track_create_mr_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_CREATE_ACTION } + end + end + + describe '.track_close_mr_action' do + subject { described_class.track_close_mr_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_CLOSE_ACTION } + end + end + + describe '.track_merge_mr_action' do + subject { described_class.track_merge_mr_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_MERGE_ACTION } + end + end + + describe '.track_reopen_mr_action' do + subject { described_class.track_reopen_mr_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_REOPEN_ACTION } + end + end + + describe '.track_create_comment_action' do + subject { described_class.track_create_comment_action(note: note) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_CREATE_COMMENT_ACTION } + end + + context 'when the note is multiline diff note' do + let(:note) { build(:diff_note_on_merge_request, author: user) } + + before do + allow(note).to receive(:multiline?).and_return(true) + end + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_CREATE_MULTILINE_COMMENT_ACTION } + end + end + end + + describe '.track_edit_comment_action' do + subject { described_class.track_edit_comment_action(note: note) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_EDIT_COMMENT_ACTION } + end + + context 'when the note is multiline diff note' do + let(:note) { build(:diff_note_on_merge_request, author: user) } + + before do + allow(note).to receive(:multiline?).and_return(true) + end + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_EDIT_MULTILINE_COMMENT_ACTION } + end + end + end + + describe '.track_remove_comment_action' do + subject { described_class.track_remove_comment_action(note: note) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_REMOVE_COMMENT_ACTION } + end + + context 'when the note is multiline diff note' do + let(:note) { build(:diff_note_on_merge_request, author: user) } + + before do + allow(note).to receive(:multiline?).and_return(true) + end + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_REMOVE_MULTILINE_COMMENT_ACTION } + end + end + end + + describe '.track_create_review_note_action' do + subject { described_class.track_create_review_note_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_CREATE_REVIEW_NOTE_ACTION } + end + end + + describe '.track_publish_review_action' do + subject { described_class.track_publish_review_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_PUBLISH_REVIEW_ACTION } + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb new file mode 100644 index 00000000000..7b5efb11034 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_redis_shared_state do + shared_examples_for 'usage counter with totals' do |counter| + it 'increments counter and returns total count' do + expect(described_class.read(counter)).to eq(0) + + 2.times { described_class.count(counter) } + + expect(described_class.read(counter)).to eq(2) + end + end + + it 'includes the right events' do + expect(described_class::KNOWN_EVENTS.size).to eq 45 + end + + described_class::KNOWN_EVENTS.each do |event| + it_behaves_like 'usage counter with totals', event + end + + describe '.fetch_supported_event' do + subject { described_class.fetch_supported_event(event_name) } + + let(:event_name) { 'package_events_i_package_composer_push_package' } + + it { is_expected.to eq 'i_package_composer_push_package' } + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 4d12bb6bd8c..fd02521622c 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -680,7 +680,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do it { is_expected.to include(:kubernetes_agent_gitops_sync) } it { is_expected.to include(:static_site_editor_views) } - it { is_expected.to include(:package_guest_i_package_composer_guest_pull) } + it { is_expected.to include(:package_events_i_package_pull_package) } + it { is_expected.to include(:package_events_i_package_delete_package_by_user) } + it { is_expected.to include(:package_events_i_package_conan_push_package) } end describe '.usage_data_counters' do @@ -1260,7 +1262,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts snippets] } + let(:ineligible_total_categories) { %w[source_code ci_secrets_management incident_management_alerts snippets terraform] } it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 748a8336a25..97fff030906 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -45,10 +45,20 @@ RSpec.describe Gitlab::UserAccess do let(:empty_project) { create(:project_empty_repo) } let(:project_access) { described_class.new(user, container: empty_project) } - it 'returns true for admins' do - user.update!(admin: true) + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns true for admins' do + user.update!(admin: true) - expect(access.can_push_to_branch?('master')).to be_truthy + expect(access.can_push_to_branch?('master')).to be_truthy + end + end + + context 'when admin mode is disabled' do + it 'returns false for admins' do + user.update!(admin: true) + + expect(access.can_push_to_branch?('master')).to be_falsey + end end it 'returns true if user is maintainer' do @@ -85,10 +95,20 @@ RSpec.describe Gitlab::UserAccess do let(:branch) { create :protected_branch, project: project, name: "test" } let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } - it 'returns true for admins' do - user.update!(admin: true) + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns true for admins' do + user.update!(admin: true) - expect(access.can_push_to_branch?(branch.name)).to be_truthy + expect(access.can_push_to_branch?(branch.name)).to be_truthy + end + end + + context 'when admin mode is disabled' do + it 'returns false for admins' do + user.update!(admin: true) + + expect(access.can_push_to_branch?(branch.name)).to be_falsey + end end it 'returns true if user is a maintainer' do diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index 521d6584a20..dfc381d0ef2 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -38,32 +38,116 @@ RSpec.describe Gitlab::Utils::UsageData do end describe '#estimate_batch_distinct_count' do + let(:error_rate) { Gitlab::Database::PostgresHll::BatchDistinctCounter::ERROR_RATE } # HyperLogLog is a probabilistic algorithm, which provides estimated data, with given error margin let(:relation) { double(:relation) } + before do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + end + it 'delegates counting to counter class instance' do + buckets = instance_double(Gitlab::Database::PostgresHll::Buckets) + expect_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter, relation, 'column') do |instance| - expect(instance).to receive(:estimate_distinct_count) + expect(instance).to receive(:execute) .with(batch_size: nil, start: nil, finish: nil) - .and_return(5) + .and_return(buckets) end + expect(buckets).to receive(:estimated_distinct_count).and_return(5) expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5) end - it 'returns default fallback value when counting fails due to database error' do - stub_const("Gitlab::Utils::UsageData::FALLBACK", 15) - allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(ActiveRecord::StatementInvalid.new('')) + context 'quasi integration test for different counting parameters' do + # HyperLogLog http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf algorithm + # used in estimate_batch_distinct_count produce probabilistic + # estimations of unique values present in dataset, because of that its results + # are always off by some small factor from real value. However for given + # dataset it provide consistent and deterministic result. In the following context + # analyzed sets consist of values: + # build_needs set: ['1', '2', '3', '4', '5'] + # ci_build set ['a', 'b'] + # with them, current implementation is expected to consistently report + # 5.217656147118495 and 2.0809220082170614 values + # This test suite is expected to assure, that HyperLogLog implementation + # behaves consistently between changes made to other parts of codebase. + # In case of fine tuning or changes to HyperLogLog algorithm implementation + # one should run in depth analysis of accuracy with supplementary rake tasks + # currently under implementation at https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51118 + # and adjust used values in this context accordingly. + let_it_be(:build) { create(:ci_build, name: 'a') } + let_it_be(:another_build) { create(:ci_build, name: 'b') } + + let(:model) { Ci::BuildNeed } + let(:column) { :name } + let(:build_needs_estimated_cardinality) { 5.217656147118495 } + let(:ci_builds_estimated_cardinality) { 2.0809220082170614 } + + context 'different counting parameters' do + before_all do + 1.upto(3) { |i| create(:ci_build_need, name: i, build: build) } + 4.upto(5) { |i| create(:ci_build_need, name: i, build: another_build) } + end + + it 'counts with symbol passed in column argument' do + expect(described_class.estimate_batch_distinct_count(model, column)).to eq(build_needs_estimated_cardinality) + end + + it 'counts with string passed in column argument' do + expect(described_class.estimate_batch_distinct_count(model, column.to_s)).to eq(build_needs_estimated_cardinality) + end + + it 'counts with table.column passed in column argument' do + expect(described_class.estimate_batch_distinct_count(model, "#{model.table_name}.#{column}")).to eq(build_needs_estimated_cardinality) + end + + it 'counts with Arel passed in column argument' do + expect(described_class.estimate_batch_distinct_count(model, model.arel_table[column])).to eq(build_needs_estimated_cardinality) + end + + it 'counts over joined relations' do + expect(described_class.estimate_batch_distinct_count(model.joins(:build), "ci_builds.name")).to eq(ci_builds_estimated_cardinality) + end - expect(described_class.estimate_batch_distinct_count(relation)).to eq(15) + it 'counts with :column field with batch_size of 50K' do + expect(described_class.estimate_batch_distinct_count(model, column, batch_size: 50_000)).to eq(build_needs_estimated_cardinality) + end + + it 'counts with different number of batches and aggregates total result' do + stub_const('Gitlab::Database::PostgresHll::BatchDistinctCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + [1, 2, 4, 5, 6].each { |i| expect(described_class.estimate_batch_distinct_count(model, column, batch_size: i)).to eq(build_needs_estimated_cardinality) } + end + + it 'counts with a start and finish' do + expect(described_class.estimate_batch_distinct_count(model, column, start: model.minimum(:id), finish: model.maximum(:id))).to eq(build_needs_estimated_cardinality) + end + end end - it 'logs error and returns DISTRIBUTED_HLL_FALLBACK value when counting raises any error', :aggregate_failures do - error = StandardError.new('') - stub_const("Gitlab::Utils::UsageData::DISTRIBUTED_HLL_FALLBACK", 15) - allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(error) + describe 'error handling' do + before do + stub_const("Gitlab::Utils::UsageData::FALLBACK", 3) + stub_const("Gitlab::Utils::UsageData::DISTRIBUTED_HLL_FALLBACK", 4) + end + + it 'returns fallback if counter raises WRONG_CONFIGURATION_ERROR' do + expect(described_class.estimate_batch_distinct_count(relation, 'id', start: 1, finish: 0)).to eq 3 + end + + it 'returns default fallback value when counting fails due to database error' do + allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(ActiveRecord::StatementInvalid.new('')) - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) - expect(described_class.estimate_batch_distinct_count(relation)).to eq(15) + expect(described_class.estimate_batch_distinct_count(relation)).to eq(3) + end + + it 'logs error and returns DISTRIBUTED_HLL_FALLBACK value when counting raises any error', :aggregate_failures do + error = StandardError.new('') + allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(error) + + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) + expect(described_class.estimate_batch_distinct_count(relation)).to eq(4) + end end end @@ -193,7 +277,7 @@ RSpec.describe Gitlab::Utils::UsageData do context 'when Prometheus server address is available from settings' do before do expect(Gitlab::Prometheus::Internal).to receive(:prometheus_enabled?).and_return(true) - expect(Gitlab::Prometheus::Internal).to receive(:server_address).and_return('prom:9090') + expect(Gitlab::Prometheus::Internal).to receive(:uri).and_return('http://prom:9090') end it_behaves_like 'try to query Prometheus with given address' @@ -256,7 +340,7 @@ RSpec.describe Gitlab::Utils::UsageData do end it 'tracks redis hll event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(value, event_name) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(event_name, values: value) described_class.track_usage_event(event_name, value) end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 36257a0605b..1052d4cbacc 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -392,6 +392,23 @@ RSpec.describe Gitlab::Utils do end end + describe ".safe_downcase!" do + using RSpec::Parameterized::TableSyntax + + where(:str, :result) do + "test".freeze | "test" + "Test".freeze | "test" + "test" | "test" + "Test" | "test" + end + + with_them do + it "downcases the string" do + expect(described_class.safe_downcase!(str)).to eq(result) + end + end + end + describe '.parse_url' do it 'returns Addressable::URI object' do expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI) diff --git a/spec/lib/gitlab/uuid_spec.rb b/spec/lib/gitlab/uuid_spec.rb index a2e28f5a24d..44c1d30fce0 100644 --- a/spec/lib/gitlab/uuid_spec.rb +++ b/spec/lib/gitlab/uuid_spec.rb @@ -49,4 +49,23 @@ RSpec.describe Gitlab::UUID do it { is_expected.to eq(production_proper_uuid) } end end + + describe 'v5?' do + using RSpec::Parameterized::TableSyntax + + where(:test_string, :is_uuid_v5) do + 'not even a uuid' | false + 'this-seems-like-a-uuid' | false + 'thislook-more-5lik-eava-liduuidbutno' | false + '9f470438-db0f-37b7-9ca9-1d47104c339a' | false + '9f470438-db0f-47b7-9ca9-1d47104c339a' | false + '9f470438-db0f-57b7-9ca9-1d47104c339a' | true + end + + with_them do + subject { described_class.v5?(test_string) } + + it { is_expected.to be(is_uuid_v5) } + end + end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 2ac343cd1e7..63c31c82d59 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -22,13 +22,25 @@ RSpec.describe Gitlab::VisibilityLevel do end describe '.levels_for_user' do - it 'returns all levels for an admin' do - user = build(:user, :admin) + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns all levels for an admin' do + user = build(:user, :admin) + + expect(described_class.levels_for_user(user)) + .to eq([Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC]) + end + end - expect(described_class.levels_for_user(user)) - .to eq([Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC]) + context 'when admin mode is disabled' do + it 'returns INTERNAL and PUBLIC for an admin' do + user = build(:user, :admin) + + expect(described_class.levels_for_user(user)) + .to eq([Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC]) + end end it 'returns INTERNAL and PUBLIC for internal users' do @@ -119,28 +131,4 @@ RSpec.describe Gitlab::VisibilityLevel do end end end - - describe '#visibility_level_decreased?' do - let(:project) { create(:project, :internal) } - - context 'when visibility level decreases' do - before do - project.update!(visibility_level: described_class::PRIVATE) - end - - it 'returns true' do - expect(project.visibility_level_decreased?).to be(true) - end - end - - context 'when visibility level does not decrease' do - before do - project.update!(visibility_level: described_class::PUBLIC) - end - - it 'returns false' do - expect(project.visibility_level_decreased?).to be(false) - end - end - end end diff --git a/spec/lib/release_highlights/validator/entry_spec.rb b/spec/lib/release_highlights/validator/entry_spec.rb new file mode 100644 index 00000000000..648356e63ba --- /dev/null +++ b/spec/lib/release_highlights/validator/entry_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ReleaseHighlights::Validator::Entry do + subject(:entry) { described_class.new(document.root.children.first) } + + let(:document) { YAML.parse(File.read(yaml_path)) } + let(:yaml_path) { 'spec/fixtures/whats_new/blank.yml' } + + describe 'validations' do + before do + allow(entry).to receive(:value_for).and_call_original + end + + context 'with a valid entry' do + let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' } + + it { is_expected.to be_valid } + end + + context 'with an invalid entry' do + let(:yaml_path) { 'spec/fixtures/whats_new/invalid.yml' } + + it 'returns line numbers in errors' do + subject.valid? + + expect(entry.errors[:packages].first).to match('(line 6)') + end + end + + context 'with a blank entry' do + it 'validate presence of title, body and stage' do + subject.valid? + + expect(subject.errors[:title]).not_to be_empty + expect(subject.errors[:body]).not_to be_empty + expect(subject.errors[:stage]).not_to be_empty + expect(subject.errors[:packages]).not_to be_empty + end + + it 'validates boolean value of "self-managed" and "gitlab-com"' do + allow(entry).to receive(:value_for).with('self-managed').and_return('nope') + allow(entry).to receive(:value_for).with('gitlab-com').and_return('yerp') + + subject.valid? + + expect(subject.errors[:'self-managed']).to include(/must be a boolean/) + expect(subject.errors[:'gitlab-com']).to include(/must be a boolean/) + end + + it 'validates URI of "url" and "image_url"' do + allow(entry).to receive(:value_for).with('image_url').and_return('imgur/gitlab_feature.gif') + allow(entry).to receive(:value_for).with('url').and_return('gitlab/newest_release.html') + + subject.valid? + + expect(subject.errors[:url]).to include(/must be a URL/) + expect(subject.errors[:image_url]).to include(/must be a URL/) + end + + it 'validates release is numerical' do + allow(entry).to receive(:value_for).with('release').and_return('one') + + subject.valid? + + expect(subject.errors[:release]).to include(/is not a number/) + end + + it 'validates published_at is a date' do + allow(entry).to receive(:value_for).with('published_at').and_return('christmas day') + + subject.valid? + + expect(subject.errors[:published_at]).to include(/must be valid Date/) + end + + it 'validates packages are included in list' do + allow(entry).to receive(:value_for).with('packages').and_return(['ALL']) + + subject.valid? + + expect(subject.errors[:packages].first).to include("must be one of", "Core", "Starter", "Premium", "Ultimate") + end + end + end +end diff --git a/spec/lib/release_highlights/validator_spec.rb b/spec/lib/release_highlights/validator_spec.rb new file mode 100644 index 00000000000..e68d9145dcd --- /dev/null +++ b/spec/lib/release_highlights/validator_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ReleaseHighlights::Validator do + let(:validator) { described_class.new(file: yaml_path) } + let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' } + let(:invalid_yaml_path) { 'spec/fixtures/whats_new/invalid.yml' } + + describe '#valid?' do + subject { validator.valid? } + + context 'with a valid file' do + it 'passes entries to entry validator and returns true' do + expect(ReleaseHighlights::Validator::Entry).to receive(:new).exactly(:twice).and_call_original + expect(subject).to be true + expect(validator.errors).to be_empty + end + end + + context 'with invalid file' do + let(:yaml_path) { invalid_yaml_path } + + it 'returns false and has errors' do + expect(subject).to be false + expect(validator.errors).not_to be_empty + end + end + end + + describe '.validate_all!' do + subject { described_class.validate_all! } + + before do + allow(ReleaseHighlight).to receive(:file_paths).and_return(yaml_paths) + end + + context 'with valid files' do + let(:yaml_paths) { [yaml_path, yaml_path] } + + it { is_expected.to be true } + end + + context 'with an invalid file' do + let(:yaml_paths) { [invalid_yaml_path, yaml_path] } + + it { is_expected.to be false } + end + end + + describe '.error_message' do + subject do + described_class.validate_all! + described_class.error_message + end + + before do + allow(ReleaseHighlight).to receive(:file_paths).and_return([yaml_path]) + end + + context 'with a valid file' do + it { is_expected.to be_empty } + end + + context 'with an invalid file' do + let(:yaml_path) { invalid_yaml_path } + + it 'returns a nice error message' do + expect(subject).to eq(<<-MESSAGE.strip_heredoc) + --------------------------------------------------------- + Validation failed for spec/fixtures/whats_new/invalid.yml + --------------------------------------------------------- + * Packages must be one of ["Core", "Starter", "Premium", "Ultimate"] (line 6) + + MESSAGE + end + end + end + + describe 'when validating all files' do + it 'they should have no errors' do + expect(described_class.validate_all!).to be_truthy, described_class.error_message + end + end +end diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb index 8425e1dbd46..ececc84bc93 100644 --- a/spec/lib/uploaded_file_spec.rb +++ b/spec/lib/uploaded_file_spec.rb @@ -27,12 +27,12 @@ RSpec.describe UploadedFile do end it 'handles a blank path' do - params['file.path'] = '' + params['path'] = '' # Not a real file, so can't determine size itself - params['file.size'] = 1.byte + params['size'] = 1.byte - expect { described_class.from_params(params, :file, upload_path) } + expect { described_class.from_params(params, upload_path) } .not_to raise_error end end @@ -50,7 +50,7 @@ RSpec.describe UploadedFile do end end - describe '.from_params_without_field' do + describe '.from_params' do let(:upload_path) { nil } after do @@ -58,7 +58,7 @@ RSpec.describe UploadedFile do end subject do - described_class.from_params_without_field(params, [upload_path, Dir.tmpdir]) + described_class.from_params(params, [upload_path, Dir.tmpdir]) end context 'when valid file is specified' do @@ -170,190 +170,6 @@ RSpec.describe UploadedFile do end end end - - describe '.from_params' do - let(:upload_path) { nil } - let(:file_path_override) { nil } - - after do - FileUtils.rm_r(upload_path) if upload_path - end - - subject do - described_class.from_params(params, :file, [upload_path, Dir.tmpdir], file_path_override) - end - - RSpec.shared_context 'filepath override' do - let(:temp_file_override) { Tempfile.new(%w[override override], temp_dir) } - let(:file_path_override) { temp_file_override.path } - - before do - FileUtils.touch(temp_file_override) - end - - after do - FileUtils.rm_f(temp_file_override) - end - end - - context 'when valid file is specified' do - context 'only local path is specified' do - let(:params) do - { 'file.path' => temp_file.path } - end - - it { is_expected.not_to be_nil } - - it "generates filename from path" do - expect(subject.original_filename).to eq(::File.basename(temp_file.path)) - end - end - - context 'all parameters are specified' do - context 'with a filepath' do - let(:params) do - { 'file.path' => temp_file.path, - 'file.name' => 'dir/my file&.txt', - 'file.type' => 'my/type', - 'file.sha256' => 'sha256' } - end - - it_behaves_like 'using the file path', - filename: 'my_file_.txt', - content_type: 'my/type', - sha256: 'sha256', - path_suffix: 'test' - end - - context 'with a filepath override' do - include_context 'filepath override' - - let(:params) do - { 'file.path' => temp_file.path, - 'file.name' => 'dir/my file&.txt', - 'file.type' => 'my/type', - 'file.sha256' => 'sha256' } - end - - it_behaves_like 'using the file path', - filename: 'my_file_.txt', - content_type: 'my/type', - sha256: 'sha256', - path_suffix: 'override' - end - - context 'with a remote id' do - let(:params) do - { - 'file.name' => 'dir/my file&.txt', - 'file.sha256' => 'sha256', - 'file.remote_url' => 'http://localhost/file', - 'file.remote_id' => '1234567890', - 'file.etag' => 'etag1234567890', - 'file.size' => '123456' - } - end - - it_behaves_like 'using the remote id', - filename: 'my_file_.txt', - content_type: 'application/octet-stream', - sha256: 'sha256', - size: 123456, - remote_id: '1234567890' - end - - context 'with a path and a remote id' do - let(:params) do - { - 'file.path' => temp_file.path, - 'file.name' => 'dir/my file&.txt', - 'file.sha256' => 'sha256', - 'file.remote_url' => 'http://localhost/file', - 'file.remote_id' => '1234567890', - 'file.etag' => 'etag1234567890', - 'file.size' => '123456' - } - end - - it_behaves_like 'using the remote id', - filename: 'my_file_.txt', - content_type: 'application/octet-stream', - sha256: 'sha256', - size: 123456, - remote_id: '1234567890' - end - - context 'with a path override and a remote id' do - include_context 'filepath override' - - let(:params) do - { - 'file.name' => 'dir/my file&.txt', - 'file.sha256' => 'sha256', - 'file.remote_url' => 'http://localhost/file', - 'file.remote_id' => '1234567890', - 'file.etag' => 'etag1234567890', - 'file.size' => '123456' - } - end - - it_behaves_like 'using the remote id', - filename: 'my_file_.txt', - content_type: 'application/octet-stream', - sha256: 'sha256', - size: 123456, - remote_id: '1234567890' - end - end - end - - context 'when no params are specified' do - let(:params) do - {} - end - - it "does not return an object" do - is_expected.to be_nil - end - end - - context 'when verifying allowed paths' do - let(:params) do - { 'file.path' => temp_file.path } - end - - context 'when file is stored in system temporary folder' do - let(:temp_dir) { Dir.tmpdir } - - it "succeeds" do - is_expected.not_to be_nil - end - end - - context 'when file is stored in user provided upload path' do - let(:upload_path) { Dir.mktmpdir } - let(:temp_dir) { upload_path } - - it "succeeds" do - is_expected.not_to be_nil - end - end - - context 'when file is stored outside of user provided upload path' do - let!(:generated_dir) { Dir.mktmpdir } - let!(:temp_dir) { Dir.mktmpdir } - - before do - # We overwrite default temporary path - allow(Dir).to receive(:tmpdir).and_return(generated_dir) - end - - it "raises an error" do - expect { subject }.to raise_error(UploadedFile::InvalidPathError, /insecure path used/) - end - end - end - end end describe '.initialize' do -- cgit v1.2.1