diff --git a/package.json b/package.json index 2f607dd..8b95f4d 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,13 @@ "private": true, "scripts": { "build": "node scripts/build.mjs", + "mock:protected-api": "node scripts/mock-protected-api.mjs", "test": "vitest run --passWithNoTests", "test:watch": "vitest --passWithNoTests" }, + "dependencies": { + "@logto/chrome-extension": "^0.1.27" + }, "license": "UNLICENSED", "devDependencies": { "jsdom": "^29.0.2", diff --git a/scripts/mock-protected-api.mjs b/scripts/mock-protected-api.mjs new file mode 100644 index 0000000..440bde9 --- /dev/null +++ b/scripts/mock-protected-api.mjs @@ -0,0 +1,72 @@ +import http from "node:http"; + +export function createMockProtectedApiServer({ port = 4319 } = {}) { + let server; + + return { + get baseUrl() { + const address = server?.address(); + const resolvedPort = + typeof address === "object" && address ? address.port : port; + + return `http://127.0.0.1:${resolvedPort}`; + }, + async start() { + server = http.createServer((request, response) => { + if (request.url !== "/api/mock/protected") { + response.writeHead(404, { "content-type": "application/json" }); + response.end(JSON.stringify({ ok: false, error: "not-found" })); + return; + } + + const authHeader = request.headers.authorization ?? ""; + const isBearer = + typeof authHeader === "string" && + authHeader.startsWith("Bearer ") && + authHeader.length > "Bearer ".length; + + if (!isBearer) { + response.writeHead(401, { "content-type": "application/json" }); + response.end(JSON.stringify({ ok: false, error: "unauthorized" })); + return; + } + + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + ok: true, + source: "mock-protected-api", + message: "authorized", + receivedAuthHeader: authHeader + }) + ); + }); + + await new Promise((resolve) => { + server.listen(port, "127.0.0.1", resolve); + }); + }, + async close() { + if (!server) { + return; + } + + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + + resolve(undefined); + }); + }); + } + }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const server = createMockProtectedApiServer(); + await server.start(); + console.log(`mock protected api listening on ${server.baseUrl}`); +} diff --git a/tests/mock-protected-api.test.ts b/tests/mock-protected-api.test.ts new file mode 100644 index 0000000..9c08639 --- /dev/null +++ b/tests/mock-protected-api.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { createMockProtectedApiServer } from "../scripts/mock-protected-api.mjs"; + +const servers: Array<{ close: () => Promise }> = []; + +afterEach(async () => { + while (servers.length > 0) { + await servers.pop()?.close(); + } +}); + +describe("mock-protected-api", () => { + test("returns mock data when a Bearer token is present", async () => { + const server = createMockProtectedApiServer({ port: 0 }); + await server.start(); + servers.push(server); + + const response = await fetch(`${server.baseUrl}/api/mock/protected`, { + headers: { + Authorization: "Bearer abc123" + } + }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual( + expect.objectContaining({ + ok: true, + source: "mock-protected-api" + }) + ); + }); + + test("returns 401 when the Authorization header is missing", async () => { + const server = createMockProtectedApiServer({ port: 0 }); + await server.start(); + servers.push(server); + + const response = await fetch(`${server.baseUrl}/api/mock/protected`); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toEqual({ + ok: false, + error: "unauthorized" + }); + }); +});