diff --git a/server/src/runtime/__tests__/fetch-handler.test.ts b/server/src/runtime/__tests__/fetch-handler.test.ts new file mode 100644 index 000000000..faa127d9e --- /dev/null +++ b/server/src/runtime/__tests__/fetch-handler.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, mock } from "bun:test"; + +const getAppFetch = mock(); + +mock.module("../app-instance", () => ({ + getApp: () => ({ + fetch: getAppFetch, + }), +})); + +describe("handleFetch", () => { + afterEach(() => { + getAppFetch.mockReset(); + }); + + it("serves static assets directly when the asset exists", async () => { + getAppFetch.mockResolvedValue(new Response("app-body", { status: 200 })); + + const { handleFetch } = await import("../fetch-handler"); + const assetFetch = mock(async () => new Response("asset-body", { status: 200 })); + + const response = await handleFetch( + new Request("http://localhost/assets/app.js"), + { + ASSETS: { + fetch: assetFetch, + }, + } as unknown as Env, + ); + + expect(await response.text()).toBe("asset-body"); + expect(assetFetch).toHaveBeenCalledTimes(1); + expect(getAppFetch).toHaveBeenCalledTimes(0); + }); + + it("routes /api/blob requests to the app before static assets", async () => { + getAppFetch.mockResolvedValue(new Response("blob-body", { status: 200 })); + + const { handleFetch } = await import("../fetch-handler"); + const assetFetch = mock(async () => new Response("asset-body", { status: 404 })); + + const response = await handleFetch( + new Request("http://localhost/api/blob/images/test.txt"), + { + ASSETS: { + fetch: assetFetch, + }, + } as unknown as Env, + ); + + expect(await response.text()).toBe("blob-body"); + expect(getAppFetch).toHaveBeenCalledTimes(1); + expect(assetFetch).toHaveBeenCalledTimes(0); + expect(new URL(getAppFetch.mock.calls[0][0].url).pathname).toBe("/blob/images/test.txt"); + }); +}); diff --git a/server/src/services/__tests__/storage.test.ts b/server/src/services/__tests__/storage.test.ts index e95947406..d56b27e5f 100644 --- a/server/src/services/__tests__/storage.test.ts +++ b/server/src/services/__tests__/storage.test.ts @@ -192,7 +192,7 @@ describe('StorageService', () => { expect(payload.url).toMatch(/^https:\/\/images\.example\.com\/images\/[a-f0-9]+\.txt$/); }); - it('should return a /blob URL when R2 is configured without S3_ACCESS_HOST', async () => { + it('should return an /api/blob URL when R2 is configured without S3_ACCESS_HOST', async () => { const putCalls: string[] = []; const r2Env = createMockEnv({ R2_BUCKET: { @@ -232,7 +232,7 @@ describe('StorageService', () => { expect(putCalls).toHaveLength(1); const payload = await res.json() as { url: string }; - expect(payload.url).toMatch(/^http:\/\/localhost\/blob\/images\/[a-f0-9]+\.txt$/); + expect(payload.url).toMatch(/^http:\/\/localhost\/api\/blob\/images\/[a-f0-9]+\.txt$/); }); it('should return 500 when S3_ENDPOINT is not defined without R2 binding', async () => { diff --git a/server/src/utils/storage.ts b/server/src/utils/storage.ts index b25755f75..1288832be 100644 --- a/server/src/utils/storage.ts +++ b/server/src/utils/storage.ts @@ -63,7 +63,7 @@ function encodeStorageKey(key: string) { function buildBlobUrl(storageKey: string, baseUrl?: string) { const encodedKey = encodeStorageKey(storageKey); - const path = `/blob/${encodedKey}`; + const path = `/api/blob/${encodedKey}`; if (!baseUrl) { return path;