220 lines
6.6 KiB
Python
220 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import timedelta
|
|
from unittest.mock import patch
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from govoplan_access.backend.db.models import Account, Group, User
|
|
from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_files.backend.db.models import FileAsset, FileShare
|
|
from govoplan_files.backend.storage.common import FileStorageError, utcnow
|
|
from govoplan_files.backend.storage.files import (
|
|
get_asset_for_user,
|
|
list_file_shares,
|
|
revoke_file_share,
|
|
share_file,
|
|
)
|
|
|
|
|
|
TENANT_ID = "tenant-1"
|
|
OWNER_ID = "owner-1"
|
|
RECIPIENT_ID = "recipient-1"
|
|
GROUP_ID = "group-1"
|
|
|
|
|
|
class FileShareLifecycleTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.engine = create_engine("sqlite:///:memory:", future=True)
|
|
Base.metadata.create_all(
|
|
bind=self.engine,
|
|
tables=[
|
|
Account.__table__,
|
|
User.__table__,
|
|
Group.__table__,
|
|
ChangeSequenceEntry.__table__,
|
|
FileAsset.__table__,
|
|
FileShare.__table__,
|
|
],
|
|
)
|
|
self.session = sessionmaker(bind=self.engine, future=True)()
|
|
self.asset = FileAsset(
|
|
id="file-1",
|
|
tenant_id=TENANT_ID,
|
|
owner_type="user",
|
|
owner_user_id=OWNER_ID,
|
|
display_path="shared.pdf",
|
|
filename="shared.pdf",
|
|
)
|
|
self.session.add(self.asset)
|
|
self.session.commit()
|
|
|
|
def tearDown(self) -> None:
|
|
self.session.close()
|
|
self.engine.dispose()
|
|
|
|
@patch(
|
|
"govoplan_files.backend.storage.files.user_group_ids",
|
|
return_value=[],
|
|
)
|
|
def test_expired_share_stops_access_immediately(self, _groups) -> None:
|
|
self.session.add(
|
|
FileShare(
|
|
id="expired-share",
|
|
tenant_id=TENANT_ID,
|
|
file_asset_id=self.asset.id,
|
|
target_type="user",
|
|
target_id=RECIPIENT_ID,
|
|
permission="read",
|
|
expires_at=utcnow() - timedelta(seconds=1),
|
|
)
|
|
)
|
|
self.session.commit()
|
|
|
|
with self.assertRaisesRegex(FileStorageError, "No access"):
|
|
get_asset_for_user(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
user_id=RECIPIENT_ID,
|
|
asset_id=self.asset.id,
|
|
)
|
|
self.assertEqual(
|
|
[],
|
|
list_file_shares(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset_id=self.asset.id,
|
|
),
|
|
)
|
|
|
|
@patch(
|
|
"govoplan_files.backend.storage.files.user_group_ids",
|
|
return_value=[GROUP_ID],
|
|
)
|
|
def test_independent_active_grant_survives_other_expiry(self, _groups) -> None:
|
|
self.session.add_all(
|
|
[
|
|
FileShare(
|
|
id="expired-user-share",
|
|
tenant_id=TENANT_ID,
|
|
file_asset_id=self.asset.id,
|
|
target_type="user",
|
|
target_id=RECIPIENT_ID,
|
|
permission="read",
|
|
expires_at=utcnow() - timedelta(seconds=1),
|
|
),
|
|
FileShare(
|
|
id="active-group-share",
|
|
tenant_id=TENANT_ID,
|
|
file_asset_id=self.asset.id,
|
|
target_type="group",
|
|
target_id=GROUP_ID,
|
|
permission="read",
|
|
expires_at=utcnow() + timedelta(hours=1),
|
|
),
|
|
]
|
|
)
|
|
self.session.commit()
|
|
|
|
result = get_asset_for_user(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
user_id=RECIPIENT_ID,
|
|
asset_id=self.asset.id,
|
|
)
|
|
|
|
self.assertEqual(self.asset.id, result.id)
|
|
self.assertEqual(
|
|
["active-group-share"],
|
|
[
|
|
share.id
|
|
for share in list_file_shares(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset_id=self.asset.id,
|
|
)
|
|
],
|
|
)
|
|
|
|
@patch(
|
|
"govoplan_files.backend.storage.files.ensure_share_target_exists",
|
|
return_value=None,
|
|
)
|
|
def test_revoke_is_idempotent_and_future_expiry_is_persisted(
|
|
self, _target_exists
|
|
) -> None:
|
|
expiry = utcnow() + timedelta(days=1)
|
|
share = share_file(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset=self.asset,
|
|
target_type="user",
|
|
target_id=RECIPIENT_ID,
|
|
permission="read",
|
|
user_id=OWNER_ID,
|
|
expires_at=expiry,
|
|
)
|
|
self.session.commit()
|
|
|
|
revoked, first_changed = revoke_file_share(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset_id=self.asset.id,
|
|
share_id=share.id,
|
|
user_id=OWNER_ID,
|
|
)
|
|
self.session.commit()
|
|
repeated, second_changed = revoke_file_share(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset_id=self.asset.id,
|
|
share_id=share.id,
|
|
user_id=OWNER_ID,
|
|
)
|
|
|
|
self.assertTrue(first_changed)
|
|
self.assertFalse(second_changed)
|
|
self.assertEqual(OWNER_ID, revoked.revoked_by_user_id)
|
|
self.assertEqual(revoked.revoked_at, repeated.revoked_at)
|
|
self.assertEqual([], list_file_shares(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset_id=self.asset.id,
|
|
))
|
|
self.assertEqual(
|
|
[share.id],
|
|
[
|
|
item.id
|
|
for item in list_file_shares(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset_id=self.asset.id,
|
|
include_inactive=True,
|
|
)
|
|
],
|
|
)
|
|
|
|
@patch(
|
|
"govoplan_files.backend.storage.files.ensure_share_target_exists",
|
|
return_value=None,
|
|
)
|
|
def test_past_expiry_is_rejected(self, _target_exists) -> None:
|
|
with self.assertRaisesRegex(FileStorageError, "future"):
|
|
share_file(
|
|
self.session,
|
|
tenant_id=TENANT_ID,
|
|
asset=self.asset,
|
|
target_type="user",
|
|
target_id=RECIPIENT_ID,
|
|
permission="read",
|
|
user_id=OWNER_ID,
|
|
expires_at=utcnow() - timedelta(seconds=1),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|