fix(api): raise FPS caps to 1000 and validate FPS before game lookup
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { validateFps } from "@/lib/api/performance-import"
|
||||
|
||||
describe("validateFps", () => {
|
||||
it("rejects missing/null fpsAvg", () => {
|
||||
const r = validateFps({ fpsAvg: null as unknown as number })
|
||||
expect(r.ok).toBe(false)
|
||||
if (!r.ok) expect(r.error).toMatch(/fpsAvg/i)
|
||||
})
|
||||
|
||||
it("rejects NaN fpsAvg", () => {
|
||||
const r = validateFps({ fpsAvg: NaN })
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects fpsAvg below 1", () => {
|
||||
const r = validateFps({ fpsAvg: 0 })
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it("accepts fpsAvg up to 1000", () => {
|
||||
const r = validateFps({ fpsAvg: 1000, fpsHigh: 999 })
|
||||
expect(r.ok).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects fpsAvg above 1000", () => {
|
||||
const r = validateFps({ fpsAvg: 1001 })
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it("accepts fpsHigh of 750 (legit >500)", () => {
|
||||
const r = validateFps({ fpsAvg: 120, fpsHigh: 750 })
|
||||
expect(r.ok).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts optional nulls for fpsLow/onePct/high", () => {
|
||||
const r = validateFps({ fpsAvg: 60, fpsLow: null, fpsOnePercentLow: null, fpsHigh: null })
|
||||
expect(r.ok).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects negative fpsLow", () => {
|
||||
const r = validateFps({ fpsAvg: 60, fpsLow: -1 })
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects fpsHigh above 1000", () => {
|
||||
const r = validateFps({ fpsAvg: 60, fpsHigh: 1200 })
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -19,6 +19,48 @@ type UpscalerType = (typeof VALID_UPSCALER_TYPES)[number]
|
||||
type FrameGenMethod = (typeof VALID_FRAME_GEN_METHODS)[number]
|
||||
type AntiCheatStatus = (typeof VALID_ANTICHEAT_STATUSES)[number]
|
||||
|
||||
const FPS_MIN_AVG = 1
|
||||
const FPS_MIN_OTHER = 0
|
||||
const FPS_MAX = 1000
|
||||
|
||||
export type FpsInput = {
|
||||
fpsAvg: number
|
||||
fpsLow?: number | null
|
||||
fpsOnePercentLow?: number | null
|
||||
fpsHigh?: number | null
|
||||
}
|
||||
|
||||
export type FpsValues = {
|
||||
fpsAvg: number
|
||||
fpsLow: number | null
|
||||
fpsOnePercentLow: number | null
|
||||
fpsHigh: number | null
|
||||
}
|
||||
|
||||
export type FpsValidationResult =
|
||||
| { ok: true; values: FpsValues }
|
||||
| { ok: false; error: string }
|
||||
|
||||
export function validateFps(input: FpsInput): FpsValidationResult {
|
||||
const fpsAvg = Number(input.fpsAvg)
|
||||
if (input.fpsAvg == null || isNaN(fpsAvg) || fpsAvg < FPS_MIN_AVG || fpsAvg > FPS_MAX) {
|
||||
return { ok: false, error: `fpsAvg must be between ${FPS_MIN_AVG} and ${FPS_MAX}` }
|
||||
}
|
||||
const fpsLow = input.fpsLow != null ? Number(input.fpsLow) : null
|
||||
if (fpsLow !== null && (isNaN(fpsLow) || fpsLow < FPS_MIN_OTHER || fpsLow > FPS_MAX)) {
|
||||
return { ok: false, error: `fpsLow must be between ${FPS_MIN_OTHER} and ${FPS_MAX}` }
|
||||
}
|
||||
const fpsOnePercentLow = input.fpsOnePercentLow != null ? Number(input.fpsOnePercentLow) : null
|
||||
if (fpsOnePercentLow !== null && (isNaN(fpsOnePercentLow) || fpsOnePercentLow < FPS_MIN_OTHER || fpsOnePercentLow > FPS_MAX)) {
|
||||
return { ok: false, error: `fpsOnePercentLow must be between ${FPS_MIN_OTHER} and ${FPS_MAX}` }
|
||||
}
|
||||
const fpsHigh = input.fpsHigh != null ? Number(input.fpsHigh) : null
|
||||
if (fpsHigh !== null && (isNaN(fpsHigh) || fpsHigh < FPS_MIN_OTHER || fpsHigh > FPS_MAX)) {
|
||||
return { ok: false, error: `fpsHigh must be between ${FPS_MIN_OTHER} and ${FPS_MAX}` }
|
||||
}
|
||||
return { ok: true, values: { fpsAvg, fpsLow, fpsOnePercentLow, fpsHigh } }
|
||||
}
|
||||
|
||||
export const performanceImportRoutes = new Elysia({
|
||||
prefix: "/performance",
|
||||
detail: { tags: ["Performance"] },
|
||||
@@ -38,6 +80,19 @@ export const performanceImportRoutes = new Elysia({
|
||||
return { error: "Unsupported import format version" }
|
||||
}
|
||||
|
||||
// ── Validate FPS fields (before game lookup so a 400 is never masked by a 404) ──
|
||||
const fpsResult = validateFps({
|
||||
fpsAvg: body.fpsAvg,
|
||||
fpsLow: body.fpsLow,
|
||||
fpsOnePercentLow: body.fpsOnePercentLow,
|
||||
fpsHigh: body.fpsHigh,
|
||||
})
|
||||
if (!fpsResult.ok) {
|
||||
set.status = 400
|
||||
return { error: fpsResult.error }
|
||||
}
|
||||
const { fpsAvg, fpsLow, fpsOnePercentLow, fpsHigh } = fpsResult.values
|
||||
|
||||
// ── Resolve game version from steamAppId ──────────────────────
|
||||
const steamAppId = body.steamAppId
|
||||
if (!steamAppId) {
|
||||
@@ -111,35 +166,6 @@ export const performanceImportRoutes = new Elysia({
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validate FPS fields ───────────────────────────────────────
|
||||
const fpsAvg = Number(body.fpsAvg)
|
||||
if (isNaN(fpsAvg) || fpsAvg < 1 || fpsAvg > 500) {
|
||||
set.status = 400
|
||||
return { error: "fpsAvg must be between 1 and 500" }
|
||||
}
|
||||
|
||||
const fpsLow = body.fpsLow != null ? Number(body.fpsLow) : null
|
||||
if (fpsLow !== null && (isNaN(fpsLow) || fpsLow < 0 || fpsLow > 500)) {
|
||||
set.status = 400
|
||||
return { error: "fpsLow must be between 0 and 500" }
|
||||
}
|
||||
|
||||
const fpsOnePercentLow =
|
||||
body.fpsOnePercentLow != null ? Number(body.fpsOnePercentLow) : null
|
||||
if (
|
||||
fpsOnePercentLow !== null &&
|
||||
(isNaN(fpsOnePercentLow) || fpsOnePercentLow < 0 || fpsOnePercentLow > 500)
|
||||
) {
|
||||
set.status = 400
|
||||
return { error: "fpsOnePercentLow must be between 0 and 500" }
|
||||
}
|
||||
|
||||
const fpsHigh = body.fpsHigh != null ? Number(body.fpsHigh) : null
|
||||
if (fpsHigh !== null && (isNaN(fpsHigh) || fpsHigh < 0 || fpsHigh > 500)) {
|
||||
set.status = 400
|
||||
return { error: "fpsHigh must be between 0 and 500" }
|
||||
}
|
||||
|
||||
// ── Validate enums ────────────────────────────────────────────
|
||||
const rawUpscalerType = body.upscalerType ?? "none"
|
||||
const upscalerType: UpscalerType = VALID_UPSCALER_TYPES.includes(
|
||||
|
||||
@@ -146,23 +146,23 @@ export const performanceSubmitRoutes = new Elysia({ prefix: "/performance", deta
|
||||
}
|
||||
|
||||
// ── Validation: fpsAvg bounds ───────────────────────────────
|
||||
if (typeof fpsAvg !== "number" || fpsAvg < 1 || fpsAvg > 500) {
|
||||
if (typeof fpsAvg !== "number" || fpsAvg < 1 || fpsAvg > 1000) {
|
||||
set.status = 400
|
||||
return { error: "fpsAvg must be between 1 and 500" }
|
||||
return { error: "fpsAvg must be between 1 and 1000" }
|
||||
}
|
||||
|
||||
// ── Validation: optional FPS bounds ─────────────────────────
|
||||
if (fpsLow !== null && (fpsLow < 0 || fpsLow > 500)) {
|
||||
if (fpsLow !== null && (fpsLow < 0 || fpsLow > 1000)) {
|
||||
set.status = 400
|
||||
return { error: "fpsLow must be between 0 and 500" }
|
||||
return { error: "fpsLow must be between 0 and 1000" }
|
||||
}
|
||||
if (fpsHigh !== null && (fpsHigh < 0 || fpsHigh > 500)) {
|
||||
if (fpsHigh !== null && (fpsHigh < 0 || fpsHigh > 1000)) {
|
||||
set.status = 400
|
||||
return { error: "fpsHigh must be between 0 and 500" }
|
||||
return { error: "fpsHigh must be between 0 and 1000" }
|
||||
}
|
||||
if (fpsOnePercentLow !== null && (fpsOnePercentLow < 0 || fpsOnePercentLow > 500)) {
|
||||
if (fpsOnePercentLow !== null && (fpsOnePercentLow < 0 || fpsOnePercentLow > 1000)) {
|
||||
set.status = 400
|
||||
return { error: "fpsOnePercentLow must be between 0 and 500" }
|
||||
return { error: "fpsOnePercentLow must be between 0 and 1000" }
|
||||
}
|
||||
|
||||
// ── Validation: settingsJson size limits ────────────────────
|
||||
|
||||
Reference in New Issue
Block a user