Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IGetAllQuery } from '../../../../../common/interface/IGetAllQuery';

export default class IGetAllQueryBuilder {
private readonly base: Partial<IGetAllQuery> = {
filter: {},
select: [],
limit: 0,
sort: {},
skip: 0,
};

build(): IGetAllQuery {
return { ...this.base } as IGetAllQuery;
}

setFilter(filter: object) {
this.base.filter = filter;
return this;
}

setSelect(select: string[]) {
this.base.select = select;
return this;
}

setLimit(limit: number) {
this.base.limit = limit;
return this;
}

setSort(sort: Record<string, 1 | -1>) {
this.base.sort = sort;
return this;
}

setSkip(skip: number) {
this.base.skip = skip;
return this;
}
}
18 changes: 18 additions & 0 deletions src/__tests__/common/interface/data/interfaceBuilderFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import IGetAllQueryBuilder from './interface/IGetAllQueryBuilder';

type BuilderName = 'IGetAllQuery';

type BuilderMap = {
IGetAllQuery: IGetAllQueryBuilder;
};

export default class InterfaceBuilderFactory {
static getBuilder<T extends BuilderName>(builderName: T): BuilderMap[T] {
switch (builderName) {
case 'IGetAllQuery':
return new IGetAllQueryBuilder() as BuilderMap[T];
default:
throw new Error(`Unknown builder name: ${builderName}`);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Injectable, OnModuleDestroy } from '@nestjs/common';

/**
* In-memory mock of RedisService for use in tests.
* Simulates set/get/keys with TTL support.
*/
@Injectable()
export class RedisServiceInMemory implements OnModuleDestroy {
private readonly store = new Map<
string,
{ value: string; expiresAt?: number }
>();

private isDestroyed = false;

async set(key: string, value: string, ttlS?: number): Promise<void> {
const expiresAt = ttlS ? Date.now() + ttlS * 1000 : undefined;
this.store.set(key, { value, expiresAt });
}

async get(key: string): Promise<string | null> {
const entry = this.store.get(key);
if (!entry) return null;

if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.store.delete(key);
return null;
}

return entry.value;
}

async getKeys(pattern: string): Promise<string[]> {
const regex = new RegExp('^' + pattern.replace('*', '.*') + '$');
const now = Date.now();

return Array.from(this.store.entries())
.filter(([key, { expiresAt }]) => {
if (expiresAt && now > expiresAt) {
this.store.delete(key);
return false;
}
return regex.test(key);
})
.map(([key]) => key);
}

async onModuleDestroy() {
this.store.clear();
this.isDestroyed = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { LeaderboardService } from '../../../leaderboard/leaderboard.service';
import ClanBuilderFactory from '../../clan/data/clanBuilderFactory';
import ClanModule from '../../clan/modules/clan.module';
import LeaderboardModule from '../modules/leaderboard.module';
import InterfaceBuilderFactory from '../../common/interface/data/interfaceBuilderFactory';
import { Clan } from '../../../clan/clan.schema';

describe('LeaderboardService.getClanLeaderboard() test suite', () => {
let service: LeaderboardService;
const queryBuilder = InterfaceBuilderFactory.getBuilder('IGetAllQuery');

const clanBuilder = ClanBuilderFactory.getBuilder('Clan');
const clan1 = clanBuilder.setName('clan-1').setPoints(100).build();
const clan2 = clanBuilder.setName('clan-2').setPoints(50).build();
const clan3 = clanBuilder.setName('clan-3').setPoints(10).build();
const clanModel = ClanModule.getClanModel();

beforeEach(async () => {
service = await LeaderboardModule.getLeaderboardService();

await clanModel.create(clan1);
await clanModel.create(clan2);
await clanModel.create(clan3);
});

it('Should return leading clans in valid order', async () => {
const query = queryBuilder.setLimit(10).setSkip(0).build();

const leaders = (await service.getClanLeaderboard(query)) as Clan[];

const clanNames = leaders.map((leader) => leader.name);

expect(clanNames).toEqual([clan1.name, clan2.name, clan3.name]);
});

it('Should return requested amount of leading clans', async () => {
const query = queryBuilder.setLimit(2).setSkip(0).build();

const leaders = await service.getClanLeaderboard(query);

expect(leaders).toHaveLength(2);
});

it('Should be able to skip leading clans', async () => {
const query = queryBuilder.setLimit(10).setSkip(2).build();

const leaders = (await service.getClanLeaderboard(query)) as Clan[];

expect(leaders).toHaveLength(1);
expect(leaders[0].name).toBe(clan3.name);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { LeaderboardService } from '../../../leaderboard/leaderboard.service';
import ClanBuilderFactory from '../../clan/data/clanBuilderFactory';
import ClanModule from '../../clan/modules/clan.module';
import LeaderboardModule from '../modules/leaderboard.module';
import { getNonExisting_id } from '../../test_utils/util/getNonExisting_id';

describe('LeaderboardService.getClanPosition() test suite', () => {
let service: LeaderboardService;

const clanBuilder = ClanBuilderFactory.getBuilder('Clan');
const clan1 = clanBuilder.setName('clan-1').setPoints(100).build();
const clan2 = clanBuilder.setName('clan-2').setPoints(50).build();
const clan3 = clanBuilder.setName('clan-3').setPoints(10).build();
const clanModel = ClanModule.getClanModel();

beforeEach(async () => {
service = await LeaderboardModule.getLeaderboardService();

const createClan1 = await clanModel.create(clan1);
clan1._id = createClan1._id.toString();
const createClan2 = await clanModel.create(clan2);
clan2._id = createClan2._id.toString();
const createClan3 = await clanModel.create(clan3);
clan3._id = createClan3._id.toString();
});

it('Should return valid order number of the requested clan', async () => {
const { position } = await service.getClanPosition(clan2._id);

expect(position).toBe(2);
});

it('Should throw ServiceError NOT_FOUND if clan with the _id can not be found', async () => {
try {
await service.getClanPosition(getNonExisting_id());
fail('getClanPosition() did not throw');
} catch (e) {
expect(e).toBeSE_NOT_FOUND();
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { LeaderboardService } from '../../../leaderboard/leaderboard.service';
import ClanBuilderFactory from '../../clan/data/clanBuilderFactory';
import ClanModule from '../../clan/modules/clan.module';
import LeaderboardModule from '../modules/leaderboard.module';
import InterfaceBuilderFactory from '../../common/interface/data/interfaceBuilderFactory';
import PlayerBuilderFactory from '../../player/data/playerBuilderFactory';
import PlayerModule from '../../player/modules/player.module';
import { Player } from '../../../player/schemas/player.schema';
import LoggedUser from '../../test_utils/const/loggedUser';

describe('LeaderboardService.getPlayerLeaderboard() test suite', () => {
let service: LeaderboardService;
const queryBuilder = InterfaceBuilderFactory.getBuilder('IGetAllQuery');

const playerBuilder = PlayerBuilderFactory.getBuilder('Player');
const player1 = playerBuilder
.setName('player-1')
.setUniqueIdentifier('player-1')
.setPoints(100)
.build();
const player2 = playerBuilder
.setName('player-2')
.setUniqueIdentifier('player-2')
.setPoints(50)
.build();
const player3 = playerBuilder
.setName('player-3')
.setUniqueIdentifier('player-3')
.setPoints(10)
.build();
const playerModel = PlayerModule.getPlayerModel();

const clanBuilder = ClanBuilderFactory.getBuilder('Clan');
const clan = clanBuilder.setName('clan').setPoints(100).build();
const clanModel = ClanModule.getClanModel();

beforeEach(async () => {
service = await LeaderboardModule.getLeaderboardService();

//Remove pre-created default player
await playerModel.deleteOne({ name: LoggedUser.getPlayer().name });

const createdClan = await clanModel.create(clan);
clan._id = createdClan._id;

player1.clan_id = clan._id;
player2.clan_id = clan._id;
player3.clan_id = clan._id;

await playerModel.create(player1);
await playerModel.create(player2);
await playerModel.create(player3);
});

it('Should return leading players in valid order', async () => {
const query = queryBuilder.setLimit(10).build();

const leaders = (await service.getPlayerLeaderboard(query)) as Player[];

const playerNames = leaders.map((leader) => leader.name);

expect(playerNames).toEqual([player1.name, player2.name, player3.name]);
});

it('Should return clanLogo data if player is in a clan', async () => {
const query = queryBuilder.setLimit(10).build();

const leaders = (await service.getPlayerLeaderboard(query)) as any[];

expect(leaders[0].clanLogo).toEqual(clan.clanLogo);
});

it('Should return requested amount of leading players', async () => {
const query = queryBuilder.setLimit(2).setSkip(0).build();
const leaders = await service.getPlayerLeaderboard(query);

expect(leaders).toHaveLength(2);
});

it('Should be able to skip leading players', async () => {
const query = queryBuilder.setLimit(10).setSkip(2).build();
const leaders = (await service.getPlayerLeaderboard(query)) as Player[];

expect(leaders).toHaveLength(1);
expect(leaders[0].name).toBe(player3.name);
});
});
11 changes: 11 additions & 0 deletions src/__tests__/leaderboard/modules/leaderboard.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LeaderboardService } from '../../../leaderboard/leaderboard.service';
import LeaderboardCommonModule from './leaderboardCommon';

export default class LeaderboardModule {
private constructor() {}

static async getLeaderboardService() {
const module = await LeaderboardCommonModule.getModule();
return module.resolve(LeaderboardService);
}
}
35 changes: 35 additions & 0 deletions src/__tests__/leaderboard/modules/leaderboardCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MongooseModule } from '@nestjs/mongoose';
import { RequestHelperModule } from '../../../requestHelper/requestHelper.module';
import { mongooseOptions, mongoString } from '../../test_utils/const/db';
import { RedisServiceInMemory } from '../../common/service/redis/mocks/RedisServiceInMemory';
import { PlayerModule } from '../../../player/player.module';
import { ClanModule } from '../../../clan/clan.module';
import { RedisModule } from '../../../common/service/redis/redis.module';
import { LeaderboardService } from '../../../leaderboard/leaderboard.service';
import { RedisService } from '../../../common/service/redis/redis.service';

export default class LeaderboardCommonModule {
private constructor() {}

private static module: TestingModule;

static async getModule() {
if (!LeaderboardCommonModule.module)
LeaderboardCommonModule.module = await Test.createTestingModule({
imports: [
MongooseModule.forRoot(mongoString, mongooseOptions),
PlayerModule,
ClanModule,
RedisModule,
RequestHelperModule,
],
providers: [LeaderboardService],
})
.overrideProvider(RedisService)
.useClass(RedisServiceInMemory)
.compile();

return LeaderboardCommonModule.module;
}
}