diff --git a/.drone.yml b/.drone.yml index f7393db..bee9001 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ steps: - git checkout $DRONE_SOURCE_BRANCH - mv .env.ci .env - name: run tests - image: node:alpine + image: node:14.15.1-alpine3.12 commands: - yarn - yarn test:ci @@ -39,7 +39,7 @@ steps: registry: registry.odit.services - name: run full license export depends_on: ["clone"] - image: node:alpine + image: node:14.15.1-alpine3.12 commands: - yarn - yarn licenses:export diff --git a/.gitignore b/.gitignore index 7ca4167..448b818 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,5 @@ build *.sqlite-jurnal /docs lib -/oss-attribution \ No newline at end of file +/oss-attribution +*.tmp \ No newline at end of file diff --git a/package.json b/package.json index 0c599d7..f552bec 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "reflect-metadata": "^0.1.13", "routing-controllers": "^0.9.0-alpha.6", "routing-controllers-openapi": "^2.1.0", - "sqlite3": "^5.0.0", + "sqlite3": "5.0.0", "typeorm": "^0.2.29", "typeorm-routing-controllers-extensions": "^0.2.0", "typeorm-seeding": "^1.6.1", diff --git a/scripts/openapi_export.ts b/scripts/openapi_export.ts index 5a97817..b72a369 100644 --- a/scripts/openapi_export.ts +++ b/scripts/openapi_export.ts @@ -48,7 +48,12 @@ const spec = routingControllersToSpec( "StatsApiToken": { "type": "http", "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." } } }, diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts new file mode 100644 index 0000000..ed7df91 --- /dev/null +++ b/src/controllers/ScanController.ts @@ -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; + + /** + * 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.
This includes the scan\'s runner\'s distance ran.' }) + async getAll() { + let responseScans: ResponseScan[] = new Array(); + 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.
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.
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.
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.
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(); + } +} diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts new file mode 100644 index 0000000..4e32971 --- /dev/null +++ b/src/controllers/ScanStationController.ts @@ -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; + + /** + * 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.
This includes their associated tracks.' }) + async getAll() { + let responseStations: ResponseScanStation[] = new Array(); + 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.
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.
Please remeber to provide the station\'s track\'s id.
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.
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.
If no station with this id exists it will just return 204(no content).
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(); + } +} diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index f03718f..094756c 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -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 { 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 { UpdateTrack } from '../models/actions/UpdateTrack'; import { Track } from '../models/entities/Track'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseTrack } from '../models/responses/ResponseTrack'; +import { ScanStationController } from './ScanStationController'; @JsonController('/tracks') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -85,10 +86,19 @@ export class TrackController { @ResponseSchema(ResponseEmpty, { statusCode: 204 }) @OnUndefined(204) @OpenAPI({ description: "Delete the track whose id you provided.
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 }); 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); return new ResponseTrack(track); } diff --git a/src/errors/ScanErrors.ts b/src/errors/ScanErrors.ts new file mode 100644 index 0000000..77894df --- /dev/null +++ b/src/errors/ScanErrors.ts @@ -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!" +} \ No newline at end of file diff --git a/src/errors/ScanStationErrors.ts b/src/errors/ScanStationErrors.ts new file mode 100644 index 0000000..c013cf5 --- /dev/null +++ b/src/errors/ScanStationErrors.ts @@ -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." +} diff --git a/src/errors/TrackErrors.ts b/src/errors/TrackErrors.ts index e3d1902..a07f643 100644 --- a/src/errors/TrackErrors.ts +++ b/src/errors/TrackErrors.ts @@ -33,4 +33,12 @@ export class TrackLapTimeCantBeNegativeError extends NotAcceptableError { @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." +} + +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." } \ No newline at end of file diff --git a/src/loaders/openapi.ts b/src/loaders/openapi.ts index c1f7744..7d8acfe 100644 --- a/src/loaders/openapi.ts +++ b/src/loaders/openapi.ts @@ -39,7 +39,12 @@ export default async (app: Application) => { "StatsApiToken": { "type": "http", "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." } } }, diff --git a/src/middlewares/ScanAuth.ts b/src/middlewares/ScanAuth.ts new file mode 100644 index 0000000..7b16420 --- /dev/null +++ b/src/middlewares/ScanAuth.ts @@ -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; \ No newline at end of file diff --git a/src/models/actions/CreateScan.ts b/src/models/actions/CreateScan.ts new file mode 100644 index 0000000..e0d0efc --- /dev/null +++ b/src/models/actions/CreateScan.ts @@ -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 { + 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 { + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } +} \ No newline at end of file diff --git a/src/models/actions/CreateScanStation.ts b/src/models/actions/CreateScanStation.ts new file mode 100644 index 0000000..5d93b7c --- /dev/null +++ b/src/models/actions/CreateScanStation.ts @@ -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 { + 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 { + const track = await getConnection().getRepository(Track).findOne({ id: this.track }); + if (!track) { + throw new TrackNotFoundError(); + } + return track; + } +} \ No newline at end of file diff --git a/src/models/actions/CreateTrackScan.ts b/src/models/actions/CreateTrackScan.ts new file mode 100644 index 0000000..2303352 --- /dev/null +++ b/src/models/actions/CreateTrackScan.ts @@ -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 { + 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 { + const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] }); + if (!track) { + throw new Error(); + } + return track; + } + + public async getStation(): Promise { + 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 { + 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; + } +} \ No newline at end of file diff --git a/src/models/actions/UpdateScan.ts b/src/models/actions/UpdateScan.ts new file mode 100644 index 0000000..00b375e --- /dev/null +++ b/src/models/actions/UpdateScan.ts @@ -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.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 { + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } +} \ No newline at end of file diff --git a/src/models/actions/UpdateScanStation.ts b/src/models/actions/UpdateScanStation.ts new file mode 100644 index 0000000..a8ebc6a --- /dev/null +++ b/src/models/actions/UpdateScanStation.ts @@ -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 { + station.description = this.description; + station.enabled = this.enabled; + + return station; + } +} \ No newline at end of file diff --git a/src/models/entities/Address.ts b/src/models/entities/Address.ts index 561808f..dbb7eb3 100644 --- a/src/models/entities/Address.ts +++ b/src/models/entities/Address.ts @@ -80,4 +80,11 @@ export class Address { */ @OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true }) addressUsers: IAddressUser[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/DistanceDonation.ts b/src/models/entities/DistanceDonation.ts index d263e90..6b9ba5d 100644 --- a/src/models/entities/DistanceDonation.ts +++ b/src/models/entities/DistanceDonation.ts @@ -39,4 +39,11 @@ export class DistanceDonation extends Donation { } return calculatedAmount; } + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/Donation.ts b/src/models/entities/Donation.ts index eea23b5..46d7d45 100644 --- a/src/models/entities/Donation.ts +++ b/src/models/entities/Donation.ts @@ -32,4 +32,11 @@ export abstract class Donation { * The exact implementation may differ for each type of donation. */ abstract amount: number; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/entities/Donor.ts b/src/models/entities/Donor.ts index 188ed09..01b365c 100644 --- a/src/models/entities/Donor.ts +++ b/src/models/entities/Donor.ts @@ -1,5 +1,6 @@ import { IsBoolean } from "class-validator"; import { ChildEntity, Column, OneToMany } from "typeorm"; +import { ResponseDonor } from '../responses/ResponseDonor'; import { Donation } from './Donation'; import { Participant } from "./Participant"; @@ -22,4 +23,11 @@ export class Donor extends Participant { */ @OneToMany(() => Donation, donation => donation.donor, { nullable: true }) donations: Donation[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseDonor { + return new ResponseDonor(this); + } } \ No newline at end of file diff --git a/src/models/entities/FixedDonation.ts b/src/models/entities/FixedDonation.ts index db53e2f..6a32066 100644 --- a/src/models/entities/FixedDonation.ts +++ b/src/models/entities/FixedDonation.ts @@ -16,4 +16,11 @@ export class FixedDonation extends Donation { @IsInt() @IsPositive() amount: number; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/entities/GroupContact.ts b/src/models/entities/GroupContact.ts index 4dbcc5e..f650259 100644 --- a/src/models/entities/GroupContact.ts +++ b/src/models/entities/GroupContact.ts @@ -81,4 +81,11 @@ export class GroupContact implements IAddressUser { */ @OneToMany(() => RunnerGroup, group => group.contact, { nullable: true }) groups: RunnerGroup[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/entities/IAddressUser.ts b/src/models/entities/IAddressUser.ts index 3a7762a..3d8eaf9 100644 --- a/src/models/entities/IAddressUser.ts +++ b/src/models/entities/IAddressUser.ts @@ -12,4 +12,9 @@ export abstract class IAddressUser { @ManyToOne(() => Address, address => address.addressUsers, { nullable: true }) address?: Address + + /** + * Turns this entity into it's response class. + */ + public abstract toResponse(); } diff --git a/src/models/entities/Participant.ts b/src/models/entities/Participant.ts index de13b5f..fa40a8f 100644 --- a/src/models/entities/Participant.ts +++ b/src/models/entities/Participant.ts @@ -9,6 +9,7 @@ import { } from "class-validator"; import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { config } from '../../config'; +import { ResponseParticipant } from '../responses/ResponseParticipant'; import { Address } from "./Address"; import { IAddressUser } from './IAddressUser'; @@ -74,4 +75,9 @@ export abstract class Participant implements IAddressUser { @IsOptional() @IsEmail() email?: string; + + /** + * Turns this entity into it's response class. + */ + public abstract toResponse(): ResponseParticipant; } \ No newline at end of file diff --git a/src/models/entities/Permission.ts b/src/models/entities/Permission.ts index 6a07312..4bba550 100644 --- a/src/models/entities/Permission.ts +++ b/src/models/entities/Permission.ts @@ -6,6 +6,7 @@ import { import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { PermissionAction } from '../enums/PermissionAction'; import { PermissionTarget } from '../enums/PermissionTargets'; +import { ResponsePermission } from '../responses/ResponsePermission'; import { Principal } from './Principal'; /** * Defines the Permission entity. @@ -51,4 +52,11 @@ export class Permission { public toString(): string { return this.target + ":" + this.action; } + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponsePermission { + return new ResponsePermission(this); + } } \ No newline at end of file diff --git a/src/models/entities/Runner.ts b/src/models/entities/Runner.ts index 5c51c11..bc6bc5a 100644 --- a/src/models/entities/Runner.ts +++ b/src/models/entities/Runner.ts @@ -1,5 +1,6 @@ import { IsInt, IsNotEmpty } from "class-validator"; import { ChildEntity, ManyToOne, OneToMany } from "typeorm"; +import { ResponseRunner } from '../responses/ResponseRunner'; import { DistanceDonation } from "./DistanceDonation"; import { Participant } from "./Participant"; import { RunnerCard } from "./RunnerCard"; @@ -47,7 +48,7 @@ export class Runner extends Participant { * This is implemented here to avoid duplicate code in other files. */ 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 { 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); + } } \ No newline at end of file diff --git a/src/models/entities/RunnerCard.ts b/src/models/entities/RunnerCard.ts index 5f48257..4ea40e2 100644 --- a/src/models/entities/RunnerCard.ts +++ b/src/models/entities/RunnerCard.ts @@ -57,4 +57,11 @@ export class RunnerCard { */ @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/RunnerGroup.ts b/src/models/entities/RunnerGroup.ts index b5bcfcd..5620aca 100644 --- a/src/models/entities/RunnerGroup.ts +++ b/src/models/entities/RunnerGroup.ts @@ -5,6 +5,7 @@ import { IsString } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; +import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup'; import { GroupContact } from "./GroupContact"; import { Runner } from "./Runner"; @@ -60,4 +61,9 @@ export abstract class RunnerGroup { public get distanceDonationAmount(): number { return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0); } + + /** + * Turns this entity into it's response class. + */ + public abstract toResponse(): ResponseRunnerGroup; } \ No newline at end of file diff --git a/src/models/entities/RunnerOrganisation.ts b/src/models/entities/RunnerOrganisation.ts index f0f8c78..ebae8fd 100644 --- a/src/models/entities/RunnerOrganisation.ts +++ b/src/models/entities/RunnerOrganisation.ts @@ -1,5 +1,6 @@ import { IsInt, IsOptional } from "class-validator"; import { ChildEntity, ManyToOne, OneToMany } from "typeorm"; +import { ResponseRunnerOrganisation } from '../responses/ResponseRunnerOrganisation'; import { Address } from './Address'; import { IAddressUser } from './IAddressUser'; import { Runner } from './Runner'; @@ -54,4 +55,11 @@ export class RunnerOrganisation extends RunnerGroup implements IAddressUser { public get distanceDonationAmount(): number { 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); + } } \ No newline at end of file diff --git a/src/models/entities/RunnerTeam.ts b/src/models/entities/RunnerTeam.ts index e84ee9b..ba4884d 100644 --- a/src/models/entities/RunnerTeam.ts +++ b/src/models/entities/RunnerTeam.ts @@ -1,5 +1,6 @@ import { IsNotEmpty } from "class-validator"; import { ChildEntity, ManyToOne } from "typeorm"; +import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam'; import { RunnerGroup } from "./RunnerGroup"; import { RunnerOrganisation } from "./RunnerOrganisation"; @@ -17,4 +18,11 @@ export class RunnerTeam extends RunnerGroup { @IsNotEmpty() @ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true }) parentGroup?: RunnerOrganisation; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseRunnerTeam { + return new ResponseRunnerTeam(this); + } } \ No newline at end of file diff --git a/src/models/entities/Scan.ts b/src/models/entities/Scan.ts index 1b66851..2424a0c 100644 --- a/src/models/entities/Scan.ts +++ b/src/models/entities/Scan.ts @@ -6,6 +6,7 @@ import { IsPositive } from "class-validator"; import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; +import { ResponseScan } from '../responses/ResponseScan'; import { Runner } from "./Runner"; /** @@ -14,7 +15,7 @@ import { Runner } from "./Runner"; */ @Entity() @TableInheritance({ column: { name: "type", type: "varchar" } }) -export abstract class Scan { +export class Scan { /** * Autogenerated unique id (primary key). */ @@ -30,14 +31,6 @@ export abstract class Scan { @ManyToOne(() => Runner, runner => runner.scans, { nullable: false }) 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). * The determination of validity will work differently for every child class. @@ -46,4 +39,37 @@ export abstract class Scan { @Column() @IsBoolean() 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); + } } \ No newline at end of file diff --git a/src/models/entities/ScanStation.ts b/src/models/entities/ScanStation.ts index 1df5abf..38f7803 100644 --- a/src/models/entities/ScanStation.ts +++ b/src/models/entities/ScanStation.ts @@ -6,6 +6,7 @@ import { IsString } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseScanStation } from '../responses/ResponseScanStation'; import { Track } from "./Track"; import { TrackScan } from "./TrackScan"; @@ -39,6 +40,14 @@ export class ScanStation { @ManyToOne(() => Track, track => track.stations, { nullable: false }) 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. * This is used to authorize a station against the api (not implemented yet). @@ -49,16 +58,30 @@ export class ScanStation { key: string; /** - * Is the station enabled (for fraud and setup reasons)? - * Default: true + * The client's api key in plain text. + * This will only be used to display the full key on creation and updates. */ - @Column() - @IsBoolean() - enabled: boolean = true; + @IsString() + @IsOptional() + cleartextkey?: string; /** * Used to link track scans to a scan station. */ @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) 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); + } } diff --git a/src/models/entities/StatsClient.ts b/src/models/entities/StatsClient.ts index 493a8da..eb48cbf 100644 --- a/src/models/entities/StatsClient.ts +++ b/src/models/entities/StatsClient.ts @@ -1,5 +1,6 @@ import { IsInt, IsOptional, IsString } from "class-validator"; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseStatsClient } from '../responses/ResponseStatsClient'; /** * Defines the StatsClient entity. * 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() @IsOptional() cleartextkey?: string; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseStatsClient { + return new ResponseStatsClient(this); + } } \ No newline at end of file diff --git a/src/models/entities/Track.ts b/src/models/entities/Track.ts index df18762..5e172f7 100644 --- a/src/models/entities/Track.ts +++ b/src/models/entities/Track.ts @@ -6,6 +6,7 @@ import { IsString } from "class-validator"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseTrack } from '../responses/ResponseTrack'; import { ScanStation } from "./ScanStation"; import { TrackScan } from "./TrackScan"; @@ -61,4 +62,11 @@ export class Track { */ @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseTrack { + return new ResponseTrack(this); + } } diff --git a/src/models/entities/TrackScan.ts b/src/models/entities/TrackScan.ts index 45be379..8c4143b 100644 --- a/src/models/entities/TrackScan.ts +++ b/src/models/entities/TrackScan.ts @@ -6,6 +6,7 @@ import { IsPositive } from "class-validator"; import { ChildEntity, Column, ManyToOne } from "typeorm"; +import { ResponseTrackScan } from '../responses/ResponseTrackScan'; import { RunnerCard } from "./RunnerCard"; import { Scan } from "./Scan"; import { ScanStation } from "./ScanStation"; @@ -59,4 +60,11 @@ export class TrackScan extends Scan { @IsDateString() @IsNotEmpty() timestamp: string; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseTrackScan { + return new ResponseTrackScan(this); + } } diff --git a/src/models/entities/UserAction.ts b/src/models/entities/UserAction.ts index d22bd37..d598be0 100644 --- a/src/models/entities/UserAction.ts +++ b/src/models/entities/UserAction.ts @@ -52,4 +52,11 @@ export class UserAction { @IsOptional() @IsString() changed: string; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/enums/PermissionTargets.ts b/src/models/enums/PermissionTargets.ts index 728cf2a..dae9192 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -10,5 +10,7 @@ export enum PermissionTarget { USERGROUP = 'USERGROUP', PERMISSION = 'PERMISSION', STATSCLIENT = 'STATSCLIENT', - DONOR = 'DONOR' + DONOR = 'DONOR', + SCAN = 'SCAN', + STATION = 'STATION' } \ No newline at end of file diff --git a/src/models/responses/ResponseRunner.ts b/src/models/responses/ResponseRunner.ts index 5fae2ee..0d2fb67 100644 --- a/src/models/responses/ResponseRunner.ts +++ b/src/models/responses/ResponseRunner.ts @@ -29,7 +29,8 @@ export class ResponseRunner extends ResponseParticipant { */ public constructor(runner: 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; } } diff --git a/src/models/responses/ResponseScan.ts b/src/models/responses/ResponseScan.ts new file mode 100644 index 0000000..e666442 --- /dev/null +++ b/src/models/responses/ResponseScan.ts @@ -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; + } +} diff --git a/src/models/responses/ResponseScanStation.ts b/src/models/responses/ResponseScanStation.ts new file mode 100644 index 0000000..7d7dc48 --- /dev/null +++ b/src/models/responses/ResponseScanStation.ts @@ -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; + } +} diff --git a/src/models/responses/ResponseTrackScan.ts b/src/models/responses/ResponseTrackScan.ts new file mode 100644 index 0000000..724a8fd --- /dev/null +++ b/src/models/responses/ResponseTrackScan.ts @@ -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; + } +} diff --git a/src/tests/scans/scans_add.spec.ts b/src/tests/scans/scans_add.spec.ts new file mode 100644 index 0000000..c08ee01 --- /dev/null +++ b/src/tests/scans/scans_add.spec.ts @@ -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 + }); + }); +}); diff --git a/src/tests/scans/scans_delete.spec.ts b/src/tests/scans/scans_delete.spec.ts new file mode 100644 index 0000000..e88e812 --- /dev/null +++ b/src/tests/scans/scans_delete.spec.ts @@ -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); + }); +}); diff --git a/src/tests/scans/scans_get.spec.ts b/src/tests/scans/scans_get.spec.ts new file mode 100644 index 0000000..da7d737 --- /dev/null +++ b/src/tests/scans/scans_get.spec.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/src/tests/scans/scans_update.spec.ts b/src/tests/scans/scans_update.spec.ts new file mode 100644 index 0000000..b4ff86c --- /dev/null +++ b/src/tests/scans/scans_update.spec.ts @@ -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 + }); + }); +}); diff --git a/src/tests/scanstations/scanstations_add.spec.ts b/src/tests/scanstations/scanstations_add.spec.ts new file mode 100644 index 0000000..e4c3570 --- /dev/null +++ b/src/tests/scanstations/scanstations_add.spec.ts @@ -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 + }); + }); +}); diff --git a/src/tests/scanstations/scanstations_delete.spec.ts b/src/tests/scanstations/scanstations_delete.spec.ts new file mode 100644 index 0000000..ad425c7 --- /dev/null +++ b/src/tests/scanstations/scanstations_delete.spec.ts @@ -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); + }); +}); diff --git a/src/tests/scanstations/scanstations_get.spec.ts b/src/tests/scanstations/scanstations_get.spec.ts new file mode 100644 index 0000000..7280c31 --- /dev/null +++ b/src/tests/scanstations/scanstations_get.spec.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/src/tests/scanstations/scanstations_update.spec.ts b/src/tests/scanstations/scanstations_update.spec.ts new file mode 100644 index 0000000..6593a13 --- /dev/null +++ b/src/tests/scanstations/scanstations_update.spec.ts @@ -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); + }); +});