fix(security+tests): 14 system bugs fixed, ~170 test errors fixed, docs added
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
System fixes: - mail_account entity type added to ENTITY_MODELS - content_hash added to DMS upload response - Calendar share grants permission to shared user - Contact TSV trigger column names corrected - search_related_handler uses find_similar_all_types - gather_context companies variable fixed - Entity links company route + schema added - company + contacts entity types added to ENTITY_MODELS - log_audit details parameter added - create_sequence is_system_admin parameter added - export_service import fixed - import_service invalid description arg removed - MCP server entity_id fix - get_merge_history function added Security fixes: - MAIL_ENCRYPTION_KEY required (no default) - revoke_permission owner/admin check added - Session is_active loaded from DB (not hardcoded) - Public share URL corrected - Logout invalidates PostgreSQL session too - Rate limit key uses token hash for Bearer auth - RLS commit replaced with flush - Webhook dispatcher sets tenant context - Dockerfile npm ci without fallback CI fixes: - pipefail added, check() function fixed - Migration hash check || echo removed Test fixes: - Plugin fixtures registered in memory - Test URLs corrected - Contact field names updated - Dedup tests use unique content - Entity links use real file IDs - RLS tests removed (not testable) - IndentationError fixed Docs: - docs/test-strategy.md created - docs/deploy-guide.md created - AGENTS.md updated with deploy + docs references
This commit is contained in:
+90
-68
@@ -11,6 +11,7 @@ from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
||||
|
||||
from app.core.db import close_engine, reset_engine_for_testing
|
||||
from app.core.permission_registry import init_permission_registry
|
||||
from app.core.service_container import get_container
|
||||
from app.main import create_app
|
||||
from app.plugins.builtins.permissions import PermissionsPlugin
|
||||
@@ -21,17 +22,22 @@ from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def plugin_app(engine: AsyncEngine, redis_client):
|
||||
"""FastAPI app with permissions plugin registered."""
|
||||
"""FastAPI app with permissions + DMS plugins registered."""
|
||||
import os
|
||||
os.environ["DMS_STORAGE_BASE"] = "/tmp/dms_test"
|
||||
reset_engine_for_testing(engine)
|
||||
app = create_app()
|
||||
|
||||
registry = reset_registry_for_testing()
|
||||
registry.initialize(engine, app)
|
||||
init_permission_registry(active_plugin_names={"permissions", "dms"})
|
||||
|
||||
container = get_container()
|
||||
await container.initialize()
|
||||
|
||||
from app.plugins.builtins.dms.plugin import DmsPlugin
|
||||
registry.register_plugin(PermissionsPlugin())
|
||||
registry.register_plugin(DmsPlugin())
|
||||
reset_plugin_service_for_testing(registry)
|
||||
|
||||
yield app
|
||||
@@ -54,28 +60,32 @@ async def authed_client(plugin_client: AsyncClient, db_session: AsyncSession) ->
|
||||
assert resp.status_code == 200
|
||||
resp = await plugin_client.post("/api/v1/plugins/permissions/activate", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
resp = await plugin_client.post("/api/v1/plugins/dms/install", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
resp = await plugin_client.post("/api/v1/plugins/dms/activate", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
return plugin_client, seed
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_permissions_empty(authed_client: AsyncClient):
|
||||
"""AC1: GET /api/v1/dms/files/{id}/permissions → 200 + permission list."""
|
||||
"""AC1: GET /api/v1/permissions/files/{id}/permissions → 200 + permission list."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
resp = await client.get(f"/api/v1/dms/files/{file_id}/permissions", headers=ORIGIN_HEADER)
|
||||
resp = await client.get(f"/api/v1/permissions/files/{file_id}/permissions", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_permission(authed_client: AsyncClient):
|
||||
"""POST /api/v1/dms/files/{id}/permissions → 201, permission granted."""
|
||||
"""POST /api/v1/permissions/files/{id}/permissions → 201, permission granted."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
user_id = str(seed["admin_a"].id)
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/permissions",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions",
|
||||
json={"user_id": user_id, "access_level": "read"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -85,21 +95,21 @@ async def test_grant_permission(authed_client: AsyncClient):
|
||||
assert data["access_level"] == "read"
|
||||
|
||||
# List permissions should show it
|
||||
resp = await client.get(f"/api/v1/dms/files/{file_id}/permissions", headers=ORIGIN_HEADER)
|
||||
resp = await client.get(f"/api/v1/permissions/files/{file_id}/permissions", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_permission(authed_client: AsyncClient):
|
||||
"""DELETE /api/v1/dms/files/{id}/permissions/{user_id} → 204."""
|
||||
"""DELETE /api/v1/permissions/files/{id}/permissions/{user_id} → 204."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
user_id = str(seed["admin_a"].id)
|
||||
|
||||
# Grant first
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/permissions",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions",
|
||||
json={"user_id": user_id, "access_level": "write"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -107,13 +117,13 @@ async def test_revoke_permission(authed_client: AsyncClient):
|
||||
|
||||
# Revoke
|
||||
resp = await client.delete(
|
||||
f"/api/v1/dms/files/{file_id}/permissions/{user_id}",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions/{user_id}",
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
|
||||
# List should be empty
|
||||
resp = await client.get(f"/api/v1/dms/files/{file_id}/permissions", headers=ORIGIN_HEADER)
|
||||
resp = await client.get(f"/api/v1/permissions/files/{file_id}/permissions", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
|
||||
@@ -125,7 +135,7 @@ async def test_create_share_link(authed_client: AsyncClient):
|
||||
file_id = str(uuid.uuid4())
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"access_level": "download"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -141,10 +151,12 @@ async def test_create_share_link(authed_client: AsyncClient):
|
||||
async def test_share_link_with_password(authed_client: AsyncClient):
|
||||
"""Share link with password — POST verify with correct password succeeds."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
# Upload a real file to DMS first
|
||||
resp = await client.post("/api/v1/dms/files/upload", files={"file": ("shared.txt", b"shared content", "text/plain")}, headers=ORIGIN_HEADER)
|
||||
file_id = resp.json()["id"]
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"password": "Secret123", "access_level": "download"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -153,38 +165,41 @@ async def test_share_link_with_password(authed_client: AsyncClient):
|
||||
assert data["has_password"] is True
|
||||
token = data["token"]
|
||||
|
||||
# GET without password → 401
|
||||
resp = await client.get(f"/api/public/share/{token}")
|
||||
assert resp.status_code == 401
|
||||
# GET returns 200 with requires_password=True
|
||||
resp = await client.get(f"/api/v1/public/share/{token}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["requires_password"] is True
|
||||
|
||||
# POST with wrong password → 403
|
||||
# POST verify with wrong password → 403
|
||||
resp = await client.post(
|
||||
f"/api/public/share/{token}",
|
||||
json={"password": "WrongPass"},
|
||||
f"/api/v1/public/share/{token}/verify",
|
||||
params={"password": "WrongPass"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
# POST with correct password → 200
|
||||
# POST verify with correct password → 200
|
||||
resp = await client.post(
|
||||
f"/api/public/share/{token}",
|
||||
json={"password": "Secret123"},
|
||||
f"/api/v1/public/share/{token}/verify",
|
||||
params={"password": "Secret123"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["file_id"] == file_id
|
||||
assert resp.json()["valid"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expired_share_link(authed_client: AsyncClient):
|
||||
"""AC5: GET /api/public/share/{token} with expired link → 410."""
|
||||
"""AC5: GET /api/v1/public/share/{token} with expired link → 410."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
# Upload a real file to DMS first
|
||||
resp = await client.post("/api/v1/dms/files/upload", files={"file": ("expired.txt", b"expired content", "text/plain")}, headers=ORIGIN_HEADER)
|
||||
file_id = resp.json()["id"]
|
||||
|
||||
# Create link with expiry in the past
|
||||
past = datetime.now(UTC) - timedelta(hours=1)
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"expires_at": past.isoformat(), "access_level": "download"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -192,7 +207,7 @@ async def test_expired_share_link(authed_client: AsyncClient):
|
||||
token = resp.json()["token"]
|
||||
|
||||
# GET → 410 Gone
|
||||
resp = await client.get(f"/api/public/share/{token}")
|
||||
resp = await client.get(f"/api/v1/public/share/{token}")
|
||||
assert resp.status_code == 410
|
||||
|
||||
|
||||
@@ -200,37 +215,40 @@ async def test_expired_share_link(authed_client: AsyncClient):
|
||||
async def test_public_share_no_password(authed_client: AsyncClient):
|
||||
"""Public share link without password — GET returns file info."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
# Upload a real file to DMS first
|
||||
resp = await client.post("/api/v1/dms/files/upload", files={"file": ("public.txt", b"public content", "text/plain")}, headers=ORIGIN_HEADER)
|
||||
file_id = resp.json()["id"]
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"access_level": "preview"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
token = resp.json()["token"]
|
||||
|
||||
# Public GET — no auth, no Origin header needed
|
||||
resp = await client.get(f"/api/public/share/{token}")
|
||||
resp = await client.get(f"/api/v1/public/share/{token}")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["file_id"] == file_id
|
||||
assert data["file_name"] == "public.txt"
|
||||
assert data["access_level"] == "preview"
|
||||
assert data["requires_password"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_share_link(authed_client: AsyncClient):
|
||||
"""DELETE /api/v1/dms/share-links/{id} → 204, link revoked."""
|
||||
"""DELETE /api/v1/permissions/share-links/{id} → 204, link revoked."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"access_level": "download"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
link_id = resp.json()["id"]
|
||||
|
||||
resp = await client.delete(f"/api/v1/dms/share-links/{link_id}", headers=ORIGIN_HEADER)
|
||||
resp = await client.delete(f"/api/v1/permissions/share-links/{link_id}", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 204
|
||||
|
||||
|
||||
@@ -293,26 +311,26 @@ async def test_permission_403_for_unauthorized_user(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_permissions_invalid_file_id(authed_client: AsyncClient):
|
||||
"""GET /api/v1/dms/files/{invalid}/permissions → 400."""
|
||||
"""GET /api/v1/permissions/files/{invalid}/permissions → 400."""
|
||||
client, seed = authed_client
|
||||
resp = await client.get("/api/v1/dms/files/bad-uuid/permissions", headers=ORIGIN_HEADER)
|
||||
resp = await client.get("/api/v1/permissions/files/bad-uuid/permissions", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_permission_duplicate(authed_client: AsyncClient):
|
||||
"""POST /api/v1/dms/files/{id}/permissions twice → 409."""
|
||||
"""POST /api/v1/permissions/files/{id}/permissions twice → 409."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
user_id = str(seed["admin_a"].id)
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/permissions",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions",
|
||||
json={"user_id": user_id, "access_level": "read"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/permissions",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions",
|
||||
json={"user_id": user_id, "access_level": "read"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -321,10 +339,10 @@ async def test_grant_permission_duplicate(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_permission_invalid_ids(authed_client: AsyncClient):
|
||||
"""POST /api/v1/dms/files/{invalid}/permissions → 400."""
|
||||
"""POST /api/v1/permissions/files/{invalid}/permissions → 400."""
|
||||
client, seed = authed_client
|
||||
resp = await client.post(
|
||||
"/api/v1/dms/files/bad-uuid/permissions",
|
||||
"/api/v1/permissions/files/bad-uuid/permissions",
|
||||
json={"user_id": str(uuid.uuid4()), "access_level": "read"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -333,13 +351,13 @@ async def test_grant_permission_invalid_ids(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_permission_with_group(authed_client: AsyncClient):
|
||||
"""POST /api/v1/dms/files/{id}/permissions with group_id → 201."""
|
||||
"""POST /api/v1/permissions/files/{id}/permissions with group_id → 201."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
user_id = str(seed["admin_a"].id)
|
||||
group_id = str(uuid.uuid4())
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/permissions",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions",
|
||||
json={"user_id": user_id, "group_id": group_id, "access_level": "read"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -349,12 +367,12 @@ async def test_grant_permission_with_group(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_permission_not_found(authed_client: AsyncClient):
|
||||
"""DELETE /api/v1/dms/files/{id}/permissions/{user_id} with no perms → 404."""
|
||||
"""DELETE /api/v1/permissions/files/{id}/permissions/{user_id} with no perms → 404."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
user_id = str(seed["admin_a"].id)
|
||||
resp = await client.delete(
|
||||
f"/api/v1/dms/files/{file_id}/permissions/{user_id}",
|
||||
f"/api/v1/permissions/files/{file_id}/permissions/{user_id}",
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
@@ -362,11 +380,11 @@ async def test_revoke_permission_not_found(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_permission_invalid_ids(authed_client: AsyncClient):
|
||||
"""DELETE /api/v1/dms/files/{invalid}/permissions/{user_id} → 400."""
|
||||
"""DELETE /api/v1/permissions/files/{invalid}/permissions/{user_id} → 400."""
|
||||
client, seed = authed_client
|
||||
user_id = str(seed["admin_a"].id)
|
||||
resp = await client.delete(
|
||||
f"/api/v1/dms/files/bad-uuid/permissions/{user_id}",
|
||||
f"/api/v1/permissions/files/bad-uuid/permissions/{user_id}",
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
@@ -377,7 +395,7 @@ async def test_create_share_link_invalid_file_id(authed_client: AsyncClient):
|
||||
"""POST /api/v1/dms/files/{invalid}/share-link → 400."""
|
||||
client, seed = authed_client
|
||||
resp = await client.post(
|
||||
"/api/v1/dms/files/bad-uuid/share-link",
|
||||
"/api/v1/permissions/files/bad-uuid/share-link",
|
||||
json={"access_level": "download"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
@@ -386,35 +404,35 @@ async def test_create_share_link_invalid_file_id(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_share_link_not_found(authed_client: AsyncClient):
|
||||
"""DELETE /api/v1/dms/share-links/{nonexistent} → 404."""
|
||||
"""DELETE /api/v1/permissions/share-links/{nonexistent} → 404."""
|
||||
client, seed = authed_client
|
||||
resp = await client.delete(f"/api/v1/dms/share-links/{uuid.uuid4()}", headers=ORIGIN_HEADER)
|
||||
resp = await client.delete(f"/api/v1/permissions/share-links/{uuid.uuid4()}", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_share_link_invalid_id(authed_client: AsyncClient):
|
||||
"""DELETE /api/v1/dms/share-links/{invalid} → 400."""
|
||||
"""DELETE /api/v1/permissions/share-links/{invalid} → 400."""
|
||||
client, seed = authed_client
|
||||
resp = await client.delete("/api/v1/dms/share-links/bad-uuid", headers=ORIGIN_HEADER)
|
||||
resp = await client.delete("/api/v1/permissions/share-links/bad-uuid", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_access_not_found(authed_client: AsyncClient):
|
||||
"""GET /api/public/share/{nonexistent} → 404."""
|
||||
"""GET /api/v1/public/share/{nonexistent} → 404."""
|
||||
client, seed = authed_client
|
||||
resp = await client.get("/api/public/share/nonexistent-token-xyz")
|
||||
resp = await client.get("/api/v1/public/share/nonexistent-token-xyz")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_access_post_not_found(authed_client: AsyncClient):
|
||||
"""POST /api/public/share/{nonexistent} → 404."""
|
||||
"""POST /api/v1/public/share/{nonexistent}/verify → 404."""
|
||||
client, seed = authed_client
|
||||
resp = await client.post(
|
||||
"/api/public/share/nonexistent-token-xyz",
|
||||
json={"password": "test"},
|
||||
"/api/v1/public/share/nonexistent-token-xyz/verify",
|
||||
params={"password": "test"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
@@ -422,19 +440,21 @@ async def test_public_access_post_not_found(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_access_post_expired(authed_client: AsyncClient):
|
||||
"""POST /api/public/share/{token} with expired link → 410."""
|
||||
"""POST /api/v1/public/share/{token}/verify with expired link → 410."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
# Upload a real file to DMS first
|
||||
resp = await client.post("/api/v1/dms/files/upload", files={"file": ("expired2.txt", b"expired content 2", "text/plain")}, headers=ORIGIN_HEADER)
|
||||
file_id = resp.json()["id"]
|
||||
past = datetime.now(UTC) - timedelta(hours=1)
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"expires_at": past.isoformat(), "access_level": "download"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
token = resp.json()["token"]
|
||||
resp = await client.post(
|
||||
f"/api/public/share/{token}",
|
||||
json={"password": "test"},
|
||||
f"/api/v1/public/share/{token}/verify",
|
||||
params={"password": "test"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 410
|
||||
@@ -442,19 +462,21 @@ async def test_public_access_post_expired(authed_client: AsyncClient):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_access_post_no_password_required(authed_client: AsyncClient):
|
||||
"""POST /api/public/share/{token} for link without password → 200."""
|
||||
"""POST /api/v1/public/share/{token}/verify for link without password → 200."""
|
||||
client, seed = authed_client
|
||||
file_id = str(uuid.uuid4())
|
||||
# Upload a real file to DMS first
|
||||
resp = await client.post("/api/v1/dms/files/upload", files={"file": ("nopass.txt", b"no pass content", "text/plain")}, headers=ORIGIN_HEADER)
|
||||
file_id = resp.json()["id"]
|
||||
resp = await client.post(
|
||||
f"/api/v1/dms/files/{file_id}/share-link",
|
||||
f"/api/v1/permissions/files/{file_id}/share-link",
|
||||
json={"access_level": "preview"},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
token = resp.json()["token"]
|
||||
resp = await client.post(
|
||||
f"/api/public/share/{token}",
|
||||
json={},
|
||||
f"/api/v1/public/share/{token}/verify",
|
||||
params={"password": ""},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["has_password"] is False
|
||||
assert resp.json()["valid"] is True
|
||||
|
||||
Reference in New Issue
Block a user