diff --git a/.env.example b/.env.example index 076d8cd..565a391 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,10 @@ APP_PORT=4010 -DB_TYPE=bla +DB_TYPE=sqlite DB_HOST=bla DB_PORT=bla DB_USER=bla DB_PASSWORD=bla -DB_NAME=bla +DB_NAME=./test.sqlite NODE_ENV=production -POSTALCODE_COUNTRYCODE=DE \ No newline at end of file +POSTALCODE_COUNTRYCODE=DE +SEED_TEST_DATA=false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d01f0..07cca56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,45 @@ All notable changes to this project will be documented in this file. Dates are displayed in UTC. +#### [v0.2.1](https://git.odit.services/lfk/backend/compare/v0.2.0...v0.2.1) + +- Created a donation runner response class for the runner selfservice [`88a7089`](https://git.odit.services/lfk/backend/commit/88a7089289e35be4468cb952b311fcb15c54c5a1) +- Readme reorganisation [skip ci] [`e2ec0a3`](https://git.odit.services/lfk/backend/commit/e2ec0a3b64a7388ae85d557dfb66354d70cd1b72) +- Added a seeder for runner test data [`9df9d9a`](https://git.odit.services/lfk/backend/commit/9df9d9ae80277d5ccc753639badb48c4afb13088) +- Created a donation respoinse class for the runner selfservice [`b89f7ac`](https://git.odit.services/lfk/backend/commit/b89f7ac1b4ddd6e53e6e2e8330c1fa2170b48591) +- Added barebones controller for the runner info selfservice [`2274b47`](https://git.odit.services/lfk/backend/commit/2274b476d6caa1de91bb13b6944f8dc233cf446e) +- Implemented a method for getting the runner object from a jwt [`8079769`](https://git.odit.services/lfk/backend/commit/80797698818f456c7746523d5a4f66267fdab10d) +- Added key-value like db table for config flags [`b15967f`](https://git.odit.services/lfk/backend/commit/b15967ff3162e9fe3a634a6f4fc5669f2314cc21) +- Added a /runners/id/scans endpoint [`a82fc0f`](https://git.odit.services/lfk/backend/commit/a82fc0fb9e9c3cbdc6be299b27164c0811e58775) +- Now creating a test contact [`1837336`](https://git.odit.services/lfk/backend/commit/1837336865893ca39d3bc628ff3c57e018a8555d) +- 🧾New changelog file version [CI SKIP] [skip ci] [`02677de`](https://git.odit.services/lfk/backend/commit/02677de5c07b2ac5dcff5567655130ba1b1d48cf) +- The data seeding now only get's triggered on the first time thx to using the key-value [`7bc6030`](https://git.odit.services/lfk/backend/commit/7bc603028dc60d26ffc5327868afbce512966d4d) +- 🧾New changelog file version [CI SKIP] [skip ci] [`3a93c9c`](https://git.odit.services/lfk/backend/commit/3a93c9c078af38ba837b55bf4590867dfd401955) +- Added a "onlyValid" query param [`b5f3dec`](https://git.odit.services/lfk/backend/commit/b5f3dec93bfe4180abbe9ce74094cb1269d0e686) +- Added a citizen org seeder [`2db6510`](https://git.odit.services/lfk/backend/commit/2db6510a8ad83300b286a3bd35ca4db103da72d1) +- 🧾New changelog file version [CI SKIP] [skip ci] [`d528134`](https://git.odit.services/lfk/backend/commit/d5281348b6f3bd6f2e6936ee4497860699b8c3c6) +- 📖New license file version [CI SKIP] [skip ci] [`d8b6669`](https://git.odit.services/lfk/backend/commit/d8b6669d126e64d9e434b5f841ae17a02117822b) +- Added get tests for the /runner/scans endpoint [`26dff4f`](https://git.odit.services/lfk/backend/commit/26dff4f41829e8571231aff3c5d0e3a7c53559d8) +- Beautified import [`c5f7cb2`](https://git.odit.services/lfk/backend/commit/c5f7cb2c68dbee0ab1e0361754f4d4b876666c82) +- Added sqlite as to env.sample db of choice [skip ci] [`f4668b6`](https://git.odit.services/lfk/backend/commit/f4668b6e81d7aeac62e24291ffcb39b00ea44aac) +- 🚀Bumped version to v0.2.1 [`6de9d54`](https://git.odit.services/lfk/backend/commit/6de9d547b736c4538dac5254353d483576337290) +- Merge pull request 'Runner scans endpoint feature/113-runner_scans' (#116) from feature/113-runner_scans into dev [`36d01a0`](https://git.odit.services/lfk/backend/commit/36d01a0a890eb74428679ec6c4fcb14708aaa9fe) +- Merge pull request 'Runner selfservice info endpoint feature/111-runner_selfservic_info' (#115) from feature/111-runner_selfservic_info into dev [`1717df1`](https://git.odit.services/lfk/backend/commit/1717df113edeab2d2628041ee1eccc27380fd379) +- Merge pull request 'Implemented more seeding feature/110-seeding' (#114) from feature/110-seeding into dev [`886c109`](https://git.odit.services/lfk/backend/commit/886c1092d60f8e39357e3b841ed01bb082ede2c4) +- Implemented the get part of the runner selfservice (no jwts are availdable yet (tm) [`da1fe34`](https://git.odit.services/lfk/backend/commit/da1fe34249a741115c1aeedcade16c5c852e896b) +- Fixed the bool converter for null values [`e12aedd`](https://git.odit.services/lfk/backend/commit/e12aedd1aad6de1f934e9593dda4607a303b2eb5) +- Added a config option for test data seeding [`67ba489`](https://git.odit.services/lfk/backend/commit/67ba489fe2f2a2706d640a668cd0e675ded6a7df) +- SEED_TEST_DATA is now false by default [`8870ebd`](https://git.odit.services/lfk/backend/commit/8870ebdb5e6d9045222440abc2c047929a74b520) +- Updated the openapi description [`1915697`](https://git.odit.services/lfk/backend/commit/191569792c9a5cee93718555bba4e7679e4391af) +- Fixed wrong amount calculation [`4ee8079`](https://git.odit.services/lfk/backend/commit/4ee807973e1995681ec549f7c482bc5514a6ec55) +- Added bool conversion for testdata seeding env var [`c18012f`](https://git.odit.services/lfk/backend/commit/c18012f65a704e07acd56870c9ed9f6d06cf97a9) +- Now also seeding runners to the test org [`eab0e63`](https://git.odit.services/lfk/backend/commit/eab0e634a26c1a80e7fa2ccb9dc368f0760b2fd8) + #### [v0.2.0](https://git.odit.services/lfk/backend/compare/v0.1.1...v0.2.0) -- 🧾New changelog file version [CI SKIP] [skip ci] [`8960aa5`](https://git.odit.services/lfk/backend/commit/8960aa5545ddeb57d4ef42c21c0ca6001dfeaea9) -- 🚀Bumped version to v0.2.0 [`ddafd90`](https://git.odit.services/lfk/backend/commit/ddafd90d3e41fb9ee37172a8306c30d8483dfe2c) -- Merge pull request 'Implemented group contacts feature/104-contacts' (#108) from feature/104-contacts into dev [`a0c2b5a`](https://git.odit.services/lfk/backend/commit/a0c2b5ade8d198ec16d33b39e47205e8b03a669f) +> 20 January 2021 + +- Merge pull request 'Alpha Release 0.2.0' (#109) from dev into main [`dd3d93e`](https://git.odit.services/lfk/backend/commit/dd3d93edc7db7ca7f133cb2d8f60c3eaf30bcbf0) - Updated contact update tests [`c3d008e`](https://git.odit.services/lfk/backend/commit/c3d008ec0ff92f80addbdb93ffc1fa2b3278a8a6) - Added contact delete tests [`dd7e5da`](https://git.odit.services/lfk/backend/commit/dd7e5dae368a8decd79357f658dda2164fa6f1e7) - Added contact add valid tests [`e165f01`](https://git.odit.services/lfk/backend/commit/e165f019307e7745357493eacf3e2fa31538122b) @@ -29,6 +63,7 @@ All notable changes to this project will be documented in this file. Dates are d - Implemented contact updateing [`28fb983`](https://git.odit.services/lfk/backend/commit/28fb9834e18bde012c5b51cc49a39585d20f7cc1) - Fixed key null constraint [`de82437`](https://git.odit.services/lfk/backend/commit/de824375d3a1da6ee4d78ea39b7da66fc05f2a02) - Implemented contact posting [`11af9c0`](https://git.odit.services/lfk/backend/commit/11af9c02d977dcd6919652256dbdb9fd5438cabd) +- 🧾New changelog file version [CI SKIP] [skip ci] [`8960aa5`](https://git.odit.services/lfk/backend/commit/8960aa5545ddeb57d4ef42c21c0ca6001dfeaea9) - Implemented contact group setting on creation [`3b06d1a`](https://git.odit.services/lfk/backend/commit/3b06d1a6ef3c95eb5bb7d485accddabba0a8e4f7) - 🧾New changelog file version [CI SKIP] [skip ci] [`32e054e`](https://git.odit.services/lfk/backend/commit/32e054eb84c869210fd483583ae5a6d0e2249cf9) - Switched Address to embedded entity [`7fbe649`](https://git.odit.services/lfk/backend/commit/7fbe649dc90f4bb9f240c5a80fed447048e5e105) @@ -39,11 +74,14 @@ All notable changes to this project will be documented in this file. Dates are d - Fixed donor address check [`4824547`](https://git.odit.services/lfk/backend/commit/4824547dde4d7f90e9e2377a26df34cabf082fdb) - Updated contact delete tests [`8ae53f1`](https://git.odit.services/lfk/backend/commit/8ae53f1c4930e2fd72eb230a5314336f3a45a611) - Added address to contact response [`09e429f`](https://git.odit.services/lfk/backend/commit/09e429fc676c7dd370bba0495b072f81867bd250) -- Updated comments [`a4e8311`](https://git.odit.services/lfk/backend/commit/a4e8311cbd22588ecb4dc2fdbe05397b07d336f8) - Updated the responseclasses to use the new address implementation [`dafac06`](https://git.odit.services/lfk/backend/commit/dafac06bc84d1b237096a561b3adcd3ca5cb1dd8) +- Added address validity check [`ae7c5ff`](https://git.odit.services/lfk/backend/commit/ae7c5ff0c387e9337d01a9dd819a4dddc208f6dd) +- 🧾New changelog file version [CI SKIP] [skip ci] [`da9a359`](https://git.odit.services/lfk/backend/commit/da9a3592510eacd2d67a127dc61e954343e0444b) +- 🚀Bumped version to v0.2.0 [`ddafd90`](https://git.odit.services/lfk/backend/commit/ddafd90d3e41fb9ee37172a8306c30d8483dfe2c) +- Merge pull request 'Implemented group contacts feature/104-contacts' (#108) from feature/104-contacts into dev [`a0c2b5a`](https://git.odit.services/lfk/backend/commit/a0c2b5ade8d198ec16d33b39e47205e8b03a669f) +- Updated comments [`a4e8311`](https://git.odit.services/lfk/backend/commit/a4e8311cbd22588ecb4dc2fdbe05397b07d336f8) - Removed (now useless) relations [`673dea2`](https://git.odit.services/lfk/backend/commit/673dea2e5754e99ff77f7556d4fc03d4cca28a94) - Added missing id property [`6b4b16c`](https://git.odit.services/lfk/backend/commit/6b4b16c13b0c2f55745ded3431cad2f4986be296) -- Added address validity check [`ae7c5ff`](https://git.odit.services/lfk/backend/commit/ae7c5ff0c387e9337d01a9dd819a4dddc208f6dd) - 🧾New changelog file version [CI SKIP] [skip ci] [`f53894b`](https://git.odit.services/lfk/backend/commit/f53894b16ac1c06ecbeeb0b63a56ac438b2fbe1b) - Updated comments [`8bc01d3`](https://git.odit.services/lfk/backend/commit/8bc01d3f2406ce8e58c2ab2963c858495c510dcf) - Fixed contact cascading [`179c2a5`](https://git.odit.services/lfk/backend/commit/179c2a5157fca036acf8d0e6a51821d377860bc1) diff --git a/README.md b/README.md index f974523..985a69f 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,18 @@ Backend Server +## Quickstart 🐳 +> Use this to run the backend with a postgresql db in docker + +1. Clone the repo or copy the docker-compose +2. Run in toe folder that contains the docker-compose file: `docker-compose up -d` +3. Visit http://127.0.0.1:4010/api/docs to check if the server is running +4. You can now use the default admin user (`demo:demo`) + ## Dev Setup 🛠 +> Local dev setup utilizing sqlite3 as the database. -### Local w/ sqlite - -1. Create a .env file in the project root containing: - ``` - APP_PORT=4010 - DB_TYPE=sqlite - DB_HOST=bla - DB_PORT=bla - DB_USER=bla - DB_PASSWORD=bla - DB_NAME=./test.sqlite - ``` +1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed) 2. Install Dependencies ```bash yarn @@ -25,15 +23,21 @@ Backend Server yarn dev ``` -### Generate Docs -``` -yarn docs -``` - -### Docker w/ postgres 🐳 - +### Run Tests ```bash -docker-compose up --build +# Run tests once (server has to run) +yarn test + +# Run test in watch mode (reruns on change) +yarn test:watch + +# Run test in ci mode (automaticly starts the dev server) +yarn test:ci +``` + +### Generate Docs +```bash +yarn docs ``` ## Recommended Editor @@ -42,22 +46,19 @@ docker-compose up --build ### Recommended Extensions -- will be automatically recommended via ./vscode/extensions.json +* will be automatically recommended via ./vscode/extensions.json -## Branches -- main: Protected "release" branch -- dev: Current dev branch for merging the different features - only push for merges or minor changes! -- feature/xyz: Feature branches - `feature/issueid-title` -- bugfix/xyz: Branches for bugfixes - `bugfix/issueid-title` (no id for readme changes needed) - - -## File Structure - -- src/models/entities\* - database models (typeorm entities) -- src/models/actions\* - actions models -- src/models/responses\* - response models -- src/controllers/\* - routing-controllers -- src/loaders/\* - loaders for the different init steps of the api server -- src/middlewares/\* - express middlewares (mainly auth r/n) -- src/errors/* - our custom (http) errors -- src/routes/\* - express routes for everything we don't do via routing-controllers (depreciated) \ No newline at end of file +## Staging +### Branches & Tags +* vX.Y.Z: Release tags created from the main branch + * The version numbers follow the semver standard + * A new release tag automaticly triggers the release ci pipeline +* main: Protected "release" branch + * The latest tag of the docker image get's build from this + * New releases get created as tags from this +* dev: Current dev branch for merging the different feature branches and bugfixes + * The dev tag of the docker image get's build from this + * Only push minor changes to this branch! + * To merge a feature branch into this please create a pull request +* feature/xyz: Feature branches - nameing scheme: `feature/issueid-title` +* bugfix/xyz: Branches for bugfixes - nameing scheme:`bugfix/issueid-title` \ No newline at end of file diff --git a/package.json b/package.json index 718d8d0..ade8a84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@odit/lfk-backend", - "version": "0.2.0", + "version": "0.2.1", "main": "src/app.ts", "repository": "https://git.odit.services/lfk/backend", "author": { diff --git a/src/config.ts b/src/config.ts index 580c90d..3bdcd27 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,8 @@ export const config = { jwt_secret: process.env.JWT_SECRET || "secretjwtsecret", phone_validation_countrycode: getPhoneCodeLocale(), postalcode_validation_countrycode: getPostalCodeLocale(), - version: process.env.VERSION || require('../package.json').version + version: process.env.VERSION || require('../package.json').version, + seedTestData: getDataSeeding() } let errors = 0 if (typeof config.internal_port !== "number") { @@ -30,4 +31,11 @@ function getPostalCodeLocale(): any { return null; } } +function getDataSeeding(): Boolean { + try { + return JSON.parse(process.env.SEED_TEST_DATA); + } catch (error) { + return false; + } +} export let e = errors \ No newline at end of file diff --git a/src/controllers/RunnerController.ts b/src/controllers/RunnerController.ts index 389697b..fbaa381 100644 --- a/src/controllers/RunnerController.ts +++ b/src/controllers/RunnerController.ts @@ -8,6 +8,8 @@ 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 { ResponseScan } from '../models/responses/ResponseScan'; +import { ResponseTrackScan } from '../models/responses/ResponseTrackScan'; import { DonationController } from './DonationController'; import { RunnerCardController } from './RunnerCardController'; import { ScanController } from './ScanController'; @@ -49,6 +51,31 @@ export class RunnerController { return new ResponseRunner(runner); } + @Get('/:id/scans') + @Authorized(["RUNNER:GET", "SCAN:GET"]) + @ResponseSchema(ResponseScan, { isArray: true }) + @ResponseSchema(ResponseTrackScan, { isArray: true }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Lists all scans of the runner whose id got provided.
If you only want the valid scans just add the ?onlyValid=true query param.' }) + async getScans(@Param('id') id: number, onlyValid?: boolean) { + let responseScans: ResponseScan[] = new Array(); + let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'scans.track', 'scans.station', 'scans.runner'] }) + if (!runner) { throw new RunnerNotFoundError(); } + + if (!onlyValid) { + for (let scan of runner.scans) { + responseScans.push(scan.toResponse()); + } + } + else { + for (let scan of runner.validScans) { + responseScans.push(scan.toResponse()); + } + } + + return responseScans; + } + @Post() @Authorized("RUNNER:CREATE") @ResponseSchema(ResponseRunner) diff --git a/src/controllers/RunnerSelfServiceController.ts b/src/controllers/RunnerSelfServiceController.ts new file mode 100644 index 0000000..b2d2539 --- /dev/null +++ b/src/controllers/RunnerSelfServiceController.ts @@ -0,0 +1,49 @@ +import * as jwt from "jsonwebtoken"; +import { Get, JsonController, OnUndefined, Param } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { config } from '../config'; +import { InvalidCredentialsError } from '../errors/AuthError'; +import { RunnerNotFoundError } from '../errors/RunnerErrors'; +import { Runner } from '../models/entities/Runner'; +import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner'; + + +@JsonController('/runners') +export class RunnerSelfServiceController { + private runnerRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.runnerRepository = getConnectionManager().get().getRepository(Runner); + } + + @Get('/me/:jwt') + @ResponseSchema(ResponseSelfServiceRunner) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @OnUndefined(RunnerNotFoundError) + @OpenAPI({ description: 'Lists all information about yourself.
Please provide your runner jwt(that code we gave you during registration) for auth.
If you lost your jwt/personalized link please contact support.' }) + async get(@Param('jwt') token: string) { + return (new ResponseSelfServiceRunner(await this.getRunner(token))); + } + + /** + * Get's a runner by a provided jwt token. + * @param token The runner jwt provided by the runner to identitfy themselves. + */ + private async getRunner(token: string): Promise { + let jwtPayload = undefined + try { + jwtPayload = jwt.verify(token, config.jwt_secret); + } catch (error) { + throw new InvalidCredentialsError(); + } + + const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }); + if (!runner) { throw new RunnerNotFoundError() } + return runner; + } + +} \ No newline at end of file diff --git a/src/loaders/database.ts b/src/loaders/database.ts index 9dda78d..0b4a16a 100644 --- a/src/loaders/database.ts +++ b/src/loaders/database.ts @@ -1,6 +1,9 @@ import { createConnection } from "typeorm"; import { runSeeder } from 'typeorm-seeding'; -import { User } from '../models/entities/User'; +import { config } from '../config'; +import { ConfigFlag } from '../models/entities/ConfigFlags'; +import SeedPublicOrg from '../seeds/SeedPublicOrg'; +import SeedTestRunners from '../seeds/SeedTestRunners'; import SeedUsers from '../seeds/SeedUsers'; /** * Loader for the database that creates the database connection and initializes the database tabels. @@ -9,8 +12,20 @@ import SeedUsers from '../seeds/SeedUsers'; export default async () => { const connection = await createConnection(); await connection.synchronize(); - if (await connection.getRepository(User).count() === 0) { + + //The data seeding part + if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:user", value: "true" }))) { await runSeeder(SeedUsers); + await connection.getRepository(ConfigFlag).save({ option: "seeded:user", value: "true" }); } + if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:citizenorg", value: "true" }))) { + await runSeeder(SeedPublicOrg); + await connection.getRepository(ConfigFlag).save({ option: "seeded:citizenorg", value: "true" }); + } + if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:testdata", value: "true" })) && config.seedTestData == true) { + await runSeeder(SeedTestRunners); + await connection.getRepository(ConfigFlag).save({ option: "seeded:testdata", value: "true" }); + } + return connection; }; \ No newline at end of file diff --git a/src/models/entities/ConfigFlags.ts b/src/models/entities/ConfigFlags.ts new file mode 100644 index 0000000..3dd6c69 --- /dev/null +++ b/src/models/entities/ConfigFlags.ts @@ -0,0 +1,27 @@ +import { + IsNotEmpty, + IsString +} from "class-validator"; +import { Column, Entity, PrimaryColumn } from "typeorm"; + +/** + * Defines the ConfigFlag entity. + * This entity can be used to set some flags on db init. +*/ +@Entity() +export class ConfigFlag { + /** + * The flag's name (primary). + */ + @PrimaryColumn() + @IsString() + option: string; + + /** + * The flag's value. + */ + @Column() + @IsString() + @IsNotEmpty() + value: string; +} diff --git a/src/models/entities/Runner.ts b/src/models/entities/Runner.ts index bc6bc5a..266abb9 100644 --- a/src/models/entities/Runner.ts +++ b/src/models/entities/Runner.ts @@ -65,7 +65,7 @@ export class Runner extends Participant { */ @IsInt() public get distanceDonationAmount(): number { - return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance; + return this.distanceDonations.reduce((sum, current) => sum + current.amount, 0); } /** diff --git a/src/models/responses/ResponseSelfServiceDonation.ts b/src/models/responses/ResponseSelfServiceDonation.ts new file mode 100644 index 0000000..4f10f3c --- /dev/null +++ b/src/models/responses/ResponseSelfServiceDonation.ts @@ -0,0 +1,36 @@ +import { IsInt, IsNotEmpty, IsPositive } from 'class-validator'; +import { DistanceDonation } from '../entities/DistanceDonation'; + +/** + * Defines the runner selfservice donation response. + * Why? B/C runner's are not allowed to view all information available to admin users. +*/ +export class ResponseSelfServiceDonation { + /** + * The donation's donor. + */ + @IsNotEmpty() + donor: string; + + /** + * The donation's amount in the smalles unit of your currency (default: euro cent). + */ + @IsInt() + amount: number; + + /** + * 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; + + public constructor(donation: DistanceDonation) { + if (!donation.donor.middlename) { this.donor = donation.donor.firstname + " " + donation.donor.lastname; } + else { this.donor = donation.donor.firstname + " " + donation.donor.middlename + " " + donation.donor.lastname; } + + this.amountPerDistance = donation.amountPerDistance; + this.amount = donation.amount; + } +} \ No newline at end of file diff --git a/src/models/responses/ResponseSelfServiceRunner.ts b/src/models/responses/ResponseSelfServiceRunner.ts new file mode 100644 index 0000000..6727c76 --- /dev/null +++ b/src/models/responses/ResponseSelfServiceRunner.ts @@ -0,0 +1,75 @@ +import { IsInt, IsString } from "class-validator"; +import { DistanceDonation } from '../entities/DistanceDonation'; +import { Runner } from '../entities/Runner'; +import { RunnerGroup } from '../entities/RunnerGroup'; +import { RunnerTeam } from '../entities/RunnerTeam'; +import { ResponseParticipant } from './ResponseParticipant'; +import { ResponseSelfServiceDonation } from './ResponseSelfServiceDonation'; + +/** + * Defines the runner selfservice response. + * Why? B/C runner's are not allowed to view all information available to admin users. +*/ +export class ResponseSelfServiceRunner extends ResponseParticipant { + + /** + * The runner's currently ran distance in meters. + */ + @IsInt() + distance: number; + + /** + * The runner's currently collected donations. + */ + @IsInt() + donationAmount: number; + + /** + * The runner's group as a string (mix of org and team). + */ + @IsString() + group: string; + + /** + * The runner's associated donations. + */ + @IsString() + donations: ResponseSelfServiceDonation[] + + /** + * Creates a ResponseRunner object from a runner. + * @param runner The user the response shall be build for. + */ + public constructor(runner: Runner) { + super(runner); + this.distance = runner.distance; + this.donationAmount = runner.distanceDonationAmount; + this.group = this.getTeamString(runner.group); + this.donations = this.getDonations(runner.distanceDonations); + } + + /** + * Parses a runner's group into a string. + * If the runner's group is a team: `org name/team name` + * If the runner's group is an org: `org name` + * @param group The group that shall get parsed to a string. + */ + private getTeamString(group: RunnerGroup): string { + if (group instanceof RunnerTeam) { + return group.parentGroup.name + "/" + group.name; + } + return group.name; + } + + /** + * Converts all of the runner's donations to ResponseSelfServiceDonations. + * @param donations The donations that shall be converted to ResponseSelfServiceDonations. + */ + private getDonations(donations: DistanceDonation[]): ResponseSelfServiceDonation[] { + let responseDonations = new Array(); + for (let donation of donations) { + responseDonations.push(new ResponseSelfServiceDonation(donation)); + } + return responseDonations; + } +} diff --git a/src/seeds/SeedPublicOrg.ts b/src/seeds/SeedPublicOrg.ts new file mode 100644 index 0000000..57685f8 --- /dev/null +++ b/src/seeds/SeedPublicOrg.ts @@ -0,0 +1,15 @@ +import { Connection } from 'typeorm'; +import { Factory, Seeder } from 'typeorm-seeding'; +import { CreateRunnerOrganisation } from '../models/actions/create/CreateRunnerOrganisation'; +import { RunnerOrganisation } from '../models/entities/RunnerOrganisation'; + +/** + * Seeds the public runner org (named: "Citizen" by default). + */ +export default class SeedPublicOrg implements Seeder { + public async run(factory: Factory, connection: Connection): Promise { + let publicOrg = new CreateRunnerOrganisation(); + publicOrg.name = "Citizen"; + await connection.getRepository(RunnerOrganisation).save(await publicOrg.toEntity()); + } +} \ No newline at end of file diff --git a/src/seeds/SeedTestRunners.ts b/src/seeds/SeedTestRunners.ts new file mode 100644 index 0000000..14725c6 --- /dev/null +++ b/src/seeds/SeedTestRunners.ts @@ -0,0 +1,93 @@ +import { Connection } from 'typeorm'; +import { Factory, Seeder } from 'typeorm-seeding'; +import { CreateGroupContact } from '../models/actions/create/CreateGroupContact'; +import { CreateRunner } from '../models/actions/create/CreateRunner'; +import { CreateRunnerOrganisation } from '../models/actions/create/CreateRunnerOrganisation'; +import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam'; +import { Address } from '../models/entities/Address'; +import { GroupContact } from '../models/entities/GroupContact'; +import { Runner } from '../models/entities/Runner'; +import { RunnerGroup } from '../models/entities/RunnerGroup'; +import { RunnerOrganisation } from '../models/entities/RunnerOrganisation'; +import { RunnerTeam } from '../models/entities/RunnerTeam'; + +/** + * Seeds a test runner org with a test runner team ans some test runners. + * Usefull for testing or demo instances. + */ +export default class SeedTestRunners implements Seeder { + public async run(factory: Factory, connection: Connection): Promise { + let testOrg: RunnerOrganisation = await this.createTestOrg(connection); + let testTeam: RunnerTeam = await this.createTestTeam(connection, testOrg); + await this.createTestContact(connection, testOrg); + await this.createTestRunners(connection, testOrg); + await this.createTestRunners(connection, testTeam); + } + + public async createTestOrg(connection: Connection): Promise { + let testOrg = new CreateRunnerOrganisation(); + testOrg.name = "Test Org"; + + testOrg.address = new Address(); + testOrg.address.address1 = "Test street 1"; + testOrg.address.city = "Herzogenaurach"; + testOrg.address.country = "Germany"; + testOrg.address.postalcode = "90174"; + + return await connection.getRepository(RunnerOrganisation).save(await testOrg.toEntity()); + } + + public async createTestTeam(connection: Connection, org: RunnerOrganisation): Promise { + let testTeam = new CreateRunnerTeam(); + testTeam.name = "Test Team"; + testTeam.parentGroup = org.id; + + return await connection.getRepository(RunnerTeam).save(await testTeam.toEntity()); + } + + public async createTestRunners(connection: Connection, group: RunnerGroup) { + for (let first of this.firstnames) { + for (let last of this.lastnames) { + let runner = new CreateRunner; + runner.firstname = first; + runner.lastname = last; + runner.middlename = group.name; + runner.group = group.id; + await connection.getRepository(Runner).save(await runner.toEntity()); + } + } + } + + public async createTestContact(connection: Connection, group: RunnerGroup) { + let contact = new CreateGroupContact; + contact.firstname = "Test"; + contact.lastname = "Contact"; + contact.email = "test.contact@dev.lauf-fuer-kaya.de"; + contact.groups = group.id; + + contact.address = new Address(); + contact.address.address1 = "First Contact Street 100"; + contact.address.city = "Herzogenaurach"; + contact.address.country = "Germany"; + contact.address.postalcode = "90174"; + + await connection.getRepository(GroupContact).save(await contact.toEntity()); + } + + private firstnames = [ + "Peter", + "Matze", + "Tine", + "Uta", + "Fabian", + "Unicode:ÖÄ?✔⚠" + ] + + private lastnames = [ + "Muster", + "Example", + "Müller", + "Unicode:搆Ǩ>ÙՠƳ|" + ] + +} \ No newline at end of file diff --git a/src/tests/scans/scans_get.spec.ts b/src/tests/scans/scans_get.spec.ts index da7d737..a4128b7 100644 --- a/src/tests/scans/scans_get.spec.ts +++ b/src/tests/scans/scans_get.spec.ts @@ -61,9 +61,17 @@ describe('adding + getting scans', () => { expect(res.status).toEqual(200); expect(res.headers['content-type']).toContain("application/json") }); - it('check if scans was added (no parameter validation)', async () => { + it('check if scans was added directly', async () => { const res = await axios.get(base + '/api/scans/' + added_scan.id, axios_config); expect(res.status).toEqual(200); expect(res.headers['content-type']).toContain("application/json"); + expect(res.data).toEqual(added_scan); + }); + it('check if scans was added via the runner/scans endpoint.', async () => { + const res = await axios.get(base + '/api/runners/' + added_runner.id + "/scans", axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_scan.runner.distance = 0; + expect(res.data).toContainEqual(added_scan); }); }); \ No newline at end of file