Backend: - Complete FastAPI backend with 42+ REST endpoints (auth, servers, config, players, bans, missions, mods, games, system) - Game adapter architecture with Arma 3 as first-class adapter - WebSocket real-time events for status, metrics, logs, players - Background thread system (process monitor, metrics, log tail, RCon poller) - Fernet encryption for sensitive config fields at rest - JWT auth with admin/viewer roles, bcrypt password hashing - SQLite with WAL mode, parameterized queries, migration system - APScheduler cleanup jobs for logs, metrics, events Frontend: - Server Detail page with 7 tabs (overview, config, players, bans, missions, mods, logs) - Settings page with password change and admin user management - Create Server wizard (4-step; known bug: silent validation failure) - New hooks: useServerDetail, useAuth, useGames - New components: ServerHeader, ConfigEditor, PlayerTable, BanTable, MissionList, ModList, LogViewer, PasswordChange, UserManager - WebSocket onEvent callback for real-time log accumulation - 120 unit tests passing (Vitest + React Testing Library) Docs: - Added .gitignore, CLAUDE.md, README.md - Updated FRONTEND.md, ARCHITECTURE.md with current implementation state - Added .env.example for backend configuration Known issues: - Create Server form: "Next" buttons don't validate before advancing, causing silent submit failure when fields are invalid - Config sub-tabs need UX redesign for non-technical users
137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
|
|
import {
|
|
useGamesList,
|
|
useGameDetail,
|
|
useGameConfigSchema,
|
|
useGameDefaults,
|
|
} from "@/hooks/useGames";
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
apiClient: {
|
|
get: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import { apiClient } from "@/lib/api";
|
|
|
|
function createWrapper() {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
};
|
|
}
|
|
|
|
describe("useGamesList", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(apiClient.get).mockReset();
|
|
});
|
|
|
|
it("should fetch games list", async () => {
|
|
const mockGames = [
|
|
{ game_type: "arma3", display_name: "Arma 3", version: "1.0", capabilities: ["rcon", "mods"] },
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
|
data: { success: true, data: mockGames },
|
|
});
|
|
|
|
const { result } = renderHook(() => useGamesList(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
expect(result.current.data).toEqual(mockGames);
|
|
expect(apiClient.get).toHaveBeenCalledWith("/api/games");
|
|
});
|
|
});
|
|
|
|
describe("useGameDetail", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(apiClient.get).mockReset();
|
|
});
|
|
|
|
it("should fetch game detail", async () => {
|
|
const mockDetail = {
|
|
game_type: "arma3",
|
|
display_name: "Arma 3",
|
|
version: "1.0",
|
|
schema_version: 1,
|
|
config_sections: ["server", "basic", "profile", "launch", "rcon"],
|
|
capabilities: ["rcon", "mods", "missions"],
|
|
allowed_executables: ["arma3server_x64.exe"],
|
|
};
|
|
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
|
data: { success: true, data: mockDetail },
|
|
});
|
|
|
|
const { result } = renderHook(() => useGameDetail("arma3"), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
expect(result.current.data).toEqual(mockDetail);
|
|
expect(apiClient.get).toHaveBeenCalledWith("/api/games/arma3");
|
|
});
|
|
|
|
it("should not fetch when gameType is empty", () => {
|
|
renderHook(() => useGameDetail(""), { wrapper: createWrapper() });
|
|
expect(apiClient.get).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("useGameConfigSchema", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(apiClient.get).mockReset();
|
|
});
|
|
|
|
it("should fetch config schema", async () => {
|
|
const mockSchema = { server: { type: "object", properties: {} } };
|
|
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
|
data: { success: true, data: mockSchema },
|
|
});
|
|
|
|
const { result } = renderHook(() => useGameConfigSchema("arma3"), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
expect(result.current.data).toEqual(mockSchema);
|
|
expect(apiClient.get).toHaveBeenCalledWith("/api/games/arma3/config-schema");
|
|
});
|
|
});
|
|
|
|
describe("useGameDefaults", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(apiClient.get).mockReset();
|
|
});
|
|
|
|
it("should fetch game defaults", async () => {
|
|
const mockDefaults = {
|
|
server: { hostname: "Arma 3 Server", max_players: 64 },
|
|
basic: { min_bandwidth: 131072 },
|
|
};
|
|
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
|
data: { success: true, data: mockDefaults },
|
|
});
|
|
|
|
const { result } = renderHook(() => useGameDefaults("arma3"), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
expect(result.current.data).toEqual(mockDefaults);
|
|
expect(apiClient.get).toHaveBeenCalledWith("/api/games/arma3/defaults");
|
|
});
|
|
|
|
it("should not fetch when gameType is empty", () => {
|
|
renderHook(() => useGameDefaults(""), { wrapper: createWrapper() });
|
|
expect(apiClient.get).not.toHaveBeenCalled();
|
|
});
|
|
}); |