mirror of
https://github.com/Nezumi-2711/google-drive-s3.git
synced 2026-09-22 13:38:30 +00:00
feat: add api for manage s3 storage
This commit is contained in:
@@ -194,13 +194,6 @@ describe("Dashboard authentication API routes", () => {
|
||||
expect(blockedRes.headers.get("Retry-After")).toBe("900");
|
||||
});
|
||||
|
||||
it("does not claim /auth when auth is a configured bucket", async () => {
|
||||
const withAuthBucket = { ...ENV, ALLOWED_BUCKETS: "test-bucket,auth" };
|
||||
const response = await worker.fetch(new Request(`${ENDPOINT}/auth/login`), withAuthBucket, CTX);
|
||||
expect(response.status).toBe(403);
|
||||
expect(await response.text()).toContain("<Code>SignatureDoesNotMatch</Code>");
|
||||
});
|
||||
|
||||
it("emits CORS headers for allowed origin", async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/auth/session`, {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { sha256 } from "../src/aws-signature";
|
||||
import worker from "../src/index";
|
||||
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;
|
||||
|
||||
let testIpCounter = 1;
|
||||
function getUniqueIp(): string {
|
||||
return `10.0.0.${testIpCounter++}`;
|
||||
}
|
||||
|
||||
async function getValidToken(ip = getUniqueIp()): Promise<string> {
|
||||
const passwordHash = 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": ip },
|
||||
body: JSON.stringify({ passwordHash }),
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
const data = (await loginRes.json()) as { token: string };
|
||||
return data.token;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await (ENV.AUTH_KV as KVNamespace).delete("drive-about");
|
||||
await (ENV.FOLDER_CACHE as KVNamespace).delete("bucket-registry");
|
||||
for (const { name } of (await (ENV.FOLDER_CACHE as KVNamespace).list()).keys) {
|
||||
await (ENV.FOLDER_CACHE as KVNamespace).delete(name);
|
||||
}
|
||||
});
|
||||
|
||||
describe("Bucket management CRUD API routes (/api/buckets, /api/import*)", () => {
|
||||
it("returns 503 on /api/buckets when DRIVE_ROOT_FOLDER is unset", async () => {
|
||||
const token = await getValidToken();
|
||||
const customEnv = { ...ENV, DRIVE_ROOT_FOLDER: undefined };
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
customEnv,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(503);
|
||||
const data = (await res.json()) as { message: string };
|
||||
expect(data.message).toBe("Storage root folder is not configured");
|
||||
});
|
||||
|
||||
it("rejects reserved bucket names on POST /api/buckets with 400", async () => {
|
||||
const token = await getValidToken();
|
||||
for (const reserved of ["auth", "api", "docs"]) {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ name: reserved }),
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
const data = (await res.json()) as { message: string };
|
||||
expect(data.message).toContain("reserved");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects invalid bucket names on POST /api/buckets with 400", async () => {
|
||||
const token = await getValidToken();
|
||||
const invalidNames = ["ab", "Abc", "bucket--name", "-bucket", "bucket-", "192.168.1.1"];
|
||||
for (const name of invalidNames) {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
it("creates a bucket with POST /api/buckets and handles conflict with 409", async () => {
|
||||
const token = await getValidToken();
|
||||
|
||||
const fakeFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
|
||||
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
||||
|
||||
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 (method === "GET") {
|
||||
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") && q.includes("mimeType='application/vnd.google-apps.folder'")) {
|
||||
return Response.json({ files: [] });
|
||||
}
|
||||
if (q.includes("name='new-bucket'")) {
|
||||
return Response.json({ files: [] });
|
||||
}
|
||||
return Response.json({ files: [] });
|
||||
}
|
||||
if (method === "POST") {
|
||||
return Response.json({ id: "new-bucket-folder-id" });
|
||||
}
|
||||
}
|
||||
if (url.pathname === "/drive/v3/files/new-bucket-folder-id" && method === "PATCH") {
|
||||
return Response.json({ id: "new-bucket-folder-id", name: "new-bucket" });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fakeFetch);
|
||||
|
||||
try {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ name: "new-bucket", publicRead: true }),
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
const data = (await res.json()) as { name: string; folderId: string; publicRead: boolean };
|
||||
expect(data.name).toBe("new-bucket");
|
||||
expect(data.folderId).toBe("new-bucket-folder-id");
|
||||
expect(data.publicRead).toBe(true);
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("updates bucket publicRead with PATCH /api/buckets/:name", async () => {
|
||||
const token = await getValidToken();
|
||||
|
||||
const fakeFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
|
||||
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
||||
|
||||
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("'root-folder-id' in parents")) {
|
||||
return Response.json({
|
||||
files: [{ id: "folder-target-bucket", name: "target-bucket", mimeType: "application/vnd.google-apps.folder", appProperties: { s3PublicRead: "false" } }],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (url.pathname === "/drive/v3/files/folder-target-bucket" && method === "PATCH") {
|
||||
return Response.json({ id: "folder-target-bucket", name: "target-bucket", appProperties: { s3PublicRead: "true" } });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fakeFetch);
|
||||
|
||||
try {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets/target-bucket`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ publicRead: true }),
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const data = (await res.json()) as { name: string; publicRead: boolean };
|
||||
expect(data.name).toBe("target-bucket");
|
||||
expect(data.publicRead).toBe(true);
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects DELETE /api/buckets/:name when bucket has children with 409", 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/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-non-empty", name: "non-empty", mimeType: "application/vnd.google-apps.folder" }],
|
||||
});
|
||||
}
|
||||
if (q.includes("'folder-non-empty' in parents")) {
|
||||
return Response.json({ files: [{ id: "child-file-1" }] });
|
||||
}
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fakeFetch);
|
||||
|
||||
try {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets/non-empty`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(409);
|
||||
const data = (await res.json()) as { message: string };
|
||||
expect(data.message).toContain("not empty");
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("deletes empty bucket with DELETE /api/buckets/:name and returns 204", async () => {
|
||||
const token = await getValidToken();
|
||||
|
||||
const fakeFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
|
||||
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
||||
|
||||
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("'root-folder-id' in parents")) {
|
||||
return Response.json({
|
||||
files: [{ id: "folder-empty", name: "empty", mimeType: "application/vnd.google-apps.folder" }],
|
||||
});
|
||||
}
|
||||
if (q.includes("'folder-empty' in parents")) {
|
||||
return Response.json({ files: [] });
|
||||
}
|
||||
}
|
||||
if (url.pathname === "/drive/v3/files/folder-empty" && method === "PATCH") {
|
||||
return Response.json({ id: "folder-empty", trashed: true });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fakeFetch);
|
||||
|
||||
try {
|
||||
const res = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/buckets/empty`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(res.status).toBe(204);
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("lists import candidates and imports selected buckets", async () => {
|
||||
const token = await getValidToken();
|
||||
|
||||
const fakeFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = new URL(typeof input === "string" ? input : input instanceof Request ? input.url : input.toString());
|
||||
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
||||
|
||||
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("'root-folder-id' in parents")) {
|
||||
return Response.json({ files: [] });
|
||||
}
|
||||
if (q.includes("'root' in parents")) {
|
||||
return Response.json({
|
||||
files: [
|
||||
{ id: "root-folder-id", name: "s3-storage", mimeType: "application/vnd.google-apps.folder" },
|
||||
{ id: "import-folder-1", name: "legacy-bucket", mimeType: "application/vnd.google-apps.folder" },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (q.includes("'import-folder-1' in parents")) {
|
||||
return Response.json({
|
||||
files: [
|
||||
{ id: "f1", mimeType: "text/plain" },
|
||||
{ id: "f2", mimeType: "text/plain" },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (url.pathname === "/drive/v3/files/import-folder-1" && method === "PATCH") {
|
||||
return Response.json({ id: "import-folder-1", name: "legacy-bucket" });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fakeFetch);
|
||||
|
||||
try {
|
||||
// GET /api/import-candidates
|
||||
const listRes = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/import-candidates`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(listRes.status).toBe(200);
|
||||
const listData = (await listRes.json()) as { candidates: Array<{ name: string; folderId: string; objectCount: number }> };
|
||||
expect(listData.candidates).toHaveLength(1);
|
||||
expect(listData.candidates[0].name).toBe("legacy-bucket");
|
||||
expect(listData.candidates[0].objectCount).toBe(2);
|
||||
|
||||
// POST /api/import
|
||||
const importRes = await worker.fetch(
|
||||
new Request(`${ENDPOINT}/api/import`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ names: ["legacy-bucket"] }),
|
||||
}),
|
||||
ENV,
|
||||
CTX,
|
||||
);
|
||||
expect(importRes.status).toBe(200);
|
||||
const importData = (await importRes.json()) as { imported: string[]; failed: unknown[] };
|
||||
expect(importData.imported).toEqual(["legacy-bucket"]);
|
||||
expect(importData.failed).toHaveLength(0);
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
});
|
||||
+13
-1
@@ -25,7 +25,19 @@ function fakeGoogleFetch(input: string | URL | Request, init?: RequestInit): Pro
|
||||
const request = input instanceof Request ? input : new Request(input, init);
|
||||
const url = new URL(request.url);
|
||||
if (url.hostname === "oauth2.googleapis.com") return Promise.resolve(Response.json({ access_token: "token", expires_in: 3600 }));
|
||||
if (url.pathname === "/drive/v3/files" && request.method === "GET") return Promise.resolve(Response.json({ files: [] }));
|
||||
if (url.pathname === "/drive/v3/files" && request.method === "GET") {
|
||||
const q = url.searchParams.get("q") ?? "";
|
||||
if (q.includes("name='s3-storage'") && q.includes("'root' in parents")) {
|
||||
return Promise.resolve(Response.json({ files: [{ id: "root-folder-id", name: "s3-storage" }] }));
|
||||
}
|
||||
if (q.includes("'root-folder-id' in parents")) {
|
||||
return Promise.resolve(Response.json({ files: [{ id: "folder-test-bucket", name: "test-bucket", mimeType: "application/vnd.google-apps.folder" }] }));
|
||||
}
|
||||
if (q.includes("name='test-bucket'")) {
|
||||
return Promise.resolve(Response.json({ files: [{ id: "folder-test-bucket", name: "test-bucket", mimeType: "application/vnd.google-apps.folder" }] }));
|
||||
}
|
||||
return Promise.resolve(Response.json({ files: [] }));
|
||||
}
|
||||
if (url.pathname === "/drive/v3/files" && request.method === "POST") return Promise.resolve(Response.json({ id: "folder-1" }));
|
||||
if (url.pathname.startsWith("/upload/drive/v3/files")) return Promise.resolve(new Response(null, { headers: { Location: "https://www.googleapis.com/upload/session/test" } }));
|
||||
if (url.pathname === "/upload/session/test") return Promise.resolve(Response.json({ id: "file-1", name: "file.txt", md5Checksum: "d41d8cd98f00b204e9800998ecf8427e" }));
|
||||
|
||||
+17
-10
@@ -24,16 +24,23 @@ describe("API documentation routes", () => {
|
||||
|
||||
it("bypasses documentation routes when disabled", async () => {
|
||||
const disabled = { ...ENV, ENABLE_DOCS: "false" };
|
||||
for (const path of ["/docs", "/openapi.yaml"]) {
|
||||
const response = await worker.fetch(new Request(`${ENDPOINT}${path}`), disabled, CTX);
|
||||
expect(response.status).toBe(403);
|
||||
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: "token", expires_in: 3600 });
|
||||
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" }] });
|
||||
}
|
||||
return Response.json({ files: [] });
|
||||
});
|
||||
vi.stubGlobal("fetch", fakeFetch);
|
||||
try {
|
||||
for (const path of ["/docs", "/openapi.yaml"]) {
|
||||
const response = await worker.fetch(new Request(`${ENDPOINT}${path}`), disabled, CTX);
|
||||
expect(response.status).toBe(403);
|
||||
}
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not claim /docs when docs is a configured bucket", async () => {
|
||||
const withDocsBucket = { ...ENV, ALLOWED_BUCKETS: "test-bucket,docs" };
|
||||
const response = await worker.fetch(new Request(`${ENDPOINT}/docs`), withDocsBucket, CTX);
|
||||
expect(response.status).toBe(403);
|
||||
expect(await response.text()).toContain("<Code>SignatureDoesNotMatch</Code>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,6 +225,14 @@ let drive: FakeDrive;
|
||||
|
||||
beforeEach(async () => {
|
||||
drive = new FakeDrive();
|
||||
// Pre-create root folder "s3-storage" under "root" and bucket folders under root folder
|
||||
const rootFolderId = "folder-root";
|
||||
drive.folders.set(rootFolderId, { id: rootFolderId, name: "s3-storage", parent: "root" });
|
||||
const testBucketId = "folder-test-bucket";
|
||||
drive.folders.set(testBucketId, { id: testBucketId, name: "test-bucket", parent: rootFolderId });
|
||||
const emptyBucketId = "folder-empty-bucket";
|
||||
drive.folders.set(emptyBucketId, { id: emptyBucketId, name: "empty-bucket", parent: rootFolderId });
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn((input, init) => drive.handle(input, init)),
|
||||
|
||||
+45
-10
@@ -92,6 +92,23 @@ describe("Dashboard status API routes (/api/*)", () => {
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
@@ -113,6 +130,11 @@ describe("Dashboard status API routes (/api/*)", () => {
|
||||
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,
|
||||
@@ -210,6 +232,7 @@ describe("Dashboard status API routes (/api/*)", () => {
|
||||
|
||||
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) => {
|
||||
@@ -219,9 +242,27 @@ describe("Dashboard status API routes (/api/*)", () => {
|
||||
}
|
||||
if (url.pathname === "/drive/v3/files") {
|
||||
const q = url.searchParams.get("q") ?? "";
|
||||
if (q.includes("name='test-bucket'")) {
|
||||
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: [
|
||||
@@ -235,6 +276,9 @@ describe("Dashboard status API routes (/api/*)", () => {
|
||||
],
|
||||
});
|
||||
}
|
||||
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 });
|
||||
@@ -265,13 +309,4 @@ describe("Dashboard status API routes (/api/*)", () => {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("routes /api to S3 handler when 'api' is configured as an allowed bucket", async () => {
|
||||
const customEnv = { ...ENV, ALLOWED_BUCKETS: "api,test-bucket" };
|
||||
const res = await worker.fetch(new Request(`${ENDPOINT}/api/status`), customEnv, CTX);
|
||||
// S3 router checks signature or returns SignatureDoesNotMatch / AccessDenied etc.
|
||||
expect(res.status).toBe(403);
|
||||
const text = await res.text();
|
||||
expect(text).toContain("<Error><Code>");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user