diff --git a/.drone.yml b/.drone.yml index 1fd9285..8b7504e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -19,10 +19,17 @@ get: path: odit-git-bot name: sshkey +--- +kind: secret +name: ci_token +get: + path: odit-ci-bot + name: apikey + --- kind: pipeline type: kubernetes -name: tests:node_latest +name: tests:node_14.15.1-alpine3.12 clone: disable: true steps: @@ -32,7 +39,7 @@ steps: - git clone $DRONE_REMOTE_URL . - git checkout $DRONE_SOURCE_BRANCH - name: run tests - image: node:latest + image: node:14.15.1-alpine3.12 commands: - yarn - yarn test:ci @@ -176,13 +183,13 @@ steps: settings: urls: https://ci.odit.services/api/repos/lfk/lfk-client-node/builds?SOURCE_TAG=${DRONE_TAG} bearer: - from_secret: BOT_DRONE_KEY + from_secret: ci_token - name: trigger js lib build image: idcooldi/drone-webhook settings: urls: https://ci.odit.services/api/repos/lfk/lfk-client-js/builds?SOURCE_TAG=${DRONE_TAG} bearer: - from_secret: BOT_DRONE_KEY + from_secret: ci_token trigger: event: - tag \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index bcb856b..ba2a93a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,12 @@ services: DB_PORT: bla DB_USER: bla DB_PASSWORD: bla - DB_NAME: dev.sqlite + DB_NAME: ./db.sqlite NODE_ENV: production + POSTALCODE_COUNTRYCODE: DE + SEED_TEST_DATA: "false" + MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer + MAILER_KEY: asdasd # APP_PORT: 4010 # DB_TYPE: postgres # DB_HOST: backend_db diff --git a/src/controllers/StatsController.ts b/src/controllers/StatsController.ts index ccc9527..eb359ce 100644 --- a/src/controllers/StatsController.ts +++ b/src/controllers/StatsController.ts @@ -1,4 +1,4 @@ -import { Get, JsonController, UseBefore } from 'routing-controllers'; +import { Get, JsonController, QueryParam, UseBefore } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnection } from 'typeorm'; import StatsAuth from '../middlewares/StatsAuth'; @@ -7,6 +7,7 @@ import { Runner } from '../models/entities/Runner'; import { RunnerOrganization } from '../models/entities/RunnerOrganization'; import { RunnerTeam } from '../models/entities/RunnerTeam'; import { Scan } from '../models/entities/Scan'; +import { TrackScan } from '../models/entities/TrackScan'; import { User } from '../models/entities/User'; import { ResponseStats } from '../models/responses/ResponseStats'; import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganization'; @@ -36,7 +37,10 @@ export class StatsController { @OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async getTopRunnersByDistance() { let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] }); - let topRunners = runners.sort((runner1, runner2) => runner1.distance - runner2.distance).slice(0, 9); + if (!runners || runners.length == 0) { + return []; + } + let topRunners = runners.sort((runner1, runner2) => runner2.distance - runner1.distance).slice(0, 10); let responseRunners: ResponseStatsRunner[] = new Array(); topRunners.forEach(runner => { responseRunners.push(new ResponseStatsRunner(runner)); @@ -49,8 +53,11 @@ export class StatsController { @ResponseSchema(ResponseStatsRunner, { isArray: true }) @OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async getTopRunnersByDonations() { - let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] }); - let topRunners = runners.sort((runner1, runner2) => runner1.distanceDonationAmount - runner2.distanceDonationAmount).slice(0, 9); + let runners = await getConnection().getRepository(Runner).find({ relations: ['group', 'distanceDonations', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }); + if (!runners || runners.length == 0) { + return []; + } + let topRunners = runners.sort((runner1, runner2) => runner2.distanceDonationAmount - runner1.distanceDonationAmount).slice(0, 10); let responseRunners: ResponseStatsRunner[] = new Array(); topRunners.forEach(runner => { responseRunners.push(new ResponseStatsRunner(runner)); @@ -58,6 +65,34 @@ export class StatsController { return responseRunners; } + @Get("/runners/laptime") + @UseBefore(StatsAuth) + @ResponseSchema(ResponseStatsRunner, { isArray: true }) + @OpenAPI({ description: "Returns the top ten runners by fastest laptime on your selected track (track by id).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) + async getTopRunnersByLaptime(@QueryParam("track") track: number) { + let scans = await getConnection().getRepository(TrackScan).find({ relations: ['track', 'runner', 'runner.group', 'runner.scans', 'runner.scans.track', 'runner.distanceDonations'] }); + if (!scans || scans.length == 0) { + return []; + } + scans = scans.filter((s) => { return s.track.id == track && s.valid == true && s.lapTime != 0 }).sort((scan1, scan2) => scan1.lapTime - scan2.lapTime); + + let topScans = new Array(); + let knownRunners = new Array(); + for (let i = 0; i < scans.length && topScans.length < 10; i++) { + const element = scans[i]; + if (!knownRunners.includes(element.runner.id)) { + topScans.push(element); + knownRunners.push(element.runner.id); + } + } + + let responseRunners: ResponseStatsRunner[] = new Array(); + topScans.forEach(scan => { + responseRunners.push(new ResponseStatsRunner(scan.runner, scan.lapTime)); + }); + return responseRunners; + } + @Get("/scans") @UseBefore(StatsAuth) @ResponseSchema(ResponseStatsRunner, { isArray: true }) @@ -71,8 +106,11 @@ export class StatsController { @ResponseSchema(ResponseStatsTeam, { isArray: true }) @OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async getTopTeamsByDistance() { - let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] }); - let topTeams = teams.sort((team1, team2) => team1.distance - team2.distance).slice(0, 9); + let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.scans.track'] }); + if (!teams || teams.length == 0) { + return []; + } + let topTeams = teams.sort((team1, team2) => team2.distance - team1.distance).slice(0, 10); let responseTeams: ResponseStatsTeam[] = new Array(); topTeams.forEach(team => { responseTeams.push(new ResponseStatsTeam(team)); @@ -85,8 +123,11 @@ export class StatsController { @ResponseSchema(ResponseStatsTeam, { isArray: true }) @OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async getTopTeamsByDonations() { - let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] }); - let topTeams = teams.sort((team1, team2) => team1.distanceDonationAmount - team2.distanceDonationAmount).slice(0, 9); + let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] }); + if (!teams || teams.length == 0) { + return []; + } + let topTeams = teams.sort((team1, team2) => team2.distanceDonationAmount - team1.distanceDonationAmount).slice(0, 10); let responseTeams: ResponseStatsTeam[] = new Array(); topTeams.forEach(team => { responseTeams.push(new ResponseStatsTeam(team)); @@ -100,7 +141,10 @@ export class StatsController { @OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async getTopOrgsByDistance() { let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] }); - let topOrgs = orgs.sort((org1, org2) => org1.distance - org2.distance).slice(0, 9); + if (!orgs || orgs.length == 0) { + return []; + } + let topOrgs = orgs.sort((org1, org2) => org2.distance - org1.distance).slice(0, 10); let responseOrgs: ResponseStatsOrgnisation[] = new Array(); topOrgs.forEach(org => { responseOrgs.push(new ResponseStatsOrgnisation(org)); @@ -113,8 +157,11 @@ export class StatsController { @ResponseSchema(ResponseStatsOrgnisation, { isArray: true }) @OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async getTopOrgsByDonations() { - let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] }); - let topOrgs = orgs.sort((org1, org2) => org1.distanceDonationAmount - org2.distanceDonationAmount).slice(0, 9); + let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.distanceDonations', 'runners.distanceDonations.runner', 'runners.distanceDonations.runner.scans', 'runners.distanceDonations.runner.scans.track', 'teams', 'teams.runners', 'teams.runners.distanceDonations', 'teams.runners.distanceDonations.runner', 'teams.runners.distanceDonations.runner.scans', 'teams.runners.distanceDonations.runner.scans.track'] }); + if (!orgs || orgs.length == 0) { + return []; + } + let topOrgs = orgs.sort((org1, org2) => org2.distanceDonationAmount - org1.distanceDonationAmount).slice(0, 10); let responseOrgs: ResponseStatsOrgnisation[] = new Array(); topOrgs.forEach(org => { responseOrgs.push(new ResponseStatsOrgnisation(org)); diff --git a/src/models/entities/RunnerGroup.ts b/src/models/entities/RunnerGroup.ts index 5620aca..332bd33 100644 --- a/src/models/entities/RunnerGroup.ts +++ b/src/models/entities/RunnerGroup.ts @@ -51,6 +51,9 @@ export abstract class RunnerGroup { */ @IsInt() public get distance(): number { + if (!this.runners || this.runners.length == 0) { + return 0; + } return this.runners.reduce((sum, current) => sum + current.distance, 0); } diff --git a/src/models/responses/ResponseStatsOrganization.ts b/src/models/responses/ResponseStatsOrganization.ts index 5339b55..89257a6 100644 --- a/src/models/responses/ResponseStatsOrganization.ts +++ b/src/models/responses/ResponseStatsOrganization.ts @@ -49,7 +49,15 @@ export class ResponseStatsOrgnisation implements IResponse { public constructor(org: RunnerOrganization) { this.name = org.name; this.id = org.id; - this.distance = org.distance; - this.donationAmount = org.distanceDonationAmount; + try { + this.distance = org.distance; + } catch { + this.distance = -1; + } + try { + this.donationAmount = org.distanceDonationAmount; + } catch { + this.donationAmount = -1; + } } } diff --git a/src/models/responses/ResponseStatsRunner.ts b/src/models/responses/ResponseStatsRunner.ts index 3aac437..6766330 100644 --- a/src/models/responses/ResponseStatsRunner.ts +++ b/src/models/responses/ResponseStatsRunner.ts @@ -1,6 +1,7 @@ import { IsInt, IsObject, + IsOptional, IsString } from "class-validator"; import { Runner } from '../entities/Runner'; @@ -55,6 +56,13 @@ export class ResponseStatsRunner implements IResponse { @IsInt() donationAmount: number; + /** + * The runner's fastest laptime in seconds. + */ + @IsInt() + @IsOptional() + minLaptime?: number; + /** * The runner's group. */ @@ -65,13 +73,28 @@ export class ResponseStatsRunner implements IResponse { * Creates a new runner stats response from a runner * @param runner The runner whoes response shall be generated - the following relations have to be resolved: scans, group, distanceDonations, scans.track */ - public constructor(runner: Runner) { + public constructor(runner: Runner, laptime?: number) { this.id = runner.id; this.firstname = runner.firstname; - this.middlename = runner.middlename; + if (runner.firstname) { + this.middlename = runner.middlename; + } this.lastname = runner.lastname; - this.distance = runner.distance; - this.donationAmount = runner.distanceDonationAmount; + try { + this.distance = runner.distance; + } + catch { + this.distance = -1; + } + try { + this.donationAmount = runner.distanceDonationAmount; + } + catch { + this.donationAmount = -1; + } + if (laptime) { + this.minLaptime = laptime; + } this.group = runner.group.toResponse(); } } diff --git a/src/models/responses/ResponseStatsTeam.ts b/src/models/responses/ResponseStatsTeam.ts index 82e4eb9..e887483 100644 --- a/src/models/responses/ResponseStatsTeam.ts +++ b/src/models/responses/ResponseStatsTeam.ts @@ -57,7 +57,15 @@ export class ResponseStatsTeam implements IResponse { this.name = team.name; this.id = team.id; this.parent = team.parentGroup.toResponse(); - this.distance = team.distance; - this.donationAmount = team.distanceDonationAmount; + try { + this.distance = team.distance; + } catch { + this.distance = -1; + } + try { + this.donationAmount = team.distanceDonationAmount; + } catch { + this.donationAmount = -1; + } } } diff --git a/src/tests/stats/stats_get.spec.ts b/src/tests/stats/stats_get.spec.ts new file mode 100644 index 0000000..2425f4a --- /dev/null +++ b/src/tests/stats/stats_get.spec.ts @@ -0,0 +1,89 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let axios_config_full; +let axios_config_stats; + +beforeAll(async () => { + jest.setTimeout(20000); + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + let access_token = res.data["access_token"]; + axios_config_full = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; + const res2 = await axios.post(base + '/api/statsclients', { username: "demo", password: "demo" }, axios_config_full); + access_token = res2.data["key"]; + axios_config_stats = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('GET /api/stats/runners/distance w/o auth should return 200', () => { + it('get with invalid token should return 401', async () => { + const res = await axios.get(base + '/api/stats/runners/distance', { + headers: { "authorization": "Bearer 123123123123123123" }, + validateStatus: undefined + }); + expect(res.status).toEqual(401); + }); +}); +// --------------- +describe('GET /api/stats should return 200', () => { + it('get w/o auth should return 200', async () => { + const res = await axios.get(base + '/api/stats', { validateStatus: undefined }); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('get w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); +// --------------- +describe('GET /api/stats/runners/* should return 200', () => { + it('get by distance w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/runners/distance', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('get by donations w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/runners/donations', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('get by laptime w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/runners/laptime', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); +// --------------- +describe('GET /api/stats/teams/* should return 200', () => { + it('get by distance w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/teams/distance', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('get by donations w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/teams/donations', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); +// --------------- +describe('GET /api/stats/organizations/* should return 200', () => { + it('get by distance w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/organizations/distance', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('get by donations w/ auth should return 200', async () => { + const res = await axios.get(base + '/api/stats/organizations/donations', axios_config_stats); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file