Merge pull request 'Added scan (station) apis feature/67-scan_apis' (#80) from feature/67-scan_apis into dev
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #80 closes #67
This commit is contained in:
commit
bf3ffae67c
@ -11,7 +11,7 @@ steps:
|
|||||||
- git checkout $DRONE_SOURCE_BRANCH
|
- git checkout $DRONE_SOURCE_BRANCH
|
||||||
- mv .env.ci .env
|
- mv .env.ci .env
|
||||||
- name: run tests
|
- name: run tests
|
||||||
image: node:alpine
|
image: node:14.15.1-alpine3.12
|
||||||
commands:
|
commands:
|
||||||
- yarn
|
- yarn
|
||||||
- yarn test:ci
|
- yarn test:ci
|
||||||
@ -39,7 +39,7 @@ steps:
|
|||||||
registry: registry.odit.services
|
registry: registry.odit.services
|
||||||
- name: run full license export
|
- name: run full license export
|
||||||
depends_on: ["clone"]
|
depends_on: ["clone"]
|
||||||
image: node:alpine
|
image: node:14.15.1-alpine3.12
|
||||||
commands:
|
commands:
|
||||||
- yarn
|
- yarn
|
||||||
- yarn licenses:export
|
- yarn licenses:export
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -134,4 +134,5 @@ build
|
|||||||
*.sqlite-jurnal
|
*.sqlite-jurnal
|
||||||
/docs
|
/docs
|
||||||
lib
|
lib
|
||||||
/oss-attribution
|
/oss-attribution
|
||||||
|
*.tmp
|
@ -40,7 +40,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"routing-controllers": "^0.9.0-alpha.6",
|
"routing-controllers": "^0.9.0-alpha.6",
|
||||||
"routing-controllers-openapi": "^2.1.0",
|
"routing-controllers-openapi": "^2.1.0",
|
||||||
"sqlite3": "^5.0.0",
|
"sqlite3": "5.0.0",
|
||||||
"typeorm": "^0.2.29",
|
"typeorm": "^0.2.29",
|
||||||
"typeorm-routing-controllers-extensions": "^0.2.0",
|
"typeorm-routing-controllers-extensions": "^0.2.0",
|
||||||
"typeorm-seeding": "^1.6.1",
|
"typeorm-seeding": "^1.6.1",
|
||||||
|
@ -48,7 +48,12 @@ const spec = routingControllersToSpec(
|
|||||||
"StatsApiToken": {
|
"StatsApiToken": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"scheme": "bearer",
|
"scheme": "bearer",
|
||||||
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients)."
|
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients). Only valid for obtaining stats."
|
||||||
|
},
|
||||||
|
"StationApiToken": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
110
src/controllers/ScanController.ts
Normal file
110
src/controllers/ScanController.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, UseBefore } from 'routing-controllers';
|
||||||
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
|
import { getConnectionManager, Repository } from 'typeorm';
|
||||||
|
import { RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||||
|
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
|
||||||
|
import ScanAuth from '../middlewares/ScanAuth';
|
||||||
|
import { CreateScan } from '../models/actions/CreateScan';
|
||||||
|
import { CreateTrackScan } from '../models/actions/CreateTrackScan';
|
||||||
|
import { UpdateScan } from '../models/actions/UpdateScan';
|
||||||
|
import { Scan } from '../models/entities/Scan';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseScan } from '../models/responses/ResponseScan';
|
||||||
|
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
|
||||||
|
|
||||||
|
@JsonController('/scans')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class ScanController {
|
||||||
|
private scanRepository: Repository<Scan>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.scanRepository = getConnectionManager().get().getRepository(Scan);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authorized("SCAN:GET")
|
||||||
|
@ResponseSchema(ResponseScan, { isArray: true })
|
||||||
|
@ResponseSchema(ResponseTrackScan, { isArray: true })
|
||||||
|
@OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' })
|
||||||
|
async getAll() {
|
||||||
|
let responseScans: ResponseScan[] = new Array<ResponseScan>();
|
||||||
|
const scans = await this.scanRepository.find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
|
||||||
|
scans.forEach(scan => {
|
||||||
|
responseScans.push(scan.toResponse());
|
||||||
|
});
|
||||||
|
return responseScans;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("SCAN:GET")
|
||||||
|
@ResponseSchema(ResponseScan)
|
||||||
|
@ResponseSchema(ResponseTrackScan)
|
||||||
|
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
|
||||||
|
@OnUndefined(ScanNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' })
|
||||||
|
async getOne(@Param('id') id: number) {
|
||||||
|
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'runner.scans', 'runner.scans.track'] })
|
||||||
|
if (!scan) { throw new ScanNotFoundError(); }
|
||||||
|
return scan.toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseBefore(ScanAuth)
|
||||||
|
@ResponseSchema(ResponseScan)
|
||||||
|
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||||
|
@OpenAPI({ description: 'Create a new scan. <br> Please remeber to provide the scan\'s runner\'s id and distance for normal scans.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
async post(@Body({ validate: true }) createScan: CreateScan) {
|
||||||
|
let scan = await createScan.toScan();
|
||||||
|
scan = await this.scanRepository.save(scan);
|
||||||
|
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/trackscans")
|
||||||
|
@UseBefore(ScanAuth)
|
||||||
|
@ResponseSchema(ResponseScan)
|
||||||
|
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||||
|
@OpenAPI({ description: 'Create a new track scan. <br> This is just a alias for posting /scans', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) {
|
||||||
|
return this.post(createScan);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("SCAN:UPDATE")
|
||||||
|
@ResponseSchema(ResponseScan)
|
||||||
|
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update the scan whose id you provided. <br> Please remember that ids can't be changed and distances must be positive." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) {
|
||||||
|
let oldScan = await this.scanRepository.findOne({ id: id });
|
||||||
|
|
||||||
|
if (!oldScan) {
|
||||||
|
throw new ScanNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldScan.id != scan.id) {
|
||||||
|
throw new ScanIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.scanRepository.save(await scan.updateScan(oldScan));
|
||||||
|
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("SCAN:DELETE")
|
||||||
|
@ResponseSchema(ResponseScan)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: 'Delete the scan whose id you provided. <br> If no scan with this id exists it will just return 204(no content).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let scan = await this.scanRepository.findOne({ id: id });
|
||||||
|
if (!scan) { return null; }
|
||||||
|
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ["runner"] });
|
||||||
|
|
||||||
|
await this.scanRepository.delete(scan);
|
||||||
|
return responseScan.toResponse();
|
||||||
|
}
|
||||||
|
}
|
108
src/controllers/ScanStationController.ts
Normal file
108
src/controllers/ScanStationController.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||||
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
|
import { getConnectionManager, Repository } from 'typeorm';
|
||||||
|
import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors';
|
||||||
|
import { TrackNotFoundError } from '../errors/TrackErrors';
|
||||||
|
import { CreateScanStation } from '../models/actions/CreateScanStation';
|
||||||
|
import { UpdateScanStation } from '../models/actions/UpdateScanStation';
|
||||||
|
import { ScanStation } from '../models/entities/ScanStation';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
|
||||||
|
import { ScanController } from './ScanController';
|
||||||
|
|
||||||
|
@JsonController('/stations')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class ScanStationController {
|
||||||
|
private stationRepository: Repository<ScanStation>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authorized("STATION:GET")
|
||||||
|
@ResponseSchema(ResponseScanStation, { isArray: true })
|
||||||
|
@OpenAPI({ description: 'Lists all stations. <br> This includes their associated tracks.' })
|
||||||
|
async getAll() {
|
||||||
|
let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>();
|
||||||
|
const stations = await this.stationRepository.find({ relations: ['track'] });
|
||||||
|
stations.forEach(station => {
|
||||||
|
responseStations.push(station.toResponse());
|
||||||
|
});
|
||||||
|
return responseStations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("STATION:GET")
|
||||||
|
@ResponseSchema(ResponseScanStation)
|
||||||
|
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
|
||||||
|
@OnUndefined(ScanStationNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Lists all information about the station whose id got provided. <br> This includes it\'s associated track.' })
|
||||||
|
async getOne(@Param('id') id: number) {
|
||||||
|
let scan = await this.stationRepository.findOne({ id: id }, { relations: ['track'] })
|
||||||
|
if (!scan) { throw new ScanStationNotFoundError(); }
|
||||||
|
return scan.toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("STATION:CREATE")
|
||||||
|
@ResponseSchema(ResponseScanStation)
|
||||||
|
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
|
||||||
|
@OpenAPI({ description: 'Create a new station. <br> Please remeber to provide the station\'s track\'s id. <br> Please also remember that the station key is only visibe on creation.' })
|
||||||
|
async post(@Body({ validate: true }) createStation: CreateScanStation) {
|
||||||
|
let newStation = await createStation.toEntity();
|
||||||
|
const station = await this.stationRepository.save(newStation);
|
||||||
|
let responseStation = (await this.stationRepository.findOne({ id: station.id }, { relations: ['track'] })).toResponse();
|
||||||
|
responseStation.key = newStation.cleartextkey;
|
||||||
|
return responseStation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("STATION:UPDATE")
|
||||||
|
@ResponseSchema(ResponseScanStation)
|
||||||
|
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(ScanStationIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update the station whose id you provided. <br> Please remember that only the description and enabled state can be changed." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) station: UpdateScanStation) {
|
||||||
|
let oldStation = await this.stationRepository.findOne({ id: id });
|
||||||
|
|
||||||
|
if (!oldStation) {
|
||||||
|
throw new ScanStationNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldStation.id != station.id) {
|
||||||
|
throw new ScanStationIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stationRepository.save(await station.updateStation(oldStation));
|
||||||
|
return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("STATION:DELETE")
|
||||||
|
@ResponseSchema(ResponseScanStation)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@ResponseSchema(ScanStationHasScansError, { statusCode: 406 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: 'Delete the station whose id you provided. <br> If no station with this id exists it will just return 204(no content). <br> If the station still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with/created by this station - please disable it instead).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let station = await this.stationRepository.findOne({ id: id });
|
||||||
|
if (!station) { return null; }
|
||||||
|
|
||||||
|
const stationScans = (await this.stationRepository.findOne({ id: station.id }, { relations: ["scans"] })).scans;
|
||||||
|
if (stationScans.length != 0 && !force) {
|
||||||
|
throw new ScanStationHasScansError();
|
||||||
|
}
|
||||||
|
const scanController = new ScanController;
|
||||||
|
for (let scan of stationScans) {
|
||||||
|
scanController.remove(scan.id, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });
|
||||||
|
await this.stationRepository.delete(station);
|
||||||
|
return responseStation.toResponse();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
import { getConnectionManager, Repository } from 'typeorm';
|
import { getConnectionManager, Repository } from 'typeorm';
|
||||||
import { TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
|
import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
|
||||||
import { CreateTrack } from '../models/actions/CreateTrack';
|
import { CreateTrack } from '../models/actions/CreateTrack';
|
||||||
import { UpdateTrack } from '../models/actions/UpdateTrack';
|
import { UpdateTrack } from '../models/actions/UpdateTrack';
|
||||||
import { Track } from '../models/entities/Track';
|
import { Track } from '../models/entities/Track';
|
||||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
import { ResponseTrack } from '../models/responses/ResponseTrack';
|
import { ResponseTrack } from '../models/responses/ResponseTrack';
|
||||||
|
import { ScanStationController } from './ScanStationController';
|
||||||
|
|
||||||
@JsonController('/tracks')
|
@JsonController('/tracks')
|
||||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
@ -85,10 +86,19 @@ export class TrackController {
|
|||||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
@OnUndefined(204)
|
@OnUndefined(204)
|
||||||
@OpenAPI({ description: "Delete the track whose id you provided. <br> If no track with this id exists it will just return 204(no content)." })
|
@OpenAPI({ description: "Delete the track whose id you provided. <br> If no track with this id exists it will just return 204(no content)." })
|
||||||
async remove(@Param("id") id: number) {
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
let track = await this.trackRepository.findOne({ id: id });
|
let track = await this.trackRepository.findOne({ id: id });
|
||||||
if (!track) { return null; }
|
if (!track) { return null; }
|
||||||
|
|
||||||
|
const trackStations = (await this.trackRepository.findOne({ id: id }, { relations: ["stations"] })).stations;
|
||||||
|
if (trackStations.length != 0 && !force) {
|
||||||
|
throw new TrackHasScanStationsError();
|
||||||
|
}
|
||||||
|
const scanController = new ScanStationController;
|
||||||
|
for (let station of trackStations) {
|
||||||
|
scanController.remove(station.id, force);
|
||||||
|
}
|
||||||
|
|
||||||
await this.trackRepository.delete(track);
|
await this.trackRepository.delete(track);
|
||||||
return new ResponseTrack(track);
|
return new ResponseTrack(track);
|
||||||
}
|
}
|
||||||
|
25
src/errors/ScanErrors.ts
Normal file
25
src/errors/ScanErrors.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a Scan couldn't be found.
|
||||||
|
*/
|
||||||
|
export class ScanNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "ScanNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Scan not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two Scans' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a Scan's id.
|
||||||
|
*/
|
||||||
|
export class ScanIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "ScanIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a Scan's id: This isn't allowed!"
|
||||||
|
}
|
36
src/errors/ScanStationErrors.ts
Normal file
36
src/errors/ScanStationErrors.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw, when a non-existant scan station get's loaded.
|
||||||
|
*/
|
||||||
|
export class ScanStationNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "ScanStationNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The scan station you provided couldn't be located in the system. \n Please check your request."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two scan stations' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a scan station's id.
|
||||||
|
*/
|
||||||
|
export class ScanStationIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "ScanStationIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a scan station's id: This isn't allowed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a station still has scans associated.
|
||||||
|
*/
|
||||||
|
export class ScanStationHasScansError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "ScanStationHasScansError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "This station still has scans associated with it. \n If you want to delete this station with all it's scans add `?force` to your query."
|
||||||
|
}
|
@ -33,4 +33,12 @@ export class TrackLapTimeCantBeNegativeError extends NotAcceptableError {
|
|||||||
|
|
||||||
@IsString()
|
@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."
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TrackHasScanStationsError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "TrackHasScanStationsError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "This track still has stations associated with it. \n If you want to delete this track with all it's stations and scans add `?force` to your query."
|
||||||
}
|
}
|
@ -39,7 +39,12 @@ export default async (app: Application) => {
|
|||||||
"StatsApiToken": {
|
"StatsApiToken": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"scheme": "bearer",
|
"scheme": "bearer",
|
||||||
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients)."
|
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients). Only valid for obtaining stats."
|
||||||
|
},
|
||||||
|
"StationApiToken": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
68
src/middlewares/ScanAuth.ts
Normal file
68
src/middlewares/ScanAuth.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { ScanStation } from '../models/entities/ScanStation';
|
||||||
|
import authchecker from './authchecker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This middleware handels the authentification of scan station api tokens.
|
||||||
|
* The tokens have to be provided via Bearer auth header.
|
||||||
|
* @param req Express request object.
|
||||||
|
* @param res Express response object.
|
||||||
|
* @param next Next function to call on success.
|
||||||
|
*/
|
||||||
|
const ScanAuth = async (req: Request, res: Response, next: () => void) => {
|
||||||
|
let provided_token: string = req.headers["authorization"];
|
||||||
|
if (provided_token == "" || provided_token === undefined || provided_token === null) {
|
||||||
|
res.status(401).send("No api token provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
provided_token = provided_token.replace("Bearer ", "");
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).send("No valid jwt or api token provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = "";
|
||||||
|
try {
|
||||||
|
prefix = provided_token.split(".")[0];
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (prefix == "" || prefix == undefined || prefix == null) {
|
||||||
|
res.status(401).send("Api token non-existant or invalid syntax.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix: prefix });
|
||||||
|
if (!station) {
|
||||||
|
let user_authorized = false;
|
||||||
|
try {
|
||||||
|
let action = { request: req, response: res, context: null, next: next }
|
||||||
|
user_authorized = await authchecker(action, ["SCAN:CREATE"]);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (user_authorized == false) {
|
||||||
|
res.status(401).send("Api token non-existant or invalid syntax.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (station.enabled == false) {
|
||||||
|
res.status(401).send("Station disabled.");
|
||||||
|
}
|
||||||
|
if (!(await argon2.verify(station.key, provided_token))) {
|
||||||
|
res.status(401).send("Api token invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default ScanAuth;
|
59
src/models/actions/CreateScan.ts
Normal file
59
src/models/actions/CreateScan.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
import { RunnerNotFoundError } from '../../errors/RunnerErrors';
|
||||||
|
import { Runner } from '../entities/Runner';
|
||||||
|
import { Scan } from '../entities/Scan';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create a new Scan entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export abstract class CreateScan {
|
||||||
|
/**
|
||||||
|
* The scan's associated runner.
|
||||||
|
* This is important to link ran distances to runners.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
runner: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the scan valid (for fraud reasons).
|
||||||
|
* The determination of validity will work differently for every child class.
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
valid?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's distance in meters.
|
||||||
|
* Can be set manually or derived from another object.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
public distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Scan entity from this.
|
||||||
|
*/
|
||||||
|
public async toScan(): Promise<Scan> {
|
||||||
|
let newScan = new Scan();
|
||||||
|
|
||||||
|
newScan.distance = this.distance;
|
||||||
|
newScan.valid = this.valid;
|
||||||
|
newScan.runner = await this.getRunner();
|
||||||
|
|
||||||
|
return newScan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a runner based on the runner id provided via this.runner.
|
||||||
|
*/
|
||||||
|
public async getRunner(): Promise<Runner> {
|
||||||
|
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
|
||||||
|
if (!runner) {
|
||||||
|
throw new RunnerNotFoundError();
|
||||||
|
}
|
||||||
|
return runner;
|
||||||
|
}
|
||||||
|
}
|
64
src/models/actions/CreateScanStation.ts
Normal file
64
src/models/actions/CreateScanStation.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import { TrackNotFoundError } from '../../errors/TrackErrors';
|
||||||
|
import { ScanStation } from '../entities/ScanStation';
|
||||||
|
import { Track } from '../entities/Track';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create a new StatsClient entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateScanStation {
|
||||||
|
/**
|
||||||
|
* The new station's description.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The station's associated track.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
track: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this station enabled?
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
enabled?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this to a ScanStation entity.
|
||||||
|
*/
|
||||||
|
public async toEntity(): Promise<ScanStation> {
|
||||||
|
let newStation: ScanStation = new ScanStation();
|
||||||
|
|
||||||
|
newStation.description = this.description;
|
||||||
|
newStation.enabled = this.enabled;
|
||||||
|
newStation.track = await this.getTrack();
|
||||||
|
|
||||||
|
let newUUID = uuid.v4().toUpperCase();
|
||||||
|
newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
|
||||||
|
newStation.key = await argon2.hash(newStation.prefix + "." + newUUID);
|
||||||
|
newStation.cleartextkey = newStation.prefix + "." + newUUID;
|
||||||
|
|
||||||
|
return newStation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's a track by it's id provided via this.track.
|
||||||
|
* Used to link the new station to a track.
|
||||||
|
*/
|
||||||
|
public async getTrack(): Promise<Track> {
|
||||||
|
const track = await getConnection().getRepository(Track).findOne({ id: this.track });
|
||||||
|
if (!track) {
|
||||||
|
throw new TrackNotFoundError();
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
}
|
84
src/models/actions/CreateTrackScan.ts
Normal file
84
src/models/actions/CreateTrackScan.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
import { RunnerNotFoundError } from '../../errors/RunnerErrors';
|
||||||
|
import { RunnerCard } from '../entities/RunnerCard';
|
||||||
|
import { ScanStation } from '../entities/ScanStation';
|
||||||
|
import { TrackScan } from '../entities/TrackScan';
|
||||||
|
import { CreateScan } from './CreateScan';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Scan entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateTrackScan extends CreateScan {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's associated track.
|
||||||
|
* This is used to determine the scan's distance.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
track: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runnerCard associated with the scan.
|
||||||
|
* This get's saved for documentation and management purposes.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
card: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scanning station that created the scan.
|
||||||
|
* Mainly used for logging and traceing back scans (or errors)
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
station: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Track entity from this.
|
||||||
|
*/
|
||||||
|
public async toScan(): Promise<TrackScan> {
|
||||||
|
let newScan: TrackScan = new TrackScan();
|
||||||
|
|
||||||
|
newScan.station = await this.getStation();
|
||||||
|
newScan.card = await this.getCard();
|
||||||
|
|
||||||
|
newScan.track = newScan.station.track;
|
||||||
|
newScan.runner = newScan.card.runner;
|
||||||
|
|
||||||
|
if (!newScan.runner) {
|
||||||
|
throw new RunnerNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
newScan.timestamp = new Date(Date.now()).toString();
|
||||||
|
newScan.valid = await this.validateScan(newScan);
|
||||||
|
|
||||||
|
return newScan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCard(): Promise<RunnerCard> {
|
||||||
|
const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] });
|
||||||
|
if (!track) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStation(): Promise<ScanStation> {
|
||||||
|
const track = await getConnection().getRepository(ScanStation).findOne({ id: this.card }, { relations: ["track"] });
|
||||||
|
if (!track) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validateScan(scan: TrackScan): Promise<boolean> {
|
||||||
|
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner }, relations: ["track"] });
|
||||||
|
if (scans.length == 0) { return true; }
|
||||||
|
|
||||||
|
const newestScan = scans[0];
|
||||||
|
if ((new Date(scan.timestamp).getTime() - new Date(newestScan.timestamp).getTime()) > scan.track.minimumLapTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
62
src/models/actions/UpdateScan.ts
Normal file
62
src/models/actions/UpdateScan.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
import { RunnerNotFoundError } from '../../errors/RunnerErrors';
|
||||||
|
import { Runner } from '../entities/Runner';
|
||||||
|
import { Scan } from '../entities/Scan';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a Scan entity (via put request)
|
||||||
|
*/
|
||||||
|
export abstract class UpdateScan {
|
||||||
|
/**
|
||||||
|
* The updated scan'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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated scan's associated runner.
|
||||||
|
* This is important to link ran distances to runners.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
runner: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the updated scan valid (for fraud reasons).
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
valid?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated scan's distance in meters.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
public distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a Scan entity based on this.
|
||||||
|
* @param scan The scan that shall be updated.
|
||||||
|
*/
|
||||||
|
public async updateScan(scan: Scan): Promise<Scan> {
|
||||||
|
scan.distance = this.distance;
|
||||||
|
scan.valid = this.valid;
|
||||||
|
scan.runner = await this.getRunner();
|
||||||
|
|
||||||
|
return scan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a runner based on the runner id provided via this.runner.
|
||||||
|
*/
|
||||||
|
public async getRunner(): Promise<Runner> {
|
||||||
|
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
|
||||||
|
if (!runner) {
|
||||||
|
throw new RunnerNotFoundError();
|
||||||
|
}
|
||||||
|
return runner;
|
||||||
|
}
|
||||||
|
}
|
39
src/models/actions/UpdateScanStation.ts
Normal file
39
src/models/actions/UpdateScanStation.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { ScanStation } from '../entities/ScanStation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a ScanStation entity (via put request)
|
||||||
|
*/
|
||||||
|
export class UpdateScanStation {
|
||||||
|
/**
|
||||||
|
* The updated station'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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated station's description.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this station enabled?
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
enabled?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a ScanStation entity based on this.
|
||||||
|
* @param station The station that shall be updated.
|
||||||
|
*/
|
||||||
|
public async updateStation(station: ScanStation): Promise<ScanStation> {
|
||||||
|
station.description = this.description;
|
||||||
|
station.enabled = this.enabled;
|
||||||
|
|
||||||
|
return station;
|
||||||
|
}
|
||||||
|
}
|
@ -80,4 +80,11 @@ export class Address {
|
|||||||
*/
|
*/
|
||||||
@OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true })
|
@OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true })
|
||||||
addressUsers: IAddressUser[];
|
addressUsers: IAddressUser[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,4 +39,11 @@ export class DistanceDonation extends Donation {
|
|||||||
}
|
}
|
||||||
return calculatedAmount;
|
return calculatedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,4 +32,11 @@ export abstract class Donation {
|
|||||||
* The exact implementation may differ for each type of donation.
|
* The exact implementation may differ for each type of donation.
|
||||||
*/
|
*/
|
||||||
abstract amount: number;
|
abstract amount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { IsBoolean } from "class-validator";
|
import { IsBoolean } from "class-validator";
|
||||||
import { ChildEntity, Column, OneToMany } from "typeorm";
|
import { ChildEntity, Column, OneToMany } from "typeorm";
|
||||||
|
import { ResponseDonor } from '../responses/ResponseDonor';
|
||||||
import { Donation } from './Donation';
|
import { Donation } from './Donation';
|
||||||
import { Participant } from "./Participant";
|
import { Participant } from "./Participant";
|
||||||
|
|
||||||
@ -22,4 +23,11 @@ export class Donor extends Participant {
|
|||||||
*/
|
*/
|
||||||
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
|
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
|
||||||
donations: Donation[];
|
donations: Donation[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseDonor {
|
||||||
|
return new ResponseDonor(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -16,4 +16,11 @@ export class FixedDonation extends Donation {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
@IsPositive()
|
@IsPositive()
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
@ -81,4 +81,11 @@ export class GroupContact implements IAddressUser {
|
|||||||
*/
|
*/
|
||||||
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
|
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
|
||||||
groups: RunnerGroup[];
|
groups: RunnerGroup[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
@ -12,4 +12,9 @@ export abstract class IAddressUser {
|
|||||||
|
|
||||||
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
|
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
|
||||||
address?: Address
|
address?: Address
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public abstract toResponse();
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
import { config } from '../../config';
|
import { config } from '../../config';
|
||||||
|
import { ResponseParticipant } from '../responses/ResponseParticipant';
|
||||||
import { Address } from "./Address";
|
import { Address } from "./Address";
|
||||||
import { IAddressUser } from './IAddressUser';
|
import { IAddressUser } from './IAddressUser';
|
||||||
|
|
||||||
@ -74,4 +75,9 @@ export abstract class Participant implements IAddressUser {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public abstract toResponse(): ResponseParticipant;
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { PermissionAction } from '../enums/PermissionAction';
|
import { PermissionAction } from '../enums/PermissionAction';
|
||||||
import { PermissionTarget } from '../enums/PermissionTargets';
|
import { PermissionTarget } from '../enums/PermissionTargets';
|
||||||
|
import { ResponsePermission } from '../responses/ResponsePermission';
|
||||||
import { Principal } from './Principal';
|
import { Principal } from './Principal';
|
||||||
/**
|
/**
|
||||||
* Defines the Permission entity.
|
* Defines the Permission entity.
|
||||||
@ -51,4 +52,11 @@ export class Permission {
|
|||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.target + ":" + this.action;
|
return this.target + ":" + this.action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponsePermission {
|
||||||
|
return new ResponsePermission(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { IsInt, IsNotEmpty } from "class-validator";
|
import { IsInt, IsNotEmpty } from "class-validator";
|
||||||
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
||||||
|
import { ResponseRunner } from '../responses/ResponseRunner';
|
||||||
import { DistanceDonation } from "./DistanceDonation";
|
import { DistanceDonation } from "./DistanceDonation";
|
||||||
import { Participant } from "./Participant";
|
import { Participant } from "./Participant";
|
||||||
import { RunnerCard } from "./RunnerCard";
|
import { RunnerCard } from "./RunnerCard";
|
||||||
@ -47,7 +48,7 @@ export class Runner extends Participant {
|
|||||||
* This is implemented here to avoid duplicate code in other files.
|
* This is implemented here to avoid duplicate code in other files.
|
||||||
*/
|
*/
|
||||||
public get validScans(): Scan[] {
|
public get validScans(): Scan[] {
|
||||||
return this.scans.filter(scan => { scan.valid === true });
|
return this.scans.filter(scan => scan.valid == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,4 +67,11 @@ export class Runner extends Participant {
|
|||||||
public get distanceDonationAmount(): number {
|
public get distanceDonationAmount(): number {
|
||||||
return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance;
|
return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseRunner {
|
||||||
|
return new ResponseRunner(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -57,4 +57,11 @@ export class RunnerCard {
|
|||||||
*/
|
*/
|
||||||
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||||
scans: TrackScan[];
|
scans: TrackScan[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
IsString
|
IsString
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
|
import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup';
|
||||||
import { GroupContact } from "./GroupContact";
|
import { GroupContact } from "./GroupContact";
|
||||||
import { Runner } from "./Runner";
|
import { Runner } from "./Runner";
|
||||||
|
|
||||||
@ -60,4 +61,9 @@ export abstract class RunnerGroup {
|
|||||||
public get distanceDonationAmount(): number {
|
public get distanceDonationAmount(): number {
|
||||||
return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public abstract toResponse(): ResponseRunnerGroup;
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { IsInt, IsOptional } from "class-validator";
|
import { IsInt, IsOptional } from "class-validator";
|
||||||
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
||||||
|
import { ResponseRunnerOrganisation } from '../responses/ResponseRunnerOrganisation';
|
||||||
import { Address } from './Address';
|
import { Address } from './Address';
|
||||||
import { IAddressUser } from './IAddressUser';
|
import { IAddressUser } from './IAddressUser';
|
||||||
import { Runner } from './Runner';
|
import { Runner } from './Runner';
|
||||||
@ -54,4 +55,11 @@ export class RunnerOrganisation extends RunnerGroup implements IAddressUser {
|
|||||||
public get distanceDonationAmount(): number {
|
public get distanceDonationAmount(): number {
|
||||||
return this.allRunners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
return this.allRunners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseRunnerOrganisation {
|
||||||
|
return new ResponseRunnerOrganisation(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { IsNotEmpty } from "class-validator";
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ChildEntity, ManyToOne } from "typeorm";
|
import { ChildEntity, ManyToOne } from "typeorm";
|
||||||
|
import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam';
|
||||||
import { RunnerGroup } from "./RunnerGroup";
|
import { RunnerGroup } from "./RunnerGroup";
|
||||||
import { RunnerOrganisation } from "./RunnerOrganisation";
|
import { RunnerOrganisation } from "./RunnerOrganisation";
|
||||||
|
|
||||||
@ -17,4 +18,11 @@ export class RunnerTeam extends RunnerGroup {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true })
|
@ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true })
|
||||||
parentGroup?: RunnerOrganisation;
|
parentGroup?: RunnerOrganisation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseRunnerTeam {
|
||||||
|
return new ResponseRunnerTeam(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
IsPositive
|
IsPositive
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
|
import { ResponseScan } from '../responses/ResponseScan';
|
||||||
import { Runner } from "./Runner";
|
import { Runner } from "./Runner";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,7 +15,7 @@ import { Runner } from "./Runner";
|
|||||||
*/
|
*/
|
||||||
@Entity()
|
@Entity()
|
||||||
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||||
export abstract class Scan {
|
export class Scan {
|
||||||
/**
|
/**
|
||||||
* Autogenerated unique id (primary key).
|
* Autogenerated unique id (primary key).
|
||||||
*/
|
*/
|
||||||
@ -30,14 +31,6 @@ export abstract class Scan {
|
|||||||
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
|
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
|
||||||
runner: Runner;
|
runner: Runner;
|
||||||
|
|
||||||
/**
|
|
||||||
* The scan's distance in meters.
|
|
||||||
* Can be set manually or derived from another object.
|
|
||||||
*/
|
|
||||||
@IsInt()
|
|
||||||
@IsPositive()
|
|
||||||
abstract distance: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the scan valid (for fraud reasons).
|
* Is the scan valid (for fraud reasons).
|
||||||
* The determination of validity will work differently for every child class.
|
* The determination of validity will work differently for every child class.
|
||||||
@ -46,4 +39,37 @@ export abstract class Scan {
|
|||||||
@Column()
|
@Column()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
valid: boolean = true;
|
valid: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's distance in meters.
|
||||||
|
* This is the "real" value used by "normal" scans..
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsInt()
|
||||||
|
private _distance?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's distance in meters.
|
||||||
|
* Can be set manually or derived from another object.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
public get distance(): number {
|
||||||
|
return this._distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's distance in meters.
|
||||||
|
* Can be set manually or derived from another object.
|
||||||
|
*/
|
||||||
|
public set distance(value: number) {
|
||||||
|
this._distance = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseScan {
|
||||||
|
return new ResponseScan(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
IsString
|
IsString
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { ResponseScanStation } from '../responses/ResponseScanStation';
|
||||||
import { Track } from "./Track";
|
import { Track } from "./Track";
|
||||||
import { TrackScan } from "./TrackScan";
|
import { TrackScan } from "./TrackScan";
|
||||||
|
|
||||||
@ -39,6 +40,14 @@ export class ScanStation {
|
|||||||
@ManyToOne(() => Track, track => track.stations, { nullable: false })
|
@ManyToOne(() => Track, track => track.stations, { nullable: false })
|
||||||
track: Track;
|
track: Track;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's api key prefix.
|
||||||
|
* This is used identitfy a client by it's api key.
|
||||||
|
*/
|
||||||
|
@Column({ unique: true })
|
||||||
|
@IsString()
|
||||||
|
prefix: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The station's api key.
|
* The station's api key.
|
||||||
* This is used to authorize a station against the api (not implemented yet).
|
* This is used to authorize a station against the api (not implemented yet).
|
||||||
@ -49,16 +58,30 @@ export class ScanStation {
|
|||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the station enabled (for fraud and setup reasons)?
|
* The client's api key in plain text.
|
||||||
* Default: true
|
* This will only be used to display the full key on creation and updates.
|
||||||
*/
|
*/
|
||||||
@Column()
|
@IsString()
|
||||||
@IsBoolean()
|
@IsOptional()
|
||||||
enabled: boolean = true;
|
cleartextkey?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to link track scans to a scan station.
|
* Used to link track scans to a scan station.
|
||||||
*/
|
*/
|
||||||
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||||
scans: TrackScan[];
|
scans: TrackScan[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this station enabled?
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseScanStation {
|
||||||
|
return new ResponseScanStation(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { IsInt, IsOptional, IsString } from "class-validator";
|
import { IsInt, IsOptional, IsString } from "class-validator";
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { ResponseStatsClient } from '../responses/ResponseStatsClient';
|
||||||
/**
|
/**
|
||||||
* Defines the StatsClient entity.
|
* Defines the StatsClient entity.
|
||||||
* StatsClients can be used to access the protected parts of the stats api (top runners, donators and so on).
|
* StatsClients can be used to access the protected parts of the stats api (top runners, donators and so on).
|
||||||
@ -45,4 +46,11 @@ export class StatsClient {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
cleartextkey?: string;
|
cleartextkey?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseStatsClient {
|
||||||
|
return new ResponseStatsClient(this);
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
IsString
|
IsString
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { ResponseTrack } from '../responses/ResponseTrack';
|
||||||
import { ScanStation } from "./ScanStation";
|
import { ScanStation } from "./ScanStation";
|
||||||
import { TrackScan } from "./TrackScan";
|
import { TrackScan } from "./TrackScan";
|
||||||
|
|
||||||
@ -61,4 +62,11 @@ export class Track {
|
|||||||
*/
|
*/
|
||||||
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||||
scans: TrackScan[];
|
scans: TrackScan[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseTrack {
|
||||||
|
return new ResponseTrack(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
IsPositive
|
IsPositive
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { ChildEntity, Column, ManyToOne } from "typeorm";
|
import { ChildEntity, Column, ManyToOne } from "typeorm";
|
||||||
|
import { ResponseTrackScan } from '../responses/ResponseTrackScan';
|
||||||
import { RunnerCard } from "./RunnerCard";
|
import { RunnerCard } from "./RunnerCard";
|
||||||
import { Scan } from "./Scan";
|
import { Scan } from "./Scan";
|
||||||
import { ScanStation } from "./ScanStation";
|
import { ScanStation } from "./ScanStation";
|
||||||
@ -59,4 +60,11 @@ export class TrackScan extends Scan {
|
|||||||
@IsDateString()
|
@IsDateString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse(): ResponseTrackScan {
|
||||||
|
return new ResponseTrackScan(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,4 +52,11 @@ export class UserAction {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
changed: string;
|
changed: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public toResponse() {
|
||||||
|
return new Error("NotImplemented");
|
||||||
|
}
|
||||||
}
|
}
|
@ -10,5 +10,7 @@ export enum PermissionTarget {
|
|||||||
USERGROUP = 'USERGROUP',
|
USERGROUP = 'USERGROUP',
|
||||||
PERMISSION = 'PERMISSION',
|
PERMISSION = 'PERMISSION',
|
||||||
STATSCLIENT = 'STATSCLIENT',
|
STATSCLIENT = 'STATSCLIENT',
|
||||||
DONOR = 'DONOR'
|
DONOR = 'DONOR',
|
||||||
|
SCAN = 'SCAN',
|
||||||
|
STATION = 'STATION'
|
||||||
}
|
}
|
@ -29,7 +29,8 @@ export class ResponseRunner extends ResponseParticipant {
|
|||||||
*/
|
*/
|
||||||
public constructor(runner: Runner) {
|
public constructor(runner: Runner) {
|
||||||
super(runner);
|
super(runner);
|
||||||
this.distance = runner.scans.filter(scan => { scan.valid === true }).reduce((sum, current) => sum + current.distance, 0);
|
if (!runner.scans) { this.distance = 0 }
|
||||||
|
else { this.distance = runner.validScans.reduce((sum, current) => sum + current.distance, 0); }
|
||||||
this.group = runner.group;
|
this.group = runner.group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
src/models/responses/ResponseScan.ts
Normal file
46
src/models/responses/ResponseScan.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { IsBoolean, IsInt, IsNotEmpty, IsPositive } from "class-validator";
|
||||||
|
import { Scan } from '../entities/Scan';
|
||||||
|
import { ResponseRunner } from './ResponseRunner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the scan response.
|
||||||
|
*/
|
||||||
|
export class ResponseScan {
|
||||||
|
/**
|
||||||
|
* The scans's id.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
id: number;;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's associated runner.
|
||||||
|
* This is important to link ran distances to runners.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
runner: ResponseRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the scan valid (for fraud reasons).
|
||||||
|
* The determination of validity will work differently for every child class.
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
valid: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scans's length/distance in meters.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ResponseScan object from a scan.
|
||||||
|
* @param scan The scan the response shall be build for.
|
||||||
|
*/
|
||||||
|
public constructor(scan: Scan) {
|
||||||
|
this.id = scan.id;
|
||||||
|
this.runner = scan.runner.toResponse();
|
||||||
|
this.distance = scan.distance;
|
||||||
|
this.valid = scan.valid;
|
||||||
|
}
|
||||||
|
}
|
70
src/models/responses/ResponseScanStation.ts
Normal file
70
src/models/responses/ResponseScanStation.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
|
||||||
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
|
|
||||||
|
IsNotEmpty,
|
||||||
|
|
||||||
|
IsObject,
|
||||||
|
|
||||||
|
IsOptional,
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { ScanStation } from '../entities/ScanStation';
|
||||||
|
import { ResponseTrack } from './ResponseTrack';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the statsClient response.
|
||||||
|
*/
|
||||||
|
export class ResponseScanStation {
|
||||||
|
/**
|
||||||
|
* The client's id.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's description.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's api key.
|
||||||
|
* Only visible on creation or regeneration.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's api key prefix.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
prefix: string;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsNotEmpty()
|
||||||
|
track: ResponseTrack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this station enabled?
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
enabled?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ResponseStatsClient object from a statsClient.
|
||||||
|
* @param client The statsClient the response shall be build for.
|
||||||
|
*/
|
||||||
|
public constructor(station: ScanStation) {
|
||||||
|
this.id = station.id;
|
||||||
|
this.description = station.description;
|
||||||
|
this.prefix = station.prefix;
|
||||||
|
this.key = "Only visible on creation.";
|
||||||
|
this.track = station.track;
|
||||||
|
this.enabled = station.enabled;
|
||||||
|
}
|
||||||
|
}
|
48
src/models/responses/ResponseTrackScan.ts
Normal file
48
src/models/responses/ResponseTrackScan.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { IsDateString, IsNotEmpty } from "class-validator";
|
||||||
|
import { RunnerCard } from '../entities/RunnerCard';
|
||||||
|
import { ScanStation } from '../entities/ScanStation';
|
||||||
|
import { TrackScan } from '../entities/TrackScan';
|
||||||
|
import { ResponseScan } from './ResponseScan';
|
||||||
|
import { ResponseTrack } from './ResponseTrack';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the trackScan response.
|
||||||
|
*/
|
||||||
|
export class ResponseTrackScan extends ResponseScan {
|
||||||
|
/**
|
||||||
|
* The scan's associated track.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
track: ResponseTrack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runnerCard associated with the scan.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
card: RunnerCard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scanning station that created the scan.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
station: ScanStation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's creation timestamp.
|
||||||
|
*/
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
timestamp: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ResponseTrackScan object from a scan.
|
||||||
|
* @param scan The trackSscan the response shall be build for.
|
||||||
|
*/
|
||||||
|
public constructor(scan: TrackScan) {
|
||||||
|
super(scan);
|
||||||
|
this.track = new ResponseTrack(scan.track);
|
||||||
|
this.card = scan.card;
|
||||||
|
this.station = scan.station;
|
||||||
|
this.timestamp = scan.timestamp;
|
||||||
|
}
|
||||||
|
}
|
231
src/tests/scans/scans_add.spec.ts
Normal file
231
src/tests/scans/scans_add.spec.ts
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
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/scans illegally', () => {
|
||||||
|
let added_org;
|
||||||
|
let added_runner;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('no input should return 400', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', null, axios_config);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('no distance should return 400', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id
|
||||||
|
}, 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/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": -1
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('invalid runner input should return 404', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": 999999999999999999999999,
|
||||||
|
"distance": 100
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(404);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('POST /api/scans successfully', () => {
|
||||||
|
let added_org;
|
||||||
|
let added_runner;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
delete res2.data.group;
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a scan with the minimum amount of parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"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({
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a valid scan should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": true
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a invalid scan should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": false
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('POST /api/scans successfully via scan station', () => {
|
||||||
|
let added_org;
|
||||||
|
let added_runner;
|
||||||
|
let added_track;
|
||||||
|
let added_station;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
delete res2.data.group;
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
added_track = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('creating a station with minimum parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id
|
||||||
|
}, axios_config);
|
||||||
|
added_station = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('creating a scan with the minimum amount of parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200
|
||||||
|
}, {
|
||||||
|
headers: { "authorization": "Bearer " + added_station.key },
|
||||||
|
validateStatus: undefined
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a valid scan should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": true
|
||||||
|
}, {
|
||||||
|
headers: { "authorization": "Bearer " + added_station.key },
|
||||||
|
validateStatus: undefined
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a invalid scan should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": false
|
||||||
|
}, {
|
||||||
|
headers: { "authorization": "Bearer " + added_station.key },
|
||||||
|
validateStatus: undefined
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 200,
|
||||||
|
"valid": false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
68
src/tests/scans/scans_delete.spec.ts
Normal file
68
src/tests/scans/scans_delete.spec.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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('DELETE scan', () => {
|
||||||
|
let added_org;
|
||||||
|
let added_runner;
|
||||||
|
let added_scan;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('correct distance and runner input should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 1000
|
||||||
|
}, axios_config);
|
||||||
|
added_scan = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('delete scan', async () => {
|
||||||
|
const res2 = await axios.delete(base + '/api/scans/' + added_scan.id, axios_config);
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
expect(res2.data).toEqual(added_scan);
|
||||||
|
});
|
||||||
|
it('check if scan really was deleted', async () => {
|
||||||
|
const res3 = await axios.get(base + '/api/scans/' + added_scan.id, axios_config);
|
||||||
|
expect(res3.status).toEqual(404);
|
||||||
|
expect(res3.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('DELETE scan (non-existant)', () => {
|
||||||
|
it('delete', async () => {
|
||||||
|
const res2 = await axios.delete(base + '/api/scans/0', axios_config);
|
||||||
|
expect(res2.status).toEqual(204);
|
||||||
|
});
|
||||||
|
});
|
69
src/tests/scans/scans_get.spec.ts
Normal file
69
src/tests/scans/scans_get.spec.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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/scans sucessfully', () => {
|
||||||
|
it('basic get should return 200', async () => {
|
||||||
|
const res = await axios.get(base + '/api/scans', axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('GET /api/scans illegally', () => {
|
||||||
|
it('get for non-existant track should return 404', async () => {
|
||||||
|
const res = await axios.get(base + '/api/scans/-1', axios_config);
|
||||||
|
expect(res.status).toEqual(404);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('adding + getting scans', () => {
|
||||||
|
let added_org;
|
||||||
|
let added_runner;
|
||||||
|
let added_scan;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('correct distance and runner input should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 1000
|
||||||
|
}, axios_config);
|
||||||
|
added_scan = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('check if scans was added (no parameter validation)', async () => {
|
||||||
|
const res = await axios.get(base + '/api/scans/' + added_scan.id, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
});
|
174
src/tests/scans/scans_update.spec.ts
Normal file
174
src/tests/scans/scans_update.spec.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
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_org;
|
||||||
|
let added_runner;
|
||||||
|
let added_scan;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
delete res2.data.group;
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a scan with the minimum amount of parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
added_scan = res.data;
|
||||||
|
});
|
||||||
|
it('updating empty should return 400', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, null, axios_config);
|
||||||
|
expect(res2.status).toEqual(400);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('updating with wrong id should return 406', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, {
|
||||||
|
"id": added_scan.id + 1,
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": added_scan.distance
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(406);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('update with negative distance should return 400', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, {
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": -1
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(400);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('update with invalid runner id should return 404', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, {
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": 9999999999999999999999999,
|
||||||
|
"distance": 123
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(404);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('adding + updating successfilly', () => {
|
||||||
|
let added_org;
|
||||||
|
let added_runner;
|
||||||
|
let added_runner2;
|
||||||
|
let added_scan;
|
||||||
|
it('creating a new org with just a name should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/organisations', {
|
||||||
|
"name": "test123"
|
||||||
|
}, axios_config);
|
||||||
|
added_org = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
delete res2.data.group;
|
||||||
|
added_runner = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('creating a scan with the minimum amount of parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/scans', {
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 200
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
added_scan = res.data;
|
||||||
|
});
|
||||||
|
it('valid distance update should return 200', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, {
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 100
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
expect(res2.data).toEqual({
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 100,
|
||||||
|
"valid": true
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('valid valid update should return 200', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, {
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner.id,
|
||||||
|
"distance": 100,
|
||||||
|
"valid": false
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
expect(res2.data).toEqual({
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner,
|
||||||
|
"distance": 100,
|
||||||
|
"valid": false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a new runner with only needed params should return 200', async () => {
|
||||||
|
const res2 = await axios.post(base + '/api/runners', {
|
||||||
|
"firstname": "first",
|
||||||
|
"lastname": "last",
|
||||||
|
"group": added_org.id
|
||||||
|
}, axios_config);
|
||||||
|
delete res2.data.group;
|
||||||
|
added_runner2 = res2.data;
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('valid runner update should return 200', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/scans/' + added_scan.id, {
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner2.id,
|
||||||
|
"distance": added_scan.distance
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json");
|
||||||
|
expect(res2.data).toEqual({
|
||||||
|
"id": added_scan.id,
|
||||||
|
"runner": added_runner2,
|
||||||
|
"distance": added_scan.distance,
|
||||||
|
"valid": added_scan.valid
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
113
src/tests/scanstations/scanstations_add.spec.ts
Normal file
113
src/tests/scanstations/scanstations_add.spec.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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/stations illegally', () => {
|
||||||
|
it('no track input should return 400', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"description": "string",
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('illegal track input should return 404', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"description": "string",
|
||||||
|
"track": -1
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('POST /api/stations successfully', () => {
|
||||||
|
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);
|
||||||
|
added_track = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('creating a station with minimum parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
delete res.data.prefix;
|
||||||
|
delete res.data.key;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"track": added_track,
|
||||||
|
"description": null,
|
||||||
|
"enabled": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a station with all parameters (optional set to true/empty) should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id,
|
||||||
|
"enabled": true,
|
||||||
|
"description": null
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
delete res.data.prefix;
|
||||||
|
delete res.data.key;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"track": added_track,
|
||||||
|
"description": null,
|
||||||
|
"enabled": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a disabled station with all parameters (optional set to true/empty) should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id,
|
||||||
|
"enabled": false,
|
||||||
|
"description": null
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
delete res.data.prefix;
|
||||||
|
delete res.data.key;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"track": added_track,
|
||||||
|
"description": null,
|
||||||
|
"enabled": false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('creating a station with all parameters (optional set) should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id,
|
||||||
|
"enabled": true,
|
||||||
|
"description": "test station for testing"
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.id;
|
||||||
|
delete res.data.prefix;
|
||||||
|
delete res.data.key;
|
||||||
|
expect(res.data).toEqual({
|
||||||
|
"track": added_track,
|
||||||
|
"description": "test station for testing",
|
||||||
|
"enabled": true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
58
src/tests/scanstations/scanstations_delete.spec.ts
Normal file
58
src/tests/scanstations/scanstations_delete.spec.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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('DELETE station', () => {
|
||||||
|
let added_track;
|
||||||
|
let added_station;
|
||||||
|
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);
|
||||||
|
added_track = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('creating a station with minimum parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id
|
||||||
|
}, axios_config);
|
||||||
|
added_station = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('delete station', async () => {
|
||||||
|
const res2 = await axios.delete(base + '/api/stations/' + added_station.id, axios_config);
|
||||||
|
expect(res2.status).toEqual(200);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
delete res2.data.key;
|
||||||
|
delete added_station.key;
|
||||||
|
expect(res2.data).toEqual(added_station);
|
||||||
|
});
|
||||||
|
it('check if station really was deleted', async () => {
|
||||||
|
const res3 = await axios.get(base + '/api/stations/' + added_station.id, axios_config);
|
||||||
|
expect(res3.status).toEqual(404);
|
||||||
|
expect(res3.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('DELETE station (non-existant)', () => {
|
||||||
|
it('delete', async () => {
|
||||||
|
const res2 = await axios.delete(base + '/api/stations/0', axios_config);
|
||||||
|
expect(res2.status).toEqual(204);
|
||||||
|
});
|
||||||
|
});
|
59
src/tests/scanstations/scanstations_get.spec.ts
Normal file
59
src/tests/scanstations/scanstations_get.spec.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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/stations sucessfully', () => {
|
||||||
|
it('basic get should return 200', async () => {
|
||||||
|
const res = await axios.get(base + '/api/stations', axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('GET /api/stations illegally', () => {
|
||||||
|
it('get for non-existant track should return 404', async () => {
|
||||||
|
const res = await axios.get(base + '/api/stations/-1', axios_config);
|
||||||
|
expect(res.status).toEqual(404);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('adding + getting stations', () => {
|
||||||
|
let added_track;
|
||||||
|
let added_station;
|
||||||
|
it('creating a track should return 200', async () => {
|
||||||
|
const res1 = await axios.post(base + '/api/tracks', {
|
||||||
|
"name": "test123",
|
||||||
|
"distance": 123
|
||||||
|
}, axios_config);
|
||||||
|
added_track = res1.data
|
||||||
|
expect(res1.status).toEqual(200);
|
||||||
|
expect(res1.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('correct description and track input for station creation return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id,
|
||||||
|
"description": "I am but a simple test."
|
||||||
|
}, axios_config);
|
||||||
|
added_station = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
it('check if station was added (no parameter validation)', async () => {
|
||||||
|
const res = await axios.get(base + '/api/stations/' + added_station.id, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
});
|
103
src/tests/scanstations/scanstations_update.spec.ts
Normal file
103
src/tests/scanstations/scanstations_update.spec.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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;
|
||||||
|
let added_station;
|
||||||
|
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);
|
||||||
|
added_track = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('creating a station with minimum parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id
|
||||||
|
}, axios_config);
|
||||||
|
added_station = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('updateing id should return 406', async () => {
|
||||||
|
const res2 = await axios.put(base + '/api/stations/' + added_station.id, {
|
||||||
|
"id": added_station.id + 1
|
||||||
|
}, axios_config);
|
||||||
|
expect(res2.status).toEqual(406);
|
||||||
|
expect(res2.headers['content-type']).toContain("application/json")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ---------------
|
||||||
|
describe('adding + updating successfilly', () => {
|
||||||
|
let added_track;
|
||||||
|
let added_station;
|
||||||
|
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);
|
||||||
|
added_track = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('creating a station with minimum parameters should return 200', async () => {
|
||||||
|
const res = await axios.post(base + '/api/stations', {
|
||||||
|
"track": added_track.id
|
||||||
|
}, axios_config);
|
||||||
|
added_station = res.data;
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
});
|
||||||
|
it('updateing nothing should return 200', async () => {
|
||||||
|
const res = await axios.put(base + '/api/stations/' + added_station.id, {
|
||||||
|
"id": added_station.id
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
delete res.data.key;
|
||||||
|
delete added_station.key;
|
||||||
|
expect(res.data).toEqual(added_station);
|
||||||
|
});
|
||||||
|
it('updateing description should return 200', async () => {
|
||||||
|
const res = await axios.put(base + '/api/stations/' + added_station.id, {
|
||||||
|
"id": added_station.id,
|
||||||
|
"description": "Hello there! General stationi you're a scanning one."
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
expect(res.data.description).toEqual("Hello there! General stationi you're a scanning one.");
|
||||||
|
});
|
||||||
|
it('updateing enabled to false should return 200', async () => {
|
||||||
|
const res = await axios.put(base + '/api/stations/' + added_station.id, {
|
||||||
|
"id": added_station.id,
|
||||||
|
"enabled": false
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
expect(res.data.enabled).toEqual(false);
|
||||||
|
});
|
||||||
|
it('updateing enabled to true should return 200', async () => {
|
||||||
|
const res = await axios.put(base + '/api/stations/' + added_station.id, {
|
||||||
|
"id": added_station.id,
|
||||||
|
"enabled": true
|
||||||
|
}, axios_config);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers['content-type']).toContain("application/json");
|
||||||
|
expect(res.data.enabled).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user