summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-14 12:06:30 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-14 12:06:30 +0000
commitd8c06be498acbfc2024c01b6b6b02d120dc499f2 (patch)
tree9e2e0852c45332d6222898676a2f6f096e600084 /spec
parent2fa7d2ddf6a7004f89616e43b8279229af831e25 (diff)
downloadgitlab-ce-d8c06be498acbfc2024c01b6b6b02d120dc499f2.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb4
-rw-r--r--spec/frontend/cycle_analytics/stage_nav_item_spec.js44
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js4
-rw-r--r--spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap2
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js2
-rw-r--r--spec/frontend/repository/utils/readme_spec.js33
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb24
-rw-r--r--spec/graphql/resolvers/commit_pipelines_resolver_spec.rb53
-rw-r--r--spec/graphql/types/commit_type_spec.rb2
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js2
-rw-r--r--spec/lib/gitlab/fogbugz_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb20
-rw-r--r--spec/models/merge_request_diff_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb46
-rw-r--r--spec/requests/api/group_export_spec.rb94
-rw-r--r--spec/support/helpers/access_matchers_helpers.rb95
-rw-r--r--spec/support/matchers/access_matchers_for_request.rb53
-rw-r--r--spec/support/matchers/access_matchers_generic.rb66
18 files changed, 485 insertions, 69 deletions
diff --git a/spec/features/projects/files/user_reads_pipeline_status_spec.rb b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
index 15f8fa7438d..9d38c44b6ef 100644
--- a/spec/features/projects/files/user_reads_pipeline_status_spec.rb
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -9,8 +9,6 @@ describe 'user reads pipeline status', :js do
let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') }
before do
- stub_feature_flags(vue_file_list: false)
-
project.add_maintainer(user)
project.repository.add_tag(user, 'x1.1.0', 'v1.1.0')
@@ -25,7 +23,7 @@ describe 'user reads pipeline status', :js do
visit project_tree_path(project, expected_pipeline.ref)
wait_for_requests
- page.within('.blob-commit-info') do
+ page.within('.commit-detail') do
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
end
diff --git a/spec/frontend/cycle_analytics/stage_nav_item_spec.js b/spec/frontend/cycle_analytics/stage_nav_item_spec.js
index ff079082ca7..a7a1d563e1e 100644
--- a/spec/frontend/cycle_analytics/stage_nav_item_spec.js
+++ b/spec/frontend/cycle_analytics/stage_nav_item_spec.js
@@ -133,45 +133,19 @@ describe('StageNavItem', () => {
hasStageName();
});
- it('renders options menu', () => {
- expect(wrapper.find('.more-actions-toggle').exists()).toBe(true);
+ it('does not render options menu', () => {
+ expect(wrapper.find('.more-actions-toggle').exists()).toBe(false);
});
- describe('Default stages', () => {
- beforeEach(() => {
- wrapper = createComponent(
- { canEdit: true, isUserAllowed: true, isDefaultStage: true },
- false,
- );
- });
- it('can hide the stage', () => {
- expect(wrapper.text()).toContain('Hide stage');
- });
- it('can not edit the stage', () => {
- expect(wrapper.text()).not.toContain('Edit stage');
- });
- it('can not remove the stage', () => {
- expect(wrapper.text()).not.toContain('Remove stage');
- });
+ it('can not edit the stage', () => {
+ expect(wrapper.text()).not.toContain('Edit stage');
+ });
+ it('can not remove the stage', () => {
+ expect(wrapper.text()).not.toContain('Remove stage');
});
- describe('Custom stages', () => {
- beforeEach(() => {
- wrapper = createComponent(
- { canEdit: true, isUserAllowed: true, isDefaultStage: false },
- false,
- );
- });
- it('can edit the stage', () => {
- expect(wrapper.text()).toContain('Edit stage');
- });
- it('can remove the stage', () => {
- expect(wrapper.text()).toContain('Remove stage');
- });
-
- it('can not hide the stage', () => {
- expect(wrapper.text()).not.toContain('Hide stage');
- });
+ it('can not hide the stage', () => {
+ expect(wrapper.text()).not.toContain('Hide stage');
});
});
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 01b56d453e6..e07ad4cf46b 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -17,7 +17,7 @@ function createCommitData(data = {}) {
avatarUrl: 'https://test.com',
webUrl: 'https://test.com/test',
},
- latestPipeline: {
+ pipeline: {
detailedStatus: {
detailsPath: 'https://test.com/pipeline',
icon: 'failed',
@@ -74,7 +74,7 @@ describe('Repository last commit component', () => {
});
it('hides pipeline components when pipeline does not exist', () => {
- factory(createCommitData({ latestPipeline: null }));
+ factory(createCommitData({ pipeline: null }));
expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
});
diff --git a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
index 3d5ec3fd411..a5e3eb4bce1 100644
--- a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Repository file preview component renders file HTML 1`] = `
<article
- class="file-holder js-hide-on-navigation limited-width-container readme-holder"
+ class="file-holder limited-width-container readme-holder"
>
<div
class="file-title"
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 954c4791c04..148e307a5d4 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -28,7 +28,7 @@ describe('Repository table component', () => {
it('renders file preview', () => {
factory('/');
- vm.setData({ entries: { blobs: [{ name: 'README.md ' }] } });
+ vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
expect(vm.find(FilePreview).exists()).toBe(true);
});
diff --git a/spec/frontend/repository/utils/readme_spec.js b/spec/frontend/repository/utils/readme_spec.js
new file mode 100644
index 00000000000..6b7876c8947
--- /dev/null
+++ b/spec/frontend/repository/utils/readme_spec.js
@@ -0,0 +1,33 @@
+import { readmeFile } from '~/repository/utils/readme';
+
+describe('readmeFile', () => {
+ describe('markdown files', () => {
+ it('returns markdown file', () => {
+ expect(readmeFile([{ name: 'README' }, { name: 'README.md' }])).toEqual({
+ name: 'README.md',
+ });
+
+ expect(readmeFile([{ name: 'README' }, { name: 'index.md' }])).toEqual({
+ name: 'index.md',
+ });
+ });
+ });
+
+ describe('plain files', () => {
+ it('returns plain file', () => {
+ expect(readmeFile([{ name: 'README' }, { name: 'TEST.md' }])).toEqual({
+ name: 'README',
+ });
+
+ expect(readmeFile([{ name: 'readme' }, { name: 'TEST.md' }])).toEqual({
+ name: 'readme',
+ });
+ });
+ });
+
+ describe('non-previewable file', () => {
+ it('returns undefined', () => {
+ expect(readmeFile([{ name: 'index.js' }, { name: 'TEST.md' }])).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index c162fdbbb47..a212bd07f35 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -13,6 +13,14 @@ describe Resolvers::BaseResolver do
end
end
+ let(:last_resolver) do
+ Class.new(described_class) do
+ def resolve(**args)
+ [1, 2]
+ end
+ end
+ end
+
describe '.single' do
it 'returns a subclass from the resolver' do
expect(resolver.single.superclass).to eq(resolver)
@@ -29,6 +37,22 @@ describe Resolvers::BaseResolver do
end
end
+ describe '.last' do
+ it 'returns a subclass from the resolver' do
+ expect(last_resolver.last.superclass).to eq(last_resolver)
+ end
+
+ it 'returns the same subclass every time' do
+ expect(last_resolver.last.object_id).to eq(last_resolver.last.object_id)
+ end
+
+ it 'returns a resolver that gives the last result from the original resolver' do
+ result = resolve(last_resolver.last)
+
+ expect(result).to eq(2)
+ end
+ end
+
context 'when field is a connection' do
it 'increases complexity based on arguments' do
field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1)
diff --git a/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb b/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb
new file mode 100644
index 00000000000..93da877d714
--- /dev/null
+++ b/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::CommitPipelinesResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let(:commit) { create(:commit, project: project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let!(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'master',
+ status: 'success'
+ )
+ end
+ let!(:pipeline2) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'master',
+ status: 'failed'
+ )
+ end
+ let!(:pipeline3) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'my_branch',
+ status: 'failed'
+ )
+ end
+
+ before do
+ commit.project.add_developer(current_user)
+ end
+
+ def resolve_pipelines
+ resolve(described_class, obj: commit, ctx: { current_user: current_user }, args: { ref: 'master' })
+ end
+
+ it 'resolves pipelines for commit and ref' do
+ pipelines = resolve_pipelines
+
+ expect(pipelines).to eq([pipeline2, pipeline])
+ end
+end
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index 1ff1c97f8db..ee9af886e60 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -10,7 +10,7 @@ describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
:id, :sha, :title, :description, :message, :authored_date,
- :author, :web_url, :latest_pipeline, :signature_html
+ :author, :web_url, :latest_pipeline, :pipelines, :signature_html
)
end
end
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index b2fe315f6c6..b53e30b6896 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
-const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables';
+const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables';
const HIDE_CLASS = 'hide';
describe('AjaxFormVariableList', () => {
diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb
index dcd1a2d9813..676511211c8 100644
--- a/spec/lib/gitlab/fogbugz_import/client_spec.rb
+++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::FogbugzImport::Client do
diff --git a/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb
deleted file mode 100644
index 136027736c3..00000000000
--- a/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Graphql::Loaders::PipelineForShaLoader do
- include GraphqlHelpers
-
- describe '#find_last' do
- it 'batch-resolves latest pipeline' do
- project = create(:project, :repository)
- pipeline1 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
- pipeline2 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
- pipeline3 = create(:ci_pipeline, project: project, ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
-
- result = batch_sync(max_queries: 1) do
- [pipeline1.sha, pipeline3.sha].map { |sha| described_class.new(project, sha).find_last }
- end
-
- expect(result).to contain_exactly(pipeline2, pipeline3)
- end
- end
-end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index b86663fd7d9..0f7f68e0b38 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -378,6 +378,14 @@ describe MergeRequestDiff do
expect(diff_with_commits.commit_shas).not_to be_empty
expect(diff_with_commits.commit_shas).to all(match(/\h{40}/))
end
+
+ context 'with limit attribute' do
+ it 'returns limited number of shas' do
+ expect(diff_with_commits.commit_shas(limit: 2).size).to eq(2)
+ expect(diff_with_commits.commit_shas(limit: 100).size).to eq(29)
+ expect(diff_with_commits.commit_shas.size).to eq(29)
+ end
+ end
end
describe '#compare_with' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f775dfb87a2..b19f7a80d63 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1261,13 +1261,49 @@ describe MergeRequest do
end
describe '#commit_shas' do
- before do
- allow(subject.merge_request_diff).to receive(:commit_shas)
- .and_return(['sha1'])
+ context 'persisted merge request' do
+ context 'with a limit' do
+ it 'returns a limited number of commit shas' do
+ expect(subject.commit_shas(limit: 2)).to eq(%w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
+ ])
+ end
+ end
+
+ context 'without a limit' do
+ it 'returns all commit shas of the merge request diff' do
+ expect(subject.commit_shas.size).to eq(29)
+ end
+ end
end
- it 'delegates to merge request diff' do
- expect(subject.commit_shas).to eq ['sha1']
+ context 'new merge request' do
+ subject { build(:merge_request) }
+
+ context 'compare commits' do
+ before do
+ subject.compare_commits = [
+ double(sha: 'sha1'), double(sha: 'sha2')
+ ]
+ end
+
+ context 'without a limit' do
+ it 'returns all shas of compare commits' do
+ expect(subject.commit_shas).to eq(%w[sha2 sha1])
+ end
+ end
+
+ context 'with a limit' do
+ it 'returns a limited number of shas' do
+ expect(subject.commit_shas(limit: 1)).to eq(['sha2'])
+ end
+ end
+ end
+
+ it 'returns diff_head_sha as an array' do
+ expect(subject.commit_shas).to eq([subject.diff_head_sha])
+ expect(subject.commit_shas(limit: 2)).to eq([subject.diff_head_sha])
+ end
end
end
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
new file mode 100644
index 00000000000..ac4853e5388
--- /dev/null
+++ b/spec/requests/api/group_export_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::GroupExport do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:path) { "/groups/#{group.id}/export" }
+ let(:download_path) { "/groups/#{group.id}/export/download" }
+
+ let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport) do |import_export|
+ expect(import_export).to receive(:storage_path).and_return(export_path)
+ end
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ describe 'GET /groups/:group_id/export/download' do
+ let(:upload) { ImportExportUpload.new(group: group) }
+
+ before do
+ stub_uploads_object_storage(ImportExportUploader)
+
+ group.add_owner(user)
+ end
+
+ context 'when export file exists' do
+ before do
+ upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
+ upload.save!
+ end
+
+ it 'downloads exported group archive' do
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ context 'when export_file.file does not exist' do
+ before do
+ expect_next_instance_of(ImportExportUploader) do |uploader|
+ expect(uploader).to receive(:file).and_return(nil)
+ end
+ end
+
+ it 'returns 404' do
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when export file does not exist' do
+ it 'returns 404' do
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /groups/:group_id/export' do
+ context 'when user is a group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'accepts download' do
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(202)
+ end
+ end
+
+ context 'when user is not a group owner' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'forbids the request' do
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/access_matchers_helpers.rb b/spec/support/helpers/access_matchers_helpers.rb
new file mode 100644
index 00000000000..9100f245d36
--- /dev/null
+++ b/spec/support/helpers/access_matchers_helpers.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module AccessMatchersHelpers
+ USER_ACCESSOR_METHOD_NAME = 'user'
+
+ def provide_user(role, membership = nil)
+ case role
+ when :admin
+ create(:admin)
+ when :auditor
+ create(:user, :auditor)
+ when :user
+ create(:user)
+ when :external
+ create(:user, :external)
+ when :visitor, :anonymous
+ nil
+ when User
+ role
+ when *Gitlab::Access.sym_options_with_owner.keys # owner, maintainer, developer, reporter, guest
+ raise ArgumentError, "cannot provide #{role} when membership reference is blank" unless membership
+
+ provide_user_by_membership(role, membership)
+ else
+ raise ArgumentError, "cannot provide user of an unknown role #{role}"
+ end
+ end
+
+ def provide_user_by_membership(role, membership)
+ if role == :owner && membership.owner
+ membership.owner
+ else
+ create(:user).tap do |user|
+ membership.public_send(:"add_#{role}", user)
+ end
+ end
+ end
+
+ def raise_if_non_block_expectation!(actual)
+ raise ArgumentError, 'This matcher supports block expectations only.' unless actual.is_a?(Proc)
+ end
+
+ def update_owner(objects, user)
+ return unless objects
+
+ objects.each do |object|
+ if object.respond_to?(:owner)
+ object.update_attribute(:owner, user)
+ elsif object.respond_to?(:user)
+ object.update_attribute(:user, user)
+ else
+ raise ArgumentError, "cannot own this object #{object}"
+ end
+ end
+ end
+
+ def patch_example_group(user)
+ return if user.nil? # for anonymous users
+
+ # This call is evaluated in context of ExampleGroup instance in which the matcher is called. Overrides the `user`
+ # (or defined by `method_name`) method generated by `let` definition in example group before it's used by `subject`.
+ # This override is per concrete example only because the example group class gets re-created for each example.
+ instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
+ if instance_variable_get(:@__#{USER_ACCESSOR_METHOD_NAME}_patched)
+ raise ArgumentError, 'An access matcher be_allowed_for/be_denied_for can be used only once per example (`it` block)'
+ end
+ instance_variable_set(:@__#{USER_ACCESSOR_METHOD_NAME}_patched, true)
+
+ def #{USER_ACCESSOR_METHOD_NAME}
+ @#{USER_ACCESSOR_METHOD_NAME} ||= User.find(#{user.id})
+ end
+ CODE
+ end
+
+ def prepare_matcher_environment(role, membership, owned_objects)
+ user = provide_user(role, membership)
+
+ if user
+ update_owner(owned_objects, user)
+ patch_example_group(user)
+ end
+ end
+
+ def run_matcher(action, role, membership, owned_objects)
+ raise_if_non_block_expectation!(action)
+
+ prepare_matcher_environment(role, membership, owned_objects)
+
+ if block_given?
+ yield action
+ else
+ action.call
+ end
+ end
+end
diff --git a/spec/support/matchers/access_matchers_for_request.rb b/spec/support/matchers/access_matchers_for_request.rb
new file mode 100644
index 00000000000..9b80bf8562c
--- /dev/null
+++ b/spec/support/matchers/access_matchers_for_request.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+# AccessMatchersForRequest
+#
+# Matchers to test the access permissions for requests specs (most useful for API tests).
+module AccessMatchersForRequest
+ extend RSpec::Matchers::DSL
+ include AccessMatchersHelpers
+
+ EXPECTED_STATUS_CODES_ALLOWED = [200, 201, 204, 302, 304].freeze
+ EXPECTED_STATUS_CODES_DENIED = [401, 403, 404].freeze
+
+ def description_for(role, type, expected, result)
+ "be #{type} for #{role} role. Expected status code: any of #{expected.join(', ')} Got: #{result}"
+ end
+
+ matcher :be_allowed_for do |role|
+ match do |action|
+ # methods called in this and negated block are being run in context of ExampleGroup
+ # (not matcher) instance so we have to pass data via local vars
+
+ run_matcher(action, role, @membership, @owned_objects)
+
+ EXPECTED_STATUS_CODES_ALLOWED.include?(response.status)
+ end
+
+ match_when_negated do |action|
+ run_matcher(action, role, @membership, @owned_objects)
+
+ EXPECTED_STATUS_CODES_DENIED.include?(response.status)
+ end
+
+ chain :of do |membership|
+ @membership = membership
+ end
+
+ chain :own do |*owned_objects|
+ @owned_objects = owned_objects
+ end
+
+ failure_message do
+ "expected this action to #{description_for(role, 'allowed', EXPECTED_STATUS_CODES_ALLOWED, response.status)}"
+ end
+
+ failure_message_when_negated do
+ "expected this action to #{description_for(role, 'denied', EXPECTED_STATUS_CODES_DENIED, response.status)}"
+ end
+
+ supports_block_expectations
+ end
+
+ RSpec::Matchers.define_negated_matcher :be_denied_for, :be_allowed_for
+end
diff --git a/spec/support/matchers/access_matchers_generic.rb b/spec/support/matchers/access_matchers_generic.rb
new file mode 100644
index 00000000000..13955750f4f
--- /dev/null
+++ b/spec/support/matchers/access_matchers_generic.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# AccessMatchersGeneric
+#
+# Matchers to test the access permissions for service classes or other generic pieces of business logic.
+module AccessMatchersGeneric
+ extend RSpec::Matchers::DSL
+ include AccessMatchersHelpers
+
+ ERROR_CLASS = Gitlab::Access::AccessDeniedError
+
+ def error_message(error)
+ str = error.class.name
+ str += ": #{error.message}" if error.message != error.class.name
+ str
+ end
+
+ def error_expectation_message(allowed, error)
+ if allowed
+ "Expected to raise nothing but #{error_message(error)} was raised."
+ else
+ "Expected to raise #{ERROR_CLASS} but nothing was raised."
+ end
+ end
+
+ def description_for(role, type, error)
+ allowed = type == 'allowed'
+ "be #{type} for #{role} role. #{error_expectation_message(allowed, error)}"
+ end
+
+ matcher :be_allowed_for do |role|
+ match do |action|
+ # methods called in this and negated block are being run in context of ExampleGroup
+ # (not matcher) instance so we have to pass data via local vars
+
+ run_matcher(action, role, @membership, @owned_objects) do |action|
+ action.call
+ rescue => e
+ @error = e
+ raise unless e.is_a?(ERROR_CLASS)
+ end
+
+ @error.nil?
+ end
+
+ chain :of do |membership|
+ @membership = membership
+ end
+
+ chain :own do |*owned_objects|
+ @owned_objects = owned_objects
+ end
+
+ failure_message do
+ "expected this action to #{description_for(role, 'allowed', @error)}"
+ end
+
+ failure_message_when_negated do
+ "expected this action to #{description_for(role, 'denied', @error)}"
+ end
+
+ supports_block_expectations
+ end
+
+ RSpec::Matchers.define_negated_matcher :be_denied_for, :be_allowed_for
+end