gitlab-org--gitlab-foss/spec/requests/api/v3/projects_spec.rb
Yorick Peterse ac382b5682
Use CTEs for nested groups and authorizations
This commit introduces the usage of Common Table Expressions (CTEs) to
efficiently retrieve nested group hierarchies, without having to rely on
the "routes" table (which is an _incredibly_ inefficient way of getting
the data). This requires a patch to ActiveRecord (found in the added
initializer) to work properly as ActiveRecord doesn't support WITH
statements properly out of the box.

Unfortunately MySQL provides no efficient way of getting nested groups.
For example, the old routes setup could easily take 5-10 seconds
depending on the amount of "routes" in a database. Providing vastly
different logic for both MySQL and PostgreSQL will negatively impact the
development process. Because of this the various nested groups related
methods return empty relations when used in combination with MySQL.

For project authorizations the logic is split up into two classes:

* Gitlab::ProjectAuthorizations::WithNestedGroups
* Gitlab::ProjectAuthorizations::WithoutNestedGroups

Both classes get the fresh project authorizations (= as they should be
in the "project_authorizations" table), including nested groups if
PostgreSQL is used. The logic of these two classes is quite different
apart from their public interface. This complicates development a bit,
but unfortunately there is no way around this.

This commit also introduces Gitlab::GroupHierarchy. This class can be
used to get the ancestors and descendants of a base relation, or both by
using a UNION. This in turn is used by methods such as:

* Namespace#ancestors
* Namespace#descendants
* User#all_expanded_groups

Again this class relies on CTEs and thus only works on PostgreSQL. The
Namespace methods will return an empty relation when MySQL is used,
while User#all_expanded_groups will return only the groups a user is a
direct member of.

Performance wise the impact is quite large. For example, on GitLab.com
Namespace#descendants used to take around 580 ms to retrieve data for a
particular user. Using CTEs we are able to reduce this down to roughly 1
millisecond, returning the exact same data.

== On The Fly Refreshing

Refreshing of authorizations on the fly (= when
users.authorized_projects_populated was not set) is removed with this
commit. This simplifies the code, and ensures any queries used for
authorizations are not mutated because they are executed in a Rails
scope (e.g. Project.visible_to_user).

This commit includes a migration to schedule refreshing authorizations
for all users, ensuring all of them have their authorizations in place.
Said migration schedules users in batches of 5000, with 5 minutes
between every batch to smear the load around a bit.

== Spec Changes

This commit also introduces some changes to various specs. For example,
some specs for ProjectTeam assumed that creating a personal project
would _not_ lead to the owner having access, which is incorrect. Because
we also no longer refresh authorizations on the fly for new users some
code had to be added to the "empty_project" factory. This chunk of code
ensures that the owner's permissions are refreshed after creating the
project, something that is normally done in Projects::CreateService.
2017-05-17 16:51:08 +02:00

1446 lines
52 KiB
Ruby

require 'spec_helper'
describe API::V3::Projects do
include Gitlab::CurrentSettings
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
create(:project,
:private,
:repository,
name: 'second_project',
path: 'second_project',
creator_id: user.id,
namespace: user.namespace,
merge_requests_enabled: false,
issues_enabled: false, wiki_enabled: false,
snippets_enabled: false)
end
let(:project_member2) do
create(:project_member,
user: user4,
project: project3,
access_level: ProjectMember::MASTER)
end
let(:project4) do
create(:empty_project,
name: 'third_project',
path: 'third_project',
creator_id: user4.id,
namespace: user4.namespace)
end
describe 'GET /projects' do
before { project }
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api('/projects')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as regular user' do
it 'returns an array of projects' do
get v3_api('/projects', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project.name)
expect(json_response.first['owner']['username']).to eq(user.username)
end
it 'includes the project labels as the tag_list' do
get v3_api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('tag_list')
end
it 'includes open_issues_count' do
get v3_api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('open_issues_count')
end
it 'does not include open_issues_count' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get v3_api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).not_to include('open_issues_count')
end
context 'GET /projects?simple=true' do
it 'returns a simplified version of all the projects' do
expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
get v3_api('/projects?simple=true', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first.keys).to match_array expected_keys
end
end
context 'and using search' do
it 'returns searched project' do
get v3_api('/projects', user), { search: project.name }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
end
context 'and using the visibility filter' do
it 'filters based on private visibility param' do
get v3_api('/projects', user), { visibility: 'private' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
end
it 'filters based on internal visibility param' do
get v3_api('/projects', user), { visibility: 'internal' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
end
it 'filters based on public visibility param' do
get v3_api('/projects', user), { visibility: 'public' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
end
end
context 'and using sorting' do
before do
project2
project3
end
it 'returns the correct order when sorted by id' do
get v3_api('/projects', user), { order_by: 'id', sort: 'desc' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
end
end
end
end
describe 'GET /projects/all' do
before { project }
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api('/projects/all')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as regular user' do
it 'returns authentication error' do
get v3_api('/projects/all', user)
expect(response).to have_http_status(403)
end
end
context 'when authenticated as admin' do
it 'returns an array of all projects' do
get v3_api('/projects/all', admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to satisfy do |response|
response.one? do |entry|
entry.has_key?('permissions') &&
entry['name'] == project.name &&
entry['owner']['username'] == user.username
end
end
end
it "does not include statistics by default" do
get v3_api('/projects/all', admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it "includes statistics if requested" do
get v3_api('/projects/all', admin), statistics: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).to include 'statistics'
end
end
end
describe 'GET /projects/owned' do
before do
project3
project4
end
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api('/projects/owned')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as project owner' do
it 'returns an array of projects the user owns' do
get v3_api('/projects/owned', user4)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project4.name)
expect(json_response.first['owner']['username']).to eq(user4.username)
end
it "does not include statistics by default" do
get v3_api('/projects/owned', user4)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it "includes statistics if requested" do
attributes = {
commit_count: 23,
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
build_artifacts_size: 345
}
project4.statistics.update!(attributes)
get v3_api('/projects/owned', user4), statistics: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['statistics']).to eq attributes.stringify_keys
end
end
end
describe 'GET /projects/visible' do
shared_examples_for 'visible projects response' do
it 'returns the visible projects' do
get v3_api('/projects/visible', current_user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
end
end
let!(:public_project) { create(:empty_project, :public) }
before do
project
project2
project3
project4
end
context 'when unauthenticated' do
it_behaves_like 'visible projects response' do
let(:current_user) { nil }
let(:projects) { [public_project] }
end
end
context 'when authenticated' do
it_behaves_like 'visible projects response' do
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3] }
end
end
context 'when authenticated as a different user' do
it_behaves_like 'visible projects response' do
let(:current_user) { user2 }
let(:projects) { [public_project] }
end
end
end
describe 'GET /projects/starred' do
let(:public_project) { create(:empty_project, :public) }
before do
project_member
user3.update_attributes(starred_projects: [project, project2, project3, public_project])
end
it 'returns the starred projects viewable by the user' do
get v3_api('/projects/starred', user3)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
end
end
describe 'POST /projects' do
context 'maximum number of projects reached' do
it 'does not create new project and respond with 403' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
expect { post v3_api('/projects', user2), name: 'foo' }.
to change {Project.count}.by(0)
expect(response).to have_http_status(403)
end
end
it 'creates new project without path but with name and returns 201' do
expect { post v3_api('/projects', user), name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
project = Project.first
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-project')
end
it 'creates new project without name but with path and returns 201' do
expect { post v3_api('/projects', user), path: 'foo_project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
project = Project.first
expect(project.name).to eq('foo_project')
expect(project.path).to eq('foo_project')
end
it 'creates new project name and path and returns 201' do
expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
project = Project.first
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-Project')
end
it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post v3_api('/projects', user2), name: 'foo'
expect(response).to have_http_status(201)
end
it 'does not create new project without name or path and return 400' do
expect { post v3_api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
it "assigns attributes to project" do
project = attributes_for(:project, {
path: 'camelCasePath',
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
only_allow_merge_if_build_succeeds: false,
request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false
})
post v3_api('/projects', user), project
project.each_pair do |k, v|
next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
# Check feature permissions attributes
project = Project.find_by_path(project[:path])
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
end
it 'sets a project as public' do
project = attributes_for(:project, :public)
post v3_api('/projects', user), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
it 'sets a project as public using :public' do
project = attributes_for(:project, { public: true })
post v3_api('/projects', user), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post v3_api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
it 'sets a project as internal overriding :public' do
project = attributes_for(:project, :internal, { public: true })
post v3_api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
it 'sets a project as private' do
project = attributes_for(:project, :private)
post v3_api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'sets a project as private using :public' do
project = attributes_for(:project, { public: false })
post v3_api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
post v3_api('/projects', user), project
expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
end
it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
post v3_api('/projects', user), project
expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
end
it 'sets a project as allowing merge even if discussions are unresolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
post v3_api('/projects', user), project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
post v3_api('/projects', user), project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
post v3_api('/projects', user), project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end
context 'when a visibility level is restricted' do
before do
@project = attributes_for(:project, { public: true })
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'does not allow a non-admin to use a restricted visibility level' do
post v3_api('/projects', user), @project
expect(response).to have_http_status(400)
expect(json_response['message']['visibility_level'].first).to(
match('restricted by your GitLab administrator')
)
end
it 'allows an admin to override restricted visibility settings' do
post v3_api('/projects', admin), @project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to(
eq(Gitlab::VisibilityLevel::PUBLIC)
)
end
end
end
describe 'POST /projects/user/:id' do
before { project }
before { admin }
it 'should create new project without path and return 201' do
expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
end
it 'responds with 400 on failure and not project' do
expect { post v3_api("/projects/user/#{user.id}", admin) }.
not_to change { Project.count }
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
it 'assigns attributes to project' do
project = attributes_for(:project, {
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
request_access_enabled: true
})
post v3_api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
project.each_pair do |k, v|
next if %i[has_external_issue_tracker path].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
end
it 'sets a project as public' do
project = attributes_for(:project, :public)
post v3_api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
it 'sets a project as public using :public' do
project = attributes_for(:project, { public: true })
post v3_api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post v3_api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
it 'sets a project as internal overriding :public' do
project = attributes_for(:project, :internal, { public: true })
post v3_api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
it 'sets a project as private' do
project = attributes_for(:project, :private)
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'sets a project as private using :public' do
project = attributes_for(:project, { public: false })
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
end
it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
end
it 'sets a project as allowing merge even if discussions are unresolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end
end
describe "POST /projects/:id/uploads" do
before { project }
it "uploads the file and returns its info" do
post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
expect(response).to have_http_status(201)
expect(json_response['alt']).to eq("dk")
expect(json_response['url']).to start_with("/uploads/")
expect(json_response['url']).to end_with("/dk.png")
end
end
describe 'GET /projects/:id' do
context 'when unauthenticated' do
it 'returns the public projects' do
public_project = create(:empty_project, :public)
get v3_api("/projects/#{public_project.id}")
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
expect(json_response.keys).not_to include('permissions')
end
end
context 'when authenticated' do
before do
project
end
it 'returns a project by id' do
group = create(:group)
link = create(:project_group_link, project: project, group: group)
get v3_api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
expect(json_response['tag_list']).to be_an Array
expect(json_response['public']).to be_falsey
expect(json_response['archived']).to be_falsey
expect(json_response['visibility_level']).to be_present
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['owner']).to be_a Hash
expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present
expect(json_response['issues_enabled']).to be_present
expect(json_response['merge_requests_enabled']).to be_present
expect(json_response['wiki_enabled']).to be_present
expect(json_response['builds_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
expect(json_response['creator_id']).to be_present
expect(json_response['namespace']).to be_present
expect(json_response['avatar_url']).to be_nil
expect(json_response['star_count']).to be_present
expect(json_response['forks_count']).to be_present
expect(json_response['public_builds']).to be_present
expect(json_response['shared_with_groups']).to be_an Array
expect(json_response['shared_with_groups'].length).to eq(1)
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
end
it 'returns a project by path name' do
get v3_api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'returns a 404 error if not found' do
get v3_api('/projects/42', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get v3_api("/projects/#{project.id}", other_user)
expect(response).to have_http_status(404)
end
it 'handles users with dots' do
dot_user = create(:user, username: 'dot.user')
project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace)
get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'exposes namespace fields' do
get v3_api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
expect(json_response['namespace']).to eq({
'id' => user.namespace.id,
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
'full_path' => user.namespace.full_path
})
end
describe 'permissions' do
context 'all projects' do
before { project.team << [user, :master] }
it 'contains permission information' do
get v3_api("/projects", user)
expect(response).to have_http_status(200)
expect(json_response.first['permissions']['project_access']['access_level']).
to eq(Gitlab::Access::MASTER)
expect(json_response.first['permissions']['group_access']).to be_nil
end
end
context 'personal project' do
it 'sets project access and returns 200' do
project.team << [user, :master]
get v3_api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
expect(json_response['permissions']['project_access']['access_level']).
to eq(Gitlab::Access::MASTER)
expect(json_response['permissions']['group_access']).to be_nil
end
end
context 'group project' do
let(:project2) { create(:empty_project, group: create(:group)) }
before { project2.group.add_owner(user) }
it 'sets the owner and return 200' do
get v3_api("/projects/#{project2.id}", user)
expect(response).to have_http_status(200)
expect(json_response['permissions']['project_access']).to be_nil
expect(json_response['permissions']['group_access']['access_level']).
to eq(Gitlab::Access::OWNER)
end
end
end
end
end
describe 'GET /projects/:id/events' do
shared_examples_for 'project events response' do
it 'returns the project events' do
member = create(:user)
create(:project_member, :developer, user: member, project: project)
note = create(:note_on_issue, note: 'What an awesome day!', project: project)
EventCreateService.new.leave_note(note, note.author)
get v3_api("/projects/#{project.id}/events", current_user)
expect(response).to have_http_status(200)
first_event = json_response.first
expect(first_event['action_name']).to eq('commented on')
expect(first_event['note']['body']).to eq('What an awesome day!')
last_event = json_response.last
expect(last_event['action_name']).to eq('joined')
expect(last_event['project_id'].to_i).to eq(project.id)
expect(last_event['author_username']).to eq(member.username)
expect(last_event['author']['name']).to eq(member.name)
end
end
context 'when unauthenticated' do
it_behaves_like 'project events response' do
let(:project) { create(:empty_project, :public) }
let(:current_user) { nil }
end
end
context 'when authenticated' do
context 'valid request' do
it_behaves_like 'project events response' do
let(:current_user) { user }
end
end
it 'returns a 404 error if not found' do
get v3_api('/projects/42/events', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get v3_api("/projects/#{project.id}/events", other_user)
expect(response).to have_http_status(404)
end
end
end
describe 'GET /projects/:id/users' do
shared_examples_for 'project users response' do
it 'returns the project users' do
member = project.owner
get v3_api("/projects/#{project.id}/users", current_user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
first_user = json_response.first
expect(first_user['username']).to eq(member.username)
expect(first_user['name']).to eq(member.name)
expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
end
end
context 'when unauthenticated' do
it_behaves_like 'project users response' do
let(:project) { create(:empty_project, :public) }
let(:current_user) { nil }
end
end
context 'when authenticated' do
context 'valid request' do
it_behaves_like 'project users response' do
let(:current_user) { user }
end
end
it 'returns a 404 error if not found' do
get v3_api('/projects/42/users', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get v3_api("/projects/#{project.id}/users", other_user)
expect(response).to have_http_status(404)
end
end
end
describe 'GET /projects/:id/snippets' do
before { snippet }
it 'returns an array of project snippets' do
get v3_api("/projects/#{project.id}/snippets", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(snippet.title)
end
end
describe 'GET /projects/:id/snippets/:snippet_id' do
it 'returns a project snippet' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(snippet.title)
end
it 'returns a 404 error if snippet id not found' do
get v3_api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/snippets' do
it 'creates a new project snippet' do
post v3_api("/projects/#{project.id}/snippets", user),
title: 'v3_api test', file_name: 'sample.rb', code: 'test',
visibility_level: '0'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('v3_api test')
end
it 'returns a 400 error if invalid snippet is given' do
post v3_api("/projects/#{project.id}/snippets", user)
expect(status).to eq(400)
end
end
describe 'PUT /projects/:id/snippets/:snippet_id' do
it 'updates an existing project snippet' do
put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('example')
expect(snippet.reload.content).to eq('updated code')
end
it 'updates an existing project snippet with new title' do
put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
title: 'other v3_api test'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('other v3_api test')
end
end
describe 'DELETE /projects/:id/snippets/:snippet_id' do
before { snippet }
it 'deletes existing project snippet' do
expect do
delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
end.to change { Snippet.count }.by(-1)
expect(response).to have_http_status(200)
end
it 'returns 404 when deleting unknown snippet id' do
delete v3_api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
it 'gets a raw project snippet' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
expect(response).to have_http_status(200)
end
it 'returns a 404 error if raw project snippet not found' do
get v3_api("/projects/#{project.id}/snippets/5555/raw", user)
expect(response).to have_http_status(404)
end
end
describe 'fork management' do
let(:project_fork_target) { create(:empty_project) }
let(:project_fork_source) { create(:empty_project, :public) }
describe 'POST /projects/:id/fork/:forked_from_id' do
let(:new_project_fork_source) { create(:empty_project, :public) }
it "is not available for non admin users" do
post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
expect(response).to have_http_status(403)
end
it 'allows project to be forked from an existing project' do
expect(project_fork_target.forked?).not_to be_truthy
post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
expect(response).to have_http_status(201)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked_project_link).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
end
it 'fails if forked_from project which does not exist' do
post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
expect(response).to have_http_status(404)
end
it 'fails with 409 if already forked' do
post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
expect(response).to have_http_status(409)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked?).to be_truthy
end
end
describe 'DELETE /projects/:id/fork' do
it "is not visible to users outside group" do
delete v3_api("/projects/#{project_fork_target.id}/fork", user)
expect(response).to have_http_status(404)
end
context 'when users belong to project group' do
let(:project_fork_target) { create(:empty_project, group: create(:group)) }
before do
project_fork_target.group.add_owner user
project_fork_target.group.add_developer user2
end
it 'is forbidden to non-owner users' do
delete v3_api("/projects/#{project_fork_target.id}/fork", user2)
expect(response).to have_http_status(403)
end
it 'makes forked project unforked' do
post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
expect(response).to have_http_status(200)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
end
it 'is idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
expect(response).to have_http_status(304)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
end
end
end
describe "POST /projects/:id/share" do
let(:group) { create(:group) }
it "shares project with group" do
expires_at = 10.days.from_now.to_date
expect do
post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
end.to change { ProjectGroupLink.count }.by(1)
expect(response).to have_http_status(201)
expect(json_response['group_id']).to eq(group.id)
expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
expect(json_response['expires_at']).to eq(expires_at.to_s)
end
it "returns a 400 error when group id is not given" do
post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
expect(response).to have_http_status(400)
end
it "returns a 400 error when access level is not given" do
post v3_api("/projects/#{project.id}/share", user), group_id: group.id
expect(response).to have_http_status(400)
end
it "returns a 400 error when sharing is disabled" do
project.namespace.update(share_with_group_lock: true)
post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
expect(response).to have_http_status(400)
end
it 'returns a 404 error when user cannot read group' do
private_group = create(:group, :private)
post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
expect(response).to have_http_status(404)
end
it 'returns a 404 error when group does not exist' do
post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
expect(response).to have_http_status(404)
end
it "returns a 400 error when wrong params passed" do
post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
expect(response).to have_http_status(400)
expect(json_response['error']).to eq 'group_access does not have a valid value'
end
end
describe 'DELETE /projects/:id/share/:group_id' do
it 'returns 204 when deleting a group share' do
group = create(:group, :public)
create(:project_group_link, group: group, project: project)
delete v3_api("/projects/#{project.id}/share/#{group.id}", user)
expect(response).to have_http_status(204)
expect(project.project_group_links).to be_empty
end
it 'returns a 400 when group id is not an integer' do
delete v3_api("/projects/#{project.id}/share/foo", user)
expect(response).to have_http_status(400)
end
it 'returns a 404 error when group link does not exist' do
delete v3_api("/projects/#{project.id}/share/1234", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 error when project does not exist' do
delete v3_api("/projects/123/share/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/search/:query' do
let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
let!(:public) { create(:empty_project, :public, name: "public #{query}") }
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
shared_examples_for 'project search response' do |args = {}|
it 'returns project search responses' do
get v3_api("/projects/search/#{args[:query]}", current_user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(args[:results])
json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
end
end
context 'when unauthenticated' do
it_behaves_like 'project search response', query: 'query', results: 1 do
let(:current_user) { nil }
end
end
context 'when authenticated' do
it_behaves_like 'project search response', query: 'query', results: 6 do
let(:current_user) { user }
end
it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
let(:current_user) { user }
end
end
context 'when authenticated as a different user' do
it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
let(:current_user) { user2 }
end
end
end
describe 'PUT /projects/:id' do
before { project }
before { user }
before { user3 }
before { user4 }
before { project3 }
before { project4 }
before { project_member2 }
before { project_member }
context 'when unauthenticated' do
it 'returns authentication error' do
project_param = { name: 'bar' }
put v3_api("/projects/#{project.id}"), project_param
expect(response).to have_http_status(401)
end
end
context 'when authenticated as project owner' do
it 'updates name' do
project_param = { name: 'bar' }
put v3_api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
end
it 'updates visibility_level' do
project_param = { visibility_level: 20 }
put v3_api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
end
it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { public: false }
put v3_api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'does not update name to existing name' do
project_param = { name: project3.name }
put v3_api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
it 'updates request_access_enabled' do
project_param = { request_access_enabled: false }
put v3_api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(200)
expect(json_response['request_access_enabled']).to eq(false)
end
it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put v3_api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
end
end
context 'when authenticated as project master' do
it 'updates path' do
project_param = { path: 'bar' }
put v3_api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
end
it 'updates other attributes' do
project_param = { issues_enabled: true,
wiki_enabled: true,
snippets_enabled: true,
merge_requests_enabled: true,
description: 'new description' }
put v3_api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
end
it 'does not update path to existing path' do
project_param = { path: project.path }
put v3_api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['path']).to eq(['has already been taken'])
end
it 'does not update name' do
project_param = { name: 'bar' }
put v3_api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
it 'does not update visibility_level' do
project_param = { visibility_level: 20 }
put v3_api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
end
context 'when authenticated as project developer' do
it 'does not update other attributes' do
project_param = { path: 'bar',
issues_enabled: true,
wiki_enabled: true,
snippets_enabled: true,
merge_requests_enabled: true,
description: 'new description',
request_access_enabled: true }
put v3_api("/projects/#{project.id}", user3), project_param
expect(response).to have_http_status(403)
end
end
end
describe 'POST /projects/:id/archive' do
context 'on an unarchived project' do
it 'archives the project' do
post v3_api("/projects/#{project.id}/archive", user)
expect(response).to have_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
context 'on an archived project' do
before do
project.archive!
end
it 'remains archived' do
post v3_api("/projects/#{project.id}/archive", user)
expect(response).to have_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
context 'user without archiving rights to the project' do
before do
project.team << [user3, :developer]
end
it 'rejects the action' do
post v3_api("/projects/#{project.id}/archive", user3)
expect(response).to have_http_status(403)
end
end
end
describe 'POST /projects/:id/unarchive' do
context 'on an unarchived project' do
it 'remains unarchived' do
post v3_api("/projects/#{project.id}/unarchive", user)
expect(response).to have_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
context 'on an archived project' do
before do
project.archive!
end
it 'unarchives the project' do
post v3_api("/projects/#{project.id}/unarchive", user)
expect(response).to have_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
context 'user without archiving rights to the project' do
before do
project.team << [user3, :developer]
end
it 'rejects the action' do
post v3_api("/projects/#{project.id}/unarchive", user3)
expect(response).to have_http_status(403)
end
end
end
describe 'POST /projects/:id/star' do
context 'on an unstarred project' do
it 'stars the project' do
expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
expect(response).to have_http_status(201)
expect(json_response['star_count']).to eq(1)
end
end
context 'on a starred project' do
before do
user.toggle_star(project)
project.reload
end
it 'does not modify the star count' do
expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
expect(response).to have_http_status(304)
end
end
end
describe 'DELETE /projects/:id/star' do
context 'on a starred project' do
before do
user.toggle_star(project)
project.reload
end
it 'unstars the project' do
expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
expect(response).to have_http_status(200)
expect(json_response['star_count']).to eq(0)
end
end
context 'on an unstarred project' do
it 'does not modify the star count' do
expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
expect(response).to have_http_status(304)
end
end
end
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
it 'removes project' do
delete v3_api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
end
it 'does not remove a project if not an owner' do
user3 = create(:user)
project.team << [user3, :developer]
delete v3_api("/projects/#{project.id}", user3)
expect(response).to have_http_status(403)
end
it 'does not remove a non existing project' do
delete v3_api('/projects/1328', user)
expect(response).to have_http_status(404)
end
it 'does not remove a project not attached to user' do
delete v3_api("/projects/#{project.id}", user2)
expect(response).to have_http_status(404)
end
end
context 'when authenticated as admin' do
it 'removes any existing project' do
delete v3_api("/projects/#{project.id}", admin)
expect(response).to have_http_status(200)
end
it 'does not remove a non existing project' do
delete v3_api('/projects/1328', admin)
expect(response).to have_http_status(404)
end
end
end
end