test_lfs_app.py
310 lines
| 11.8 KiB
| text/x-python
|
PythonLexer
r180 | # RhodeCode VCSServer provides access to different vcs backends via network. | |||
r1327 | # Copyright (C) 2014-2024 RhodeCode GmbH | |||
r180 | # | |||
# 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 | ||||
r1243 | from vcsserver.lib.ext_json import json | |||
r1249 | from vcsserver.lib.str_utils import safe_bytes | |||
r180 | from vcsserver.git_lfs.app import create_app | |||
r1194 | from vcsserver.git_lfs.lib import LFSOidStore | |||
r180 | ||||
@pytest.fixture(scope='function') | ||||
def git_lfs_app(tmpdir): | ||||
custom_app = WebObTestApp(create_app( | ||||
r700 | 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')) | ||||
r180 | custom_app._store = str(tmpdir) | |||
return custom_app | ||||
@pytest.fixture() | ||||
def http_auth(): | ||||
return {'HTTP_AUTHORIZATION': "Basic XXXXX"} | ||||
r1152 | class TestLFSApplication: | |||
r180 | ||||
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 | ||||
r1044 | assert json.loads(response.text) == {'message': 'LFS: v1 api not supported'} | |||
r180 | ||||
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 | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'GIT LFS locking api not supported'} | |||
r180 | ||||
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 | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'GIT LFS locking api not supported'} | |||
r180 | ||||
r700 | def test_app_batch_api_missing_auth(self, git_lfs_app): | |||
r180 | 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) | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'unsupported operation mode: `None`'} | |||
r180 | ||||
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) | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'missing objects data'} | |||
r180 | ||||
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) | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'unsupported data in objects'} | |||
r180 | ||||
def test_app_batch_api_download_missing_object( | ||||
self, git_lfs_app, http_auth): | ||||
r1300 | params = { | |||
'operation': 'download', | ||||
'objects': [{'oid': '123', 'size': '1024'}] | ||||
} | ||||
r180 | response = git_lfs_app.post_json( | |||
'/repo/info/lfs/objects/batch', params=params, | ||||
extra_environ=http_auth) | ||||
expected_objects = [ | ||||
r1300 | { | |||
'oid': '123', | ||||
'size': '1024', | ||||
'authenticated': True, | ||||
'errors': {'error': {'code': 404, 'message': 'object: 123 does not exist in store'}}, | ||||
} | ||||
r180 | ] | |||
r1300 | ||||
r198 | assert json.loads(response.text) == { | |||
r1300 | 'objects': expected_objects, | |||
'transfer': 'basic' | ||||
} | ||||
r180 | ||||
def test_app_batch_api_download(self, git_lfs_app, http_auth): | ||||
oid = '456' | ||||
r1194 | oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path | |||
r181 | if not os.path.isdir(os.path.dirname(oid_path)): | |||
os.makedirs(os.path.dirname(oid_path)) | ||||
r180 | with open(oid_path, 'wb') as f: | |||
r1048 | f.write(safe_bytes('OID_CONTENT')) | |||
r180 | ||||
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 = [ | ||||
r1044 | {'authenticated': True, | |||
'actions': { | ||||
'download': { | ||||
'header': {'Authorization': 'Basic XXXXX'}, | ||||
'href': 'http://localhost/repo/info/lfs/objects/456'}, | ||||
r180 | }, | |||
r1044 | 'oid': '456', | |||
'size': '1024'} | ||||
r180 | ] | |||
r198 | assert json.loads(response.text) == { | |||
r1300 | 'objects': expected_objects, | |||
'transfer': 'basic' | ||||
} | ||||
r180 | ||||
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 = [ | ||||
r1300 | { | |||
'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' | ||||
} | ||||
r180 | ] | |||
r198 | assert json.loads(response.text) == { | |||
r1300 | 'objects': expected_objects, | |||
'transfer': 'basic' | ||||
} | ||||
r180 | ||||
r700 | 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 = [ | ||||
r1044 | {'authenticated': True, | |||
'actions': { | ||||
'upload': { | ||||
'header': {'Authorization': 'Basic XXXXX', | ||||
r1300 | 'Transfer-Encoding': 'chunked'}, | |||
r1044 | 'href': 'https://localhost/repo/info/lfs/objects/123'}, | |||
'verify': { | ||||
'header': {'Authorization': 'Basic XXXXX'}, | ||||
'href': 'https://localhost/repo/info/lfs/verify'} | ||||
r700 | }, | |||
r1044 | 'oid': '123', | |||
'size': '1024'} | ||||
r700 | ] | |||
assert json.loads(response.text) == { | ||||
'objects': expected_objects, 'transfer': 'basic'} | ||||
r180 | def test_app_verify_api_missing_data(self, git_lfs_app): | |||
r700 | params = {'oid': 'missing'} | |||
r180 | response = git_lfs_app.post_json( | |||
'/repo/info/lfs/verify', params=params, | ||||
status=400) | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'missing oid and size in request data'} | |||
r180 | ||||
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) | ||||
r198 | assert json.loads(response.text) == { | |||
r1300 | 'message': 'oid `missing` does not exists in store' | |||
} | ||||
r180 | ||||
def test_app_verify_api_size_mismatch(self, git_lfs_app): | ||||
oid = 'existing' | ||||
r1194 | oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path | |||
r181 | if not os.path.isdir(os.path.dirname(oid_path)): | |||
os.makedirs(os.path.dirname(oid_path)) | ||||
r180 | with open(oid_path, 'wb') as f: | |||
r1048 | f.write(safe_bytes('OID_CONTENT')) | |||
r180 | ||||
params = {'oid': oid, 'size': '1024'} | ||||
response = git_lfs_app.post_json( | ||||
'/repo/info/lfs/verify', params=params, status=422) | ||||
r198 | assert json.loads(response.text) == { | |||
r1300 | 'message': 'requested file size mismatch store size:11 requested:1024' | |||
} | ||||
r180 | ||||
def test_app_verify_api(self, git_lfs_app): | ||||
oid = 'existing' | ||||
r1194 | oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path | |||
r181 | if not os.path.isdir(os.path.dirname(oid_path)): | |||
os.makedirs(os.path.dirname(oid_path)) | ||||
r180 | with open(oid_path, 'wb') as f: | |||
r1048 | f.write(safe_bytes('OID_CONTENT')) | |||
r180 | ||||
params = {'oid': oid, 'size': 11} | ||||
response = git_lfs_app.post_json( | ||||
'/repo/info/lfs/verify', params=params) | ||||
r198 | assert json.loads(response.text) == { | |||
r1300 | 'message': {'size': 11, 'oid': oid} | |||
} | ||||
r180 | ||||
def test_app_download_api_oid_not_existing(self, git_lfs_app): | ||||
oid = 'missing' | ||||
r1300 | response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}', status=404) | |||
r180 | ||||
r198 | assert json.loads(response.text) == { | |||
r1044 | 'message': 'requested file with oid `missing` not found in store'} | |||
r180 | ||||
def test_app_download_api(self, git_lfs_app): | ||||
oid = 'existing' | ||||
r1194 | oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path | |||
r181 | if not os.path.isdir(os.path.dirname(oid_path)): | |||
os.makedirs(os.path.dirname(oid_path)) | ||||
r180 | with open(oid_path, 'wb') as f: | |||
r1048 | f.write(safe_bytes('OID_CONTENT')) | |||
r180 | ||||
r1300 | response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}') | |||
r180 | assert response | |||
def test_app_upload(self, git_lfs_app): | ||||
r1300 | oid = '65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12' | |||
r180 | ||||
response = git_lfs_app.put( | ||||
r1300 | f'/repo/info/lfs/objects/{oid}', params='CONTENT') | |||
r180 | ||||
r1300 | assert json.loads(response.text) == {'upload': 'ok', 'state': 'written'} | |||
r180 | ||||
# verify that we actually wrote that OID | ||||
r1194 | oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path | |||
r180 | assert os.path.isfile(oid_path) | |||
assert 'CONTENT' == open(oid_path).read() | ||||
r1300 | ||||
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'} | ||||