Merge pull request 'Alpha Release 0.0.9' (#82) from dev into main
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details

Reviewed-on: #82
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
This commit is contained in:
Nicolai Ort 2021-01-08 19:52:55 +00:00
commit c66b06c2c9
56 changed files with 1989 additions and 63 deletions

View File

@ -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
View File

@ -134,4 +134,5 @@ build
*.sqlite-jurnal
/docs
lib
/oss-attribution
/oss-attribution
*.tmp

View File

@ -1,6 +1,6 @@
{
"name": "@odit/lfk-backend",
"version": "0.0.8",
"version": "0.0.9",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"author": {
@ -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",

View File

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

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

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

View File

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

View File

@ -29,7 +29,7 @@ export class UserController {
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions directly granted to them (if existing/associated).' })
async getAll() {
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
const users = await this.userRepository.find({ relations: ['permissions', 'groups'] });
const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
users.forEach(user => {
responseUsers.push(new ResponseUser(user));
});
@ -43,7 +43,7 @@ export class UserController {
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that only permissions granted directly to the user will show up here, not permissions inherited from groups.' })
async getOne(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
}

25
src/errors/ScanErrors.ts Normal file
View 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!"
}

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

View File

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

View File

@ -106,23 +106,6 @@ export class JwtUser {
this.refreshTokenCount = user.refreshTokenCount;
this.uuid = user.uuid;
this.profilePic = user.profilePic;
this.permissions = this.getPermissions(user);
}
/**
* Handels getting the permissions granted to this user (direct or indirect).
* @param user User which's permissions shall be gotten.
*/
public getPermissions(user: User): string[] {
let returnPermissions: string[] = new Array<string>();
for (let permission of user.permissions) {
returnPermissions.push(permission.toString());
}
for (let group of user.groups) {
for (let permission of group.permissions) {
returnPermissions.push(permission.toString());
}
}
return Array.from(new Set(returnPermissions));
this.permissions = user.allPermissions;
}
}

View File

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

View 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;

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

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

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

View File

@ -1,5 +1,5 @@
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid';
import { config } from '../../config';
@ -78,7 +78,13 @@ export class CreateUser {
@IsOptional()
groups?: number[] | number
//TODO: ProfilePics
/**
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsUrl()
@IsOptional()
profilePic?: string;
/**
* Converts this to a User entity.
@ -100,7 +106,9 @@ export class CreateUser {
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
newUser.enabled = this.enabled;
//TODO: ProfilePics
if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { newUser.profilePic = this.profilePic; }
return newUser;
}

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

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

View File

@ -1,5 +1,5 @@
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { UsernameOrEmailNeededError } from '../../errors/AuthError';
@ -87,7 +87,16 @@ export class UpdateUser {
groups?: UserGroup[]
/**
* Updates a provided User entity based on this.
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsUrl()
@IsOptional()
profilePic?: string;
/**
* Updates a user entity based on this.
* @param user The user that shall be updated.
*/
public async updateUser(user: User): Promise<User> {
user.email = this.email;
@ -106,7 +115,9 @@ export class UpdateUser {
user.lastname = this.lastname
user.phone = this.phone;
user.groups = await this.getGroups();
//TODO: ProfilePics
if (!this.profilePic) { user.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { user.profilePic = this.profilePic; }
return user;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUUID } from "class-validator";
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
import { ChildEntity, Column, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
@ -106,10 +106,10 @@ export class User extends Principal {
* The user's profile picture.
* We haven't decided yet if this will be a bas64 encoded image or just a link to the profile picture.
*/
@Column({ nullable: true, unique: false })
@Column({ nullable: false, unique: false })
@IsString()
@IsOptional()
profilePic?: string;
@IsUrl()
profilePic: string;
/**
* The last time the user requested a password reset.
@ -128,6 +128,26 @@ export class User extends Principal {
@OneToMany(() => UserAction, action => action.user, { nullable: true })
actions: UserAction[]
/**
* Resolves all permissions granted to this user through groups or directly to the string enum format.
*/
public get allPermissions(): string[] {
let returnPermissions: string[] = new Array<string>();
if (!this.permissions) { return returnPermissions; }
for (let permission of this.permissions) {
returnPermissions.push(permission.toString());
}
if (!this.groups) { return returnPermissions; }
for (let group of this.groups) {
for (let permission of group.permissions) {
returnPermissions.push(permission.toString());
}
}
return Array.from(new Set(returnPermissions));
}
/**
* Turns this entity into it's response class.
*/

View File

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

View File

@ -10,5 +10,7 @@ export enum PermissionTarget {
USERGROUP = 'USERGROUP',
PERMISSION = 'PERMISSION',
STATSCLIENT = 'STATSCLIENT',
DONOR = 'DONOR'
DONOR = 'DONOR',
SCAN = 'SCAN',
STATION = 'STATION'
}

View File

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

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

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

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

View File

@ -5,7 +5,6 @@ import {
IsOptional,
IsString
} from "class-validator";
import { Permission } from '../entities/Permission';
import { User } from '../entities/User';
import { UserGroup } from '../entities/UserGroup';
import { ResponsePrincipal } from './ResponsePrincipal';
@ -57,11 +56,10 @@ export class ResponseUser extends ResponsePrincipal {
enabled: boolean = true;
/**
* The user's profile pic.
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsOptional()
profilePic?: string;
profilePic: string;
/**
* The groups that the user is a part of.
@ -75,7 +73,7 @@ export class ResponseUser extends ResponsePrincipal {
*/
@IsArray()
@IsOptional()
permissions: Permission[];
permissions: string[];
/**
* Creates a ResponseUser object from a user.
@ -92,6 +90,7 @@ export class ResponseUser extends ResponsePrincipal {
this.enabled = user.enabled;
this.profilePic = user.profilePic;
this.groups = user.groups;
this.permissions = user.permissions;
this.permissions = user.allPermissions;
this.groups.forEach(function (g) { delete g.permissions });
}
}

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

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

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

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

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

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

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

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