Files

313 lines
13 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { sha256 } from "../src/aws-signature";
import worker from "../src/index";
import type { BucketStatItem, BucketStatsResponse, GatewayStatusResponse } from "../src/status-api";
import type { Env } from "../src/types";
import { env } from "cloudflare:test";
const ENV = env as unknown as Env;
const ENDPOINT = "https://s3-api.example.com";
const CTX = { waitUntil: vi.fn(), passThroughOnException: vi.fn() } as unknown as ExecutionContext;
async function getValidToken(): Promise<string> {
const correctHash = await sha256("test-dashboard-password");
const loginRes = await worker.fetch(
new Request(`${ENDPOINT}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json", "CF-Connecting-IP": "127.0.0.1" },
body: JSON.stringify({ passwordHash: correctHash }),
}),
ENV,
CTX,
);
expect(loginRes.status).toBe(200);
const data = (await loginRes.json()) as { token: string };
return data.token;
}
describe("Dashboard status API routes (/api/*)", () => {
it("returns 401 when Authorization header is missing or invalid", async () => {
const res = await worker.fetch(new Request(`${ENDPOINT}/api/status`), ENV, CTX);
expect(res.status).toBe(401);
const invalidRes = await worker.fetch(
new Request(`${ENDPOINT}/api/status`, {
headers: { Authorization: "Bearer invalid-token" },
}),
ENV,
CTX,
);
expect(invalidRes.status).toBe(401);
});
it("returns 503 when DASHBOARD_PASSWORD is not configured", async () => {
const withoutPassword = { ...ENV, DASHBOARD_PASSWORD: undefined };
const res = await worker.fetch(
new Request(`${ENDPOINT}/api/status`, {
headers: { Authorization: "Bearer any-token" },
}),
withoutPassword,
CTX,
);
expect(res.status).toBe(503);
});
it("returns 405 for non-GET methods", async () => {
const token = await getValidToken();
const res = await worker.fetch(
new Request(`${ENDPOINT}/api/status`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
}),
ENV,
CTX,
);
expect(res.status).toBe(405);
});
it("returns 200 with valid token and healthy drive response on /api/status", async () => {
const token = await getValidToken();
const fakeFetch = vi.fn(async (input: RequestInfo | URL) => {
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
if (url.origin === "https://oauth2.googleapis.com") {
return Response.json({ access_token: "mock-access-token", expires_in: 3600 });
}
if (url.pathname === "/drive/v3/about") {
return Response.json({
user: {
emailAddress: "test@example.com",
displayName: "Test User",
},
storageQuota: {
limit: "15000000000",
usage: "5000000000",
usageInDrive: "4500000000",
usageInDriveTrash: "500000000",
},
});
}
if (url.pathname === "/drive/v3/files") {
const q = url.searchParams.get("q") ?? "";
if (q.includes("name='s3-storage'") && q.includes("'root' in parents")) {
return Response.json({ files: [{ id: "root-folder-id", name: "s3-storage" }] });
}
if (q.includes("'root-folder-id' in parents")) {
return Response.json({
files: [
{ id: "folder-test-bucket", name: "test-bucket", mimeType: "application/vnd.google-apps.folder" },
{ id: "folder-empty-bucket", name: "empty-bucket", mimeType: "application/vnd.google-apps.folder" },
{ id: "folder-my-bucket", name: "my-bucket", mimeType: "application/vnd.google-apps.folder" },
],
});
}
return Response.json({ files: [] });
}
return new Response("Not found", { status: 404 });
});
vi.stubGlobal("fetch", fakeFetch);
try {
const res = await worker.fetch(
new Request(`${ENDPOINT}/api/status`, {
headers: { Authorization: `Bearer ${token}` },
}),
ENV,
CTX,
);
expect(res.status).toBe(200);
const data = (await res.json()) as GatewayStatusResponse;
expect(data.gateway.status).toBe("ok");
expect(data.gateway.region).toBe("auto");
expect(data.gateway.multipartEnabled).toBe(true);
expect(data.gateway.buckets).toEqual(["test-bucket", "empty-bucket", "my-bucket"]);
expect(data.gateway.rootFolder).toEqual({
name: "s3-storage",
id: "root-folder-id",
configured: true,
});
expect(data.gateway.credentials).toEqual({
s3Keys: true,
googleOAuth: true,
dashboardPassword: true,
});
expect(data.drive.connected).toBe(true);
expect(data.drive.account?.email).toBe("test@example.com");
expect(data.drive.quota?.limit).toBe(15000000000);
expect(data.drive.quota?.usage).toBe(5000000000);
expect(data.drive.quota?.free).toBe(10000000000);
expect(data.drive.quota?.percentUsed).toBe(33.3);
expect(data.drive.error).toBeNull();
} finally {
vi.unstubAllGlobals();
}
});
it("does not leak secrets in /api/status response", async () => {
const token = await getValidToken();
const fakeFetch = vi.fn(async (input: RequestInfo | URL) => {
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
if (url.origin === "https://oauth2.googleapis.com") {
return Response.json({ access_token: "mock-access-token", expires_in: 3600 });
}
if (url.pathname === "/drive/v3/about") {
return Response.json({
user: { emailAddress: "test@example.com", displayName: "Test User" },
storageQuota: { limit: "1000", usage: "500" },
});
}
return new Response("Not found", { status: 404 });
});
vi.stubGlobal("fetch", fakeFetch);
try {
const res = await worker.fetch(
new Request(`${ENDPOINT}/api/status`, {
headers: { Authorization: `Bearer ${token}` },
}),
ENV,
CTX,
);
const text = await res.text();
expect(text).not.toContain("test-secret-key");
expect(text).not.toContain("test-refresh-token");
expect(text).not.toContain("test-dashboard-password");
expect(text).not.toContain("test-client-secret");
expect(text).not.toContain("test-access-key");
} finally {
vi.unstubAllGlobals();
}
});
it("handles Drive errors gracefully with degraded status on /api/status", async () => {
await (ENV.AUTH_KV as KVNamespace).delete("drive-about");
const token = await getValidToken();
const fakeFetch = vi.fn(async (input: RequestInfo | URL) => {
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
if (url.origin === "https://oauth2.googleapis.com") {
return Response.json({ access_token: "mock-access-token", expires_in: 3600 });
}
if (url.pathname === "/drive/v3/about") {
return new Response("Internal Server Error", { status: 500 });
}
return new Response("Not found", { status: 404 });
});
vi.stubGlobal("fetch", fakeFetch);
try {
const res = await worker.fetch(
new Request(`${ENDPOINT}/api/status`, {
headers: { Authorization: `Bearer ${token}` },
}),
ENV,
CTX,
);
expect(res.status).toBe(200);
const data = (await res.json()) as GatewayStatusResponse;
expect(data.gateway.status).toBe("degraded");
expect(data.drive.connected).toBe(false);
expect(data.drive.account).toBeNull();
expect(data.drive.quota).toBeNull();
expect(data.drive.error).toContain("Drive about request failed");
} finally {
vi.unstubAllGlobals();
}
});
it("returns bucket statistics on /api/buckets", async () => {
await (ENV.AUTH_KV as KVNamespace).delete("drive-about");
await (ENV.FOLDER_CACHE as KVNamespace).delete("bucket-registry");
const token = await getValidToken();
const fakeFetch = vi.fn(async (input: RequestInfo | URL) => {
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
if (url.origin === "https://oauth2.googleapis.com") {
return Response.json({ access_token: "mock-access-token", expires_in: 3600 });
}
if (url.pathname === "/drive/v3/files") {
const q = url.searchParams.get("q") ?? "";
if (q.includes("name='s3-storage'") && q.includes("'root' in parents")) {
return Response.json({ files: [{ id: "root-folder-id", name: "s3-storage" }] });
}
if (q.includes("name='test-bucket'") && q.includes("'root-folder-id' in parents")) {
return Response.json({ files: [{ id: "folder-test-bucket", name: "test-bucket" }] });
}
if (q.includes("name='empty-bucket'") && q.includes("'root-folder-id' in parents")) {
return Response.json({ files: [{ id: "folder-empty-bucket", name: "empty-bucket" }] });
}
if (q.includes("name='my-bucket'") && q.includes("'root-folder-id' in parents")) {
return Response.json({ files: [{ id: "folder-my-bucket", name: "my-bucket" }] });
}
if (q.includes("'root-folder-id' in parents")) {
return Response.json({
files: [
{ id: "folder-test-bucket", name: "test-bucket", mimeType: "application/vnd.google-apps.folder" },
{ id: "folder-empty-bucket", name: "empty-bucket", mimeType: "application/vnd.google-apps.folder" },
{ id: "folder-my-bucket", name: "my-bucket", mimeType: "application/vnd.google-apps.folder" },
],
});
}
if (q.includes("'folder-test-bucket' in parents")) {
return Response.json({
files: [
{
id: "file-1",
name: "hello.txt",
mimeType: "text/plain",
size: "1024",
modifiedTime: "2026-08-19T00:00:00.000Z",
},
],
});
}
if (q.includes("'folder-empty-bucket' in parents") || q.includes("'folder-my-bucket' in parents")) {
return Response.json({ files: [] });
}
return Response.json({ files: [] });
}
return new Response("Not found", { status: 404 });
});
vi.stubGlobal("fetch", fakeFetch);
try {
const res = await worker.fetch(
new Request(`${ENDPOINT}/api/buckets?refresh=1`, {
headers: { Authorization: `Bearer ${token}` },
}),
ENV,
CTX,
);
expect(res.status).toBe(200);
const data = (await res.json()) as BucketStatsResponse;
expect(data.buckets).toHaveLength(3);
const testBucket = data.buckets.find((b: BucketStatItem) => b.name === "test-bucket");
expect(testBucket).toBeTruthy();
expect(testBucket?.objectCount).toBe(1);
expect(testBucket?.totalSize).toBe(1024);
expect(testBucket?.lastModified).toBe("2026-08-19T00:00:00.000Z");
expect(data.totals.objectCount).toBe(1);
expect(data.totals.totalSize).toBe(1024);
} finally {
vi.unstubAllGlobals();
}
});
});