diff --git a/apps/web/lib/api/__tests__/plugin-public.test.ts b/apps/web/lib/api/__tests__/plugin-public.test.ts index 5bfa415..41b37dd 100644 --- a/apps/web/lib/api/__tests__/plugin-public.test.ts +++ b/apps/web/lib/api/__tests__/plugin-public.test.ts @@ -77,4 +77,40 @@ describe("buildPluginGameResponse — shape contract", () => { expect(r.topEntries.length).toBe(2) expect(r.topEntries[0].id).toBe("e1") // pinned first }) + + it("handles all-null fps values gracefully", async () => { + const entries = [ + { id: "e1", hardwareSlug: "steamdeck-oled", fpsAvg: 60, fpsLow: null, fpsOnePercentLow: null, fpsHigh: null, + upscalerType: "none", frameGenMethod: "none", protonVersion: null, osVersion: null, tdpWatts: null, + settingsJson: null, upvotes: 0, isPinned: false, createdAt: new Date("2026-01-01"), + userName: null, userImage: null }, + ] + const r = await buildPluginGameResponse({ game: { id: "g1", steamAppId: 123, title: "X", slug: "x" }, entries, recent: entries }) + expect(r.estFps).not.toBeNull() + expect(r.estFps!.avg).toBe(60) + expect(r.estFps!.low).toBeNull() + expect(r.estFps!.onePct).toBeNull() + expect(r.estFps!.high).toBeNull() + expect(r.estFps!.count).toBe(1) + }) + + it("trims recent entries separately from topEntries", async () => { + const top = [ + { id: "e1", hardwareSlug: "steamdeck-oled", fpsAvg: 60, fpsLow: 40, fpsOnePercentLow: 45, fpsHigh: 90, + upscalerType: "none", frameGenMethod: "none", protonVersion: "9", osVersion: "SteamOS 3", tdpWatts: 12, + settingsJson: null, upvotes: 5, isPinned: true, createdAt: new Date("2026-01-01"), + userName: "u", userImage: null }, + ] + const recent = [ + { id: "e2", hardwareSlug: "steamdeck-oled", fpsAvg: 80, fpsLow: 55, fpsOnePercentLow: 60, fpsHigh: 120, + upscalerType: "fsr", frameGenMethod: "none", protonVersion: "9", osVersion: "SteamOS 3", tdpWatts: 15, + settingsJson: null, upvotes: 2, isPinned: false, createdAt: new Date("2026-02-01"), + userName: "u2", userImage: null }, + ] + const r = await buildPluginGameResponse({ game: { id: "g1", steamAppId: 123, title: "X", slug: "x" }, entries: top, recent }) + expect(r.topEntries).toHaveLength(1) + expect(r.topEntries[0].id).toBe("e1") + expect(r.recentEntries).toHaveLength(1) + expect(r.recentEntries[0].id).toBe("e2") + }) }) \ No newline at end of file diff --git a/apps/web/lib/api/plugin-public.ts b/apps/web/lib/api/plugin-public.ts index c738df6..a13db86 100644 --- a/apps/web/lib/api/plugin-public.ts +++ b/apps/web/lib/api/plugin-public.ts @@ -207,6 +207,7 @@ export const pluginPublicRoutes = new Elysia({ .limit(query.limit ?? 3) set.headers["Cache-Control"] = "public, max-age=60" + set.headers["Vary"] = "search-params" return await buildPluginGameResponse({ game, entries: topRows as unknown as PluginEntryRow[], @@ -230,6 +231,10 @@ export const pluginPublicRoutes = new Elysia({ "/game/:steamAppId/devices", async ({ params, set }) => { const steamAppId = Number(params.steamAppId) + if (!Number.isInteger(steamAppId) || steamAppId <= 0) { + set.status = 400 + return { error: "Invalid steamAppId", devices: [] } + } const [game] = await db .select({ id: games.id }) .from(games) @@ -248,10 +253,17 @@ export const pluginPublicRoutes = new Elysia({ .from(performanceEntries) .innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug)) .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) - .where(and(eq(gameVersions.gameId, game.id), eq(performanceEntries.isRemoved, false))) + .where( + and( + eq(gameVersions.gameId, game.id), + eq(gameVersions.isLatest, true), + eq(performanceEntries.isRemoved, false), + ), + ) .groupBy(performanceEntries.hardwareSlug, hardware.name) .orderBy(desc(sql`count(*)`)) set.headers["Cache-Control"] = "public, max-age=60" + set.headers["Vary"] = "search-params" return { devices: rows } }, {