Skip to content

Commit 7d095de

Browse files
Merge pull request #4 from dota2classic/testcontainers
Testcontainers
2 parents 106bfea + 0983b78 commit 7d095de

28 files changed

+655
-713
lines changed

bun.lockb

472 Bytes
Binary file not shown.

jest.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './src/util/promise';
12
import { EventBus, IEvent } from '@nestjs/cqrs';
23
import { inspect } from 'util';
34
import { expect, jest } from '@jest/globals';

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@nestjs/testing": "^10.4.10",
6666
"@swc/cli": "^0.5.1",
6767
"@swc/core": "^1.9.3",
68+
"@testcontainers/postgresql": "^10.15.0",
6869
"@types/cron": "^2.4.3",
6970
"@types/express": "^5.0.0",
7071
"@types/jest": "29.5.14",
@@ -78,7 +79,7 @@
7879
"jest": "29.7.0",
7980
"nodemon": "^3.1.7",
8081
"prettier": "^3.4.1",
81-
"supertest": "^7.0.0",
82+
"supertest": "^6.3.4",
8283
"testcontainers": "^10.15.0",
8384
"ts-jest": "29.2.5",
8485
"ts-loader": "^9.5.1",

src/@test/cqrs.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import {
77
ofType,
88
QueryBus,
99
QueryHandler,
10-
} from "@nestjs/cqrs";
11-
import { Provider, Type } from "@nestjs/common";
10+
} from '@nestjs/cqrs';
11+
import { Provider, Type } from '@nestjs/common';
1212
import { RuntimeRepository } from 'util/runtime-repository';
13-
import { TestDataService } from '@test/test-util';
1413

1514
const ebusProvider: Provider = {
1615
provide: EventBus,
@@ -41,8 +40,7 @@ export const TestEnvironment = () => [
4140
TestEventBus(),
4241
TestCommandBus(),
4342
TestQueryBus(),
44-
EventPublisher,
45-
TestDataService
43+
EventPublisher
4644
];
4745

4846
export function clearRepositories() {

src/@test/create-fake-match.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Repository } from 'typeorm';
2+
import FinishedMatchEntity from 'gameserver/model/finished-match.entity';
3+
import { getRepositoryToken } from '@nestjs/typeorm';
4+
import { GameSeasonEntity } from 'gameserver/model/game-season.entity';
5+
import { TestingModule } from '@nestjs/testing';
6+
import { MatchEntity } from 'gameserver/model/match.entity';
7+
import { Dota2Version } from 'gateway/shared-types/dota2version';
8+
import { MatchmakingMode } from 'gateway/shared-types/matchmaking-mode';
9+
import { Dota_GameMode } from 'gateway/shared-types/dota-game-mode';
10+
import { DotaTeam } from 'gateway/shared-types/dota-team';
11+
import PlayerInMatchEntity from 'gameserver/model/player-in-match.entity';
12+
13+
export async function createSeason(module: TestingModule) {
14+
const seasonRep = module.get<Repository<GameSeasonEntity>>(
15+
getRepositoryToken(GameSeasonEntity),
16+
);
17+
const gs3 = new GameSeasonEntity();
18+
gs3.id = 3;
19+
gs3.start_timestamp = new Date("2023-08-31 20:00:00.000000");
20+
gs3.version = Dota2Version.Dota_684;
21+
await seasonRep.save(gs3);
22+
}
23+
24+
export async function createFakeMatch(
25+
module: TestingModule,
26+
winner: DotaTeam = DotaTeam.RADIANT,
27+
mode: MatchmakingMode = MatchmakingMode.UNRANKED,
28+
duration: number = 100
29+
): Promise<FinishedMatchEntity> {
30+
const matchRep = module.get<Repository<FinishedMatchEntity>>(
31+
getRepositoryToken(FinishedMatchEntity),
32+
);
33+
34+
const meRep = module.get<Repository<MatchEntity>>(
35+
getRepositoryToken(MatchEntity),
36+
);
37+
38+
// seasons setup
39+
40+
const me = new MatchEntity();
41+
me.finished = true;
42+
me.started = true;
43+
me.server = "";
44+
me.mode = mode;
45+
await meRep.save(me);
46+
47+
const match = new FinishedMatchEntity(
48+
me.id,
49+
winner,
50+
new Date().toISOString(),
51+
Dota_GameMode.RANKED_AP,
52+
mode,
53+
duration,
54+
"",
55+
);
56+
57+
await matchRep.save(match);
58+
59+
return match;
60+
}
61+
62+
63+
export async function fillMatch(module: TestingModule, fm: FinishedMatchEntity, count: number = 10){
64+
const pRep = module.get<Repository<PlayerInMatchEntity>>(
65+
getRepositoryToken(PlayerInMatchEntity),
66+
);
67+
68+
const randInt = (r: number) => Math.round(Math.random() * r)
69+
70+
const pims: PlayerInMatchEntity[] = [];
71+
for (let i = 0; i < count; i++) {
72+
const pim = new PlayerInMatchEntity();
73+
pim.match = fm;
74+
pim.playerId = Math.round((Math.random() * 10000 + 1000000)).toString();
75+
pim.abandoned = false;
76+
pim.denies = randInt(50)
77+
pim.last_hits = randInt(250)
78+
pim.kills = randInt(20);
79+
pim.deaths = randInt(10);
80+
pim.assists = randInt(20);
81+
pim.team = i < count / 2 ? DotaTeam.RADIANT : DotaTeam.DIRE;
82+
pim.level = randInt(25);
83+
pim.gpm = randInt(800);
84+
pim.xpm = randInt(700);
85+
pim.hero = 'npc_dota_hero_riki';
86+
pim.item0 = randInt(100)
87+
pim.item1 = randInt(100)
88+
pim.item2 = randInt(100)
89+
pim.item3 = randInt(100)
90+
pim.item4 = randInt(100)
91+
pim.item5 = randInt(100)
92+
await pRep.save(pim);
93+
pims.push(pim)
94+
}
95+
96+
return pims;
97+
}

src/@test/testcontainers.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
4+
export const typeorm = (container: StartedPostgreSqlContainer, Entities: any[]) => [
5+
TypeOrmModule.forRoot({
6+
host: container.getHost(),
7+
port: container.getFirstMappedPort(),
8+
9+
type: "postgres",
10+
database: "postgres",
11+
12+
username: container.getUsername(),
13+
password: container.getPassword(),
14+
entities: Entities,
15+
synchronize: true,
16+
dropSchema: false,
17+
ssl: false,
18+
}),
19+
TypeOrmModule.forFeature(Entities),
20+
]

src/app.module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { CoreController } from 'core.controller';
88
import { TypeOrmModule } from '@nestjs/typeorm';
99
import { Entities, prodDbConfig } from 'util/typeorm-config';
1010
import { QueryController } from 'query.controller';
11-
import { MatchController } from 'rest/match.controller';
1211
import { Mapper } from 'rest/mapper';
1312
import { PlayerController } from 'rest/player.controller';
1413
import { InfoController } from 'rest/info.controller';
@@ -22,6 +21,8 @@ import { QueryCache } from 'rcache';
2221
import { CacheModule } from '@nestjs/cache-manager';
2322
import { MatchService } from 'rest/service/match.service';
2423
import { CrimeController } from 'rest/crime.controller';
24+
import { MatchMapper } from 'rest/match/match.mapper';
25+
import { MatchController } from 'rest/match/match.controller';
2526

2627

2728
export function qCache<T, B>() {
@@ -69,6 +70,7 @@ export function qCache<T, B>() {
6970
MetaService,
7071
MatchService,
7172
PlayerService,
73+
MatchMapper,
7274
Mapper,
7375
...GameServerDomain,
7476
outerQuery(GetUserInfoQuery, 'QueryCore', qCache())

src/core.controller.ts

-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ export class CoreController {
7272

7373
@EventPattern(PlayerBanHammeredEvent.name)
7474
async PlayerBanHammeredEvent(data: PlayerBanHammeredEvent) {
75-
console.log("H?", data)
7675
this.event(PlayerBanHammeredEvent, data);
7776
}
7877

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { TestEnvironment } from '@test/cqrs';
3+
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
4+
import FinishedMatchEntity from 'gameserver/model/finished-match.entity';
5+
import { createFakeMatch, fillMatch } from '@test/create-fake-match';
6+
import { MmrChangeLogEntity } from 'gameserver/model/mmr-change-log.entity';
7+
import { NestApplication } from '@nestjs/core';
8+
import { typeorm } from '@test/testcontainers';
9+
import { VersionPlayerEntity } from 'gameserver/model/version-player.entity';
10+
import { ProcessRankedMatchHandler } from 'gameserver/command/ProcessRankedMatch/process-ranked-match.handler';
11+
import { GameServerService } from 'gameserver/gameserver.service';
12+
import { GameSeasonEntity } from 'gameserver/model/game-season.entity';
13+
import { Dota2Version } from 'gateway/shared-types/dota2version';
14+
import { DotaTeam } from 'gateway/shared-types/dota-team';
15+
import PlayerInMatchEntity from 'gameserver/model/player-in-match.entity';
16+
import { MatchEntity } from 'gameserver/model/match.entity';
17+
import { ProcessRankedMatchCommand } from 'gameserver/command/ProcessRankedMatch/process-ranked-match.command';
18+
import { PlayerId } from 'gateway/shared-types/player-id';
19+
import { getRepositoryToken } from '@nestjs/typeorm';
20+
import { Repository } from 'typeorm';
21+
import { MatchmakingMode } from 'gateway/shared-types/matchmaking-mode';
22+
23+
describe("MatchController", () => {
24+
jest.setTimeout(60000);
25+
26+
let container: StartedPostgreSqlContainer;
27+
let module: TestingModule;
28+
let app: NestApplication;
29+
30+
let gsServiceMock = {
31+
getCurrentSeason() {
32+
const season = new GameSeasonEntity();
33+
season.start_timestamp = new Date("2020-07-07 00:00:00.000000");
34+
season.id = 1;
35+
season.version = Dota2Version.Dota_684;
36+
return Promise.resolve(season);
37+
},
38+
getGamesPlayed: jest.fn(),
39+
};
40+
41+
42+
const processRanked = async (fm: FinishedMatchEntity, pims: PlayerInMatchEntity[]) => {
43+
await app.get(ProcessRankedMatchHandler).execute(
44+
new ProcessRankedMatchCommand(
45+
fm.id,
46+
pims
47+
.filter((t) => t.team === DotaTeam.RADIANT)
48+
.map((it) => new PlayerId(it.playerId)),
49+
pims
50+
.filter((t) => t.team === DotaTeam.DIRE)
51+
.map((it) => new PlayerId(it.playerId)),
52+
fm.matchmaking_mode,
53+
),
54+
);
55+
}
56+
57+
beforeAll(async () => {
58+
container = await new PostgreSqlContainer()
59+
.withUsername("username")
60+
.withPassword("password")
61+
.start();
62+
63+
const Entities = [
64+
VersionPlayerEntity,
65+
MatchEntity,
66+
FinishedMatchEntity,
67+
MmrChangeLogEntity,
68+
PlayerInMatchEntity,
69+
];
70+
71+
module = await Test.createTestingModule({
72+
imports: [...typeorm(container, Entities)],
73+
controllers: [],
74+
providers: [
75+
ProcessRankedMatchHandler,
76+
...TestEnvironment(),
77+
{
78+
provide: GameServerService,
79+
useValue: gsServiceMock,
80+
},
81+
],
82+
}).compile();
83+
84+
app = module.createNestApplication();
85+
await app.init();
86+
});
87+
88+
afterAll(async () => {
89+
await app.close();
90+
await container.stop();
91+
});
92+
93+
it("should spin up", () => {});
94+
95+
it("should update mmr after ranked match", async () => {
96+
const fm = await createFakeMatch(module, DotaTeam.RADIANT);
97+
const pims = await fillMatch(module, fm, 10);
98+
99+
gsServiceMock.getGamesPlayed = jest.fn(
100+
(
101+
season: GameSeasonEntity,
102+
pid: PlayerId,
103+
modes: MatchmakingMode[] | undefined,
104+
beforeTimestamp: string,
105+
) => {
106+
if (pims.findIndex((t) => t.playerId === pid.value) % 2 === 0) return 0;
107+
else return 100;
108+
},
109+
);
110+
111+
await processRanked(fm, pims);
112+
113+
const mmrRepo: Repository<MmrChangeLogEntity> = app.get(
114+
getRepositoryToken(MmrChangeLogEntity),
115+
);
116+
const changes = await mmrRepo.find({ where: { matchId: fm.id } });
117+
expect(changes).toHaveLength(10);
118+
119+
changes.forEach(change => {
120+
expect(change.mmrBefore).toEqual(VersionPlayerEntity.STARTING_MMR)
121+
})
122+
123+
changes
124+
.filter((t) => t.winner)
125+
.forEach((winner) => {
126+
expect(winner.change).toBeGreaterThan(0);
127+
});
128+
129+
changes
130+
.filter((t) => !t.winner)
131+
.forEach((loser) => {
132+
expect(loser.change).toBeLessThan(0);
133+
});
134+
135+
136+
const calib = changes.find(t => t.playerId === pims[0].playerId);
137+
expect(calib.change).toBeGreaterThan(90)
138+
139+
const nonCalib = changes.find(t => t.playerId === pims[1].playerId);
140+
141+
142+
expect(nonCalib.change).toBeGreaterThan(10)
143+
expect(nonCalib.change).toBeLessThan(50)
144+
});
145+
146+
it('should always subtract mmr for leavers', async () => {
147+
const fm = await createFakeMatch(module, DotaTeam.RADIANT);
148+
const pims = await fillMatch(module, fm, 10);
149+
150+
const pimRep = module.get(getRepositoryToken(PlayerInMatchEntity));
151+
pims[3].abandoned = true
152+
pims[7].abandoned = true
153+
pimRep.save(pims)
154+
155+
gsServiceMock.getGamesPlayed = jest.fn(
156+
(
157+
season: GameSeasonEntity,
158+
pid: PlayerId,
159+
modes: MatchmakingMode[] | undefined,
160+
beforeTimestamp: string,
161+
) => {
162+
return 100;
163+
},
164+
);
165+
166+
await processRanked(fm, pims);
167+
168+
169+
const mmrRepo: Repository<MmrChangeLogEntity> = app.get(
170+
getRepositoryToken(MmrChangeLogEntity),
171+
);
172+
const changes = await mmrRepo.find({ where: { matchId: fm.id } });
173+
expect(changes).toHaveLength(10);
174+
175+
176+
expect(changes[0].change).toBeGreaterThan(0)
177+
expect(changes[1].change).toBeGreaterThan(0)
178+
expect(changes[2].change).toBeGreaterThan(0)
179+
expect(changes[3].change).toBeLessThan(0)
180+
expect(changes[4].change).toBeGreaterThan(0)
181+
182+
expect(changes[5].change).toBeLessThan(0)
183+
expect(changes[6].change).toBeLessThan(0)
184+
expect(changes[7].change).toBeLessThan(0)
185+
expect(changes[8].change).toBeLessThan(0)
186+
expect(changes[9].change).toBeLessThan(0)
187+
188+
189+
});
190+
});

0 commit comments

Comments
 (0)