You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

410 lines
14 KiB

import pytest
@pytest.mark.anyio
async def test_get_file_detail(api_client, create_file_metadata) -> None:
created = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
response = await api_client.get(f"/api/files/{created.id}")
assert response.status_code == 200
payload = response.json()["file"]
assert payload["id"] == created.id
assert payload["name"] == "movie.mp4"
assert payload["mime_type"] == "video/mp4"
assert payload["size_bytes"] == 1048576
assert response.json()["preview_artifacts"] == []
assert response.json()["replicas"] == []
assert response.json()["placement_file_class"] == "video"
assert [item["id"] for item in response.json()["desired_backends"]] == ["bkd_local_default"]
assert response.json()["ready_replica_backend_ids"] == []
assert response.json()["missing_backend_ids"] == ["bkd_local_default"]
@pytest.mark.anyio
async def test_get_file_detail_returns_404_for_missing_file(api_client) -> None:
response = await api_client.get("/api/files/fil_missing")
assert response.status_code == 404
assert response.json()["error"]["code"] == "file_not_found"
@pytest.mark.anyio
async def test_download_file_returns_local_persisted_content(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "11",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"hello world",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "movie.mp4"},
)
assert finalize_response.status_code == 200
file_id = finalize_response.json()["file"]["id"]
download_response = await api_client.get(f"/api/files/{file_id}/download")
assert download_response.status_code == 200
assert download_response.content == b"hello world"
assert download_response.headers["content-type"].startswith("video/mp4")
detail_response = await api_client.get(f"/api/files/{file_id}")
replica = detail_response.json()["replicas"][0]
assert replica["backend_id"] == "bkd_local_default"
assert replica["backend_type"] == "local_directory"
assert replica["status"] == "ready"
assert detail_response.json()["ready_replica_backend_ids"] == ["bkd_local_default"]
assert detail_response.json()["missing_backend_ids"] == []
@pytest.mark.anyio
async def test_download_file_returns_404_for_missing_file(api_client) -> None:
response = await api_client.get("/api/files/fil_missing/download")
assert response.status_code == 404
assert response.json()["error"]["code"] == "file_not_found"
@pytest.mark.anyio
async def test_stream_file_returns_full_content_without_range(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "11",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"hello world",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "movie.mp4"},
)
file_id = finalize_response.json()["file"]["id"]
response = await api_client.get(f"/api/files/{file_id}/stream")
assert response.status_code == 200
assert response.content == b"hello world"
assert response.headers["accept-ranges"] == "bytes"
@pytest.mark.anyio
async def test_stream_file_returns_partial_content_for_range(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "11",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"hello world",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "movie.mp4"},
)
file_id = finalize_response.json()["file"]["id"]
response = await api_client.get(
f"/api/files/{file_id}/stream",
headers={"Range": "bytes=6-10"},
)
assert response.status_code == 206
assert response.content == b"world"
assert response.headers["content-range"] == "bytes 6-10/11"
assert response.headers["content-length"] == "5"
@pytest.mark.anyio
async def test_stream_file_rejects_invalid_range(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "5",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"hello",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "movie.mp4"},
)
file_id = finalize_response.json()["file"]["id"]
response = await api_client.get(
f"/api/files/{file_id}/stream",
headers={"Range": "bytes=99-100"},
)
assert response.status_code == 416
assert response.json()["error"]["code"] == "range_not_satisfiable"
@pytest.mark.anyio
async def test_preview_file_returns_inline_for_pdf(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "8",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"%PDF-1.7",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "doc.pdf"},
)
file_id = finalize_response.json()["file"]["id"]
response = await api_client.get(f"/api/files/{file_id}/preview")
assert response.status_code == 200
assert response.content == b"%PDF-1.7"
assert response.headers["content-type"].startswith("application/pdf")
assert "inline" in response.headers["content-disposition"]
@pytest.mark.anyio
async def test_preview_file_rejects_unsupported_type(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "11",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"hello world",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "movie.mp4"},
)
file_id = finalize_response.json()["file"]["id"]
response = await api_client.get(f"/api/files/{file_id}/preview")
assert response.status_code == 400
assert response.json()["error"]["code"] == "preview_not_supported"
@pytest.mark.anyio
async def test_file_detail_includes_generated_preview_artifacts_for_image(api_client) -> None:
create_response = await api_client.post(
"/files",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Length": "8",
},
)
upload_id = create_response.headers["Location"].rsplit("/", 1)[-1]
patch_response = await api_client.patch(
f"/files/{upload_id}",
headers={
"Tus-Resumable": "1.0.0",
"Upload-Offset": "0",
"Content-Type": "application/offset+octet-stream",
},
content=b"imgbytes",
)
assert patch_response.status_code == 204
finalize_response = await api_client.post(
f"/api/uploads/{upload_id}/finalize",
json={"directory_id": "dir_root", "filename": "photo.jpg"},
)
file_id = finalize_response.json()["file"]["id"]
run_response = await api_client.post("/api/jobs/run-pending")
assert run_response.status_code == 200
assert run_response.json()["completed"] >= 1
detail_response = await api_client.get(f"/api/files/{file_id}")
assert detail_response.status_code == 200
artifact_types = [item["artifact_type"] for item in detail_response.json()["preview_artifacts"]]
assert artifact_types == ["inline_preview", "thumbnail"]
@pytest.mark.anyio
async def test_rename_file(api_client, create_file_metadata) -> None:
created = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
response = await api_client.post(
f"/api/files/{created.id}/rename",
json={"name": "movie-final.mp4"},
)
assert response.status_code == 200
assert response.json()["file"]["name"] == "movie-final.mp4"
list_response = await api_client.get("/api/directories/dir_root/children")
assert [item["name"] for item in list_response.json()["items"]] == ["movie-final.mp4"]
@pytest.mark.anyio
async def test_move_file(api_client, create_file_metadata) -> None:
target = await api_client.post("/api/directories", json={"parent_id": "dir_root", "name": "archive"})
created = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
response = await api_client.post(
f"/api/files/{created.id}/move",
json={"target_parent_id": target.json()["directory"]["id"]},
)
assert response.status_code == 200
assert response.json()["file"]["directory_id"] == target.json()["directory"]["id"]
root_children = await api_client.get("/api/directories/dir_root/children")
assert [item["name"] for item in root_children.json()["items"]] == ["archive"]
target_children = await api_client.get(f"/api/directories/{target.json()['directory']['id']}/children")
assert [item["name"] for item in target_children.json()["items"]] == ["movie.mp4"]
@pytest.mark.anyio
async def test_rename_file_rejects_conflict(api_client, create_file_metadata) -> None:
first = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
await create_file_metadata(name="other.mp4", content_hash="hash_other_mp4")
response = await api_client.post(
f"/api/files/{first.id}/rename",
json={"name": "other.mp4"},
)
assert response.status_code == 409
assert response.json()["error"]["code"] == "file_conflict"
@pytest.mark.anyio
async def test_delete_file_moves_it_to_recycle_bin(api_client, create_file_metadata) -> None:
created = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
delete_response = await api_client.delete(f"/api/files/{created.id}")
assert delete_response.status_code == 200
deleted_file = delete_response.json()["file"]
assert deleted_file["id"] == created.id
assert deleted_file["deleted_at"] is not None
detail_response = await api_client.get(f"/api/files/{created.id}")
assert detail_response.status_code == 404
children_response = await api_client.get("/api/directories/dir_root/children")
assert children_response.status_code == 200
assert children_response.json()["items"] == []
recycle_bin_response = await api_client.get("/api/files/recycle-bin")
assert recycle_bin_response.status_code == 200
assert [item["id"] for item in recycle_bin_response.json()["items"]] == [created.id]
@pytest.mark.anyio
async def test_restore_file_removes_it_from_recycle_bin(api_client, create_file_metadata) -> None:
created = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
delete_response = await api_client.delete(f"/api/files/{created.id}")
assert delete_response.status_code == 200
restore_response = await api_client.post(f"/api/files/{created.id}/restore")
assert restore_response.status_code == 200
restored_file = restore_response.json()["file"]
assert restored_file["id"] == created.id
assert restored_file["name"] == "movie.mp4"
children_response = await api_client.get("/api/directories/dir_root/children")
assert children_response.status_code == 200
assert [item["name"] for item in children_response.json()["items"]] == ["movie.mp4"]
recycle_bin_response = await api_client.get("/api/files/recycle-bin")
assert recycle_bin_response.status_code == 200
assert recycle_bin_response.json()["items"] == []
@pytest.mark.anyio
async def test_restore_file_rejects_name_conflict(api_client, create_file_metadata) -> None:
original = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_mp4")
delete_response = await api_client.delete(f"/api/files/{original.id}")
assert delete_response.status_code == 200
conflicting = await create_file_metadata(name="movie.mp4", content_hash="hash_movie_new")
assert conflicting.id != original.id
restore_response = await api_client.post(f"/api/files/{original.id}/restore")
assert restore_response.status_code == 409
assert restore_response.json()["error"]["code"] == "file_conflict"