import { describe, expect, it, vi, beforeEach } from "vitest"; import { appRouter } from "./routers"; import { COOKIE_NAME } from "../shared/const"; import type { TrpcContext } from "./_core/context"; 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(); }); }); // ===== 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(); }); }); 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(); }); });