diff --git a/ee/config/feature_flags/wip/validate_codeowner_users.yml b/ee/config/feature_flags/wip/validate_codeowner_users.yml new file mode 100644 index 0000000000000000000000000000000000000000..d401f8a4feb2ca8314b41168acb141e855e0fc91 --- /dev/null +++ b/ee/config/feature_flags/wip/validate_codeowner_users.yml @@ -0,0 +1,9 @@ +--- +name: validate_codeowner_users +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502583 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179310 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/519201 +milestone: '17.9' +group: group::source code +type: wip +default_enabled: false diff --git a/ee/lib/gitlab/code_owners/entry.rb b/ee/lib/gitlab/code_owners/entry.rb index b1e9d3eb1f34d00658c9be882e2b0fef19c1bbe3..f3f8b4b843be271d9d8d88da7df28d02bebc04ea 100644 --- a/ee/lib/gitlab/code_owners/entry.rb +++ b/ee/lib/gitlab/code_owners/entry.rb @@ -5,20 +5,18 @@ module CodeOwners class Entry include ::Gitlab::Utils::StrongMemoize - Data = Struct.new(:pattern, :owner_line, :section, :optional, :approvals_required, :exclusion) + Data = Struct.new(:pattern, :owner_line, :section, :optional, :approvals_required, :exclusion, :line_number) attr_reader :data protected :data - delegate :pattern, :hash, :owner_line, :section, :approvals_required, :exclusion, to: :data + delegate :pattern, :hash, :owner_line, :section, :approvals_required, :exclusion, :line_number, to: :data - # rubocop:disable Metrics/ParameterLists -- TODO: Reduce number of parameters in this method def initialize( - pattern, owner_line, section = Section::DEFAULT, optional = false, approvals_required = 0, - exclusion = false) - @data = Data.new(pattern, owner_line, section, optional, approvals_required, exclusion) + pattern, owner_line, section: Section::DEFAULT, optional: false, approvals_required: 0, + line_number: nil, exclusion: false) + @data = Data.new(pattern, owner_line, section, optional, approvals_required, exclusion, line_number) end - # rubocop:enable Metrics/ParameterLists def all_users(project) strong_memoize(:all_users) do diff --git a/ee/lib/gitlab/code_owners/error.rb b/ee/lib/gitlab/code_owners/error.rb index a2392ac57a2e520c806b9c25b62e2823d06717bb..87a115da5e1855a8ddae85e5f8ffc8a1e8d5cd4a 100644 --- a/ee/lib/gitlab/code_owners/error.rb +++ b/ee/lib/gitlab/code_owners/error.rb @@ -9,6 +9,7 @@ class Error MISSING_SECTION_NAME = :missing_section_name INVALID_APPROVAL_REQUIREMENT = :invalid_approval_requirement INVALID_SECTION_FORMAT = :invalid_section_format + OWNER_WITHOUT_PERMISSION = :owner_without_permission def initialize(message:, line_number:, path:) @message = message diff --git a/ee/lib/gitlab/code_owners/file.rb b/ee/lib/gitlab/code_owners/file.rb index 7f5b64347c8eea3096cd9bc9ca8675a3e9ac1b82..3c5fcde04309296727393763eab05a0e00d4df9d 100644 --- a/ee/lib/gitlab/code_owners/file.rb +++ b/ee/lib/gitlab/code_owners/file.rb @@ -9,6 +9,10 @@ class File # `FNM_PATHNAME` makes sure ** matches path separators FNMATCH_FLAGS = (::File::FNM_DOTMATCH | ::File::FNM_PATHNAME).freeze + # Maxmimum number of references to validate + # This maximum is currently not based on any benchmark + MAX_REFERENCES = 200 + def initialize(blob) @blob = blob @errors = [] @@ -61,7 +65,7 @@ def entries_for_path(path) matches = [] parsed_data.each do |_, section_entries| - if Feature.enabled?(:codeowners_file_exclusions, @blob.repository.project) + if Feature.enabled?(:codeowners_file_exclusions, project) matching_patterns = section_entries.keys.reverse.select { |pattern| path_matches?(pattern, path) } matching_entries = matching_patterns.map { |pattern| section_entries[pattern] } @@ -82,12 +86,30 @@ def entries_for_path(path) def valid? parsed_data + validate_users errors.none? end private + def project + @blob&.repository&.project + end + + def validate_users + # Avoids querying the database for users if there are still syntax errors + return if errors.present? + + return unless project && Feature.enabled?(:validate_codeowner_users, project) + + entries = parsed_data.values.flat_map(&:values) + validation_errors = UserPermissionCheck.new(project, entries, limit: MAX_REFERENCES).errors + validation_errors.each do |e| + add_error(e[:error], e[:line_number]) + end + end + def data return "" if @blob.nil? || @blob.binary? @@ -130,7 +152,7 @@ def get_parsed_data def parse_entry(line, parsed, section, line_number) pattern, _separator, entry_owners = line.partition(/(?<!\\)\s+/) - if Feature.enabled?(:codeowners_file_exclusions, @blob.repository.project) + if Feature.enabled?(:codeowners_file_exclusions, project) is_exclusion = pattern.start_with?('!') pattern = pattern[1..] if is_exclusion end @@ -145,17 +167,15 @@ def parse_entry(line, parsed, section, line_number) add_error(Error::MISSING_ENTRY_OWNER, line_number) if owners.blank? - entry_args = [ + parsed[section.name][normalized_pattern] = Entry.new( pattern, owners, - section.name, - section.optional, - section.approvals - ] - - entry_args << is_exclusion if Feature.enabled?(:codeowners_file_exclusions, @blob.repository.project) - - parsed[section.name][normalized_pattern] = Entry.new(*entry_args) + section: section.name, + optional: section.optional, + approvals_required: section.approvals, + exclusion: Feature.enabled?(:codeowners_file_exclusions, project) && is_exclusion, + line_number: line_number + ) end def skip?(line) diff --git a/ee/lib/gitlab/code_owners/user_permission_check.rb b/ee/lib/gitlab/code_owners/user_permission_check.rb new file mode 100644 index 0000000000000000000000000000000000000000..8ffac4dc5ca554e33b0be58f9d35ee4214723dff --- /dev/null +++ b/ee/lib/gitlab/code_owners/user_permission_check.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Gitlab + module CodeOwners + class UserPermissionCheck + def initialize(project, code_owners_entries, limit:) + @project = project + @entries = code_owners_entries + @limit = limit + end + + def errors + groups = GroupsLoader.new(project, names: extracted_names).groups + possible_usernames = extracted_names - groups.map(&:full_path) + + users = UsersLoader.new(project, names: possible_usernames).members + + # Preload for ability check + ActiveRecord::Associations::Preloader.new(records: users, associations: :namespace_bans).call + project.team.max_member_access_for_user_ids(users.map(&:id)) + + users_with_permission = users.select do |user| + user.can?(:update_merge_request, project) + end + + error_usernames = possible_usernames - users_with_permission.map(&:username) + entries.each_with_object([]) do |entry, errors| + next unless Gitlab::CodeOwners::ReferenceExtractor.new(entry.owner_line).names.intersect?(error_usernames) + + errors << { + error: Error::OWNER_WITHOUT_PERMISSION, + line_number: entry.line_number + } + end + end + + private + + attr_reader :entries, :project, :limit + + def extracted_names + owner_lines = entries.map(&:owner_line) + ReferenceExtractor.new(owner_lines).names.first(limit) + end + end + end +end diff --git a/ee/spec/graphql/api/validate_code_owner_file_spec.rb b/ee/spec/graphql/api/validate_code_owner_file_spec.rb index 758693248f9830c8abd7590672d28bd2b5a33115..c8c176a10019f4c5ca0b4fd696cf18720a3e7243 100644 --- a/ee/spec/graphql/api/validate_code_owner_file_spec.rb +++ b/ee/spec/graphql/api/validate_code_owner_file_spec.rb @@ -203,12 +203,19 @@ context 'when code owners file is correct' do let_it_be(:ref) { 'good-branch' } - + let_it_be(:documentation_owner) { create(:user, developer_of: project) } + let_it_be(:nested_group) { create(:group, :nested) } + let_it_be(:test_owner) { create(:user, developer_of: project) } + let_it_be(:owner) { create(:user, developer_of: project) } + let_it_be(:owner2) { create(:user, developer_of: project) } + + let!(:project_group_link) { create(:project_group_link, project: project, group: nested_group.parent) } + let!(:project_group_link2) { create(:project_group_link, project: project, group: nested_group) } let_it_be(:code_owners_content) do <<~CODEOWNERS - docs/* @documentation-owner - docs/CODEOWNERS @owner-1 owner2@gitlab.org @owner-3 @documentation-owner - spec/* @test-owner @test-group @test-group/nested-group + docs/* @#{documentation_owner.username} + docs/CODEOWNERS @#{owner.username} owner2@gitlab.org @#{owner2.username} @#{documentation_owner.username} + spec/* @#{test_owner.username} @#{nested_group.parent.full_path} @#{nested_group.full_path} CODEOWNERS end diff --git a/ee/spec/lib/gitlab/code_owners/entry_spec.rb b/ee/spec/lib/gitlab/code_owners/entry_spec.rb index 74f3840d5b6ce639daaf1c781389292c6d01dc43..1e218da84a93ec3a8f4a568d759897df710b31ac 100644 --- a/ee/spec/lib/gitlab/code_owners/entry_spec.rb +++ b/ee/spec/lib/gitlab/code_owners/entry_spec.rb @@ -6,7 +6,7 @@ described_class.new( "/**/file", "@user jane@gitlab.org @group @group/nested-group @@maintainer @@developers @@maintainer", - "Documentation" + section: "Documentation" ) end @@ -155,9 +155,9 @@ described_class.new( "/**/file", "@user jane@gitlab.org @group @group/nested-group", - "Documentation", - false, - 2) + section: "Documentation", + optional: false, + approvals_required: 2) end it 'returns 2' do @@ -170,7 +170,7 @@ described_class.new( "/**/file", "@user jane@gitlab.org @group @group/nested-group", - "Documentation") + section: "Documentation") end it 'returns 0' do diff --git a/ee/spec/lib/gitlab/code_owners/file_spec.rb b/ee/spec/lib/gitlab/code_owners/file_spec.rb index fd667b70b83e324512e3e80df7ef7359418f10b2..d469aff400eb25a332b9eafdca12f069243eedf9 100644 --- a/ee/spec/lib/gitlab/code_owners/file_spec.rb +++ b/ee/spec/lib/gitlab/code_owners/file_spec.rb @@ -7,7 +7,7 @@ # 'project' is required for the #fake_blob helper # - let(:project) { build(:project) } + let_it_be_with_reload(:project) { create(:project, :in_group) } let(:blob) { fake_blob(path: 'CODEOWNERS', data: file_content) } let(:file_content) do @@ -556,14 +556,7 @@ def owner_line(pattern) end describe '#valid?' do - context 'when codeowners file is correct' do - it 'does not detect errors' do - expect(file.valid?).to eq(true) - expect(file.errors).to be_empty - end - end - - context 'when codeowners file has errors' do + context 'when codeowners file has syntax errors' do let(:file_content) do <<~CONTENT *.rb @@ -595,6 +588,48 @@ def owner_line(pattern) ] ) end + + it 'does not call UserPermissionCheck' do + expect(Gitlab::CodeOwners::UserPermissionCheck).not_to receive(:new) + + file.valid? + end + end + + context 'when the codeowners file does not have syntax errors' do + let(:file_content) do + <<~CONTENT + file1 @reference + + README.md @other_reference + + CONTENT + end + + it 'calls UserPermissionCheck' do + expect(Gitlab::CodeOwners::UserPermissionCheck).to receive(:new).with( + project, + [ + a_kind_of(Gitlab::CodeOwners::Entry), + a_kind_of(Gitlab::CodeOwners::Entry) + ], + limit: described_class::MAX_REFERENCES + ).and_return(instance_double(Gitlab::CodeOwners::UserPermissionCheck, errors: [])) + + file.valid? + end + + context 'when the validate_codeowner_users feature flag is not enabled' do + before do + stub_feature_flags(validate_codeowner_users: false) + end + + it 'does not call UserPermissionCheck' do + expect(Gitlab::CodeOwners::UserPermissionCheck).not_to receive(:new) + + file.valid? + end + end end end end diff --git a/ee/spec/lib/gitlab/code_owners/loader_spec.rb b/ee/spec/lib/gitlab/code_owners/loader_spec.rb index 6116cf68523820d5d3386616f017bb570f366afc..ace81592bddaf06f468d483b7f4fd8326bf995d0 100644 --- a/ee/spec/lib/gitlab/code_owners/loader_spec.rb +++ b/ee/spec/lib/gitlab/code_owners/loader_spec.rb @@ -39,7 +39,11 @@ end describe '#entries' do - let(:expected_entry) { Gitlab::CodeOwners::Entry.new('docs/CODEOWNERS', '@owner-1 owner2@gitlab.org @owner-3 @documentation-owner') } + let(:expected_entry) { Gitlab::CodeOwners::Entry.new('docs/CODEOWNERS', '@owner-1 owner2@gitlab.org @owner-3 @documentation-owner', line_number: 2) } + let(:spec_entry) do + Gitlab::CodeOwners::Entry.new('spec/*', '@test-owner @test-group @test-group/nested-group', line_number: 3) + end + let(:first_entry) { loader.entries.first } it 'returns entries for the matched line' do @@ -71,9 +75,7 @@ let(:paths) { ['docs/CODEOWNERS', 'spec/loader_spec.rb', 'spec/entry_spec.rb'] } it 'loads 2 entries' do - other_entry = Gitlab::CodeOwners::Entry.new('spec/*', '@test-owner @test-group @test-group/nested-group') - - expect(loader.entries).to contain_exactly(expected_entry, other_entry) + expect(loader.entries).to contain_exactly(expected_entry, spec_entry) end it 'performs 10 queries for users and groups' do @@ -100,7 +102,6 @@ context 'group as a code owner' do let(:paths) { ['spec/loader_spec.rb'] } - let(:expected_entry) { Gitlab::CodeOwners::Entry.new('spec/*', '@test-owner @test-group @test-group/nested-group') } it 'loads group members as code owners' do test_group = create(:group, path: 'test-group') @@ -111,7 +112,7 @@ test_group.add_developer(group_user) test_group.add_developer(test_owner) - expect(loader.entries).to contain_exactly(expected_entry) + expect(loader.entries).to contain_exactly(spec_entry) expect(loader.members).to contain_exactly(group_user, test_owner) entry = loader.entries.first @@ -121,7 +122,7 @@ context 'role as a code owner' do let(:paths) { ['app/accounts_helper.rb'] } - let(:expected_entry) { Gitlab::CodeOwners::Entry.new('app/*', '@@developer') } + let(:expected_entry) { Gitlab::CodeOwners::Entry.new('app/*', '@@developer', line_number: 4) } it 'loads users with developer role as code owners' do expect(loader.entries).to contain_exactly(expected_entry) @@ -135,12 +136,12 @@ context 'for multiple paths' do let(:paths) { ['app/accounts_helper.rb', 'spec/entry_spec.rb', 'config/boot.rb'] } + let(:other_entry_2) do + Gitlab::CodeOwners::Entry.new('config/*', '@@maintainers', line_number: 5) + end it 'loads 3 entries' do - other_entry_1 = Gitlab::CodeOwners::Entry.new('spec/*', '@test-owner @test-group @test-group/nested-group') - other_entry_2 = Gitlab::CodeOwners::Entry.new('config/*', '@@maintainers') - - expect(loader.entries).to contain_exactly(expected_entry, other_entry_1, other_entry_2) + expect(loader.entries).to contain_exactly(expected_entry, spec_entry, other_entry_2) end end end @@ -293,10 +294,24 @@ subject { loader.track_file_validation } context 'when file has no linting error' do - it 'sends no snowplow event' do + context 'when the validate_codeowner_users flag is not enabled' do + before do + stub_feature_flags(validate_codeowner_users: false) + end + + it 'sends no snowplow event' do + subject + + expect_no_snowplow_event(category: described_class.name) + end + end + + it 'sends errors for invalid user references' do subject - expect_no_snowplow_event(category: described_class.name) + expect_snowplow_event(category: described_class.name, action: 'file_validation', label: 'owner_without_permission', project: project, extra: { file_path: "CODEOWNERS", line_number: 2 }) + expect_snowplow_event(category: described_class.name, action: 'file_validation', label: 'owner_without_permission', project: project, extra: { file_path: "CODEOWNERS", line_number: 3 }) + expect_snowplow_event(category: described_class.name, action: 'file_validation', label: 'owner_without_permission', project: project, extra: { file_path: "CODEOWNERS", line_number: 3 }) end end diff --git a/ee/spec/lib/gitlab/code_owners/user_permission_check_spec.rb b/ee/spec/lib/gitlab/code_owners/user_permission_check_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c01b96a2728e144e4ad55edaf9470e91be20045 --- /dev/null +++ b/ee/spec/lib/gitlab/code_owners/user_permission_check_spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::CodeOwners::UserPermissionCheck, feature_category: :source_code_management do + let_it_be_with_reload(:project) { create(:project, :in_group) } + let(:limit) { Gitlab::CodeOwners::File::MAX_REFERENCES } + let(:section) { Gitlab::CodeOwners::Section::DEFAULT } + let(:group_entry) do + Gitlab::CodeOwners::Entry.new( + 'file1', + "@#{project.group.full_path}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 1 + ) + end + + let(:entry) do + Gitlab::CodeOwners::Entry.new( + 'file2', + "@#{user.username}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 3 + ) + end + + let(:entries) { [group_entry, entry] } + let_it_be(:guest) { create(:user, guest_of: project) } + let_it_be(:developer) { create(:user, developer_of: project) } + let_it_be(:group_developer) { create(:user, developer_of: project.group) } + let(:not_persisted_user) { build(:user) } + let(:non_member) { create(:user) } + + subject(:errors) { described_class.new(project, entries, limit: limit).errors } + + context 'when the entries mention a username' do + shared_examples_for 'validation error for name without permissions' do + it 'has warnings' do + expect(errors).to contain_exactly({ + error: Gitlab::CodeOwners::Error::OWNER_WITHOUT_PERMISSION, + line_number: entry.line_number + }) + end + end + + shared_examples_for 'valid entries' do + it { is_expected.to be_blank } + end + + context 'when the user does not exist' do + let(:user) { not_persisted_user } + + it_behaves_like 'validation error for name without permissions' + end + + context 'when the user is not a member' do + let(:user) { non_member } + + it_behaves_like 'validation error for name without permissions' + end + + context 'when the user is directly invited' do + let(:user) { guest } + + it_behaves_like 'validation error for name without permissions' + + context 'with sufficient access' do + let(:user) { developer } + + it_behaves_like 'valid entries' + end + end + + context 'when the user inherits access from the parent group' do + let(:user) { group_developer } + + it_behaves_like 'valid entries' + end + + context 'when the user is from an invited group' do + let(:access_level) { :guest } + let(:project_group_link) { create :project_group_link, access_level, project: project } + let(:user) { create(:user, developer_of: project_group_link.group) } + + it_behaves_like 'validation error for name without permissions' + + context 'with sufficient access' do + let(:access_level) { :developer } + + it_behaves_like 'valid entries' + end + end + end + + describe 'queries' do + let(:multiple_reference_entry) do + Gitlab::CodeOwners::Entry.new( + 'file1', + "@#{project.group.full_path} @#{group_developer.username}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 1 + ) + end + + let(:single_reference_entry) do + Gitlab::CodeOwners::Entry.new( + 'file2', + "@#{project.group.full_path}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 1 + ) + end + + let(:invalid_entry_with_multiple_references) do + Gitlab::CodeOwners::Entry.new( + 'file4', + "@#{developer.username} @#{non_member.username} @#{guest.username}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 2 + ) + end + + let(:invalid_entry) do + Gitlab::CodeOwners::Entry.new( + 'file3', + "@#{not_persisted_user.username}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 3 + ) + end + + let!(:entries) do + [ + multiple_reference_entry, + single_reference_entry, + invalid_entry_with_multiple_references, + invalid_entry + ] + end + + context 'when limiting the number of references' do + context 'when the number of references is within the limit' do + it 'validates all the references' do + expect(Gitlab::CodeOwners::UsersLoader).to receive(:new).with( + project, names: [group_developer, developer, non_member, guest, not_persisted_user].map(&:username) + ).and_call_original + expect(errors.all? { |e| e[:error] == Gitlab::CodeOwners::Error::OWNER_WITHOUT_PERMISSION }).to be(true) + expect(errors.pluck(:line_number)).to match_array([ + invalid_entry_with_multiple_references.line_number, + invalid_entry.line_number + ]) + end + end + + context 'when there are more references than the limit' do + before do + stub_const('Gitlab::CodeOwners::File::MAX_REFERENCES', 4) + end + + it 'only validates up to the maximum' do + expect(Gitlab::CodeOwners::UsersLoader).to receive(:new).with( + project, names: [group_developer, developer, non_member].map(&:username) + ).and_call_original + error = errors.first + + expect(errors.size).to eq(1) + expect(error[:error]).to eq(Gitlab::CodeOwners::Error::OWNER_WITHOUT_PERMISSION) + expect(error[:line_number]).to eq(invalid_entry_with_multiple_references.line_number) + end + end + end + + it 'does not perform N+1 queries', :request_store, :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true, skip_cached: false) do + described_class.new(project, [invalid_entry_with_multiple_references, multiple_reference_entry], + limit: limit).errors + end + + extra_developer = create(:user, developer_of: project) + extra_guest = create(:user, guest_of: project) + extra_non_member = create(:user) + extra_invalid_entry = Gitlab::CodeOwners::Entry.new( + 'extra_file', + "@#{extra_developer.username} @#{extra_non_member.username} @#{extra_guest.username}", + section: section, + optional: true, + approvals_required: 0, + exclusion: false, + line_number: 10 + ) + + project.reload + + expect do + described_class.new(project, [ + invalid_entry_with_multiple_references, + multiple_reference_entry, + extra_invalid_entry + ], limit: limit).errors + end.to issue_same_number_of_queries_as(control) + end + end +end diff --git a/ee/spec/models/approval_merge_request_rule_spec.rb b/ee/spec/models/approval_merge_request_rule_spec.rb index 4877ede6642244af39f448c2936699a1fa62f639..847dd427b07c43b93ad91b63b8dfb121dff4b12c 100644 --- a/ee/spec/models/approval_merge_request_rule_spec.rb +++ b/ee/spec/models/approval_merge_request_rule_spec.rb @@ -260,7 +260,7 @@ end context "when the existing rule matches name but not section" do - let(:entry) { Gitlab::CodeOwners::Entry.new("*.rb", "@user", "example_section") } + let(:entry) { Gitlab::CodeOwners::Entry.new("*.rb", "@user", section: "example_section") } it "creates a new rule" do expect(rule).not_to eq(existing_code_owner_rule) @@ -273,14 +273,14 @@ create(:code_owner_rule, name: '*.rb', merge_request: merge_request, approvals_required: 2) end - let(:entry) { Gitlab::CodeOwners::Entry.new("*.rb", "@user", "codeowners", false, 2) } + let(:entry) { Gitlab::CodeOwners::Entry.new("*.rb", "@user", section: "codeowners", optional: false, approvals_required: 2) } it 'finds the existing rule' do expect(rule).to eq(existing_code_owner_rule) end context "when the existing rule matches name but not section" do - let(:entry) { Gitlab::CodeOwners::Entry.new("*.rb", "@user", "example_section", false, 2) } + let(:entry) { Gitlab::CodeOwners::Entry.new("*.rb", "@user", section: "example_section", optional: false, approvals_required: 2) } it "creates a new rule" do expect(rule).not_to eq(existing_code_owner_rule) @@ -313,7 +313,7 @@ end context "when section is present" do - let(:entry) { Gitlab::CodeOwners::Entry.new("*.js", "@user", "Test Section") } + let(:entry) { Gitlab::CodeOwners::Entry.new("*.js", "@user", section: "Test Section") } it "creates a new rule and saves section when present" do expect(subject.section).to eq(entry.section) diff --git a/ee/spec/services/merge_requests/sync_code_owner_approval_rules_spec.rb b/ee/spec/services/merge_requests/sync_code_owner_approval_rules_spec.rb index b34daeb00f2f5ad9267d7f8db70cdbaf2ae864b6..b07bcbe74a0397cd81c0253b78ad07434eb47b29 100644 --- a/ee/spec/services/merge_requests/sync_code_owner_approval_rules_spec.rb +++ b/ee/spec/services/merge_requests/sync_code_owner_approval_rules_spec.rb @@ -28,7 +28,8 @@ def build_entry(pattern, users, groups, roles, section = Gitlab::CodeOwners::Section::DEFAULT, approvals_required = 0) text = "#{(users + groups).map(&:to_reference).join(' ')} #{roles}" - entry = Gitlab::CodeOwners::Entry.new(pattern, text, section, false, approvals_required) + entry = Gitlab::CodeOwners::Entry.new(pattern, text, section: section, optional: false, + approvals_required: approvals_required) entry.add_matching_users_from(users) entry.add_matching_groups_from(groups)