test_doclib.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """文档库权限/目录/分享的数据库集成测试;不连接开发库。"""
  2. import io
  3. from datetime import datetime
  4. import pytest
  5. from fastapi import HTTPException
  6. from sqlalchemy import create_engine
  7. from sqlalchemy.orm import sessionmaker
  8. from sqlalchemy.pool import StaticPool
  9. from app.models.models import SysDept, SysUser
  10. from app.models.doclib import DoclibFolder, DoclibFile, DoclibShare
  11. from app.services.doclib_service import DoclibService
  12. @pytest.fixture
  13. def db():
  14. engine = create_engine('sqlite://', poolclass=StaticPool, connect_args={'check_same_thread': False})
  15. for table in [SysDept.__table__, SysUser.__table__, DoclibFolder.__table__, DoclibFile.__table__, DoclibShare.__table__]:
  16. table.create(engine)
  17. session = sessionmaker(bind=engine, expire_on_commit=False)()
  18. for uid, tenant in [('owner', 'a'), ('reader', 'a'), ('stranger', 'a'), ('admin', 'a'), ('outsider', 'b')]:
  19. session.add(SysUser(id=uid, tenant_no=tenant, user_no=uid, nick_name=uid, status='1'))
  20. session.commit()
  21. yield session
  22. session.close()
  23. engine.dispose()
  24. def svc(db, uid='owner', tenant='a', role='admin'):
  25. return DoclibService(db, {'user_id': uid, 'tenant_no': tenant, 'role': role, 'region_id': 'dept'})
  26. def seed_file(db, service=None, folder_id='', name='制度.pdf'):
  27. service = service or svc(db)
  28. return service.create_file(name=name, folder_id=folder_id, size=12, content_type='application/pdf', object_key='opaque/'+name)
  29. def denied(fn, status=404):
  30. with pytest.raises(HTTPException) as exc:
  31. fn()
  32. assert exc.value.status_code == status
  33. def test_private_by_default_and_no_storage_url(db):
  34. doc = seed_file(db)
  35. assert len(svc(db).list_files()['list']) == 1
  36. assert svc(db, 'stranger').list_files(scope='shared')['total'] == 0
  37. denied(lambda: svc(db, 'stranger').get_file(doc['id']))
  38. assert 'objectKey' not in doc and 'url' not in doc
  39. assert doc['canManage'] is True and doc['shares'] == []
  40. def test_share_receiver_read_only_and_revocation(db):
  41. doc = seed_file(db)
  42. svc(db).set_shares(doc['id'], ['reader'])
  43. reader = svc(db, 'reader')
  44. assert reader.list_files(scope='shared')['total'] == 1
  45. assert reader.get_file(doc['id'])['canManage'] is False
  46. denied(lambda: reader.set_shares(doc['id'], ['stranger']), 403)
  47. denied(lambda: reader.delete_files([doc['id']]), 403)
  48. svc(db).set_shares(doc['id'], [])
  49. assert reader.list_files(scope='shared')['total'] == 0
  50. denied(lambda: reader.download_record(doc['id']))
  51. def test_admin_only_inside_current_tenant(db):
  52. local = seed_file(db)
  53. remote = seed_file(db, svc(db, 'outsider', 'b'))
  54. admin = svc(db, 'admin', role='super_admin')
  55. assert admin.list_files(scope='all')['total'] == 1
  56. assert admin.get_file(local['id'])['canManage'] is True
  57. denied(lambda: admin.get_file(remote['id']))
  58. denied(lambda: svc(db).list_files(scope='all'), 403)
  59. admin.delete_files([local['id']])
  60. assert db.get(DoclibFile, local['id']).del_flag == 1
  61. denied(lambda: svc(db).get_file(local['id']))
  62. def test_cross_tenant_and_inactive_share_is_atomic(db):
  63. doc = seed_file(db)
  64. svc(db).set_shares(doc['id'], ['reader'])
  65. denied(lambda: svc(db).set_shares(doc['id'], ['stranger', 'outsider']), 400)
  66. assert [u['id'] for u in svc(db).get_file(doc['id'])['shares']] == ['reader']
  67. db.get(SysUser, 'stranger').status = '0'
  68. db.commit()
  69. denied(lambda: svc(db).set_shares(doc['id'], ['stranger']), 400)
  70. assert {u['id'] for u in svc(db).users()} == {'reader', 'admin'}
  71. def test_nested_folder_tree_shared_only_relevant_path(db):
  72. service = svc(db)
  73. parent = service.create_folder('公司制度', '')
  74. child = service.create_folder('人事', parent['folderId'])
  75. hidden = service.create_folder('财务', parent['folderId'])
  76. doc = seed_file(db, folder_id=child['folderId'])
  77. seed_file(db, folder_id=hidden['folderId'], name='预算.xlsx')
  78. service.set_shares(doc['id'], ['reader'])
  79. reader = svc(db, 'reader')
  80. shared = next(n for n in reader.tree()['nodes'] if n['scope'] == 'shared')
  81. assert shared['count'] == 1
  82. assert [n['name'] for n in shared['children']] == ['公司制度']
  83. assert [n['name'] for n in shared['children'][0]['children']] == ['人事']
  84. assert reader.get_file(doc['id'])['folderPath'] == '公司制度 / 人事'
  85. assert reader.list_files(scope='shared', folder_id=hidden['folderId'])['total'] == 0
  86. denied(lambda: reader.create_folder('越权子目录', parent['folderId']), 403)
  87. denied(lambda: reader.create_file(name='bad.txt', folder_id=child['folderId'], size=1, content_type='text/plain', object_key='x'), 403)
  88. def test_folder_rename_empty_delete_and_duplicates(db):
  89. service = svc(db)
  90. folder = service.create_folder('资料', '')
  91. fid = folder['folderId']
  92. denied(lambda: service.create_folder('资料', ''), 409)
  93. assert service.rename_folder(fid, '新资料')['fullPath'] == '新资料'
  94. doc = seed_file(db, folder_id=fid)
  95. denied(lambda: service.delete_folder(fid), 409)
  96. service.delete_files([doc['id']])
  97. service.delete_folder(fid)
  98. assert db.get(DoclibFolder, fid).del_flag == 1
  99. assert service.create_folder('新资料', '')['folderId'] != fid
  100. def test_parent_cannot_be_deleted_with_child(db):
  101. service = svc(db)
  102. f = service.create_folder('父目录', '')
  103. service.create_folder('子目录', f['folderId'])
  104. denied(lambda: service.delete_folder(f['folderId']), 409)
  105. def test_batch_delete_denied_is_all_or_nothing(db):
  106. mine = seed_file(db)
  107. other = seed_file(db, svc(db, 'reader'), name='other.txt')
  108. svc(db, 'reader').set_shares(other['id'], ['owner'])
  109. denied(lambda: svc(db).delete_files([mine['id'], other['id']]), 403)
  110. assert svc(db).get_file(mine['id'])['name'] == '制度.pdf'
  111. def test_paging_search_sort_and_soft_deleted_shares(db):
  112. service = svc(db)
  113. old = seed_file(db, name='旧制度.pdf')
  114. new = seed_file(db, name='新制度.pdf')
  115. db.get(DoclibFile, old['id']).create_time = datetime(2025, 1, 1)
  116. db.commit()
  117. page = service.list_files(keyword='制度', page=1, page_size=1)
  118. assert page['total'] == 2 and page['list'][0]['id'] == new['id']
  119. service.set_shares(new['id'], ['reader', 'reader'])
  120. assert len(service.get_file(new['id'])['shares']) == 1
  121. service.set_shares(new['id'], [])
  122. assert db.query(DoclibShare).filter_by(file_id=new['id'], del_flag=0).count() == 0
  123. service.set_shares(new['id'], ['reader'])
  124. assert svc(db, 'reader').list_files(scope='shared')['total'] == 1
  125. def test_schema_rejects_client_tenant_and_invalid_names():
  126. from pydantic import ValidationError
  127. from app.schemas.doclib import FolderCreateBody, FolderUpdateBody, ShareBody
  128. with pytest.raises(ValidationError):
  129. FolderCreateBody(name='资料', tenant_no='b')
  130. for name in ['', ' ', '../secret', 'A/B', 'A\\B']:
  131. with pytest.raises(ValidationError):
  132. FolderUpdateBody(name=name)
  133. assert FolderCreateBody(name=' 资料 ').name == '资料'
  134. assert ShareBody(userIds=[]).userIds == []