Commit 3007979c authored by David Fernandez's avatar David Fernandez Committed by Etienne Baqué

Refactor NPM requests specs for shared endpoints

Use tabled based specs to better test different conditions.
Make sure that we use the Bearer http authorization header and not the
request parameter.
Move package tags shared examples in a dedicated file for npm specs.
parent 0ebd272f
...@@ -6,7 +6,7 @@ RSpec.describe API::NpmInstancePackages do ...@@ -6,7 +6,7 @@ RSpec.describe API::NpmInstancePackages do
include_context 'npm api setup' include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do describe 'GET /api/v4/packages/npm/*package_name' do
it_behaves_like 'handling get metadata requests' do it_behaves_like 'handling get metadata requests', scope: :instance do
let(:url) { api("/packages/npm/#{package_name}") } let(:url) { api("/packages/npm/#{package_name}") }
end end
end end
......
...@@ -6,25 +6,25 @@ RSpec.describe API::NpmProjectPackages do ...@@ -6,25 +6,25 @@ RSpec.describe API::NpmProjectPackages do
include_context 'npm api setup' include_context 'npm api setup'
describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do
it_behaves_like 'handling get metadata requests' do it_behaves_like 'handling get metadata requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") } let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
end end
end end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
it_behaves_like 'handling get dist tags requests' do it_behaves_like 'handling get dist tags requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") } let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") }
end end
end end
describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling create dist tag requests' do it_behaves_like 'handling create dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end end
end end
describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests' do it_behaves_like 'handling delete dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end end
end end
...@@ -32,10 +32,14 @@ RSpec.describe API::NpmProjectPackages do ...@@ -32,10 +32,14 @@ RSpec.describe API::NpmProjectPackages do
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
let_it_be(:package_file) { package.package_files.first } let_it_be(:package_file) { package.package_files.first }
let(:params) { {} } let(:headers) { {} }
let(:url) { api("/projects/#{project.id}/packages/npm/#{package_file.package.name}/-/#{package_file.file_name}") } let(:url) { api("/projects/#{project.id}/packages/npm/#{package.name}/-/#{package_file.file_name}") }
subject { get(url, params: params) } subject { get(url, headers: headers) }
before do
project.add_developer(user)
end
shared_examples 'a package file that requires auth' do shared_examples 'a package file that requires auth' do
it 'denies download with no token' do it 'denies download with no token' do
...@@ -45,7 +49,7 @@ RSpec.describe API::NpmProjectPackages do ...@@ -45,7 +49,7 @@ RSpec.describe API::NpmProjectPackages do
end end
context 'with access token' do context 'with access token' do
let(:params) { { access_token: token.token } } let(:headers) { build_token_auth_header(token.token) }
it 'returns the file' do it 'returns the file' do
subject subject
...@@ -56,7 +60,7 @@ RSpec.describe API::NpmProjectPackages do ...@@ -56,7 +60,7 @@ RSpec.describe API::NpmProjectPackages do
end end
context 'with job token' do context 'with job token' do
let(:params) { { job_token: job.token } } let(:headers) { build_token_auth_header(job.token) }
it 'returns the file' do it 'returns the file' do
subject subject
...@@ -86,7 +90,7 @@ RSpec.describe API::NpmProjectPackages do ...@@ -86,7 +90,7 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'a package file that requires auth' it_behaves_like 'a package file that requires auth'
context 'with guest' do context 'with guest' do
let(:params) { { access_token: token.token } } let(:headers) { build_token_auth_header(token.token) }
it 'denies download when not enough permissions' do it 'denies download when not enough permissions' do
project.add_guest(user) project.add_guest(user)
...@@ -108,7 +112,11 @@ RSpec.describe API::NpmProjectPackages do ...@@ -108,7 +112,11 @@ RSpec.describe API::NpmProjectPackages do
end end
describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
RSpec.shared_examples 'handling invalid record with 400 error' do before do
project.add_developer(user)
end
shared_examples 'handling invalid record with 400 error' do
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
expect { upload_package_with_token(package_name, params) } expect { upload_package_with_token(package_name, params) }
.not_to change { project.packages.count } .not_to change { project.packages.count }
...@@ -261,7 +269,9 @@ RSpec.describe API::NpmProjectPackages do ...@@ -261,7 +269,9 @@ RSpec.describe API::NpmProjectPackages do
end end
def upload_package(package_name, params = {}) def upload_package(package_name, params = {})
put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params token = params.delete(:access_token) || params.delete(:job_token)
headers = build_token_auth_header(token)
put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers
end end
def upload_package_with_token(package_name, params = {}) def upload_package_with_token(package_name, params = {})
......
...@@ -4,10 +4,10 @@ RSpec.shared_context 'npm api setup' do ...@@ -4,10 +4,10 @@ RSpec.shared_context 'npm api setup' do
include PackagesManagerApiSpecHelpers include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers include HttpBasicAuthHelpers
let_it_be(:user) { create(:user) } let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
let_it_be(:package, reload: true) { create(:npm_package, project: project) } let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) } let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
...@@ -15,8 +15,15 @@ RSpec.shared_context 'npm api setup' do ...@@ -15,8 +15,15 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:package_name) { package.name } let(:package_name) { package.name }
end
before do RSpec.shared_context 'set package name from package name type' do
project.add_developer(user) let(:package_name) do
case package_name_type
when :scoped_naming_convention
"@#{group.path}/scoped-package"
when :non_existing
'non-existing-package'
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'handling get metadata requests' do RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
using RSpec::Parameterized::TableSyntax
let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) } let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) }
let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) } let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) }
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
let(:params) { {} }
let(:headers) { {} } let(:headers) { {} }
subject { get(url, params: params, headers: headers) } subject { get(url, headers: headers) }
shared_examples 'returning the npm package info' do shared_examples 'accept metadata request' do |status:|
it 'returns the package info' do it 'accepts the metadata request' do
subject subject
expect_a_valid_package_response expect(response).to have_gitlab_http_status(status)
expect(response.media_type).to eq('application/json')
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|
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 end
end end
shared_examples 'a package that requires auth' do shared_examples 'reject metadata request' do |status:|
it 'denies request without oauth token' do it 'rejects the metadata request' do
subject subject
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(status)
end end
end
context 'with oauth token' do shared_examples 'redirect metadata request' do |status:|
let(:params) { { access_token: token.token } } it 'redirects metadata request' do
subject
it 'returns the package info with oauth token' do
subject
expect_a_valid_package_response expect(response).to have_gitlab_http_status(:found)
end expect(response.headers['Location']).to eq("https://registry.npmjs.org/#{package_name}")
end end
end
context 'with job token' do where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
let(:params) { { job_token: job.token } } nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
it 'returns the package info with running job token' do nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
subject nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found
nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found
nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found
nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found
nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
:oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
:oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
:oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
:oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
:oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
:oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
:oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
:oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
:oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
:oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
:oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
:oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
:oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
:oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
:oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
:oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
:oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
:oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
:oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
:oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
:oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
:oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
:oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
:oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
:personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
:personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
:personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
:personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
:personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
:personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
:personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
:personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
:personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
:personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
:personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
:personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
:personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
:personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
:personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
:personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
:personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
:personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
:personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
:personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
:personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
:personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
:personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
:personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
:job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok
:job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok
:job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected
:job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found
:job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok
:job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok
:job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected
:job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found
:job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok
:job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok
:job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected
:job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found
:deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
:deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
:deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
:deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
:deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok
:deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok
:deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
:deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
:deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok
:deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok
:deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
:deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
end
expect_a_valid_package_response with_them do
include_context 'set package name from package name type'
let(:headers) do
case auth
when :oauth
build_token_auth_header(token.token)
when :personal_access_token
build_token_auth_header(personal_access_token.token)
when :job_token
build_token_auth_header(job.token)
when :deploy_token
build_token_auth_header(deploy_token.token)
else
{}
end end
end
it 'denies request without running job token' do before do
job.update!(status: :success) project.send("add_#{user_role}", user) if user_role
project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
package.update!(name: package_name) unless package_name == 'non-existing-package'
stub_application_setting(npm_package_requests_forwarding: request_forward)
end
subject example_name = "#{params[:expected_result]} metadata request"
status = params[:expected_status]
expect(response).to have_gitlab_http_status(:unauthorized) if scope == :instance && params[:package_name_type] != :scoped_naming_convention
if params[:request_forward]
example_name = 'redirect metadata request'
status = :redirected
else
example_name = 'reject metadata request'
status = :not_found
end end
end end
context 'with deploy token' do it_behaves_like example_name, status: status
let(:headers) { build_token_auth_header(deploy_token.token) } end
it 'returns the package info with deploy token' do context 'with a developer' do
subject let(:headers) { build_token_auth_header(personal_access_token.token) }
expect_a_valid_package_response before do
end project.add_developer(user)
end end
end
context 'a public project' do
it_behaves_like 'returning the npm package info'
context 'project path with a dot' do context 'project path with a dot' do
before do before do
project.update!(path: 'foo.bar') project.update!(path: 'foo.bar')
end end
it_behaves_like 'returning the npm package info' it_behaves_like 'accept metadata request', status: :ok
end end
context 'with request forward disabled' do context 'with a job token' do
let(:headers) { build_token_auth_header(job.token) }
before do before do
stub_application_setting(npm_package_requests_forwarding: false) job.update!(status: :success)
end end
it_behaves_like 'returning the npm package info' it_behaves_like 'reject metadata request', status: :unauthorized
end
end
end
RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
context 'with unknown package' do let_it_be(:package_tag1) { create(:packages_tag, package: package) }
let(:package_name) { 'unknown' } let_it_be(:package_tag2) { create(:packages_tag, package: package) }
it 'returns the proper response' do let(:headers) { {} }
subject
expect(response).to have_gitlab_http_status(:not_found) subject { get(url, headers: headers) }
end
end shared_examples 'reject package tags request' do |status:|
before do
package.update!(name: package_name) unless package_name == 'non-existing-package'
end end
context 'with request forward enabled' do it_behaves_like 'returning response status', status
before do end
stub_application_setting(npm_package_requests_forwarding: true)
end
it_behaves_like 'returning the npm package info' shared_examples 'handling different package names, visibilities and user roles' do
where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
:scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok
:scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok
:scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok
:non_existing | 'PUBLIC' | :anonymous | :reject | :not_found
:non_existing | 'PUBLIC' | :guest | :reject | :not_found
:non_existing | 'PUBLIC' | :reporter | :reject | :not_found
:scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
:scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
:scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok
:non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
:non_existing | 'PRIVATE' | :guest | :reject | :forbidden
:non_existing | 'PRIVATE' | :reporter | :reject | :not_found
:scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found
:scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok
:scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok
:non_existing | 'INTERNAL' | :anonymous | :reject | :not_found
:non_existing | 'INTERNAL' | :guest | :reject | :not_found
:non_existing | 'INTERNAL' | :reporter | :reject | :not_found
end
with_them do
let(:anonymous) { user_role == :anonymous }
context 'with unknown package' do subject { get(url, headers: anonymous ? {} : headers) }
let(:package_name) { 'unknown' }
it 'returns a redirect' do before do
subject project.send("add_#{user_role}", user) unless anonymous
project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
end
expect(response).to have_gitlab_http_status(:found) example_name = "#{params[:expected_result]} package tags request"
expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') status = params[:expected_status]
end
it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward' if scope == :instance && params[:package_name_type] != :scoped_naming_convention
example_name = 'reject package tags request'
status = :not_found
end end
it_behaves_like example_name, status: status
end end
end end
context 'internal project' do context 'with oauth token' do
before do let(:headers) { build_token_auth_header(token.token) }
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it_behaves_like 'a package that requires auth' it_behaves_like 'handling different package names, visibilities and user roles'
end end
context 'private project' do context 'with personal access token' do
before do let(:headers) { build_token_auth_header(personal_access_token.token) }
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it_behaves_like 'a package that requires auth' it_behaves_like 'handling different package names, visibilities and user roles'
end
end
context 'with guest' do RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
let(:params) { { access_token: token.token } } using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
it 'denies request when not enough permissions' do let_it_be(:tag_name) { 'test' }
project.add_guest(user)
subject let(:params) { {} }
let(:version) { package.version }
let(:env) { { 'api.request.body': version } }
let(:headers) { {} }
expect(response).to have_gitlab_http_status(:forbidden) shared_examples 'reject create package tag request' do |status:|
end before do
package.update!(name: package_name) unless package_name == 'non-existing-package'
end end
it_behaves_like 'returning response status', status
end end
def expect_a_valid_package_response shared_examples 'handling different package names, visibilities and user roles' do
expect(response).to have_gitlab_http_status(:ok) where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
expect(response.media_type).to eq('application/json') :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
expect(response).to match_response_schema('public_api/v4/packages/npm_package') :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
expect(json_response['name']).to eq(package.name) :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok
expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any :non_existing | 'PUBLIC' | :developer | :reject | :not_found
:scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
:scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
:scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok
:non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
:non_existing | 'PRIVATE' | :guest | :reject | :forbidden
:non_existing | 'PRIVATE' | :developer | :reject | :not_found
:scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden
:scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden
:scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok
:non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
:non_existing | 'INTERNAL' | :guest | :reject | :forbidden
:non_existing | 'INTERNAL' | :developer | :reject | :not_found
end end
expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
end
RSpec.shared_examples 'handling get dist tags requests' do with_them do
let_it_be(:package_tag1) { create(:packages_tag, package: package) } let(:anonymous) { user_role == :anonymous }
let_it_be(:package_tag2) { create(:packages_tag, package: package) }
let(:params) { {} } subject { put(url, env: env, headers: headers) }
subject { get(url, params: params) } before do
project.send("add_#{user_role}", user) unless anonymous
project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
end
context 'with public project' do example_name = "#{params[:expected_result]} create package tag request"
context 'with authenticated user' do status = params[:expected_status]
let(:params) { { private_token: personal_access_token.token } }
it_behaves_like 'returns package tags', :maintainer if scope == :instance && params[:package_name_type] != :scoped_naming_convention
it_behaves_like 'returns package tags', :developer example_name = 'reject create package tag request'
it_behaves_like 'returns package tags', :reporter status = :not_found
it_behaves_like 'returns package tags', :guest end
end
context 'with unauthenticated user' do it_behaves_like example_name, status: status
it_behaves_like 'returns package tags', :no_type
end end
end end
context 'with private project' do context 'with oauth token' do
before do let(:headers) { build_token_auth_header(token.token) }
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do it_behaves_like 'handling different package names, visibilities and user roles'
let(:params) { { private_token: personal_access_token.token } } end
it_behaves_like 'returns package tags', :maintainer context 'with personal access token' do
it_behaves_like 'returns package tags', :developer let(:headers) { build_token_auth_header(personal_access_token.token) }
it_behaves_like 'returns package tags', :reporter
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do it_behaves_like 'handling different package names, visibilities and user roles'
it_behaves_like 'rejects package tags access', :no_type, :not_found
end
end end
end end
RSpec.shared_examples 'handling create dist tag requests' do RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
let_it_be(:tag_name) { 'test' } using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
let(:params) { {} } let_it_be(:package_tag) { create(:packages_tag, package: package) }
let(:env) { {} }
let(:version) { package.version }
subject { put(url, env: env, params: params) }
context 'with public project' do let(:tag_name) { package_tag.name }
context 'with authenticated user' do let(:headers) { {} }
let(:params) { { private_token: personal_access_token.token } }
let(:env) { { 'api.request.body': version } }
it_behaves_like 'create package tag', :maintainer shared_examples 'reject delete package tag request' do |status:|
it_behaves_like 'create package tag', :developer before do
it_behaves_like 'rejects package tags access', :reporter, :forbidden package.update!(name: package_name) unless package_name == 'non-existing-package'
it_behaves_like 'rejects package tags access', :guest, :forbidden
end end
context 'with unauthenticated user' do it_behaves_like 'returning response status', status
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end end
end
RSpec.shared_examples 'handling delete dist tag requests' do shared_examples 'handling different package names, visibilities and user roles' do
let_it_be(:package_tag) { create(:packages_tag, package: package) } where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
:scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
:scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
:scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok
:non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
:non_existing | 'PUBLIC' | :guest | :reject | :forbidden
:non_existing | 'PUBLIC' | :maintainer | :reject | :not_found
:scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
:scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
:scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok
:non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
:non_existing | 'INTERNAL' | :guest | :reject | :forbidden
:non_existing | 'INTERNAL' | :maintainer | :reject | :not_found
end
let(:params) { {} } with_them do
let(:tag_name) { package_tag.name } let(:anonymous) { user_role == :anonymous }
subject { delete(url, headers: headers) }
subject { delete(url, params: params) } before do
project.send("add_#{user_role}", user) unless anonymous
project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
end
context 'with public project' do example_name = "#{params[:expected_result]} delete package tag request"
context 'with authenticated user' do status = params[:expected_status]
let(:params) { { private_token: personal_access_token.token } }
it_behaves_like 'delete package tag', :maintainer if scope == :instance && params[:package_name_type] != :scoped_naming_convention
it_behaves_like 'rejects package tags access', :developer, :forbidden example_name = 'reject delete package tag request'
it_behaves_like 'rejects package tags access', :reporter, :forbidden status = :not_found
it_behaves_like 'rejects package tags access', :guest, :forbidden end
end
context 'with unauthenticated user' do it_behaves_like example_name, status: status
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end end
end end
context 'with private project' do context 'with oauth token' do
before do let(:headers) { build_token_auth_header(token.token) }
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do it_behaves_like 'handling different package names, visibilities and user roles'
let(:params) { { private_token: personal_access_token.token } } end
it_behaves_like 'delete package tag', :maintainer context 'with personal access token' do
it_behaves_like 'rejects package tags access', :developer, :forbidden let(:headers) { build_token_auth_header(personal_access_token.token) }
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do it_behaves_like 'handling different package names, visibilities and user roles'
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'rejects package tags access' do |user_type, status| RSpec.shared_examples 'rejects package tags access' do |status:|
context "for user type #{user_type}" do before do
before do package.update!(name: package_name) unless package_name == 'non-existing-package'
project.send("add_#{user_type}", user) unless user_type == :no_type
end
it_behaves_like 'returning response status', status
end end
it_behaves_like 'returning response status', status
end end
RSpec.shared_examples 'returns package tags' do |user_type| RSpec.shared_examples 'accept package tags request' do |status:|
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
before do before do
stub_application_setting(npm_package_requests_forwarding: false) stub_application_setting(npm_package_requests_forwarding: false)
project.send("add_#{user_type}", user) unless user_type == :no_type
end end
it_behaves_like 'returning response status', :success context 'with valid package name' do
before do
package.update!(name: package_name) unless package_name == 'non-existing-package'
end
it 'returns a valid json response' do it_behaves_like 'returning response status', status
subject
expect(response.media_type).to eq('application/json') it 'returns a valid json response' do
expect(json_response).to be_a(Hash) subject
end
it 'returns two package tags' do expect(response.media_type).to eq('application/json')
subject expect(json_response).to be_a(Hash)
end
expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags') it 'returns two package tags' do
expect(json_response.length).to eq(3) # two tags + latest (auto added) subject
expect(json_response[package_tag1.name]).to eq(package.version)
expect(json_response[package_tag2.name]).to eq(package.version) expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
expect(json_response['latest']).to eq(package.version) expect(json_response.length).to eq(3) # two tags + latest (auto added)
expect(json_response[package_tag1.name]).to eq(package.version)
expect(json_response[package_tag2.name]).to eq(package.version)
expect(json_response['latest']).to eq(package.version)
end
end end
context 'with invalid package name' do context 'with invalid package name' do
...@@ -49,47 +52,49 @@ RSpec.shared_examples 'returns package tags' do |user_type| ...@@ -49,47 +52,49 @@ RSpec.shared_examples 'returns package tags' do |user_type|
end end
end end
RSpec.shared_examples 'create package tag' do |user_type| RSpec.shared_examples 'accept create package tag request' do |user_type|
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
before do context 'with valid package name' do
project.send("add_#{user_type}", user) unless user_type == :no_type before do
end package.update!(name: package_name) unless package_name == 'non-existing-package'
end
it_behaves_like 'returning response status', :no_content it_behaves_like 'returning response status', :no_content
it 'creates the package tag' do it 'creates the package tag' do
expect { subject }.to change { Packages::Tag.count }.by(1) expect { subject }.to change { Packages::Tag.count }.by(1)
last_tag = Packages::Tag.last last_tag = Packages::Tag.last
expect(last_tag.name).to eq(tag_name) expect(last_tag.name).to eq(tag_name)
expect(last_tag.package).to eq(package) expect(last_tag.package).to eq(package)
end end
it 'returns a valid response' do it 'returns a valid response' do
subject subject
expect(response.body).to be_empty expect(response.body).to be_empty
end end
context 'with already existing tag' do context 'with already existing tag' do
let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') } let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) } let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
it_behaves_like 'returning response status', :no_content it_behaves_like 'returning response status', :no_content
it 'reuses existing tag' do it 'reuses existing tag' do
expect(package.tags).to be_empty expect(package.tags).to be_empty
expect(package2.tags).to eq([tag]) expect(package2.tags).to eq([tag])
expect { subject }.to not_change { Packages::Tag.count } expect { subject }.to not_change { Packages::Tag.count }
expect(package.reload.tags).to eq([tag]) expect(package.reload.tags).to eq([tag])
expect(package2.reload.tags).to be_empty expect(package2.reload.tags).to be_empty
end end
it 'returns a valid response' do it 'returns a valid response' do
subject subject
expect(response.body).to be_empty expect(response.body).to be_empty
end
end end
end end
...@@ -129,14 +134,14 @@ RSpec.shared_examples 'create package tag' do |user_type| ...@@ -129,14 +134,14 @@ RSpec.shared_examples 'create package tag' do |user_type|
end end
end end
RSpec.shared_examples 'delete package tag' do |user_type| RSpec.shared_examples 'accept delete package tag request' do |user_type|
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
before do context 'with valid package name' do
project.send("add_#{user_type}", user) unless user_type == :no_type before do
end package.update!(name: package_name) unless package_name == 'non-existing-package'
end
context "for #{user_type} user" do
it_behaves_like 'returning response status', :no_content it_behaves_like 'returning response status', :no_content
it 'returns a valid response' do it 'returns a valid response' do
...@@ -157,29 +162,29 @@ RSpec.shared_examples 'delete package tag' do |user_type| ...@@ -157,29 +162,29 @@ RSpec.shared_examples 'delete package tag' do |user_type|
it_behaves_like 'returning response status', :not_found it_behaves_like 'returning response status', :not_found
end end
end
context 'with invalid package name' do context 'with invalid package name' do
where(:package_name, :status) do where(:package_name, :status) do
'unknown' | :not_found 'unknown' | :not_found
'' | :not_found '' | :not_found
'%20' | :bad_request '%20' | :bad_request
end end
with_them do with_them do
it_behaves_like 'returning response status', params[:status] it_behaves_like 'returning response status', params[:status]
end
end end
end
context 'with invalid tag name' do context 'with invalid tag name' do
where(:tag_name, :status) do where(:tag_name, :status) do
'unknown' | :not_found 'unknown' | :not_found
'' | :not_found '' | :not_found
'%20' | :bad_request '%20' | :bad_request
end end
with_them do with_them do
it_behaves_like 'returning response status', params[:status] it_behaves_like 'returning response status', params[:status]
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment