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"