diff --git a/src/controllers/DonorController.ts b/src/controllers/DonorController.ts new file mode 100644 index 0000000..54a5ba4 --- /dev/null +++ b/src/controllers/DonorController.ts @@ -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; + + /** + * 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.
This includes the runner\'s group and distance ran.' }) + async getAll() { + let responseDonors: ResponseDonor[] = new Array(); + 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.
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.
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.
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); + } +} diff --git a/src/errors/DonorErrors.ts b/src/errors/DonorErrors.ts new file mode 100644 index 0000000..0cd534e --- /dev/null +++ b/src/errors/DonorErrors.ts @@ -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." +} \ No newline at end of file diff --git a/src/models/actions/CreateDonor.ts b/src/models/actions/CreateDonor.ts new file mode 100644 index 0000000..25a631c --- /dev/null +++ b/src/models/actions/CreateDonor.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/src/models/actions/CreateRunnerOrganisation.ts b/src/models/actions/CreateRunnerOrganisation.ts index 017675e..a0733de 100644 --- a/src/models/actions/CreateRunnerOrganisation.ts +++ b/src/models/actions/CreateRunnerOrganisation.ts @@ -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; } diff --git a/src/models/actions/UpdateDonor.ts b/src/models/actions/UpdateDonor.ts new file mode 100644 index 0000000..b7139c8 --- /dev/null +++ b/src/models/actions/UpdateDonor.ts @@ -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.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; + } +} \ No newline at end of file diff --git a/src/models/actions/UpdateRunnerOrganisation.ts b/src/models/actions/UpdateRunnerOrganisation.ts index 3a5b1fb..9a2cafa 100644 --- a/src/models/actions/UpdateRunnerOrganisation.ts +++ b/src/models/actions/UpdateRunnerOrganisation.ts @@ -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; } diff --git a/src/models/entities/Address.ts b/src/models/entities/Address.ts index 99d69ce..561808f 100644 --- a/src/models/entities/Address.ts +++ b/src/models/entities/Address.ts @@ -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[]; } diff --git a/src/models/entities/Donation.ts b/src/models/entities/Donation.ts index 3ddc272..eea23b5 100644 --- a/src/models/entities/Donation.ts +++ b/src/models/entities/Donation.ts @@ -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.). diff --git a/src/models/entities/Donor.ts b/src/models/entities/Donor.ts index c0f914e..188ed09 100644 --- a/src/models/entities/Donor.ts +++ b/src/models/entities/Donor.ts @@ -1,5 +1,6 @@ import { IsBoolean } from "class-validator"; -import { ChildEntity, Column } 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[]; } \ No newline at end of file diff --git a/src/models/entities/GroupContact.ts b/src/models/entities/GroupContact.ts index f524863..4dbcc5e 100644 --- a/src/models/entities/GroupContact.ts +++ b/src/models/entities/GroupContact.ts @@ -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; /** diff --git a/src/models/entities/IAddressUser.ts b/src/models/entities/IAddressUser.ts new file mode 100644 index 0000000..3a7762a --- /dev/null +++ b/src/models/entities/IAddressUser.ts @@ -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 +} diff --git a/src/models/entities/Participant.ts b/src/models/entities/Participant.ts index e2e2263..de13b5f 100644 --- a/src/models/entities/Participant.ts +++ b/src/models/entities/Participant.ts @@ -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[]; } \ No newline at end of file diff --git a/src/models/entities/Runner.ts b/src/models/entities/Runner.ts index 1c509d6..5c51c11 100644 --- a/src/models/entities/Runner.ts +++ b/src/models/entities/Runner.ts @@ -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; /** diff --git a/src/models/entities/RunnerOrganisation.ts b/src/models/entities/RunnerOrganisation.ts index 877784b..f0f8c78 100644 --- a/src/models/entities/RunnerOrganisation.ts +++ b/src/models/entities/RunnerOrganisation.ts @@ -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; /** diff --git a/src/models/enums/PermissionTargets.ts b/src/models/enums/PermissionTargets.ts index 526aaa3..728cf2a 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -9,5 +9,6 @@ export enum PermissionTarget { USER = 'USER', USERGROUP = 'USERGROUP', PERMISSION = 'PERMISSION', - STATSCLIENT = 'STATSCLIENT' + STATSCLIENT = 'STATSCLIENT', + DONOR = 'DONOR' } \ No newline at end of file diff --git a/src/models/responses/ResponseDonor.ts b/src/models/responses/ResponseDonor.ts new file mode 100644 index 0000000..89fea60 --- /dev/null +++ b/src/models/responses/ResponseDonor.ts @@ -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; + } +} diff --git a/src/models/responses/ResponseRunnerOrganisation.ts b/src/models/responses/ResponseRunnerOrganisation.ts index edc1ff0..69ccaf1 100644 --- a/src/models/responses/ResponseRunnerOrganisation.ts +++ b/src/models/responses/ResponseRunnerOrganisation.ts @@ -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; /** diff --git a/src/tests/donors/donor_add.spec.ts b/src/tests/donors/donor_add.spec.ts new file mode 100644 index 0000000..8022184 --- /dev/null +++ b/src/tests/donors/donor_add.spec.ts @@ -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") + }); +}); \ No newline at end of file diff --git a/src/tests/donors/donor_delete.spec.ts b/src/tests/donors/donor_delete.spec.ts new file mode 100644 index 0000000..63e10fe --- /dev/null +++ b/src/tests/donors/donor_delete.spec.ts @@ -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") + }); +}); \ No newline at end of file diff --git a/src/tests/donors/donor_get.spec.ts b/src/tests/donors/donor_get.spec.ts new file mode 100644 index 0000000..fd9d17d --- /dev/null +++ b/src/tests/donors/donor_get.spec.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/src/tests/donors/donor_update.spec.ts b/src/tests/donors/donor_update.spec.ts new file mode 100644 index 0000000..26f6eb6 --- /dev/null +++ b/src/tests/donors/donor_update.spec.ts @@ -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") + }); +}); \ No newline at end of file diff --git a/src/tests/tracks.spec.ts b/src/tests/tracks.spec.ts index 4646878..81e4850 100644 --- a/src/tests/tracks.spec.ts +++ b/src/tests/tracks.spec.ts @@ -72,30 +72,26 @@ describe('adding + getting tracks', () => { }); // --------------- describe('adding + getting + updating', () => { - let added_track_id + let added_track; 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") + expect(res.headers['content-type']).toContain("application/json"); + added_track = res.data; }); it('get should return 200', async () => { - const res1 = await axios.get(base + '/api/tracks', axios_config); + const res1 = await axios.get(base + '/api/tracks/' + added_track.id, 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 - }) + const compareTrack = res1.data; + expect(compareTrack).toEqual(added_track) }) it('get should return 200', async () => { - const res2 = await axios.put(base + '/api/tracks/' + added_track_id, { - "id": added_track_id, + const res2 = await axios.put(base + '/api/tracks/' + added_track.id, { + "id": added_track.id, "name": "apitrack", "distance": 5100 }, axios_config); @@ -103,10 +99,10 @@ describe('adding + getting + updating', () => { expect(res2.headers['content-type']).toContain("application/json") }) it('get should return 200', async () => { - const res3 = await axios.get(base + '/api/tracks', axios_config); + const res3 = await axios.get(base + '/api/tracks/' + added_track.id, axios_config); expect(res3.status).toEqual(200); expect(res3.headers['content-type']).toContain("application/json") - let added_track2 = res3.data[res3.data.length - 1] + let added_track2 = res3.data; delete added_track2.id expect(added_track2).toEqual({ "name": "apitrack",