desloppify: code health overhaul plus abuse hardening (140 green, strict 87.2)
Honest baseline after removing 45 gaming suppressions. Structural: lib/game plus root hooks extraction, CORS origin echo, DEV_ORIGINS layering. Fixes: error-cause binding, discord 5xx split, null-cache guard, co-located tests, rejecting-fetch coverage, naming and typing cleanup, socket controller, cache bound, cascade helpers, latch resets. Hardening: per-IP rate limit on API routes, deterministic-miss UX, retry cooldown plus cap. Suite 140 green, build plus lint clean.
This commit is contained in:
78 files changed
+4908
-1430
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { useGameProfile } from "@/hooks/use-game-profile";
|
||||
import { profileFixture } from "@/test/fixtures";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("useGameProfile", () => {
|
||||
it("loads a profile and derives view state", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({ ok: true, json: async () => profileFixture() }) as Response),
|
||||
);
|
||||
const { result } = renderHook(() => useGameProfile("1", "en", 0));
|
||||
await waitFor(() => expect(result.current.profile?.name).toBe("Test Game"));
|
||||
expect(result.current.failed).toBe(false);
|
||||
expect(result.current.localeTag).toBe("en-US");
|
||||
expect(result.current.hasDetails).toBe(true);
|
||||
});
|
||||
|
||||
it("flags failure when the API errors", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({ ok: false, status: 500 }) as Response),
|
||||
);
|
||||
const { result } = renderHook(() => useGameProfile("1", "en", 0));
|
||||
await waitFor(() => expect(result.current.failed).toBe(true));
|
||||
expect(result.current.profile).toBeNull();
|
||||
});
|
||||
|
||||
it("retries on demand", async () => {
|
||||
let calls = 0;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => {
|
||||
calls += 1;
|
||||
return { ok: false, status: 500 } as Response;
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() => useGameProfile("1", "en", 0));
|
||||
await waitFor(() => expect(result.current.failed).toBe(true));
|
||||
expect(result.current.canRetry).toBe(true);
|
||||
result.current.retry();
|
||||
await waitFor(() => expect(calls).toBeGreaterThan(1));
|
||||
});
|
||||
|
||||
it("blocks double retry during cooldown and restores on reset", async () => {
|
||||
let calls = 0;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => {
|
||||
calls += 1;
|
||||
return { ok: false, status: 500 } as Response;
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() => useGameProfile("1", "en", 0));
|
||||
await waitFor(() => expect(result.current.failed).toBe(true));
|
||||
act(() => {
|
||||
result.current.retry();
|
||||
});
|
||||
expect(result.current.canRetry).toBe(false);
|
||||
const afterFirstRetry = calls;
|
||||
act(() => {
|
||||
result.current.retry();
|
||||
});
|
||||
expect(calls).toBe(afterFirstRetry);
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
expect(result.current.canRetry).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { GameProfile, ItadReview } from "@/lib/game/game-profile-types";
|
||||
import type { Locale } from "@/lib/i18n/locales";
|
||||
import { describeProfile, resolveGameLocale } from "@/lib/game/game-format";
|
||||
import { fetchJson } from "@/lib/client-fetch";
|
||||
|
||||
export const MAX_RETRIES = 3;
|
||||
export const RETRY_COOLDOWN_MS = 5_000;
|
||||
|
||||
export interface GameProfileState {
|
||||
profile: GameProfile | null;
|
||||
failed: boolean;
|
||||
localeTag: string;
|
||||
hero: string | null;
|
||||
dbRating: number | null;
|
||||
itadReviews: ItadReview[];
|
||||
hasDetails: boolean;
|
||||
hasReviews: boolean;
|
||||
partialFail: boolean;
|
||||
metascore: ItadReview | null;
|
||||
opencritic: ItadReview | null;
|
||||
isDeterministicMiss: boolean;
|
||||
canRetry: boolean;
|
||||
setBannerPick: (shot: string | null) => void;
|
||||
retry: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useGameProfile(
|
||||
applicationId: string,
|
||||
lang: Locale,
|
||||
heroSeed: number,
|
||||
): GameProfileState {
|
||||
const [profile, setProfile] = useState<GameProfile | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const [bannerPick, setBannerPick] = useState<string | null>(null);
|
||||
const [retries, setRetries] = useState(0);
|
||||
const [cooling, setCooling] = useState(false);
|
||||
const coolingTimer = useRef<number | undefined>(undefined);
|
||||
const { localeTag, country } = resolveGameLocale(lang);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function load(): Promise<void> {
|
||||
const json = await fetchJson<GameProfile>(
|
||||
`/api/game/${applicationId}?lang=${lang}&country=${country}`,
|
||||
);
|
||||
if (!cancelled) {
|
||||
if (json) {
|
||||
setProfile(json);
|
||||
setFailed(false);
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
void load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [applicationId, lang, country, attempt]);
|
||||
|
||||
const partialFail =
|
||||
!!profile && !profile.sources.igdb && !profile.sources.steam && !profile.sources.itad;
|
||||
const isDeterministicMiss =
|
||||
!!profile && !profile.steamId && !profile.sources.igdb && !profile.sources.itad;
|
||||
|
||||
const hero = useMemo(() => {
|
||||
if (bannerPick) {
|
||||
return bannerPick;
|
||||
}
|
||||
const shots = profile?.screenshots ?? [];
|
||||
if (shots.length > 0) {
|
||||
return shots[heroSeed % shots.length] ?? null;
|
||||
}
|
||||
return profile?.artworkUrl ?? null;
|
||||
}, [profile, heroSeed, bannerPick]);
|
||||
|
||||
const summary = describeProfile(profile);
|
||||
|
||||
const canRetry = retries < MAX_RETRIES && !cooling;
|
||||
|
||||
function retry(): void {
|
||||
if (!canRetry) {
|
||||
return;
|
||||
}
|
||||
setCooling(true);
|
||||
setRetries((n) => n + 1);
|
||||
setFailed(false);
|
||||
setAttempt((n) => n + 1);
|
||||
window.clearTimeout(coolingTimer.current);
|
||||
coolingTimer.current = window.setTimeout(() => setCooling(false), RETRY_COOLDOWN_MS);
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
window.clearTimeout(coolingTimer.current);
|
||||
setCooling(false);
|
||||
setRetries(0);
|
||||
setProfile(null);
|
||||
setFailed(false);
|
||||
setAttempt((n) => n + 1);
|
||||
}
|
||||
|
||||
useEffect(() => () => window.clearTimeout(coolingTimer.current), []);
|
||||
|
||||
return {
|
||||
profile,
|
||||
failed,
|
||||
localeTag,
|
||||
hero,
|
||||
...summary,
|
||||
partialFail,
|
||||
isDeterministicMiss,
|
||||
canRetry,
|
||||
setBannerPick,
|
||||
retry,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { useLanyard } from "@/hooks/use-lanyard";
|
||||
import { lanyardFixture } from "@/test/fixtures";
|
||||
|
||||
describe("useLanyard", () => {
|
||||
it("returns the initial snapshot synchronously", () => {
|
||||
const initial = lanyardFixture();
|
||||
const { result, unmount } = renderHook(() => useLanyard(initial));
|
||||
expect(result.current?.discord_user.username).toBe("testuser");
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("starts empty without initial data", () => {
|
||||
const { result, unmount } = renderHook(() => useLanyard(null));
|
||||
expect(result.current).toBeNull();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { LanyardData } from "@/lib/game/lanyard";
|
||||
import { DISCORD_ID, LANYARD_API, LANYARD_WS } from "@/lib/game/lanyard";
|
||||
import { fetchJson } from "@/lib/client-fetch";
|
||||
|
||||
const WS_OPEN_TIMEOUT_MS = 10_000;
|
||||
const RECONNECT_BASE_MS = 2_000;
|
||||
const RECONNECT_MAX_MS = 30_000;
|
||||
const SAFETY_POLL_MS = 60_000;
|
||||
const HEARTBEAT_FALLBACK_MS = 30_000;
|
||||
|
||||
interface LanyardMessage {
|
||||
op: number;
|
||||
t?: string;
|
||||
d?: unknown;
|
||||
}
|
||||
|
||||
function parseLanyardMessage(data: unknown): LanyardMessage | null {
|
||||
if (typeof data !== "string") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(data) as LanyardMessage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isLanyardData(value: unknown): value is LanyardData {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as { activities?: unknown; discord_status?: unknown };
|
||||
if (!Array.isArray(candidate.activities)) {
|
||||
return false;
|
||||
}
|
||||
const status = candidate.discord_status;
|
||||
return status === "online" || status === "idle" || status === "dnd" || status === "offline";
|
||||
}
|
||||
function nextBackoffDelay(current: number): number {
|
||||
return Math.min(current * 2, RECONNECT_MAX_MS);
|
||||
}
|
||||
|
||||
async function fetchLanyardSnapshot(): Promise<LanyardData | null> {
|
||||
const json = await fetchJson<{ success: boolean; data: LanyardData }>(LANYARD_API, {
|
||||
cache: "no-store",
|
||||
});
|
||||
return json && json.success ? json.data : null;
|
||||
}
|
||||
|
||||
interface LanyardSocketEvents {
|
||||
onData: (data: LanyardData) => void;
|
||||
}
|
||||
|
||||
interface LanyardSocket {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
function createLanyardSocket(events: LanyardSocketEvents): LanyardSocket {
|
||||
let disposed = false;
|
||||
let socket: WebSocket | null = null;
|
||||
let heartbeatTimer: number | undefined;
|
||||
let reconnectTimer: number | undefined;
|
||||
let safetyTimer: number | undefined;
|
||||
let reconnectDelay = RECONNECT_BASE_MS;
|
||||
|
||||
async function pollOnce(): Promise<void> {
|
||||
const snapshot = await fetchLanyardSnapshot();
|
||||
if (snapshot && !disposed) {
|
||||
events.onData(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect(): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(reconnectTimer);
|
||||
reconnectTimer = window.setTimeout(() => {
|
||||
reconnectDelay = nextBackoffDelay(reconnectDelay);
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
function dropSocket(current: WebSocket): void {
|
||||
current.onclose = null;
|
||||
current.onerror = null;
|
||||
current.onmessage = null;
|
||||
if (socket === current) {
|
||||
socket = null;
|
||||
}
|
||||
try {
|
||||
current.close();
|
||||
} catch {
|
||||
if (socket === current) {
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function connect(): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
let current: WebSocket;
|
||||
try {
|
||||
current = new WebSocket(LANYARD_WS);
|
||||
} catch {
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
socket = current;
|
||||
let settled = false;
|
||||
|
||||
const openTimeout = window.setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
dropSocket(current);
|
||||
if (!disposed) {
|
||||
void pollOnce();
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
}, WS_OPEN_TIMEOUT_MS);
|
||||
|
||||
const handleDrop = (): void => {
|
||||
if (settled && socket !== current) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
window.clearTimeout(openTimeout);
|
||||
window.clearInterval(heartbeatTimer);
|
||||
dropSocket(current);
|
||||
if (!disposed) {
|
||||
void pollOnce();
|
||||
scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
current.onmessage = (event: MessageEvent) => {
|
||||
const message = parseLanyardMessage(event.data);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (message.op === 1) {
|
||||
settled = true;
|
||||
window.clearTimeout(openTimeout);
|
||||
reconnectDelay = RECONNECT_BASE_MS;
|
||||
const interval =
|
||||
(message.d as { heartbeat_interval?: number } | undefined)
|
||||
?.heartbeat_interval ?? HEARTBEAT_FALLBACK_MS;
|
||||
window.clearInterval(heartbeatTimer);
|
||||
heartbeatTimer = window.setInterval(() => {
|
||||
try {
|
||||
current.send(JSON.stringify({ op: 3 }));
|
||||
} catch {
|
||||
handleDrop();
|
||||
}
|
||||
}, interval);
|
||||
current.send(JSON.stringify({ op: 2, d: { subscribe_to_id: DISCORD_ID } }));
|
||||
} else if (
|
||||
message.op === 0 &&
|
||||
(message.t === "INIT_STATE" || message.t === "PRESENCE_UPDATE")
|
||||
) {
|
||||
if (!disposed && isLanyardData(message.d)) {
|
||||
events.onData(message.d);
|
||||
}
|
||||
}
|
||||
};
|
||||
current.onerror = handleDrop;
|
||||
current.onclose = handleDrop;
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
connect();
|
||||
safetyTimer = window.setInterval(() => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
void pollOnce();
|
||||
}
|
||||
}, SAFETY_POLL_MS);
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
disposed = true;
|
||||
window.clearTimeout(reconnectTimer);
|
||||
window.clearInterval(safetyTimer);
|
||||
window.clearInterval(heartbeatTimer);
|
||||
if (socket) {
|
||||
dropSocket(socket);
|
||||
}
|
||||
}
|
||||
|
||||
return { start, stop };
|
||||
}
|
||||
|
||||
export function useLanyard(initial: LanyardData | null): LanyardData | null {
|
||||
const [data, setData] = useState<LanyardData | null>(initial);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = createLanyardSocket({ onData: (snapshot) => setData(snapshot) });
|
||||
controller.start();
|
||||
return () => controller.stop();
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
}
|
||||
Reference in new issue
Block a user