Files
gh-christianlouis-docuelevate/tests/test_sharing.py
copilot-swe-agent[bot] c7d3ec57c3 fix: restore all code deleted/truncated by d2217531 Jules SSRF commit
Commit d2217531 (google-labs-jules SSRF fix) catastrophically deleted
11,500+ lines across 100+ files while fixing an unrelated IMAP issue.

Restored from d2217531^ (pre-bad-commit state):

Deleted files (fully restored):
- app/api/{automation,classification_rules,comments,sharing}.py
- app/middleware/upload_rate_limit.py
- app/tasks/{automation_tasks,classify_document}.py
- app/utils/{automation_hooks,classification_rules}.py
- docs/AppleAppStoreCompliance.md
- frontend/input.css, package.json, package-lock.json, tailwind.config.js
- frontend/static/js/{annotations,claim,comments,sharing}.js
- frontend/templates/{admin_connections,file_annotations,file_summary}.html
- tests/{test_api_files_comprehensive,test_auth_extended,test_sharing,
         test_comments,test_connections,test_imap_profiles,test_api_sessions,
         test_automation,test_classification_rules,test_api_advanced_filters,
         test_api_classification_rules,test_upload_rate_limit,test_api_dropbox,
         test_classify_document,test_comments_ui,test_upload_to_icloud,
         test_api_onedrive_comprehensive,test_frontend_build,test_sentry,
         test_diagnostic,test_database,test_views_dropbox,test_local_auth}.py

Truncated files (content restored):
- app/{auth,config,main,models,celery_worker,database}.py
- app/api/{__init__,api_tokens,diagnostic,dropbox,files,google_drive,
           integrations,local_auth,mobile,onedrive,pipelines,qr_auth,
           settings,url_upload}.py
- app/middleware/upload_rate_limit.py
- app/tasks/upload_to_nextcloud.py
- app/utils/{allowed_types,settings_service,settings_sync,user_scope,webhook}.py
- app/views/{base,dropbox,files,google_drive,onedrive,settings}.py
- docs/{API,AuthenticationSetup,ConfigurationGuide,DatabaseConfiguration,
        DeploymentGuide,DropboxSetup,GoogleDriveSetup,KubernetesDeployment,
        MobileApp,OneDriveSetup,ProductionReadiness,SentrySetup,
        SocialLoginSetup,UserGuide}.md
- frontend/static/{js/upload.js,styles.css}
- frontend/templates/{api_tokens,base,devices,dropbox,dropbox_callback,
                      file_view,files,google_drive,onedrive,onedrive_callback,
                      signup}.html
- frontend/translations/en.json
- migrations/env.py
- tests/{conftest,test_api_integrations,test_api_mobile,test_api_settings,
         test_api_tokens,test_audit_logs,test_duplicates,test_imap_tasks,
         test_setup_wizard,test_views_files_comprehensive}.py

Security fixes kept from post-d2217531 commits:
- app/utils/network.py: DNS SSRF fail-secure fix (06b0fced)
- app/utils/file_operations.py: path traversal fix (1018ea17)
- tests/test_imap_tasks.py: re-applied 4 is_private_ip mock patches

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/51133dd8-9bec-41ab-aa10-3de753634187
2026-03-23 23:52:39 +00:00

692 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for the file sharing API (FileShare model and /api/files/{id}/shares endpoints)."""
import pytest
from app.models import FILE_SHARE_ROLE_EDITOR, FILE_SHARE_ROLE_VIEWER, FileRecord, FileShare, UserProfile
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _create_file(db_session, owner_id="owner1") -> FileRecord:
"""Create a minimal owned FileRecord."""
f = FileRecord(
owner_id=owner_id,
filehash="sharehash",
original_filename="shared.pdf",
local_filename="shared.pdf",
file_size=1024,
mime_type="application/pdf",
)
db_session.add(f)
db_session.commit()
db_session.refresh(f)
return f
def _create_unowned_file(db_session) -> FileRecord:
"""Create a FileRecord with no owner."""
f = FileRecord(
owner_id=None,
filehash="unownedhash",
original_filename="unowned.pdf",
local_filename="unowned.pdf",
file_size=512,
mime_type="application/pdf",
)
db_session.add(f)
db_session.commit()
db_session.refresh(f)
return f
def _create_profile(db_session, user_id: str, display_name: str | None = None) -> UserProfile:
p = UserProfile(user_id=user_id, display_name=display_name)
db_session.add(p)
db_session.commit()
db_session.refresh(p)
return p
# ---------------------------------------------------------------------------
# get_file_role helper
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGetFileRole:
"""Tests for the get_file_role() utility."""
def test_owner_returns_owner(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import get_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert get_file_role(f, "alice", db_session) == "owner"
def test_non_owner_no_share_returns_none(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import get_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert get_file_role(f, "bob", db_session) is None
def test_shared_viewer_returns_viewer(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import get_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role=FILE_SHARE_ROLE_VIEWER)
db_session.add(share)
db_session.commit()
assert get_file_role(f, "bob", db_session) == FILE_SHARE_ROLE_VIEWER
def test_shared_editor_returns_editor(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import get_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="carol", role=FILE_SHARE_ROLE_EDITOR)
db_session.add(share)
db_session.commit()
assert get_file_role(f, "carol", db_session) == FILE_SHARE_ROLE_EDITOR
def test_unowned_file_returns_viewer_when_setting_allows(self, db_session, monkeypatch):
from app.utils import user_scope
monkeypatch.setattr(user_scope.settings, "multi_user_enabled", True)
monkeypatch.setattr(user_scope.settings, "unowned_docs_visible_to_all", True)
f = _create_unowned_file(db_session)
role = user_scope.get_file_role(f, "anyone", db_session)
assert role == FILE_SHARE_ROLE_VIEWER
def test_none_user_returns_none(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import get_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert get_file_role(f, None, db_session) is None
# ---------------------------------------------------------------------------
# has_file_role helper
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestHasFileRole:
"""Tests for the has_file_role() utility."""
def test_owner_satisfies_viewer(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import has_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert has_file_role(f, "alice", db_session, minimum_role="viewer") is True
def test_owner_satisfies_editor(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import has_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert has_file_role(f, "alice", db_session, minimum_role="editor") is True
def test_owner_satisfies_owner(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import has_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert has_file_role(f, "alice", db_session, minimum_role="owner") is True
def test_viewer_does_not_satisfy_editor(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import has_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role=FILE_SHARE_ROLE_VIEWER)
db_session.add(share)
db_session.commit()
assert has_file_role(f, "bob", db_session, minimum_role="editor") is False
def test_editor_satisfies_viewer(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import has_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="carol", role=FILE_SHARE_ROLE_EDITOR)
db_session.add(share)
db_session.commit()
assert has_file_role(f, "carol", db_session, minimum_role="viewer") is True
def test_no_access_returns_false(self, db_session, monkeypatch):
from app.config import settings as real_settings
from app.utils.user_scope import has_file_role
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
assert has_file_role(f, "stranger", db_session) is False
# ---------------------------------------------------------------------------
# List shares
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestListShares:
"""Tests for GET /api/files/{file_id}/shares."""
def test_owner_can_list_shares(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role=FILE_SHARE_ROLE_VIEWER)
db_session.add(share)
db_session.commit()
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
resp = client.get(f"/api/files/{f.id}/shares")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) == 1
assert data[0]["shared_with_user_id"] == "bob"
assert data[0]["role"] == FILE_SHARE_ROLE_VIEWER
def test_non_owner_cannot_list_shares(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
f = _create_file(db_session, owner_id="alice")
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "bob")
# bob has no access to alice's file — the get_file_role call in list_shares
# will return None for bob, giving 404 not 403 (file not found for bob)
resp = client.get(f"/api/files/{f.id}/shares")
assert resp.status_code in (403, 404)
def test_list_shares_file_not_found(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
resp = client.get("/api/files/99999/shares")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# Create share
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestCreateShare:
"""Tests for POST /api/files/{file_id}/shares."""
def test_owner_can_share_with_viewer(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": "bob", "role": "viewer"},
)
assert resp.status_code == 201
data = resp.json()
assert data["shared_with_user_id"] == "bob"
assert data["role"] == "viewer"
assert data["file_id"] == f.id
def test_owner_can_share_with_editor(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": "carol", "role": "editor"},
)
assert resp.status_code == 201
assert resp.json()["role"] == "editor"
def test_non_owner_cannot_share(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "bob")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": "carol", "role": "viewer"},
)
# bob doesn't own the file; get_file_role returns None → 404 for non-owner
assert resp.status_code in (403, 404)
def test_share_with_self_rejected(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": "alice", "role": "viewer"},
)
assert resp.status_code == 422
def test_invalid_role_rejected(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": "bob", "role": "admin"},
)
assert resp.status_code == 422
def test_empty_user_id_rejected(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": " ", "role": "viewer"},
)
assert resp.status_code == 422
def test_duplicate_share_updates_role(self, client, db_session, monkeypatch):
"""Creating a share for an already-shared user updates the role."""
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
resp = client.post(
f"/api/files/{f.id}/shares",
json={"shared_with_user_id": "bob", "role": "editor"},
)
assert resp.status_code == 201
assert resp.json()["role"] == "editor"
# ---------------------------------------------------------------------------
# Update share role
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestUpdateShare:
"""Tests for PUT /api/files/{file_id}/shares/{share_id}."""
def test_owner_can_update_role(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
db_session.refresh(share)
resp = client.put(
f"/api/files/{f.id}/shares/{share.id}",
json={"role": "editor"},
)
assert resp.status_code == 200
assert resp.json()["role"] == "editor"
def test_non_owner_cannot_update_role(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "bob")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
db_session.refresh(share)
resp = client.put(
f"/api/files/{f.id}/shares/{share.id}",
json={"role": "editor"},
)
assert resp.status_code in (403, 404)
def test_invalid_role_rejected(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
db_session.refresh(share)
resp = client.put(
f"/api/files/{f.id}/shares/{share.id}",
json={"role": "superuser"},
)
assert resp.status_code == 422
def test_share_not_found(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.put(
f"/api/files/{f.id}/shares/99999",
json={"role": "editor"},
)
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# Revoke share
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestRevokeShare:
"""Tests for DELETE /api/files/{file_id}/shares/{share_id}."""
def test_owner_can_revoke(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
db_session.refresh(share)
resp = client.delete(f"/api/files/{f.id}/shares/{share.id}")
assert resp.status_code == 200
assert resp.json()["status"] == "success"
# Confirm the share is gone
db_session.expire_all()
assert db_session.query(FileShare).filter(FileShare.id == share.id).first() is None
def test_non_owner_cannot_revoke(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "carol")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
db_session.refresh(share)
resp = client.delete(f"/api/files/{f.id}/shares/{share.id}")
assert resp.status_code in (403, 404)
def test_revoke_not_found(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.delete(f"/api/files/{f.id}/shares/99999")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# List shared-with
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestListSharedWith:
"""Tests for GET /api/files/{file_id}/shared-with."""
def test_owner_can_see_shared_with(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
_create_profile(db_session, "bob", "Bob Smith")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
resp = client.get(f"/api/files/{f.id}/shared-with")
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["user_id"] == "bob"
assert data[0]["display_name"] == "Bob Smith"
assert data[0]["role"] == "viewer"
def test_viewer_can_see_shared_with(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "bob")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
resp = client.get(f"/api/files/{f.id}/shared-with")
assert resp.status_code == 200
def test_unauthorized_user_gets_404(self, client, db_session, monkeypatch):
import app.api.sharing as sharing_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(sharing_mod, "get_current_owner_id", lambda req: "stranger")
f = _create_file(db_session, owner_id="alice")
resp = client.get(f"/api/files/{f.id}/shared-with")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# Auto-share on mention
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestAutoShareOnMention:
"""Tests that @mentioning a user in a comment auto-shares the file."""
def test_mention_auto_shares_with_viewer(self, client, db_session, monkeypatch):
"""When multi_user_enabled is True, mentioning a user auto-shares the file."""
import app.api.comments as comments_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(comments_mod, "get_current_owner_id", lambda req: "alice")
monkeypatch.setattr(comments_mod, "get_current_user_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/comments",
json={"body": "Hey @bob, please look at this."},
)
assert resp.status_code == 201
# bob should now have a viewer share on the file
share = (
db_session.query(FileShare)
.filter(FileShare.file_id == f.id, FileShare.shared_with_user_id == "bob")
.first()
)
assert share is not None
assert share.role == FILE_SHARE_ROLE_VIEWER
def test_mention_does_not_duplicate_share(self, client, db_session, monkeypatch):
"""Mentioning a user that already has a share does not create a duplicate."""
import app.api.comments as comments_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(comments_mod, "get_current_owner_id", lambda req: "alice")
monkeypatch.setattr(comments_mod, "get_current_user_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
existing = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="editor")
db_session.add(existing)
db_session.commit()
existing_id = existing.id
resp = client.post(
f"/api/files/{f.id}/comments",
json={"body": "Hey @bob again!"},
)
assert resp.status_code == 201
shares = (
db_session.query(FileShare).filter(FileShare.file_id == f.id, FileShare.shared_with_user_id == "bob").all()
)
assert len(shares) == 1
assert shares[0].id == existing_id
assert shares[0].role == "editor" # role unchanged
def test_mention_skipped_when_single_user_mode(self, client, db_session, monkeypatch):
import app.api.comments as comments_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", False)
monkeypatch.setattr(comments_mod, "get_current_owner_id", lambda req: "alice")
monkeypatch.setattr(comments_mod, "get_current_user_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.post(
f"/api/files/{f.id}/comments",
json={"body": "Hey @carol, look here."},
)
assert resp.status_code == 201
share = (
db_session.query(FileShare)
.filter(FileShare.file_id == f.id, FileShare.shared_with_user_id == "carol")
.first()
)
assert share is None
# ---------------------------------------------------------------------------
# Delete file owner-only enforcement
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDeleteFileOwnerOnly:
"""Ensure non-owners (shared viewers/editors) cannot delete files."""
def test_owner_can_delete_in_multi_user_mode(self, client, db_session, monkeypatch):
import app.api.files as files_mod
import app.utils.user_scope as user_scope_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(real_settings, "allow_file_delete", True)
monkeypatch.setattr(files_mod, "get_current_owner_id", lambda req: "alice")
monkeypatch.setattr(user_scope_mod, "get_current_owner_id", lambda req: "alice")
f = _create_file(db_session, owner_id="alice")
resp = client.delete(f"/api/files/{f.id}")
assert resp.status_code == 200
def test_viewer_cannot_delete(self, client, db_session, monkeypatch):
import app.api.files as files_mod
import app.utils.user_scope as user_scope_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(real_settings, "allow_file_delete", True)
monkeypatch.setattr(files_mod, "get_current_owner_id", lambda req: "bob")
monkeypatch.setattr(user_scope_mod, "get_current_owner_id", lambda req: "bob")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="bob", role="viewer")
db_session.add(share)
db_session.commit()
resp = client.delete(f"/api/files/{f.id}")
assert resp.status_code == 403
def test_editor_cannot_delete(self, client, db_session, monkeypatch):
import app.api.files as files_mod
import app.utils.user_scope as user_scope_mod
from app.config import settings as real_settings
monkeypatch.setattr(real_settings, "multi_user_enabled", True)
monkeypatch.setattr(real_settings, "allow_file_delete", True)
monkeypatch.setattr(files_mod, "get_current_owner_id", lambda req: "carol")
monkeypatch.setattr(user_scope_mod, "get_current_owner_id", lambda req: "carol")
f = _create_file(db_session, owner_id="alice")
share = FileShare(file_id=f.id, owner_id="alice", shared_with_user_id="carol", role="editor")
db_session.add(share)
db_session.commit()
resp = client.delete(f"/api/files/{f.id}")
assert resp.status_code == 403