diff --git a/README.md b/README.md index 2ff0b72..268eec1 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,18 @@ The popup dev panel is controlled by `enableDevAuthPanel`. 6. Click `测试受保护接口` 7. Confirm the popup shows JSON containing `"source": "mock-protected-api"` and `"message": "authorized"` +## Batch Submit Mock Test + +1. Run `npm run mock:protected-api` +2. Run `npm run build` +3. Reload the unpacked extension from `dist/` +4. Open `https://xingtu.cn/ad/creator/market` +5. Choose an export range in the plugin toolbar +6. Click `提交批次` +7. Enter a batch name in the browser prompt +8. Confirm the toolbar shows `批次提交成功` +9. Confirm the mock batch response accepts the payload and reports the submitted `batchId` + ## Market Auth Gate When the market page is opened without a valid auth state, the content script renders diff --git a/scripts/mock-protected-api.mjs b/scripts/mock-protected-api.mjs index 440bde9..174c35a 100644 --- a/scripts/mock-protected-api.mjs +++ b/scripts/mock-protected-api.mjs @@ -12,34 +12,49 @@ export function createMockProtectedApiServer({ port = 4319 } = {}) { 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" })); + server = http.createServer(async (request, response) => { + if (request.url === "/api/mock/protected") { + const authHeader = readBearerToken(request, response); + if (!authHeader) { + return; + } + + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + ok: true, + source: "mock-protected-api", + message: "authorized", + receivedAuthHeader: authHeader + }) + ); return; } - const authHeader = request.headers.authorization ?? ""; - const isBearer = - typeof authHeader === "string" && - authHeader.startsWith("Bearer ") && - authHeader.length > "Bearer ".length; + if (request.url === "/api/mock/batches" && request.method === "POST") { + const authHeader = readBearerToken(request, response); + if (!authHeader) { + return; + } - if (!isBearer) { - response.writeHead(401, { "content-type": "application/json" }); - response.end(JSON.stringify({ ok: false, error: "unauthorized" })); + const payload = await readJsonBody(request); + const authors = Array.isArray(payload?.authors) ? payload.authors : []; + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + ok: true, + source: "mock-batch-submit", + acceptedCount: authors.length, + batchId: + typeof payload?.batchId === "string" ? payload.batchId : null, + receivedAuthHeader: authHeader + }) + ); return; } - response.writeHead(200, { "content-type": "application/json" }); - response.end( - JSON.stringify({ - ok: true, - source: "mock-protected-api", - message: "authorized", - receivedAuthHeader: authHeader - }) - ); + response.writeHead(404, { "content-type": "application/json" }); + response.end(JSON.stringify({ ok: false, error: "not-found" })); }); await new Promise((resolve) => { @@ -65,6 +80,36 @@ export function createMockProtectedApiServer({ port = 4319 } = {}) { }; } +function readBearerToken(request, response) { + 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 null; + } + + return authHeader; +} + +async function readJsonBody(request) { + const chunks = []; + + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + + if (chunks.length === 0) { + return null; + } + + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +} + if (import.meta.url === `file://${process.argv[1]}`) { const server = createMockProtectedApiServer(); await server.start(); diff --git a/tests/mock-protected-api.test.ts b/tests/mock-protected-api.test.ts index 9c08639..8081dfa 100644 --- a/tests/mock-protected-api.test.ts +++ b/tests/mock-protected-api.test.ts @@ -44,4 +44,37 @@ describe("mock-protected-api", () => { error: "unauthorized" }); }); + + test("accepts a batch payload 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/batches`, { + body: JSON.stringify({ + authors: [{ authorId: "111", authorName: "达人A" }], + batchId: "批次A-2026-04-22T12:30:00.000Z", + batchName: "批次A", + createdAt: "2026-04-22T12:30:00.000Z", + creatorName: "王少卿", + logtoUserId: "p7pdhhtde8kj", + resource: "https://talent-search.intelligrow.cn" + }), + headers: { + Authorization: "Bearer abc123", + "Content-Type": "application/json" + }, + method: "POST" + }); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual( + expect.objectContaining({ + acceptedCount: 1, + batchId: "批次A-2026-04-22T12:30:00.000Z", + ok: true, + source: "mock-batch-submit" + }) + ); + }); });