import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { appRouter } from "./routers"; import { COOKIE_NAME } from "../shared/const"; import type { TrpcContext } from "./_core/context"; import * as db from "./db"; import * as trainingAutomation from "./trainingAutomation"; import { ENV } from "./_core/env"; import { sdk } from "./_core/sdk"; type AuthenticatedUser = NonNullable; function createTestUser(overrides?: Partial): AuthenticatedUser { return { id: 1, openId: "test-user-001", email: "test@example.com", name: "TestPlayer", loginMethod: "username", role: "user", skillLevel: "beginner", trainingGoals: null, ntrpRating: 1.5, totalSessions: 0, totalMinutes: 0, totalShots: 0, currentStreak: 0, longestStreak: 0, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), ...overrides, }; } function createMockContext(user: AuthenticatedUser | null = null): { ctx: TrpcContext; clearedCookies: { name: string; options: Record }[]; setCookies: { name: string; value: string; options: Record }[]; } { const clearedCookies: { name: string; options: Record }[] = []; const setCookies: { name: string; value: string; options: Record }[] = []; return { ctx: { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: (name: string, options: Record) => { clearedCookies.push({ name, options }); }, cookie: (name: string, value: string, options: Record) => { setCookies.push({ name, value, options }); }, } as TrpcContext["res"], }, clearedCookies, setCookies, }; } // ===== AUTH TESTS ===== describe("auth.me", () => { it("returns null for unauthenticated users", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); const result = await caller.auth.me(); expect(result).toBeNull(); }); it("returns user data for authenticated users", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const result = await caller.auth.me(); expect(result).toBeDefined(); expect(result?.name).toBe("TestPlayer"); expect(result?.openId).toBe("test-user-001"); }); }); describe("auth.logout", () => { it("clears the session cookie and reports success", async () => { const user = createTestUser(); const { ctx, clearedCookies } = createMockContext(user); const caller = appRouter.createCaller(ctx); const result = await caller.auth.logout(); expect(result).toEqual({ success: true }); expect(clearedCookies).toHaveLength(1); expect(clearedCookies[0]?.name).toBe(COOKIE_NAME); expect(clearedCookies[0]?.options).toMatchObject({ maxAge: -1, secure: true, sameSite: "none", httpOnly: true, path: "/", }); }); }); describe("auth.loginWithUsername input validation", () => { it("rejects empty username", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.auth.loginWithUsername({ username: "" })).rejects.toThrow(); }); it("rejects username over 64 chars", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.auth.loginWithUsername({ username: "a".repeat(65) })).rejects.toThrow(); }); }); describe("auth.loginWithUsername invite flow", () => { const originalInviteCode = ENV.registrationInviteCode; beforeEach(() => { ENV.registrationInviteCode = "CA2026"; }); afterEach(() => { ENV.registrationInviteCode = originalInviteCode; vi.restoreAllMocks(); }); it("allows existing users to log in without an invite code", async () => { const existingUser = createTestUser({ name: "ExistingPlayer", openId: "existing-1" }); const { ctx, setCookies } = createMockContext(null); const caller = appRouter.createCaller(ctx); vi.spyOn(db, "getUserByUsername").mockResolvedValueOnce(existingUser); const createUsernameAccountSpy = vi.spyOn(db, "createUsernameAccount").mockResolvedValueOnce({ user: existingUser, isNew: false, }); vi.spyOn(sdk, "createSessionToken").mockResolvedValueOnce("session-token"); const result = await caller.auth.loginWithUsername({ username: "ExistingPlayer" }); expect(result.isNew).toBe(false); expect(createUsernameAccountSpy).toHaveBeenCalledWith("ExistingPlayer", undefined); expect(setCookies[0]?.name).toBe(COOKIE_NAME); }); it("rejects new users without the correct invite code", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); vi.spyOn(db, "getUserByUsername").mockResolvedValueOnce(undefined); const createUsernameAccountSpy = vi.spyOn(db, "createUsernameAccount"); await expect(caller.auth.loginWithUsername({ username: "NewPlayer" })).rejects.toThrow("新用户注册需要正确的邀请码"); expect(createUsernameAccountSpy).not.toHaveBeenCalled(); }); it("allows new users with the correct invite code", async () => { const newUser = createTestUser({ name: "NewPlayer", openId: "new-1" }); const { ctx, setCookies } = createMockContext(null); const caller = appRouter.createCaller(ctx); vi.spyOn(db, "getUserByUsername").mockResolvedValueOnce(undefined); const createUsernameAccountSpy = vi.spyOn(db, "createUsernameAccount").mockResolvedValueOnce({ user: newUser, isNew: true, }); vi.spyOn(sdk, "createSessionToken").mockResolvedValueOnce("session-token"); const result = await caller.auth.loginWithUsername({ username: "NewPlayer", inviteCode: "CA2026" }); expect(result.isNew).toBe(true); expect(createUsernameAccountSpy).toHaveBeenCalledWith("NewPlayer", "CA2026"); expect(setCookies[0]?.name).toBe(COOKIE_NAME); }); }); // ===== PROFILE TESTS ===== describe("profile.stats", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.profile.stats()).rejects.toThrow(); }); }); describe("profile.update input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.profile.update({ skillLevel: "beginner" })).rejects.toThrow(); }); it("rejects invalid skill level", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect(caller.profile.update({ skillLevel: "expert" as any })).rejects.toThrow(); }); it("accepts valid skill levels", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); for (const level of ["beginner", "intermediate", "advanced"] as const) { try { await caller.profile.update({ skillLevel: level }); } catch (e: any) { // DB errors expected, but input validation should pass expect(e.message).not.toContain("invalid_enum_value"); } } }); }); // ===== TRAINING PLAN TESTS ===== describe("plan.generate input validation", () => { it("rejects invalid skill level", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.plan.generate({ skillLevel: "expert" as any, durationDays: 7 }) ).rejects.toThrow(); }); it("rejects invalid duration (0)", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.plan.generate({ skillLevel: "beginner", durationDays: 0 }) ).rejects.toThrow(); }); it("rejects duration over 30", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.plan.generate({ skillLevel: "beginner", durationDays: 31 }) ).rejects.toThrow(); }); it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.plan.generate({ skillLevel: "beginner", durationDays: 7 }) ).rejects.toThrow(); }); }); describe("plan.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.plan.list()).rejects.toThrow(); }); }); describe("plan.active", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.plan.active()).rejects.toThrow(); }); it("returns null when the user has no active plan", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const getActivePlanSpy = vi.spyOn(db, "getActivePlan").mockResolvedValueOnce(null); await expect(caller.plan.active()).resolves.toBeNull(); getActivePlanSpy.mockRestore(); }); }); describe("plan.adjust input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.plan.adjust({ planId: 1 })).rejects.toThrow(); }); }); // ===== VIDEO TESTS ===== describe("video.upload input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.video.upload({ title: "test", format: "mp4", fileSize: 1000, fileBase64: "dGVzdA==", }) ).rejects.toThrow(); }); it("rejects missing title", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.video.upload({ title: undefined as any, format: "mp4", fileSize: 1000, fileBase64: "dGVzdA==", }) ).rejects.toThrow(); }); }); describe("video.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.video.list()).rejects.toThrow(); }); }); describe("video.registerExternal input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.video.registerExternal({ title: "session", url: "/media/assets/sessions/demo/recording.webm", fileKey: "media/sessions/demo/recording.webm", format: "webm", }) ).rejects.toThrow(); }); it("rejects missing url", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.video.registerExternal({ title: "session", url: "", fileKey: "media/sessions/demo/recording.webm", format: "webm", }) ).rejects.toThrow(); }); }); describe("video.get input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.video.get({ videoId: 1 })).rejects.toThrow(); }); }); // ===== ANALYSIS TESTS ===== describe("analysis.save input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.analysis.save({ videoId: 1, overallScore: 75 }) ).rejects.toThrow(); }); }); describe("analysis.getCorrections input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.analysis.getCorrections({ poseMetrics: {}, exerciseType: "forehand", detectedIssues: [], }) ).rejects.toThrow(); }); }); describe("analysis.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.analysis.list()).rejects.toThrow(); }); }); describe("analysis.getByVideo", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.analysis.getByVideo({ videoId: 1 })).rejects.toThrow(); }); }); // ===== RECORD TESTS ===== describe("record.create input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.record.create({ exerciseName: "正手挥拍", durationMinutes: 30 }) ).rejects.toThrow(); }); it("accepts valid exercise name", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.record.create({ exerciseName: "正手挥拍", durationMinutes: 30 }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); }); describe("record.complete input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.record.complete({ recordId: 1 })).rejects.toThrow(); }); }); describe("record.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.record.list()).rejects.toThrow(); }); }); // ===== RATING TESTS ===== describe("rating.history", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.rating.history()).rejects.toThrow(); }); }); describe("rating.current", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.rating.current()).rejects.toThrow(); }); }); // ===== DAILY CHECK-IN TESTS ===== describe("checkin.today", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.checkin.today()).rejects.toThrow(); }); }); describe("checkin.do", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.checkin.do()).rejects.toThrow(); }); it("accepts optional notes and minutesTrained", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.checkin.do({ notes: "练了正手", minutesTrained: 30 }); } catch (e: any) { // DB errors expected, input validation should pass expect(e.message).not.toContain("invalid_type"); } }); it("accepts empty input", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.checkin.do(); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); }); describe("checkin.history", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.checkin.history()).rejects.toThrow(); }); it("accepts custom limit", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.checkin.history({ limit: 30 }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); }); // ===== BADGE TESTS ===== describe("badge.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.badge.list()).rejects.toThrow(); }); }); describe("badge.check", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.badge.check()).rejects.toThrow(); }); }); describe("badge.definitions", () => { it("returns badge definitions without authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); const result = await caller.badge.definitions(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThan(0); // Check badge structure const firstBadge = result[0]; expect(firstBadge).toHaveProperty("key"); expect(firstBadge).toHaveProperty("name"); expect(firstBadge).toHaveProperty("description"); expect(firstBadge).toHaveProperty("icon"); expect(firstBadge).toHaveProperty("category"); }); it("contains expected badge categories", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); const result = await caller.badge.definitions(); const categories = [...new Set(result.map((b: any) => b.category))]; expect(categories).toContain("milestone"); expect(categories).toContain("training"); expect(categories).toContain("streak"); expect(categories).toContain("video"); expect(categories).toContain("analysis"); expect(categories).toContain("rating"); }); it("has unique badge keys", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); const result = await caller.badge.definitions(); const keys = result.map((b: any) => b.key); const uniqueKeys = [...new Set(keys)]; expect(keys.length).toBe(uniqueKeys.length); }); }); // ===== LEADERBOARD TESTS ===== describe("leaderboard.get", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.leaderboard.get()).rejects.toThrow(); }); it("accepts 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 }); } catch (e: any) { expect(e.message).not.toContain("invalid_enum_value"); } } }); it("rejects invalid sortBy", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.leaderboard.get({ sortBy: "invalidField" as any }) ).rejects.toThrow(); }); }); // ===== BADGE DEFINITIONS UNIT TESTS ===== describe("BADGE_DEFINITIONS via badge.definitions endpoint", () => { it("all badges have required fields", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); const badges = await caller.badge.definitions(); for (const badge of badges) { expect(typeof badge.key).toBe("string"); expect(badge.key.length).toBeGreaterThan(0); expect(typeof badge.name).toBe("string"); expect(typeof badge.description).toBe("string"); expect(typeof badge.icon).toBe("string"); expect(typeof badge.category).toBe("string"); } }); it("has at least 20 badges defined", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); const badges = await caller.badge.definitions(); expect(badges.length).toBeGreaterThanOrEqual(20); }); }); // ===== TUTORIAL TESTS ===== describe("tutorial.list", () => { it("works without authentication (public)", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); try { const result = await caller.tutorial.list({}); expect(Array.isArray(result)).toBe(true); } catch (e: any) { // DB error expected, but should not be auth error expect(e.code).not.toBe("UNAUTHORIZED"); } }); it("accepts category filter", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); try { await caller.tutorial.list({ category: "forehand" }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); it("accepts skillLevel filter", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); try { await caller.tutorial.list({ skillLevel: "beginner" }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); }); describe("tutorial.progress", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.tutorial.progress()).rejects.toThrow(); }); }); describe("tutorial.updateProgress input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.tutorial.updateProgress({ tutorialId: 1 }) ).rejects.toThrow(); }); it("requires tutorialId", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect( caller.tutorial.updateProgress({ tutorialId: undefined as any }) ).rejects.toThrow(); }); it("accepts optional watched, selfScore, notes", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.tutorial.updateProgress({ tutorialId: 1, watched: 1, selfScore: 4, notes: "Great tutorial", }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); }); // ===== REMINDER TESTS ===== describe("reminder.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.reminder.list()).rejects.toThrow(); }); }); describe("reminder.create input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.reminder.create({ reminderType: "training", title: "Test", timeOfDay: "08:00", daysOfWeek: [1, 2, 3], }) ).rejects.toThrow(); }); it("accepts empty title (no min validation on server)", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.reminder.create({ reminderType: "training", title: "", timeOfDay: "08:00", daysOfWeek: [1], }); } catch (e: any) { // DB error expected, but input validation should pass expect(e.message).not.toContain("invalid_type"); } }); it("accepts any string as reminderType (no enum validation on server)", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.reminder.create({ reminderType: "custom_type", title: "Test", timeOfDay: "08:00", daysOfWeek: [1], }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); } }); it("accepts valid reminder creation", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); try { await caller.reminder.create({ reminderType: "training", title: "Morning Training", message: "Time to practice!", timeOfDay: "08:00", daysOfWeek: [1, 2, 3, 4, 5], }); } catch (e: any) { expect(e.message).not.toContain("invalid_type"); expect(e.message).not.toContain("invalid_enum_value"); } }); }); describe("reminder.toggle input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.reminder.toggle({ reminderId: 1, isActive: 1 }) ).rejects.toThrow(); }); }); describe("reminder.delete input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.reminder.delete({ reminderId: 1 }) ).rejects.toThrow(); }); }); // ===== NOTIFICATION TESTS ===== describe("notification.list", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.notification.list()).rejects.toThrow(); }); }); describe("notification.unreadCount", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.notification.unreadCount()).rejects.toThrow(); }); }); describe("notification.markRead input validation", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect( caller.notification.markRead({ notificationId: 1 }) ).rejects.toThrow(); }); }); describe("notification.markAllRead", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.notification.markAllRead()).rejects.toThrow(); }); }); // ===== VISION LIBRARY TESTS ===== describe("vision.library", () => { it("requires authentication", async () => { const { ctx } = createMockContext(null); const caller = appRouter.createCaller(ctx); await expect(caller.vision.library()).rejects.toThrow(); }); it("returns seeded references for authenticated users", async () => { const user = createTestUser(); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const seedSpy = vi.spyOn(db, "seedVisionReferenceImages").mockResolvedValueOnce(); const listSpy = vi.spyOn(db, "listVisionReferenceImages").mockResolvedValueOnce([ { id: 1, slug: "ref-1", title: "标准图:正手挥拍", exerciseType: "forehand", imageUrl: "https://example.com/forehand.jpg", sourcePageUrl: "https://example.com/source", sourceLabel: "Example", author: null, license: null, expectedFocus: ["肩髋转动"], tags: ["forehand"], notes: null, sortOrder: 1, isPublished: 1, createdAt: new Date(), updatedAt: new Date(), }, ] as any); const result = await caller.vision.library(); expect(seedSpy).toHaveBeenCalledTimes(1); expect(listSpy).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); }); }); describe("vision.runs", () => { it("limits regular users to their own vision test runs", async () => { const user = createTestUser({ id: 7, role: "user" }); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const listSpy = vi.spyOn(db, "listVisionTestRuns").mockResolvedValueOnce([]); await caller.vision.runs({ limit: 20 }); expect(listSpy).toHaveBeenCalledWith(7, 20); }); it("allows admin users to view all vision test runs", async () => { const admin = createTestUser({ id: 9, role: "admin", name: "H1" }); const { ctx } = createMockContext(admin); const caller = appRouter.createCaller(ctx); const listSpy = vi.spyOn(db, "listVisionTestRuns").mockResolvedValueOnce([]); await caller.vision.runs({ limit: 30 }); expect(listSpy).toHaveBeenCalledWith(undefined, 30); }); }); describe("vision.seedLibrary", () => { it("rejects non-admin users", async () => { const user = createTestUser({ role: "user" }); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect(caller.vision.seedLibrary()).rejects.toThrow(); }); }); describe("achievement.list", () => { afterEach(() => { vi.restoreAllMocks(); }); it("returns achievement progress for authenticated users", async () => { const user = createTestUser({ id: 12 }); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const listSpy = vi.spyOn(db, "listUserAchievements").mockResolvedValueOnce([ { id: 1, key: "training_day_1", name: "开练", description: "完成首个训练日", category: "consistency", rarity: "common", icon: "🎾", metricKey: "training_days", targetValue: 1, tier: 1, isHidden: 0, isActive: 1, sortOrder: 1, createdAt: new Date(), updatedAt: new Date(), currentValue: 1, progressPct: 100, unlockedAt: new Date(), unlocked: true, }, ] as any); const result = await caller.achievement.list(); expect(listSpy).toHaveBeenCalledWith(12); expect(result).toHaveLength(1); expect((result[0] as any).key).toBe("training_day_1"); }); }); describe("analysis.liveSessionSave", () => { afterEach(() => { vi.restoreAllMocks(); }); it("persists a live session and syncs training data", async () => { const user = createTestUser({ id: 5 }); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const createSessionSpy = vi.spyOn(db, "createLiveAnalysisSession").mockResolvedValueOnce(101); const createSegmentsSpy = vi.spyOn(db, "createLiveActionSegments").mockResolvedValueOnce(); const syncSpy = vi.spyOn(trainingAutomation, "syncLiveTrainingData").mockResolvedValueOnce({ recordId: 88, unlocked: ["training_day_1"], }); const result = await caller.analysis.liveSessionSave({ title: "实时分析 正手", sessionMode: "practice", startedAt: Date.now() - 4_000, endedAt: Date.now(), durationMs: 4_000, dominantAction: "forehand", overallScore: 84, postureScore: 82, balanceScore: 78, techniqueScore: 86, footworkScore: 75, consistencyScore: 80, totalActionCount: 3, effectiveSegments: 2, totalSegments: 3, unknownSegments: 1, feedback: ["节奏稳定"], metrics: { sampleCount: 12 }, segments: [ { actionType: "forehand", isUnknown: false, startMs: 500, endMs: 2_500, durationMs: 2_000, confidenceAvg: 0.82, score: 84, peakScore: 90, frameCount: 24, issueSummary: ["击球点前移"], keyFrames: [500, 1500, 2500], clipLabel: "正手挥拍 00:00 - 00:02", }, ], }); expect(createSessionSpy).toHaveBeenCalledTimes(1); expect(createSegmentsSpy).toHaveBeenCalledTimes(1); expect(syncSpy).toHaveBeenCalledWith(expect.objectContaining({ userId: 5, sessionId: 101, dominantAction: "forehand", sessionMode: "practice", })); expect(result).toEqual({ sessionId: 101, trainingRecordId: 88 }); }); }); describe("rating.refreshMine", () => { afterEach(() => { vi.restoreAllMocks(); }); it("creates an async NTRP refresh task for the current user", async () => { const user = createTestUser({ id: 22 }); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); const createTaskSpy = vi.spyOn(db, "createBackgroundTask").mockResolvedValueOnce(); const result = await caller.rating.refreshMine(); expect(createTaskSpy).toHaveBeenCalledWith(expect.objectContaining({ userId: 22, type: "ntrp_refresh_user", payload: { targetUserId: 22 }, })); expect(result.taskId).toBeTruthy(); }); }); describe("admin.users", () => { afterEach(() => { vi.restoreAllMocks(); }); it("rejects non-admin users", async () => { const user = createTestUser({ role: "user" }); const { ctx } = createMockContext(user); const caller = appRouter.createCaller(ctx); await expect(caller.admin.users({ limit: 20 })).rejects.toThrow(); }); it("returns user list for admin users", async () => { const admin = createTestUser({ id: 1, role: "admin", name: "H1" }); const { ctx } = createMockContext(admin); const caller = appRouter.createCaller(ctx); const usersSpy = vi.spyOn(db, "listUsersForAdmin").mockResolvedValueOnce([ { id: 1, name: "H1", role: "admin", ntrpRating: 3.4, totalSessions: 10, totalMinutes: 320, totalShots: 240, currentStreak: 6, longestStreak: 12, createdAt: new Date(), lastSignedIn: new Date(), }, ] as any); const result = await caller.admin.users({ limit: 20 }); expect(usersSpy).toHaveBeenCalledWith(20); expect(result).toHaveLength(1); expect((result[0] as any).name).toBe("H1"); }); });