diff --git a/backend/package-lock.json b/backend/package-lock.json index c16f4c3..1b7f458 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -5988,6 +5988,15 @@ "@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": { "version": "1.2.1", "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", "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": { "version": "26.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index e357571..f71c008 100644 --- a/backend/package.json +++ b/backend/package.json @@ -45,6 +45,7 @@ "eslint-config-prettier": "^6.10.0", "eslint-plugin-import": "^2.20.1", "jest": "^25.1.0", + "jest-mock-extended": "^1.0.10", "prettier": "^1.19.1", "supertest": "^4.0.2", "ts-jest": "^26.1.1", diff --git a/backend/src/api/timeutil.spec.ts b/backend/src/api/timeutil.spec.ts new file mode 100644 index 0000000..6eae5c5 --- /dev/null +++ b/backend/src/api/timeutil.spec.ts @@ -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]); + } + }); +}); \ No newline at end of file diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/backend/src/app.controller.spec.ts +++ /dev/null @@ -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); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts index 8e91a5c..f35a87c 100644 --- a/backend/src/app.controller.ts +++ b/backend/src/app.controller.ts @@ -33,12 +33,9 @@ export class AppController { } @Get('/stats/season/:id') - async getStats(@Param('id') id: string): Promise { - return this.databaseService + async getStats(@Param('id') id?: string): Promise { + return await this.databaseService .fetchStats(id) - .then(value => { - return value; - }) .catch(err => { logger.error(`Error occured when fetching stats.`, err); throw new HttpException( diff --git a/backend/src/database/database.service.spec.ts b/backend/src/database/database.service.spec.ts index b806f31..a8f2995 100644 --- a/backend/src/database/database.service.spec.ts +++ b/backend/src/database/database.service.spec.ts @@ -1,13 +1,25 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { seasons } from '@prisma/client'; 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', () => { let service: DatabaseService; + const mockedPrismaService: MockProxy = mockDeep(); beforeEach(async () => { + mockReset(mockedPrismaService); + const module: TestingModule = await Test.createTestingModule({ - providers: [DatabaseService], - }).compile(); + providers: [DatabaseService, PrismaService, TimeTrackerPredicate], + }) + .overrideProvider(PrismaService) + .useValue(mockedPrismaService) + .compile(); service = module.get(DatabaseService); }); @@ -15,4 +27,43 @@ describe('DatabaseService', () => { it('should be defined', () => { 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); + // }); }); diff --git a/backend/src/database/database.service.ts b/backend/src/database/database.service.ts index 48f0e7a..1076660 100644 --- a/backend/src/database/database.service.ts +++ b/backend/src/database/database.service.ts @@ -6,40 +6,35 @@ import { UserStatsResponse, } from '../models/aliases'; import { PrismaService } from '../prisma/prisma.service'; +import { seasons } from '@prisma/client'; @Injectable() 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( private readonly prismaClient: PrismaService, private readonly timetrackerPredicate: TimeTrackerPredicate, ) {} - public fetchSeasonInfos = async (seasonId: string): Promise => { - let seasons; - return this.prismaClient.seasons - .findOne({ - where: { - // eslint-disable-next-line @typescript-eslint/camelcase - season_id: Number(seasonId), - }, - }) - .then(result => (seasons = result)) - .then(this.fetchMaxSeasonId) - .then(result => { - seasons.maxSeasonId = result; - return seasons; - }); + public fetchSeasonInfos = async (seasonId?: string): Promise => { + let maxSeasonId: number; + + return this.fetchMaxSeasonId().then(value => { + maxSeasonId = value; + return value; + }) + .then(value => this.prismaClient.seasons.findUnique({ + where: { + season_id: (!seasonId ? value : seasonId) + } + }) as Promise) + .then((result: SeasonInfo) => { + result.maxSeasonId = maxSeasonId; + return result; + }); }; - public fetchStats = async (seasonId: string): Promise => { + public fetchStats = async (seasonId?: string): Promise => { let response; return this.fetchSeasonInfos(seasonId) .then(result => { @@ -61,18 +56,6 @@ export class DatabaseService { }); }; - // public fetchStats = async (seasonId: string): Promise => 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 = ( seasonId: string, ) => diff --git a/backend/src/database/timetracking.predicate.spec.ts b/backend/src/database/timetracking.predicate.spec.ts new file mode 100644 index 0000000..dc28a1c --- /dev/null +++ b/backend/src/database/timetracking.predicate.spec.ts @@ -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); + }) +}); diff --git a/backend/src/database/timetracking.predicate.ts b/backend/src/database/timetracking.predicate.ts index e16aa09..893e80f 100644 --- a/backend/src/database/timetracking.predicate.ts +++ b/backend/src/database/timetracking.predicate.ts @@ -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'; - -type UserStats = timetracker & { user: user; ranks: ranks }; +import { TimeTrackerStats } from '../models/aliases'; +import { Predicate } from '../api/predicate'; +import { TableEntry } from '../models/TableEntry'; +import { TimeUtil } from '../api/timeutil'; +import { Sort } from '../api/sort'; @Injectable() export class TimeTrackerPredicate - implements Predicate { - process(input: UserStats[]): TableEntry[] { + implements Predicate { + process(input: TimeTrackerStats[]): TableEntry[] { return input - .filter((userStats: UserStats) => userStats.time != null) - .map((userStats: UserStats) => { + .filter((userStats: TimeTrackerStats) => userStats.time != null) + .map((userStats: TimeTrackerStats) => { return { name: userStats.user.name, rawTime: userStats.time, diff --git a/backend/src/logger.middleware.spec.ts b/backend/src/logger.middleware.spec.ts deleted file mode 100644 index 677d413..0000000 --- a/backend/src/logger.middleware.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LoggerMiddleware } from './logger.middleware'; - -describe('LoggerMiddleware', () => { - it('should be defined', () => { - expect(new LoggerMiddleware()).toBeDefined(); - }); -}); diff --git a/backend/src/prisma/prisma.service.spec.ts b/backend/src/prisma/prisma.service.spec.ts deleted file mode 100644 index a68cb9e..0000000 --- a/backend/src/prisma/prisma.service.spec.ts +++ /dev/null @@ -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); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -});