165 lines
7.0 KiB
Python
165 lines
7.0 KiB
Python
"""文档库权限/目录/分享的数据库集成测试;不连接开发库。"""
|
|
import io
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.models.models import SysDept, SysUser
|
|
from app.models.doclib import DoclibFolder, DoclibFile, DoclibShare
|
|
from app.services.doclib_service import DoclibService
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
engine = create_engine('sqlite://', poolclass=StaticPool, connect_args={'check_same_thread': False})
|
|
for table in [SysDept.__table__, SysUser.__table__, DoclibFolder.__table__, DoclibFile.__table__, DoclibShare.__table__]:
|
|
table.create(engine)
|
|
session = sessionmaker(bind=engine, expire_on_commit=False)()
|
|
for uid, tenant in [('owner', 'a'), ('reader', 'a'), ('stranger', 'a'), ('admin', 'a'), ('outsider', 'b')]:
|
|
session.add(SysUser(id=uid, tenant_no=tenant, user_no=uid, nick_name=uid, status='1'))
|
|
session.commit()
|
|
yield session
|
|
session.close()
|
|
engine.dispose()
|
|
|
|
|
|
def svc(db, uid='owner', tenant='a', role='admin'):
|
|
return DoclibService(db, {'user_id': uid, 'tenant_no': tenant, 'role': role, 'region_id': 'dept'})
|
|
|
|
|
|
def seed_file(db, service=None, folder_id='', name='制度.pdf'):
|
|
service = service or svc(db)
|
|
return service.create_file(name=name, folder_id=folder_id, size=12, content_type='application/pdf', object_key='opaque/'+name)
|
|
|
|
|
|
def denied(fn, status=404):
|
|
with pytest.raises(HTTPException) as exc:
|
|
fn()
|
|
assert exc.value.status_code == status
|
|
|
|
|
|
def test_private_by_default_and_no_storage_url(db):
|
|
doc = seed_file(db)
|
|
assert len(svc(db).list_files()['list']) == 1
|
|
assert svc(db, 'stranger').list_files(scope='shared')['total'] == 0
|
|
denied(lambda: svc(db, 'stranger').get_file(doc['id']))
|
|
assert 'objectKey' not in doc and 'url' not in doc
|
|
assert doc['canManage'] is True and doc['shares'] == []
|
|
|
|
|
|
def test_share_receiver_read_only_and_revocation(db):
|
|
doc = seed_file(db)
|
|
svc(db).set_shares(doc['id'], ['reader'])
|
|
reader = svc(db, 'reader')
|
|
assert reader.list_files(scope='shared')['total'] == 1
|
|
assert reader.get_file(doc['id'])['canManage'] is False
|
|
denied(lambda: reader.set_shares(doc['id'], ['stranger']), 403)
|
|
denied(lambda: reader.delete_files([doc['id']]), 403)
|
|
svc(db).set_shares(doc['id'], [])
|
|
assert reader.list_files(scope='shared')['total'] == 0
|
|
denied(lambda: reader.download_record(doc['id']))
|
|
|
|
|
|
def test_admin_only_inside_current_tenant(db):
|
|
local = seed_file(db)
|
|
remote = seed_file(db, svc(db, 'outsider', 'b'))
|
|
admin = svc(db, 'admin', role='super_admin')
|
|
assert admin.list_files(scope='all')['total'] == 1
|
|
assert admin.get_file(local['id'])['canManage'] is True
|
|
denied(lambda: admin.get_file(remote['id']))
|
|
denied(lambda: svc(db).list_files(scope='all'), 403)
|
|
admin.delete_files([local['id']])
|
|
assert db.get(DoclibFile, local['id']).del_flag == 1
|
|
denied(lambda: svc(db).get_file(local['id']))
|
|
|
|
|
|
def test_cross_tenant_and_inactive_share_is_atomic(db):
|
|
doc = seed_file(db)
|
|
svc(db).set_shares(doc['id'], ['reader'])
|
|
denied(lambda: svc(db).set_shares(doc['id'], ['stranger', 'outsider']), 400)
|
|
assert [u['id'] for u in svc(db).get_file(doc['id'])['shares']] == ['reader']
|
|
db.get(SysUser, 'stranger').status = '0'
|
|
db.commit()
|
|
denied(lambda: svc(db).set_shares(doc['id'], ['stranger']), 400)
|
|
assert {u['id'] for u in svc(db).users()} == {'reader', 'admin'}
|
|
|
|
|
|
def test_nested_folder_tree_shared_only_relevant_path(db):
|
|
service = svc(db)
|
|
parent = service.create_folder('公司制度', '')
|
|
child = service.create_folder('人事', parent['folderId'])
|
|
hidden = service.create_folder('财务', parent['folderId'])
|
|
doc = seed_file(db, folder_id=child['folderId'])
|
|
seed_file(db, folder_id=hidden['folderId'], name='预算.xlsx')
|
|
service.set_shares(doc['id'], ['reader'])
|
|
reader = svc(db, 'reader')
|
|
shared = next(n for n in reader.tree()['nodes'] if n['scope'] == 'shared')
|
|
assert shared['count'] == 1
|
|
assert [n['name'] for n in shared['children']] == ['公司制度']
|
|
assert [n['name'] for n in shared['children'][0]['children']] == ['人事']
|
|
assert reader.get_file(doc['id'])['folderPath'] == '公司制度 / 人事'
|
|
assert reader.list_files(scope='shared', folder_id=hidden['folderId'])['total'] == 0
|
|
denied(lambda: reader.create_folder('越权子目录', parent['folderId']), 403)
|
|
denied(lambda: reader.create_file(name='bad.txt', folder_id=child['folderId'], size=1, content_type='text/plain', object_key='x'), 403)
|
|
|
|
|
|
def test_folder_rename_empty_delete_and_duplicates(db):
|
|
service = svc(db)
|
|
folder = service.create_folder('资料', '')
|
|
fid = folder['folderId']
|
|
denied(lambda: service.create_folder('资料', ''), 409)
|
|
assert service.rename_folder(fid, '新资料')['fullPath'] == '新资料'
|
|
doc = seed_file(db, folder_id=fid)
|
|
denied(lambda: service.delete_folder(fid), 409)
|
|
service.delete_files([doc['id']])
|
|
service.delete_folder(fid)
|
|
assert db.get(DoclibFolder, fid).del_flag == 1
|
|
assert service.create_folder('新资料', '')['folderId'] != fid
|
|
|
|
|
|
def test_parent_cannot_be_deleted_with_child(db):
|
|
service = svc(db)
|
|
f = service.create_folder('父目录', '')
|
|
service.create_folder('子目录', f['folderId'])
|
|
denied(lambda: service.delete_folder(f['folderId']), 409)
|
|
|
|
|
|
def test_batch_delete_denied_is_all_or_nothing(db):
|
|
mine = seed_file(db)
|
|
other = seed_file(db, svc(db, 'reader'), name='other.txt')
|
|
svc(db, 'reader').set_shares(other['id'], ['owner'])
|
|
denied(lambda: svc(db).delete_files([mine['id'], other['id']]), 403)
|
|
assert svc(db).get_file(mine['id'])['name'] == '制度.pdf'
|
|
|
|
|
|
def test_paging_search_sort_and_soft_deleted_shares(db):
|
|
service = svc(db)
|
|
old = seed_file(db, name='旧制度.pdf')
|
|
new = seed_file(db, name='新制度.pdf')
|
|
db.get(DoclibFile, old['id']).create_time = datetime(2025, 1, 1)
|
|
db.commit()
|
|
page = service.list_files(keyword='制度', page=1, page_size=1)
|
|
assert page['total'] == 2 and page['list'][0]['id'] == new['id']
|
|
service.set_shares(new['id'], ['reader', 'reader'])
|
|
assert len(service.get_file(new['id'])['shares']) == 1
|
|
service.set_shares(new['id'], [])
|
|
assert db.query(DoclibShare).filter_by(file_id=new['id'], del_flag=0).count() == 0
|
|
service.set_shares(new['id'], ['reader'])
|
|
assert svc(db, 'reader').list_files(scope='shared')['total'] == 1
|
|
|
|
|
|
def test_schema_rejects_client_tenant_and_invalid_names():
|
|
from pydantic import ValidationError
|
|
from app.schemas.doclib import FolderCreateBody, FolderUpdateBody, ShareBody
|
|
with pytest.raises(ValidationError):
|
|
FolderCreateBody(name='资料', tenant_no='b')
|
|
for name in ['', ' ', '../secret', 'A/B', 'A\\B']:
|
|
with pytest.raises(ValidationError):
|
|
FolderUpdateBody(name=name)
|
|
assert FolderCreateBody(name=' 资料 ').name == '资料'
|
|
assert ShareBody(userIds=[]).userIds == []
|