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.
397 lines
14 KiB
397 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"] == []
|
|
|
|
|
|
@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")
|
|
|
|
|
|
@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"
|
|
|