diff --git a/.env.example b/.env.example index 5595533..076d8cd 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,4 @@ DB_USER=bla DB_PASSWORD=bla DB_NAME=bla NODE_ENV=production -POSTALCODE_COUNTRYCODE=null \ No newline at end of file +POSTALCODE_COUNTRYCODE=DE \ No newline at end of file diff --git a/src/controllers/GroupContactController.ts b/src/controllers/GroupContactController.ts new file mode 100644 index 0000000..3d6f83e --- /dev/null +++ b/src/controllers/GroupContactController.ts @@ -0,0 +1,107 @@ +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { getConnection, getConnectionManager, Repository } from 'typeorm'; +import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors'; +import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors'; +import { CreateGroupContact } from '../models/actions/create/CreateGroupContact'; +import { UpdateGroupContact } from '../models/actions/update/UpdateGroupContact'; +import { GroupContact } from '../models/entities/GroupContact'; +import { RunnerGroup } from '../models/entities/RunnerGroup'; +import { ResponseEmpty } from '../models/responses/ResponseEmpty'; +import { ResponseGroupContact } from '../models/responses/ResponseGroupContact'; + +@JsonController('/contacts') +@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) +export class GroupContactController { + private contactRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.contactRepository = getConnectionManager().get().getRepository(GroupContact); + } + + @Get() + @Authorized("CONTACT:GET") + @ResponseSchema(ResponseGroupContact, { isArray: true }) + @OpenAPI({ description: 'Lists all contacts.
This includes the contact\'s associated groups.' }) + async getAll() { + let responseContacts: ResponseGroupContact[] = new Array(); + const contacts = await this.contactRepository.find({ relations: ['groups'] }); + contacts.forEach(contact => { + responseContacts.push(contact.toResponse()); + }); + return responseContacts; + } + + @Get('/:id') + @Authorized("CONTACT:GET") + @ResponseSchema(ResponseGroupContact) + @ResponseSchema(GroupContactNotFoundError, { statusCode: 404 }) + @OnUndefined(GroupContactNotFoundError) + @OpenAPI({ description: 'Lists all information about the contact whose id got provided.
This includes the contact\'s associated groups.' }) + async getOne(@Param('id') id: number) { + let contact = await this.contactRepository.findOne({ id: id }, { relations: ['groups'] }) + if (!contact) { throw new GroupContactNotFoundError(); } + return contact.toResponse(); + } + + @Post() + @Authorized("CONTACT:CREATE") + @ResponseSchema(ResponseGroupContact) + @ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Create a new contact.' }) + async post(@Body({ validate: true }) createContact: CreateGroupContact) { + let contact; + try { + contact = await createContact.toEntity(); + } catch (error) { + throw error; + } + + contact = await this.contactRepository.save(contact) + return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups'] })).toResponse(); + } + + @Put('/:id') + @Authorized("CONTACT:UPDATE") + @ResponseSchema(ResponseGroupContact) + @ResponseSchema(GroupContactNotFoundError, { statusCode: 404 }) + @ResponseSchema(GroupContactIdsNotMatchingError, { statusCode: 406 }) + @ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: "Update the contact whose id you provided.
Please remember that ids can't be changed." }) + async put(@Param('id') id: number, @Body({ validate: true }) contact: UpdateGroupContact) { + let oldContact = await this.contactRepository.findOne({ id: id }); + + if (!oldContact) { + throw new GroupContactNotFoundError(); + } + + if (oldContact.id != contact.id) { + throw new GroupContactIdsNotMatchingError(); + } + + await this.contactRepository.save(await contact.update(oldContact)); + return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups'] })).toResponse(); + } + + @Delete('/:id') + @Authorized("CONTACT:DELETE") + @ResponseSchema(ResponseGroupContact) + @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @OnUndefined(204) + @OpenAPI({ description: 'Delete the contact whose id you provided.
If no contact with this id exists it will just return 204(no content).
This won\'t delete any groups associated with the contact.' }) + async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + let contact = await this.contactRepository.findOne({ id: id }); + if (!contact) { return null; } + const responseContact = await this.contactRepository.findOne(contact, { relations: ['groups'] }); + for (let group of responseContact.groups) { + group.contact = null; + await getConnection().getRepository(RunnerGroup).save(group); + } + + await this.contactRepository.delete(contact); + return responseContact.toResponse(); + } +} diff --git a/src/controllers/RunnerOrganisationController.ts b/src/controllers/RunnerOrganisationController.ts index fbbd03a..4d985e4 100644 --- a/src/controllers/RunnerOrganisationController.ts +++ b/src/controllers/RunnerOrganisationController.ts @@ -94,7 +94,7 @@ export class RunnerOrganisationController { @ResponseSchema(RunnerOrganisationHasTeamsError, { statusCode: 406 }) @ResponseSchema(RunnerOrganisationHasRunnersError, { statusCode: 406 }) @OnUndefined(204) - @OpenAPI({ description: 'Delete the organsisation whose id you provided.
If the organisation still has runners and/or teams associated this will fail.
To delete the organisation with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while).
If no organisation with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete the organsisation whose id you provided.
If the organisation still has runners and/or teams associated this will fail.
To delete the organisation with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while).
This won\'t delete the associated contact.
If no organisation with this id exists it will just return 204(no content).' }) async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { let organisation = await this.runnerOrganisationRepository.findOne({ id: id }); if (!organisation) { return null; } diff --git a/src/controllers/RunnerTeamController.ts b/src/controllers/RunnerTeamController.ts index 5b97f2b..ce706ec 100644 --- a/src/controllers/RunnerTeamController.ts +++ b/src/controllers/RunnerTeamController.ts @@ -93,7 +93,7 @@ export class RunnerTeamController { @ResponseSchema(ResponseEmpty, { statusCode: 204 }) @ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 }) @OnUndefined(204) - @OpenAPI({ description: 'Delete the team whose id you provided.
If the team still has runners associated this will fail.
To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while).
If no team with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete the team whose id you provided.
If the team still has runners associated this will fail.
To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while).
This won\'t delete the associated contact.
If no team with this id exists it will just return 204(no content).' }) async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { let team = await this.runnerTeamRepository.findOne({ id: id }); if (!team) { return null; } diff --git a/src/errors/GroupContactErrors.ts b/src/errors/GroupContactErrors.ts index 92fd4a2..fffbdd5 100644 --- a/src/errors/GroupContactErrors.ts +++ b/src/errors/GroupContactErrors.ts @@ -2,18 +2,7 @@ import { IsString } from 'class-validator'; import { NotAcceptableError, NotFoundError } from 'routing-controllers'; /** - * Error to throw, when a provided groupContact doesn't belong to the accepted types. - */ -export class GroupContactWrongTypeError extends NotAcceptableError { - @IsString() - name = "GroupContactWrongTypeError" - - @IsString() - message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type." -} - -/** - * Error to throw, when a non-existent groupContact get's loaded. + * Error to throw, when a non-existent contact get's requested. */ export class GroupContactNotFoundError extends NotFoundError { @IsString() @@ -21,4 +10,16 @@ export class GroupContactNotFoundError extends NotFoundError { @IsString() message = "The groupContact you provided couldn't be located in the system. \n Please check your request." -} \ No newline at end of file +} + +/** + * Error to throw when two contacts' ids don't match. + * Usually occurs when a user tries to change a contact's id. + */ +export class GroupContactIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "GroupContactIdsNotMatchingError" + + @IsString() + message = "The ids don't match! \n And if you wanted to change a contact's id: This isn't allowed!" +} diff --git a/src/models/actions/create/CreateGroupContact.ts b/src/models/actions/create/CreateGroupContact.ts index c6538e8..0775b2d 100644 --- a/src/models/actions/create/CreateGroupContact.ts +++ b/src/models/actions/create/CreateGroupContact.ts @@ -1,10 +1,13 @@ import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; +import { getConnectionManager } from 'typeorm'; import { config } from '../../../config'; +import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors'; import { Address } from '../../entities/Address'; import { GroupContact } from '../../entities/GroupContact'; +import { RunnerGroup } from '../../entities/RunnerGroup'; /** - * This classed is used to create a new Group entity from a json body (post request). + * This classed is used to create a new GroupContact entity from a json body (post request). */ export class CreateGroupContact { /** @@ -44,25 +47,51 @@ export class CreateGroupContact { phone?: string; /** - * The contact's email address. + * The new contact's email address. */ @IsOptional() @IsEmail() email?: string; + /** + * The new contacts's groups' ids. + * You can provide either one groupId or an array of groupIDs. + */ + @IsOptional() + groups?: number[] | number + /** - * Creates a new Address entity from this. + * Get's all groups for this contact by their id's; + */ + public async getGroups(): Promise { + if (!this.groups) { return null; } + let groups = new Array(); + if (!Array.isArray(this.groups)) { + this.groups = [this.groups] + } + for (let group of this.groups) { + let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group }); + if (!found) { throw new RunnerGroupNotFoundError(); } + groups.push(found); + } + return groups; + } + + /** + * Creates a new GroupContact entity from this. */ public async toEntity(): Promise { - let contact: GroupContact = new GroupContact(); - contact.firstname = this.firstname; - contact.middlename = this.middlename; - contact.lastname = this.lastname; - contact.email = this.email; - contact.phone = this.phone; - contact.address = this.address; - Address.validate(contact.address); - return contact; + let newContact: GroupContact = new GroupContact(); + newContact.firstname = this.firstname; + newContact.middlename = this.middlename; + newContact.lastname = this.lastname; + newContact.email = this.email; + newContact.phone = this.phone; + newContact.address = this.address; + Address.validate(newContact.address); + newContact.groups = await this.getGroups(); + + return newContact; } } \ No newline at end of file diff --git a/src/models/actions/update/UpdateGroupContact.ts b/src/models/actions/update/UpdateGroupContact.ts new file mode 100644 index 0000000..72f919a --- /dev/null +++ b/src/models/actions/update/UpdateGroupContact.ts @@ -0,0 +1,106 @@ +import { IsEmail, IsInt, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; +import { getConnectionManager } from 'typeorm'; +import { config } from '../../../config'; +import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors'; +import { Address } from '../../entities/Address'; +import { GroupContact } from '../../entities/GroupContact'; +import { RunnerGroup } from '../../entities/RunnerGroup'; + + +/** + * This class is used to update a GroupContact entity (via put request). + */ +export class UpdateGroupContact { + /** + * The updated contact'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 contact's first name. + */ + @IsNotEmpty() + @IsString() + firstname: string; + + /** + * The updated contact's middle name. + */ + @IsOptional() + @IsString() + middlename?: string; + + /** + * The updated contact's last name. + */ + @IsNotEmpty() + @IsString() + lastname: string; + + /** + * The updated contact's address. + */ + @IsOptional() + @IsObject() + address?: Address; + + /** + * The updated contact's phone number. + * This will be validated against the configured country phone numer syntax (default: international). + */ + @IsOptional() + @IsPhoneNumber(config.phone_validation_countrycode) + phone?: string; + + /** + * The updated contact's email address. + */ + @IsOptional() + @IsEmail() + email?: string; + + /** + * The updated contacts's groups' ids. + * You can provide either one groupId or an array of groupIDs. + */ + @IsOptional() + groups?: number[] | number + + + /** + * Get's all groups for this contact by their id's; + */ + public async getGroups(): Promise { + if (!this.groups) { return null; } + let groups = new Array(); + if (!Array.isArray(this.groups)) { + this.groups = [this.groups] + } + for (let group of this.groups) { + let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group }); + if (!found) { throw new RunnerGroupNotFoundError(); } + groups.push(found); + } + return groups; + } + + /** + * Updates a provided Donor entity based on this. + * @param contact the contact you want to update. + */ + public async update(contact: GroupContact): Promise { + contact.firstname = this.firstname; GroupContact + contact.middlename = this.middlename; + contact.lastname = this.lastname; + contact.phone = this.phone; + contact.email = this.email; + if (!this.address) { contact.address.reset(); } + else { contact.address = this.address; } + Address.validate(contact.address); + contact.groups = await this.getGroups(); + + return contact; + } +} \ No newline at end of file diff --git a/src/models/entities/Address.ts b/src/models/entities/Address.ts index 21ec92a..6526c08 100644 --- a/src/models/entities/Address.ts +++ b/src/models/entities/Address.ts @@ -1,6 +1,4 @@ import { - IsNotEmpty, - IsOptional, IsPostalCode, IsString } from "class-validator"; @@ -18,10 +16,9 @@ export class Address { * The address's first line. * Containing the street and house number. */ - @Column() + @Column({ nullable: true }) @IsString() - @IsNotEmpty() - address1: string; + address1?: string; /** * The address's second line. @@ -29,33 +26,29 @@ export class Address { */ @Column({ nullable: true }) @IsString() - @IsOptional() address2?: string; /** * The address's postal code. * This will get checked against the postal code syntax for the configured country. */ - @Column() + @Column({ nullable: true }) @IsString() - @IsNotEmpty() @IsPostalCode(config.postalcode_validation_countrycode) postalcode: string; /** * The address's city. */ - @Column() + @Column({ nullable: true }) @IsString() - @IsNotEmpty() city: string; /** * The address's country. */ - @Column() + @Column({ nullable: true }) @IsString() - @IsNotEmpty() country: string; public reset() { diff --git a/src/models/entities/GroupContact.ts b/src/models/entities/GroupContact.ts index d2b0abe..dd6eed1 100644 --- a/src/models/entities/GroupContact.ts +++ b/src/models/entities/GroupContact.ts @@ -9,6 +9,7 @@ import { } from "class-validator"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { config } from '../../config'; +import { ResponseGroupContact } from '../responses/ResponseGroupContact'; import { Address } from "./Address"; import { RunnerGroup } from "./RunnerGroup"; @@ -53,7 +54,6 @@ export class GroupContact { * The contact's address. * This is a address object to prevent any formatting differences. */ - @IsOptional() @Column(type => Address) address?: Address; @@ -84,7 +84,7 @@ export class GroupContact { /** * Turns this entity into it's response class. */ - public toResponse() { - return new Error("NotImplemented"); + public toResponse(): ResponseGroupContact { + return new ResponseGroupContact(this); } } \ No newline at end of file diff --git a/src/models/enums/PermissionTargets.ts b/src/models/enums/PermissionTargets.ts index 86c547d..1dac026 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -14,5 +14,6 @@ export enum PermissionTarget { SCAN = 'SCAN', STATION = 'STATION', CARD = 'CARD', - DONATION = 'DONATION' + DONATION = 'DONATION', + CONTACT = 'CONTACT' } \ No newline at end of file diff --git a/src/models/responses/ResponseGroupContact.ts b/src/models/responses/ResponseGroupContact.ts new file mode 100644 index 0000000..291dde5 --- /dev/null +++ b/src/models/responses/ResponseGroupContact.ts @@ -0,0 +1,76 @@ +import { IsInt, IsObject, IsString } from "class-validator"; +import { Address } from '../entities/Address'; +import { GroupContact } from '../entities/GroupContact'; +import { ResponseRunnerGroup } from './ResponseRunnerGroup'; + +/** + * Defines the group contact response. +*/ +export class ResponseGroupContact { + /** + * The contact's id. + */ + @IsInt() + id: number; + + /** + * The contact's first name. + */ + @IsString() + firstname: string; + + /** + * The contact's middle name. + */ + @IsString() + middlename?: string; + + /** + * The contact's last name. + */ + @IsString() + lastname: string; + + /** + * The contact's phone number. + */ + @IsString() + phone?: string; + + /** + * The contact's e-mail address. + */ + @IsString() + email?: string; + + /** + * The contact's associated runner groups. + */ + @IsObject() + groups: ResponseRunnerGroup[]; + + /** + * The contact's address. + * This is a address object to prevent any formatting differences. + */ + @IsObject() + address?: Address; + + /** + * Creates a ResponseGroupContact object from a contact. + * @param contact The contact the response shall be build for. + */ + public constructor(contact: GroupContact) { + this.id = contact.id; + this.firstname = contact.firstname; + this.middlename = contact.middlename; + this.lastname = contact.lastname; + this.phone = contact.phone; + this.email = contact.email; + this.address = contact.address; + this.groups = new Array(); + for (let group of contact.groups) { + this.groups.push(group.toResponse()); + } + } +} diff --git a/src/tests/contacts/contact_add.spec.ts b/src/tests/contacts/contact_add.spec.ts new file mode 100644 index 0000000..0cb5160 --- /dev/null +++ b/src/tests/contacts/contact_add.spec.ts @@ -0,0 +1,216 @@ +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/contacts with errors', () => { + it('creating a new contact without any parameters should return 400', async () => { + const res = await axios.post(base + '/api/contacts', null, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact without a last name should return 400', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "middlename": "middle" + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a invalid phone number should return 400', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "middlename": "middle", + "lastname": "last", + "phone": "123" + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a invalid mail address should return 400', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "string", + "middlename": "string", + "lastname": "string", + "phone": null, + "email": "123", + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with an invalid address 400', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "string", + "middlename": "string", + "lastname": "string", + "address": { + "city": "Testcity" + } + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a invalid group should return 404', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "string", + "middlename": "string", + "lastname": "string", + "groups": 9999999999999 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/contacts working (simple)', () => { + it('creating a new contact with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with all non-relationship optional params should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "middlename": "middle", + "lastname": "last", + "email": "testContact@lauf-fuer-kaya.de", + "phone": "+49017612345678", + "address": { + "address1": "test", + "address2": null, + "city": "herzogenaurach", + "country": "germany", + "postalcode": "91074", + } + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/contacts working (with group)', () => { + let added_org; + let added_team; + let added_contact; + 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); + delete res.data.contact; + delete res.data.teams; + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new team with a parent org should return 200', async () => { + const res = await axios.post(base + '/api/teams', { + "name": "test_team", + "parentGroup": added_org.id + }, axios_config); + delete res.data.contact; + delete res.data.parentGroup; + added_team = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a valid org should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": added_org.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_org] + }); + }); + it('creating a new contact with a valid team should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": added_team.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_team] + }); + }); + it('creating a new contact with a valid org and team should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": [added_org.id, added_team.id] + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_contact = res.data + delete res.data.id; + expect(res.data).toEqual({ + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_org, added_team] + }); + }); + it('checking if the added team\'s contact is the new contact should return 200', async () => { + const res = await axios.get(base + '/api/teams/' + added_team.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.contact.groups; + delete res.data.contact.id; + delete added_contact.groups; + expect(res.data.contact).toEqual(added_contact); + }); +}); \ No newline at end of file diff --git a/src/tests/contacts/contact_delete.spec.ts b/src/tests/contacts/contact_delete.spec.ts new file mode 100644 index 0000000..f8d8c64 --- /dev/null +++ b/src/tests/contacts/contact_delete.spec.ts @@ -0,0 +1,183 @@ +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 res = await axios.delete(base + '/api/contacts/0', axios_config); + expect(res.status).toEqual(204); + }); +}); +// --------------- +describe('add+delete (simple)', () => { + let added_contact; + it('creating a new contact with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete contact', async () => { + const res = await axios.delete(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + let deleted_contact = res.data + expect(deleted_contact).toEqual(added_contact); + }); + it('check if contact really was deleted', async () => { + const res = await axios.get(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('add+delete (with org)', () => { + let added_org; + let added_contact; + 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); + delete res.data.contact; + delete res.data.teams; + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a valid org should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": added_org.id + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('delete contact', async () => { + const res = await axios.delete(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + delete res.data.groups[0].contact; + expect(res.data).toEqual(added_contact); + }); + it('check if contact really was deleted', async () => { + const res = await axios.get(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('add+delete (with team)', () => { + let added_org; + let added_team; + let added_contact; + 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); + delete res.data.contact; + delete res.data.teams; + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new team with a parent org should return 200', async () => { + const res = await axios.post(base + '/api/teams', { + "name": "test_team", + "parentGroup": added_org.id + }, axios_config); + delete res.data.contact; + delete res.data.parentGroup; + added_team = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a valid team should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": added_team.id + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('delete contact', async () => { + const res = await axios.delete(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + delete res.data.groups[0].contact; + expect(res.data).toEqual(added_contact); + }); + it('check if contact really was deleted', async () => { + const res = await axios.get(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('add+delete (with org&team)', () => { + let added_org; + let added_team; + let added_contact; + 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); + delete res.data.contact; + delete res.data.teams; + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new team with a parent org should return 200', async () => { + const res = await axios.post(base + '/api/teams', { + "name": "test_team", + "parentGroup": added_org.id + }, axios_config); + delete res.data.contact; + delete res.data.parentGroup; + added_team = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a valid org and team should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": [added_org.id, added_team.id] + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('delete contact', async () => { + const res = await axios.delete(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + delete res.data.groups[0].contact; + delete res.data.groups[1].contact; + expect(res.data).toEqual(added_contact); + }); + it('check if contact really was deleted', async () => { + const res = await axios.get(base + '/api/contacts/' + added_contact.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/contacts/contact_get.spec.ts b/src/tests/contacts/contact_get.spec.ts new file mode 100644 index 0000000..31aa895 --- /dev/null +++ b/src/tests/contacts/contact_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/contacts', () => { + it('basic get should return 200', async () => { + const res = await axios.get(base + '/api/contacts', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/contacts/0', () => { + it('basic get should return 404', async () => { + const res = await axios.get(base + '/api/contacts/0', axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/contacts after adding', () => { + let added_contact; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('explicit get should return 200', async () => { + const res = await axios.get(base + '/api/contacts/' + added_contact.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + let gotten_donor = res.data + expect(gotten_donor).toEqual(added_contact); + }); + it('get from all runners should return 200', async () => { + const res = await axios.get(base + '/api/contacts/', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + let gotten_donors = res.data + expect(gotten_donors).toContainEqual(added_contact); + }); +}); \ No newline at end of file diff --git a/src/tests/contacts/contact_update.spec.ts b/src/tests/contacts/contact_update.spec.ts new file mode 100644 index 0000000..e6d733d --- /dev/null +++ b/src/tests/contacts/contact_update.spec.ts @@ -0,0 +1,237 @@ +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 contact name after adding', () => { + let added_contact; + it('creating a new contact with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('valid update should return 200', async () => { + let contact_copy = added_contact + contact_copy.firstname = "second" + const res = await axios.put(base + '/api/contacts/' + added_contact.id, contact_copy, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + expect(res.data).toEqual(contact_copy); + }); +}); +// --------------- +describe('Update contact id after adding(should fail)', () => { + let added_contact; + it('creating a new donor with only needed params should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last" + }, axios_config); + added_contact = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('invalid update should return 406', async () => { + added_contact.id++; + const res = await axios.put(base + '/api/contacts/' + (added_contact.id - 1), added_contact, axios_config); + expect(res.status).toEqual(406); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('Update contact group after adding (should work)', () => { + let added_org; + let added_team; + let added_contact; + 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); + delete res.data.contact; + delete res.data.teams; + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new team with a parent org should return 200', async () => { + const res = await axios.post(base + '/api/teams', { + "name": "test_team", + "parentGroup": added_org.id + }, axios_config); + delete res.data.contact; + delete res.data.parentGroup; + added_team = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a valid org should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": added_org.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_contact = res.data + expect(res.data).toEqual({ + "id": res.data.id, + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_org] + }); + }); + it('valid group update to single team should return 200', async () => { + const res = await axios.put(base + '/api/contacts/' + added_contact.id, { + "id": added_contact.id, + "firstname": "first", + "lastname": "last", + "groups": added_team.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual({ + "id": res.data.id, + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_team] + }); + }); + it('valid group update to org and team should return 200', async () => { + const res = await axios.put(base + '/api/contacts/' + added_contact.id, { + "id": added_contact.id, + "firstname": "first", + "lastname": "last", + "groups": [added_org.id, added_team.id] + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual({ + "id": res.data.id, + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_org, added_team] + }); + }); + it('valid group update to none should return 200', async () => { + const res = await axios.put(base + '/api/contacts/' + added_contact.id, { + "id": added_contact.id, + "firstname": "first", + "lastname": "last", + "groups": null + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual({ + "id": res.data.id, + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [] + }); + }); +}); +// --------------- +describe('Update contact group invalid after adding (should fail)', () => { + let added_org; + let added_contact; + 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); + delete res.data.contact; + delete res.data.teams; + added_org = res.data + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('creating a new contact with a valid org should return 200', async () => { + const res = await axios.post(base + '/api/contacts', { + "firstname": "first", + "lastname": "last", + "groups": added_org.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_contact = res.data + expect(res.data).toEqual({ + "id": res.data.id, + "firstname": "first", + "middlename": null, + "lastname": "last", + "phone": null, + "email": null, + "address": { + "address1": null, + "address2": null, + "postalcode": null, + "city": null, + "country": null + }, + "groups": [added_org] + }); + }); + it('invalid group update to single team should return 404', async () => { + const res = await axios.put(base + '/api/contacts/' + added_contact.id, { + "id": added_contact.id, + "firstname": "first", + "lastname": "last", + "groups": 999999999999999 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file