Skip to content

Commit 4596cb8

Browse files
committed
fix: mock external services in e2e tests to fix CI bootstrap
During app.init(), several modules make external calls that fail in CI where only TimescaleDB and Redis are available: - HasuraService: fetch calls to non-existent Hasura (GameServerNodeModule, MatchesModule) - TypeSenseService.setup(): infinite retry loop to typesense:8108 - SystemService: K8s config loading + infinite detectFeatures() loop - DiscordBotService: Discord API calls Fix: override these providers with mocks via NestJS TestingModule, add all missing env vars (Postgres, App, Steam, Discord, Tailscale, Typesense), extract shared createTestApp() helper, and set 30s test timeout for module bootstrap.
1 parent 046f089 commit 4596cb8

File tree

6 files changed

+150
-35
lines changed

6 files changed

+150
-35
lines changed

test/app.e2e-spec.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import { Test, TestingModule } from "@nestjs/testing";
21
import { INestApplication } from "@nestjs/common";
3-
import { WsAdapter } from "@nestjs/platform-ws";
42
import * as request from "supertest";
5-
import { AppModule } from "../src/app.module";
3+
import { createTestApp } from "./test-helpers";
64

75
describe("AppController (e2e)", () => {
86
let app: INestApplication;
97

108
beforeAll(async () => {
11-
const moduleFixture: TestingModule = await Test.createTestingModule({
12-
imports: [AppModule],
13-
}).compile();
14-
15-
app = moduleFixture.createNestApplication();
16-
app.useWebSocketAdapter(new WsAdapter(app));
17-
await app.init();
9+
app = await createTestApp();
1810
});
1911

2012
afterAll(async () => {

test/auth.e2e-spec.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import { Test, TestingModule } from "@nestjs/testing";
21
import { INestApplication } from "@nestjs/common";
3-
import { WsAdapter } from "@nestjs/platform-ws";
42
import * as request from "supertest";
5-
import { AppModule } from "../src/app.module";
3+
import { createTestApp } from "./test-helpers";
64

75
describe("Auth Guards (e2e)", () => {
86
let app: INestApplication;
97

108
beforeAll(async () => {
11-
const moduleFixture: TestingModule = await Test.createTestingModule({
12-
imports: [AppModule],
13-
}).compile();
14-
15-
app = moduleFixture.createNestApplication();
16-
app.useWebSocketAdapter(new WsAdapter(app));
17-
await app.init();
9+
app = await createTestApp();
1810
});
1911

2012
afterAll(async () => {

test/jest-e2e.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"moduleFileExtensions": ["js", "json", "ts"],
33
"rootDir": ".",
44
"testEnvironment": "node",
5+
"testTimeout": 30000,
56
"testRegex": ".e2e-spec.ts$",
67
"transform": {
78
"^.+\\.(t|j)s$": "ts-jest"

test/matches.e2e-spec.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { Test, TestingModule } from "@nestjs/testing";
21
import { INestApplication } from "@nestjs/common";
3-
import { WsAdapter } from "@nestjs/platform-ws";
42
import * as request from "supertest";
5-
import { AppModule } from "../src/app.module";
3+
import { createTestApp } from "./test-helpers";
64

75
/**
86
* E2E tests for match lifecycle.
@@ -16,13 +14,7 @@ describe("Matches (e2e)", () => {
1614
let app: INestApplication;
1715

1816
beforeAll(async () => {
19-
const moduleFixture: TestingModule = await Test.createTestingModule({
20-
imports: [AppModule],
21-
}).compile();
22-
23-
app = moduleFixture.createNestApplication();
24-
app.useWebSocketAdapter(new WsAdapter(app));
25-
await app.init();
17+
app = await createTestApp();
2618
});
2719

2820
afterAll(async () => {

test/test-env.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,58 @@
11
// Test environment defaults — loaded via jest setupFiles before each test worker.
22
// Values are only set if not already present, so CI or local .env can override.
33

4+
// App config
5+
process.env.APP_KEY = process.env.APP_KEY || "test-app-key-for-e2e-tests";
6+
process.env.ENC_SECRET = process.env.ENC_SECRET || "test-enc-secret-32chars!!!!!!!!";
7+
process.env.WS_DOMAIN = process.env.WS_DOMAIN || "localhost";
8+
process.env.WEB_DOMAIN = process.env.WEB_DOMAIN || "localhost";
9+
process.env.API_DOMAIN = process.env.API_DOMAIN || "localhost";
10+
process.env.RELAY_DOMAIN = process.env.RELAY_DOMAIN || "localhost";
411
process.env.DEMOS_DOMAIN = process.env.DEMOS_DOMAIN || "localhost";
5-
process.env.S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "test-key";
6-
process.env.S3_SECRET = process.env.S3_SECRET || "test-secret";
7-
process.env.S3_BUCKET = process.env.S3_BUCKET || "test-bucket";
8-
process.env.S3_ENDPOINT = process.env.S3_ENDPOINT || "localhost";
9-
process.env.S3_PORT = process.env.S3_PORT || "9000";
1012

13+
// Postgres — matches test/docker-compose.test.yml
14+
process.env.POSTGRES_HOST = process.env.POSTGRES_HOST || "localhost";
15+
process.env.POSTGRES_SERVICE_PORT = process.env.POSTGRES_SERVICE_PORT || "5433";
16+
process.env.POSTGRES_DB = process.env.POSTGRES_DB || "hasura_test";
17+
process.env.POSTGRES_USER = process.env.POSTGRES_USER || "hasura";
18+
process.env.POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || "test_password";
19+
20+
// Redis — matches test/docker-compose.test.yml
1121
process.env.REDIS_HOST = process.env.REDIS_HOST || "localhost";
1222
process.env.REDIS_SERVICE_PORT = process.env.REDIS_SERVICE_PORT || "6380";
1323

24+
// Hasura (mocked in tests, but config must be valid)
1425
process.env.HASURA_GRAPHQL_ENDPOINT =
1526
process.env.HASURA_GRAPHQL_ENDPOINT || "http://localhost:8080";
1627
process.env.HASURA_GRAPHQL_ADMIN_SECRET =
1728
process.env.HASURA_GRAPHQL_ADMIN_SECRET || "test-secret";
29+
30+
// S3/MinIO
31+
process.env.S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "test-key";
32+
process.env.S3_SECRET = process.env.S3_SECRET || "test-secret";
33+
process.env.S3_BUCKET = process.env.S3_BUCKET || "test-bucket";
34+
process.env.S3_ENDPOINT = process.env.S3_ENDPOINT || "localhost";
35+
process.env.S3_PORT = process.env.S3_PORT || "9000";
36+
37+
// Steam (dummy values — no real Steam calls in tests)
38+
process.env.STEAM_WEB_API_KEY =
39+
process.env.STEAM_WEB_API_KEY || "test-steam-key";
40+
process.env.STEAM_USER = process.env.STEAM_USER || "test-steam-user";
41+
process.env.STEAM_PASSWORD = process.env.STEAM_PASSWORD || "test-steam-password";
42+
43+
// Discord (empty token causes DiscordBotService.setup() to skip)
44+
process.env.DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "";
45+
process.env.DISCORD_CLIENT_ID =
46+
process.env.DISCORD_CLIENT_ID || "test-discord-client-id";
47+
process.env.DISCORD_CLIENT_SECRET =
48+
process.env.DISCORD_CLIENT_SECRET || "test-discord-secret";
49+
50+
// Tailscale (dummy values)
51+
process.env.TAILSCALE_CLIENT_ID =
52+
process.env.TAILSCALE_CLIENT_ID || "test-tailscale-id";
53+
process.env.TAILSCALE_SECRET_ID =
54+
process.env.TAILSCALE_SECRET_ID || "test-tailscale-secret";
55+
56+
// Typesense (mocked in tests)
57+
process.env.TYPESENSE_API_KEY =
58+
process.env.TYPESENSE_API_KEY || "test-typesense-key";

test/test-helpers.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Test, TestingModule } from "@nestjs/testing";
2+
import { INestApplication } from "@nestjs/common";
3+
import { WsAdapter } from "@nestjs/platform-ws";
4+
import { AppModule } from "../src/app.module";
5+
import { HasuraService } from "../src/hasura/hasura.service";
6+
import { TypeSenseService } from "../src/type-sense/type-sense.service";
7+
import { SystemService } from "../src/system/system.service";
8+
import { DiscordBotService } from "../src/discord-bot/discord-bot.service";
9+
10+
/**
11+
* Creates a mock Hasura query result via Proxy. Collections return empty
12+
* arrays; aggregate queries return count > 0 to skip data-generation
13+
* bootstrap logic (e.g. MatchesModule.generatePlayerRatings).
14+
*/
15+
function createMockQueryResult(): any {
16+
return new Proxy(
17+
{},
18+
{
19+
get(_, prop) {
20+
if (typeof prop === "string") {
21+
if (prop.includes("aggregate")) {
22+
return { aggregate: { count: 1 } };
23+
}
24+
return [];
25+
}
26+
return undefined;
27+
},
28+
},
29+
);
30+
}
31+
32+
export const mockHasuraService = {
33+
query: jest
34+
.fn()
35+
.mockImplementation(() => Promise.resolve(createMockQueryResult())),
36+
mutation: jest.fn().mockResolvedValue({}),
37+
setup: jest.fn().mockResolvedValue(undefined),
38+
checkSecret: jest.fn().mockReturnValue(false),
39+
getHasuraHeaders: jest.fn().mockResolvedValue({}),
40+
};
41+
42+
export const mockTypeSenseService = {
43+
setup: jest.fn().mockResolvedValue(undefined),
44+
updatePlayer: jest.fn().mockResolvedValue(undefined),
45+
removePlayer: jest.fn().mockResolvedValue(undefined),
46+
upsertCvars: jest.fn().mockResolvedValue(undefined),
47+
resetCvars: jest.fn().mockResolvedValue(undefined),
48+
createPlayerCollection: jest.fn().mockResolvedValue(undefined),
49+
createCvarsCollection: jest.fn().mockResolvedValue(undefined),
50+
};
51+
52+
export const mockSystemService = {
53+
detectFeatures: jest.fn().mockResolvedValue(undefined),
54+
getSetting: jest
55+
.fn()
56+
.mockImplementation((_name, defaultValue) =>
57+
Promise.resolve(defaultValue),
58+
),
59+
};
60+
61+
export const mockDiscordBotService = {
62+
setup: jest.fn().mockResolvedValue(undefined),
63+
login: jest.fn().mockResolvedValue(undefined),
64+
client: null,
65+
};
66+
67+
/**
68+
* Creates a NestJS test application with external services mocked out.
69+
*
70+
* Mocked services (not available in test CI):
71+
* - HasuraService → prevents GraphQL fetches to non-existent Hasura
72+
* - TypeSenseService → prevents infinite retry loop to typesense:8108
73+
* - SystemService → prevents K8s config loading + infinite detectFeatures loop
74+
* - DiscordBotService → prevents Discord API calls
75+
*
76+
* Uses WsAdapter to match src/main.ts bootstrap.
77+
*/
78+
export async function createTestApp(): Promise<INestApplication> {
79+
const moduleFixture: TestingModule = await Test.createTestingModule({
80+
imports: [AppModule],
81+
})
82+
.overrideProvider(HasuraService)
83+
.useValue(mockHasuraService)
84+
.overrideProvider(TypeSenseService)
85+
.useValue(mockTypeSenseService)
86+
.overrideProvider(SystemService)
87+
.useValue(mockSystemService)
88+
.overrideProvider(DiscordBotService)
89+
.useValue(mockDiscordBotService)
90+
.compile();
91+
92+
const app = moduleFixture.createNestApplication();
93+
app.useWebSocketAdapter(new WsAdapter(app));
94+
await app.init();
95+
96+
return app;
97+
}

0 commit comments

Comments
 (0)