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:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -134,4 +134,5 @@ build | ||||
| *.sqlite-jurnal | ||||
| /docs | ||||
| lib | ||||
| /oss-attribution | ||||
| /oss-attribution | ||||
| *.tmp | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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." | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|   | ||||
							
								
								
									
										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 { 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. <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 }); | ||||
| 		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); | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										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() | ||||
| 	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": { | ||||
|             "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." | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|   | ||||
							
								
								
									
										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 }) | ||||
|   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; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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. | ||||
|    */ | ||||
|   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 { 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); | ||||
|   } | ||||
| } | ||||
| @@ -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"); | ||||
|   } | ||||
| } | ||||
| @@ -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"); | ||||
|   } | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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"); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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"); | ||||
|   } | ||||
| } | ||||
| @@ -10,5 +10,7 @@ export enum PermissionTarget { | ||||
|     USERGROUP = 'USERGROUP', | ||||
|     PERMISSION = 'PERMISSION', | ||||
|     STATSCLIENT = 'STATSCLIENT', | ||||
|     DONOR = 'DONOR' | ||||
|     DONOR = 'DONOR', | ||||
|     SCAN = 'SCAN', | ||||
|     STATION = 'STATION' | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										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); | ||||
| 	}); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user