| 1 | import { readdirSync, readFileSync } from "node:fs"; |
| 2 | import { join } from "node:path"; |
| 3 | import { fileURLToPath } from "node:url"; |
| 4 | import { describe, expect, it } from "vitest"; |
| 5 | import { handleProductTelemetry } from "@/app/api/product-telemetry/route"; |
| 6 | |
| 7 | const APP_DIR = fileURLToPath(new URL("../app", import.meta.url)); |
| 8 | |
| 9 | function sourceFiles(dir: string): string[] { |
| 10 | return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { |
| 11 | const path = join(dir, entry.name); |
| 12 | if (entry.isDirectory()) return sourceFiles(path); |
| 13 | return /\.(m?[jt]sx?)$/.test(entry.name) ? [path] : []; |
| 14 | }); |
| 15 | } |
| 16 | |
| 17 | // `export const runtime = "edge"`, `runtime: 'experimental-edge'`, and so on. |
| 18 | // Route segment config only takes effect in app/ files, so that is the scan. |
| 19 | const EDGE_RUNTIME = |
| 20 | /(?:\bexport\s+const\s+runtime\s*=|\bruntime\s*:)\s*["'](?:experimental-)?edge["']/; |
| 21 | |
| 22 | describe("/api/product-telemetry", () => { |
| 23 | it("recognizes edge and experimental-edge declarations", () => { |
| 24 | for (const source of [ |
| 25 | 'export const runtime = "edge";', |
| 26 | "export const runtime = 'experimental-edge';", |
| 27 | 'export const config = { runtime: "edge" };', |
| 28 | ]) { |
| 29 | expect(EDGE_RUNTIME.test(source), source).toBe(true); |
| 30 | } |
| 31 | expect(EDGE_RUNTIME.test('export const runtime = "nodejs";')).toBe(false); |
| 32 | }); |
| 33 | |
| 34 | // @opennextjs/cloudflare does not support the edge runtime (its migrate |
| 35 | // command says to remove the declaration). This is a source guard; it does |
| 36 | // not exercise the adapter or the deployed worker. |
| 37 | it("no app route or page opts into the edge runtime", () => { |
| 38 | const edge = sourceFiles(APP_DIR).filter((file) => |
| 39 | EDGE_RUNTIME.test(readFileSync(file, "utf8")), |
| 40 | ); |
| 41 | expect(edge).toEqual([]); |
| 42 | }); |
| 43 | |
| 44 | it("answers an empty POST without a server error", async () => { |
| 45 | const post = () => |
| 46 | new Request("https://codewhale.net/api/product-telemetry", { |
| 47 | method: "POST", |
| 48 | headers: { "content-type": "application/json" }, |
| 49 | body: "{}", |
| 50 | }); |
| 51 | |
| 52 | const disabled = await handleProductTelemetry(post(), { ingestUrl: null }); |
| 53 | expect(disabled.status).toBe(200); |
| 54 | expect(await disabled.json()).toEqual({ accepted: false, reason: "disabled" }); |
| 55 | |
| 56 | const enabled = await handleProductTelemetry(post(), { |
| 57 | ingestUrl: "https://telemetry.codewhale.net/v1/telemetry", |
| 58 | forward: async () => { |
| 59 | throw new Error("an invalid envelope must not be forwarded"); |
| 60 | }, |
| 61 | }); |
| 62 | expect(enabled.status).toBe(422); |
| 63 | expect(await enabled.json()).toEqual({ accepted: false, reason: "schema" }); |
| 64 | }); |
| 65 | }); |
| 66 |