diff options
Diffstat (limited to 'spec/support_specs')
-rw-r--r-- | spec/support_specs/ability_check_spec.rb | 148 | ||||
-rw-r--r-- | spec/support_specs/helpers/packages/npm_spec.rb | 133 | ||||
-rw-r--r-- | spec/support_specs/matchers/exceed_redis_call_limit_spec.rb | 59 |
3 files changed, 340 insertions, 0 deletions
diff --git a/spec/support_specs/ability_check_spec.rb b/spec/support_specs/ability_check_spec.rb new file mode 100644 index 00000000000..ce841112d86 --- /dev/null +++ b/spec/support_specs/ability_check_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +require 'declarative_policy' +require 'request_store' +require 'tempfile' + +require 'gitlab/safe_request_store' + +require_relative '../../app/models/ability' +require_relative '../support/ability_check' + +RSpec.describe Support::AbilityCheck, feature_category: :system_access do # rubocop:disable RSpec/FilePath + let(:user) { :user } + let(:child) { Testing::Child.new } + let(:parent) { Testing::Parent.new(child) } + + before do + # Usually done in spec/spec_helper. + described_class.inject(Ability.singleton_class) + + stub_const('Testing::BasePolicy', Class.new(DeclarativePolicy::Base)) + + stub_const('Testing::Parent', Struct.new(:parent_of)) + stub_const('Testing::ParentPolicy', Class.new(Testing::BasePolicy) do + delegate { @subject.parent_of } + condition(:is_adult) { @subject.is_a?(Testing::Parent) } + rule { is_adult }.enable :drink_coffee + end) + + stub_const('Testing::Child', Class.new) + stub_const('Testing::ChildPolicy', Class.new(Testing::BasePolicy) do + condition(:always) { true } + rule { always }.enable :eat_ice + end) + end + + def expect_no_deprecation_warning(&block) + expect(&block).not_to output.to_stderr + end + + def expect_deprecation_warning(policy_class, ability, &block) + expect(&block) + .to output(/DEPRECATION WARNING: Ability :#{ability} in #{policy_class} not found./) + .to_stderr + end + + def expect_allowed(user, ability, subject) + expect(Ability.allowed?(user, ability, subject)) + end + + shared_examples 'ability found' do + it 'policy ability is found' do + expect_no_deprecation_warning do + expect_allowed(user, ability, subject).to eq(true) + end + end + end + + shared_examples 'ability not found' do |warning:| + description = 'policy ability is not found' + description += warning ? ' and emits a warning' : ' without warning' + + it description do + check = -> { expect_allowed(user, ability, subject).to eq(false) } + + if warning + expect_deprecation_warning(warning, ability, &check) + else + expect_no_deprecation_warning(&check) + end + end + end + + shared_context 'with custom TODO YAML' do + let(:yaml_file) { Tempfile.new } + + before do + yaml_file.write(yaml_content) + yaml_file.rewind + + stub_const("#{described_class}::Checker::TODO_YAML", yaml_file.path) + described_class::Checker.clear_memoization(:todo_list) + end + + after do + described_class::Checker.clear_memoization(:todo_list) + yaml_file.unlink + end + end + + describe 'checking ability' do + context 'with valid direct ability' do + let(:subject) { parent } + let(:ability) { :drink_coffee } + + include_examples 'ability found' + + context 'with empty TODO yaml' do + let(:yaml_content) { nil } + + include_context 'with custom TODO YAML' + include_examples 'ability found' + end + + context 'with non-Hash TODO yaml' do + let(:yaml_content) { '[]' } + + include_context 'with custom TODO YAML' + include_examples 'ability found' + end + end + + context 'with unreachable ability' do + let(:subject) { child } + let(:ability) { :drink_coffee } + + include_examples 'ability not found', warning: 'Testing::ChildPolicy' + + context 'when ignored in TODO YAML' do + let(:yaml_content) do + <<~YAML + Testing::ChildPolicy: + - #{ability} + YAML + end + + include_context 'with custom TODO YAML' + include_examples 'ability not found', warning: false + end + end + + context 'with unknown ability' do + let(:subject) { parent } + let(:ability) { :unknown } + + include_examples 'ability not found', warning: 'Testing::ParentPolicy' + end + + context 'with delegated ability' do + let(:subject) { parent } + let(:ability) { :eat_ice } + + include_examples 'ability found' + end + end +end diff --git a/spec/support_specs/helpers/packages/npm_spec.rb b/spec/support_specs/helpers/packages/npm_spec.rb new file mode 100644 index 00000000000..e1316a10fb1 --- /dev/null +++ b/spec/support_specs/helpers/packages/npm_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registry do # rubocop: disable RSpec/FilePath + let(:object) { klass.new(params) } + let(:klass) do + Struct.new(:params) do + include ::API::Helpers + include ::API::Helpers::Packages::Npm + end + end + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:namespace) { group } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:package) { create(:npm_package, project: project) } + + describe '#endpoint_scope' do + subject { object.endpoint_scope } + + context 'when params includes an id' do + let(:params) { { id: 42, package_name: 'foo' } } + + it { is_expected.to eq(:project) } + end + + context 'when params does not include an id' do + let(:params) { { package_name: 'foo' } } + + it { is_expected.to eq(:instance) } + end + end + + describe '#finder_for_endpoint_scope' do + subject { object.finder_for_endpoint_scope(package_name) } + + let(:package_name) { package.name } + + context 'when called with project scope' do + let(:params) { { id: project.id } } + + it 'returns a PackageFinder for project scope' do + expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, project: project) + + subject + end + end + + context 'when called with instance scope' do + let(:params) { { package_name: package_name } } + + it 'returns a PackageFinder for namespace scope' do + expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, namespace: group) + + subject + end + end + end + + describe '#project_id_or_nil' do + subject { object.project_id_or_nil } + + context 'when called with project scope' do + let(:params) { { id: project.id } } + + it { is_expected.to eq(project.id) } + end + + context 'when called with namespace scope' do + context 'when given an unscoped name' do + let(:params) { { package_name: 'foo' } } + + it { is_expected.to eq(nil) } + end + + context 'when given a scope that does not match a group name' do + let(:params) { { package_name: '@nonexistent-group/foo' } } + + it { is_expected.to eq(nil) } + end + + context 'when given a scope that matches a group name' do + let(:params) { { package_name: package.name } } + + it { is_expected.to eq(project.id) } + + context 'with another package with the same name, in another project in the namespace' do + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) } + + it 'returns the project id for the newest matching package within the scope' do + expect(subject).to eq(project2.id) + end + end + end + + context 'with npm_allow_packages_in_multiple_projects disabled' do + before do + stub_feature_flags(npm_allow_packages_in_multiple_projects: false) + end + + context 'when given an unscoped name' do + let(:params) { { package_name: 'foo' } } + + it { is_expected.to eq(nil) } + end + + context 'when given a scope that does not match a group name' do + let(:params) { { package_name: '@nonexistent-group/foo' } } + + it { is_expected.to eq(nil) } + end + + context 'when given a scope that matches a group name' do + let(:params) { { package_name: package.name } } + + it { is_expected.to eq(project.id) } + + context 'with another package with the same name, in another project in the namespace' do + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) } + + it 'returns the project id for the newest matching package within the scope' do + expect(subject).to eq(project2.id) + end + end + end + end + end + end +end diff --git a/spec/support_specs/matchers/exceed_redis_call_limit_spec.rb b/spec/support_specs/matchers/exceed_redis_call_limit_spec.rb new file mode 100644 index 00000000000..819f50e26b6 --- /dev/null +++ b/spec/support_specs/matchers/exceed_redis_call_limit_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'RedisCommand matchers', :use_clean_rails_redis_caching, feature_category: :source_code_management do + let(:control) do + RedisCommands::Recorder.new do + Rails.cache.read('test') + Rails.cache.read('test') + Rails.cache.write('test', 1) + end + end + + before do + Rails.cache.read('warmup') + end + + it 'verifies maximum number of Redis calls' do + expect(control).not_to exceed_redis_calls_limit(3) + + expect(control).not_to exceed_redis_command_calls_limit(:get, 2) + expect(control).not_to exceed_redis_command_calls_limit(:set, 1) + end + + it 'verifies minimum number of Redis calls' do + expect(control).to exceed_redis_calls_limit(2) + + expect(control).to exceed_redis_command_calls_limit(:get, 1) + expect(control).to exceed_redis_command_calls_limit(:set, 0) + end + + context 'with Recorder matching only some Redis calls' do + it 'counts only Redis calls captured by Recorder' do + Rails.cache.write('ignored', 1) + + control = RedisCommands::Recorder.new do + Rails.cache.read('recorded') + end + + Rails.cache.write('also_ignored', 1) + + expect(control).not_to exceed_redis_calls_limit(1) + expect(control).not_to exceed_redis_command_calls_limit(:set, 0) + expect(control).not_to exceed_redis_command_calls_limit(:get, 1) + end + end + + context 'when expect part is a function' do + it 'automatically enables RedisCommand::Recorder for it' do + func = -> do + Rails.cache.read('test') + Rails.cache.read('test') + end + + expect { func.call }.not_to exceed_redis_calls_limit(2) + expect { func.call }.not_to exceed_redis_command_calls_limit(:get, 2) + end + end +end |