# RhodeCode VCSServer provides access to different vcs backends via network. # Copyright (C) 2014-2023 RhodeCode GmbH # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import os import pytest from webtest.app import TestApp as WebObTestApp from vcsserver.lib.ext_json import json from vcsserver.lib.str_utils import safe_bytes from vcsserver.git_lfs.app import create_app from vcsserver.git_lfs.lib import LFSOidStore @pytest.fixture(scope='function') def git_lfs_app(tmpdir): custom_app = WebObTestApp(create_app( git_lfs_enabled=True, git_lfs_store_path=str(tmpdir), git_lfs_http_scheme='http')) custom_app._store = str(tmpdir) return custom_app @pytest.fixture(scope='function') def git_lfs_https_app(tmpdir): custom_app = WebObTestApp(create_app( git_lfs_enabled=True, git_lfs_store_path=str(tmpdir), git_lfs_http_scheme='https')) custom_app._store = str(tmpdir) return custom_app @pytest.fixture() def http_auth(): return {'HTTP_AUTHORIZATION': "Basic XXXXX"} class TestLFSApplication: def test_app_wrong_path(self, git_lfs_app): git_lfs_app.get('/repo/info/lfs/xxx', status=404) def test_app_deprecated_endpoint(self, git_lfs_app): response = git_lfs_app.post('/repo/info/lfs/objects', status=501) assert response.status_code == 501 assert json.loads(response.text) == {'message': 'LFS: v1 api not supported'} def test_app_lock_verify_api_not_available(self, git_lfs_app): response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501) assert response.status_code == 501 assert json.loads(response.text) == { 'message': 'GIT LFS locking api not supported'} def test_app_lock_api_not_available(self, git_lfs_app): response = git_lfs_app.post('/repo/info/lfs/locks', status=501) assert response.status_code == 501 assert json.loads(response.text) == { 'message': 'GIT LFS locking api not supported'} def test_app_batch_api_missing_auth(self, git_lfs_app): git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params={}, status=403) def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth): response = git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params={}, status=400, extra_environ=http_auth) assert json.loads(response.text) == { 'message': 'unsupported operation mode: `None`'} def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth): response = git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params={'operation': 'download'}, status=400, extra_environ=http_auth) assert json.loads(response.text) == { 'message': 'missing objects data'} def test_app_batch_api_unsupported_data_in_objects( self, git_lfs_app, http_auth): params = {'operation': 'download', 'objects': [{}]} response = git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params=params, status=400, extra_environ=http_auth) assert json.loads(response.text) == { 'message': 'unsupported data in objects'} def test_app_batch_api_download_missing_object( self, git_lfs_app, http_auth): params = { 'operation': 'download', 'objects': [{'oid': '123', 'size': '1024'}] } response = git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params=params, extra_environ=http_auth) expected_objects = [ { 'oid': '123', 'size': '1024', 'authenticated': True, 'errors': {'error': {'code': 404, 'message': 'object: 123 does not exist in store'}}, } ] assert json.loads(response.text) == { 'objects': expected_objects, 'transfer': 'basic' } def test_app_batch_api_download(self, git_lfs_app, http_auth): oid = '456' oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path if not os.path.isdir(os.path.dirname(oid_path)): os.makedirs(os.path.dirname(oid_path)) with open(oid_path, 'wb') as f: f.write(safe_bytes('OID_CONTENT')) params = {'operation': 'download', 'objects': [{'oid': oid, 'size': '1024'}]} response = git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params=params, extra_environ=http_auth) expected_objects = [ {'authenticated': True, 'actions': { 'download': { 'header': {'Authorization': 'Basic XXXXX'}, 'href': 'http://localhost/repo/info/lfs/objects/456'}, }, 'oid': '456', 'size': '1024'} ] assert json.loads(response.text) == { 'objects': expected_objects, 'transfer': 'basic' } def test_app_batch_api_upload(self, git_lfs_app, http_auth): params = {'operation': 'upload', 'objects': [{'oid': '123', 'size': '1024'}]} response = git_lfs_app.post_json( '/repo/info/lfs/objects/batch', params=params, extra_environ=http_auth) expected_objects = [ { 'authenticated': True, 'actions': { 'upload': { 'header': { 'Authorization': 'Basic XXXXX', 'Transfer-Encoding': 'chunked' }, 'href': 'http://localhost/repo/info/lfs/objects/123' }, 'verify': { 'header': { 'Authorization': 'Basic XXXXX' }, 'href': 'http://localhost/repo/info/lfs/verify' } }, 'oid': '123', 'size': '1024' } ] assert json.loads(response.text) == { 'objects': expected_objects, 'transfer': 'basic' } def test_app_batch_api_upload_for_https(self, git_lfs_https_app, http_auth): params = {'operation': 'upload', 'objects': [{'oid': '123', 'size': '1024'}]} response = git_lfs_https_app.post_json( '/repo/info/lfs/objects/batch', params=params, extra_environ=http_auth) expected_objects = [ {'authenticated': True, 'actions': { 'upload': { 'header': {'Authorization': 'Basic XXXXX', 'Transfer-Encoding': 'chunked'}, 'href': 'https://localhost/repo/info/lfs/objects/123'}, 'verify': { 'header': {'Authorization': 'Basic XXXXX'}, 'href': 'https://localhost/repo/info/lfs/verify'} }, 'oid': '123', 'size': '1024'} ] assert json.loads(response.text) == { 'objects': expected_objects, 'transfer': 'basic'} def test_app_verify_api_missing_data(self, git_lfs_app): params = {'oid': 'missing'} response = git_lfs_app.post_json( '/repo/info/lfs/verify', params=params, status=400) assert json.loads(response.text) == { 'message': 'missing oid and size in request data'} def test_app_verify_api_missing_obj(self, git_lfs_app): params = {'oid': 'missing', 'size': '1024'} response = git_lfs_app.post_json( '/repo/info/lfs/verify', params=params, status=404) assert json.loads(response.text) == { 'message': 'oid `missing` does not exists in store' } def test_app_verify_api_size_mismatch(self, git_lfs_app): oid = 'existing' oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path if not os.path.isdir(os.path.dirname(oid_path)): os.makedirs(os.path.dirname(oid_path)) with open(oid_path, 'wb') as f: f.write(safe_bytes('OID_CONTENT')) params = {'oid': oid, 'size': '1024'} response = git_lfs_app.post_json( '/repo/info/lfs/verify', params=params, status=422) assert json.loads(response.text) == { 'message': 'requested file size mismatch store size:11 requested:1024' } def test_app_verify_api(self, git_lfs_app): oid = 'existing' oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path if not os.path.isdir(os.path.dirname(oid_path)): os.makedirs(os.path.dirname(oid_path)) with open(oid_path, 'wb') as f: f.write(safe_bytes('OID_CONTENT')) params = {'oid': oid, 'size': 11} response = git_lfs_app.post_json( '/repo/info/lfs/verify', params=params) assert json.loads(response.text) == { 'message': {'size': 11, 'oid': oid} } def test_app_download_api_oid_not_existing(self, git_lfs_app): oid = 'missing' response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}', status=404) assert json.loads(response.text) == { 'message': 'requested file with oid `missing` not found in store'} def test_app_download_api(self, git_lfs_app): oid = 'existing' oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path if not os.path.isdir(os.path.dirname(oid_path)): os.makedirs(os.path.dirname(oid_path)) with open(oid_path, 'wb') as f: f.write(safe_bytes('OID_CONTENT')) response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}') assert response def test_app_upload(self, git_lfs_app): oid = '65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12' response = git_lfs_app.put( f'/repo/info/lfs/objects/{oid}', params='CONTENT') assert json.loads(response.text) == {'upload': 'ok', 'state': 'written'} # verify that we actually wrote that OID oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path assert os.path.isfile(oid_path) assert 'CONTENT' == open(oid_path).read() response = git_lfs_app.put( f'/repo/info/lfs/objects/{oid}', params='CONTENT') assert json.loads(response.text) == {'upload': 'ok', 'state': 'in-store'} def test_app_upload_wrong_sha(self, git_lfs_app): oid = 'i-am-a-wrong-sha' response = git_lfs_app.put(f'/repo/info/lfs/objects/{oid}', params='CONTENT', status=400) assert json.loads(response.text) == { 'message': 'oid i-am-a-wrong-sha does not match expected sha ' '65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12'} # check this OID wasn't written to store response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}', status=404) assert json.loads(response.text) == {'message': 'requested file with oid `i-am-a-wrong-sha` not found in store'}