Merge branch 'fix/logs-user-search' into 'main'
fix: support fuzzy logs user search See merge request shangzy/new-api-analytics!2
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
|
|
||||||
const queryMock = mock(async () => [
|
type QueryRow = Record<string, unknown>;
|
||||||
|
type QueryParams = Array<string | number | boolean | null>;
|
||||||
|
|
||||||
|
const queryMock = mock(async (): Promise<QueryRow[]> => [
|
||||||
{
|
{
|
||||||
date: "2026-04-01 13:00:00",
|
date: "2026-04-01 13:00:00",
|
||||||
calls: 1,
|
calls: 1,
|
||||||
@@ -16,12 +19,12 @@ mock.module("./db", () => ({
|
|||||||
query: queryMock,
|
query: queryMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { getTrends, getUserDetail } = await import("./queries");
|
const { getLogs, getTrends, getUserDetail } = await import("./queries");
|
||||||
|
|
||||||
describe("getTrends", () => {
|
describe("getTrends", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
queryMock.mockClear();
|
queryMock.mockClear();
|
||||||
queryMock.mockImplementation(async () => [
|
queryMock.mockImplementation(async (): Promise<QueryRow[]> => [
|
||||||
{
|
{
|
||||||
date: "2026-04-01 13:00:00",
|
date: "2026-04-01 13:00:00",
|
||||||
calls: 1,
|
calls: 1,
|
||||||
@@ -62,7 +65,7 @@ describe("getUserDetail", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns token breakdown with nested model rows for user details", async () => {
|
test("returns token breakdown with nested model rows for user details", async () => {
|
||||||
queryMock.mockImplementation(async (sql: string) => {
|
queryMock.mockImplementation(async (sql: string): Promise<QueryRow[]> => {
|
||||||
if (sql.includes("token_models AS")) {
|
if (sql.includes("token_models AS")) {
|
||||||
return [
|
return [
|
||||||
{ token_name: "prod-key", model: "claude-sonnet-4", calls: 3, tokens: 100, cache_creation: 5, cache_read: 7, quota: 50 },
|
{ token_name: "prod-key", model: "claude-sonnet-4", calls: 3, tokens: 100, cache_creation: 5, cache_read: 7, quota: 50 },
|
||||||
@@ -123,3 +126,51 @@ describe("getUserDetail", () => {
|
|||||||
expect(queryMock.mock.calls.some(([sql]) => String(sql).includes("token_name"))).toBe(true);
|
expect(queryMock.mock.calls.some(([sql]) => String(sql).includes("token_name"))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getLogs", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
queryMock.mockClear();
|
||||||
|
queryMock.mockImplementation(async (sql: string): Promise<QueryRow[]> => {
|
||||||
|
if (sql.includes("SELECT COUNT(*)::int as total")) {
|
||||||
|
return [{ total: 0 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sql.includes("SELECT id, display_name FROM users")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sql.includes("SELECT id, name FROM channels")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters logs by fuzzy display name through the users table", async () => {
|
||||||
|
await getLogs({ startTs: 501, endTs: 601, username: "张三" });
|
||||||
|
|
||||||
|
const countCall = queryMock.mock.calls[0];
|
||||||
|
const countSql = String(countCall[0]);
|
||||||
|
const countParams = countCall[1] as QueryParams;
|
||||||
|
|
||||||
|
expect(countSql).toContain("username ILIKE $3");
|
||||||
|
expect(countSql).toContain("SELECT id FROM users");
|
||||||
|
expect(countSql).toContain("display_name ILIKE $3");
|
||||||
|
expect(countSql).toContain("users.username ILIKE $3");
|
||||||
|
expect(countParams).toEqual([501, 601, "%张三%"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the same fuzzy user filter for paginated log rows", async () => {
|
||||||
|
await getLogs({ page: 2, pageSize: 25, username: "adm" });
|
||||||
|
|
||||||
|
const dataCall = queryMock.mock.calls[3];
|
||||||
|
const dataSql = String(dataCall[0]);
|
||||||
|
const dataParams = dataCall[1] as QueryParams;
|
||||||
|
|
||||||
|
expect(dataSql).toContain("username ILIKE $1");
|
||||||
|
expect(dataSql).toContain("display_name ILIKE $1");
|
||||||
|
expect(dataSql).toContain("users.username ILIKE $1");
|
||||||
|
expect(dataParams).toEqual(["%adm%", 25, 25]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -692,8 +692,14 @@ export async function getLogs(options: {
|
|||||||
let where = timeWhere(params, options.startTs, options.endTs);
|
let where = timeWhere(params, options.startTs, options.endTs);
|
||||||
|
|
||||||
if (options.username) {
|
if (options.username) {
|
||||||
params.push(options.username);
|
params.push(`%${options.username}%`);
|
||||||
where += ` AND username = $${params.length}`;
|
where += ` AND (
|
||||||
|
username ILIKE $${params.length}
|
||||||
|
OR user_id IN (
|
||||||
|
SELECT id FROM users
|
||||||
|
WHERE display_name ILIKE $${params.length} OR users.username ILIKE $${params.length}
|
||||||
|
)
|
||||||
|
)`;
|
||||||
}
|
}
|
||||||
if (options.model) {
|
if (options.model) {
|
||||||
params.push(options.model);
|
params.push(options.model);
|
||||||
|
|||||||
Reference in New Issue
Block a user