Read about our upcoming Code of Conduct on this issue

Commit 4cb85f1a authored by Rubén Dávila's avatar Rubén Dávila
Browse files

Backport of LFS File Locking API

parent 66c920293e87
FactoryBot.define do
factory :lfs_file_lock do
user
project
path 'README.md'
end
end
......@@ -177,5 +177,44 @@
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
......@@ -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
......@@ -530,3 +530,9 @@ ProjectCustomAttribute:
- project_id
- key
- value
LfsFileLock:
- id
- path
- user_id
- project_id
- created_at
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
......@@ -80,6 +80,7 @@
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
......
......@@ -262,6 +262,28 @@ def expect_to_raise_storage_error
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) }
......
......@@ -1208,7 +1208,7 @@ def authorize_user_key
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
......
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
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
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
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
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