diff options
author | Rubén Dávila <ruben@gitlab.com> | 2018-02-07 08:00:53 -0500 |
---|---|---|
committer | Rubén Dávila <ruben@gitlab.com> | 2018-02-07 09:04:00 -0500 |
commit | bed948321173b49564f39837e97212ee4dd96e03 (patch) | |
tree | 72e6faa9f68378f997f876cf9550561bad546028 /spec | |
parent | ead97c55eac773444dee547a934112aa282c2e2e (diff) | |
download | gitlab-ce-bed948321173b49564f39837e97212ee4dd96e03.tar.gz |
Backport of LFS File Locking APIrd-35856-backport-lfs-file-locking-api
Diffstat (limited to 'spec')
-rw-r--r-- | spec/factories/lfs_file_locks.rb | 7 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/change_access_spec.rb | 39 | ||||
-rw-r--r-- | spec/lib/gitlab/import_export/all_models.yml | 3 | ||||
-rw-r--r-- | spec/lib/gitlab/import_export/safe_model_attributes.yml | 6 | ||||
-rw-r--r-- | spec/models/lfs_file_lock_spec.rb | 57 | ||||
-rw-r--r-- | spec/models/project_spec.rb | 1 | ||||
-rw-r--r-- | spec/models/repository_spec.rb | 22 | ||||
-rw-r--r-- | spec/requests/lfs_http_spec.rb | 2 | ||||
-rw-r--r-- | spec/requests/lfs_locks_api_spec.rb | 159 | ||||
-rw-r--r-- | spec/serializers/lfs_file_lock_entity_spec.rb | 19 | ||||
-rw-r--r-- | spec/services/lfs/lock_file_service_spec.rb | 62 | ||||
-rw-r--r-- | spec/services/lfs/locks_finder_service_spec.rb | 101 | ||||
-rw-r--r-- | spec/services/lfs/unlock_file_service_spec.rb | 105 |
13 files changed, 582 insertions, 1 deletions
diff --git a/spec/factories/lfs_file_locks.rb b/spec/factories/lfs_file_locks.rb new file mode 100644 index 00000000000..b9d24f82b65 --- /dev/null +++ b/spec/factories/lfs_file_locks.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :lfs_file_lock do + user + project + path 'README.md' + end +end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index c2bca816aae..475b5c5cfb2 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -177,5 +177,44 @@ describe Gitlab::Checks::ChangeAccess do expect { subject.exec }.not_to raise_error end end + + context 'LFS file lock check' do + let(:owner) { create(:user) } + let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } + + before do + allow(project.repository).to receive(:new_commits).and_return( + project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') + ) + end + + context 'with LFS not enabled' do + it 'skips the validation' do + expect_any_instance_of(described_class).not_to receive(:lfs_file_locks_validation) + + subject.exec + end + end + + context 'with LFS enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + context 'when change is sent by a different user' do + it 'raises an error if the user is not allowed to update the file' do + expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + end + end + + context 'when change is sent by the author od the lock' do + let(:user) { owner } + + it "doesn't raise any error" do + expect { subject.exec }.not_to raise_error + end + end + end + end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 0ecb50f7110..41a55027f4d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -276,6 +276,7 @@ project: - fork_network_member - fork_network - custom_attributes +- lfs_file_locks award_emoji: - awardable - user @@ -290,3 +291,5 @@ push_event_payload: issue_assignees: - issue - assignee +lfs_file_locks: +- user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 5a33fa3fd53..feaab6673cd 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -530,3 +530,9 @@ ProjectCustomAttribute: - project_id - key - value +LfsFileLock: +- id +- path +- user_id +- project_id +- created_at diff --git a/spec/models/lfs_file_lock_spec.rb b/spec/models/lfs_file_lock_spec.rb new file mode 100644 index 00000000000..ce87b01b49c --- /dev/null +++ b/spec/models/lfs_file_lock_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +describe LfsFileLock do + set(:lfs_file_lock) { create(:lfs_file_lock) } + subject { lfs_file_lock } + + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:user) } + + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:user_id) } + it { is_expected.to validate_presence_of(:path) } + + describe '#can_be_unlocked_by?' do + let(:developer) { create(:user) } + let(:master) { create(:user) } + + before do + project = lfs_file_lock.project + + project.add_developer(developer) + project.add_master(master) + end + + context "when it's forced" do + it 'can be unlocked by the author' do + user = lfs_file_lock.user + + expect(lfs_file_lock.can_be_unlocked_by?(user, true)).to eq(true) + end + + it 'can be unlocked by a master' do + expect(lfs_file_lock.can_be_unlocked_by?(master, true)).to eq(true) + end + + it "can't be unlocked by other user" do + expect(lfs_file_lock.can_be_unlocked_by?(developer, true)).to eq(false) + end + end + + context "when it isn't forced" do + it 'can be unlocked by the author' do + user = lfs_file_lock.user + + expect(lfs_file_lock.can_be_unlocked_by?(user)).to eq(true) + end + + it "can't be unlocked by a master" do + expect(lfs_file_lock.can_be_unlocked_by?(master)).to eq(false) + end + + it "can't be unlocked by other user" do + expect(lfs_file_lock.can_be_unlocked_by?(developer)).to eq(false) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9dca7f326d3..c6ca038a2ba 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -80,6 +80,7 @@ describe Project do it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } + it { is_expected.to have_many(:lfs_file_locks) } context 'after initialized' do it "has a project_feature" do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 02a5ee54262..a6d48e369ac 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -262,6 +262,28 @@ describe Repository do end end + describe '#new_commits' do + let(:new_refs) do + double(:git_rev_list, new_refs: %w[ + c1acaa58bbcbc3eafe538cb8274ba387047b69f8 + 5937ac0a7beb003549fc5fd26fc247adbce4a52e + ]) + end + + it 'delegates to Gitlab::Git::RevList' do + expect(Gitlab::Git::RevList).to receive(:new).with( + repository.raw, + newrev: 'aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj').and_return(new_refs) + + commits = repository.new_commits('aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj') + + expect(commits).to eq([ + repository.commit('c1acaa58bbcbc3eafe538cb8274ba387047b69f8'), + repository.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + ]) + end + end + describe '#commits_by' do set(:project) { create(:project, :repository) } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 930ef49b7f3..971b45c411d 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1208,7 +1208,7 @@ describe 'Git LFS API and storage' do end def post_lfs_json(url, body = nil, headers = nil) - post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) end def json_response diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb new file mode 100644 index 00000000000..e44a11a7232 --- /dev/null +++ b/spec/requests/lfs_locks_api_spec.rb @@ -0,0 +1,159 @@ +require 'spec_helper' + +describe 'Git LFS File Locking API' do + include WorkhorseHelpers + + let(:project) { create(:project) } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:guest) { create(:user) } + let(:path) { 'README.md' } + let(:headers) do + { + 'Authorization' => authorization + }.compact + end + + shared_examples 'unauthorized request' do + context 'when user is not authorized' do + let(:authorization) { authorize_user(guest) } + + it 'returns a forbidden 403 response' do + post_lfs_json url, body, headers + + expect(response).to have_gitlab_http_status(403) + end + end + end + + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + + project.add_developer(master) + project.add_developer(developer) + project.add_guest(guest) + end + + describe 'Create File Lock endpoint' do + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" } + let(:authorization) { authorize_user(developer) } + let(:body) { { path: path } } + + include_examples 'unauthorized request' + + context 'with an existent lock' do + before do + lock_file('README.md', developer) + end + + it 'return an error message' do + post_lfs_json url, body, headers + + expect(response).to have_gitlab_http_status(409) + + expect(json_response.keys).to match_array(%w(lock message documentation_url)) + expect(json_response['message']).to match(/already locked/) + end + + it 'returns the existen lock' do + post_lfs_json url, body, headers + + expect(json_response['lock']['path']).to eq('README.md') + end + end + + context 'without an existent lock' do + it 'creates the lock' do + post_lfs_json url, body, headers + + expect(response).to have_gitlab_http_status(201) + + expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) + end + end + end + + describe 'Listing File Locks endpoint' do + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" } + let(:authorization) { authorize_user(developer) } + + include_examples 'unauthorized request' + + it 'returns the list of locked files' do + lock_file('README.md', developer) + lock_file('README', developer) + + do_get url, nil, headers + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['locks'].size).to eq(2) + expect(json_response['locks'].first.keys).to match_array(%w(id path locked_at owner)) + end + end + + describe 'List File Locks for verification endpoint' do + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/verify" } + let(:authorization) { authorize_user(developer) } + + include_examples 'unauthorized request' + + it 'returns the list of locked files grouped by owner' do + lock_file('README.md', master) + lock_file('README', developer) + + post_lfs_json url, nil, headers + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['ours'].size).to eq(1) + expect(json_response['ours'].first['path']).to eq('README') + expect(json_response['theirs'].size).to eq(1) + expect(json_response['theirs'].first['path']).to eq('README.md') + end + end + + describe 'Delete File Lock endpoint' do + let!(:lock) { lock_file('README.md', developer) } + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/#{lock[:id]}/unlock" } + let(:authorization) { authorize_user(developer) } + + include_examples 'unauthorized request' + + context 'with an existent lock' do + it 'deletes the lock' do + post_lfs_json url, nil, headers + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns the deleted lock' do + post_lfs_json url, nil, headers + + expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) + end + end + end + + def lock_file(path, author) + result = Lfs::LockFileService.new(project, author, { path: path }).execute + + result[:lock] + end + + def authorize_user(user) + ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + end + + def post_lfs_json(url, body = nil, headers = nil) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) + end + + def do_get(url, params = nil, headers = nil) + get(url, (params || {}), (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) + end + + def json_response + @json_response ||= JSON.parse(response.body) + end +end diff --git a/spec/serializers/lfs_file_lock_entity_spec.rb b/spec/serializers/lfs_file_lock_entity_spec.rb new file mode 100644 index 00000000000..5919f473a90 --- /dev/null +++ b/spec/serializers/lfs_file_lock_entity_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe LfsFileLockEntity do + let(:user) { create(:user) } + let(:resource) { create(:lfs_file_lock, user: user) } + + let(:request) { double('request', current_user: user) } + + subject { described_class.new(resource, request: request).as_json } + + it 'exposes basic attrs of the lock' do + expect(subject).to include(:id, :path, :locked_at) + end + + it 'exposes the owner info' do + expect(subject).to include(:owner) + expect(subject[:owner][:name]).to eq(user.name) + end +end diff --git a/spec/services/lfs/lock_file_service_spec.rb b/spec/services/lfs/lock_file_service_spec.rb new file mode 100644 index 00000000000..3e58eea2501 --- /dev/null +++ b/spec/services/lfs/lock_file_service_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Lfs::LockFileService do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + + subject { described_class.new(project, current_user, params) } + + describe '#execute' do + let(:params) { { path: 'README.md' } } + + context 'when not authorized' do + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(403) + expect(result[:message]).to eq('You have no permissions') + end + end + + context 'when authorized' do + before do + project.add_developer(current_user) + end + + context 'with an existent lock' do + let!(:lock) { create(:lfs_file_lock, project: project) } + + it "doesn't succeed" do + expect(subject.execute[:status]).to eq(:error) + end + + it "doesn't create the Lock" do + expect do + subject.execute + end.not_to change { LfsFileLock.count } + end + end + + context 'without an existent lock' do + it "succeeds" do + expect(subject.execute[:status]).to eq(:success) + end + + it "creates the Lock" do + expect do + subject.execute + end.to change { LfsFileLock.count }.by(1) + end + end + + context 'when an error is raised' do + it "doesn't succeed" do + allow_any_instance_of(described_class).to receive(:create_lock!).and_raise(StandardError) + + expect(subject.execute[:status]).to eq(:error) + end + end + end + end +end diff --git a/spec/services/lfs/locks_finder_service_spec.rb b/spec/services/lfs/locks_finder_service_spec.rb new file mode 100644 index 00000000000..e409b77babf --- /dev/null +++ b/spec/services/lfs/locks_finder_service_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe Lfs::LocksFinderService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:params) { {} } + + subject { described_class.new(project, user, params) } + + shared_examples 'no results' do + it 'returns an empty list' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks]).to be_blank + end + end + + describe '#execute' do + let!(:lock_1) { create(:lfs_file_lock, project: project) } + let!(:lock_2) { create(:lfs_file_lock, project: project, path: 'README') } + + context 'find by id' do + context 'with results' do + let(:params) do + { id: lock_1.id } + end + + it 'returns the record' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks].size).to eq(1) + expect(result[:locks].first).to eq(lock_1) + end + end + + context 'without results' do + let(:params) do + { id: 123 } + end + + include_examples 'no results' + end + end + + context 'find by path' do + context 'with results' do + let(:params) do + { path: lock_1.path } + end + + it 'returns the record' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks].size).to eq(1) + expect(result[:locks].first).to eq(lock_1) + end + end + + context 'without results' do + let(:params) do + { path: 'not-found' } + end + + include_examples 'no results' + end + end + + context 'find all' do + context 'with results' do + it 'returns all the records' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks].size).to eq(2) + end + end + + context 'without results' do + before do + LfsFileLock.delete_all + end + + include_examples 'no results' + end + end + + context 'when an error is raised' do + it "doesn't succeed" do + allow_any_instance_of(described_class).to receive(:find_locks).and_raise(StandardError) + + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:locks]).to be_blank + end + end + end +end diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb new file mode 100644 index 00000000000..4bea112b9c6 --- /dev/null +++ b/spec/services/lfs/unlock_file_service_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe Lfs::UnlockFileService do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + let(:lock_author) { create(:user) } + let!(:lock) { create(:lfs_file_lock, user: lock_author, project: project) } + let(:params) { {} } + + subject { described_class.new(project, current_user, params) } + + describe '#execute' do + context 'when not authorized' do + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(403) + expect(result[:message]).to eq('You have no permissions') + end + end + + context 'when authorized' do + before do + project.add_developer(current_user) + end + + context 'when lock does not exists' do + let(:params) { { id: 123 } } + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(404) + end + end + + context 'when unlocked by the author' do + let(:current_user) { lock_author } + let(:params) { { id: lock.id } } + + it "succeeds" do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:lock]).to be_present + end + end + + context 'when unlocked by a different user' do + let(:current_user) { create(:user) } + let(:params) { { id: lock.id } } + + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to match(/is locked by GitLab User #{lock_author.id}/) + expect(result[:http_status]).to eq(403) + end + end + + context 'when forced' do + let(:developer) { create(:user) } + let(:master) { create(:user) } + + before do + project.add_developer(developer) + project.add_master(master) + end + + context 'by a regular user' do + let(:current_user) { developer } + let(:params) do + { id: lock.id, + force: true } + end + + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to match(/You must have master access/) + expect(result[:http_status]).to eq(403) + end + end + + context 'by a master user' do + let(:current_user) { master } + let(:params) do + { id: lock.id, + force: true } + end + + it "succeeds" do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:lock]).to be_present + end + end + end + end + end +end |