Alpha Release 0.0.7 #73
							
								
								
									
										22
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								.drone.yml
									
									
									
									
									
								
							| @@ -37,11 +37,23 @@ steps: | ||||
|       tags: | ||||
|         - dev | ||||
|       registry: registry.odit.services | ||||
|     when: | ||||
|       branch: | ||||
|         - dev | ||||
|       event: | ||||
|         - push | ||||
|   - name: run full license export | ||||
|     depends_on: ["clone"] | ||||
|     image: node:alpine | ||||
|     commands: | ||||
|       - yarn | ||||
|       - yarn licenses:export | ||||
|   - name: push new licenses file to repo | ||||
|     depends_on: ["run full license export"] | ||||
|     image: appleboy/drone-git-push | ||||
|     settings: | ||||
|       branch: dev | ||||
|       commit: true | ||||
|       commit_message: new license file version [CI SKIP] | ||||
|       author_email: bot@odit.services | ||||
|       remote: git@git.odit.services:lfk/backend.git | ||||
|       ssh_key: | ||||
|         from_secret: GITLAB_SSHKEY | ||||
|  | ||||
| trigger: | ||||
|   branch: | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -133,4 +133,5 @@ build | ||||
| *.sqlite | ||||
| *.sqlite-jurnal | ||||
| /docs | ||||
| lib | ||||
| lib | ||||
| /oss-attribution | ||||
							
								
								
									
										1296
									
								
								licenses.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1296
									
								
								licenses.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@odit/lfk-backend", | ||||
|   "version": "0.0.6", | ||||
|   "version": "0.0.7", | ||||
|   "main": "src/app.ts", | ||||
|   "repository": "https://git.odit.services/lfk/backend", | ||||
|   "author": { | ||||
| @@ -48,6 +48,7 @@ | ||||
|     "validator": "^13.5.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@odit/license-exporter": "^0.0.8", | ||||
|     "@types/cors": "^2.8.8", | ||||
|     "@types/csvtojson": "^1.1.5", | ||||
|     "@types/express": "^4.17.9", | ||||
| @@ -74,7 +75,8 @@ | ||||
|     "test:watch": "jest --watchAll", | ||||
|     "test:ci": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test", | ||||
|     "seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed", | ||||
|     "openapi:export": "ts-node src/openapi_export.ts" | ||||
|     "openapi:export": "node scripts/openapi_export.js", | ||||
|     "licenses:export": "license-exporter --md" | ||||
|   }, | ||||
|   "nodemonConfig": { | ||||
|     "ignore": [ | ||||
|   | ||||
| @@ -4,9 +4,9 @@ import fs from "fs"; | ||||
| import "reflect-metadata"; | ||||
| import { createExpressServer, getMetadataArgsStorage } from "routing-controllers"; | ||||
| import { routingControllersToSpec } from 'routing-controllers-openapi'; | ||||
| import { config } from './config'; | ||||
| import authchecker from "./middlewares/authchecker"; | ||||
| import { ErrorHandler } from './middlewares/ErrorHandler'; | ||||
| import { config } from '../src/config'; | ||||
| import authchecker from "../src/middlewares/authchecker"; | ||||
| import { ErrorHandler } from '../src/middlewares/ErrorHandler'; | ||||
| 
 | ||||
| const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts'; | ||||
| createExpressServer({ | ||||
| @@ -70,7 +70,6 @@ export class AuthController { | ||||
| 		if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) { | ||||
| 			refreshAuth.token = refresh_token; | ||||
| 		} | ||||
| 		console.log(req.headers) | ||||
| 		let auth; | ||||
| 		try { | ||||
| 			auth = await refreshAuth.toAuth(); | ||||
|   | ||||
							
								
								
									
										105
									
								
								src/controllers/DonorController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/controllers/DonorController.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| 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 { DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors'; | ||||
| import { CreateDonor } from '../models/actions/CreateDonor'; | ||||
| import { UpdateDonor } from '../models/actions/UpdateDonor'; | ||||
| import { Donor } from '../models/entities/Donor'; | ||||
| import { ResponseDonor } from '../models/responses/ResponseDonor'; | ||||
| import { ResponseEmpty } from '../models/responses/ResponseEmpty'; | ||||
|  | ||||
| @JsonController('/donors') | ||||
| @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) | ||||
| export class DonorController { | ||||
| 	private donorRepository: Repository<Donor>; | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the repository of this controller's model/entity. | ||||
| 	 */ | ||||
| 	constructor() { | ||||
| 		this.donorRepository = getConnectionManager().get().getRepository(Donor); | ||||
| 	} | ||||
|  | ||||
| 	@Get() | ||||
| 	@Authorized("DONOR:GET") | ||||
| 	@ResponseSchema(ResponseDonor, { isArray: true }) | ||||
| 	@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' }) | ||||
| 	async getAll() { | ||||
| 		let responseDonors: ResponseDonor[] = new Array<ResponseDonor>(); | ||||
| 		const donors = await this.donorRepository.find(); | ||||
| 		donors.forEach(donor => { | ||||
| 			responseDonors.push(new ResponseDonor(donor)); | ||||
| 		}); | ||||
| 		return responseDonors; | ||||
| 	} | ||||
|  | ||||
| 	@Get('/:id') | ||||
| 	@Authorized("DONOR:GET") | ||||
| 	@ResponseSchema(ResponseDonor) | ||||
| 	@ResponseSchema(DonorNotFoundError, { statusCode: 404 }) | ||||
| 	@OnUndefined(DonorNotFoundError) | ||||
| 	@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) | ||||
| 	async getOne(@Param('id') id: number) { | ||||
| 		let donor = await this.donorRepository.findOne({ id: id }) | ||||
| 		if (!donor) { throw new DonorNotFoundError(); } | ||||
| 		return new ResponseDonor(donor); | ||||
| 	} | ||||
|  | ||||
| 	@Post() | ||||
| 	@Authorized("DONOR:CREATE") | ||||
| 	@ResponseSchema(ResponseDonor) | ||||
| 	@OpenAPI({ description: 'Create a new runner. <br> Please remeber to provide the runner\'s group\'s id.' }) | ||||
| 	async post(@Body({ validate: true }) createRunner: CreateDonor) { | ||||
| 		let donor; | ||||
| 		try { | ||||
| 			donor = await createRunner.toDonor(); | ||||
| 		} catch (error) { | ||||
| 			throw error; | ||||
| 		} | ||||
|  | ||||
| 		donor = await this.donorRepository.save(donor) | ||||
| 		return new ResponseDonor(await this.donorRepository.findOne(donor)); | ||||
| 	} | ||||
|  | ||||
| 	@Put('/:id') | ||||
| 	@Authorized("DONOR:UPDATE") | ||||
| 	@ResponseSchema(ResponseDonor) | ||||
| 	@ResponseSchema(DonorNotFoundError, { statusCode: 404 }) | ||||
| 	@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 }) | ||||
| 	@OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." }) | ||||
| 	async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) { | ||||
| 		let oldDonor = await this.donorRepository.findOne({ id: id }); | ||||
|  | ||||
| 		if (!oldDonor) { | ||||
| 			throw new DonorNotFoundError(); | ||||
| 		} | ||||
|  | ||||
| 		if (oldDonor.id != donor.id) { | ||||
| 			throw new DonorIdsNotMatchingError(); | ||||
| 		} | ||||
|  | ||||
| 		await this.donorRepository.save(await donor.updateDonor(oldDonor)); | ||||
| 		return new ResponseDonor(await this.donorRepository.findOne({ id: id })); | ||||
| 	} | ||||
|  | ||||
| 	@Delete('/:id') | ||||
| 	@Authorized("DONOR:DELETE") | ||||
| 	@ResponseSchema(ResponseDonor) | ||||
| 	@ResponseSchema(ResponseEmpty, { statusCode: 204 }) | ||||
| 	@OnUndefined(204) | ||||
| 	@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' }) | ||||
| 	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { | ||||
| 		let donor = await this.donorRepository.findOne({ id: id }); | ||||
| 		if (!donor) { return null; } | ||||
| 		const responseDonor = await this.donorRepository.findOne(donor); | ||||
|  | ||||
| 		if (!donor) { | ||||
| 			throw new DonorNotFoundError(); | ||||
| 		} | ||||
|  | ||||
| 		//TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66) | ||||
|  | ||||
| 		await this.donorRepository.delete(donor); | ||||
| 		return new ResponseDonor(responseDonor); | ||||
| 	} | ||||
| } | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers'; | ||||
| import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody } from 'typeorm-routing-controllers-extensions'; | ||||
| import { TrackIdsNotMatchingError, TrackNotFoundError } from "../errors/TrackErrors"; | ||||
| import { TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors"; | ||||
| import { CreateTrack } from '../models/actions/CreateTrack'; | ||||
| import { UpdateTrack } from '../models/actions/UpdateTrack'; | ||||
| import { Track } from '../models/entities/Track'; | ||||
| import { ResponseEmpty } from '../models/responses/ResponseEmpty'; | ||||
| import { ResponseTrack } from '../models/responses/ResponseTrack'; | ||||
| @@ -48,6 +48,7 @@ export class TrackController { | ||||
| 	@Post() | ||||
| 	@Authorized("TRACK:CREATE") | ||||
| 	@ResponseSchema(ResponseTrack) | ||||
| 	@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) | ||||
| 	@OpenAPI({ description: "Create a new track. <br> Please remember that the track\'s distance must be greater than 0." }) | ||||
| 	async post( | ||||
| 		@Body({ validate: true }) | ||||
| @@ -61,20 +62,21 @@ export class TrackController { | ||||
| 	@ResponseSchema(ResponseTrack) | ||||
| 	@ResponseSchema(TrackNotFoundError, { statusCode: 404 }) | ||||
| 	@ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 }) | ||||
| 	@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) | ||||
| 	@OpenAPI({ description: "Update the track whose id you provided. <br> Please remember that ids can't be changed." }) | ||||
| 	async put(@Param('id') id: number, @EntityFromBody() track: Track) { | ||||
| 	async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) { | ||||
| 		let oldTrack = await this.trackRepository.findOne({ id: id }); | ||||
|  | ||||
| 		if (!oldTrack) { | ||||
| 			throw new TrackNotFoundError(); | ||||
| 		} | ||||
|  | ||||
| 		if (oldTrack.id != track.id) { | ||||
| 		if (oldTrack.id != updateTrack.id) { | ||||
| 			throw new TrackIdsNotMatchingError(); | ||||
| 		} | ||||
| 		await this.trackRepository.save(await updateTrack.updateTrack(oldTrack)); | ||||
|  | ||||
| 		await this.trackRepository.save(track); | ||||
| 		return new ResponseTrack(track); | ||||
| 		return new ResponseTrack(await this.trackRepository.findOne({ id: id })); | ||||
| 	} | ||||
|  | ||||
| 	@Delete('/:id') | ||||
|   | ||||
							
								
								
									
										36
									
								
								src/errors/DonorErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/errors/DonorErrors.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import { IsString } from 'class-validator'; | ||||
| import { NotAcceptableError, NotFoundError } from 'routing-controllers'; | ||||
|  | ||||
| /** | ||||
|  * Error to throw when a donor couldn't be found. | ||||
|  */ | ||||
| export class DonorNotFoundError extends NotFoundError { | ||||
| 	@IsString() | ||||
| 	name = "DonorNotFoundError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "Donor not found!" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when two donors' ids don't match. | ||||
|  * Usually occurs when a user tries to change a donor's id. | ||||
|  */ | ||||
| export class DonorIdsNotMatchingError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "DonorIdsNotMatchingError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "The ids don't match! \n And if you wanted to change a donor's id: This isn't allowed!" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when a donor needs a receipt, but no address is associated with them. | ||||
|  */ | ||||
| export class DonorReceiptAddressNeededError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "DonorReceiptAddressNeededError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "An address is needed to create a receipt for a donor. \n You didn't provide one." | ||||
| } | ||||
| @@ -22,4 +22,15 @@ export class TrackIdsNotMatchingError extends NotAcceptableError { | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when a track's lap time is set to a negative value. | ||||
|  */ | ||||
| export class TrackLapTimeCantBeNegativeError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "TrackLapTimeCantBeNegativeError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null." | ||||
| } | ||||
							
								
								
									
										38
									
								
								src/models/actions/CreateDonor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/models/actions/CreateDonor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors'; | ||||
| import { Donor } from '../entities/Donor'; | ||||
| import { CreateParticipant } from './CreateParticipant'; | ||||
|  | ||||
| /** | ||||
|  * This classed is used to create a new Donor entity from a json body (post request). | ||||
|  */ | ||||
| export class CreateDonor extends CreateParticipant { | ||||
|  | ||||
|     /** | ||||
|      * Does this donor need a receipt? | ||||
|      */ | ||||
|     @IsBoolean() | ||||
|     @IsOptional() | ||||
|     receiptNeeded?: boolean = false; | ||||
|  | ||||
|     /** | ||||
|      * Creates a new Donor entity from this. | ||||
|      */ | ||||
|     public async toDonor(): Promise<Donor> { | ||||
|         let newDonor: Donor = new Donor(); | ||||
|  | ||||
|         newDonor.firstname = this.firstname; | ||||
|         newDonor.middlename = this.middlename; | ||||
|         newDonor.lastname = this.lastname; | ||||
|         newDonor.phone = this.phone; | ||||
|         newDonor.email = this.email; | ||||
|         newDonor.address = await this.getAddress(); | ||||
|         newDonor.receiptNeeded = this.receiptNeeded; | ||||
|  | ||||
|         if (this.receiptNeeded == true && this.address == null) { | ||||
|             throw new DonorReceiptAddressNeededError() | ||||
|         } | ||||
|  | ||||
|         return newDonor; | ||||
|     } | ||||
| } | ||||
| @@ -41,7 +41,7 @@ export class CreateRunnerOrganisation extends CreateRunnerGroup { | ||||
|  | ||||
|         newRunnerOrganisation.name = this.name; | ||||
|         newRunnerOrganisation.contact = await this.getContact(); | ||||
|         newRunnerOrganisation.address = await this.getAddress(); | ||||
|         // newRunnerOrganisation.address = await this.getAddress(); | ||||
|  | ||||
|         return newRunnerOrganisation; | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator'; | ||||
| import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator'; | ||||
| import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors'; | ||||
| import { Track } from '../entities/Track'; | ||||
|  | ||||
| /** | ||||
| @@ -19,6 +20,14 @@ export class CreateTrack { | ||||
|     @IsPositive() | ||||
|     distance: number; | ||||
|  | ||||
|     /** | ||||
|      * The minimum time a runner should take to run a lap on this track (in seconds). | ||||
|      * Will be used for fraud detection. | ||||
|      */ | ||||
|     @IsInt() | ||||
|     @IsOptional() | ||||
|     minimumLapTime: number; | ||||
|  | ||||
|     /** | ||||
|      * Creates a new Track entity from this. | ||||
|      */ | ||||
| @@ -27,6 +36,10 @@ export class CreateTrack { | ||||
|  | ||||
|         newTrack.name = this.name; | ||||
|         newTrack.distance = this.distance; | ||||
|         newTrack.minimumLapTime = this.minimumLapTime; | ||||
|         if (this.minimumLapTime < 0) { | ||||
|             throw new TrackLapTimeCantBeNegativeError(); | ||||
|         } | ||||
|  | ||||
|         return newTrack; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										44
									
								
								src/models/actions/UpdateDonor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/models/actions/UpdateDonor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import { IsBoolean, IsInt, IsOptional } from 'class-validator'; | ||||
| import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors'; | ||||
| import { Donor } from '../entities/Donor'; | ||||
| import { CreateParticipant } from './CreateParticipant'; | ||||
|  | ||||
| /** | ||||
|  * This class is used to update a Donor entity (via put request). | ||||
|  */ | ||||
| export class UpdateDonor extends CreateParticipant { | ||||
|  | ||||
|     /** | ||||
|      * The updated donor'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; | ||||
|  | ||||
|     /** | ||||
|      * Does the updated donor need a receipt? | ||||
|      */ | ||||
|     @IsBoolean() | ||||
|     @IsOptional() | ||||
|     receiptNeeded?: boolean; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Updates a provided Donor entity based on this. | ||||
|      */ | ||||
|     public async updateDonor(donor: Donor): Promise<Donor> { | ||||
|         donor.firstname = this.firstname; | ||||
|         donor.middlename = this.middlename; | ||||
|         donor.lastname = this.lastname; | ||||
|         donor.phone = this.phone; | ||||
|         donor.email = this.email; | ||||
|         donor.receiptNeeded = this.receiptNeeded; | ||||
|         donor.address = await this.getAddress(); | ||||
|  | ||||
|         if (this.receiptNeeded == true && this.address == null) { | ||||
|             throw new DonorReceiptAddressNeededError() | ||||
|         } | ||||
|  | ||||
|         return donor; | ||||
|     } | ||||
| } | ||||
| @@ -45,7 +45,7 @@ export class UpdateRunnerOrganisation extends CreateRunnerGroup { | ||||
|  | ||||
|         organisation.name = this.name; | ||||
|         organisation.contact = await this.getContact(); | ||||
|         organisation.address = await this.getAddress(); | ||||
|         // organisation.address = await this.getAddress(); | ||||
|  | ||||
|         return organisation; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										50
									
								
								src/models/actions/UpdateTrack.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/models/actions/UpdateTrack.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator'; | ||||
| import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors'; | ||||
| import { Track } from '../entities/Track'; | ||||
|  | ||||
| /** | ||||
|  * This class is used to update a Track entity (via put request). | ||||
|  */ | ||||
| export class UpdateTrack { | ||||
|     /** | ||||
|      * The updated track's id. | ||||
|      * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to). | ||||
|      */ | ||||
|     @IsInt() | ||||
|     id: number; | ||||
|  | ||||
|     @IsString() | ||||
|     @IsNotEmpty() | ||||
|     name: string; | ||||
|  | ||||
|     /** | ||||
|      * The updated track's distance in meters (must be greater than 0). | ||||
|      */ | ||||
|     @IsInt() | ||||
|     @IsPositive() | ||||
|     distance: number; | ||||
|  | ||||
|     /** | ||||
|      * The minimum time a runner should take to run a lap on this track (in seconds). | ||||
|      * Will be used for fraud detection. | ||||
|      */ | ||||
|     @IsInt() | ||||
|     @IsOptional() | ||||
|     minimumLapTime: number; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Update a Track entity based on this. | ||||
|      * @param track The track that shall be updated. | ||||
|      */ | ||||
|     public updateTrack(track: Track): Track { | ||||
|         track.name = this.name; | ||||
|         track.distance = this.distance; | ||||
|         track.minimumLapTime = this.minimumLapTime; | ||||
|         if (this.minimumLapTime < 0) { | ||||
|             throw new TrackLapTimeCantBeNegativeError(); | ||||
|         } | ||||
|  | ||||
|         return track; | ||||
|     } | ||||
| } | ||||
| @@ -7,8 +7,7 @@ import { | ||||
| } from "class-validator"; | ||||
| import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; | ||||
| import { config } from '../../config'; | ||||
| import { Participant } from "./Participant"; | ||||
| import { RunnerOrganisation } from "./RunnerOrganisation"; | ||||
| import { IAddressUser } from './IAddressUser'; | ||||
|  | ||||
| /** | ||||
|  * Defines the Address entity. | ||||
| @@ -79,12 +78,6 @@ export class Address { | ||||
|   /** | ||||
|    * Used to link the address to participants. | ||||
|    */ | ||||
|   @OneToMany(() => Participant, participant => participant.address, { nullable: true }) | ||||
|   participants: Participant[]; | ||||
|  | ||||
|   /** | ||||
|    * Used to link the address to runner groups. | ||||
|    */ | ||||
|   @OneToMany(() => RunnerOrganisation, group => group.address, { nullable: true }) | ||||
|   groups: RunnerOrganisation[]; | ||||
|   @OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true }) | ||||
|   addressUsers: IAddressUser[]; | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { | ||||
|   IsNotEmpty | ||||
| } from "class-validator"; | ||||
| import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; | ||||
| import { Participant } from "./Participant"; | ||||
| import { Donor } from './Donor'; | ||||
|  | ||||
| /** | ||||
|  * Defines the Donation entity. | ||||
| @@ -24,8 +24,8 @@ export abstract class Donation { | ||||
|    * The donations's donor. | ||||
|    */ | ||||
|   @IsNotEmpty() | ||||
|   @ManyToOne(() => Participant, donor => donor.donations) | ||||
|   donor: Participant; | ||||
|   @ManyToOne(() => Donor, donor => donor.donations) | ||||
|   donor: Donor; | ||||
|  | ||||
|   /** | ||||
|    * The donation's amount in cents (or whatever your currency's smallest unit is.). | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { IsBoolean } from "class-validator"; | ||||
| import { ChildEntity, Column } from "typeorm"; | ||||
| import { ChildEntity, Column, OneToMany } from "typeorm"; | ||||
| import { Donation } from './Donation'; | ||||
| import { Participant } from "./Participant"; | ||||
|  | ||||
| /** | ||||
| @@ -14,4 +15,11 @@ export class Donor extends Participant { | ||||
|   @Column() | ||||
|   @IsBoolean() | ||||
|   receiptNeeded: boolean = false; | ||||
|  | ||||
|   /** | ||||
|  * Used to link the participant as the donor of a donation. | ||||
|  * Attention: Only runner's can be associated as a distanceDonations distance source. | ||||
|  */ | ||||
|   @OneToMany(() => Donation, donation => donation.donor, { nullable: true }) | ||||
|   donations: Donation[]; | ||||
| } | ||||
| @@ -10,6 +10,7 @@ import { | ||||
| import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; | ||||
| import { config } from '../../config'; | ||||
| import { Address } from "./Address"; | ||||
| import { IAddressUser } from './IAddressUser'; | ||||
| import { RunnerGroup } from "./RunnerGroup"; | ||||
|  | ||||
| /** | ||||
| @@ -17,7 +18,7 @@ import { RunnerGroup } from "./RunnerGroup"; | ||||
|  * Mainly it's own class to reduce duplicate code and enable contact's to be associated with multiple groups. | ||||
| */ | ||||
| @Entity() | ||||
| export class GroupContact { | ||||
| export class GroupContact implements IAddressUser { | ||||
|   /** | ||||
|    * Autogenerated unique id (primary key). | ||||
|    */ | ||||
| @@ -54,7 +55,7 @@ export class GroupContact { | ||||
|    * This is a address object to prevent any formatting differences. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @ManyToOne(() => Address, address => address.participants, { nullable: true }) | ||||
|   @ManyToOne(() => Address, address => address.addressUsers, { nullable: true }) | ||||
|   address?: Address; | ||||
|  | ||||
|   /** | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/models/entities/IAddressUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/models/entities/IAddressUser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { Entity, ManyToOne, PrimaryColumn } from 'typeorm'; | ||||
| import { Address } from './Address'; | ||||
|  | ||||
| /** | ||||
|  * The interface(tm) all entities using addresses have to implement. | ||||
|  * This is a abstract class, because apparently typeorm can't really work with interfaces :/ | ||||
|  */ | ||||
| @Entity() | ||||
| export abstract class IAddressUser { | ||||
|     @PrimaryColumn() | ||||
|     id: number; | ||||
|  | ||||
|     @ManyToOne(() => Address, address => address.addressUsers, { nullable: true }) | ||||
|     address?: Address | ||||
| } | ||||
| @@ -7,10 +7,10 @@ import { | ||||
|  | ||||
|   IsString | ||||
| } from "class-validator"; | ||||
| import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; | ||||
| import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; | ||||
| import { config } from '../../config'; | ||||
| import { Address } from "./Address"; | ||||
| import { Donation } from "./Donation"; | ||||
| import { IAddressUser } from './IAddressUser'; | ||||
|  | ||||
| /** | ||||
|  * Defines the Participant entity. | ||||
| @@ -18,7 +18,7 @@ import { Donation } from "./Donation"; | ||||
| */ | ||||
| @Entity() | ||||
| @TableInheritance({ column: { name: "type", type: "varchar" } }) | ||||
| export abstract class Participant { | ||||
| export abstract class Participant implements IAddressUser { | ||||
|   /** | ||||
|    * Autogenerated unique id (primary key). | ||||
|    */ | ||||
| @@ -54,7 +54,7 @@ export abstract class Participant { | ||||
|    * The participant's address. | ||||
|    * This is a address object to prevent any formatting differences. | ||||
|    */ | ||||
|   @ManyToOne(() => Address, address => address.participants, { nullable: true }) | ||||
|   @ManyToOne(() => Address, address => address.addressUsers, { nullable: true }) | ||||
|   address?: Address; | ||||
|  | ||||
|   /** | ||||
| @@ -74,11 +74,4 @@ export abstract class Participant { | ||||
|   @IsOptional() | ||||
|   @IsEmail() | ||||
|   email?: string; | ||||
|  | ||||
|   /** | ||||
|    * Used to link the participant as the donor of a donation. | ||||
|    * Attention: Only runner's can be associated as a distanceDonations distance source. | ||||
|    */ | ||||
|   @OneToMany(() => Donation, donation => donation.donor, { nullable: true }) | ||||
|   donations: Donation[]; | ||||
| } | ||||
| @@ -18,7 +18,7 @@ export class Runner extends Participant { | ||||
|    * Can be a runner team or organisation. | ||||
|    */ | ||||
|   @IsNotEmpty() | ||||
|   @ManyToOne(() => RunnerGroup, group => group.runners, { nullable: false }) | ||||
|   @ManyToOne(() => RunnerGroup, group => group.runners) | ||||
|   group: RunnerGroup; | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { IsInt, IsOptional } from "class-validator"; | ||||
| import { ChildEntity, ManyToOne, OneToMany } from "typeorm"; | ||||
| import { Address } from "./Address"; | ||||
| import { Address } from './Address'; | ||||
| import { IAddressUser } from './IAddressUser'; | ||||
| import { Runner } from './Runner'; | ||||
| import { RunnerGroup } from "./RunnerGroup"; | ||||
| import { RunnerTeam } from "./RunnerTeam"; | ||||
| @@ -10,13 +11,13 @@ import { RunnerTeam } from "./RunnerTeam"; | ||||
|  * This usually is a school, club or company. | ||||
| */ | ||||
| @ChildEntity() | ||||
| export class RunnerOrganisation extends RunnerGroup { | ||||
| export class RunnerOrganisation extends RunnerGroup implements IAddressUser { | ||||
|  | ||||
|   /** | ||||
|    * The organisations's address. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @ManyToOne(() => Address, address => address.groups, { nullable: true }) | ||||
|   @ManyToOne(() => Address, address => address.addressUsers, { nullable: true }) | ||||
|   address?: Address; | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { | ||||
|   IsInt, | ||||
|   IsNotEmpty, | ||||
|   IsOptional, | ||||
|   IsPositive, | ||||
|   IsString | ||||
| } from "class-validator"; | ||||
| @@ -18,7 +19,7 @@ export class Track { | ||||
|    */ | ||||
|   @PrimaryGeneratedColumn() | ||||
|   @IsInt() | ||||
|   id: number;; | ||||
|   id: number; | ||||
|  | ||||
|   /** | ||||
|    * The track's name. | ||||
| @@ -38,6 +39,15 @@ export class Track { | ||||
|   @IsPositive() | ||||
|   distance: number; | ||||
|  | ||||
|   /** | ||||
|    * The minimum time a runner should take to run a lap on this track (in seconds). | ||||
|    * Will be used for fraud detection. | ||||
|    */ | ||||
|   @Column({ nullable: true }) | ||||
|   @IsInt() | ||||
|   @IsOptional() | ||||
|   minimumLapTime?: number; | ||||
|  | ||||
|   /** | ||||
|    * Used to link scan stations to a certain track. | ||||
|    * This makes the configuration of the scan stations easier. | ||||
|   | ||||
| @@ -9,5 +9,6 @@ export enum PermissionTarget { | ||||
|     USER = 'USER', | ||||
|     USERGROUP = 'USERGROUP', | ||||
|     PERMISSION = 'PERMISSION', | ||||
|     STATSCLIENT = 'STATSCLIENT' | ||||
|     STATSCLIENT = 'STATSCLIENT', | ||||
|     DONOR = 'DONOR' | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/models/responses/ResponseDonor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/models/responses/ResponseDonor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { | ||||
|     IsBoolean | ||||
| } from "class-validator"; | ||||
| import { Donor } from '../entities/Donor'; | ||||
| import { ResponseParticipant } from './ResponseParticipant'; | ||||
|  | ||||
| /** | ||||
|  * Defines the donor response. | ||||
| */ | ||||
| export class ResponseDonor extends ResponseParticipant { | ||||
|  | ||||
|     /** | ||||
|      * Does this donor need a receipt? | ||||
|      */ | ||||
|     @IsBoolean() | ||||
|     receiptNeeded: boolean; | ||||
|  | ||||
|     /** | ||||
|      * Creates a ResponseRunner object from a runner. | ||||
|      * @param runner The user the response shall be build for. | ||||
|      */ | ||||
|     public constructor(donor: Donor) { | ||||
|         super(donor); | ||||
|         this.receiptNeeded = donor.receiptNeeded; | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { | ||||
|     IsArray, | ||||
|     IsNotEmpty, | ||||
|     IsObject | ||||
|  | ||||
|     IsObject, | ||||
|     IsOptional | ||||
| } from "class-validator"; | ||||
| import { Address } from '../entities/Address'; | ||||
| import { RunnerOrganisation } from '../entities/RunnerOrganisation'; | ||||
| @@ -17,7 +18,7 @@ export class ResponseRunnerOrganisation extends ResponseRunnerGroup { | ||||
|      * The runnerOrganisation's address. | ||||
|      */ | ||||
|     @IsObject() | ||||
|     @IsNotEmpty() | ||||
|     @IsOptional() | ||||
|     address?: Address; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { IsInt, IsString } from "class-validator"; | ||||
| import { IsInt, IsOptional, IsString } from "class-validator"; | ||||
| import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors'; | ||||
| import { Track } from '../entities/Track'; | ||||
|  | ||||
| /** | ||||
| @@ -23,6 +24,14 @@ export class ResponseTrack { | ||||
|     @IsInt() | ||||
|     distance: number; | ||||
|  | ||||
|     /** | ||||
|      * The minimum time a runner should take to run a lap on this track (in seconds). | ||||
|      * Will be used for fraud detection. | ||||
|      */ | ||||
|     @IsInt() | ||||
|     @IsOptional() | ||||
|     minimumLapTime?: number; | ||||
|  | ||||
|     /** | ||||
|      * Creates a ResponseTrack object from a track. | ||||
|      * @param track The track the response shall be build for. | ||||
| @@ -31,5 +40,9 @@ export class ResponseTrack { | ||||
|         this.id = track.id; | ||||
|         this.name = track.name; | ||||
|         this.distance = track.distance; | ||||
|         this.minimumLapTime = track.minimumLapTime; | ||||
|         if (this.minimumLapTime < 0) { | ||||
|             throw new TrackLapTimeCantBeNegativeError(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										94
									
								
								src/tests/donors/donor_add.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/tests/donors/donor_add.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| 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/donors with errors', () => { | ||||
|     it('creating a new donor without any parameters should return 400', async () => { | ||||
|         const res1 = await axios.post(base + '/api/donors', null, axios_config); | ||||
|         expect(res1.status).toEqual(400); | ||||
|         expect(res1.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('creating a new donor without a last name should return 400', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "middlename": "middle" | ||||
|         }, axios_config); | ||||
|         expect(res2.status).toEqual(400); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('creating a new donor with a invalid address should return 404', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "middlename": "middle", | ||||
|             "lastname": "last", | ||||
|             "address": 0 | ||||
|         }, axios_config); | ||||
|         expect(res2.status).toEqual(404); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('creating a new donor with a invalid phone number should return 400', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "middlename": "middle", | ||||
|             "lastname": "last", | ||||
|             "phone": "123" | ||||
|         }, axios_config); | ||||
|         expect(res2.status).toEqual(400); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('creating a new donor with a invalid mail address should return 400', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "string", | ||||
|             "middlename": "string", | ||||
|             "lastname": "string", | ||||
|             "phone": null, | ||||
|             "email": "123", | ||||
|         }, axios_config); | ||||
|         expect(res2.status).toEqual(400); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('creating a new donor without an address but with receiptNeeded=true 406', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "string", | ||||
|             "middlename": "string", | ||||
|             "lastname": "string", | ||||
|             "receiptNeeded": true | ||||
|         }, axios_config); | ||||
|         expect(res2.status).toEqual(406); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
| // --------------- | ||||
| describe('POST /api/donors working', () => { | ||||
|     it('creating a new donor with only needed params should return 200', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "lastname": "last" | ||||
|         }, axios_config); | ||||
|         expect(res2.status).toEqual(200); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('creating a new donor with all non-relationship optional params should return 200', async () => { | ||||
|         const res3 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "middlename": "middle", | ||||
|             "lastname": "last", | ||||
|             "receiptNeeded": false | ||||
|         }, axios_config); | ||||
|         expect(res3.status).toEqual(200); | ||||
|         expect(res3.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										47
									
								
								src/tests/donors/donor_delete.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/tests/donors/donor_delete.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| 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 + deletion (non-existant)', () => { | ||||
|     it('delete', async () => { | ||||
|         const res2 = await axios.delete(base + '/api/donors/0', axios_config); | ||||
|         expect(res2.status).toEqual(204); | ||||
|     }); | ||||
| }); | ||||
| // --------------- | ||||
| describe('add+delete', () => { | ||||
|     let added_donor; | ||||
|     it('creating a new donor with only needed params should return 200', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "lastname": "last" | ||||
|         }, axios_config); | ||||
|         added_donor = res2.data; | ||||
|         expect(res2.status).toEqual(200); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('delete donor', async () => { | ||||
|         const res3 = await axios.delete(base + '/api/donors/' + added_donor.id, axios_config); | ||||
|         expect(res3.status).toEqual(200); | ||||
|         expect(res3.headers['content-type']).toContain("application/json") | ||||
|         let deleted_runner = res3.data | ||||
|         expect(deleted_runner).toEqual(added_donor); | ||||
|     }); | ||||
|     it('check if donor really was deleted', async () => { | ||||
|         const res4 = await axios.get(base + '/api/donors/' + added_donor.id, axios_config); | ||||
|         expect(res4.status).toEqual(404); | ||||
|         expect(res4.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										57
									
								
								src/tests/donors/donor_get.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/tests/donors/donor_get.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| 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/donors', () => { | ||||
|     it('basic get should return 200', async () => { | ||||
|         const res = await axios.get(base + '/api/donors', axios_config); | ||||
|         expect(res.status).toEqual(200); | ||||
|         expect(res.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
| // --------------- | ||||
| describe('GET /api/donors/0', () => { | ||||
|     it('basic get should return 404', async () => { | ||||
|         const res = await axios.get(base + '/api/donors/0', axios_config); | ||||
|         expect(res.status).toEqual(404); | ||||
|         expect(res.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
| // --------------- | ||||
| describe('GET /api/donors after adding', () => { | ||||
|     let added_donor; | ||||
|     it('creating a new donor with only needed params should return 200', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "lastname": "last" | ||||
|         }, axios_config); | ||||
|         added_donor = res2.data; | ||||
|         expect(res2.status).toEqual(200); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('explicit get should return 200', async () => { | ||||
|         const res3 = await axios.get(base + '/api/donors/' + added_donor.id, axios_config); | ||||
|         expect(res3.status).toEqual(200); | ||||
|         expect(res3.headers['content-type']).toContain("application/json") | ||||
|         let gotten_donor = res3.data | ||||
|         expect(gotten_donor).toEqual(added_donor); | ||||
|     }); | ||||
|     it('get from all runners should return 200', async () => { | ||||
|         const res4 = await axios.get(base + '/api/donors/', axios_config); | ||||
|         expect(res4.status).toEqual(200); | ||||
|         expect(res4.headers['content-type']).toContain("application/json") | ||||
|         let gotten_donors = res4.data | ||||
|         expect(gotten_donors).toContainEqual(added_donor); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										75
									
								
								src/tests/donors/donor_update.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/tests/donors/donor_update.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| 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('Update donor name after adding', () => { | ||||
|     let added_donor; | ||||
|     it('creating a new runner with only needed params should return 200', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "lastname": "last" | ||||
|         }, axios_config); | ||||
|         added_donor = res2.data; | ||||
|         expect(res2.status).toEqual(200); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('valid update should return 200', async () => { | ||||
|         let donor_copy = added_donor | ||||
|         donor_copy.firstname = "second" | ||||
|         const res3 = await axios.put(base + '/api/donors/' + added_donor.id, donor_copy, axios_config); | ||||
|         expect(res3.status).toEqual(200); | ||||
|         expect(res3.headers['content-type']).toContain("application/json") | ||||
|         let updated_donor = res3.data | ||||
|         expect(updated_donor).toEqual(donor_copy); | ||||
|     }); | ||||
| }); | ||||
| // --------------- | ||||
| describe('Update donor id after adding(should fail)', () => { | ||||
|     let added_donor; | ||||
|     it('creating a new donor with only needed params should return 200', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "lastname": "last" | ||||
|         }, axios_config); | ||||
|         added_donor = res2.data; | ||||
|         expect(res2.status).toEqual(200); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('invalid update should return 406', async () => { | ||||
|         added_donor.id++; | ||||
|         const res3 = await axios.put(base + '/api/donors/' + (added_donor.id - 1), added_donor, axios_config); | ||||
|         expect(res3.status).toEqual(406); | ||||
|         expect(res3.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
| // --------------- | ||||
| describe('Update donor without address but receiptNeeded=true should fail', () => { | ||||
|     let added_donor; | ||||
|     it('creating a new donor with only needed params should return 200', async () => { | ||||
|         const res2 = await axios.post(base + '/api/donors', { | ||||
|             "firstname": "first", | ||||
|             "lastname": "last", | ||||
|         }, axios_config); | ||||
|         added_donor = res2.data; | ||||
|         expect(res2.status).toEqual(200); | ||||
|         expect(res2.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
|     it('invalid update should return 406', async () => { | ||||
|         added_donor.receiptNeeded = true; | ||||
|         const res3 = await axios.put(base + '/api/donors/' + added_donor.id, added_donor, axios_config); | ||||
|         expect(res3.status).toEqual(406); | ||||
|         expect(res3.headers['content-type']).toContain("application/json") | ||||
|     }); | ||||
| }); | ||||
| @@ -1,116 +0,0 @@ | ||||
| import axios from 'axios'; | ||||
| import { config } from '../config'; | ||||
| const base = "http://localhost:" + config.internal_port | ||||
|  | ||||
| let access_token; | ||||
| let axios_config; | ||||
|  | ||||
| beforeAll(async () => { | ||||
| 	const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); | ||||
| 	access_token = res.data["access_token"]; | ||||
| 	axios_config = { | ||||
| 		headers: { "authorization": "Bearer " + access_token }, | ||||
| 		validateStatus: undefined | ||||
| 	}; | ||||
| }); | ||||
|  | ||||
| describe('GET /api/tracks', () => { | ||||
| 	it('basic get should return 200', async () => { | ||||
| 		const res = await axios.get(base + '/api/tracks', axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": 400 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('POST /api/tracks', () => { | ||||
| 	it('illegal distance input should return 400', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": -1 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(400); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": 400 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('adding + getting tracks', () => { | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": 1000 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('check if track was added', async () => { | ||||
| 		const res = await axios.get(base + '/api/tracks', axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 		let added_track = res.data[res.data.length - 1] | ||||
| 		delete added_track.id | ||||
| 		expect(added_track).toEqual({ | ||||
| 			"name": "string", | ||||
| 			"distance": 1000 | ||||
| 		}) | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('adding + getting + updating', () => { | ||||
| 	let added_track_id | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": 1500 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('get should return 200', async () => { | ||||
| 		const res1 = await axios.get(base + '/api/tracks', axios_config); | ||||
| 		expect(res1.status).toEqual(200); | ||||
| 		expect(res1.headers['content-type']).toContain("application/json") | ||||
| 		let added_track = res1.data[res1.data.length - 1] | ||||
| 		added_track_id = added_track.id | ||||
| 		delete added_track.id | ||||
| 		expect(added_track).toEqual({ | ||||
| 			"name": "string", | ||||
| 			"distance": 1500 | ||||
| 		}) | ||||
| 	}) | ||||
| 	it('get should return 200', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track_id, { | ||||
| 			"id": added_track_id, | ||||
| 			"name": "apitrack", | ||||
| 			"distance": 5100 | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(200); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}) | ||||
| 	it('get should return 200', async () => { | ||||
| 		const res3 = await axios.get(base + '/api/tracks', axios_config); | ||||
| 		expect(res3.status).toEqual(200); | ||||
| 		expect(res3.headers['content-type']).toContain("application/json") | ||||
| 		let added_track2 = res3.data[res3.data.length - 1] | ||||
| 		delete added_track2.id | ||||
| 		expect(added_track2).toEqual({ | ||||
| 			"name": "apitrack", | ||||
| 			"distance": 5100 | ||||
| 		}) | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										90
									
								
								src/tests/tracks/track_add.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/tests/tracks/track_add.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| import axios from 'axios'; | ||||
| import { config } from '../../config'; | ||||
| const base = "http://localhost:" + config.internal_port | ||||
|  | ||||
| let access_token; | ||||
| let axios_config; | ||||
|  | ||||
| beforeAll(async () => { | ||||
| 	const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); | ||||
| 	access_token = res.data["access_token"]; | ||||
| 	axios_config = { | ||||
| 		headers: { "authorization": "Bearer " + access_token }, | ||||
| 		validateStatus: undefined | ||||
| 	}; | ||||
| }); | ||||
|  | ||||
|  | ||||
| describe('POST /api/tracks illegally', () => { | ||||
| 	it('no distance input should return 400', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(400); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('illegal distance input should return 400', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": -1 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(400); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('negative minimum lap time input should return 406', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": -1 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(406); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('POST /api/tracks successfully', () => { | ||||
| 	it('creating a track with the minimum amount of parameters should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 		delete res.data.id; | ||||
| 		expect(res.data).toEqual({ | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": null | ||||
| 		}) | ||||
| 	}); | ||||
| 	it('creating a track with all parameters (optional set to null) should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": null | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 		delete res.data.id; | ||||
| 		expect(res.data).toEqual({ | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": null | ||||
| 		}) | ||||
| 	}); | ||||
| 	it('creating a track with all parameters should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": 123 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 		delete res.data.id; | ||||
| 		expect(res.data).toEqual({ | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": 123 | ||||
| 		}) | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										53
									
								
								src/tests/tracks/track_delete.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/tests/tracks/track_delete.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import axios from 'axios'; | ||||
| import { config } from '../../config'; | ||||
| const base = "http://localhost:" + config.internal_port | ||||
|  | ||||
| let access_token; | ||||
| let axios_config; | ||||
|  | ||||
| beforeAll(async () => { | ||||
| 	const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); | ||||
| 	access_token = res.data["access_token"]; | ||||
| 	axios_config = { | ||||
| 		headers: { "authorization": "Bearer " + access_token }, | ||||
| 		validateStatus: undefined | ||||
| 	}; | ||||
| }); | ||||
|  | ||||
| // --------------- | ||||
| describe('DETELE track', () => { | ||||
| 	let added_track | ||||
| 	it('creating a track with the minimum amount of parameters should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 		added_track = res.data | ||||
| 	}); | ||||
| 	it('delete track', async () => { | ||||
| 		const res2 = await axios.delete(base + '/api/tracks/' + added_track.id, axios_config); | ||||
| 		expect(res2.status).toEqual(200); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 		let deleted_track = res2.data | ||||
| 		delete deleted_track.id; | ||||
| 		expect(res2.data).toEqual({ | ||||
| 			"name": "testtrack", | ||||
| 			"distance": 200, | ||||
| 			"minimumLapTime": null | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('check if track really was deleted', async () => { | ||||
| 		const res3 = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); | ||||
| 		expect(res3.status).toEqual(404); | ||||
| 		expect(res3.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('DELETE track (non-existant)', () => { | ||||
| 	it('delete', async () => { | ||||
| 		const res2 = await axios.delete(base + '/api/tracks/0', axios_config); | ||||
| 		expect(res2.status).toEqual(204); | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										98
									
								
								src/tests/tracks/track_update.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/tests/tracks/track_update.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| import axios from 'axios'; | ||||
| import { config } from '../../config'; | ||||
| const base = "http://localhost:" + config.internal_port | ||||
|  | ||||
| let access_token; | ||||
| let axios_config; | ||||
|  | ||||
| beforeAll(async () => { | ||||
| 	const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); | ||||
| 	access_token = res.data["access_token"]; | ||||
| 	axios_config = { | ||||
| 		headers: { "authorization": "Bearer " + access_token }, | ||||
| 		validateStatus: undefined | ||||
| 	}; | ||||
| }); | ||||
|  | ||||
| describe('adding + updating illegally', () => { | ||||
| 	let added_track; | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "apitrack", | ||||
| 			"distance": 1500 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 		added_track = res.data; | ||||
| 	}); | ||||
| 	it('update with negative distance should return 400', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { | ||||
| 			"id": added_track.id, | ||||
| 			"name": "apitrack", | ||||
| 			"distance": -1 | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(400); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('update with negative laptime should return 406', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { | ||||
| 			"id": added_track.id, | ||||
| 			"name": "apitrack", | ||||
| 			"distance": 2, | ||||
| 			"minimumLapTime": -1 | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(406); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('adding + updating successfilly', () => { | ||||
| 	let added_track; | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "apitrack2", | ||||
| 			"distance": 1500 | ||||
| 		}, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 		added_track = res.data; | ||||
| 	}); | ||||
| 	it('valid name change should return 200', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { | ||||
| 			"id": added_track.id, | ||||
| 			"name": "apitrackk", | ||||
| 			"distance": 1500 | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(200); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('valid distance change should return 200', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { | ||||
| 			"id": added_track.id, | ||||
| 			"name": "apitrack2", | ||||
| 			"distance": 5100 | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(200); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('valid laptime change (to a set number) should return 200', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { | ||||
| 			"id": added_track.id, | ||||
| 			"name": "apitrack2", | ||||
| 			"distance": 5100, | ||||
| 			"minimumLapTime": 3 | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(200); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('valid laptime change (to a set null) should return 200', async () => { | ||||
| 		const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { | ||||
| 			"id": added_track.id, | ||||
| 			"name": "apitrack2", | ||||
| 			"distance": 5100, | ||||
| 			"minimumLapTime": null | ||||
| 		}, axios_config); | ||||
| 		expect(res2.status).toEqual(200); | ||||
| 		expect(res2.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										49
									
								
								src/tests/tracks/tracks_get.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/tests/tracks/tracks_get.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import axios from 'axios'; | ||||
| import { config } from '../../config'; | ||||
| const base = "http://localhost:" + config.internal_port | ||||
|  | ||||
| let access_token; | ||||
| let axios_config; | ||||
|  | ||||
| beforeAll(async () => { | ||||
| 	const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); | ||||
| 	access_token = res.data["access_token"]; | ||||
| 	axios_config = { | ||||
| 		headers: { "authorization": "Bearer " + access_token }, | ||||
| 		validateStatus: undefined | ||||
| 	}; | ||||
| }); | ||||
|  | ||||
| describe('GET /api/tracks sucessfully', () => { | ||||
| 	it('basic get should return 200', async () => { | ||||
| 		const res = await axios.get(base + '/api/tracks', axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('GET /api/tracks illegally', () => { | ||||
| 	it('get for non-existant track should return 404', async () => { | ||||
| 		const res = await axios.get(base + '/api/tracks/-1', axios_config); | ||||
| 		expect(res.status).toEqual(404); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| }); | ||||
| // --------------- | ||||
| describe('adding + getting tracks', () => { | ||||
| 	let added_track; | ||||
| 	it('correct distance input should return 200', async () => { | ||||
| 		const res = await axios.post(base + '/api/tracks', { | ||||
| 			"name": "string", | ||||
| 			"distance": 1000 | ||||
| 		}, axios_config); | ||||
| 		added_track = res.data; | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json") | ||||
| 	}); | ||||
| 	it('check if track was added (no parameter validation)', async () => { | ||||
| 		const res = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); | ||||
| 		expect(res.status).toEqual(200); | ||||
| 		expect(res.headers['content-type']).toContain("application/json"); | ||||
| 	}); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user