feat: implement full backend + frontend server detail, settings, and create server pages
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
This commit is contained in:
137
frontend/src/__tests__/useGames.test.tsx
Normal file
137
frontend/src/__tests__/useGames.test.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user