Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions apps/web/__tests__/unit/developer-api-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,11 +641,32 @@ describe("browser access", () => {
"access-control-allow-headers": "Content-Type,Authorization",
"access-control-expose-headers":
"Retry-After,X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,X-Request-Id",
"access-control-max-age": "7200",
"cache-control": "private, no-store",
vary: "Access-Control-Request-Headers",
});
});

it.each([
"/api/developer/sdk/v1/upload/multipart/initiate",
"/api/developer/sdk/v1/upload/multipart/presign-part",
"/api/developer/sdk/v1/upload/multipart/complete",
"/api/developer/sdk/v1/upload/multipart/abort",
])(
"lets a browser cache the preflight for %s for two hours",
async (path) => {
const preflight = await call(sdk, "OPTIONS", path, {
headers: {
...PREFLIGHT_HEADERS,
"Access-Control-Request-Method": "POST",
},
});

expect(preflight.status).toBe(204);
expect(preflight.headers.get("Access-Control-Max-Age")).toBe("7200");
},
);

it("still lets a page read an upload route's answer", async () => {
const response = await call(
sdk,
Expand All @@ -663,6 +684,48 @@ describe("browser access", () => {
});
});

describe("the credential sent to the REST API", () => {
it("says the key is missing when no Authorization header is sent", async () => {
const response = await getUsage("");

expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({
code: "invalid_secret_key",
error: "Missing secret key",
hint: expect.stringContaining("No Authorization header was sent"),
});
});

it("refuses a public key with its own code", async () => {
const response = await getUsage(freshKey("cpk"));

expect(response.status).toBe(403);
expect(await response.json()).toMatchObject({
code: "public_key_not_allowed",
error: "Public keys cannot call the REST API",
hint: expect.stringContaining("csk_"),
});
expect(mocks.billing).not.toHaveBeenCalled();
});

it("keeps the answers for any other credential and for a wrong secret key", async () => {
const other = await getUsage("not_a_key");
mocks.keyRows = [];
const wrong = await getUsage(freshKey("csk"));

expect(other.status).toBe(401);
expect(await other.json()).toMatchObject({
code: "invalid_secret_key",
error: "Invalid secret key",
});
expect(wrong.status).toBe(401);
expect(await wrong.json()).toMatchObject({
code: "invalid_secret_key",
error: "Invalid or revoked secret key",
});
});
});

describe("wrong method and unknown path", () => {
it.each([
[rest, "PATCH", "/api/developer/v1/videos/video-1", "csk", "GET, DELETE"],
Expand Down
18 changes: 10 additions & 8 deletions apps/web/__tests__/unit/developer-sdk-upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,17 +677,19 @@ describe("client mistakes on complete and abort", () => {
},
);

it("answers 404 when aborting an upload that does not exist", async () => {
it("answers a repeated abort with success, whether storage accepts it again or no longer knows the upload", async () => {
mocks.video = uploaded;
mocks.abort.mockImplementation(fails("NoSuchUpload", 404));

const response = await post("/abort", { uploadId: "not-an-upload" });
const first = await post("/abort", { uploadId: "upload-1" });
const repeated = await post("/abort", { uploadId: "upload-1" });
mocks.abort.mockImplementation(fails("NoSuchUpload", 404));
const gone = await post("/abort", { uploadId: "upload-1" });

expect(await refusal(response)).toMatchObject({
status: 404,
code: "upload_not_found",
hint: expect.stringContaining("Nothing was aborted"),
});
for (const response of [first, repeated, gone]) {
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ success: true });
}
expect(mocks.abort).toHaveBeenCalledTimes(3);
expect(console.error).not.toHaveBeenCalled();
});

Expand Down
26 changes: 19 additions & 7 deletions apps/web/__tests__/unit/developer-transactions-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,22 @@ vi.mock("@cap/database", async () => {
b.createdAt.getTime() - a.createdAt.getTime() ||
(a.id < b.id ? 1 : -1),
);
if (cursor.length > 0) {
const time = asTime(cursor[0]);
const at = (index: number) => asTime(cursor[index]);
if (cursor.length === 3) {
const id = String(cursor.at(-1));
rows = rows.filter(
(row) =>
row.createdAt.getTime() < time ||
(row.createdAt.getTime() === time && row.id < id),
row.createdAt.getTime() < at(0) ||
(row.createdAt.getTime() === at(0) && row.id < id),
);
} else if (rendered.sql.includes(">= ?")) {
rows = rows.filter(
(row) =>
row.createdAt.getTime() >= at(0) &&
row.createdAt.getTime() <= at(1),
);
} else if (cursor.length === 1) {
rows = rows.filter((row) => row.createdAt.getTime() === at(0));
}
if (fields && "total" in fields) return [{ total: rows.length }];
return rows.slice(offset, offset + limit);
Expand Down Expand Up @@ -186,10 +194,14 @@ describe("GET /transactions", () => {
const publicKey = await get("/transactions", "cpk_public_key");
const unknown = await get("/transactions", "csk_not_a_key");

for (const response of [missing, publicKey, unknown]) {
expect(response.status).toBe(401);
for (const [response, status, code] of [
[missing, 401, "invalid_secret_key"],
[publicKey, 403, "public_key_not_allowed"],
[unknown, 401, "invalid_secret_key"],
] as const) {
expect(response.status).toBe(status);
expect(await response.json()).toMatchObject({
code: "invalid_secret_key",
code,
hint: expect.any(String),
docs: expect.any(String),
});
Expand Down
26 changes: 17 additions & 9 deletions apps/web/__tests__/unit/developer-transcribe-download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,25 +431,33 @@ describe("downloading a URL source from a slow server", () => {
head,
});

const WEB_PAGE = "The URL returned a web page, not a media file";
const NOT_MEDIA = "The file is not a supported audio or video format";
const bytes = (text: string) => new TextEncoder().encode(text);

it.each([
["an HTML page", new TextEncoder().encode("<!doctype html><html>")],
["a playlist", new TextEncoder().encode("#EXTM3U\n#EXT-X-VERSION:3\n")],
["random bytes", new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])],
["an HTML page", bytes("<!doctype html><html>"), WEB_PAGE],
[
"an HTML page after a BOM and blank lines",
new Uint8Array([0xef, 0xbb, 0xbf, ...bytes("\r\n\t <!DOCTYPE HTML>")]),
WEB_PAGE,
],
["an HTML page with no doctype", bytes("\n<HTML lang=en>"), WEB_PAGE],
["an XML document", bytes('<?xml version="1.0"?><a/>'), NOT_MEDIA],
["a playlist", bytes("#EXTM3U\n#EXT-X-VERSION:3\n"), NOT_MEDIA],
["random bytes", new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), NOT_MEDIA],
])(
"is refused from its first bytes when it is %s, with nothing stored",
async (_label, head) => {
async (_label, head, message) => {
const server = await notMedia(head);

const result = await run();

expect(result).toEqual({
success: false,
message: "The file is not a supported audio or video format",
});
expect(result).toEqual({ success: false, message });
expect(transcript()).toMatchObject({
status: "error",
errorCode: "unsupported_format",
errorMessage: "The file is not a supported audio or video format",
errorMessage: message,
billedMicroCredits: 0,
reservedMicroCredits: 0,
});
Expand Down
66 changes: 43 additions & 23 deletions apps/web/__tests__/unit/developer-transcripts-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,29 +101,49 @@ describe("developer transcripts API", () => {

describe("authentication", () => {
it.each([
["no key", {}],
["a public key", { Authorization: "Bearer cpk_public_key" }],
["an agent token", { Authorization: "Bearer cap_cli_token" }],
["a malformed header", { Authorization: "csk_valid_key" }],
])("rejects %s before touching transcripts", async (_label, headers) => {
for (const [method, path] of [
["GET", "/"],
["GET", "/tr-1"],
["DELETE", "/tr-1"],
["POST", "/tr-1/start"],
] as const) {
const response = await app.request(path, { method, headers });
expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({
code: "invalid_secret_key",
});
}
const created = await post("/", { url: "https://a.example/a" }, headers);
expect(created.status).toBe(401);
for (const mock of Object.values(mocks.store)) {
expect(mock).not.toHaveBeenCalled();
}
});
["no key", {}, 401, "invalid_secret_key"],
[
"a public key",
{ Authorization: "Bearer cpk_public_key" },
403,
"public_key_not_allowed",
],
[
"an agent token",
{ Authorization: "Bearer cap_cli_token" },
401,
"invalid_secret_key",
],
[
"a malformed header",
{ Authorization: "csk_valid_key" },
401,
"invalid_secret_key",
],
])(
"rejects %s before touching transcripts",
async (_label, headers, status, code) => {
for (const [method, path] of [
["GET", "/"],
["GET", "/tr-1"],
["DELETE", "/tr-1"],
["POST", "/tr-1/start"],
] as const) {
const response = await app.request(path, { method, headers });
expect(response.status).toBe(status);
expect(await response.json()).toMatchObject({ code });
}
const created = await post(
"/",
{ url: "https://a.example/a" },
headers,
);
expect(created.status).toBe(status);
for (const mock of Object.values(mocks.store)) {
expect(mock).not.toHaveBeenCalled();
}
},
);

it("rejects a revoked or unknown secret key", async () => {
mocks.keyRows = [];
Expand Down
41 changes: 22 additions & 19 deletions apps/web/__tests__/unit/developer-upload-token-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,21 +274,24 @@ describe("creating an upload with the secret key", () => {
});

it.each([
["a public key", "cpk_app1_page"],
["an upload token", "cut_abc.def"],
["no key", null],
])("refuses %s and inserts nothing", async (_label, credential) => {
const response = await post(
rest,
"/api/developer/v1/uploads",
credential,
{},
);
["a public key", "cpk_app1_page", 403, "public_key_not_allowed"],
["an upload token", "cut_abc.def", 401, "invalid_secret_key"],
["no key", null, 401, "invalid_secret_key"],
])(
"refuses %s and inserts nothing",
async (_label, credential, status, code) => {
const response = await post(
rest,
"/api/developer/v1/uploads",
credential,
{},
);

expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({ code: "invalid_secret_key" });
expect(inserts()).toHaveLength(0);
});
expect(response.status).toBe(status);
expect(await response.json()).toMatchObject({ code });
expect(inserts()).toHaveLength(0);
},
);

it.each([
[60, 60],
Expand Down Expand Up @@ -794,15 +797,15 @@ describe("renewing the token of an open upload", () => {
});

it.each([
["a public key", "cpk_app1_page"],
["no key", null],
])("refuses %s", async (_label, credential) => {
["a public key", "cpk_app1_page", 403, "public_key_not_allowed"],
["no key", null, 401, "invalid_secret_key"],
])("refuses %s", async (_label, credential, status, code) => {
const first = await created();

const response = await renew(first.videoId, {}, credential);

expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({ code: "invalid_secret_key" });
expect(response.status).toBe(status);
expect(await response.json()).toMatchObject({ code });
});

it("cannot be renewed with the upload token itself", async () => {
Expand Down
Loading
Loading