Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-23 21:11:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-23 21:11:07 +0300
commit5e9fe672fa0eda6322bd392e8502d1886804bd07 (patch)
treee2dbccd4f9b92ead855d87ff4fcffaff4fe9fb72 /spec
parenta7f478c9b1806a67ec9d991c3f54c242bb596f60 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/packages.rb2
-rw-r--r--spec/finders/packages/npm/package_finder_spec.rb92
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js58
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js23
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb139
-rw-r--r--spec/requests/api/issues/issues_spec.rb47
-rw-r--r--spec/rubocop/cop/performance/active_record_subtransactions_spec.rb62
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb15
-rw-r--r--spec/views/groups/group_members/index.html.haml_spec.rb68
-rw-r--r--spec/views/projects/project_members/index.html.haml_spec.rb96
10 files changed, 501 insertions, 101 deletions
diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb
index cd9c8a8bfbb..d4993cc765a 100644
--- a/spec/factories/packages.rb
+++ b/spec/factories/packages.rb
@@ -112,7 +112,7 @@ FactoryBot.define do
factory :npm_package do
sequence(:name) { |n| "@#{project.root_namespace.path}/package-#{n}"}
- version { '1.0.0' }
+ sequence(:version) { |n| "1.0.#{n}" }
package_type { :npm }
after :create do |package|
diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb
index a995f3b96c4..99174e2dd3a 100644
--- a/spec/finders/packages/npm/package_finder_spec.rb
+++ b/spec/finders/packages/npm/package_finder_spec.rb
@@ -68,6 +68,20 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to be_empty }
end
+
+ context 'with npm_presenter_queries_tuning disabled' do
+ before do
+ stub_feature_flags(npm_presenter_queries_tuning: false)
+ end
+
+ it_behaves_like 'finding packages by name'
+
+ context 'set to nil' do
+ let(:project) { nil }
+
+ it { is_expected.to be_empty }
+ end
+ end
end
context 'with a namespace' do
@@ -80,6 +94,20 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to be_empty }
end
+
+ context 'with npm_presenter_queries_tuning disabled' do
+ before do
+ stub_feature_flags(npm_presenter_queries_tuning: false)
+ end
+
+ it_behaves_like 'accepting a namespace for', 'finding packages by name'
+
+ context 'set to nil' do
+ let_it_be(:namespace) { nil }
+
+ it { is_expected.to be_empty }
+ end
+ end
end
end
@@ -109,6 +137,24 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it_behaves_like 'accepting a namespace for', 'finding packages by version'
end
+
+ context 'with npm_presenter_queries_tuning disabled' do
+ before do
+ stub_feature_flags(npm_presenter_queries_tuning: false)
+ end
+
+ context 'with a project' do
+ let(:finder) { described_class.new(package_name, project: project) }
+
+ it_behaves_like 'finding packages by version'
+ end
+
+ context 'with a namespace' do
+ let(:finder) { described_class.new(package_name, namespace: namespace) }
+
+ it_behaves_like 'accepting a namespace for', 'finding packages by version'
+ end
+ end
end
describe '#last' do
@@ -118,31 +164,43 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to eq(package) }
end
- context 'with a project' do
- let(:finder) { described_class.new(package_name, project: project) }
+ shared_examples 'handling project or namespace parameter' do
+ context 'with a project' do
+ let(:finder) { described_class.new(package_name, project: project) }
- it_behaves_like 'finding package by last'
- end
+ it_behaves_like 'finding package by last'
+ end
- context 'with a namespace' do
- let(:finder) { described_class.new(package_name, namespace: namespace) }
+ context 'with a namespace' do
+ let(:finder) { described_class.new(package_name, namespace: namespace) }
- it_behaves_like 'accepting a namespace for', 'finding package by last'
+ it_behaves_like 'accepting a namespace for', 'finding package by last'
- context 'with duplicate packages' do
- let_it_be(:namespace) { create(:group) }
- let_it_be(:subgroup1) { create(:group, parent: namespace) }
- let_it_be(:subgroup2) { create(:group, parent: namespace) }
- let_it_be(:project2) { create(:project, namespace: subgroup2) }
- let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
+ context 'with duplicate packages' do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:subgroup1) { create(:group, parent: namespace) }
+ let_it_be(:subgroup2) { create(:group, parent: namespace) }
+ let_it_be(:project2) { create(:project, namespace: subgroup2) }
+ let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
- before do
- project.update!(namespace: subgroup1)
+ before do
+ project.update!(namespace: subgroup1)
+ end
+
+ # the most recent one is returned
+ it { is_expected.to eq(package2) }
end
+ end
+ end
- # the most recent one is returned
- it { is_expected.to eq(package2) }
+ it_behaves_like 'handling project or namespace parameter'
+
+ context 'with npm_presenter_queries_tuning disabled' do
+ before do
+ stub_feature_flags(npm_presenter_queries_tuning: false)
end
+
+ it_behaves_like 'handling project or namespace parameter'
end
end
end
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 24b3779057c..868473faa14 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -115,6 +115,64 @@ describe('markdownSerializer', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
+ it('correctly serializes a link', () => {
+ expect(serialize(paragraph(link({ href: 'https://example.com' }, 'example url')))).toBe(
+ '[example url](https://example.com)',
+ );
+ });
+
+ it('correctly serializes a link with a title', () => {
+ expect(
+ serialize(
+ paragraph(link({ href: 'https://example.com', title: 'click this link' }, 'example url')),
+ ),
+ ).toBe('[example url](https://example.com "click this link")');
+ });
+
+ it('correctly serializes a link with a canonicalSrc', () => {
+ expect(
+ serialize(
+ paragraph(
+ link(
+ {
+ href: '/uploads/abcde/file.zip',
+ canonicalSrc: 'file.zip',
+ title: 'click here to download',
+ },
+ 'download file',
+ ),
+ ),
+ ),
+ ).toBe('[download file](file.zip "click here to download")');
+ });
+
+ it('correctly serializes an image', () => {
+ expect(serialize(paragraph(image({ src: 'img.jpg', alt: 'foo bar' })))).toBe(
+ '![foo bar](img.jpg)',
+ );
+ });
+
+ it('correctly serializes an image with a title', () => {
+ expect(serialize(paragraph(image({ src: 'img.jpg', title: 'baz', alt: 'foo bar' })))).toBe(
+ '![foo bar](img.jpg "baz")',
+ );
+ });
+
+ it('correctly serializes an image with a canonicalSrc', () => {
+ expect(
+ serialize(
+ paragraph(
+ image({
+ src: '/uploads/abcde/file.png',
+ alt: 'this is an image',
+ canonicalSrc: 'file.png',
+ title: 'foo bar baz',
+ }),
+ ),
+ ),
+ ).toBe('![this is an image](file.png "foo bar baz")');
+ });
+
it('correctly serializes a table with inline content', () => {
expect(
serialize(
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index c8ac7ffc9d9..6f186ba3227 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -645,29 +645,6 @@ describe('URL utility', () => {
});
});
- describe('urlParamsToObject', () => {
- it('parses path for label with trailing +', () => {
- // eslint-disable-next-line import/no-deprecated
- expect(urlUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
- label_name: ['label+'],
- });
- });
-
- it('parses path for milestone with trailing +', () => {
- // eslint-disable-next-line import/no-deprecated
- expect(urlUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
- milestone_title: 'A+',
- });
- });
-
- it('parses path for search terms with spaces', () => {
- // eslint-disable-next-line import/no-deprecated
- expect(urlUtils.urlParamsToObject('search=two+words', {})).toEqual({
- search: 'two words',
- });
- });
- });
-
describe('queryToObject', () => {
it.each`
case | query | options | result
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index e524edaadc6..a8ecc809a77 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -5,62 +5,131 @@ require 'spec_helper'
RSpec.describe ::Packages::Npm::PackagePresenter do
let_it_be(:project) { create(:project) }
let_it_be(:package_name) { "@#{project.root_namespace.path}/test" }
+ let_it_be(:package1) { create(:npm_package, version: '2.0.4', project: project, name: package_name) }
+ let_it_be(:package2) { create(:npm_package, version: '2.0.6', project: project, name: package_name) }
+ let_it_be(:latest_package) { create(:npm_package, version: '2.0.11', project: project, name: package_name) }
- let!(:package1) { create(:npm_package, version: '1.0.4', project: project, name: package_name) }
- let!(:package2) { create(:npm_package, version: '1.0.6', project: project, name: package_name) }
- let!(:latest_package) { create(:npm_package, version: '1.0.11', project: project, name: package_name) }
let(:packages) { project.packages.npm.with_name(package_name).last_of_each_version }
let(:presenter) { described_class.new(package_name, packages) }
describe '#versions' do
subject { presenter.versions }
- context 'for packages without dependencies' do
- it { is_expected.to be_a(Hash) }
- it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
+ shared_examples 'returning packages versions' do |expect_n_plus_one: false|
+ context 'for packages without dependencies' do
+ it { is_expected.to be_a(Hash) }
+ it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
+ it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- described_class::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- it { expect(subject.dig(package1.version, dependency_type)).to be nil }
- it { expect(subject.dig(package2.version, dependency_type)).to be nil }
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ it { expect(subject.dig(package1.version, dependency_type)).to be nil }
+ it { expect(subject.dig(package2.version, dependency_type)).to be nil }
+ end
+
+ it 'avoids N+1 database queries' do
+ check_n_plus_one(:versions, expect_it: expect_n_plus_one) do
+ create_list(:npm_package, 5, project: project, name: package_name)
+ end
+ end
end
- end
- context 'for packages with dependencies' do
- described_class::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- let!("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) }
+ context 'for packages with dependencies' do
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) }
+ end
+
+ it { is_expected.to be_a(Hash) }
+ it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
+ it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any }
+ end
+
+ it 'avoids N+1 database queries' do
+ check_n_plus_one(:versions, expect_it: expect_n_plus_one) do
+ create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package|
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
+ end
+ end
+ end
+ end
end
+ end
+
+ it_behaves_like 'returning packages versions'
- it { is_expected.to be_a(Hash) }
- it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- described_class::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any }
+ context 'with npm_presenter_queries_tuning disabled' do
+ before do
+ stub_feature_flags(npm_presenter_queries_tuning: false)
end
+
+ it_behaves_like 'returning packages versions', expect_n_plus_one: true
end
end
describe '#dist_tags' do
subject { presenter.dist_tags }
- context 'for packages without tags' do
- it { is_expected.to be_a(Hash) }
- it { expect(subject["latest"]).to eq(latest_package.version) }
+ shared_examples 'returning packages tags' do
+ context 'for packages without tags' do
+ it { is_expected.to be_a(Hash) }
+ it { expect(subject["latest"]).to eq(latest_package.version) }
+
+ it 'avoids N+1 database queries' do
+ check_n_plus_one(:dist_tags) do
+ create_list(:npm_package, 5, project: project, name: package_name)
+ end
+ end
+ end
+
+ context 'for packages with tags' do
+ let_it_be(:package_tag1) { create(:packages_tag, package: package1, name: 'release_a') }
+ let_it_be(:package_tag2) { create(:packages_tag, package: package1, name: 'test_release') }
+ let_it_be(:package_tag3) { create(:packages_tag, package: package2, name: 'release_b') }
+ let_it_be(:package_tag4) { create(:packages_tag, package: latest_package, name: 'release_c') }
+ let_it_be(:package_tag5) { create(:packages_tag, package: latest_package, name: 'latest') }
+
+ it { is_expected.to be_a(Hash) }
+ it { expect(subject[package_tag1.name]).to eq(package1.version) }
+ it { expect(subject[package_tag2.name]).to eq(package1.version) }
+ it { expect(subject[package_tag3.name]).to eq(package2.version) }
+ it { expect(subject[package_tag4.name]).to eq(latest_package.version) }
+ it { expect(subject[package_tag5.name]).to eq(latest_package.version) }
+
+ it 'avoids N+1 database queries' do
+ check_n_plus_one(:dist_tags) do
+ create_list(:npm_package, 5, project: project, name: package_name).each_with_index do |npm_package, index|
+ create(:packages_tag, package: npm_package, name: "tag_#{index}")
+ end
+ end
+ end
+ end
end
- context 'for packages with tags' do
- let!(:package_tag1) { create(:packages_tag, package: package1, name: 'release_a') }
- let!(:package_tag2) { create(:packages_tag, package: package1, name: 'test_release') }
- let!(:package_tag3) { create(:packages_tag, package: package2, name: 'release_b') }
- let!(:package_tag4) { create(:packages_tag, package: latest_package, name: 'release_c') }
- let!(:package_tag5) { create(:packages_tag, package: latest_package, name: 'latest') }
-
- it { is_expected.to be_a(Hash) }
- it { expect(subject[package_tag1.name]).to eq(package1.version) }
- it { expect(subject[package_tag2.name]).to eq(package1.version) }
- it { expect(subject[package_tag3.name]).to eq(package2.version) }
- it { expect(subject[package_tag4.name]).to eq(latest_package.version) }
- it { expect(subject[package_tag5.name]).to eq(latest_package.version) }
+ it_behaves_like 'returning packages tags'
+
+ context 'with npm_presenter_queries_tuning disabled' do
+ before do
+ stub_feature_flags(npm_presenter_queries_tuning: false)
+ end
+
+ it_behaves_like 'returning packages tags'
+ end
+ end
+
+ def check_n_plus_one(field, expect_it: false)
+ pkgs = project.packages.npm.with_name(package_name).last_of_each_version.preload_files
+ control = ActiveRecord::QueryRecorder.new { described_class.new(package_name, pkgs).public_send(field) }
+
+ yield
+
+ pkgs = project.packages.npm.with_name(package_name).last_of_each_version.preload_files
+
+ if expect_it
+ expect { described_class.new(package_name, pkgs).public_send(field) }.to exceed_query_limit(control)
+ else
+ expect { described_class.new(package_name, pkgs).public_send(field) }.not_to exceed_query_limit(control)
end
end
end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 125db58ed69..4f13a72e40c 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -9,15 +9,17 @@ RSpec.describe API::Issues do
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:author) { create(:author) }
let_it_be(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
- let(:issue_title) { 'foo' }
- let(:issue_description) { 'closed' }
- let!(:closed_issue) do
+ let_it_be(:admin) { create(:user, :admin) }
+
+ let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) }
+
+ let_it_be(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
@@ -29,7 +31,7 @@ RSpec.describe API::Issues do
closed_at: 1.hour.ago
end
- let!(:confidential_issue) do
+ let_it_be(:confidential_issue) do
create :issue,
:confidential,
project: project,
@@ -39,7 +41,7 @@ RSpec.describe API::Issues do
updated_at: 2.hours.ago
end
- let!(:issue) do
+ let_it_be(:issue) do
create :issue,
author: user,
assignees: [user],
@@ -47,21 +49,16 @@ RSpec.describe API::Issues do
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
+ title: 'foo',
+ description: 'bar'
end
let_it_be(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
- let!(:label_link) { create(:label_link, label: label, target: issue) }
- let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let_it_be(:empty_milestone) do
- create(:milestone, title: '2.0.0', project: project)
- end
-
- let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+ let_it_be(:label_link) { create(:label_link, label: label, target: issue) }
+ let_it_be(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
@@ -711,8 +708,8 @@ RSpec.describe API::Issues do
milestone: milestone,
created_at: closed_issue.created_at,
updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
+ title: 'foo',
+ description: 'bar'
end
it 'page breaks first page correctly' do
@@ -1001,13 +998,15 @@ RSpec.describe API::Issues do
end
describe 'DELETE /projects/:id/issues/:issue_iid' do
+ let(:issue_for_deletion) { create(:issue, author: user, assignees: [user], project: project) }
+
it 'rejects a non member from deleting an issue' do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
+ delete api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", non_member)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'rejects a developer from deleting an issue' do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
+ delete api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", author)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -1016,13 +1015,13 @@ RSpec.describe API::Issues do
let(:project) { create(:project, namespace: owner.namespace) }
it 'deletes the issue if an admin requests it' do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
+ delete api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", owner)
expect(response).to have_gitlab_http_status(:no_content)
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
+ let(:request) { api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", owner) }
end
end
@@ -1035,7 +1034,7 @@ RSpec.describe API::Issues do
end
it 'returns 404 when using the issue ID instead of IID' do
- delete api("/projects/#{project.id}/issues/#{issue.id}", user)
+ delete api("/projects/#{project.id}/issues/#{issue_for_deletion.id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
new file mode 100644
index 00000000000..0da2e30062a
--- /dev/null
+++ b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../../rubocop/cop/performance/active_record_subtransactions'
+
+RSpec.describe RuboCop::Cop::Performance::ActiveRecordSubtransactions do
+ subject(:cop) { described_class.new }
+
+ let(:message) { described_class::MSG }
+
+ context 'when calling #transaction with only requires_new: true' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ ApplicationRecord.transaction(requires_new: true) do
+ ^^^^^^^^^^^^^^^^^^ #{message}
+ Project.create!(name: 'MyProject')
+ end
+ RUBY
+ end
+ end
+
+ context 'when passing multiple arguments to #transaction, including requires_new: true' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ ApplicationRecord.transaction(isolation: :read_committed, requires_new: true) do
+ ^^^^^^^^^^^^^^^^^^ #{message}
+ Project.create!(name: 'MyProject')
+ end
+ RUBY
+ end
+ end
+
+ context 'when calling #transaction with requires_new: false' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ ApplicationRecord.transaction(requires_new: false) do
+ Project.create!(name: 'MyProject')
+ end
+ RUBY
+ end
+ end
+
+ context 'when calling #transaction with other options' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ ApplicationRecord.transaction(isolation: :read_committed) do
+ Project.create!(name: 'MyProject')
+ end
+ RUBY
+ end
+ end
+
+ context 'when calling #transaction with no arguments' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ ApplicationRecord.transaction do
+ Project.create!(name: 'MyProject')
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index 0390e60747f..2af7b616659 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -21,11 +21,24 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
expect(response).to match_response_schema('public_api/v4/packages/npm_package')
expect(json_response['name']).to eq(package.name)
expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
- ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
end
expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
+
+ it 'avoids N+1 database queries' do
+ control = ActiveRecord::QueryRecorder.new { get(url, headers: headers) }
+
+ create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package|
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ create(:packages_dependency_link, package: package, dependency_type: dependency_type)
+ end
+ end
+
+ # query count can slightly change between the examples so we're using a custom threshold
+ expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4)
+ end
end
shared_examples 'reject metadata request' do |status:|
diff --git a/spec/views/groups/group_members/index.html.haml_spec.rb b/spec/views/groups/group_members/index.html.haml_spec.rb
new file mode 100644
index 00000000000..8e190c24495
--- /dev/null
+++ b/spec/views/groups/group_members/index.html.haml_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'groups/group_members/index', :aggregate_failures do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before do
+ allow(view).to receive(:group_members_app_data).and_return({})
+ allow(view).to receive(:current_user).and_return(user)
+ assign(:group, group)
+ assign(:group_member, build(:group_member, group: group))
+ end
+
+ context 'when user can invite members for the group' do
+ before do
+ group.add_owner(user)
+ end
+
+ context 'when modal is enabled' do
+ it 'renders as expected' do
+ render
+
+ expect(rendered).to have_content('Group members')
+ expect(rendered).to have_content('You can invite a new member')
+
+ expect(rendered).to have_selector('.js-invite-group-trigger')
+ expect(rendered).to have_selector('.js-invite-members-trigger')
+ expect(response).to render_template(partial: 'groups/_invite_members_modal')
+
+ expect(rendered).not_to have_selector('#invite-member-tab')
+ expect(rendered).not_to have_selector('#invite-group-tab')
+ expect(response).not_to render_template(partial: 'shared/members/_invite_group')
+ end
+ end
+
+ context 'when modal is not enabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'renders as expected' do
+ render
+
+ expect(rendered).to have_content('Group members')
+ expect(rendered).to have_content('You can invite a new member')
+
+ expect(rendered).to have_selector('#invite-member-tab')
+ expect(rendered).to have_selector('#invite-group-tab')
+ expect(response).to render_template(partial: 'shared/members/_invite_group')
+
+ expect(rendered).not_to have_selector('.js-invite-group-trigger')
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ expect(response).not_to render_template(partial: 'groups/_invite_members_modal')
+ end
+ end
+ end
+
+ context 'when user can not invite members for the group' do
+ it 'renders as expected', :aggregate_failures do
+ render
+
+ expect(rendered).not_to have_content('Group members')
+ expect(rendered).not_to have_content('You can invite a new member')
+ end
+ end
+end
diff --git a/spec/views/projects/project_members/index.html.haml_spec.rb b/spec/views/projects/project_members/index.html.haml_spec.rb
new file mode 100644
index 00000000000..ba255a474e2
--- /dev/null
+++ b/spec/views/projects/project_members/index.html.haml_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/project_members/index', :aggregate_failures do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source) { create(:project, :empty_repo) }
+ let_it_be(:project) { ProjectPresenter.new(source, current_user: user) }
+
+ before do
+ allow(view).to receive(:project_members_app_data_json).and_return({})
+ allow(view).to receive(:current_user).and_return(user)
+ assign(:project, project)
+ assign(:project_member, build(:project_member, project: source))
+ end
+
+ context 'when user can invite members for the project' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when modal is enabled' do
+ it 'renders as expected' do
+ render
+
+ expect(rendered).to have_content('Project members')
+ expect(rendered).to have_content('You can invite a new member')
+ expect(rendered).to have_link('Import a project')
+ expect(rendered).to have_selector('.js-invite-group-trigger')
+ expect(rendered).to have_selector('.js-invite-members-trigger')
+ expect(rendered).not_to have_content('Members can be added by project')
+ expect(response).to render_template(partial: 'projects/_invite_members_modal')
+ end
+
+ context 'when project is not allowed to share with group' do
+ before do
+ project.namespace.share_with_group_lock = true
+ end
+
+ it 'renders as expected' do
+ render
+
+ expect(rendered).not_to have_selector('.js-invite-group-trigger')
+ end
+ end
+ end
+
+ context 'when modal is not enabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'renders as expected' do
+ render
+
+ expect(rendered).to have_content('Project members')
+ expect(rendered).to have_content('You can invite a new member')
+ expect(rendered).not_to have_selector('.js-invite-group-trigger')
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ expect(rendered).not_to have_content('Members can be added by project')
+ expect(response).not_to render_template(partial: 'projects/_invite_members_modal')
+ expect(response).to render_template(partial: 'shared/members/_invite_member')
+ end
+
+ context 'when project can not be shared' do
+ before do
+ project.namespace.share_with_group_lock = true
+ end
+
+ it 'renders as expected' do
+ render
+
+ expect(rendered).to have_content('Project members')
+ expect(rendered).to have_content('You can invite a new member')
+ expect(response).not_to render_template(partial: 'projects/_invite_members_modal')
+ end
+ end
+ end
+ end
+
+ context 'when user can not invite members or group for the project' do
+ context 'when project can be shared' do
+ it 'renders as expected', :aggregate_failures do
+ render
+
+ expect(rendered).to have_content('Project members')
+ expect(rendered).not_to have_content('You can invite a new member')
+ expect(rendered).not_to have_link('Import a project')
+ expect(rendered).not_to have_selector('.js-invite-group-trigger')
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ expect(rendered).to have_content('Members can be added by project')
+ expect(response).not_to render_template(partial: 'projects/_invite_members_modal')
+ end
+ end
+ end
+end