diff --git a/.drone.yml b/.drone.yml index 2d5c9b2..4e7d618 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,24 +1,3 @@ ---- -kind: pipeline -name: tests:14.15.1-alpine3.12 -clone: - disable: true -steps: - - name: checkout pr - image: alpine/git - commands: - - git clone $DRONE_REMOTE_URL . - - git checkout $DRONE_SOURCE_BRANCH - - mv .env.ci .env - - name: run tests - image: node:14.15.1-alpine3.12 - commands: - - yarn - - yarn test:ci -trigger: - event: - - pull_request - --- kind: pipeline name: tests:node_latest diff --git a/src/controllers/DonationController.ts b/src/controllers/DonationController.ts new file mode 100644 index 0000000..9c2696d --- /dev/null +++ b/src/controllers/DonationController.ts @@ -0,0 +1,145 @@ +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 { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors'; +import { DonorNotFoundError } from '../errors/DonorErrors'; +import { RunnerNotFoundError } from '../errors/RunnerErrors'; +import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation'; +import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation'; +import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation'; +import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonation'; +import { DistanceDonation } from '../models/entities/DistanceDonation'; +import { Donation } from '../models/entities/Donation'; +import { FixedDonation } from '../models/entities/FixedDonation'; +import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation'; +import { ResponseDonation } from '../models/responses/ResponseDonation'; +import { ResponseEmpty } from '../models/responses/ResponseEmpty'; + +@JsonController('/donations') +@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) +export class DonationController { + private donationRepository: Repository; + private distanceDonationRepository: Repository; + private fixedDonationRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.donationRepository = getConnectionManager().get().getRepository(Donation); + this.distanceDonationRepository = getConnectionManager().get().getRepository(DistanceDonation); + this.fixedDonationRepository = getConnectionManager().get().getRepository(FixedDonation); + } + + @Get() + @Authorized("DONATION:GET") + @ResponseSchema(ResponseDonation, { isArray: true }) + @ResponseSchema(ResponseDistanceDonation, { isArray: true }) + @OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors.
This includes the donations\'s runner\'s distance ran(if distance donation).' }) + async getAll() { + let responseDonations: ResponseDonation[] = new Array(); + const donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] }); + donations.forEach(donation => { + responseDonations.push(donation.toResponse()); + }); + return responseDonations; + } + + @Get('/:id') + @Authorized("DONATION:GET") + @ResponseSchema(ResponseDonation) + @ResponseSchema(ResponseDistanceDonation) + @ResponseSchema(DonationNotFoundError, { statusCode: 404 }) + @OnUndefined(DonationNotFoundError) + @OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' }) + async getOne(@Param('id') id: number) { + let donation = await this.donationRepository.findOne({ id: id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] }) + if (!donation) { throw new DonationNotFoundError(); } + return donation.toResponse(); + } + + @Post('/fixed') + @Authorized("DONATION:CREATE") + @ResponseSchema(ResponseDonation) + @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Create a fixed donation (not distance donation - use /donations/distance instead).
Please rmemember to provide the donation\'s donors\'s id and amount.' }) + async postFixed(@Body({ validate: true }) createDonation: CreateFixedDonation) { + let donation = await createDonation.toEntity(); + donation = await this.fixedDonationRepository.save(donation); + return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse(); + } + + @Post('/distance') + @Authorized("DONATION:CREATE") + @ResponseSchema(ResponseDistanceDonation) + @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Create a distance donation (not fixed donation - use /donations/fixed instead).
Please rmemember to provide the donation\'s donors\'s and runner\s ids and amount per distance (kilometer).' }) + async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) { + let donation = await createDonation.toEntity(); + donation = await this.distanceDonationRepository.save(donation); + return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse(); + } + + @Put('/fixed/:id') + @Authorized("DONATION:UPDATE") + @ResponseSchema(ResponseDonation) + @ResponseSchema(DonationNotFoundError, { statusCode: 404 }) + @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update the fixed donation (not distance donation - use /donations/distance instead) whose id you provided.
Please remember that ids can't be changed and amounts must be positive." }) + async putFixed(@Param('id') id: number, @Body({ validate: true }) donation: UpdateFixedDonation) { + let oldDonation = await this.fixedDonationRepository.findOne({ id: id }); + + if (!oldDonation) { + throw new DonationNotFoundError(); + } + + if (oldDonation.id != donation.id) { + throw new DonationIdsNotMatchingError(); + } + + await this.fixedDonationRepository.save(await donation.update(oldDonation)); + return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse(); + } + + @Put('/distance/:id') + @Authorized("DONATION:UPDATE") + @ResponseSchema(ResponseDonation) + @ResponseSchema(DonationNotFoundError, { statusCode: 404 }) + @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update the distance donation (not fixed donation - use /donations/fixed instead) whose id you provided.
Please remember that ids can't be changed and amountPerDistance must be positive." }) + async putDistance(@Param('id') id: number, @Body({ validate: true }) donation: UpdateDistanceDonation) { + let oldDonation = await this.distanceDonationRepository.findOne({ id: id }); + + if (!oldDonation) { + throw new DonationNotFoundError(); + } + + if (oldDonation.id != donation.id) { + throw new DonationIdsNotMatchingError(); + } + + await this.distanceDonationRepository.save(await donation.update(oldDonation)); + return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse(); + } + + @Delete('/:id') + @Authorized("DONATION:DELETE") + @ResponseSchema(ResponseDonation) + @ResponseSchema(ResponseDistanceDonation) + @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @OnUndefined(204) + @OpenAPI({ description: 'Delete the donation whose id you provided.
If no donation with this id exists it will just return 204(no content).' }) + async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + let donation = await this.donationRepository.findOne({ id: id }); + if (!donation) { return null; } + const responseScan = await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] }); + + await this.donationRepository.delete(donation); + return responseScan.toResponse(); + } +} diff --git a/src/controllers/DonorController.ts b/src/controllers/DonorController.ts index f6b8527..4b0d508 100644 --- a/src/controllers/DonorController.ts +++ b/src/controllers/DonorController.ts @@ -1,12 +1,13 @@ 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 { DonorHasDonationsError, DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors'; import { CreateDonor } from '../models/actions/create/CreateDonor'; import { UpdateDonor } from '../models/actions/update/UpdateDonor'; import { Donor } from '../models/entities/Donor'; import { ResponseDonor } from '../models/responses/ResponseDonor'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; +import { DonationController } from './DonationController'; @JsonController('/donors') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -23,10 +24,10 @@ export class DonorController { @Get() @Authorized("DONOR:GET") @ResponseSchema(ResponseDonor, { isArray: true }) - @OpenAPI({ description: 'Lists all runners from all teams/orgs.
This includes the runner\'s group and distance ran.' }) + @OpenAPI({ description: 'Lists all donor.
This includes the donor\'s current donation amount.' }) async getAll() { let responseDonors: ResponseDonor[] = new Array(); - const donors = await this.donorRepository.find(); + const donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }); donors.forEach(donor => { responseDonors.push(new ResponseDonor(donor)); }); @@ -38,9 +39,9 @@ export class DonorController { @ResponseSchema(ResponseDonor) @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) @OnUndefined(DonorNotFoundError) - @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + @OpenAPI({ description: 'Lists all information about the donor whose id got provided.
This includes the donor\'s current donation amount.' }) async getOne(@Param('id') id: number) { - let donor = await this.donorRepository.findOne({ id: id }) + let donor = await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }) if (!donor) { throw new DonorNotFoundError(); } return new ResponseDonor(donor); } @@ -48,7 +49,7 @@ export class DonorController { @Post() @Authorized("DONOR:CREATE") @ResponseSchema(ResponseDonor) - @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + @OpenAPI({ description: 'Create a new donor.' }) async post(@Body({ validate: true }) createRunner: CreateDonor) { let donor; try { @@ -58,7 +59,7 @@ export class DonorController { } donor = await this.donorRepository.save(donor) - return new ResponseDonor(await this.donorRepository.findOne(donor)); + return new ResponseDonor(await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })); } @Put('/:id') @@ -66,7 +67,7 @@ export class DonorController { @ResponseSchema(ResponseDonor) @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) @ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 }) - @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) + @OpenAPI({ description: "Update the donor whose id you provided.
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 }); @@ -79,7 +80,7 @@ export class DonorController { } await this.donorRepository.save(await donor.update(oldDonor)); - return new ResponseDonor(await this.donorRepository.findOne({ id: id })); + return new ResponseDonor(await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })); } @Delete('/:id') @@ -87,17 +88,24 @@ export class DonorController { @ResponseSchema(ResponseDonor) @ResponseSchema(ResponseEmpty, { statusCode: 204 }) @OnUndefined(204) - @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete the donor whose id you provided.
If no donor with this id exists it will just return 204(no content).
If the donor still has donations associated this will fail, please provide the query param ?force=true to delete the donor with all associated donations.' }) 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); + const responseDonor = await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }); if (!donor) { throw new DonorNotFoundError(); } - //TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66) + const donorDonations = (await this.donorRepository.findOne({ id: donor.id }, { relations: ["donations"] })).donations; + if (donorDonations.length > 0 && !force) { + throw new DonorHasDonationsError(); + } + const donationController = new DonationController(); + for (let donation of donorDonations) { + await donationController.remove(donation.id, force); + } await this.donorRepository.delete(donor); return new ResponseDonor(responseDonor); diff --git a/src/controllers/RunnerController.ts b/src/controllers/RunnerController.ts index 2d83a02..389697b 100644 --- a/src/controllers/RunnerController.ts +++ b/src/controllers/RunnerController.ts @@ -1,13 +1,14 @@ 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 { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors'; +import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors'; import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors'; import { CreateRunner } from '../models/actions/create/CreateRunner'; import { UpdateRunner } from '../models/actions/update/UpdateRunner'; import { Runner } from '../models/entities/Runner'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseRunner } from '../models/responses/ResponseRunner'; +import { DonationController } from './DonationController'; import { RunnerCardController } from './RunnerCardController'; import { ScanController } from './ScanController'; @@ -91,6 +92,7 @@ export class RunnerController { @Authorized("RUNNER:DELETE") @ResponseSchema(ResponseRunner) @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @ResponseSchema(RunnerHasDistanceDonationsError, { statusCode: 406 }) @OnUndefined(204) @OpenAPI({ description: 'Delete the runner whose id you provided.
This will also delete all scans and cards associated with the runner.
If no runner with this id exists it will just return 204(no content).' }) async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { @@ -102,10 +104,19 @@ export class RunnerController { throw new RunnerNotFoundError(); } + const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations; + if (runnerDonations.length > 0 && !force) { + throw new RunnerHasDistanceDonationsError(); + } + const donationController = new DonationController(); + for (let donation of runnerDonations) { + await donationController.remove(donation.id, force); + } + const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards; const cardController = new RunnerCardController; - for (let scan of runnerCards) { - await cardController.remove(scan.id, force); + for (let card of runnerCards) { + await cardController.remove(card.id, force); } const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans; diff --git a/src/errors/DonationErrors.ts b/src/errors/DonationErrors.ts new file mode 100644 index 0000000..2c69800 --- /dev/null +++ b/src/errors/DonationErrors.ts @@ -0,0 +1,25 @@ +import { IsString } from 'class-validator'; +import { NotAcceptableError, NotFoundError } from 'routing-controllers'; + +/** + * Error to throw when a Donation couldn't be found. + */ +export class DonationNotFoundError extends NotFoundError { + @IsString() + name = "DonationNotFoundError" + + @IsString() + message = "Donation not found!" +} + +/** + * Error to throw when two Donations' ids don't match. + * Usually occurs when a user tries to change a Donation's id. + */ +export class DonationIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "DonationIdsNotMatchingError" + + @IsString() + message = "The ids don't match! \n And if you wanted to change a Donation's id: This isn't allowed!" +} \ No newline at end of file diff --git a/src/errors/DonorErrors.ts b/src/errors/DonorErrors.ts index 0cd534e..bdf505a 100644 --- a/src/errors/DonorErrors.ts +++ b/src/errors/DonorErrors.ts @@ -33,4 +33,15 @@ export class DonorReceiptAddressNeededError extends NotAcceptableError { @IsString() message = "An address is needed to create a receipt for a donor. \n You didn't provide one." +} + +/** +* Error to throw when a donor still has donations associated. +*/ +export class DonorHasDonationsError extends NotAcceptableError { + @IsString() + name = "DonorHasDonationsError" + + @IsString() + message = "This donor still has donations associated with it. \n If you want to delete this donor with all it's donations and teams add `?force` to your query." } \ No newline at end of file diff --git a/src/errors/RunnerErrors.ts b/src/errors/RunnerErrors.ts index b60a70d..4dad85f 100644 --- a/src/errors/RunnerErrors.ts +++ b/src/errors/RunnerErrors.ts @@ -33,4 +33,15 @@ export class RunnerGroupNeededError extends NotAcceptableError { @IsString() message = "Runner's need to be part of one group (team or organisation)! \n You provided neither." +} + +/** +* Error to throw when a runner still has distance donations associated. +*/ +export class RunnerHasDistanceDonationsError extends NotAcceptableError { + @IsString() + name = "RunnerHasDistanceDonationsError" + + @IsString() + message = "This runner still has distance donations associated with it. \n If you want to delete this runner with all it's donations and teams add `?force` to your query." } \ No newline at end of file diff --git a/src/models/actions/create/CreateDistanceDonation.ts b/src/models/actions/create/CreateDistanceDonation.ts new file mode 100644 index 0000000..e212068 --- /dev/null +++ b/src/models/actions/create/CreateDistanceDonation.ts @@ -0,0 +1,52 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../../errors/RunnerErrors'; +import { DistanceDonation } from '../../entities/DistanceDonation'; +import { Runner } from '../../entities/Runner'; +import { CreateDonation } from './CreateDonation'; + +/** + * This class is used to create a new FixedDonation entity from a json body (post request). + */ +export class CreateDistanceDonation extends CreateDonation { + + /** + * The donation's associated runner. + * This is important to link the runner's distance ran to the donation. + */ + @IsInt() + @IsPositive() + runner: number; + + /** + * The donation's amount per distance (full kilometer aka 1000 meters). + * The unit is your currency's smallest unit (default: euro cent). + */ + @IsInt() + @IsPositive() + amountPerDistance: number; + + /** + * Creates a new FixedDonation entity from this. + */ + public async toEntity(): Promise { + let newDonation = new DistanceDonation; + + newDonation.amountPerDistance = this.amountPerDistance; + newDonation.donor = await this.getDonor(); + newDonation.runner = await this.getRunner(); + + return newDonation; + } + + /** + * Gets a runner based on the runner id provided via this.runner. + */ + public async getRunner(): Promise { + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } +} \ No newline at end of file diff --git a/src/models/actions/create/CreateDonation.ts b/src/models/actions/create/CreateDonation.ts new file mode 100644 index 0000000..3b9218f --- /dev/null +++ b/src/models/actions/create/CreateDonation.ts @@ -0,0 +1,34 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { DonorNotFoundError } from '../../../errors/DonorErrors'; +import { Donation } from '../../entities/Donation'; +import { Donor } from '../../entities/Donor'; + +/** + * This class is used to create a new Donation entity from a json body (post request). + */ +export abstract class CreateDonation { + /** + * The donation's associated donor. + * This is important to link donations to donors. + */ + @IsInt() + @IsPositive() + donor: number; + + /** + * Creates a new Donation entity from this. + */ + public abstract toEntity(): Promise; + + /** + * Gets a donor based on the donor id provided via this.donor. + */ + public async getDonor(): Promise { + const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor }); + if (!donor) { + throw new DonorNotFoundError(); + } + return donor; + } +} \ No newline at end of file diff --git a/src/models/actions/create/CreateFixedDonation.ts b/src/models/actions/create/CreateFixedDonation.ts new file mode 100644 index 0000000..4d73f50 --- /dev/null +++ b/src/models/actions/create/CreateFixedDonation.ts @@ -0,0 +1,28 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { FixedDonation } from '../../entities/FixedDonation'; +import { CreateDonation } from './CreateDonation'; + +/** + * This class is used to create a new FixedDonation entity from a json body (post request). + */ +export class CreateFixedDonation extends CreateDonation { + /** + * The donation's amount. + * The unit is your currency's smallest unit (default: euro cent). + */ + @IsInt() + @IsPositive() + amount: number; + + /** + * Creates a new FixedDonation entity from this. + */ + public async toEntity(): Promise { + let newDonation = new FixedDonation; + + newDonation.amount = this.amount; + newDonation.donor = await this.getDonor(); + + return newDonation; + } +} \ No newline at end of file diff --git a/src/models/actions/update/UpdateDistanceDonation.ts b/src/models/actions/update/UpdateDistanceDonation.ts new file mode 100644 index 0000000..85a5473 --- /dev/null +++ b/src/models/actions/update/UpdateDistanceDonation.ts @@ -0,0 +1,51 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../../errors/RunnerErrors'; +import { DistanceDonation } from '../../entities/DistanceDonation'; +import { Runner } from '../../entities/Runner'; +import { UpdateDonation } from './UpdateDonation'; + +/** + * This class is used to update a DistanceDonation entity (via put request). + */ +export class UpdateDistanceDonation extends UpdateDonation { + + /** + * The donation's associated runner. + * This is important to link the runner's distance ran to the donation. + */ + @IsInt() + @IsPositive() + runner: number; + + /** + * The donation's amount per distance (full kilometer aka 1000 meters). + * The unit is your currency's smallest unit (default: euro cent). + */ + @IsInt() + @IsPositive() + amountPerDistance: number; + + /** + * Update a DistanceDonation entity based on this. + * @param donation The donation that shall be updated. + */ + public async update(donation: DistanceDonation): Promise { + donation.amountPerDistance = this.amountPerDistance; + donation.donor = await this.getDonor(); + donation.runner = await this.getRunner(); + + return donation; + } + + /** + * Gets a runner based on the runner id provided via this.runner. + */ + public async getRunner(): Promise { + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } +} \ No newline at end of file diff --git a/src/models/actions/update/UpdateDonation.ts b/src/models/actions/update/UpdateDonation.ts new file mode 100644 index 0000000..7f10f97 --- /dev/null +++ b/src/models/actions/update/UpdateDonation.ts @@ -0,0 +1,41 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { DonorNotFoundError } from '../../../errors/DonorErrors'; +import { Donation } from '../../entities/Donation'; +import { Donor } from '../../entities/Donor'; + +/** + * This class is used to update a Donation entity (via put request). + */ +export abstract class UpdateDonation { + /** + * The updated donation'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 donation's associated donor. + * This is important to link donations to donors. + */ + @IsInt() + @IsPositive() + donor: number; + + /** + * Creates a new Donation entity from this. + */ + public abstract update(donation: Donation): Promise; + + /** + * Gets a donor based on the donor id provided via this.donor. + */ + public async getDonor(): Promise { + const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor }); + if (!donor) { + throw new DonorNotFoundError(); + } + return donor; + } +} \ No newline at end of file diff --git a/src/models/actions/update/UpdateFixedDonation.ts b/src/models/actions/update/UpdateFixedDonation.ts new file mode 100644 index 0000000..5e31068 --- /dev/null +++ b/src/models/actions/update/UpdateFixedDonation.ts @@ -0,0 +1,27 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { FixedDonation } from '../../entities/FixedDonation'; +import { UpdateDonation } from './UpdateDonation'; + +/** + * This class is used to update a FixedDonation entity (via put request). + */ +export class UpdateFixedDonation extends UpdateDonation { + /** + * The updated donation's amount. + * The unit is your currency's smallest unit (default: euro cent). + */ + @IsInt() + @IsPositive() + amount: number; + + /** + * Update a FixedDonation entity based on this. + * @param donation The donation that shall be updated. + */ + public async update(donation: FixedDonation): Promise { + donation.amount = this.amount; + donation.donor = await this.getDonor(); + + return donation; + } +} \ No newline at end of file diff --git a/src/models/entities/DistanceDonation.ts b/src/models/entities/DistanceDonation.ts index 6b9ba5d..a9b323e 100644 --- a/src/models/entities/DistanceDonation.ts +++ b/src/models/entities/DistanceDonation.ts @@ -1,5 +1,6 @@ import { IsInt, IsNotEmpty, IsPositive } from "class-validator"; import { ChildEntity, Column, ManyToOne } from "typeorm"; +import { ResponseDistanceDonation } from '../responses/ResponseDistanceDonation'; import { Donation } from "./Donation"; import { Runner } from "./Runner"; @@ -31,7 +32,7 @@ export class DistanceDonation extends Donation { * Get's calculated from the runner's distance ran and the amount donated per kilometer. */ public get amount(): number { - let calculatedAmount = -1; + let calculatedAmount = 0; try { calculatedAmount = this.amountPerDistance * (this.runner.distance / 1000); } catch (error) { @@ -43,7 +44,7 @@ export class DistanceDonation extends Donation { /** * Turns this entity into it's response class. */ - public toResponse() { - return new Error("NotImplemented"); + public toResponse(): ResponseDistanceDonation { + return new ResponseDistanceDonation(this); } } diff --git a/src/models/entities/Donation.ts b/src/models/entities/Donation.ts index 46d7d45..1dd023d 100644 --- a/src/models/entities/Donation.ts +++ b/src/models/entities/Donation.ts @@ -3,6 +3,7 @@ import { IsNotEmpty } from "class-validator"; import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; +import { ResponseDonation } from '../responses/ResponseDonation'; import { Donor } from './Donor'; /** @@ -31,12 +32,13 @@ export abstract class Donation { * The donation's amount in cents (or whatever your currency's smallest unit is.). * The exact implementation may differ for each type of donation. */ - abstract amount: number; + public abstract get amount(): number; + /** * Turns this entity into it's response class. */ - public toResponse() { - return new Error("NotImplemented"); + public toResponse(): ResponseDonation { + return new ResponseDonation(this); } } \ No newline at end of file diff --git a/src/models/entities/Donor.ts b/src/models/entities/Donor.ts index 01b365c..acf5536 100644 --- a/src/models/entities/Donor.ts +++ b/src/models/entities/Donor.ts @@ -1,4 +1,4 @@ -import { IsBoolean } from "class-validator"; +import { IsBoolean, IsInt } from "class-validator"; import { ChildEntity, Column, OneToMany } from "typeorm"; import { ResponseDonor } from '../responses/ResponseDonor'; import { Donation } from './Donation'; @@ -24,6 +24,15 @@ export class Donor extends Participant { @OneToMany(() => Donation, donation => donation.donor, { nullable: true }) donations: Donation[]; + /** + * Returns the total donations of a donor based on his linked donations. + */ + @IsInt() + public get donationAmount(): number { + if (!this.donations) { return 0; } + return this.donations.reduce((sum, current) => sum + current.amount, 0); + } + /** * Turns this entity into it's response class. */ diff --git a/src/models/entities/FixedDonation.ts b/src/models/entities/FixedDonation.ts index 6a32066..c6454d4 100644 --- a/src/models/entities/FixedDonation.ts +++ b/src/models/entities/FixedDonation.ts @@ -1,5 +1,6 @@ import { IsInt, IsPositive } from "class-validator"; import { ChildEntity, Column } from "typeorm"; +import { ResponseDonation } from '../responses/ResponseDonation'; import { Donation } from "./Donation"; /** @@ -11,16 +12,33 @@ export class FixedDonation extends Donation { /** * The donation's amount in cents (or whatever your currency's smallest unit is.). + * This is the "real" value used by fixed donations. */ @Column() @IsInt() @IsPositive() - amount: number; + private _amount: number; + + /** + * The donation's amount in cents (or whatever your currency's smallest unit is.). + */ + @IsInt() + @IsPositive() + public get amount(): number { + return this._amount; + } + + /** + * The donation's amount in cents (or whatever your currency's smallest unit is.). + */ + public set amount(value: number) { + this._amount = value; + } /** * Turns this entity into it's response class. */ - public toResponse() { - return new Error("NotImplemented"); + public toResponse(): ResponseDonation { + return new ResponseDonation(this); } } \ No newline at end of file diff --git a/src/models/enums/PermissionTargets.ts b/src/models/enums/PermissionTargets.ts index 551ea5c..86c547d 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -13,5 +13,6 @@ export enum PermissionTarget { DONOR = 'DONOR', SCAN = 'SCAN', STATION = 'STATION', - CARD = 'CARD' + CARD = 'CARD', + DONATION = 'DONATION' } \ No newline at end of file diff --git a/src/models/responses/ResponseDistanceDonation.ts b/src/models/responses/ResponseDistanceDonation.ts new file mode 100644 index 0000000..388ece0 --- /dev/null +++ b/src/models/responses/ResponseDistanceDonation.ts @@ -0,0 +1,35 @@ +import { IsInt, IsObject, IsPositive } from 'class-validator'; +import { DistanceDonation } from '../entities/DistanceDonation'; +import { ResponseDonation } from './ResponseDonation'; +import { ResponseRunner } from './ResponseRunner'; + +/** + * Defines the distance donation response. +*/ +export class ResponseDistanceDonation extends ResponseDonation { + + /** + * The donation's associated runner. + * Used as the source of the donation's distance. + */ + @IsObject() + runner: ResponseRunner; + + /** + * The donation's amount donated per distance. + * The amount the donor set to be donated per kilometer that the runner ran. + */ + @IsInt() + @IsPositive() + amountPerDistance: number; + + /** + * Creates a ResponseDistanceDonation object from a scan. + * @param donation The distance donation the response shall be build for. + */ + public constructor(donation: DistanceDonation) { + super(donation); + this.runner = donation.runner.toResponse(); + this.amountPerDistance = donation.amountPerDistance; + } +} diff --git a/src/models/responses/ResponseDonation.ts b/src/models/responses/ResponseDonation.ts new file mode 100644 index 0000000..c2789e3 --- /dev/null +++ b/src/models/responses/ResponseDonation.ts @@ -0,0 +1,37 @@ +import { IsInt, IsNotEmpty, IsPositive } from "class-validator"; +import { Donation } from '../entities/Donation'; +import { ResponseDonor } from './ResponseDonor'; + +/** + * Defines the donation response. +*/ +export class ResponseDonation { + /** + * The donation's id. + */ + @IsInt() + @IsPositive() + id: number; + + /** + * The donation's donor. + */ + @IsNotEmpty() + donor: ResponseDonor; + + /** + * The donation's amount in the smalles unit of your currency (default: euro cent). + */ + @IsInt() + amount: number; + + /** + * Creates a ResponseDonation object from a scan. + * @param donation The donation the response shall be build for. + */ + public constructor(donation: Donation) { + this.id = donation.id; + this.donor = donation.donor.toResponse(); + this.amount = donation.amount; + } +} diff --git a/src/models/responses/ResponseDonor.ts b/src/models/responses/ResponseDonor.ts index 89fea60..5f659f9 100644 --- a/src/models/responses/ResponseDonor.ts +++ b/src/models/responses/ResponseDonor.ts @@ -1,5 +1,5 @@ import { - IsBoolean + IsBoolean, IsInt } from "class-validator"; import { Donor } from '../entities/Donor'; import { ResponseParticipant } from './ResponseParticipant'; @@ -15,6 +15,12 @@ export class ResponseDonor extends ResponseParticipant { @IsBoolean() receiptNeeded: boolean; + /** + * Returns the total donations of a donor based on his linked donations. + */ + @IsInt() + donationAmount: number; + /** * Creates a ResponseRunner object from a runner. * @param runner The user the response shall be build for. @@ -22,5 +28,6 @@ export class ResponseDonor extends ResponseParticipant { public constructor(donor: Donor) { super(donor); this.receiptNeeded = donor.receiptNeeded; + this.donationAmount = donor.donationAmount; } } diff --git a/src/tests/donations/donations_add.spec.ts b/src/tests/donations/donations_add.spec.ts new file mode 100644 index 0000000..523b767 --- /dev/null +++ b/src/tests/donations/donations_add.spec.ts @@ -0,0 +1,236 @@ +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/donations illegally', () => { + it('posting to a non-existant endpoint should return 4040', async () => { + const res1 = await axios.post(base + '/api/donations', null, axios_config); + expect(res1.status).toEqual(404); + }); +}); +// --------------- +describe('POST /api/donations/fixed illegally', () => { + let added_donor; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no input should return 400', async () => { + const res = await axios.post(base + '/api/donations/fixed', null, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no donor should return 400', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "amount": 100 + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no amount should return 400', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('illegal amount input should return 400', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": -1 + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('invalid donor input should return 404', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": 999999999999999999999999, + "amount": 100 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/donations/distance illegally', () => { + let added_donor; + let added_org; + let added_runner; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no input should return 400', async () => { + const res = await axios.post(base + '/api/donations/distance', null, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no donor should return 400', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no amountPerDistance should return 400', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no runner should return 400', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('illegal amountPerDistance input should return 400', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": -1, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('invalid donor input should return 404', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "donor": 999999999999999999999999, + "runner": added_runner.id, + "amountPerDistance": 100, + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); + it('invalid runner input should return 404', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "donor": added_donor.id, + "runner": 999999999999999999999999, + "amountPerDistance": 100, + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/donations/fixed successfully', () => { + let added_donor; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + delete res.data.id; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual({ + "donor": added_donor, + "amount": 1000 + }); + }); +}); +// --------------- +describe('POST /api/donations/distance successfully', () => { + let added_donor; + let added_org; + let added_runner; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res.data.group; + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + delete res.data.id; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual({ + "donor": added_donor, + "amountPerDistance": 100, + "runner": added_runner, + "amount": 0 + }) + }); +}); \ No newline at end of file diff --git a/src/tests/donations/donations_delete.spec.ts b/src/tests/donations/donations_delete.spec.ts new file mode 100644 index 0000000..c238836 --- /dev/null +++ b/src/tests/donations/donations_delete.spec.ts @@ -0,0 +1,113 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('DELETE donation (non-existant)', () => { + it('delete', async () => { + const res = await axios.delete(base + '/api/donations/0', axios_config); + expect(res.status).toEqual(204); + }); +}); +// --------------- +describe('DELETE fixed donation', () => { + let added_donor; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete donation', async () => { + const res = await axios.delete(base + '/api/donations/' + added_donation.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + expect(res.data).toEqual(added_donation); + }); + it('check if donation really was deleted', async () => { + const res = await axios.get(base + '/api/donations/' + added_donation.id, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE distance donation', () => { + let added_donor; + let added_org; + let added_runner; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + delete res.data.runner.distance; + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete donation', async () => { + const res = await axios.delete(base + '/api/donations/' + added_donation.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + delete res.data.runner.distance; + expect(res.data).toEqual(added_donation); + }); + it('check if donation really was deleted', async () => { + const res = await axios.get(base + '/api/donations/' + added_donation.id, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); \ No newline at end of file diff --git a/src/tests/donations/donations_get.spec.ts b/src/tests/donations/donations_get.spec.ts new file mode 100644 index 0000000..8dd1a0f --- /dev/null +++ b/src/tests/donations/donations_get.spec.ts @@ -0,0 +1,108 @@ +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/donations sucessfully', () => { + it('basic get should return 200', async () => { + const res = await axios.get(base + '/api/donations', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/donations illegally', () => { + it('get for non-existant track should return 404', async () => { + const res = await axios.get(base + '/api/donations/-1', axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + getting fixed donation', () => { + let added_donor; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('check if donation was added (no parameter validation)', async () => { + const res = await axios.get(base + '/api/donations/' + added_donation.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); +// --------------- +describe('adding + getting distance donation', () => { + let added_donor; + let added_org; + let added_runner; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('check if donation was added (no parameter validation)', async () => { + const res = await axios.get(base + '/api/donations/' + added_donation.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file diff --git a/src/tests/donations/donations_update.spec.ts b/src/tests/donations/donations_update.spec.ts new file mode 100644 index 0000000..2df6914 --- /dev/null +++ b/src/tests/donations/donations_update.spec.ts @@ -0,0 +1,343 @@ +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 fixed donation illegally', () => { + let added_donor; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating empty should return 400', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, null, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with wrong id should return 406', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, { + "id": added_donation.id + 1, + "donor": added_donor.id, + "amount": 100 + }, axios_config); + expect(res.status).toEqual(406); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with negative amount should return 400', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, { + "id": added_donation.id, + "donor": added_donor.id, + "amount": -1 + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with invalid donor should return 404', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, { + "id": added_donation.id, + "donor": 9999999999999999999, + "amount": 100 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + updating distance donation illegally', () => { + let added_donor; + let added_org; + let added_runner; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating empty should return 400', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, null, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with wrong id should return 406', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id + 1, + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(406); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with negative amountPerDistance should return 400', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": added_runner.id, + "amountPerDistance": -1, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with invalid donor should return 404', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": 9999999999999999999 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating with invalid runner should return 404', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": 9999999999999999999, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + updating fixed donation valid', () => { + let added_donor; + let added_donor2; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + delete res.data.donationAmount; + added_donor2 = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + delete res.data.donor.donationAmount; + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating nothing should return 200', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, { + "id": added_donation.id, + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + delete res.data.donor.donationAmount; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual(added_donation); + }); + it('updating amount should return 200', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, { + "id": added_donation.id, + "donor": added_donor.id, + "amount": 42 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.amount).toEqual(42); + }); + it('updating donor should return 200', async () => { + const res = await axios.put(base + '/api/donations/fixed/' + added_donation.id, { + "id": added_donation.id, + "donor": added_donor2.id, + "amount": 42 + }, axios_config); + delete res.data.donor.donationAmount; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.donor).toEqual(added_donor2); + }); +}); +// --------------- +describe('adding + updating distance donation valid', () => { + let added_donor; + let added_donor2; + let added_org; + let added_runner; + let added_runner2; + let added_donation; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + delete res.data.donationAmount; + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + delete res.data.donationAmount; + added_donor2 = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res.data.group; + added_runner2 = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + delete res.data.donor.donationAmount; + added_donation = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating nothing should return 200', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + delete res.data.donor.donationAmount; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual(added_donation); + }); + it('updating amount should return 200', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": added_runner.id, + "amountPerDistance": 69, + "donor": added_donor.id + }, axios_config); + delete res.data.donor.donationAmount; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.amountPerDistance).toEqual(69); + }); + it('updating runner should return 200', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": added_runner2.id, + "amountPerDistance": 69, + "donor": added_donor.id + }, axios_config); + delete res.data.runner.group; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.runner).toEqual(added_runner2); + }); + it('updating donor should return 200', async () => { + const res = await axios.put(base + '/api/donations/distance/' + added_donation.id, { + "id": added_donation.id, + "runner": added_runner2.id, + "amountPerDistance": 69, + "donor": added_donor2.id + }, axios_config); + delete res.data.donor.donationAmount; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.donor).toEqual(added_donor2); + }); +}); \ No newline at end of file diff --git a/src/tests/donors/donor_delete.spec.ts b/src/tests/donors/donor_delete.spec.ts index 63e10fe..344dc20 100644 --- a/src/tests/donors/donor_delete.spec.ts +++ b/src/tests/donors/donor_delete.spec.ts @@ -44,4 +44,114 @@ describe('add+delete', () => { expect(res4.status).toEqual(404); expect(res4.headers['content-type']).toContain("application/json") }); +}); +// --------------- +describe('DELETE donor with donations invalid', () => { + let added_donor; + let added_org; + let added_runner; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete donor w/o force', async () => { + const res = await axios.delete(base + '/api/donors/' + added_donor.id, axios_config); + expect(res.status).toEqual(406); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE donor with donations valid', () => { + let added_donor; + let added_org; + let added_runner; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new fixed donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/fixed', { + "donor": added_donor.id, + "amount": 1000 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete donor w/ force ', async () => { + const res = await axios.delete(base + '/api/donors/' + added_donor.id + "?force=true", axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); }); \ No newline at end of file diff --git a/src/tests/runners/runner_delete.spec.ts b/src/tests/runners/runner_delete.spec.ts index 36e72e7..de96333 100644 --- a/src/tests/runners/runner_delete.spec.ts +++ b/src/tests/runners/runner_delete.spec.ts @@ -55,4 +55,98 @@ describe('add+delete', () => { expect(res4.status).toEqual(404); expect(res4.headers['content-type']).toContain("application/json") }); +}); +// --------------- +describe('DELETE donor with donations invalid', () => { + let added_donor; + let added_org; + let added_runner; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete runner w/o force', async () => { + const res = await axios.delete(base + '/api/runners/' + added_runner.id, axios_config); + expect(res.status).toEqual(406); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE donor with donations valid', () => { + let added_donor; + let added_org; + let added_runner; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/donors', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_donor = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new org with just a name should return 200', async () => { + const res = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new distance donation should return 200', async () => { + const res = await axios.post(base + '/api/donations/distance', { + "runner": added_runner.id, + "amountPerDistance": 100, + "donor": added_donor.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete donor w/ force ', async () => { + const res = await axios.delete(base + '/api/runners/' + added_runner.id + "?force=true", axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); }); \ No newline at end of file