Skip to content

Commit 71499ad

Browse files
committed
test: add unit tests for controllers and web utilities to reach 80% coverage
Adds tests for the five server controllers (games, discord, config, env, costs) and three web modules (api.service, useClickOutside, useFileManager) — all previously untested. Also raises vitest coverage thresholds from the baseline floor (64%/75%/67%/64%) to 80% across all four metrics. Controllers are tested by instantiating them directly with typed vi.fn() stubs — no Nest testing module needed. Web tests use @testing-library/react renderHook for hooks and vi.stubGlobal('fetch') for the API service. https://claude.ai/code/session_013coUXcACPpVfmbK9i2D86Z
1 parent 1e6c120 commit 71499ad

9 files changed

Lines changed: 1201 additions & 4 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import 'reflect-metadata';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import { ConfigController } from './config.controller.js';
4+
import type { ConfigService, WatchdogConfig } from '../services/ConfigService.js';
5+
6+
vi.mock('../logger.js', () => ({
7+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
8+
}));
9+
10+
/** Default watchdog settings used by most tests. */
11+
const DEFAULT_CONFIG: WatchdogConfig = {
12+
watchdog_interval_minutes: 15,
13+
watchdog_idle_checks: 4,
14+
watchdog_min_packets: 100,
15+
};
16+
17+
/** Build a ConfigService stub wired up with the provided watchdog config. */
18+
function makeConfig(current: WatchdogConfig = DEFAULT_CONFIG): ConfigService {
19+
return {
20+
getConfig: vi.fn().mockReturnValue(current),
21+
saveConfig: vi.fn(),
22+
} as unknown as ConfigService;
23+
}
24+
25+
describe('ConfigController', () => {
26+
describe('get', () => {
27+
it('should return the current watchdog config from ConfigService', () => {
28+
const result = new ConfigController(makeConfig()).get();
29+
expect(result).toEqual(DEFAULT_CONFIG);
30+
});
31+
});
32+
33+
describe('update', () => {
34+
it('should merge a partial body with the current config and return success', () => {
35+
const config = makeConfig();
36+
const result = new ConfigController(config).update({ watchdog_interval_minutes: 30 });
37+
expect(result.success).toBe(true);
38+
expect(result.config.watchdog_interval_minutes).toBe(30);
39+
// Fields not in the body should retain their current values.
40+
expect(result.config.watchdog_idle_checks).toBe(4);
41+
expect(result.config.watchdog_min_packets).toBe(100);
42+
});
43+
44+
it('should persist the merged config to disk via ConfigService.saveConfig', () => {
45+
const config = makeConfig();
46+
new ConfigController(config).update({ watchdog_idle_checks: 6 });
47+
expect(config.saveConfig).toHaveBeenCalledWith(
48+
expect.objectContaining({ watchdog_idle_checks: 6 }),
49+
);
50+
});
51+
52+
it('should leave all fields unchanged when the body is empty', () => {
53+
const config = makeConfig();
54+
const result = new ConfigController(config).update({});
55+
expect(result.config).toEqual(DEFAULT_CONFIG);
56+
});
57+
58+
it('should update all three fields at once when all are supplied', () => {
59+
const config = makeConfig();
60+
const result = new ConfigController(config).update({
61+
watchdog_interval_minutes: 5,
62+
watchdog_idle_checks: 2,
63+
watchdog_min_packets: 50,
64+
});
65+
expect(result.config).toEqual({
66+
watchdog_interval_minutes: 5,
67+
watchdog_idle_checks: 2,
68+
watchdog_min_packets: 50,
69+
});
70+
});
71+
});
72+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import 'reflect-metadata';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import { CostsController } from './costs.controller.js';
4+
import type { ConfigService, TfOutputs } from '../services/ConfigService.js';
5+
import type { CostService } from '../services/CostService.js';
6+
import type { EcsService } from '../services/EcsService.js';
7+
8+
vi.mock('../logger.js', () => ({
9+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
10+
}));
11+
12+
/** A canned estimate object returned by CostService stubs. */
13+
const MOCK_ESTIMATE = {
14+
vcpu: 4,
15+
memoryGb: 16,
16+
costPerHour: 0.5,
17+
costPerDay24h: 12,
18+
costPerMonth4hpd: 60,
19+
};
20+
21+
/** Build a ConfigService stub with a minimal set of Terraform outputs. */
22+
function makeConfig(outputs: Partial<TfOutputs> | null = { game_names: ['minecraft'] }): ConfigService {
23+
return {
24+
getTfOutputs: vi.fn().mockReturnValue(outputs),
25+
} as unknown as ConfigService;
26+
}
27+
28+
/** Build a CostService stub whose estimateForSpec returns the canned estimate. */
29+
function makeCosts(): CostService {
30+
return {
31+
estimateForSpec: vi.fn().mockReturnValue(MOCK_ESTIMATE),
32+
getActualCosts: vi.fn().mockResolvedValue({ daily: [], total: 0, currency: 'USD', days: 7 }),
33+
} as unknown as CostService;
34+
}
35+
36+
/**
37+
* Build an EcsService stub. Pass `null` to simulate a missing task definition
38+
* (e.g. the game has never been deployed).
39+
*/
40+
function makeEcs(td: { cpu: number; memory: number } | null = { cpu: 4096, memory: 16384 }): EcsService {
41+
return {
42+
getTaskDefinition: vi.fn().mockResolvedValue(td),
43+
} as unknown as EcsService;
44+
}
45+
46+
describe('CostsController', () => {
47+
describe('estimate', () => {
48+
it('should return zeroed estimates when Terraform has not been applied', async () => {
49+
const result = await new CostsController(makeConfig(null), makeCosts(), makeEcs()).estimate();
50+
expect(result).toEqual({ games: {}, totalPerHourIfAllOn: 0 });
51+
});
52+
53+
it('should call getTaskDefinition and estimateForSpec for each game', async () => {
54+
const ecs = makeEcs();
55+
const costs = makeCosts();
56+
await new CostsController(makeConfig(), costs, ecs).estimate();
57+
expect(ecs.getTaskDefinition).toHaveBeenCalledWith('minecraft');
58+
expect(costs.estimateForSpec).toHaveBeenCalledWith(4096, 16384);
59+
});
60+
61+
it('should fall back to 2048 cpu / 8192 memory when getTaskDefinition returns null', async () => {
62+
const ecs = makeEcs(null);
63+
const costs = makeCosts();
64+
await new CostsController(makeConfig(), costs, ecs).estimate();
65+
expect(costs.estimateForSpec).toHaveBeenCalledWith(2048, 8192);
66+
});
67+
68+
it('should sum costPerHour across all games for totalPerHourIfAllOn', async () => {
69+
const config = makeConfig({ game_names: ['minecraft', 'palworld'] });
70+
const costs = makeCosts();
71+
vi.mocked(costs.estimateForSpec).mockReturnValue({ ...MOCK_ESTIMATE, costPerHour: 0.25 });
72+
const result = await new CostsController(config, costs, makeEcs()).estimate();
73+
// 2 games × $0.25/hr = $0.50/hr, rounded to 4 decimal places
74+
expect(result.totalPerHourIfAllOn).toBe(0.5);
75+
});
76+
77+
it('should include an estimate entry for each game', async () => {
78+
const config = makeConfig({ game_names: ['minecraft', 'palworld'] });
79+
const result = await new CostsController(config, makeCosts(), makeEcs()).estimate();
80+
expect(Object.keys(result.games)).toEqual(['minecraft', 'palworld']);
81+
});
82+
});
83+
84+
describe('actual', () => {
85+
it('should default to 7 days when the query param is absent', () => {
86+
const costs = makeCosts();
87+
new CostsController(makeConfig(), costs, makeEcs()).actual(undefined);
88+
expect(costs.getActualCosts).toHaveBeenCalledWith(7);
89+
});
90+
91+
it('should parse the days string and forward the integer to CostService', () => {
92+
const costs = makeCosts();
93+
new CostsController(makeConfig(), costs, makeEcs()).actual('14');
94+
expect(costs.getActualCosts).toHaveBeenCalledWith(14);
95+
});
96+
97+
it('should return whatever CostService returns', async () => {
98+
const costs = makeCosts();
99+
vi.mocked(costs.getActualCosts).mockResolvedValue({
100+
daily: [{ date: '2026-05-01', cost: 1.23 }],
101+
total: 1.23,
102+
currency: 'USD',
103+
days: 7,
104+
});
105+
const result = await new CostsController(makeConfig(), costs, makeEcs()).actual('7');
106+
expect(result).toMatchObject({ total: 1.23, currency: 'USD' });
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)