Merge pull request 'Minimum lap times for tracks feature/71-track_times' (#72) from feature/71-track_times into dev
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing

Reviewed-on: #72
closes #71
This commit is contained in:
Nicolai Ort 2021-01-03 17:17:59 +00:00
commit e7ab302c61
12 changed files with 398 additions and 122 deletions

View File

@ -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();

View File

@ -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. <br> 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. <br> 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')

View File

@ -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."
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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();
}
}
}

View File

@ -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
})
});
});

View File

@ -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
})
});
});

View File

@ -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);
});
});

View File

@ -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")
});
});

View File

@ -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");
});
});