feature(database-connection): Add tests for timetracking and add fallback value to max season for fetching stats

This commit is contained in:
2021-01-27 22:51:26 +01:00
parent 53710079e8
commit 9db53d5354
11 changed files with 194 additions and 100 deletions

View File

@@ -5988,6 +5988,15 @@
"@jest/types": "^25.5.0" "@jest/types": "^25.5.0"
} }
}, },
"jest-mock-extended": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-1.0.10.tgz",
"integrity": "sha512-R2wKiOgEUPoHZ2kLsAQeQP2IfVEgo3oQqWLSXKdMXK06t3UHkQirA2Xnsdqg/pX6KPWTsdnrzE2ig6nqNjdgVw==",
"dev": true,
"requires": {
"ts-essentials": "^4.0.0"
}
},
"jest-pnp-resolver": { "jest-pnp-resolver": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz",
@@ -9459,6 +9468,12 @@
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
}, },
"ts-essentials": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-4.0.0.tgz",
"integrity": "sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==",
"dev": true
},
"ts-jest": { "ts-jest": {
"version": "26.1.1", "version": "26.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz",

View File

@@ -45,6 +45,7 @@
"eslint-config-prettier": "^6.10.0", "eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.20.1",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-mock-extended": "^1.0.10",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"supertest": "^4.0.2", "supertest": "^4.0.2",
"ts-jest": "^26.1.1", "ts-jest": "^26.1.1",

View File

@@ -0,0 +1,25 @@
import { TimeUtil } from './timeutil';
describe('TimeUtil', () => {
it('should humanize raw seconds', () => {
// const rawTime = 12345;
// const expected = "0d 3h 25m 45s";
const input = {
rawTime: [
12345,
123,
4
],
expected: [
"0d 3h 25m 45s",
"0d 0h 2m 3s",
"0d 0h 0m 4s"
]
};
for (let i = 0; i < input.rawTime.length; i++) {
const actual = TimeUtil.humanize(input.rawTime[i]);
expect(actual).toEqual(input.expected[i]);
}
});
});

View File

@@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -33,12 +33,9 @@ export class AppController {
} }
@Get('/stats/season/:id') @Get('/stats/season/:id')
async getStats(@Param('id') id: string): Promise<UserStatsResponse> { async getStats(@Param('id') id?: string): Promise<UserStatsResponse> {
return this.databaseService return await this.databaseService
.fetchStats(id) .fetchStats(id)
.then(value => {
return value;
})
.catch(err => { .catch(err => {
logger.error(`Error occured when fetching stats.`, err); logger.error(`Error occured when fetching stats.`, err);
throw new HttpException( throw new HttpException(

View File

@@ -1,13 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { seasons } from '@prisma/client';
import { DatabaseService } from './database.service'; import { DatabaseService } from './database.service';
import { mockReset, MockProxy, mockDeep } from 'jest-mock-extended';
import { SeasonInfo } from '../models/aliases';
import { PrismaService } from '../prisma/prisma.service';
import { TimeTrackerPredicate } from './timetracking.predicate';
import { mocked } from 'ts-jest';
describe('DatabaseService', () => { describe('DatabaseService', () => {
let service: DatabaseService; let service: DatabaseService;
const mockedPrismaService: MockProxy<PrismaService> = mockDeep<PrismaService>();
beforeEach(async () => { beforeEach(async () => {
mockReset(mockedPrismaService);
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseService], providers: [DatabaseService, PrismaService, TimeTrackerPredicate],
}).compile(); })
.overrideProvider(PrismaService)
.useValue(mockedPrismaService)
.compile();
service = module.get<DatabaseService>(DatabaseService); service = module.get<DatabaseService>(DatabaseService);
}); });
@@ -15,4 +27,43 @@ describe('DatabaseService', () => {
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
// TODO This will be skipped as Prisma__#client can barely be mocked.
// it('fetch season info with season ID given', async () => {
// const seasonIdToBeTested = 1;
// const mockedValue: seasons = {
// season_id: 2,
// start_date: new Date('1995-12-17T03:24:00'),
// end_date: null
// };
// const mockedMaxSeasonId = {
// max: {
// season_id: 2
// }
// };
//
// mockedPrismaService.seasons.findUnique.calledWith({
// where: {
// season_id: Number(seasonIdToBeTested)
// }
// }).mockImplementation((subset) => {
// return new Promise(resolve => resolve(mockedValue));
// });
//
// mockedPrismaService.seasons.aggregate.calledWith({
// max: {
// season_id: true
// }
// }).mockImplementation(() => Promise.resolve(mockedMaxSeasonId));
//
// const expected: SeasonInfo = {
// season_id: 2,
// maxSeasonId: 2,
// start_date: new Date('1995-12-17T03:24:00'),
// end_date: null
// }
// const actual = service.fetchSeasonInfos();
//
// await expect(actual).resolves.toEqual(expected);
// });
}); });

View File

@@ -6,40 +6,35 @@ import {
UserStatsResponse, UserStatsResponse,
} from '../models/aliases'; } from '../models/aliases';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { seasons } from '@prisma/client';
@Injectable() @Injectable()
export class DatabaseService { export class DatabaseService {
// private host = process.env.MYSQL_HOST
// private port = process.env.MYSQL_PORT
// private credentials = {
// username: process.env.MYSQL_USERNAME,
// password: process.env.MYSQL_PASSWORD
// }
// private database = process.env.MYSQL_DATABASE
constructor( constructor(
private readonly prismaClient: PrismaService, private readonly prismaClient: PrismaService,
private readonly timetrackerPredicate: TimeTrackerPredicate, private readonly timetrackerPredicate: TimeTrackerPredicate,
) {} ) {}
public fetchSeasonInfos = async (seasonId: string): Promise<SeasonInfo> => { public fetchSeasonInfos = async (seasonId?: string): Promise<SeasonInfo> => {
let seasons; let maxSeasonId: number;
return this.prismaClient.seasons
.findOne({ return this.fetchMaxSeasonId().then(value => {
where: { maxSeasonId = value;
// eslint-disable-next-line @typescript-eslint/camelcase return value;
season_id: Number(seasonId), })
}, .then(value => this.prismaClient.seasons.findUnique({
}) where: {
.then(result => (seasons = result)) season_id: (!seasonId ? value : seasonId)
.then(this.fetchMaxSeasonId) }
.then(result => { }) as Promise<seasons>)
seasons.maxSeasonId = result; .then((result: SeasonInfo) => {
return seasons; result.maxSeasonId = maxSeasonId;
}); return result;
});
}; };
public fetchStats = async (seasonId: string): Promise<UserStatsResponse> => { public fetchStats = async (seasonId?: string): Promise<UserStatsResponse> => {
let response; let response;
return this.fetchSeasonInfos(seasonId) return this.fetchSeasonInfos(seasonId)
.then(result => { .then(result => {
@@ -61,18 +56,6 @@ export class DatabaseService {
}); });
}; };
// public fetchStats = async (seasonId: string): Promise<TableEntry[]> => this.prismaClient.timetracker.findMany({
// include: {
// user: true,
// ranks: true
// },
// where: {
// // eslint-disable-next-line @typescript-eslint/camelcase
// season_id: Number(seasonId)
// }
// })
// .then(this.timetrackerPredicate.process);
fetchTimeTrackerStats: (string) => Promise<TimeTrackerStats[]> = ( fetchTimeTrackerStats: (string) => Promise<TimeTrackerStats[]> = (
seasonId: string, seasonId: string,
) => ) =>

View File

@@ -0,0 +1,71 @@
import { TimeTrackerPredicate } from './timetracking.predicate';
import { TimeTrackerStats } from '../models/aliases';
describe('TimeTrackerPredicate', () => {
let timeTrackerPredicate: TimeTrackerPredicate;
beforeEach(() => {
timeTrackerPredicate = new TimeTrackerPredicate();
});
it('should be defined', () => {
expect(timeTrackerPredicate).toBeDefined();
});
it('should process UserStats properly to TableEntry', () => {
const input: TimeTrackerStats[] = [
{
entry_id: 1,
user_uid: 'TEST_UUID1',
season_id: 1,
rank_id: 1,
time: 12345,
user: {
uid: 'TEST_UUID1',
name: 'Test User 1'
},
ranks: {
entry_id: 1,
season_id: 1,
rank_id: 1,
rank_name: 'Test Rank 1'
}
},
{
entry_id: 2,
user_uid: 'TEST_UUID2',
season_id: 1,
rank_id: 2,
time: 123455,
user: {
uid: 'TEST_UUID2',
name: 'Test User 2'
},
ranks: {
entry_id: 1,
season_id: 1,
rank_id: 2,
rank_name: 'Test Rank 2'
}
},
];
const expected = [
{
name: 'Test User 2',
onlineTime: "1d 10h 17m 35s",
rank: 'Test Rank 2',
rawTime: 123455
},
{
name: 'Test User 1',
onlineTime: "0d 3h 25m 45s",
rank: 'Test Rank 1',
rawTime: 12345
}
];
const actual = timeTrackerPredicate.process(input);
expect(actual).toEqual(expected);
})
});

View File

@@ -1,19 +1,17 @@
import { ranks, seasons, timetracker, user } from '@prisma/client';
import { Predicate } from 'src/api/predicate';
import { Sort } from 'src/api/sort';
import { TimeUtil } from 'src/api/timeutil';
import { TableEntry } from 'src/models/TableEntry';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TimeTrackerStats } from '../models/aliases';
type UserStats = timetracker & { user: user; ranks: ranks }; import { Predicate } from '../api/predicate';
import { TableEntry } from '../models/TableEntry';
import { TimeUtil } from '../api/timeutil';
import { Sort } from '../api/sort';
@Injectable() @Injectable()
export class TimeTrackerPredicate export class TimeTrackerPredicate
implements Predicate<UserStats[], TableEntry[]> { implements Predicate<TimeTrackerStats[], TableEntry[]> {
process(input: UserStats[]): TableEntry[] { process(input: TimeTrackerStats[]): TableEntry[] {
return input return input
.filter((userStats: UserStats) => userStats.time != null) .filter((userStats: TimeTrackerStats) => userStats.time != null)
.map((userStats: UserStats) => { .map((userStats: TimeTrackerStats) => {
return { return {
name: userStats.user.name, name: userStats.user.name,
rawTime: userStats.time, rawTime: userStats.time,

View File

@@ -1,7 +0,0 @@
import { LoggerMiddleware } from './logger.middleware';
describe('LoggerMiddleware', () => {
it('should be defined', () => {
expect(new LoggerMiddleware()).toBeDefined();
});
});

View File

@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from './prisma.service';
describe('PrismaService', () => {
let service: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PrismaService],
}).compile();
service = module.get<PrismaService>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});