File
Blob: tests/worker/control/api/files.workers.test.ts
| 1 | import { describe, expect, it } from "vitest"; |
| 2 | |
| 3 | import { |
| 4 | FileListResponseSchema, |
| 5 | FileMetadataResponseSchema, |
| 6 | MeResponseSchema, |
| 7 | PatCreateResponseSchema, |
| 8 | } from "@/worker/openapi/schemas"; |
| 9 | import { completeOidcLogin } from "@tests/worker/helpers/auth"; |
| 10 | import { basicPat } from "@tests/worker/helpers/dav"; |
| 11 | import { controlRequest, fetchWorker, subjectRequest } from "@tests/worker/helpers/http"; |
| 12 | |
| 13 | function jsonHeaders(sessionCookie: string): HeadersInit { |
| 14 | return { |
| 15 | cookie: sessionCookie, |
| 16 | origin: "https://dav.example.com", |
| 17 | "content-type": "application/json", |
| 18 | }; |
| 19 | } |
| 20 | |
| 21 | async function createFilesPat(sessionCookie: string): Promise<string> { |
| 22 | const response = await fetchWorker( |
| 23 | controlRequest("/api/v1/pats", { |
| 24 | method: "POST", |
| 25 | headers: jsonHeaders(sessionCookie), |
| 26 | body: JSON.stringify({ name: "files api", scopes: ["files.full"] }), |
| 27 | }), |
| 28 | ); |
| 29 | expect(response.status).toBe(201); |
| 30 | return PatCreateResponseSchema.parse(await response.json()).token; |
| 31 | } |
| 32 | |
| 33 | describe("file JSON API", () => { |
| 34 | it("wraps FILE_DAV metadata, list, mkdir, rename, and delete operations", async () => { |
| 35 | const login = await completeOidcLogin(); |
| 36 | const meResponse = await fetchWorker(controlRequest("/api/v1/me", { headers: { cookie: login.sessionCookie } })); |
| 37 | const me = MeResponseSchema.parse(await meResponse.json()); |
| 38 | const token = await createFilesPat(login.sessionCookie); |
| 39 | const auth = basicPat(token); |
| 40 | const directory = `api-${crypto.randomUUID()}`; |
| 41 | |
| 42 | const mkdir = await fetchWorker( |
| 43 | controlRequest("/api/v1/files/mkdir", { |
| 44 | method: "POST", |
| 45 | headers: jsonHeaders(login.sessionCookie), |
| 46 | body: JSON.stringify({ path: directory }), |
| 47 | }), |
| 48 | ); |
| 49 | expect(mkdir.status).toBe(201); |
| 50 | expect(FileMetadataResponseSchema.parse(await mkdir.json())).toMatchObject({ |
| 51 | href: `/files/${directory}/`, |
| 52 | kind: "collection", |
| 53 | }); |
| 54 | |
| 55 | const put = await fetchWorker( |
| 56 | subjectRequest(me.host_label, `/files/${directory}/note.txt`, { |
| 57 | method: "PUT", |
| 58 | headers: { authorization: auth, "content-type": "text/plain" }, |
| 59 | body: "hello", |
| 60 | }), |
| 61 | ); |
| 62 | expect(put.status).toBe(201); |
| 63 | |
| 64 | const metadata = await fetchWorker( |
| 65 | controlRequest(`/api/v1/files/metadata?path=${encodeURIComponent(`${directory}/note.txt`)}`, { |
| 66 | headers: { cookie: login.sessionCookie }, |
| 67 | }), |
| 68 | ); |
| 69 | expect(metadata.status).toBe(200); |
| 70 | expect(FileMetadataResponseSchema.parse(await metadata.json())).toMatchObject({ |
| 71 | href: `/files/${directory}/note.txt`, |
| 72 | name: "note.txt", |
| 73 | kind: "file", |
| 74 | size: 5, |
| 75 | content_type: "text/plain", |
| 76 | }); |
| 77 | |
| 78 | const list = await fetchWorker( |
| 79 | controlRequest(`/api/v1/files/list?path=${encodeURIComponent(directory)}`, { |
| 80 | headers: { cookie: login.sessionCookie }, |
| 81 | }), |
| 82 | ); |
| 83 | expect(list.status).toBe(200); |
| 84 | const listed = FileListResponseSchema.parse(await list.json()); |
| 85 | expect(listed.directory.href).toBe(`/files/${directory}/`); |
| 86 | expect(listed.entries.map((entry) => entry.href)).toContain(`/files/${directory}/note.txt`); |
| 87 | |
| 88 | const rename = await fetchWorker( |
| 89 | controlRequest("/api/v1/files/rename", { |
| 90 | method: "POST", |
| 91 | headers: jsonHeaders(login.sessionCookie), |
| 92 | body: JSON.stringify({ from: `${directory}/note.txt`, to: `${directory}/renamed.txt` }), |
| 93 | }), |
| 94 | ); |
| 95 | expect(rename.status).toBe(200); |
| 96 | expect(FileMetadataResponseSchema.parse(await rename.json())).toMatchObject({ |
| 97 | href: `/files/${directory}/renamed.txt`, |
| 98 | name: "renamed.txt", |
| 99 | }); |
| 100 | |
| 101 | const oldPath = await fetchWorker( |
| 102 | subjectRequest(me.host_label, `/files/${directory}/note.txt`, { headers: { authorization: auth } }), |
| 103 | ); |
| 104 | expect(oldPath.status).toBe(404); |
| 105 | const renamed = await fetchWorker( |
| 106 | subjectRequest(me.host_label, `/files/${directory}/renamed.txt`, { headers: { authorization: auth } }), |
| 107 | ); |
| 108 | expect(renamed.status).toBe(200); |
| 109 | await expect(renamed.text()).resolves.toBe("hello"); |
| 110 | |
| 111 | const deleted = await fetchWorker( |
| 112 | controlRequest("/api/v1/files/delete", { |
| 113 | method: "DELETE", |
| 114 | headers: jsonHeaders(login.sessionCookie), |
| 115 | body: JSON.stringify({ path: `${directory}/renamed.txt` }), |
| 116 | }), |
| 117 | ); |
| 118 | expect(deleted.status).toBe(204); |
| 119 | |
| 120 | const afterDelete = await fetchWorker( |
| 121 | controlRequest(`/api/v1/files/metadata?path=${encodeURIComponent(`${directory}/renamed.txt`)}`, { |
| 122 | headers: { cookie: login.sessionCookie }, |
| 123 | }), |
| 124 | ); |
| 125 | expect(afterDelete.status).toBe(404); |
| 126 | }); |
| 127 | }); |