Add market watch and match hub workflows
这个提交包含在:
@@ -3,6 +3,7 @@ import { appRouter } from "./routers";
|
||||
import { COOKIE_NAME } from "../shared/const";
|
||||
import type { TrpcContext } from "./_core/context";
|
||||
import * as db from "./db";
|
||||
import * as matchStore from "./matchStore";
|
||||
import * as trainingAutomation from "./trainingAutomation";
|
||||
import { ENV } from "./_core/env";
|
||||
import { sdk } from "./_core/sdk";
|
||||
@@ -831,14 +832,28 @@ describe("leaderboard.get", () => {
|
||||
await expect(caller.leaderboard.get()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("accepts sortBy parameter", async () => {
|
||||
it("accepts training sortBy parameter", async () => {
|
||||
const user = createTestUser();
|
||||
const { ctx } = createMockContext(user);
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
|
||||
for (const sortBy of ["ntrpRating", "totalMinutes", "totalSessions", "totalShots"] as const) {
|
||||
try {
|
||||
await caller.leaderboard.get({ sortBy, limit: 10 });
|
||||
await caller.leaderboard.get({ scope: "training", sortBy, limit: 10 });
|
||||
} catch (e: any) {
|
||||
expect(e.message).not.toContain("invalid_enum_value");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts competitive sortBy parameter", async () => {
|
||||
const user = createTestUser();
|
||||
const { ctx } = createMockContext(user);
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
|
||||
for (const sortBy of ["wins", "winRate", "setsWon", "pointsWon", "matches"] as const) {
|
||||
try {
|
||||
await caller.leaderboard.get({ scope: "competitive", sortBy, limit: 10 });
|
||||
} catch (e: any) {
|
||||
expect(e.message).not.toContain("invalid_enum_value");
|
||||
}
|
||||
@@ -856,6 +871,94 @@ describe("leaderboard.get", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("match router", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("requires authentication for listing matches", async () => {
|
||||
const { ctx } = createMockContext(null);
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
await expect(caller.match.list()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("rejects creating a match that does not include the current user when not admin", async () => {
|
||||
const user = createTestUser({ id: 99, name: "Viewer" });
|
||||
const { ctx } = createMockContext(user);
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
|
||||
vi.spyOn(db, "getUserById")
|
||||
.mockResolvedValueOnce(createTestUser({ id: 1, name: "PlayerA" }))
|
||||
.mockResolvedValueOnce(createTestUser({ id: 2, name: "PlayerB" }));
|
||||
|
||||
await expect(caller.match.create({
|
||||
title: "League Match",
|
||||
matchMode: "competitive",
|
||||
playerAUserId: 1,
|
||||
playerBUserId: 2,
|
||||
durationMinutes: 90,
|
||||
})).rejects.toThrow("只能创建包含自己的比赛");
|
||||
});
|
||||
|
||||
it("rejects reading a match when the user is not an admin or participant", async () => {
|
||||
const user = createTestUser({ id: 77, name: "Outside" });
|
||||
const { ctx } = createMockContext(user);
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
|
||||
vi.spyOn(matchStore, "getMatchDetail").mockResolvedValueOnce({
|
||||
id: 12,
|
||||
createdByUserId: 1,
|
||||
matchMode: "daily",
|
||||
workflowStatus: "review_pending",
|
||||
title: "Morning Match",
|
||||
courtName: "Court 1",
|
||||
notes: null,
|
||||
durationMinutes: 90,
|
||||
scheduledAt: new Date(),
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
suggestionStatus: "ready",
|
||||
suggestionTaskId: "task-1",
|
||||
suggestedScore: null,
|
||||
suggestedMetrics: null,
|
||||
finalScore: null,
|
||||
finalMetrics: null,
|
||||
reviewNotes: null,
|
||||
reviewSubmittedAt: null,
|
||||
reviewedByUserId: null,
|
||||
reviewedAt: null,
|
||||
finalizedByUserId: null,
|
||||
finalizedAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
participants: [
|
||||
{ userId: 1, playerSlot: "player_a" },
|
||||
{ userId: 2, playerSlot: "player_b" },
|
||||
] as any,
|
||||
events: [],
|
||||
eventCount: 0,
|
||||
} as any);
|
||||
|
||||
await expect(caller.match.get({ matchId: 12 })).rejects.toThrow("当前账号不能访问这场比赛");
|
||||
});
|
||||
|
||||
it("requires admin permission for review submission", async () => {
|
||||
const user = createTestUser({ id: 7, role: "user" });
|
||||
const { ctx } = createMockContext(user);
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
|
||||
await expect(caller.match.reviewSubmit({
|
||||
matchId: 1,
|
||||
reviewNotes: "ok",
|
||||
finalScore: {
|
||||
sets: { player_a: 2, player_b: 0 },
|
||||
games: { player_a: 12, player_b: 6 },
|
||||
points: { player_a: 60, player_b: 42 },
|
||||
},
|
||||
})).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== BADGE DEFINITIONS UNIT TESTS =====
|
||||
|
||||
describe("BADGE_DEFINITIONS via badge.definitions endpoint", () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户