171 lines
5.8 KiB
TypeScript
171 lines
5.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
getAccessToken: vi.fn(),
|
|
fetch: vi.fn<typeof fetch>(),
|
|
}));
|
|
|
|
vi.mock("@/lib/auth", () => ({
|
|
getAuth: () => ({ api: { getAccessToken: mocks.getAccessToken } }),
|
|
}));
|
|
|
|
function jsonResponse(body: unknown, status = 200) {
|
|
return Response.json(body, { status });
|
|
}
|
|
|
|
describe("gscClient", () => {
|
|
beforeEach(() => {
|
|
mocks.getAccessToken.mockReset();
|
|
mocks.getAccessToken.mockResolvedValue({ accessToken: "tok_123" });
|
|
mocks.fetch.mockReset();
|
|
vi.stubGlobal("fetch", mocks.fetch);
|
|
});
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("lists sites with a bearer token", async () => {
|
|
mocks.fetch.mockResolvedValue(
|
|
jsonResponse({
|
|
siteEntry: [{ siteUrl: "https://x/", permissionLevel: "siteOwner" }],
|
|
}),
|
|
);
|
|
const { createGscClient } = await import("./gscClient");
|
|
const sites = await createGscClient({ userId: "u1" }).listSites();
|
|
|
|
expect(sites).toHaveLength(1);
|
|
const [url, init] = mocks.fetch.mock.calls[0];
|
|
expect(url).toBe("https://www.googleapis.com/webmasters/v3/sites");
|
|
expect(init?.headers).toMatchObject({ Authorization: "Bearer tok_123" });
|
|
});
|
|
|
|
it("targets the selected Better Auth grant by Google sub", async () => {
|
|
mocks.fetch.mockResolvedValue(jsonResponse({ siteEntry: [] }));
|
|
const { createGscClient } = await import("./gscClient");
|
|
|
|
await createGscClient({
|
|
userId: "u1",
|
|
gscAccountId: "google-sub-a",
|
|
}).listSites();
|
|
|
|
expect(mocks.getAccessToken).toHaveBeenCalledWith({
|
|
body: {
|
|
providerId: "google-search-console",
|
|
userId: "u1",
|
|
accountId: "google-sub-a",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("omits accountId for the legacy null-account fallback", async () => {
|
|
mocks.fetch.mockResolvedValue(jsonResponse({ siteEntry: [] }));
|
|
const { createGscClient } = await import("./gscClient");
|
|
|
|
await createGscClient({ userId: "u1" }).listSites();
|
|
|
|
expect(mocks.getAccessToken).toHaveBeenCalledWith({
|
|
body: { providerId: "google-search-console", userId: "u1" },
|
|
});
|
|
});
|
|
|
|
it("fetches the Google account email from userinfo", async () => {
|
|
mocks.fetch.mockResolvedValue(
|
|
jsonResponse({ email: "client@example.com" }),
|
|
);
|
|
const { createGscClient } = await import("./gscClient");
|
|
|
|
const email = await createGscClient({
|
|
userId: "u1",
|
|
gscAccountId: "google-sub-a",
|
|
}).getUserInfoEmail();
|
|
|
|
expect(email).toBe("client@example.com");
|
|
const [url, init] = mocks.fetch.mock.calls[0];
|
|
expect(url).toBe("https://openidconnect.googleapis.com/v1/userinfo");
|
|
expect(init?.headers).toMatchObject({ Authorization: "Bearer tok_123" });
|
|
});
|
|
|
|
it("encodes the siteUrl in the searchAnalytics path (both property forms)", async () => {
|
|
mocks.fetch.mockImplementation(async () => jsonResponse({ rows: [] }));
|
|
const { createGscClient } = await import("./gscClient");
|
|
const client = createGscClient({ userId: "u1" });
|
|
|
|
await client.querySearchAnalytics("sc-domain:example.com", {
|
|
startDate: "2026-01-01",
|
|
endDate: "2026-01-28",
|
|
});
|
|
expect(mocks.fetch.mock.calls[0][0]).toBe(
|
|
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query",
|
|
);
|
|
|
|
await client.querySearchAnalytics("https://example.com/", {
|
|
startDate: "2026-01-01",
|
|
endDate: "2026-01-28",
|
|
});
|
|
expect(mocks.fetch.mock.calls[1][0]).toBe(
|
|
"https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query",
|
|
);
|
|
});
|
|
|
|
it("posts to the URL Inspection endpoint and returns the result", async () => {
|
|
mocks.fetch.mockResolvedValue(
|
|
jsonResponse({
|
|
inspectionResult: {
|
|
indexStatusResult: { verdict: "PASS", coverageState: "Indexed" },
|
|
},
|
|
}),
|
|
);
|
|
const { createGscClient } = await import("./gscClient");
|
|
const result = await createGscClient({ userId: "u1" }).inspectUrl(
|
|
"sc-domain:example.com",
|
|
"https://example.com/post",
|
|
"en-US",
|
|
);
|
|
|
|
const [url, init] = mocks.fetch.mock.calls[0];
|
|
expect(url).toBe(
|
|
"https://searchconsole.googleapis.com/v1/urlInspection/index:inspect",
|
|
);
|
|
expect(init?.method).toBe("POST");
|
|
expect(init?.headers).toMatchObject({ Authorization: "Bearer tok_123" });
|
|
const body = init?.body;
|
|
const payload =
|
|
typeof body === "string" ? (JSON.parse(body) as unknown) : null;
|
|
expect(payload).toEqual({
|
|
siteUrl: "sc-domain:example.com",
|
|
inspectionUrl: "https://example.com/post",
|
|
languageCode: "en-US",
|
|
});
|
|
expect(result?.indexStatusResult?.verdict).toBe("PASS");
|
|
});
|
|
|
|
it("maps 403 to a no-access GscApiError", async () => {
|
|
mocks.fetch.mockImplementation(async () =>
|
|
jsonResponse({ error: "forbidden" }, 403),
|
|
);
|
|
const { createGscClient, GscApiError } = await import("./gscClient");
|
|
await expect(
|
|
createGscClient({ userId: "u1" }).listSites(),
|
|
).rejects.toMatchObject({ status: 403 });
|
|
await expect(
|
|
createGscClient({ userId: "u1" }).listSites(),
|
|
).rejects.toBeInstanceOf(GscApiError);
|
|
});
|
|
|
|
it("maps 429 to a rate-limit GscApiError", async () => {
|
|
mocks.fetch.mockResolvedValue(jsonResponse({ error: "slow down" }, 429));
|
|
const { createGscClient } = await import("./gscClient");
|
|
await expect(
|
|
createGscClient({ userId: "u1" }).listSites(),
|
|
).rejects.toMatchObject({ status: 429 });
|
|
});
|
|
|
|
it("throws GscTokenError when no access token can be minted", async () => {
|
|
mocks.getAccessToken.mockRejectedValue(new Error("revoked"));
|
|
const { createGscClient, GscTokenError } = await import("./gscClient");
|
|
await expect(
|
|
createGscClient({ userId: "u1" }).listSites(),
|
|
).rejects.toBeInstanceOf(GscTokenError);
|
|
});
|
|
});
|