From 575830efa51cdafaeea3dda6d130771c5beda610 Mon Sep 17 00:00:00 2001 From: nexryai Date: Sat, 17 Jan 2026 14:31:05 +0000 Subject: [PATCH] Fix path --- src/index.ts | 82 +++++--- test/s3.test.ts | 493 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 542 insertions(+), 33 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9d8558d..27f471b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ /** * S3-Compatible API Server on Cloudflare Workers - * Backend: Google Drive with streaming support + * Backend: Google Drive with streaming support and nested directory structure */ export default { @@ -204,13 +204,15 @@ async function getAccessToken(env: Env): Promise { return data.access_token; } -async function getOrCreateFolder(accessToken: string, folderName: string, env: Env): Promise { - // キャッシュを確認 - const cached = await env.FOLDER_CACHE.get(folderName); +async function getOrCreateFolder(accessToken: string, folderName: string, parentId: string | null, env: Env): Promise { + // キャッシュキーにparentIdを含める + const cacheKey = parentId ? `${parentId}/${folderName}` : folderName; + const cached = await env.FOLDER_CACHE.get(cacheKey); if (cached) return cached; - // フォルダを検索 - const searchRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=name='${encodeURIComponent(folderName)}' and mimeType='application/vnd.google-apps.folder' and trashed=false`, { + // フォルダを検索(親フォルダを指定) + const parentQuery = parentId ? ` and '${parentId}' in parents` : ""; + const searchRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=name='${encodeURIComponent(folderName)}' and mimeType='application/vnd.google-apps.folder' and trashed=false${parentQuery}`, { headers: { Authorization: `Bearer ${accessToken}` }, }); @@ -218,35 +220,65 @@ async function getOrCreateFolder(accessToken: string, folderName: string, env: E if (searchData.files && searchData.files.length > 0) { const folderId = searchData.files[0].id; - await env.FOLDER_CACHE.put(folderName, folderId, { expirationTtl: 3600 }); + await env.FOLDER_CACHE.put(cacheKey, folderId, { expirationTtl: 3600 }); return folderId; } // フォルダが存在しない場合は作成 + const createBody: any = { + name: folderName, + mimeType: "application/vnd.google-apps.folder", + }; + + if (parentId) { + createBody.parents = [parentId]; + } + const createRes = await fetch("https://www.googleapis.com/drive/v3/files", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - name: folderName, - mimeType: "application/vnd.google-apps.folder", - }), + body: JSON.stringify(createBody), }); const createData: any = await createRes.json(); - await env.FOLDER_CACHE.put(folderName, createData.id, { expirationTtl: 3600 }); + await env.FOLDER_CACHE.put(cacheKey, createData.id, { expirationTtl: 3600 }); return createData.id; } +async function resolvePathToFolderAndFile(accessToken: string, bucket: string, objectKey: string, env: Env): Promise<{ parentFolderId: string; fileName: string }> { + // バケットのルートフォルダを取得 + let currentFolderId = await getOrCreateFolder(accessToken, bucket, null, env); + + // objectKeyを/で分割 + const parts = objectKey.split("/").filter((p) => p); + + if (parts.length === 0) { + throw new Error("Invalid object key"); + } + + const fileName = parts[parts.length - 1]; + const directories = parts.slice(0, -1); + + for (const dir of directories) { + currentFolderId = await getOrCreateFolder(accessToken, dir, currentFolderId, env); + } + + return { + parentFolderId: currentFolderId, + fileName: fileName, + }; +} + async function streamUploadToDrive(accessToken: string, stream: ReadableStream | null, bucket: string, objectKey: string, mimeType: string, env: Env): Promise { if (!stream) { throw new Error("Request body is required"); } - // バケット名に対応するフォルダIDを取得 - const folderId = await getOrCreateFolder(accessToken, bucket, env); + // パスを解析してフォルダ階層を作成 + const { parentFolderId, fileName } = await resolvePathToFolderAndFile(accessToken, bucket, objectKey, env); // Resumable uploadの初期化 const initRes = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable", { @@ -257,8 +289,8 @@ async function streamUploadToDrive(accessToken: string, stream: ReadableStream | "Content-Type": "application/json; charset=UTF-8", }, body: JSON.stringify({ - name: objectKey, - parents: [folderId], + name: fileName, + parents: [parentFolderId], }), }); @@ -297,18 +329,18 @@ async function findFileInFolder(accessToken: string, folderId: string, fileName: } async function streamDownloadFromDrive(accessToken: string, bucket: string, objectKey: string, env: Env): Promise<{ body: ReadableStream; contentType: string; size: number; id: string }> { - const folderId = await getOrCreateFolder(accessToken, bucket, env); - const file = await findFileInFolder(accessToken, folderId, objectKey); + const { parentFolderId, fileName } = await resolvePathToFolderAndFile(accessToken, bucket, objectKey, env); + const file = await findFileInFolder(accessToken, parentFolderId, fileName); if (!file) { throw new Error("File not found"); } - const controller = new AbortController(); + const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, 5000); - + const downloadRes = await fetch(`https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`, { headers: { Authorization: `Bearer ${accessToken}` }, signal: controller.signal, @@ -331,8 +363,8 @@ async function streamDownloadFromDrive(accessToken: string, bucket: string, obje } async function deleteFromDrive(accessToken: string, bucket: string, objectKey: string, env: Env): Promise { - const folderId = await getOrCreateFolder(accessToken, bucket, env); - const file = await findFileInFolder(accessToken, folderId, objectKey); + const { parentFolderId, fileName } = await resolvePathToFolderAndFile(accessToken, bucket, objectKey, env); + const file = await findFileInFolder(accessToken, parentFolderId, fileName); if (!file) { throw new Error("File not found"); @@ -349,8 +381,8 @@ async function deleteFromDrive(accessToken: string, bucket: string, objectKey: s } async function getFileMetadata(accessToken: string, bucket: string, objectKey: string, env: Env): Promise<{ id: string; mimeType: string; size: number }> { - const folderId = await getOrCreateFolder(accessToken, bucket, env); - const file = await findFileInFolder(accessToken, folderId, objectKey); + const { parentFolderId, fileName } = await resolvePathToFolderAndFile(accessToken, bucket, objectKey, env); + const file = await findFileInFolder(accessToken, parentFolderId, fileName); if (!file) { throw new Error("File not found"); @@ -364,7 +396,7 @@ async function getFileMetadata(accessToken: string, bucket: string, objectKey: s } async function listFiles(accessToken: string, bucket: string, env: Env): Promise { - const folderId = await getOrCreateFolder(accessToken, bucket, env); + const folderId = await getOrCreateFolder(accessToken, bucket, null, env); const listRes = await fetch(`https://www.googleapis.com/drive/v3/files?q='${folderId}' in parents and trashed=false&fields=files(id,name,mimeType,size,modifiedTime)`, { headers: { Authorization: `Bearer ${accessToken}` }, diff --git a/test/s3.test.ts b/test/s3.test.ts index a006977..8536a36 100644 --- a/test/s3.test.ts +++ b/test/s3.test.ts @@ -140,7 +140,6 @@ describe("S3 API Server with Google Drive Backend", () => { const response = await worker.fetch(signedReq, ENV, CTX); expect(response.status).toBe(200); - const result = await response.json(); expect(result).toHaveProperty("id"); }); @@ -229,7 +228,6 @@ describe("S3 API Server with Google Drive Backend", () => { const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); const parsedUrl = new URL(url); - const testUrl = new URL(endpoint); testUrl.hostname = parsedUrl.hostname; testUrl.pathname = parsedUrl.pathname; @@ -237,7 +235,6 @@ describe("S3 API Server with Google Drive Backend", () => { const request = new Request(testUrl.toString(), { method: "GET" }); const response = await worker.fetch(request, ENV, CTX); - expect(response.status).toBe(200); }); @@ -264,7 +261,6 @@ describe("S3 API Server with Google Drive Backend", () => { service: "s3", }); - // ファイル一覧を返すようにモックを更新 const originalFetch = global.fetch; (global.fetch as any) = vi.fn(async (url: string, init?: any) => { const urlStr = typeof url === "string" ? url : url.toString(); @@ -313,7 +309,6 @@ describe("S3 API Server with Google Drive Backend", () => { const response = await worker.fetch(signedReq, ENV, CTX); expect(response.status).toBe(200); - const xmlText = await response.text(); expect(xmlText).toContain(" { service: "s3", }); - // ファイルが見つからないケース const originalFetch = global.fetch; (global.fetch as any) = vi.fn(async (url: string, init?: any) => { const urlStr = typeof url === "string" ? url : url.toString(); @@ -356,7 +350,6 @@ describe("S3 API Server with Google Drive Backend", () => { } if (urlStr.includes("drive/v3/files") && urlStr.includes("in parents")) { - // ファイルが存在しない return new Response(JSON.stringify({ files: [] }), { status: 200 }); } @@ -533,11 +526,495 @@ describe("S3 API Server with Google Drive Backend", () => { const response = await worker.fetch(signedReq, ENV, CTX); expect(response.status).toBe(200); - const xmlText = await response.text(); expect(xmlText).toContain(""); global.fetch = originalFetch; }); + + // 12. ネストされたフォルダーへのアップロード + it("should upload file to nested directory structure", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const folderIds = { + bucket: "bucket-id-123", + dir1: "dir1-id-456", + dir2: "dir2-id-789", + }; + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + // OAuth トークン + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + // フォルダ作成 (POST) + if (urlStr === "https://www.googleapis.com/drive/v3/files" && init?.method === "POST") { + const body = JSON.parse(init.body); + if (body.mimeType === "application/vnd.google-apps.folder") { + // フォルダ名に応じてIDを返す + if (body.name === "dir1") { + return new Response(JSON.stringify({ id: folderIds.dir1, name: "dir1" }), { status: 200 }); + } else if (body.name === "dir2") { + return new Response(JSON.stringify({ id: folderIds.dir2, name: "dir2" }), { status: 200 }); + } + } + } + + // フォルダ検索 - バケット (親なし) + if (urlStr.includes("name='test-bucket'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'") && !urlStr.includes("in parents")) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.bucket, name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + // フォルダ検索 - dir1 + if (urlStr.includes("name='dir1'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.bucket}' in parents`)) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.dir1, name: "dir1" }], + }), + { status: 200 }, + ); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + // フォルダ検索 - dir2 + if (urlStr.includes("name='dir2'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.dir1}' in parents`)) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.dir2, name: "dir2" }], + }), + { status: 200 }, + ); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + // Resumable upload 初期化 + if (urlStr.includes("uploadType=resumable") && init?.method === "POST") { + const body = JSON.parse(init.body); + // 親フォルダがdir2であることを確認 + expect(body.parents).toEqual([folderIds.dir2]); + expect(body.name).toBe("file.txt"); + + return new Response(null, { + status: 200, + headers: { Location: "https://www.googleapis.com/upload/drive/v3/files/uploadid999" }, + }); + } + + // Resumable upload 実行 + if (urlStr.includes("upload/drive/v3/files/uploadid999")) { + return new Response( + JSON.stringify({ + id: "nested-file-id-999", + name: "file.txt", + mimeType: "text/plain", + }), + { status: 200 }, + ); + } + + console.error("Unhandled URL:", urlStr); + return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/dir1/dir2/file.txt`; + const signedReq = await aws4.sign(requestUrl, { + method: "PUT", + headers: { + "Content-Type": "text/plain", + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + body: "Nested content", + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(200); + const result = await response.json(); + expect(result.id).toBe("nested-file-id-999"); + expect(result.name).toBe("file.txt"); + + global.fetch = originalFetch; + }); + + // 13. ネストされたフォルダーからのダウンロード + it("should download file from nested directory structure", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const folderIds = { + bucket: "bucket-id-123", + images: "images-id-456", + photos: "photos-id-789", + }; + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + // フォルダ作成 (POST) + if (urlStr === "https://www.googleapis.com/drive/v3/files" && init?.method === "POST") { + const body = JSON.parse(init.body); + if (body.mimeType === "application/vnd.google-apps.folder") { + if (body.name === "images") { + return new Response(JSON.stringify({ id: folderIds.images, name: "images" }), { status: 200 }); + } else if (body.name === "photos") { + return new Response(JSON.stringify({ id: folderIds.photos, name: "photos" }), { status: 200 }); + } + } + } + + // バケット検索 + if (urlStr.includes("name='test-bucket'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'") && !urlStr.includes("in parents")) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.bucket, name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + // images フォルダ検索 + if (urlStr.includes("name='images'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.bucket}' in parents`)) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.images, name: "images" }], + }), + { status: 200 }, + ); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + // photos フォルダ検索 + if (urlStr.includes("name='photos'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.images}' in parents`)) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.photos, name: "photos" }], + }), + { status: 200 }, + ); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + // ファイル検索 - photo.jpg + if (urlStr.includes("name='photo.jpg'") && urlStr.includes(`'${folderIds.photos}' in parents`)) { + return new Response( + JSON.stringify({ + files: [ + { + id: "photo-file-id-999", + name: "photo.jpg", + mimeType: "image/jpeg", + size: "12345", + }, + ], + }), + { status: 200 }, + ); + } + + // ファイルダウンロード + if (urlStr.includes("photo-file-id-999") && urlStr.includes("alt=media")) { + return new Response("Binary image data", { + status: 200, + headers: { "Content-Type": "image/jpeg" }, + }); + } + + console.error("Unhandled URL:", urlStr); + return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/images/photos/photo.jpg`; + const signedReq = await aws4.sign(requestUrl, { + method: "GET", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("image/jpeg"); + expect(await response.text()).toBe("Binary image data"); + + global.fetch = originalFetch; + }); + + // 14. ネストされたフォルダーからの削除 + it("should delete file from nested directory structure", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const folderIds = { + bucket: "bucket-id-123", + docs: "docs-id-456", + archive: "archive-id-789", + }; + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + // フォルダ作成 (POST) + if (urlStr === "https://www.googleapis.com/drive/v3/files" && init?.method === "POST") { + const body = JSON.parse(init.body); + if (body.mimeType === "application/vnd.google-apps.folder") { + if (body.name === "docs") { + return new Response(JSON.stringify({ id: folderIds.docs, name: "docs" }), { status: 200 }); + } else if (body.name === "archive") { + return new Response(JSON.stringify({ id: folderIds.archive, name: "archive" }), { status: 200 }); + } + } + } + + if (urlStr.includes("name='test-bucket'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'") && !urlStr.includes("in parents")) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.bucket, name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("name='docs'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.bucket}' in parents`)) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.docs, name: "docs" }], + }), + { status: 200 }, + ); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + if (urlStr.includes("name='archive'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.docs}' in parents`)) { + return new Response( + JSON.stringify({ + files: [{ id: folderIds.archive, name: "archive" }], + }), + { status: 200 }, + ); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + if (urlStr.includes("name='old.pdf'") && urlStr.includes(`'${folderIds.archive}' in parents`)) { + return new Response( + JSON.stringify({ + files: [ + { + id: "old-pdf-id-999", + name: "old.pdf", + mimeType: "application/pdf", + size: "54321", + }, + ], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("old-pdf-id-999") && init?.method === "DELETE") { + return new Response(null, { status: 204 }); + } + + console.error("Unhandled URL:", urlStr); + return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/docs/archive/old.pdf`; + const signedReq = await aws4.sign(requestUrl, { + method: "DELETE", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(204); + + global.fetch = originalFetch; + }); + + // 15. 深くネストされたフォルダーのメタデータ取得 + it("should get metadata for file in deeply nested structure", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const folderIds = { + bucket: "bucket-id-123", + a: "a-id-111", + b: "b-id-222", + c: "c-id-333", + d: "d-id-444", + }; + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + // フォルダ作成 (POST) + if (urlStr === "https://www.googleapis.com/drive/v3/files" && init?.method === "POST") { + const body = JSON.parse(init.body); + if (body.mimeType === "application/vnd.google-apps.folder") { + if (body.name === "a") { + return new Response(JSON.stringify({ id: folderIds.a, name: "a" }), { status: 200 }); + } else if (body.name === "b") { + return new Response(JSON.stringify({ id: folderIds.b, name: "b" }), { status: 200 }); + } else if (body.name === "c") { + return new Response(JSON.stringify({ id: folderIds.c, name: "c" }), { status: 200 }); + } else if (body.name === "d") { + return new Response(JSON.stringify({ id: folderIds.d, name: "d" }), { status: 200 }); + } + } + } + + // フォルダ階層の検索 + if (urlStr.includes("name='test-bucket'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'") && !urlStr.includes("in parents")) { + return new Response(JSON.stringify({ files: [{ id: folderIds.bucket, name: "test-bucket" }] }), { status: 200 }); + } + if (urlStr.includes("name='a'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.bucket}' in parents`)) { + return new Response(JSON.stringify({ files: [{ id: folderIds.a, name: "a" }] }), { status: 200 }); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + if (urlStr.includes("name='b'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.a}' in parents`)) { + return new Response(JSON.stringify({ files: [{ id: folderIds.b, name: "b" }] }), { status: 200 }); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + if (urlStr.includes("name='c'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.b}' in parents`)) { + return new Response(JSON.stringify({ files: [{ id: folderIds.c, name: "c" }] }), { status: 200 }); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + if (urlStr.includes("name='d'") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + if (urlStr.includes(`'${folderIds.c}' in parents`)) { + return new Response(JSON.stringify({ files: [{ id: folderIds.d, name: "d" }] }), { status: 200 }); + } else { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + } + + // ファイル検索 + if (urlStr.includes("name='deep.txt'") && urlStr.includes(`'${folderIds.d}' in parents`)) { + return new Response( + JSON.stringify({ + files: [ + { + id: "deep-file-id-999", + name: "deep.txt", + mimeType: "text/plain", + size: "999", + }, + ], + }), + { status: 200 }, + ); + } + + console.error("Unhandled URL:", urlStr); + return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/a/b/c/d/deep.txt`; + const signedReq = await aws4.sign(requestUrl, { + method: "HEAD", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("text/plain"); + expect(response.headers.get("Content-Length")).toBe("999"); + expect(response.headers.get("ETag")).toBe('"deep-file-id-999"'); + + global.fetch = originalFetch; + }); });