feat: add logto auth and backend metrics integration

This commit is contained in:
2026-04-22 15:52:12 +08:00
parent b1bb28f5aa
commit c7ae2fbfcb
38 changed files with 4886 additions and 34 deletions
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, test } from "vitest";
import { readAuthConfig } from "../src/shared/auth-config";
describe("auth-config", () => {
test("returns the configured Logto settings", () => {
expect(readAuthConfig()).toEqual({
apiResource: "https://talent-search.intelligrow.cn",
appId: "i4jkllbvih0554r4n0fd3",
enableDevAuthPanel: true,
logtoEndpoint: "https://login-api.intelligrow.cn",
scopes: [
"openid",
"profile",
"offline_access",
"talent-search:read"
]
});
});
test("rejects empty endpoint values", () => {
expect(() =>
readAuthConfig({
logtoEndpoint: ""
})
).toThrow(/logtoEndpoint/i);
});
});
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, test } from "vitest";
import {
isAuthRequestMessage,
isAuthResponseMessage
} from "../src/shared/auth-messages";
describe("auth-messages", () => {
test("accepts a get-state request", () => {
expect(isAuthRequestMessage({ type: "auth:get-state" })).toBe(true);
});
test("rejects unknown auth requests", () => {
expect(isAuthRequestMessage({ type: "auth:wat" })).toBe(false);
});
test("accepts a successful auth response envelope", () => {
expect(
isAuthResponseMessage({
ok: true,
type: "auth:state",
value: { isAuthenticated: false }
})
).toBe(true);
});
});
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, test, vi } from "vitest";
import { DEFAULT_BACKEND_METRICS_BASE_URL } from "../src/shared/backend-metrics-config";
import {
buildBackendMetricsSearchRequestBody,
buildBackendMetricsSearchUrl,
createBackendMetricsClient,
mapBackendMetricsSearchResponse
} from "../src/shared/backend-metrics-client";
describe("backend-metrics-client", () => {
test("exports the default backend metrics base url", () => {
expect(DEFAULT_BACKEND_METRICS_BASE_URL).toBe("http://192.168.31.29:8083");
});
test("builds the backend search url", () => {
expect(buildBackendMetricsSearchUrl("http://192.168.31.29:8083")).toBe(
"http://192.168.31.29:8083/api/v1/history/talents/search"
);
});
test("builds a star_id batch request body", () => {
expect(
buildBackendMetricsSearchRequestBody(["7252982749131178039", "7290491710910496809"])
).toEqual({
page: 1,
size: 20,
type: "star_id",
values: ["7252982749131178039", "7290491710910496809"]
});
});
test("maps backend metrics rows into display-ready values", () => {
expect(
mapBackendMetricsSearchResponse({
data: {
data: [
{
avg_a3_increase_cnt: 78366.22448979592,
avg_after_view_search_cnt: 9689.959183673469,
avg_after_view_search_rate: 0.0036203703369054683,
avg_new_a3_rate: 0.034428135017531614,
cp_search: 14.460581961550774,
cpa3: 1.788046443373538,
star_id: "7252982749131178039"
}
]
},
success: true
})
).toEqual([
{
a3IncreaseCount: "78,366.22",
afterViewSearchCount: "9,689.96",
afterViewSearchRate: "0.36%",
cpSearch: "14.46",
cpa3: "1.79",
newA3Rate: "3.44%",
starId: "7252982749131178039"
}
]);
});
test("posts star ids with bearer auth when searching backend metrics", async () => {
const fetchImpl = async (_input: string, init?: RequestInit) => ({
json: async () => ({
data: {
data: []
},
success: true
}),
ok: true,
status: 200,
url: "http://192.168.31.29:8083/api/v1/history/talents/search"
});
const fetchSpy = vi.fn(fetchImpl);
const client = createBackendMetricsClient({
fetchImpl: fetchSpy,
getAccessToken: async () => "test-token"
});
await client.searchByStarIds(["111", "222"]);
expect(fetchSpy).toHaveBeenCalledWith(
"http://192.168.31.29:8083/api/v1/history/talents/search",
expect.objectContaining({
body: JSON.stringify({
page: 1,
size: 20,
type: "star_id",
values: ["111", "222"]
}),
headers: {
Authorization: "Bearer test-token",
"Content-Type": "application/json"
},
method: "POST"
})
);
});
});
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, test } from "vitest";
import {
isBackendMetricsResponseMessage,
isBackendMetricsSearchRequestMessage
} from "../src/shared/backend-metrics-messages";
describe("backend-metrics-messages", () => {
test("accepts a backend metrics search request", () => {
expect(
isBackendMetricsSearchRequestMessage({
type: "backend-metrics:search",
value: {
starIds: ["111", "222"]
}
})
).toBe(true);
});
test("accepts a successful backend metrics response", () => {
expect(
isBackendMetricsResponseMessage({
ok: true,
type: "backend-metrics:result",
value: {
rows: [
{
a3IncreaseCount: "10.00",
afterViewSearchCount: "20.00",
afterViewSearchRate: "0.20%",
cpSearch: "1.10",
cpa3: "2.20",
newA3Rate: "1.50%",
starId: "111"
}
]
}
})
).toBe(true);
});
});
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, test, vi } from "vitest";
import { createLogtoAuthClient } from "../src/background/auth/client";
vi.mock("@logto/chrome-extension", () => {
const signIn = vi.fn(async () => undefined);
const signOut = vi.fn(async () => undefined);
const MockLogtoClient = vi.fn(function MockLogtoClient() {
return {
getAccessToken: vi.fn(async () => "token"),
getIdTokenClaims: vi.fn(async () => null),
isAuthenticated: vi.fn(async () => false),
signIn,
signOut
};
});
return {
default: MockLogtoClient
};
});
describe("background-auth-client", () => {
test("uses chrome identity redirect URLs for sign in and sign out", async () => {
const getRedirectURL = vi.fn((path?: string) =>
path ? `https://extension.chromiumapp.org${path}` : "https://extension.chromiumapp.org/"
);
(
globalThis as typeof globalThis & {
chrome?: {
identity?: {
getRedirectURL?: (path?: string) => string;
};
};
}
).chrome = {
identity: {
getRedirectURL
}
};
const authClient = createLogtoAuthClient();
await authClient.signIn();
await authClient.signOut();
expect(getRedirectURL).toHaveBeenNthCalledWith(1, "/callback");
expect(getRedirectURL).toHaveBeenNthCalledWith(2);
});
});
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, test, vi } from "vitest";
import { createAuthController } from "../src/background/auth/controller";
describe("background-auth-controller", () => {
test("returns unauthenticated state when the client is logged out", async () => {
const controller = createAuthController({
authClient: {
getAccessToken: vi.fn(),
getIdTokenClaims: vi.fn(),
isAuthenticated: vi.fn(async () => false),
signIn: vi.fn(),
signOut: vi.fn()
}
});
await expect(controller.getAuthState()).resolves.toEqual(
expect.objectContaining({
isAuthenticated: false
})
);
});
test("delegates sign in to the auth client", async () => {
const signIn = vi.fn(async () => undefined);
const controller = createAuthController({
authClient: {
getAccessToken: vi.fn(),
getIdTokenClaims: vi.fn(),
isAuthenticated: vi.fn(async () => false),
signIn,
signOut: vi.fn()
}
});
await controller.signIn();
expect(signIn).toHaveBeenCalledTimes(1);
});
});
+140
View File
@@ -127,4 +127,144 @@ describe("background-index", () => {
value: { accessToken: "test-access-token" }
});
});
test("submits batches through the background message handler", async () => {
const listeners: Array<
(message: unknown, sender: unknown, sendResponse: (response: unknown) => void) => boolean | void
> = [];
const sendResponse = vi.fn();
const submitBatch = vi.fn(async () => ({
acceptedCount: 1,
batchId: "批次A-2026-04-22T12:30:00.000Z",
ok: true
}));
registerBackgroundMessageHandler(
{
runtime: {
onMessage: {
addListener(listener) {
listeners.push(listener);
}
}
}
},
{
authController: {
getAccessToken: vi.fn(async () => "test-access-token"),
getAuthState: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn()
},
submitBatch
}
);
const result = listeners[0](
{
payload: {
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"
},
type: "batch:submit"
},
{},
sendResponse
);
expect(result).toBe(true);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(submitBatch).toHaveBeenCalledWith(
expect.objectContaining({
batchId: "批次A-2026-04-22T12:30:00.000Z"
})
);
expect(sendResponse).toHaveBeenCalledWith({
ok: true,
type: "batch:ack",
value: {
acceptedCount: 1,
batchId: "批次A-2026-04-22T12:30:00.000Z",
ok: true
}
});
});
test("searches backend metrics through the background message handler", async () => {
const listeners: Array<
(message: unknown, sender: unknown, sendResponse: (response: unknown) => void) => boolean | void
> = [];
const sendResponse = vi.fn();
const searchBackendMetrics = vi.fn(async () => [
{
a3IncreaseCount: "78,366.22",
afterViewSearchCount: "9,689.96",
afterViewSearchRate: "0.36%",
cpSearch: "14.46",
cpa3: "1.79",
newA3Rate: "3.44%",
starId: "111"
}
]);
registerBackgroundMessageHandler(
{
runtime: {
onMessage: {
addListener(listener) {
listeners.push(listener);
}
}
}
},
{
authController: {
getAccessToken: vi.fn(async () => "test-access-token"),
getAuthState: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn()
},
searchBackendMetrics
}
);
const result = listeners[0](
{
type: "backend-metrics:search",
value: {
starIds: ["111", "222"]
}
},
{},
sendResponse
);
expect(result).toBe(true);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(searchBackendMetrics).toHaveBeenCalledWith(["111", "222"]);
expect(sendResponse).toHaveBeenCalledWith({
ok: true,
type: "backend-metrics:result",
value: {
rows: [
{
a3IncreaseCount: "78,366.22",
afterViewSearchCount: "9,689.96",
afterViewSearchRate: "0.36%",
cpSearch: "14.46",
cpa3: "1.79",
newA3Rate: "3.44%",
starId: "111"
}
]
}
});
});
});
+11 -2
View File
@@ -11,10 +11,19 @@ describe("manifest", () => {
);
});
test("declares the downloads permission and background worker for csv export", () => {
test("declares the downloads and auth permissions plus background worker", () => {
expect(manifest.permissions).toEqual(
expect.arrayContaining(["downloads"])
expect.arrayContaining(["downloads", "identity", "storage"])
);
expect(manifest.host_permissions).toEqual(
expect.arrayContaining([
"http://*/*",
"https://login-api.intelligrow.cn/*",
"http://127.0.0.1:4319/*",
"https://*/*"
])
);
expect(manifest.background?.service_worker).toBe("background/index.js");
expect(manifest.action?.default_popup).toBe("popup/index.html");
});
});
+27
View File
@@ -0,0 +1,27 @@
// @vitest-environment jsdom
// @vitest-environment-options {"url":"https://xingtu.cn/ad/creator/market"}
import { describe, expect, test, vi } from "vitest";
import { bootContentScript } from "../src/content/index";
describe("market-auth-gating", () => {
test("shows a login gate instead of booting the market controller when unauthenticated", async () => {
document.body.innerHTML = "<div></div>";
const createMarketController = vi.fn();
await bootContentScript({
createMarketController,
document,
sendAuthMessage: vi.fn(async () => ({
ok: true,
type: "auth:state",
value: { isAuthenticated: false }
})),
window
});
expect(createMarketController).not.toHaveBeenCalled();
expect(document.body.textContent).toContain("请先登录插件");
});
});
+48
View File
@@ -241,6 +241,48 @@ describe("market-content-entry", () => {
).toBe("0.03% - 0.2%");
});
test("batch loads backend metrics for the visible page and renders the metrics panel", async () => {
document.body.innerHTML = buildMarketFixture();
const searchBackendMetrics = vi.fn(async (starIds: string[]) =>
starIds
.filter((starId) => starId === "a")
.map((starId) => ({
a3IncreaseCount: "78,366.22",
afterViewSearchCount: "9,689.96",
afterViewSearchRate: "0.36%",
cpSearch: "14.46",
cpa3: "1.79",
newA3Rate: "3.44%",
starId
}))
);
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
searchBackendMetrics,
window
}));
await controller.ready;
expect(searchBackendMetrics).toHaveBeenCalledTimes(1);
expect(searchBackendMetrics).toHaveBeenCalledWith(["a", "b"]);
expect(
document.querySelector('[data-market-row-cell="backendMetrics"]')?.textContent
).toContain("看后搜率");
expect(
document.querySelector('[data-market-row-cell="backendMetrics"]')?.textContent
).toContain("0.36%");
expect(
document.querySelectorAll('[data-market-row-cell="backendMetrics"]')[1]?.textContent
).toBe("暂无数据");
});
test("boots the controller only after auth succeeds", async () => {
const createMarketController = vi.fn(() => ({
ready: Promise.resolve()
@@ -302,12 +344,14 @@ describe("market-content-entry", () => {
"¥450,000",
"0.02% - 0.1%",
"0.03% - 0.2%",
"",
"下单"
]);
expect(readDivRightRowTexts(1)).toEqual([
"¥20,000",
"0.5% - 1%",
"0.01% - 0.1%",
"",
"下单"
]);
});
@@ -368,12 +412,14 @@ describe("market-content-entry", () => {
"¥450,000",
"0.02%",
"0.03% - 0.2%",
"",
"下单"
]);
expect(readDivRightRowTexts(1)).toEqual([
"¥20,000",
"0.5% - 1%",
"0.01% - 0.1%",
"",
"下单"
]);
});
@@ -429,12 +475,14 @@ describe("market-content-entry", () => {
"¥450,000",
"0.02%",
"0.03% - 0.2%",
"",
"下单"
]);
expect(readDivRightRowTexts(1)).toEqual([
"¥20,000",
"0.5% - 1%",
"0.01% - 0.1%",
"",
"下单"
]);
});
+58 -3
View File
@@ -47,10 +47,13 @@ describe("market-dom-sync", () => {
'[data-market-header-cell="personalVideoAfterSearchRate"]'
)
).not.toBeNull();
expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(4);
expect(
document.querySelector('[data-market-header-cell="backendMetrics"]')
).not.toBeNull();
expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(6);
});
test("renders loading, success, and failed states", () => {
test("renders loading, success, missing, and failed states", () => {
const table = syncMarketTable(document);
if (!table) {
throw new Error("Expected market table");
@@ -67,6 +70,15 @@ describe("market-dom-sync", () => {
renderMarketRowState(betaRow, {
authorId: "b",
authorName: "Beta",
backendMetrics: {
a3IncreaseCount: "78,366.22",
afterViewSearchCount: "9,689.96",
afterViewSearchRate: "0.36%",
cpSearch: "14.46",
cpa3: "1.79",
newA3Rate: "3.44%"
},
backendMetricsStatus: "success",
status: "success",
rates: {
singleVideoAfterSearchRate: "0.5%-1%",
@@ -75,16 +87,34 @@ describe("market-dom-sync", () => {
});
expect(alphaRow.singleCell.textContent).toBe("加载中...");
expect(alphaRow.backendMetricsCell.textContent).toBe("加载中...");
expect(betaRow.singleCell.textContent).toBe("0.5% - 1%");
expect(betaRow.personalCell.textContent).toBe("0.02% - 0.1%");
expect(betaRow.backendMetricsCell.textContent).toContain("看后搜率");
expect(betaRow.backendMetricsCell.textContent).toContain("0.36%");
expect(betaRow.backendMetricsCell.textContent).toContain("CPA3");
renderMarketRowState(betaRow, {
authorId: "b",
authorName: "Beta",
backendMetricsStatus: "missing",
status: "success",
rates: {
singleVideoAfterSearchRate: "0.5%-1%",
personalVideoAfterSearchRate: "0.02 - 0.1%"
}
});
expect(betaRow.backendMetricsCell.textContent).toBe("暂无数据");
renderMarketRowState(betaRow, {
authorId: "b",
authorName: "Beta",
backendMetricsStatus: "failed",
status: "failed"
});
expect(betaRow.singleCell.textContent).toBe("加载失败");
expect(betaRow.personalCell.textContent).toBe("加载失败");
expect(betaRow.backendMetricsCell.textContent).toBe("加载失败");
});
test("hides rows outside the visible author ids", () => {
@@ -126,13 +156,37 @@ describe("market-dom-sync", () => {
"21-60s报价",
"单视频看后搜率",
"个人视频看后搜率",
"秒探指标",
"操作"
]);
expect(
Number.parseFloat(
(
document.querySelector('[data-testid="right-header"]') as HTMLElement
).style.width
)
).toBeGreaterThan(350);
expect(
Number.parseFloat(
(
document.querySelector('[data-testid="right-section"]') as HTMLElement
).style.width
)
).toBeGreaterThan(350);
expect(table.rows.map((row) => row.authorId)).toEqual(["111", "222"]);
renderMarketRowState(table.rows[0], {
authorId: "111",
authorName: "达人 A",
backendMetrics: {
a3IncreaseCount: "78,366.22",
afterViewSearchCount: "9,689.96",
afterViewSearchRate: "0.36%",
cpSearch: "14.46",
cpa3: "1.79",
newA3Rate: "3.44%"
},
backendMetricsStatus: "success",
status: "success",
rates: {
singleVideoAfterSearchRate: "0.5%-1%",
@@ -144,6 +198,7 @@ describe("market-dom-sync", () => {
"¥450,000",
"0.5% - 1%",
"0.02% - 0.1%",
"看后搜率0.36%看后搜数9,689.96新增A3数78,366.22新增A3率3.44%CPA31.79cp_search14.46",
"下单"
]);
@@ -156,7 +211,7 @@ describe("market-dom-sync", () => {
applyRowOrder(table, ["222", "111"]);
expect(readAuthorNames()).toEqual(["达人 B", "达人 A"]);
expect(readRightRowTexts(0)).toEqual(["¥20,000", "", "", "下单"]);
expect(readRightRowTexts(0)).toEqual(["¥20,000", "", "", "", "下单"]);
expect(table.rows[0].exportFields).toMatchObject({
"21-60s报价": "¥450,000",
"代表视频": "代表视频A",