diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index cddb677..9672de5 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -70,7 +70,6 @@ export class AuthController { if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) { refreshAuth.token = refresh_token; } - console.log(req.headers) let auth; try { auth = await refreshAuth.toAuth(); diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index 1f74193..f03718f 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -1,9 +1,9 @@ import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { EntityFromBody } from 'typeorm-routing-controllers-extensions'; -import { TrackIdsNotMatchingError, TrackNotFoundError } from "../errors/TrackErrors"; +import { TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors"; import { CreateTrack } from '../models/actions/CreateTrack'; +import { UpdateTrack } from '../models/actions/UpdateTrack'; import { Track } from '../models/entities/Track'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseTrack } from '../models/responses/ResponseTrack'; @@ -48,6 +48,7 @@ export class TrackController { @Post() @Authorized("TRACK:CREATE") @ResponseSchema(ResponseTrack) + @ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) @OpenAPI({ description: "Create a new track.
Please remember that the track\'s distance must be greater than 0." }) async post( @Body({ validate: true }) @@ -61,20 +62,21 @@ export class TrackController { @ResponseSchema(ResponseTrack) @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) @ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 }) + @ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) @OpenAPI({ description: "Update the track whose id you provided.
Please remember that ids can't be changed." }) - async put(@Param('id') id: number, @EntityFromBody() track: Track) { + async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) { let oldTrack = await this.trackRepository.findOne({ id: id }); if (!oldTrack) { throw new TrackNotFoundError(); } - if (oldTrack.id != track.id) { + if (oldTrack.id != updateTrack.id) { throw new TrackIdsNotMatchingError(); } + await this.trackRepository.save(await updateTrack.updateTrack(oldTrack)); - await this.trackRepository.save(track); - return new ResponseTrack(track); + return new ResponseTrack(await this.trackRepository.findOne({ id: id })); } @Delete('/:id') diff --git a/src/errors/TrackErrors.ts b/src/errors/TrackErrors.ts index 7d4cfa9..e3d1902 100644 --- a/src/errors/TrackErrors.ts +++ b/src/errors/TrackErrors.ts @@ -22,4 +22,15 @@ export class TrackIdsNotMatchingError extends NotAcceptableError { @IsString() message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed" +} + +/** + * Error to throw when a track's lap time is set to a negative value. + */ +export class TrackLapTimeCantBeNegativeError extends NotAcceptableError { + @IsString() + name = "TrackLapTimeCantBeNegativeError" + + @IsString() + message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null." } \ No newline at end of file diff --git a/src/models/actions/CreateTrack.ts b/src/models/actions/CreateTrack.ts index f04e55b..fdeae71 100644 --- a/src/models/actions/CreateTrack.ts +++ b/src/models/actions/CreateTrack.ts @@ -1,4 +1,5 @@ -import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator'; +import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator'; +import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors'; import { Track } from '../entities/Track'; /** @@ -19,6 +20,14 @@ export class CreateTrack { @IsPositive() distance: number; + /** + * The minimum time a runner should take to run a lap on this track (in seconds). + * Will be used for fraud detection. + */ + @IsInt() + @IsOptional() + minimumLapTime: number; + /** * Creates a new Track entity from this. */ @@ -27,6 +36,10 @@ export class CreateTrack { newTrack.name = this.name; newTrack.distance = this.distance; + newTrack.minimumLapTime = this.minimumLapTime; + if (this.minimumLapTime < 0) { + throw new TrackLapTimeCantBeNegativeError(); + } return newTrack; } diff --git a/src/models/actions/UpdateTrack.ts b/src/models/actions/UpdateTrack.ts new file mode 100644 index 0000000..bc64d54 --- /dev/null +++ b/src/models/actions/UpdateTrack.ts @@ -0,0 +1,50 @@ +import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator'; +import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors'; +import { Track } from '../entities/Track'; + +/** + * This class is used to update a Track entity (via put request). + */ +export class UpdateTrack { + /** + * The updated track's id. + * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to). + */ + @IsInt() + id: number; + + @IsString() + @IsNotEmpty() + name: string; + + /** + * The updated track's distance in meters (must be greater than 0). + */ + @IsInt() + @IsPositive() + distance: number; + + /** + * The minimum time a runner should take to run a lap on this track (in seconds). + * Will be used for fraud detection. + */ + @IsInt() + @IsOptional() + minimumLapTime: number; + + + /** + * Update a Track entity based on this. + * @param track The track that shall be updated. + */ + public updateTrack(track: Track): Track { + track.name = this.name; + track.distance = this.distance; + track.minimumLapTime = this.minimumLapTime; + if (this.minimumLapTime < 0) { + throw new TrackLapTimeCantBeNegativeError(); + } + + return track; + } +} \ No newline at end of file diff --git a/src/models/entities/Track.ts b/src/models/entities/Track.ts index 22544d0..df18762 100644 --- a/src/models/entities/Track.ts +++ b/src/models/entities/Track.ts @@ -1,6 +1,7 @@ import { IsInt, IsNotEmpty, + IsOptional, IsPositive, IsString } from "class-validator"; @@ -18,7 +19,7 @@ export class Track { */ @PrimaryGeneratedColumn() @IsInt() - id: number;; + id: number; /** * The track's name. @@ -38,6 +39,15 @@ export class Track { @IsPositive() distance: number; + /** + * The minimum time a runner should take to run a lap on this track (in seconds). + * Will be used for fraud detection. + */ + @Column({ nullable: true }) + @IsInt() + @IsOptional() + minimumLapTime?: number; + /** * Used to link scan stations to a certain track. * This makes the configuration of the scan stations easier. diff --git a/src/models/responses/ResponseTrack.ts b/src/models/responses/ResponseTrack.ts index 83da863..27ef813 100644 --- a/src/models/responses/ResponseTrack.ts +++ b/src/models/responses/ResponseTrack.ts @@ -1,4 +1,5 @@ -import { IsInt, IsString } from "class-validator"; +import { IsInt, IsOptional, IsString } from "class-validator"; +import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors'; import { Track } from '../entities/Track'; /** @@ -23,6 +24,14 @@ export class ResponseTrack { @IsInt() distance: number; + /** + * The minimum time a runner should take to run a lap on this track (in seconds). + * Will be used for fraud detection. + */ + @IsInt() + @IsOptional() + minimumLapTime?: number; + /** * Creates a ResponseTrack object from a track. * @param track The track the response shall be build for. @@ -31,5 +40,9 @@ export class ResponseTrack { this.id = track.id; this.name = track.name; this.distance = track.distance; + this.minimumLapTime = track.minimumLapTime; + if (this.minimumLapTime < 0) { + throw new TrackLapTimeCantBeNegativeError(); + } } } diff --git a/src/tests/tracks.spec.ts b/src/tests/tracks.spec.ts deleted file mode 100644 index 81e4850..0000000 --- a/src/tests/tracks.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import axios from 'axios'; -import { config } from '../config'; -const base = "http://localhost:" + config.internal_port - -let access_token; -let axios_config; - -beforeAll(async () => { - const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); - access_token = res.data["access_token"]; - axios_config = { - headers: { "authorization": "Bearer " + access_token }, - validateStatus: undefined - }; -}); - -describe('GET /api/tracks', () => { - it('basic get should return 200', async () => { - const res = await axios.get(base + '/api/tracks', axios_config); - expect(res.status).toEqual(200); - expect(res.headers['content-type']).toContain("application/json") - }); - it('correct distance input should return 200', async () => { - const res = await axios.post(base + '/api/tracks', { - "name": "string", - "distance": 400 - }, axios_config); - expect(res.status).toEqual(200); - expect(res.headers['content-type']).toContain("application/json") - }); -}); -// --------------- -describe('POST /api/tracks', () => { - it('illegal distance input should return 400', async () => { - const res = await axios.post(base + '/api/tracks', { - "name": "string", - "distance": -1 - }, axios_config); - expect(res.status).toEqual(400); - expect(res.headers['content-type']).toContain("application/json") - }); - it('correct distance input should return 200', async () => { - const res = await axios.post(base + '/api/tracks', { - "name": "string", - "distance": 400 - }, axios_config); - expect(res.status).toEqual(200); - expect(res.headers['content-type']).toContain("application/json") - }); -}); -// --------------- -describe('adding + getting tracks', () => { - it('correct distance input should return 200', async () => { - const res = await axios.post(base + '/api/tracks', { - "name": "string", - "distance": 1000 - }, axios_config); - expect(res.status).toEqual(200); - expect(res.headers['content-type']).toContain("application/json") - }); - it('check if track was added', async () => { - const res = await axios.get(base + '/api/tracks', axios_config); - expect(res.status).toEqual(200); - expect(res.headers['content-type']).toContain("application/json") - let added_track = res.data[res.data.length - 1] - delete added_track.id - expect(added_track).toEqual({ - "name": "string", - "distance": 1000 - }) - }); -}); -// --------------- -describe('adding + getting + updating', () => { - let added_track; - it('correct distance input should return 200', async () => { - const res = await axios.post(base + '/api/tracks', { - "name": "string", - "distance": 1500 - }, axios_config); - expect(res.status).toEqual(200); - expect(res.headers['content-type']).toContain("application/json"); - added_track = res.data; - }); - it('get should return 200', async () => { - const res1 = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); - expect(res1.status).toEqual(200); - expect(res1.headers['content-type']).toContain("application/json") - const compareTrack = res1.data; - expect(compareTrack).toEqual(added_track) - }) - it('get should return 200', async () => { - const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { - "id": added_track.id, - "name": "apitrack", - "distance": 5100 - }, axios_config); - expect(res2.status).toEqual(200); - expect(res2.headers['content-type']).toContain("application/json") - }) - it('get should return 200', async () => { - const res3 = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); - expect(res3.status).toEqual(200); - expect(res3.headers['content-type']).toContain("application/json") - let added_track2 = res3.data; - delete added_track2.id - expect(added_track2).toEqual({ - "name": "apitrack", - "distance": 5100 - }) - }); -}); diff --git a/src/tests/tracks/track_add.spec.ts b/src/tests/tracks/track_add.spec.ts new file mode 100644 index 0000000..7f39f8d --- /dev/null +++ b/src/tests/tracks/track_add.spec.ts @@ -0,0 +1,90 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + + +describe('POST /api/tracks illegally', () => { + it('no distance input should return 400', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "string", + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('illegal distance input should return 400', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "string", + "distance": -1 + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('negative minimum lap time input should return 406', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "string", + "distance": 200, + "minimumLapTime": -1 + }, axios_config); + expect(res.status).toEqual(406); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/tracks successfully', () => { + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "name": "testtrack", + "distance": 200, + "minimumLapTime": null + }) + }); + it('creating a track with all parameters (optional set to null) should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + "minimumLapTime": null + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "name": "testtrack", + "distance": 200, + "minimumLapTime": null + }) + }); + it('creating a track with all parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + "minimumLapTime": 123 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "name": "testtrack", + "distance": 200, + "minimumLapTime": 123 + }) + }); +}); diff --git a/src/tests/tracks/track_delete.spec.ts b/src/tests/tracks/track_delete.spec.ts new file mode 100644 index 0000000..cb7ae89 --- /dev/null +++ b/src/tests/tracks/track_delete.spec.ts @@ -0,0 +1,53 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +// --------------- +describe('DETELE track', () => { + let added_track + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_track = res.data + }); + it('delete track', async () => { + const res2 = await axios.delete(base + '/api/tracks/' + added_track.id, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + let deleted_track = res2.data + delete deleted_track.id; + expect(res2.data).toEqual({ + "name": "testtrack", + "distance": 200, + "minimumLapTime": null + }); + }); + it('check if track really was deleted', async () => { + const res3 = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); + expect(res3.status).toEqual(404); + expect(res3.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE track (non-existant)', () => { + it('delete', async () => { + const res2 = await axios.delete(base + '/api/tracks/0', axios_config); + expect(res2.status).toEqual(204); + }); +}); diff --git a/src/tests/tracks/track_update.spec.ts b/src/tests/tracks/track_update.spec.ts new file mode 100644 index 0000000..ab2b707 --- /dev/null +++ b/src/tests/tracks/track_update.spec.ts @@ -0,0 +1,98 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('adding + updating illegally', () => { + let added_track; + it('correct distance input should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "apitrack", + "distance": 1500 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_track = res.data; + }); + it('update with negative distance should return 400', async () => { + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, + "name": "apitrack", + "distance": -1 + }, axios_config); + expect(res2.status).toEqual(400); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('update with negative laptime should return 406', async () => { + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, + "name": "apitrack", + "distance": 2, + "minimumLapTime": -1 + }, axios_config); + expect(res2.status).toEqual(406); + expect(res2.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + updating successfilly', () => { + let added_track; + it('correct distance input should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "apitrack2", + "distance": 1500 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_track = res.data; + }); + it('valid name change should return 200', async () => { + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, + "name": "apitrackk", + "distance": 1500 + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('valid distance change should return 200', async () => { + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, + "name": "apitrack2", + "distance": 5100 + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('valid laptime change (to a set number) should return 200', async () => { + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, + "name": "apitrack2", + "distance": 5100, + "minimumLapTime": 3 + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('valid laptime change (to a set null) should return 200', async () => { + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, + "name": "apitrack2", + "distance": 5100, + "minimumLapTime": null + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); +}); diff --git a/src/tests/tracks/tracks_get.spec.ts b/src/tests/tracks/tracks_get.spec.ts new file mode 100644 index 0000000..1057034 --- /dev/null +++ b/src/tests/tracks/tracks_get.spec.ts @@ -0,0 +1,49 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('GET /api/tracks sucessfully', () => { + it('basic get should return 200', async () => { + const res = await axios.get(base + '/api/tracks', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/tracks illegally', () => { + it('get for non-existant track should return 404', async () => { + const res = await axios.get(base + '/api/tracks/-1', axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + getting tracks', () => { + let added_track; + it('correct distance input should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "string", + "distance": 1000 + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('check if track was added (no parameter validation)', async () => { + const res = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file