From f7ef3f44f93fd10e9d4498e27c3aaccd0b362c18 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 26 Jun 2021 15:35:31 +0200 Subject: [PATCH 01/40] refactor: new Astraservice --- src/astra/astra.service.ts | 148 +++++++++++++++++++++++++++++++++ src/standup/standup.module.ts | 5 +- src/standup/standup.service.ts | 43 +++++----- 3 files changed, 173 insertions(+), 23 deletions(-) create mode 100644 src/astra/astra.service.ts diff --git a/src/astra/astra.service.ts b/src/astra/astra.service.ts new file mode 100644 index 00000000..bf058848 --- /dev/null +++ b/src/astra/astra.service.ts @@ -0,0 +1,148 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { from, Observable } from 'rxjs'; +import { + deleteItem, + documentId, + findResult, +} from '@cahllagerfeld/nestjs-astra'; +const DATASTAX_CLIENT = 'DATASTAX_CLIENT'; + +@Injectable() +export class AstraService { + private collection: any; + constructor(@Inject(DATASTAX_CLIENT) private readonly client: any) {} + + private setupClient(namespace: string, collection: string) { + this.collection = this.client.namespace(namespace).collection(collection); + console.log(this.collection); + } + + /** + * Gets a document from a collection by its ID. + * @param id ID of the document, that should be retrieved + * @returns + */ + public get( + id: string, + namespace: string, + collection: string, + ): Observable { + this.setupClient(namespace, collection); + const response: Promise = this.collection.get(id); + return from(response); + } + + /** + * Creates a new Document + * @param document The document that should be created + * @param id The desired ID + * @returns document ID of created document + */ + public create( + document: T, + namespace: string, + collection: string, + id?: string, + ): Observable { + this.setupClient(namespace, collection); + let promise: Promise; + if (!id) { + promise = this.collection.create(document); + return from(promise); + } + promise = this.collection.create(id, document); + return from(promise); + } + + /** + * Search a collection + * @param query The query for searching the collection + * @param options Possible searchoptions + * @returns + */ + public find( + namespace: string, + collection: string, + query?: any, + options?: any, + ): Observable | null> { + this.setupClient(namespace, collection); + const promise: Promise | null> = this.collection.find( + query, + options, + ); + return from(promise); + } + + /** + * Find a single document + * @param query The query for searching the collection + * @param options Possible searchoptions + * @returns + */ + public findOne( + query: any, + namespace: string, + collection: string, + options?: any, + ): Observable { + this.setupClient(namespace, collection); + const promise: Promise = this.collection.findOne(query, options); + return from(promise); + } + + /** + * Partially update a existing document + * @param path Path to document, may also be path to a subdocument + * @param document Document with which the existing should be updated + * @returns + */ + public update( + path: string, + document: T, + namespace: string, + collection: string, + ): Observable { + this.setupClient(namespace, collection); + const promise: Promise = this.collection.update( + path, + document, + ); + return from(promise); + } + + /** + * + * @param path Path to document, that should be replaced + * @param document Document with which the specified docuent should be updated + * @returns + */ + public replace( + path: string, + document: T, + namespace: string, + collection: string, + ): Observable { + this.setupClient(namespace, collection); + const promise: Promise = this.collection.replace( + path, + document, + ); + return from(promise); + } + + /** + * + * @param path Path to document, that should be deleted + * @returns + */ + public delete( + path: string, + namespace: string, + collection: string, + ): Observable { + this.setupClient(namespace, collection); + const promise: Promise = this.collection.delete(path); + return from(promise); + } +} diff --git a/src/standup/standup.module.ts b/src/standup/standup.module.ts index b8013fd9..2ba2cda7 100644 --- a/src/standup/standup.module.ts +++ b/src/standup/standup.module.ts @@ -3,13 +3,14 @@ import { AuthModule } from '../auth/auth.module'; import { StandupController } from './standup.controller'; import { StandupService } from './standup.service'; import { AstraModule } from '@cahllagerfeld/nestjs-astra'; +import { AstraService } from '../astra/astra.service'; @Module({ imports: [ AuthModule, - AstraModule.forFeature({ namespace: 'eddiehub', collection: 'standup' }), + // AstraModule.forFeature({ namespace: 'eddiehub', collection: 'standup' }), ], controllers: [StandupController], - providers: [StandupService], + providers: [StandupService, AstraService], }) export class StandupModule {} diff --git a/src/standup/standup.service.ts b/src/standup/standup.service.ts index e1ac0134..10cb4233 100644 --- a/src/standup/standup.service.ts +++ b/src/standup/standup.service.ts @@ -1,8 +1,4 @@ -import { - AstraService, - deleteItem, - documentId, -} from '@cahllagerfeld/nestjs-astra'; +import { deleteItem, documentId } from '@cahllagerfeld/nestjs-astra'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { StandupDTO } from './dto/standup.dto'; import { Standup } from './interfaces/standup.interface'; @@ -10,6 +6,7 @@ import { catchError, concatMap, filter } from 'rxjs/operators'; import { Author } from '../auth/getAuthorFromHeaders.decorator'; import { ValidationService } from '../auth/header-validation.service'; import { from } from 'rxjs'; +import { AstraService } from '../astra/astra.service'; @Injectable() export class StandupService { @@ -28,25 +25,29 @@ export class StandupService { createdOn: new Date(Date.now()), }; - return this.astraService.create(newStandup).pipe( - filter((data: documentId) => { - if (data === null) { - throw new HttpException( - 'Creation didnt pass as expected', - HttpStatus.BAD_REQUEST, - ); - } - return true; - }), - ); + return this.astraService + .create(newStandup, 'eddiehub', 'standup') + .pipe( + filter((data: documentId) => { + if (data === null) { + throw new HttpException( + 'Creation didnt pass as expected', + HttpStatus.BAD_REQUEST, + ); + } + return true; + }), + ); } findAll() { - return this.astraService.find().pipe(catchError(() => from([{}]))); + return this.astraService + .find('eddiehub', 'standup') + .pipe(catchError(() => from([{}]))); } findById(id: string) { - return this.astraService.get(id).pipe( + return this.astraService.get(id, 'eddiehub', 'standup').pipe( filter((data: Standup) => { if (data === null) { throw new HttpException( @@ -60,7 +61,7 @@ export class StandupService { } deleteStandup(id: string, authorObject: Author) { - return this.astraService.get(id).pipe( + return this.astraService.get(id, 'eddiehub', 'standup').pipe( filter((data: Standup) => { if (data === null) { throw new HttpException( @@ -84,7 +85,7 @@ export class StandupService { }), concatMap(() => this.astraService - .delete(id) + .delete(id, 'eddiehub', 'standup') .pipe(filter((data: deleteItem) => data.deleted === true)), ), ); @@ -98,7 +99,7 @@ export class StandupService { ); } return this.astraService - .find({ 'author.uid': { $eq: uid } }) + .find('eddiehub', 'standup', { 'author.uid': { $eq: uid } }) .pipe( filter((data) => { if (data === null) { From cfb27a3b3c3a1efabeab119d2a22b0c645b59d70 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 26 Jun 2021 15:43:59 +0200 Subject: [PATCH 02/40] style: remove log --- src/astra/astra.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/astra/astra.service.ts b/src/astra/astra.service.ts index bf058848..0f693a67 100644 --- a/src/astra/astra.service.ts +++ b/src/astra/astra.service.ts @@ -14,7 +14,6 @@ export class AstraService { private setupClient(namespace: string, collection: string) { this.collection = this.client.namespace(namespace).collection(collection); - console.log(this.collection); } /** From 15fa97f643c873923e0af5d9f7afff0b971ebe53 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:35:18 +0200 Subject: [PATCH 03/40] style: add js-doc --- src/astra/astra.service.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/astra/astra.service.ts b/src/astra/astra.service.ts index 0f693a67..f3526f5c 100644 --- a/src/astra/astra.service.ts +++ b/src/astra/astra.service.ts @@ -19,6 +19,8 @@ export class AstraService { /** * Gets a document from a collection by its ID. * @param id ID of the document, that should be retrieved + * @param namespace Namespace in Database + * @param collection Collection-Name in Database * @returns */ public get( @@ -34,7 +36,9 @@ export class AstraService { /** * Creates a new Document * @param document The document that should be created - * @param id The desired ID + * @param namespace Namespace in Database + * @param collection Collection-Name in Database + * @param id The desired ID * * @returns document ID of created document */ public create( @@ -55,6 +59,8 @@ export class AstraService { /** * Search a collection + * @param namespace Namespace in Database + * @param collection Collection-Name in Database * @param query The query for searching the collection * @param options Possible searchoptions * @returns @@ -76,6 +82,8 @@ export class AstraService { /** * Find a single document * @param query The query for searching the collection + * @param namespace Namespace in Database + * @param collection Collection-Name in Database * @param options Possible searchoptions * @returns */ @@ -94,6 +102,8 @@ export class AstraService { * Partially update a existing document * @param path Path to document, may also be path to a subdocument * @param document Document with which the existing should be updated + * @param namespace Namespace in Database + * @param collection Collection-Name in Database * @returns */ public update( @@ -114,6 +124,8 @@ export class AstraService { * * @param path Path to document, that should be replaced * @param document Document with which the specified docuent should be updated + * @param namespace Namespace in Database + * @param collection Collection-Name in Database * @returns */ public replace( @@ -133,6 +145,8 @@ export class AstraService { /** * * @param path Path to document, that should be deleted + * @param namespace Namespace in Database + * @param collection Collection-Name in Database * @returns */ public delete( From 33ed254d64ca62832491bdc5db0f9f09d128c054 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:51:02 +0200 Subject: [PATCH 04/40] refactor: configure namespace by header for standup --- src/standup/standup.controller.ts | 33 +++++++++++++++++++++---------- src/standup/standup.service.ts | 20 +++++++++---------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/standup/standup.controller.ts b/src/standup/standup.controller.ts index d92b69d9..d5b4b99e 100644 --- a/src/standup/standup.controller.ts +++ b/src/standup/standup.controller.ts @@ -1,4 +1,5 @@ import { + Headers, Body, Controller, Delete, @@ -23,24 +24,31 @@ export class StandupController { @Post() @UseGuards(TokenGuard) @ApiSecurity('token') - createStandup(@Body() body: StandupDTO) { - return this.standupService.create(body); + @ApiHeader({ name: 'keyspace', required: true }) + createStandup( + @Body() body: StandupDTO, + @Headers('keyspace') keyspace: string, + ) { + return this.standupService.create(body, keyspace); } @Get() - findAllStandups() { - return this.standupService.findAll(); + @ApiHeader({ name: 'keyspace', required: true }) + findAllStandups(@Headers('keyspace') keyspace: string) { + return this.standupService.findAll(keyspace); } @Get('search') @ApiQuery({ name: 'uid', type: 'string' }) - search(@Query('uid') uid: string) { - return this.standupService.search(uid); + @ApiHeader({ name: 'keyspace', required: true }) + search(@Query('uid') uid: string, @Headers('keyspace') keyspace: string) { + return this.standupService.search(uid, keyspace); } @Get(':id') - findById(@Param('id') id: string) { - return this.standupService.findById(id); + @ApiHeader({ name: 'keyspace', required: true }) + findById(@Param('id') id: string, @Headers('keyspace') keyspace: string) { + return this.standupService.findById(id, keyspace); } @Delete(':id') @@ -49,7 +57,12 @@ export class StandupController { @HttpCode(204) @ApiHeader({ name: 'User-Uid', required: true }) @ApiHeader({ name: 'Platform', required: true }) - deleteStandup(@Param('id') id: string, @AuthorObject() author: Author) { - return this.standupService.deleteStandup(id, author); + @ApiHeader({ name: 'keyspace', required: true }) + deleteStandup( + @Param('id') id: string, + @AuthorObject() author: Author, + @Headers('keyspace') keyspace: string, + ) { + return this.standupService.deleteStandup(id, author, keyspace); } } diff --git a/src/standup/standup.service.ts b/src/standup/standup.service.ts index 10cb4233..ed7045b9 100644 --- a/src/standup/standup.service.ts +++ b/src/standup/standup.service.ts @@ -15,7 +15,7 @@ export class StandupService { private readonly validationService: ValidationService, ) {} - create(body: StandupDTO) { + create(body: StandupDTO, keyspaceName: string) { const { author, todayMessage, yesterdayMessage } = body; const newStandup: Standup = { @@ -26,7 +26,7 @@ export class StandupService { }; return this.astraService - .create(newStandup, 'eddiehub', 'standup') + .create(newStandup, keyspaceName, 'standup') .pipe( filter((data: documentId) => { if (data === null) { @@ -40,14 +40,14 @@ export class StandupService { ); } - findAll() { + findAll(keyspaceName: string) { return this.astraService - .find('eddiehub', 'standup') + .find(keyspaceName, 'standup') .pipe(catchError(() => from([{}]))); } - findById(id: string) { - return this.astraService.get(id, 'eddiehub', 'standup').pipe( + findById(id: string, keyspaceName: string) { + return this.astraService.get(id, keyspaceName, 'standup').pipe( filter((data: Standup) => { if (data === null) { throw new HttpException( @@ -60,8 +60,8 @@ export class StandupService { ); } - deleteStandup(id: string, authorObject: Author) { - return this.astraService.get(id, 'eddiehub', 'standup').pipe( + deleteStandup(id: string, authorObject: Author, keyspaceName: string) { + return this.astraService.get(id, keyspaceName, 'standup').pipe( filter((data: Standup) => { if (data === null) { throw new HttpException( @@ -91,7 +91,7 @@ export class StandupService { ); } - search(uid: string) { + search(uid: string, keyspaceName: string) { if (!uid) { throw new HttpException( 'Please provide search context', @@ -99,7 +99,7 @@ export class StandupService { ); } return this.astraService - .find('eddiehub', 'standup', { 'author.uid': { $eq: uid } }) + .find(keyspaceName, 'standup', { 'author.uid': { $eq: uid } }) .pipe( filter((data) => { if (data === null) { From 26cb288f7bab9d004fc8955203579d1f42fbfc82 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Thu, 15 Jul 2021 08:53:27 +0200 Subject: [PATCH 05/40] feat: JWT-Client-Based Authentication --- package-lock.json | 342 +++++++++++++++++- package.json | 11 +- src/auth/auth.controller.spec.ts | 18 + src/auth/auth.controller.ts | 41 +++ src/auth/auth.module.ts | 10 +- src/auth/auth.service.spec.ts | 18 + src/auth/auth.service.ts | 61 ++++ .../interfaces/token-payload.interface.ts | 5 + src/auth/jwt.strategy.ts | 37 ++ src/auth/token.strategy.ts | 17 +- src/standup/standup.controller.ts | 36 +- src/swagger.ts | 13 +- test/step-definitions/requests.ts | 15 +- 13 files changed, 590 insertions(+), 34 deletions(-) create mode 100644 src/auth/auth.controller.spec.ts create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.service.spec.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/interfaces/token-payload.interface.ts create mode 100644 src/auth/jwt.strategy.ts diff --git a/package-lock.json b/package-lock.json index cc5cb559..e6e873c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "api", - "version": "0.6.10", + "version": "0.6.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "api", - "version": "0.6.10", + "version": "0.6.11", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -15,6 +15,7 @@ "@nestjs/common": "^7.6.13", "@nestjs/config": "^0.6.3", "@nestjs/core": "^7.6.13", + "@nestjs/jwt": "^8.0.0", "@nestjs/mapped-types": "^0.3.0", "@nestjs/passport": "^7.1.5", "@nestjs/platform-express": "^7.6.13", @@ -24,11 +25,13 @@ "date-fns": "^2.21.1", "helmet": "^4.5.0", "passport": "^0.4.1", - "passport-unique-token": "^2.0.0", + "passport-jwt": "^4.0.0", + "passport-unique-token": "^3.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.6", - "swagger-ui-express": "^4.1.6" + "swagger-ui-express": "^4.1.6", + "uuid": "^8.3.2" }, "devDependencies": { "@commitlint/cli": "^12.1.1", @@ -40,7 +43,9 @@ "@types/express": "4.17.11", "@types/jest": "26.0.20", "@types/node": "14.14.31", + "@types/passport-jwt": "^3.0.6", "@types/supertest": "2.0.10", + "@types/uuid": "^8.3.1", "@typescript-eslint/eslint-plugin": "^4.6.1", "@typescript-eslint/parser": "^4.6.1", "chai": "^4.3.0", @@ -1680,6 +1685,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, + "node_modules/@nestjs/jwt": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz", + "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==", + "dependencies": { + "@types/jsonwebtoken": "8.5.4", + "jsonwebtoken": "8.5.1" + }, + "peerDependencies": { + "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.3.0.tgz", @@ -2066,6 +2083,14 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", + "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -2095,6 +2120,36 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@types/passport": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz", + "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz", + "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/prettier": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.1.tgz", @@ -2186,6 +2241,12 @@ "node": ">=0.10.0" } }, + "node_modules/@types/uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", + "dev": true + }, "node_modules/@types/validator": { "version": "13.1.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.3.tgz", @@ -3306,6 +3367,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -4447,6 +4513,14 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7526,6 +7600,32 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -7550,6 +7650,25 @@ "node >=0.6.0" ] }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -7646,6 +7765,41 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "node_modules/lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -8671,6 +8825,15 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "dependencies": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -8680,14 +8843,14 @@ } }, "node_modules/passport-unique-token": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-unique-token/-/passport-unique-token-2.0.0.tgz", - "integrity": "sha512-SVsNT7wZ6QboP1Lm94BWjbwghV3Qoo71w0SZ075e4kGp+ibnFmIywZf1DgoFa5l+eSDzgAjW3h4zWFQbTDEv2A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-unique-token/-/passport-unique-token-3.0.0.tgz", + "integrity": "sha512-BkSWODzwS1i8Z5ImmPQOWZ05dw9oS09VMBIZOogubKACrm3UO3wlJnwT/fCMQh5iTtFLYA+X4yWmtsqufftgXw==", "dependencies": { "passport-strategy": "^1.0.0" }, "engines": { - "node": ">= 10.22.x" + "node": ">= 12" } }, "node_modules/path-exists": { @@ -9651,7 +9814,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, "bin": { "semver": "bin/semver" } @@ -13758,6 +13920,15 @@ } } }, + "@nestjs/jwt": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz", + "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==", + "requires": { + "@types/jsonwebtoken": "8.5.4", + "jsonwebtoken": "8.5.1" + } + }, "@nestjs/mapped-types": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.3.0.tgz", @@ -14125,6 +14296,14 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/jsonwebtoken": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", + "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==", + "requires": { + "@types/node": "*" + } + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -14154,6 +14333,36 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/passport": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz", + "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/passport-jwt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz", + "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/prettier": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.1.tgz", @@ -14244,6 +14453,12 @@ } } }, + "@types/uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", + "dev": true + }, "@types/validator": { "version": "13.1.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.3.tgz", @@ -15206,6 +15421,11 @@ "ieee754": "^1.1.13" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -16138,6 +16358,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -18601,6 +18829,30 @@ "through": ">=2.2.7 <3" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -18621,6 +18873,25 @@ } } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -18699,6 +18970,41 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -19524,15 +19830,24 @@ "pause": "0.0.1" } }, + "passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "requires": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, "passport-unique-token": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-unique-token/-/passport-unique-token-2.0.0.tgz", - "integrity": "sha512-SVsNT7wZ6QboP1Lm94BWjbwghV3Qoo71w0SZ075e4kGp+ibnFmIywZf1DgoFa5l+eSDzgAjW3h4zWFQbTDEv2A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-unique-token/-/passport-unique-token-3.0.0.tgz", + "integrity": "sha512-BkSWODzwS1i8Z5ImmPQOWZ05dw9oS09VMBIZOogubKACrm3UO3wlJnwT/fCMQh5iTtFLYA+X4yWmtsqufftgXw==", "requires": { "passport-strategy": "^1.0.0" } @@ -20304,8 +20619,7 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "send": { "version": "0.17.1", diff --git a/package.json b/package.json index 10835550..1f4d2b53 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@nestjs/common": "^7.6.13", "@nestjs/config": "^0.6.3", "@nestjs/core": "^7.6.13", + "@nestjs/jwt": "^8.0.0", "@nestjs/mapped-types": "^0.3.0", "@nestjs/passport": "^7.1.5", "@nestjs/platform-express": "^7.6.13", @@ -39,11 +40,13 @@ "date-fns": "^2.21.1", "helmet": "^4.5.0", "passport": "^0.4.1", - "passport-unique-token": "^2.0.0", + "passport-jwt": "^4.0.0", + "passport-unique-token": "^3.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.6", - "swagger-ui-express": "^4.1.6" + "swagger-ui-express": "^4.1.6", + "uuid": "^8.3.2" }, "devDependencies": { "@commitlint/cli": "^12.1.1", @@ -55,7 +58,9 @@ "@types/express": "4.17.11", "@types/jest": "26.0.20", "@types/node": "14.14.31", + "@types/passport-jwt": "^3.0.6", "@types/supertest": "2.0.10", + "@types/uuid": "^8.3.1", "@typescript-eslint/eslint-plugin": "^4.6.1", "@typescript-eslint/parser": "^4.6.1", "chai": "^4.3.0", @@ -91,4 +96,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts new file mode 100644 index 00000000..27a31e61 --- /dev/null +++ b/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 00000000..678e97e3 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,41 @@ +import { + Body, + Controller, + Delete, + HttpCode, + Post, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiHeader, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { AuthService } from './auth.service'; +import { TokenGuard } from './token.strategy'; + +@ApiTags('Auth') +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post() + @UseGuards(TokenGuard) + @ApiSecurity('token') + @ApiHeader({ name: 'keyspace', description: 'Keyspace' }) + register(@Req() req) { + //TODO Hardcoded ServerID + return this.authService.register('123456', req.user.keyspace); + } + + @Delete() + @UseGuards(TokenGuard) + @ApiSecurity('token') + @ApiQuery({ + name: 'token', + description: 'Token to delete', + required: true, + }) + @HttpCode(204) + deleteClient(@Query() query) { + return this.authService.removeClient(query.token); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index d354ed0f..2f5b9f25 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,8 +1,16 @@ import { Module } from '@nestjs/common'; import { TokenStrategy } from './token.strategy'; import { ValidationService } from './header-validation.service'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtModule } from '@nestjs/jwt'; +import { JwtStrategy } from './jwt.strategy'; + @Module({ - providers: [TokenStrategy, ValidationService], + //TODO: hardcoded secret + imports: [JwtModule.register({ secret: 'test' })], + providers: [TokenStrategy, ValidationService, AuthService, JwtStrategy], exports: [ValidationService], + controllers: [AuthController], }) export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 00000000..800ab662 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 00000000..491a399e --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,61 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { TokenPayload } from './interfaces/token-payload.interface'; +import { JwtService } from '@nestjs/jwt'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class AuthService { + //TODO move configCollection to database => own ConfigDataModule + private configCollection: { [id: string]: { knownClients: String[] } } = {}; + constructor(private readonly JwtService: JwtService) {} + + public register(serverId: string, keyspace: string): { accessToken: string } { + const clientId = uuidv4(); + const payload: TokenPayload = { + clientId, + keyspace, + serverId, + }; + if (!this.configCollection[serverId]) { + this.configCollection[serverId] = { knownClients: [] }; + } + this.configCollection[serverId].knownClients = [ + ...this.configCollection[serverId].knownClients, + clientId, + ]; + const signedToken = this.JwtService.sign(payload); + console.log(JSON.stringify(payload)); + console.log(this.configCollection); + return { accessToken: signedToken }; + } + + public validateClient(payload: TokenPayload): boolean { + const { serverId, clientId } = payload; + if ( + this.configCollection[serverId] && + this.configCollection[serverId].knownClients.includes(clientId) + ) { + return true; + } + return false; + } + + public removeClient(token: string) { + if (!token) + throw new HttpException('Please provide token', HttpStatus.BAD_REQUEST); + + const decoded = this.JwtService.decode(token) as TokenPayload; + + try { + this.configCollection[ + decoded.serverId + ].knownClients = this.configCollection[ + decoded.serverId + ].knownClients.filter((client) => client !== decoded.clientId); + console.log(this.configCollection); + return; + } catch (e) { + throw new HttpException('Invalid client id', HttpStatus.BAD_REQUEST); + } + } +} diff --git a/src/auth/interfaces/token-payload.interface.ts b/src/auth/interfaces/token-payload.interface.ts new file mode 100644 index 00000000..8860794c --- /dev/null +++ b/src/auth/interfaces/token-payload.interface.ts @@ -0,0 +1,5 @@ +export interface TokenPayload { + clientId: string; + keyspace: string; + serverId: string; +} diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts new file mode 100644 index 00000000..307b8a44 --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,37 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthGuard, PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { AuthService } from './auth.service'; +import { TokenPayload } from './interfaces/token-payload.interface'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private readonly authService: AuthService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + //TODO: hardcoded Secret + secretOrKey: 'test', + }); + } + validate(payload: TokenPayload): TokenPayload { + if (!this.authService.validateClient(payload)) + throw new UnauthorizedException(); + return payload; + } +} + +export const JWTGuard = AuthGuard('jwt'); + +export class MyAuthGuard extends AuthGuard(['jwt', 'discordGithub-strategy']) { + constructor() { + super(); + } + canActivate(ctx: ExecutionContext) { + return super.canActivate(ctx); + } +} diff --git a/src/auth/token.strategy.ts b/src/auth/token.strategy.ts index 0a08173a..c922aca3 100644 --- a/src/auth/token.strategy.ts +++ b/src/auth/token.strategy.ts @@ -1,6 +1,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AuthGuard, PassportStrategy } from '@nestjs/passport'; +import express from 'express'; import { UniqueTokenStrategy } from 'passport-unique-token'; @Injectable() @@ -9,17 +10,27 @@ export class TokenStrategy extends PassportStrategy( 'discordGithub-strategy', ) { constructor(private config: ConfigService) { - super({ tokenHeader: 'Client-Token' }); + super({ + tokenHeader: 'Client-Token', + tokenQuery: 'Client-Token', + passReqToCallback: true, + failOnMissing: false, + }); } - async validate(token: string, done) { + validate(req: express.Request, token: string, done: any) { + let userObject; const approvedTokens: Array = this.config .get('APPROVED_TOKENS') .split(','); if (!approvedTokens.includes(token)) { throw new UnauthorizedException(); } - return done(null, token); + + if (req.headers.keyspace) { + userObject = { keyspace: req.headers.keyspace }; + } + return done(null, userObject ? userObject : token); } } export const TokenGuard = AuthGuard('discordGithub-strategy'); diff --git a/src/standup/standup.controller.ts b/src/standup/standup.controller.ts index 20855f1e..aa2aa0f9 100644 --- a/src/standup/standup.controller.ts +++ b/src/standup/standup.controller.ts @@ -9,9 +9,18 @@ import { Post, Query, UseGuards, + Req, } from '@nestjs/common'; -import { ApiHeader, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + ApiHeader, + ApiQuery, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; import { Author, AuthorObject } from '../auth/author-headers'; +import { TokenPayload } from '../auth/interfaces/token-payload.interface'; +import { JWTGuard, MyAuthGuard } from '../auth/jwt.strategy'; import { TokenGuard } from '../auth/token.strategy'; import { StandupDTO } from './dto/standup.dto'; import { StandupService } from './standup.service'; @@ -33,22 +42,29 @@ export class StandupController { } @Get() - @ApiHeader({ name: 'keyspace', required: true }) - findAllStandups(@Headers('keyspace') keyspace: string) { - return this.standupService.findAll(keyspace); + @ApiBearerAuth() + @ApiSecurity('token') + @UseGuards(new MyAuthGuard()) + findAllStandups(@Req() req) { + const user: TokenPayload = req.user; + return this.standupService.findAll(user.keyspace); } @Get('search') @ApiQuery({ name: 'uid', type: 'string' }) - @ApiHeader({ name: 'keyspace', required: true }) - search(@Query('uid') uid: string, @Headers('keyspace') keyspace: string) { - return this.standupService.search(uid, keyspace); + @ApiBearerAuth() + @UseGuards(JWTGuard) + search(@Query('uid') uid: string, @Req() req) { + const user: TokenPayload = req.user; + return this.standupService.search(uid, user.keyspace); } @Get(':id') - @ApiHeader({ name: 'keyspace', required: true }) - findById(@Param('id') id: string, @Headers('keyspace') keyspace: string) { - return this.standupService.findById(id, keyspace); + @ApiQuery({ name: 'uid', type: 'string' }) + @ApiBearerAuth() + findById(@Param('id') id: string, @Req() req) { + const user: TokenPayload = req.user; + return this.standupService.findById(id, user.keyspace); } @Delete(':id') diff --git a/src/swagger.ts b/src/swagger.ts index 510255b9..e231d6b1 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -4,5 +4,16 @@ export const swaggerConfig = new DocumentBuilder() .setTitle('EddieHubCommunity API') .setDescription('An API to manage our community data') .setVersion(process.env.npm_package_version) - .addSecurity('token', { type: 'apiKey', in: 'header', name: 'Client-Token' }) + .addSecurity('token', { + type: 'apiKey', + in: 'header', + name: 'Client-Token', + description: 'Token for writing data', + }) + .addBearerAuth({ + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Token for getting Data', + }) .build(); diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 59ac78b7..9a3e4fb5 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -4,8 +4,9 @@ import * as request from 'supertest'; import { exec } from 'child_process'; import { AppModule } from '../../src/app.module'; import Context from '../support/world'; -import { ValidationPipe } from '@nestjs/common'; +import { ExecutionContext, ValidationPipe } from '@nestjs/common'; import { BeforeAll, setDefaultTimeout } from 'cucumber'; +import { JWTGuard } from '../../src/auth/jwt.strategy'; setDefaultTimeout(60 * 1000); @@ -45,7 +46,17 @@ export class requests { public async before(): Promise { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], - }).compile(); + }) + .overrideGuard(JWTGuard) + .useValue({ + canActivate: (ctx: ExecutionContext) => { + const req = ctx.switchToHttp().getRequest(); + //TODO add predefined user here + req.user = {}; + return true; + }, + }) + .compile(); this.context.app = moduleFixture.createNestApplication(); this.context.app.useGlobalPipes(new ValidationPipe({ transform: true })); From ef5886d3a026483b34061fe563d7be50752684f9 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Thu, 15 Jul 2021 13:47:03 +0200 Subject: [PATCH 06/40] feat: add scopes guard --- src/auth/auth.service.ts | 5 +++- src/auth/decorators/scopes.decorator.ts | 3 ++ src/auth/guards/scopes.guard.ts | 26 ++++++++++++++++ .../interfaces/token-payload.interface.ts | 6 ++++ src/auth/jwt.strategy.ts | 16 +++++----- src/standup/standup.controller.ts | 30 ++++++++++++------- 6 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 src/auth/decorators/scopes.decorator.ts create mode 100644 src/auth/guards/scopes.guard.ts diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 491a399e..cc0a9e55 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -6,15 +6,18 @@ import { v4 as uuidv4 } from 'uuid'; @Injectable() export class AuthService { //TODO move configCollection to database => own ConfigDataModule - private configCollection: { [id: string]: { knownClients: String[] } } = {}; + private configCollection: { [id: string]: { knownClients: string[] } } = {}; constructor(private readonly JwtService: JwtService) {} public register(serverId: string, keyspace: string): { accessToken: string } { const clientId = uuidv4(); + //TODO scopes need to by dynamic + const scopes = ['Data.Read']; const payload: TokenPayload = { clientId, keyspace, serverId, + scopes, }; if (!this.configCollection[serverId]) { this.configCollection[serverId] = { knownClients: [] }; diff --git a/src/auth/decorators/scopes.decorator.ts b/src/auth/decorators/scopes.decorator.ts new file mode 100644 index 00000000..437bffc6 --- /dev/null +++ b/src/auth/decorators/scopes.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from '@nestjs/common'; + +export const Scopes = (...scopes: string[]) => SetMetadata('scopes', scopes); diff --git a/src/auth/guards/scopes.guard.ts b/src/auth/guards/scopes.guard.ts new file mode 100644 index 00000000..ee553908 --- /dev/null +++ b/src/auth/guards/scopes.guard.ts @@ -0,0 +1,26 @@ +import { + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { TokenPayload } from '../interfaces/token-payload.interface'; + +@Injectable() +export class ScopesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + canActivate(context: ExecutionContext): boolean { + const scopes = this.reflector.get('scopes', context.getHandler()); + if (!scopes) { + return true; + } + const request = context.switchToHttp().getRequest(); + const user = request.user as TokenPayload; + return this.matchScopes(scopes, user); + } + private matchScopes(scopes: string[], user: any) { + if (user.scopes.some((scope) => scopes.includes(scope))) return true; + throw new ForbiddenException(); + } +} diff --git a/src/auth/interfaces/token-payload.interface.ts b/src/auth/interfaces/token-payload.interface.ts index 8860794c..7dfe6c93 100644 --- a/src/auth/interfaces/token-payload.interface.ts +++ b/src/auth/interfaces/token-payload.interface.ts @@ -2,4 +2,10 @@ export interface TokenPayload { clientId: string; keyspace: string; serverId: string; + scopes: string[]; +} + +export enum ScopesDictionary { + READ = 'Data.Read', + WRITE = 'Data.Write', } diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 307b8a44..59e49f4b 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -27,11 +27,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) { export const JWTGuard = AuthGuard('jwt'); -export class MyAuthGuard extends AuthGuard(['jwt', 'discordGithub-strategy']) { - constructor() { - super(); - } - canActivate(ctx: ExecutionContext) { - return super.canActivate(ctx); - } -} +// export class MyAuthGuard extends AuthGuard(['jwt', 'discordGithub-strategy']) { +// constructor() { +// super(); +// } +// canActivate(ctx: ExecutionContext) { +// return super.canActivate(ctx); +// } +// } diff --git a/src/standup/standup.controller.ts b/src/standup/standup.controller.ts index aa2aa0f9..e4d2f1b5 100644 --- a/src/standup/standup.controller.ts +++ b/src/standup/standup.controller.ts @@ -19,9 +19,13 @@ import { ApiTags, } from '@nestjs/swagger'; import { Author, AuthorObject } from '../auth/author-headers'; -import { TokenPayload } from '../auth/interfaces/token-payload.interface'; -import { JWTGuard, MyAuthGuard } from '../auth/jwt.strategy'; -import { TokenGuard } from '../auth/token.strategy'; +import { Scopes } from '../auth/decorators/scopes.decorator'; +import { ScopesGuard } from '../auth/guards/scopes.guard'; +import { + ScopesDictionary, + TokenPayload, +} from '../auth/interfaces/token-payload.interface'; +import { JWTGuard } from '../auth/jwt.strategy'; import { StandupDTO } from './dto/standup.dto'; import { StandupService } from './standup.service'; @@ -31,8 +35,9 @@ export class StandupController { constructor(private readonly standupService: StandupService) {} @Post() - @UseGuards(TokenGuard) - @ApiSecurity('token') + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) @ApiHeader({ name: 'keyspace', required: true }) createStandup( @Body() body: StandupDTO, @@ -43,8 +48,8 @@ export class StandupController { @Get() @ApiBearerAuth() - @ApiSecurity('token') - @UseGuards(new MyAuthGuard()) + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.READ) findAllStandups(@Req() req) { const user: TokenPayload = req.user; return this.standupService.findAll(user.keyspace); @@ -53,7 +58,8 @@ export class StandupController { @Get('search') @ApiQuery({ name: 'uid', type: 'string' }) @ApiBearerAuth() - @UseGuards(JWTGuard) + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.READ) search(@Query('uid') uid: string, @Req() req) { const user: TokenPayload = req.user; return this.standupService.search(uid, user.keyspace); @@ -62,14 +68,18 @@ export class StandupController { @Get(':id') @ApiQuery({ name: 'uid', type: 'string' }) @ApiBearerAuth() + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.READ) findById(@Param('id') id: string, @Req() req) { const user: TokenPayload = req.user; return this.standupService.findById(id, user.keyspace); } @Delete(':id') - @UseGuards(TokenGuard) - @ApiSecurity('token') + @UseGuards(JWTGuard, ScopesGuard) + // @ApiSecurity('token') + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) @HttpCode(204) @ApiHeader({ name: 'User-Uid', required: true }) @ApiHeader({ name: 'Platform', required: true }) From 8074056d69807f4d12d50fe29338006ecc2299be Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Fri, 16 Jul 2021 12:18:32 +0200 Subject: [PATCH 07/40] fix: fixed todos --- .env.example | 1 + src/auth/auth.controller.ts | 8 +++---- src/auth/auth.module.ts | 12 ++++++++-- src/auth/auth.service.ts | 24 +++++++++---------- src/auth/dto/auth.dto.ts | 22 +++++++++++++++++ src/auth/guards/scopes.guard.ts | 3 ++- .../interfaces/token-payload.interface.ts | 1 - src/auth/jwt.strategy.ts | 3 +-- src/auth/token.strategy.ts | 1 - 9 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 src/auth/dto/auth.dto.ts diff --git a/.env.example b/.env.example index 6406c304..4b8a7d9b 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ APPROVED_TOKENS=abc,def DEBUG=false +SECRET= STARGATE_BASEURL=http://localhost:8082 STARGATE_BASE_API_PATH=/v2/namespaces diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 678e97e3..c9bdfc4b 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -10,6 +10,7 @@ import { } from '@nestjs/common'; import { ApiHeader, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; import { AuthService } from './auth.service'; +import { AuthDTO } from './dto/auth.dto'; import { TokenGuard } from './token.strategy'; @ApiTags('Auth') @@ -20,10 +21,9 @@ export class AuthController { @Post() @UseGuards(TokenGuard) @ApiSecurity('token') - @ApiHeader({ name: 'keyspace', description: 'Keyspace' }) - register(@Req() req) { - //TODO Hardcoded ServerID - return this.authService.register('123456', req.user.keyspace); + // @ApiHeader({ name: 'keyspace', description: 'Keyspace', required: true }) + register(@Body() body: AuthDTO) { + return this.authService.register(body); } @Delete() diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 2f5b9f25..b54d3474 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -5,10 +5,18 @@ import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './jwt.strategy'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ - //TODO: hardcoded secret - imports: [JwtModule.register({ secret: 'test' })], + imports: [ + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('SECRET'), + }), + inject: [ConfigService], + }), + ], providers: [TokenStrategy, ValidationService, AuthService, JwtStrategy], exports: [ValidationService], controllers: [AuthController], diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index cc0a9e55..303b7f8c 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -2,6 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { TokenPayload } from './interfaces/token-payload.interface'; import { JwtService } from '@nestjs/jwt'; import { v4 as uuidv4 } from 'uuid'; +import { AuthDTO } from './dto/auth.dto'; @Injectable() export class AuthService { @@ -9,14 +10,13 @@ export class AuthService { private configCollection: { [id: string]: { knownClients: string[] } } = {}; constructor(private readonly JwtService: JwtService) {} - public register(serverId: string, keyspace: string): { accessToken: string } { + public register(body: AuthDTO): { accessToken: string } { const clientId = uuidv4(); - //TODO scopes need to by dynamic - const scopes = ['Data.Read']; + const { serverId, scopes } = body; + const payload: TokenPayload = { clientId, - keyspace, - serverId, + keyspace: serverId, scopes, }; if (!this.configCollection[serverId]) { @@ -27,16 +27,14 @@ export class AuthService { clientId, ]; const signedToken = this.JwtService.sign(payload); - console.log(JSON.stringify(payload)); - console.log(this.configCollection); - return { accessToken: signedToken }; + return { ...payload, accessToken: signedToken }; } public validateClient(payload: TokenPayload): boolean { - const { serverId, clientId } = payload; + const { keyspace, clientId } = payload; if ( - this.configCollection[serverId] && - this.configCollection[serverId].knownClients.includes(clientId) + this.configCollection[keyspace] && + this.configCollection[keyspace].knownClients.includes(clientId) ) { return true; } @@ -51,9 +49,9 @@ export class AuthService { try { this.configCollection[ - decoded.serverId + decoded.keyspace ].knownClients = this.configCollection[ - decoded.serverId + decoded.keyspace ].knownClients.filter((client) => client !== decoded.clientId); console.log(this.configCollection); return; diff --git a/src/auth/dto/auth.dto.ts b/src/auth/dto/auth.dto.ts new file mode 100644 index 00000000..41f98373 --- /dev/null +++ b/src/auth/dto/auth.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsIn, IsNotEmpty, IsString } from 'class-validator'; + +const scopes = ['Data.Read', 'Data.Write']; + +export class AuthDTO { + @IsArray() + @IsNotEmpty() + @IsString({ each: true }) + @IsIn(scopes, { each: true }) + @ApiProperty({ + required: true, + enum: scopes, + isArray: true, + }) + scopes: string[]; + + @IsString() + @IsNotEmpty() + @ApiProperty({ required: true }) + serverId: string; +} diff --git a/src/auth/guards/scopes.guard.ts b/src/auth/guards/scopes.guard.ts index ee553908..ccf3afa6 100644 --- a/src/auth/guards/scopes.guard.ts +++ b/src/auth/guards/scopes.guard.ts @@ -20,7 +20,8 @@ export class ScopesGuard implements CanActivate { return this.matchScopes(scopes, user); } private matchScopes(scopes: string[], user: any) { - if (user.scopes.some((scope) => scopes.includes(scope))) return true; + if (scopes.every((scope: string) => user.scopes.includes(scope))) + return true; throw new ForbiddenException(); } } diff --git a/src/auth/interfaces/token-payload.interface.ts b/src/auth/interfaces/token-payload.interface.ts index 7dfe6c93..dc0b2114 100644 --- a/src/auth/interfaces/token-payload.interface.ts +++ b/src/auth/interfaces/token-payload.interface.ts @@ -1,7 +1,6 @@ export interface TokenPayload { clientId: string; keyspace: string; - serverId: string; scopes: string[]; } diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 59e49f4b..ea6bdecf 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -14,8 +14,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - //TODO: hardcoded Secret - secretOrKey: 'test', + secretOrKey: process.env.SECRET, }); } validate(payload: TokenPayload): TokenPayload { diff --git a/src/auth/token.strategy.ts b/src/auth/token.strategy.ts index c922aca3..1ce4b8af 100644 --- a/src/auth/token.strategy.ts +++ b/src/auth/token.strategy.ts @@ -14,7 +14,6 @@ export class TokenStrategy extends PassportStrategy( tokenHeader: 'Client-Token', tokenQuery: 'Client-Token', passReqToCallback: true, - failOnMissing: false, }); } From 47b6c9c55a01dabfa9554e0a1f27bf65127ccefa Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 17 Jul 2021 10:47:59 +0200 Subject: [PATCH 08/40] feat: add keyspace interceptor --- src/astra/astra-api.module.ts | 9 ++++ src/astra/keyspace.interceptor.spec.ts | 7 ++++ src/astra/keyspace.interceptor.ts | 24 +++++++++++ src/astra/keyspace.service.ts | 58 ++++++++++++++++++++++++++ src/auth/auth.controller.ts | 3 +- src/auth/auth.service.ts | 12 +++--- src/auth/decorators/user.decorator.ts | 9 ++++ src/auth/jwt.strategy.ts | 6 +-- src/standup/standup.controller.ts | 37 ++++++---------- src/standup/standup.module.ts | 3 +- 10 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 src/astra/astra-api.module.ts create mode 100644 src/astra/keyspace.interceptor.spec.ts create mode 100644 src/astra/keyspace.interceptor.ts create mode 100644 src/astra/keyspace.service.ts create mode 100644 src/auth/decorators/user.decorator.ts diff --git a/src/astra/astra-api.module.ts b/src/astra/astra-api.module.ts new file mode 100644 index 00000000..4e50b03e --- /dev/null +++ b/src/astra/astra-api.module.ts @@ -0,0 +1,9 @@ +import { HttpModule, Module } from '@nestjs/common'; +import { KeyspaceService } from './keyspace.service'; + +@Module({ + imports: [HttpModule], + providers: [KeyspaceService], + exports: [KeyspaceService], +}) +export class AstraApiModule {} diff --git a/src/astra/keyspace.interceptor.spec.ts b/src/astra/keyspace.interceptor.spec.ts new file mode 100644 index 00000000..3801599b --- /dev/null +++ b/src/astra/keyspace.interceptor.spec.ts @@ -0,0 +1,7 @@ +import { KeyspaceInterceptor } from './keyspace.interceptor'; + +describe('KeyspaceInterceptor', () => { + it('should be defined', () => { + expect(new KeyspaceInterceptor()).toBeDefined(); + }); +}); diff --git a/src/astra/keyspace.interceptor.ts b/src/astra/keyspace.interceptor.ts new file mode 100644 index 00000000..df209ec0 --- /dev/null +++ b/src/astra/keyspace.interceptor.ts @@ -0,0 +1,24 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Request } from 'express'; +import { TokenPayload } from '../auth/interfaces/token-payload.interface'; +import { KeyspaceService } from './keyspace.service'; + +@Injectable() +export class KeyspaceInterceptor implements NestInterceptor { + private existingKeyspaces = []; + constructor(private readonly keyspaceService: KeyspaceService) {} + async intercept(context: ExecutionContext, next: CallHandler) { + const request: Request = context.switchToHttp().getRequest(); + const user: TokenPayload = request.user as TokenPayload; + if (this.existingKeyspaces.includes(user.keyspace)) return next.handle(); + + await this.keyspaceService.createKeyspace(user.keyspace); + this.existingKeyspaces = [...this.existingKeyspaces, user.keyspace]; + return next.handle(); + } +} diff --git a/src/astra/keyspace.service.ts b/src/astra/keyspace.service.ts new file mode 100644 index 00000000..5889b402 --- /dev/null +++ b/src/astra/keyspace.service.ts @@ -0,0 +1,58 @@ +import { HttpService, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class KeyspaceService { + constructor( + private readonly http: HttpService, + private readonly config: ConfigService, + ) {} + + private getUrl(): string { + const databaseId = this.config.get('ASTRA_DATABASE_ID'); + const databaseRegion = this.config.get('ASTRA_DATABASE_REGION'); + const baseUrl = this.config.get('STARGATE_BASEURL'); + let url: string = null; + + if (databaseId && databaseRegion) + url = `https://${databaseId}-${databaseRegion}.apps.astra.datastax.com/api/rest`; + if (baseUrl) url = baseUrl; + + if (!url) throw new Error('could not return Url'); + return url; + } + + private async getAuthToken(): Promise { + const astraToken = this.config.get('ASTRA_APPLICATION_TOKEN'); + const stargateToken = this.config.get('STARGATE_AUTH_TOKEN'); + const authUrl = this.config.get('STARGATE_AUTH_URL'); + + if (astraToken) return astraToken; + if (stargateToken) return stargateToken; + if (authUrl) { + const response = await this.http + .post(authUrl, { + username: this.config.get('STARGATE_USERNAME'), + password: this.config.get('STARGATE_PASSWORD'), + }) + .toPromise(); + return response.data.authToken; + } + throw new Error('Could not return AuthToken'); + } + + public async createKeyspace(serverId: string): Promise { + const authToken: string = await this.getAuthToken(); + const url = `${this.getUrl()}/v2/schemas/keyspaces`; + const postBody = { name: serverId }; + + await this.http + .post(url, postBody, { + headers: { + 'X-Cassandra-Token': authToken, + 'Content-Type': 'application/json', + }, + }) + .toPromise(); + } +} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index c9bdfc4b..ecd54f89 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -5,10 +5,9 @@ import { HttpCode, Post, Query, - Req, UseGuards, } from '@nestjs/common'; -import { ApiHeader, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { AuthDTO } from './dto/auth.dto'; import { TokenGuard } from './token.strategy'; diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 303b7f8c..5c4f65e4 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -8,9 +8,9 @@ import { AuthDTO } from './dto/auth.dto'; export class AuthService { //TODO move configCollection to database => own ConfigDataModule private configCollection: { [id: string]: { knownClients: string[] } } = {}; - constructor(private readonly JwtService: JwtService) {} + constructor(private readonly jwtService: JwtService) {} - public register(body: AuthDTO): { accessToken: string } { + public register(body: AuthDTO) { const clientId = uuidv4(); const { serverId, scopes } = body; @@ -26,8 +26,10 @@ export class AuthService { ...this.configCollection[serverId].knownClients, clientId, ]; - const signedToken = this.JwtService.sign(payload); - return { ...payload, accessToken: signedToken }; + const signedToken = this.jwtService.sign(payload, { expiresIn: '1y' }); + const decoded: any = this.jwtService.decode(signedToken); + const expiresIn: number = decoded.exp - Math.round(Date.now() / 1000); + return { ...payload, accessToken: signedToken, expiresIn }; } public validateClient(payload: TokenPayload): boolean { @@ -45,7 +47,7 @@ export class AuthService { if (!token) throw new HttpException('Please provide token', HttpStatus.BAD_REQUEST); - const decoded = this.JwtService.decode(token) as TokenPayload; + const decoded = this.jwtService.decode(token) as TokenPayload; try { this.configCollection[ diff --git a/src/auth/decorators/user.decorator.ts b/src/auth/decorators/user.decorator.ts new file mode 100644 index 00000000..674ad93b --- /dev/null +++ b/src/auth/decorators/user.decorator.ts @@ -0,0 +1,9 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; + +export const User = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request: Request = ctx.switchToHttp().getRequest(); + return request.user; + }, +); diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index ea6bdecf..1b2b91d6 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -1,8 +1,4 @@ -import { - ExecutionContext, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthGuard, PassportStrategy } from '@nestjs/passport'; import { Strategy, ExtractJwt } from 'passport-jwt'; import { AuthService } from './auth.service'; diff --git a/src/standup/standup.controller.ts b/src/standup/standup.controller.ts index e4d2f1b5..82d20c00 100644 --- a/src/standup/standup.controller.ts +++ b/src/standup/standup.controller.ts @@ -1,5 +1,4 @@ import { - Headers, Body, Controller, Delete, @@ -9,17 +8,13 @@ import { Post, Query, UseGuards, - Req, + UseInterceptors, } from '@nestjs/common'; -import { - ApiBearerAuth, - ApiHeader, - ApiQuery, - ApiSecurity, - ApiTags, -} from '@nestjs/swagger'; +import { ApiBearerAuth, ApiHeader, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { KeyspaceInterceptor } from '../astra/keyspace.interceptor'; import { Author, AuthorObject } from '../auth/author-headers'; import { Scopes } from '../auth/decorators/scopes.decorator'; +import { User } from '../auth/decorators/user.decorator'; import { ScopesGuard } from '../auth/guards/scopes.guard'; import { ScopesDictionary, @@ -38,20 +33,16 @@ export class StandupController { @UseGuards(JWTGuard, ScopesGuard) @ApiBearerAuth() @Scopes(ScopesDictionary.WRITE) - @ApiHeader({ name: 'keyspace', required: true }) - createStandup( - @Body() body: StandupDTO, - @Headers('keyspace') keyspace: string, - ) { - return this.standupService.create(body, keyspace); + createStandup(@Body() body: StandupDTO, @User() user: TokenPayload) { + return this.standupService.create(body, user.keyspace); } @Get() @ApiBearerAuth() + @UseInterceptors(KeyspaceInterceptor) @UseGuards(JWTGuard, ScopesGuard) @Scopes(ScopesDictionary.READ) - findAllStandups(@Req() req) { - const user: TokenPayload = req.user; + findAllStandups(@User() user) { return this.standupService.findAll(user.keyspace); } @@ -60,8 +51,7 @@ export class StandupController { @ApiBearerAuth() @UseGuards(JWTGuard, ScopesGuard) @Scopes(ScopesDictionary.READ) - search(@Query('uid') uid: string, @Req() req) { - const user: TokenPayload = req.user; + search(@Query('uid') uid: string, @User() user: TokenPayload) { return this.standupService.search(uid, user.keyspace); } @@ -70,25 +60,22 @@ export class StandupController { @ApiBearerAuth() @UseGuards(JWTGuard, ScopesGuard) @Scopes(ScopesDictionary.READ) - findById(@Param('id') id: string, @Req() req) { - const user: TokenPayload = req.user; + findById(@Param('id') id: string, @User() user: TokenPayload) { return this.standupService.findById(id, user.keyspace); } @Delete(':id') @UseGuards(JWTGuard, ScopesGuard) - // @ApiSecurity('token') @ApiBearerAuth() @Scopes(ScopesDictionary.WRITE) @HttpCode(204) @ApiHeader({ name: 'User-Uid', required: true }) @ApiHeader({ name: 'Platform', required: true }) - @ApiHeader({ name: 'keyspace', required: true }) deleteStandup( @Param('id') id: string, @AuthorObject() author: Author, - @Headers('keyspace') keyspace: string, + @User() user: TokenPayload, ) { - return this.standupService.deleteStandup(id, author, keyspace); + return this.standupService.deleteStandup(id, author, user.keyspace); } } diff --git a/src/standup/standup.module.ts b/src/standup/standup.module.ts index 2ba2cda7..63afd7fb 100644 --- a/src/standup/standup.module.ts +++ b/src/standup/standup.module.ts @@ -2,12 +2,13 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '../auth/auth.module'; import { StandupController } from './standup.controller'; import { StandupService } from './standup.service'; -import { AstraModule } from '@cahllagerfeld/nestjs-astra'; import { AstraService } from '../astra/astra.service'; +import { AstraApiModule } from '../astra/astra-api.module'; @Module({ imports: [ AuthModule, + AstraApiModule, // AstraModule.forFeature({ namespace: 'eddiehub', collection: 'standup' }), ], controllers: [StandupController], From 281abaee5f1bf64bb3c5244627cc61479c6a0be4 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 17 Jul 2021 18:45:41 +0200 Subject: [PATCH 09/40] test: update unittests --- src/astra/keyspace.interceptor.spec.ts | 8 +++++++- src/auth/auth.controller.spec.ts | 13 +++++++++++++ src/auth/auth.service.spec.ts | 11 +++++++++++ src/auth/auth.service.ts | 1 + src/standup/standup.controller.spec.ts | 2 ++ test/step-definitions/requests.ts | 3 ++- 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/astra/keyspace.interceptor.spec.ts b/src/astra/keyspace.interceptor.spec.ts index 3801599b..bab47247 100644 --- a/src/astra/keyspace.interceptor.spec.ts +++ b/src/astra/keyspace.interceptor.spec.ts @@ -1,7 +1,13 @@ +import { HttpService } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { KeyspaceInterceptor } from './keyspace.interceptor'; +import { KeyspaceService } from './keyspace.service'; describe('KeyspaceInterceptor', () => { + const http = new HttpService(); + const config = new ConfigService(); + const keyspaceService = new KeyspaceService(http, config); it('should be defined', () => { - expect(new KeyspaceInterceptor()).toBeDefined(); + expect(new KeyspaceInterceptor(keyspaceService)).toBeDefined(); }); }); diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 27a31e61..9ff8fe49 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,12 +1,25 @@ +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; describe('AuthController', () => { let controller: AuthController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('SECRET'), + }), + inject: [ConfigService], + }), + ], controllers: [AuthController], + providers: [AuthService], }).compile(); controller = module.get(AuthController); diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 800ab662..919d86a6 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -1,3 +1,5 @@ +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; @@ -6,6 +8,15 @@ describe('AuthService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('SECRET'), + }), + inject: [ConfigService], + }), + ], providers: [AuthService], }).compile(); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5c4f65e4..fa7a3beb 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -26,6 +26,7 @@ export class AuthService { ...this.configCollection[serverId].knownClients, clientId, ]; + //TODO token-expiry const signedToken = this.jwtService.sign(payload, { expiresIn: '1y' }); const decoded: any = this.jwtService.decode(signedToken); const expiresIn: number = decoded.exp - Math.round(Date.now() / 1000); diff --git a/src/standup/standup.controller.spec.ts b/src/standup/standup.controller.spec.ts index 6611af06..bea3835f 100644 --- a/src/standup/standup.controller.spec.ts +++ b/src/standup/standup.controller.spec.ts @@ -1,6 +1,7 @@ import { AstraModule } from '@cahllagerfeld/nestjs-astra'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; +import { AstraApiModule } from '../astra/astra-api.module'; import { AstraConfigService } from '../astra/astra-config.service'; import { AuthModule } from '../auth/auth.module'; import { StandupController } from './standup.controller'; @@ -12,6 +13,7 @@ describe('StandupController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ + AstraApiModule, AuthModule, ConfigModule.forRoot({ isGlobal: true, diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 9a3e4fb5..7737dd02 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -7,6 +7,7 @@ import Context from '../support/world'; import { ExecutionContext, ValidationPipe } from '@nestjs/common'; import { BeforeAll, setDefaultTimeout } from 'cucumber'; import { JWTGuard } from '../../src/auth/jwt.strategy'; +import { TokenPayload } from '../../src/auth/interfaces/token-payload.interface'; setDefaultTimeout(60 * 1000); @@ -52,7 +53,7 @@ export class requests { canActivate: (ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest(); //TODO add predefined user here - req.user = {}; + req.user = {} as TokenPayload; return true; }, }) From 2320df94ddcdcb1720b8accf84b87d983cb697c9 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 17 Jul 2021 18:57:14 +0200 Subject: [PATCH 10/40] test: adjust unittests --- src/auth/auth.controller.spec.ts | 11 +---------- src/auth/auth.service.spec.ts | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 9ff8fe49..9a025668 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,4 +1,3 @@ -import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; import { AuthController } from './auth.controller'; @@ -9,15 +8,7 @@ describe('AuthController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ - JwtModule.registerAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('SECRET'), - }), - inject: [ConfigService], - }), - ], + imports: [JwtModule.register({ secret: 'Test' })], controllers: [AuthController], providers: [AuthService], }).compile(); diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 919d86a6..b3788213 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -1,4 +1,3 @@ -import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; @@ -8,15 +7,7 @@ describe('AuthService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ - JwtModule.registerAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('SECRET'), - }), - inject: [ConfigService], - }), - ], + imports: [JwtModule.register({ secret: 'Test' })], providers: [AuthService], }).compile(); From f96db250c14be36de177bea6ea6ac9b1e2842818 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 17 Jul 2021 19:05:42 +0200 Subject: [PATCH 11/40] test: adjust unittests --- src/auth/auth.controller.spec.ts | 1 + src/auth/auth.service.spec.ts | 1 + src/standup/standup.controller.spec.ts | 1 + src/standup/standup.service.spec.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 9a025668..3de912a0 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -7,6 +7,7 @@ describe('AuthController', () => { let controller: AuthController; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [JwtModule.register({ secret: 'Test' })], controllers: [AuthController], diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index b3788213..337a1467 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -6,6 +6,7 @@ describe('AuthService', () => { let service: AuthService; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [JwtModule.register({ secret: 'Test' })], providers: [AuthService], diff --git a/src/standup/standup.controller.spec.ts b/src/standup/standup.controller.spec.ts index bea3835f..84366b4e 100644 --- a/src/standup/standup.controller.spec.ts +++ b/src/standup/standup.controller.spec.ts @@ -11,6 +11,7 @@ describe('StandupController', () => { let controller: StandupController; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AstraApiModule, diff --git a/src/standup/standup.service.spec.ts b/src/standup/standup.service.spec.ts index bb2eaa8c..3fdd5d3c 100644 --- a/src/standup/standup.service.spec.ts +++ b/src/standup/standup.service.spec.ts @@ -9,6 +9,7 @@ describe('StandupService', () => { let service: StandupService; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, From 21885faed0486fce43b1e903b74708683509f0e6 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 17 Jul 2021 19:10:29 +0200 Subject: [PATCH 12/40] test: adjust unittests --- src/app.controller.spec.ts | 1 + src/calendar/calendar.controller.spec.ts | 1 + src/calendar/calendar.service.spec.ts | 1 + src/discord/discord.controller.spec.ts | 1 + src/discord/discord.service.spec.ts | 1 + src/github/github.controller.spec.ts | 1 + src/github/github.service.spec.ts | 1 + 7 files changed, 7 insertions(+) diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index f5620234..01cd6f6a 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -8,6 +8,7 @@ describe('AppController', () => { let appController: AppController; beforeEach(async () => { + process.env.SECRET = 'Test'; const app: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/calendar/calendar.controller.spec.ts b/src/calendar/calendar.controller.spec.ts index 254be365..0949d99f 100644 --- a/src/calendar/calendar.controller.spec.ts +++ b/src/calendar/calendar.controller.spec.ts @@ -10,6 +10,7 @@ describe('CalendarController', () => { let controller: CalendarController; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/calendar/calendar.service.spec.ts b/src/calendar/calendar.service.spec.ts index 2082deba..9ff1767e 100644 --- a/src/calendar/calendar.service.spec.ts +++ b/src/calendar/calendar.service.spec.ts @@ -9,6 +9,7 @@ describe('CalendarService', () => { let service: CalendarService; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/discord/discord.controller.spec.ts b/src/discord/discord.controller.spec.ts index efd85131..24ffbd86 100644 --- a/src/discord/discord.controller.spec.ts +++ b/src/discord/discord.controller.spec.ts @@ -10,6 +10,7 @@ describe('DiscordController', () => { let controller: DiscordController; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/discord/discord.service.spec.ts b/src/discord/discord.service.spec.ts index b5d52fba..9635a2b8 100644 --- a/src/discord/discord.service.spec.ts +++ b/src/discord/discord.service.spec.ts @@ -9,6 +9,7 @@ describe('DiscordService', () => { let service: DiscordService; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/github/github.controller.spec.ts b/src/github/github.controller.spec.ts index 20474079..de90f1af 100644 --- a/src/github/github.controller.spec.ts +++ b/src/github/github.controller.spec.ts @@ -13,6 +13,7 @@ describe('GithubController', () => { let controller: GithubController; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ HttpModule, diff --git a/src/github/github.service.spec.ts b/src/github/github.service.spec.ts index 0673b11c..78558db7 100644 --- a/src/github/github.service.spec.ts +++ b/src/github/github.service.spec.ts @@ -12,6 +12,7 @@ describe('GithubService', () => { let service: GithubService; beforeEach(async () => { + process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, From cd4c0f25d7910a2eaf00e84f5850789980d13d64 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 17 Jul 2021 19:14:04 +0200 Subject: [PATCH 13/40] test: adjust unittests --- .env.example | 2 +- src/app.controller.spec.ts | 1 - src/auth/auth.controller.spec.ts | 1 - src/auth/auth.service.spec.ts | 1 - src/calendar/calendar.controller.spec.ts | 1 - src/calendar/calendar.service.spec.ts | 1 - src/discord/discord.controller.spec.ts | 1 - src/discord/discord.service.spec.ts | 1 - src/github/github.controller.spec.ts | 1 - src/github/github.service.spec.ts | 1 - src/standup/standup.controller.spec.ts | 1 - src/standup/standup.service.spec.ts | 1 - 12 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 4b8a7d9b..d528d42b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ APPROVED_TOKENS=abc,def DEBUG=false -SECRET= +SECRET=Test STARGATE_BASEURL=http://localhost:8082 STARGATE_BASE_API_PATH=/v2/namespaces diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index 01cd6f6a..f5620234 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -8,7 +8,6 @@ describe('AppController', () => { let appController: AppController; beforeEach(async () => { - process.env.SECRET = 'Test'; const app: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 3de912a0..9a025668 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -7,7 +7,6 @@ describe('AuthController', () => { let controller: AuthController; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [JwtModule.register({ secret: 'Test' })], controllers: [AuthController], diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 337a1467..b3788213 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -6,7 +6,6 @@ describe('AuthService', () => { let service: AuthService; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [JwtModule.register({ secret: 'Test' })], providers: [AuthService], diff --git a/src/calendar/calendar.controller.spec.ts b/src/calendar/calendar.controller.spec.ts index 0949d99f..254be365 100644 --- a/src/calendar/calendar.controller.spec.ts +++ b/src/calendar/calendar.controller.spec.ts @@ -10,7 +10,6 @@ describe('CalendarController', () => { let controller: CalendarController; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/calendar/calendar.service.spec.ts b/src/calendar/calendar.service.spec.ts index 9ff1767e..2082deba 100644 --- a/src/calendar/calendar.service.spec.ts +++ b/src/calendar/calendar.service.spec.ts @@ -9,7 +9,6 @@ describe('CalendarService', () => { let service: CalendarService; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/discord/discord.controller.spec.ts b/src/discord/discord.controller.spec.ts index 24ffbd86..efd85131 100644 --- a/src/discord/discord.controller.spec.ts +++ b/src/discord/discord.controller.spec.ts @@ -10,7 +10,6 @@ describe('DiscordController', () => { let controller: DiscordController; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/discord/discord.service.spec.ts b/src/discord/discord.service.spec.ts index 9635a2b8..b5d52fba 100644 --- a/src/discord/discord.service.spec.ts +++ b/src/discord/discord.service.spec.ts @@ -9,7 +9,6 @@ describe('DiscordService', () => { let service: DiscordService; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/github/github.controller.spec.ts b/src/github/github.controller.spec.ts index de90f1af..20474079 100644 --- a/src/github/github.controller.spec.ts +++ b/src/github/github.controller.spec.ts @@ -13,7 +13,6 @@ describe('GithubController', () => { let controller: GithubController; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ HttpModule, diff --git a/src/github/github.service.spec.ts b/src/github/github.service.spec.ts index 78558db7..0673b11c 100644 --- a/src/github/github.service.spec.ts +++ b/src/github/github.service.spec.ts @@ -12,7 +12,6 @@ describe('GithubService', () => { let service: GithubService; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, diff --git a/src/standup/standup.controller.spec.ts b/src/standup/standup.controller.spec.ts index 84366b4e..bea3835f 100644 --- a/src/standup/standup.controller.spec.ts +++ b/src/standup/standup.controller.spec.ts @@ -11,7 +11,6 @@ describe('StandupController', () => { let controller: StandupController; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AstraApiModule, diff --git a/src/standup/standup.service.spec.ts b/src/standup/standup.service.spec.ts index 3fdd5d3c..bb2eaa8c 100644 --- a/src/standup/standup.service.spec.ts +++ b/src/standup/standup.service.spec.ts @@ -9,7 +9,6 @@ describe('StandupService', () => { let service: StandupService; beforeEach(async () => { - process.env.SECRET = 'Test'; const module: TestingModule = await Test.createTestingModule({ imports: [ AuthModule, From 513303ce73996869719d53dc610915962ae87506 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 18 Jul 2021 08:52:22 +0200 Subject: [PATCH 14/40] test: update e2e-tests --- test/features/calendar.feature | 482 ++++++++++++++--------------- test/features/discord.feature | 260 ++++++++-------- test/features/github.feature | 376 +++++++++++----------- test/features/standup.feature | 20 +- test/step-definitions/requests.ts | 71 +++-- test/step-definitions/responses.ts | 25 +- test/support/world.ts | 1 + 7 files changed, 641 insertions(+), 594 deletions(-) diff --git a/test/features/calendar.feature b/test/features/calendar.feature index 437fc6a6..04efd8f6 100644 --- a/test/features/calendar.feature +++ b/test/features/calendar.feature @@ -1,253 +1,253 @@ -@calendar -Feature: calendar module +# @calendar +# Feature: calendar module - Scenario: add a new event - Given authorisation - When make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a GET request to "/calendar/{id}" - Then the response status code should be 200 - And the response should contain: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - | createdOn | "TYPE:DATE" | - | updatedOn | "TYPE:DATE" | +# Scenario: add a new event +# Given authorisation +# When make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a GET request to "/calendar/{id}" +# Then the response status code should be 200 +# And the response should contain: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# | createdOn | "TYPE:DATE" | +# | updatedOn | "TYPE:DATE" | - Scenario: get list of events - Given make a GET request to "/calendar" - Then the response status code should be 200 - And the response should contain: - | future | {} | - | ongoing | {} | +# Scenario: get list of events +# Given make a GET request to "/calendar" +# Then the response status code should be 200 +# And the response should contain: +# | future | {} | +# | ongoing | {} | - Scenario: add an empty event - Given authorisation - And make a POST request to "/calendar" with: - | test | "test" | - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | error | "Bad Request" | - And the response property "message" has items: - | name must be a string | - | name should not be empty | - | platform must be one of the following values: YouTube, Twitch | - | platform must be a string | - | platform should not be empty | - | author should not be empty | - | startDate should not be empty | - | startDate must be a Date instance | - | endDate must be a Date instance | - | endDate should not be empty | +# Scenario: add an empty event +# Given authorisation +# And make a POST request to "/calendar" with: +# | test | "test" | +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | error | "Bad Request" | +# And the response property "message" has items: +# | name must be a string | +# | name should not be empty | +# | platform must be one of the following values: YouTube, Twitch | +# | platform must be a string | +# | platform should not be empty | +# | author should not be empty | +# | startDate should not be empty | +# | startDate must be a Date instance | +# | endDate must be a Date instance | +# | endDate should not be empty | - Scenario: update an event - Given authorisation - When make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - And the response should contain: - | documentId | "TYPE:ID" | - Then set header "User-Uid" with value "hubber" - Then set header "Platform" with value "discord" - When make a PUT request to "/calendar/{id}" with: - | name | "Livestream YZ" | - | description | "undescriptive Description" | - | url | "https://mydomain.com" | - | platform | "Twitch" | - | author | {"platform":"discord","uid":"hubby"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - Then the response status code should be 200 - When make a GET request to "/calendar/{id}" - Then the response status code should be 200 - And the response should contain: - | name | "Livestream YZ" | - | description | "undescriptive Description" | - | url | "https://mydomain.com" | - | platform | "Twitch" | - | author | {"platform":"discord","uid":"hubby"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - | createdOn | "TYPE:DATE" | - | updatedOn | "TYPE:DATE" | +# Scenario: update an event +# Given authorisation +# When make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# Then set header "User-Uid" with value "hubber" +# Then set header "Platform" with value "discord" +# When make a PUT request to "/calendar/{id}" with: +# | name | "Livestream YZ" | +# | description | "undescriptive Description" | +# | url | "https://mydomain.com" | +# | platform | "Twitch" | +# | author | {"platform":"discord","uid":"hubby"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# Then the response status code should be 200 +# When make a GET request to "/calendar/{id}" +# Then the response status code should be 200 +# And the response should contain: +# | name | "Livestream YZ" | +# | description | "undescriptive Description" | +# | url | "https://mydomain.com" | +# | platform | "Twitch" | +# | author | {"platform":"discord","uid":"hubby"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# | createdOn | "TYPE:DATE" | +# | updatedOn | "TYPE:DATE" | - Scenario: update an event with wrong author - Given authorisation - When make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a PUT request to "/calendar/{id}" with: - | name | "Livestream YZ" | - | description | "undescriptive Description" | - | url | "https://mydomain.com" | - | platform | "Twitch" | - | author | {"platform":"discord","uid":"hubby"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | message | "update failed: author doesn't match" | +# Scenario: update an event with wrong author +# Given authorisation +# When make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a PUT request to "/calendar/{id}" with: +# | name | "Livestream YZ" | +# | description | "undescriptive Description" | +# | url | "https://mydomain.com" | +# | platform | "Twitch" | +# | author | {"platform":"discord","uid":"hubby"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | message | "update failed: author doesn't match" | - Scenario: update an non-existing event - Given authorisation - When make a PUT request to "/calendar/321" with: - | name | "Livestream YZ" | - | description | "undescriptive Description" | - | url | "https://mydomain.com" | - | platform | "Twitch" | - | author | {"platform":"discord","uid":"hubby"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - Then the response status code should be 404 - And the response should contain: - | message | "no event for 321 found" | - | statusCode | 404 | +# Scenario: update an non-existing event +# Given authorisation +# When make a PUT request to "/calendar/321" with: +# | name | "Livestream YZ" | +# | description | "undescriptive Description" | +# | url | "https://mydomain.com" | +# | platform | "Twitch" | +# | author | {"platform":"discord","uid":"hubby"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# Then the response status code should be 404 +# And the response should contain: +# | message | "no event for 321 found" | +# | statusCode | 404 | - Scenario: delete an event - Given authorisation - When make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - And the response should contain: - | documentId | "TYPE:ID" | - Then set header "User-Uid" with value "hubber" - Then set header "Platform" with value "discord" - When make a DELETE request to "/calendar/{id}" - Then the response status code should be 204 +# Scenario: delete an event +# Given authorisation +# When make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# Then set header "User-Uid" with value "hubber" +# Then set header "Platform" with value "discord" +# When make a DELETE request to "/calendar/{id}" +# Then the response status code should be 204 - Scenario: delete an event with wrong author - Given authorisation - When make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a DELETE request to "/calendar/{id}" - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | message | "deletion failed: author doesn't match" | +# Scenario: delete an event with wrong author +# Given authorisation +# When make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a DELETE request to "/calendar/{id}" +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | message | "deletion failed: author doesn't match" | - Scenario: delete non-existing event - Given authorisation - When make a DELETE request to "/calendar/321" - Then the response status code should be 404 - And the response should contain: - | statusCode | 404 | - | message | "no event for 321 found" | +# Scenario: delete non-existing event +# Given authorisation +# When make a DELETE request to "/calendar/321" +# Then the response status code should be 404 +# And the response should contain: +# | statusCode | 404 | +# | message | "no event for 321 found" | - Scenario: get event with authenticated request - Given authorisation - When make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a GET request to "/calendar/{id}" - Then the response status code should be 200 - And the response should contain: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - | createdOn | "TYPE:DATE" | - | updatedOn | "TYPE:DATE" | +# Scenario: get event with authenticated request +# Given authorisation +# When make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a GET request to "/calendar/{id}" +# Then the response status code should be 200 +# And the response should contain: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# | createdOn | "TYPE:DATE" | +# | updatedOn | "TYPE:DATE" | - Scenario: create a event without authorization - Given make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2021-01-01T00:00:00.000Z" | - Then the response status code should be 401 - And the response should contain: - | statusCode | 401 | - | message | "Unauthorized" | +# Scenario: create a event without authorization +# Given make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2021-01-01T00:00:00.000Z" | +# Then the response status code should be 401 +# And the response should contain: +# | statusCode | 401 | +# | message | "Unauthorized" | - Scenario: get sorted ongoing and future events - Given authorisation - And make a POST request to "/calendar" with: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2022-01-01T00:00:00.000Z" | - | endDate | "2023-01-01T00:00:00.000Z" | - When make a POST request to "/calendar" with: - | name | "Livestream YZ" | - | description | "undescriptive Description" | - | url | "https://mydomain.com" | - | platform | "Twitch" | - | author | {"platform":"discord","uid":"hubby"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2022-01-01T00:00:00.000Z" | - When make a GET request to "/calendar" - Then the response status code should be 200 - And the response property "future" has a subobject with a field "name" that is equal to "Livestream XY" should contain: - | name | "Livestream XY" | - | description | "descriptive Description" | - | url | "https://domain.com" | - | platform | "YouTube" | - | author | {"platform":"discord","uid":"hubber"} | - | startDate | "2022-01-01T00:00:00.000Z" | - | endDate | "2023-01-01T00:00:00.000Z" | - | createdOn | "TYPE:DATE" | - | updatedOn | "TYPE:DATE" | - And the response property "ongoing" has a subobject with a field "name" that is equal to "Livestream YZ" should contain: - | name | "Livestream YZ" | - | description | "undescriptive Description" | - | url | "https://mydomain.com" | - | platform | "Twitch" | - | author | {"platform":"discord","uid":"hubby"} | - | startDate | "2021-01-01T00:00:00.000Z" | - | endDate | "2022-01-01T00:00:00.000Z" | - | createdOn | "TYPE:DATE" | - | updatedOn | "TYPE:DATE" | +# Scenario: get sorted ongoing and future events +# Given authorisation +# And make a POST request to "/calendar" with: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2022-01-01T00:00:00.000Z" | +# | endDate | "2023-01-01T00:00:00.000Z" | +# When make a POST request to "/calendar" with: +# | name | "Livestream YZ" | +# | description | "undescriptive Description" | +# | url | "https://mydomain.com" | +# | platform | "Twitch" | +# | author | {"platform":"discord","uid":"hubby"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2022-01-01T00:00:00.000Z" | +# When make a GET request to "/calendar" +# Then the response status code should be 200 +# And the response property "future" has a subobject with a field "name" that is equal to "Livestream XY" should contain: +# | name | "Livestream XY" | +# | description | "descriptive Description" | +# | url | "https://domain.com" | +# | platform | "YouTube" | +# | author | {"platform":"discord","uid":"hubber"} | +# | startDate | "2022-01-01T00:00:00.000Z" | +# | endDate | "2023-01-01T00:00:00.000Z" | +# | createdOn | "TYPE:DATE" | +# | updatedOn | "TYPE:DATE" | +# And the response property "ongoing" has a subobject with a field "name" that is equal to "Livestream YZ" should contain: +# | name | "Livestream YZ" | +# | description | "undescriptive Description" | +# | url | "https://mydomain.com" | +# | platform | "Twitch" | +# | author | {"platform":"discord","uid":"hubby"} | +# | startDate | "2021-01-01T00:00:00.000Z" | +# | endDate | "2022-01-01T00:00:00.000Z" | +# | createdOn | "TYPE:DATE" | +# | updatedOn | "TYPE:DATE" | diff --git a/test/features/discord.feature b/test/features/discord.feature index 37400fb8..4b514192 100644 --- a/test/features/discord.feature +++ b/test/features/discord.feature @@ -1,140 +1,140 @@ -@discord -Feature: discord module +# @discord +# Feature: discord module - Scenario: add a new user - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response status code should be 201 - And the response should contain: - | documentId | "TYPE:ID" | +# Scenario: add a new user +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response status code should be 201 +# And the response should contain: +# | documentId | "TYPE:ID" | - Scenario: get list of users - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response should contain: - | documentId | "TYPE:ID" | - When make a GET request to "/discord" - Then the response status code should be 200 - And the response in item where property "author" has a subobject "uid" which contains a field that is equal to "hubber" should contain: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - | updatedOn | "TYPE:DATE" | - | createdOn | "TYPE:DATE" | +# Scenario: get list of users +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response should contain: +# | documentId | "TYPE:ID" | +# When make a GET request to "/discord" +# Then the response status code should be 200 +# And the response in item where property "author" has a subobject "uid" which contains a field that is equal to "hubber" should contain: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# | updatedOn | "TYPE:DATE" | +# | createdOn | "TYPE:DATE" | - Scenario: add an empty user - Given authorisation - And make a POST request to "/discord" with: - | test | "test" | - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | error | "Bad Request" | - And the response property "message" has items: - | author should not be empty | +# Scenario: add an empty user +# Given authorisation +# And make a POST request to "/discord" with: +# | test | "test" | +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | error | "Bad Request" | +# And the response property "message" has items: +# | author should not be empty | - Scenario: update a user - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response should contain: - | documentId | "TYPE:ID" | - When set header "User-Uid" with value "hubber" - Then make a PUT request to "/discord/{id}" with: - | author | {"platform":"discord","uid":"hubby"} | - | bio | "Updated user bio" | - | socials | {"discord":"update-user"} | - When make a GET request to "/discord/{id}" - Then the response status code should be 200 - And the response should contain: - | bio | "Updated user bio" | - | author | {"platform":"discord","uid":"hubby"} | - | socials | {"discord":"update-user","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - | updatedOn | "TYPE:DATE" | - | createdOn | "TYPE:DATE" | +# Scenario: update a user +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response should contain: +# | documentId | "TYPE:ID" | +# When set header "User-Uid" with value "hubber" +# Then make a PUT request to "/discord/{id}" with: +# | author | {"platform":"discord","uid":"hubby"} | +# | bio | "Updated user bio" | +# | socials | {"discord":"update-user"} | +# When make a GET request to "/discord/{id}" +# Then the response status code should be 200 +# And the response should contain: +# | bio | "Updated user bio" | +# | author | {"platform":"discord","uid":"hubby"} | +# | socials | {"discord":"update-user","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# | updatedOn | "TYPE:DATE" | +# | createdOn | "TYPE:DATE" | - Scenario: update a user with wrong author - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response should contain: - | documentId | "TYPE:ID" | - When make a PUT request to "/discord/{id}" with: - | author | {"platform":"discord","uid":"hubby"} | - | bio | "Updated user bio" | - | socials | {"discord":"update-user"} | - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | message | "update failed: author doesn't match" | +# Scenario: update a user with wrong author +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response should contain: +# | documentId | "TYPE:ID" | +# When make a PUT request to "/discord/{id}" with: +# | author | {"platform":"discord","uid":"hubby"} | +# | bio | "Updated user bio" | +# | socials | {"discord":"update-user"} | +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | message | "update failed: author doesn't match" | - Scenario: delete a user - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response should contain: - | documentId | "TYPE:ID" | - Then set header "User-Uid" with value "hubber" - When make a DELETE request to "/discord/{id}" - Then the response status code should be 204 +# Scenario: delete a user +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response should contain: +# | documentId | "TYPE:ID" | +# Then set header "User-Uid" with value "hubber" +# When make a DELETE request to "/discord/{id}" +# Then the response status code should be 204 - Scenario: delete a user with wrong author - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response should contain: - | documentId | "TYPE:ID" | - When make a DELETE request to "/discord/{id}" - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | message | "deletion failed: author doesn't match" | +# Scenario: delete a user with wrong author +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response should contain: +# | documentId | "TYPE:ID" | +# When make a DELETE request to "/discord/{id}" +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | message | "deletion failed: author doesn't match" | - Scenario: delete non-existing user - Given authorisation - When make a DELETE request to "/discord/321" - Then the response status code should be 404 - And the response should contain: - | statusCode | 404 | - | message | "no discord-profile for 321 found" | +# Scenario: delete non-existing user +# Given authorisation +# When make a DELETE request to "/discord/321" +# Then the response status code should be 404 +# And the response should contain: +# | statusCode | 404 | +# | message | "no discord-profile for 321 found" | - Scenario: get user with authenticated request - Given authorisation - And make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response should contain: - | documentId | "TYPE:ID" | - When make a GET request to "/discord/{id}" - Then the response status code should be 200 - And the response should contain: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - | updatedOn | "TYPE:DATE" | - | createdOn | "TYPE:DATE" | +# Scenario: get user with authenticated request +# Given authorisation +# And make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response should contain: +# | documentId | "TYPE:ID" | +# When make a GET request to "/discord/{id}" +# Then the response status code should be 200 +# And the response should contain: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# | updatedOn | "TYPE:DATE" | +# | createdOn | "TYPE:DATE" | - Scenario: create a user without authorization - Given make a POST request to "/discord" with: - | bio | "This is a GitHub Campus Expert" | - | author | {"platform":"discord","uid":"hubber"} | - | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | - Then the response status code should be 401 - And the response should contain: - | statusCode | 401 | - | message | "Unauthorized" | +# Scenario: create a user without authorization +# Given make a POST request to "/discord" with: +# | bio | "This is a GitHub Campus Expert" | +# | author | {"platform":"discord","uid":"hubber"} | +# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | +# Then the response status code should be 401 +# And the response should contain: +# | statusCode | 401 | +# | message | "Unauthorized" | diff --git a/test/features/github.feature b/test/features/github.feature index 497f516b..e7f9019a 100644 --- a/test/features/github.feature +++ b/test/features/github.feature @@ -1,197 +1,197 @@ -@github -Feature: github module +# @github +# Feature: github module - Scenario: add a new githubprofile - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - Then the response status code should be 201 - And the response should contain: - | documentId | "TYPE:ID" | +# Scenario: add a new githubprofile +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# Then the response status code should be 201 +# And the response should contain: +# | documentId | "TYPE:ID" | - Scenario: get list of githubprofiles - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a GET request to "/github" - Then the response status code should be 200 - And the response in item where field "username" is equal to "eddiehubber" should contain: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | communityStats | {"push":1} | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | - | updatedOn | "TYPE:DATE" | - | createdOn | "TYPE:DATE" | +# Scenario: get list of githubprofiles +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a GET request to "/github" +# Then the response status code should be 200 +# And the response in item where field "username" is equal to "eddiehubber" should contain: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | communityStats | {"push":1} | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | +# | updatedOn | "TYPE:DATE" | +# | createdOn | "TYPE:DATE" | - Scenario: add an empty githubprofile - Given authorisation - And make a POST request to "/github" with: - | test | "test" | - Then the response status code should be 400 - And the response should contain: - | statusCode | 400 | - | error | "Bad Request" | - And the response property "message" has items: - | username must be a string | - | username should not be empty | +# Scenario: add an empty githubprofile +# Given authorisation +# And make a POST request to "/github" with: +# | test | "test" | +# Then the response status code should be 400 +# And the response should contain: +# | statusCode | 400 | +# | error | "Bad Request" | +# And the response property "message" has items: +# | username must be a string | +# | username should not be empty | - Scenario: delete a githubprofile - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a DELETE request to "/github/{id}" - Then the response status code should be 204 +# Scenario: delete a githubprofile +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a DELETE request to "/github/{id}" +# Then the response status code should be 204 - Scenario: delete non-existent githubprofile - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - And the response should contain: - | documentId | "TYPE:ID" | - Then make a DELETE request to "/github/66" - Then the response status code should be 404 - And the response should contain: - | statusCode | 404 | - | message | "no github-profile for 66 found" | +# Scenario: delete non-existent githubprofile +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# Then make a DELETE request to "/github/66" +# Then the response status code should be 404 +# And the response should contain: +# | statusCode | 404 | +# | message | "no github-profile for 66 found" | - Scenario: update githubprofile with previously used event - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - And the response should contain: - | documentId | "TYPE:ID" | - Then make a PUT request to "/github/{id}" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - | event | "push" | - Then the response status code should be 200 - And the response should contain: - | documentId | "TYPE:ID" | +# Scenario: update githubprofile with previously used event +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# Then make a PUT request to "/github/{id}" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# | event | "push" | +# Then the response status code should be 200 +# And the response should contain: +# | documentId | "TYPE:ID" | - Scenario: update githubprofile with previously unused event - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - And the response should contain: - | documentId | "TYPE:ID" | - Then make a PUT request to "/github/{id}" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - | event | "label" | - Then the response status code should be 200 - And the response should contain: - | documentId | "TYPE:ID" | +# Scenario: update githubprofile with previously unused event +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# Then make a PUT request to "/github/{id}" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# | event | "label" | +# Then the response status code should be 200 +# And the response should contain: +# | documentId | "TYPE:ID" | - Scenario: get githubprofile with authenticated request - Given authorisation - And make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - And the response should contain: - | documentId | "TYPE:ID" | - When make a GET request to "/github/{id}" - Then the response status code should be 200 - And the response should contain: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | communityStats | {"push":1} | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | - | updatedOn | "TYPE:DATE" | - | createdOn | "TYPE:DATE" | +# Scenario: get githubprofile with authenticated request +# Given authorisation +# And make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# And the response should contain: +# | documentId | "TYPE:ID" | +# When make a GET request to "/github/{id}" +# Then the response status code should be 200 +# And the response should contain: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | communityStats | {"push":1} | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | +# | updatedOn | "TYPE:DATE" | +# | createdOn | "TYPE:DATE" | - Scenario: create a githubprofile without authentication - Given make a POST request to "/github" with: - | username | "eddiehubber" | - | bio | "I love to code" | - | avatarUrl | "https://dummy.com/avatar" | - | followers | 500 | - | repos | 32 | - | event | "push" | - | blog | "https://www.myBlog.com" | - | organization | "Eddiehub" | - | location | "London" | - Then the response status code should be 401 - And the response should contain: - | statusCode | 401 | - | message | "Unauthorized" | +# Scenario: create a githubprofile without authentication +# Given make a POST request to "/github" with: +# | username | "eddiehubber" | +# | bio | "I love to code" | +# | avatarUrl | "https://dummy.com/avatar" | +# | followers | 500 | +# | repos | 32 | +# | event | "push" | +# | blog | "https://www.myBlog.com" | +# | organization | "Eddiehub" | +# | location | "London" | +# Then the response status code should be 401 +# And the response should contain: +# | statusCode | 401 | +# | message | "Unauthorized" | diff --git a/test/features/standup.feature b/test/features/standup.feature index d974522e..446aa7f6 100644 --- a/test/features/standup.feature +++ b/test/features/standup.feature @@ -2,7 +2,7 @@ Feature: Standup module Scenario: add a new standup - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -12,7 +12,7 @@ Feature: Standup module | documentId | "TYPE:ID" | Scenario: search existing standup - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -28,7 +28,7 @@ Feature: Standup module | createdOn | "TYPE:DATE" | Scenario: search non-existing standup - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -40,7 +40,7 @@ Feature: Standup module And the response should be "{}" Scenario: provide no search context - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -54,7 +54,7 @@ Feature: Standup module | message | "Please provide search context" | Scenario: add an empty standup - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | test | "test" | Then the response status code should be 400 @@ -69,7 +69,7 @@ Feature: Standup module | todayMessage must be a string | Scenario: delete standup - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -82,7 +82,7 @@ Feature: Standup module Then the response status code should be 204 Scenario: delete standup with wrong credentials - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -96,7 +96,7 @@ Feature: Standup module | message | "deletion failed: author doesn't match" | Scenario: delete non-existent standup - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -110,7 +110,7 @@ Feature: Standup module | message | "no standup for 66 found" | Scenario: get standup with authenticated request - Given authorisation + Given authorization with Writing-Scopes And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -126,7 +126,7 @@ Feature: Standup module | createdOn | "TYPE:DATE" | Scenario: create standup without authorization - Given make a POST request to "/standup" with: + When make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | | todayMessage | "Today I'll do this" | diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 7737dd02..a7d6acc8 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -1,13 +1,12 @@ -import { binding, given, when, before } from 'cucumber-tsflow'; +import { ValidationPipe } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import * as request from 'supertest'; import { exec } from 'child_process'; +import { BeforeAll, setDefaultTimeout } from 'cucumber'; +import { before, binding, given, when } from 'cucumber-tsflow'; +import { sign } from 'jsonwebtoken'; +import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; import Context from '../support/world'; -import { ExecutionContext, ValidationPipe } from '@nestjs/common'; -import { BeforeAll, setDefaultTimeout } from 'cucumber'; -import { JWTGuard } from '../../src/auth/jwt.strategy'; -import { TokenPayload } from '../../src/auth/interfaces/token-payload.interface'; setDefaultTimeout(60 * 1000); @@ -47,23 +46,34 @@ export class requests { public async before(): Promise { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], - }) - .overrideGuard(JWTGuard) - .useValue({ - canActivate: (ctx: ExecutionContext) => { - const req = ctx.switchToHttp().getRequest(); - //TODO add predefined user here - req.user = {} as TokenPayload; - return true; - }, - }) - .compile(); + }).compile(); this.context.app = moduleFixture.createNestApplication(); this.context.app.useGlobalPipes(new ValidationPipe({ transform: true })); await this.context.app.init(); } + @given(/authorization with Reading-Scopes/) + public async generateReadToken() { + const token = sign( + { scopes: ['Data.Read'], serverId: 'eddiehub' }, + process.env.SECRET, + ); + this.context.bearerToken = token; + } + + @given(/authorization with Writing-Scopes/) + public async generateWriteToken() { + const token = sign( + { + scopes: ['Data.Read', 'Data.Write'], + keyspace: 'eddiehub', + }, + process.env.SECRET, + ); + this.context.bearerToken = token; + } + @given(/authorisation/) public async authorisation() { this.context.token = 'abc'; @@ -72,9 +82,13 @@ export class requests { @given(/make a GET request to "([^"]*)"/) public async getRequest(url: string) { url = this.prepareURL(url); - this.context.response = await request(this.context.app.getHttpServer()).get( - url, - ); + + const get = request(this.context.app.getHttpServer()).get(url); + + if (this.context.bearerToken) { + get.set('Authorization', `Bearer ${this.context.bearerToken}`); + } + this.context.response = await get.send(); } @given(/make a POST request to "([^"]*)" with:/) @@ -87,11 +101,14 @@ export class requests { post.set('Client-Token', this.context.token); } + if (this.context.bearerToken) { + post.set('Authorization', `Bearer ${this.context.bearerToken}`); + } this.context.response = await post.send(this.context.tableToObject(table)); - this.context.preRequest = await request( - this.context.app.getHttpServer(), - ).get(url); + // this.context.preRequest = await request( + // this.context.app.getHttpServer(), + // ).get(url); } @when(/set header "([^"]*)" with value "([^"]*)"/) @@ -113,6 +130,10 @@ export class requests { putReq.set(this.context.headers); } + if (this.context.bearerToken) { + putReq.set('Authorization', `Bearer ${this.context.bearerToken}`); + } + this.context.response = await putReq.send( this.context.tableToObject(table), ); @@ -131,6 +152,10 @@ export class requests { deleteReq.set(this.context.headers); } + if (this.context.bearerToken) { + deleteReq.set('Authorization', `Bearer ${this.context.bearerToken}`); + } + this.context.response = await deleteReq.send(); } } diff --git a/test/step-definitions/responses.ts b/test/step-definitions/responses.ts index 67daed1b..d3913e0e 100644 --- a/test/step-definitions/responses.ts +++ b/test/step-definitions/responses.ts @@ -4,7 +4,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from '../../src/app.module'; import Context from '../support/world'; import { getRegex } from '../support/regexes'; -import { ValidationPipe } from '@nestjs/common'; +import { + ExecutionContext, + UnauthorizedException, + ValidationPipe, +} from '@nestjs/common'; +import { JWTGuard } from '../../src/auth/jwt.strategy'; +import { TokenPayload } from '../../src/auth/interfaces/token-payload.interface'; +import { decode } from 'jsonwebtoken'; +import { Request } from 'express'; @binding([Context]) export class responses { @@ -14,7 +22,20 @@ export class responses { public async before(): Promise { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], - }).compile(); + }) + .overrideGuard(JWTGuard) + .useValue({ + canActivate: (ctx: ExecutionContext) => { + const req: Request = ctx.switchToHttp().getRequest(); + if (req.headers.authorization) { + const accessToken = req.headers.authorization.split(' ')[1]; + req.user = decode(accessToken) as TokenPayload; + return true; + } + throw new UnauthorizedException(); + }, + }) + .compile(); this.context.app = moduleFixture.createNestApplication(); this.context.app.useGlobalPipes(new ValidationPipe({ transform: true })); diff --git a/test/support/world.ts b/test/support/world.ts index beccea7f..2037eecb 100644 --- a/test/support/world.ts +++ b/test/support/world.ts @@ -4,6 +4,7 @@ export default class Context { public preRequest; public headers: { [field: string]: string } = {}; public token; + public bearerToken: string; public documentId: string; public tableToObject(table) { From a88488cab3b6afaa5dd4671bf31759bf2867c7b2 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 18 Jul 2021 11:24:02 +0200 Subject: [PATCH 15/40] feat: github-module to new feature --- src/github/github.controller.ts | 56 ++++++++++++++------- src/github/github.module.ts | 21 ++++---- src/github/github.service.ts | 89 ++++++++++++++++++--------------- 3 files changed, 97 insertions(+), 69 deletions(-) diff --git a/src/github/github.controller.ts b/src/github/github.controller.ts index e5fe1417..e2e98086 100644 --- a/src/github/github.controller.ts +++ b/src/github/github.controller.ts @@ -9,8 +9,15 @@ import { Put, UseGuards, } from '@nestjs/common'; -import { ApiSecurity, ApiTags } from '@nestjs/swagger'; -import { TokenGuard } from '../auth/token.strategy'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Scopes } from '../auth/decorators/scopes.decorator'; +import { User } from '../auth/decorators/user.decorator'; +import { ScopesGuard } from '../auth/guards/scopes.guard'; +import { + ScopesDictionary, + TokenPayload, +} from '../auth/interfaces/token-payload.interface'; +import { JWTGuard } from '../auth/jwt.strategy'; import { GithubDTO } from './dto/github.dto'; import { GithubService } from './github.service'; @ApiTags('Github') @@ -18,35 +25,48 @@ import { GithubService } from './github.service'; export class GithubController { constructor(private readonly githubService: GithubService) {} @Post() - @ApiSecurity('token') - @UseGuards(TokenGuard) - async create(@Body() body: GithubDTO) { - return await this.githubService.create(body); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) + async create(@Body() body: GithubDTO, @User() user: TokenPayload) { + return await this.githubService.create(body, user.keyspace); } @Get() - findAll() { - return this.githubService.findAll(); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.READ) + findAll(@User() user: TokenPayload) { + return this.githubService.findAll(user.keyspace); } @Get(':id') - findOne(@Param('id') id: string) { - return this.githubService.findOne(id); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.READ) + findOne(@Param('id') id: string, @User() user: TokenPayload) { + return this.githubService.findOne(id, user.keyspace); } @Put(':id') - @UseGuards(TokenGuard) + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) @HttpCode(200) - @ApiSecurity('token') - async update(@Param('id') id: string, @Body() body: GithubDTO) { - return await this.githubService.update(id, body); + async update( + @Param('id') id: string, + @Body() body: GithubDTO, + @User() user: TokenPayload, + ) { + return await this.githubService.update(id, body, user.keyspace); } @Delete(':id') - @UseGuards(TokenGuard) - @ApiSecurity('token') + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) @HttpCode(204) - remove(@Param('id') id: string) { - return this.githubService.remove(id); + remove(@Param('id') id: string, @User() user: TokenPayload) { + return this.githubService.remove(id, user.keyspace); } } diff --git a/src/github/github.module.ts b/src/github/github.module.ts index 8696d1ff..4c6bf793 100644 --- a/src/github/github.module.ts +++ b/src/github/github.module.ts @@ -1,18 +1,19 @@ import { HttpModule, Module } from '@nestjs/common'; -import { GithubController } from './github.controller'; -import { GithubService } from './github.service'; +import { AstraService } from '../astra/astra.service'; +import { AuthModule } from '../auth/auth.module'; import { CommunitystatsMappingService } from './communitystats-mapping.service'; import { GeocodingService } from './geocoding.service'; -import { AuthModule } from '../auth/auth.module'; -import { AstraModule } from '@cahllagerfeld/nestjs-astra'; +import { GithubController } from './github.controller'; +import { GithubService } from './github.service'; @Module({ - imports: [ - HttpModule, - AuthModule, - AstraModule.forFeature({ namespace: 'eddiehub', collection: 'github' }), - ], + imports: [HttpModule, AuthModule], controllers: [GithubController], - providers: [GithubService, CommunitystatsMappingService, GeocodingService], + providers: [ + GithubService, + CommunitystatsMappingService, + GeocodingService, + AstraService, + ], }) export class GithubModule {} diff --git a/src/github/github.service.ts b/src/github/github.service.ts index 936ddc61..574e9ee0 100644 --- a/src/github/github.service.ts +++ b/src/github/github.service.ts @@ -3,13 +3,10 @@ import { CommunityStats, GithubProfile } from './interfaces/github.interface'; import { GithubDTO } from './dto/github.dto'; import { CommunitystatsMappingService } from './communitystats-mapping.service'; import { GeocodingService } from './geocoding.service'; -import { - AstraService, - deleteItem, - documentId, -} from '@cahllagerfeld/nestjs-astra'; +import { deleteItem, documentId } from '@cahllagerfeld/nestjs-astra'; import { from, Observable } from 'rxjs'; import { catchError, concatMap, filter } from 'rxjs/operators'; +import { AstraService } from '../astra/astra.service'; @Injectable() export class GithubService { @@ -19,14 +16,14 @@ export class GithubService { private readonly astraService: AstraService, ) {} - async create(body: GithubDTO): Promise { + async create(body: GithubDTO, keyspaceName: string): Promise { let newGithubProfile: GithubProfile; let creationResponse; try { newGithubProfile = await this.createGithub(body); creationResponse = await this.astraService - .create(newGithubProfile) + .create(newGithubProfile, keyspaceName, 'github') .toPromise(); } catch (e) { throw new HttpException( @@ -38,24 +35,30 @@ export class GithubService { return creationResponse; } - findAll() { + findAll(keyspaceName: string) { return this.astraService - .find() + .find(keyspaceName, 'github') .pipe(catchError(() => from([{}]))); } - findOne(id: string): Observable { - return this.astraService.get(id).pipe( - catchError(() => { - throw new HttpException( - `no github-profile for ${id} found`, - HttpStatus.NOT_FOUND, - ); - }), - ); + findOne(id: string, keyspaceName: string): Observable { + return this.astraService + .get(id, keyspaceName, 'github') + .pipe( + catchError(() => { + throw new HttpException( + `no github-profile for ${id} found`, + HttpStatus.NOT_FOUND, + ); + }), + ); } - async update(id: string, body: GithubDTO): Promise { + async update( + id: string, + body: GithubDTO, + keyspaceName: string, + ): Promise { const { username, bio, @@ -70,7 +73,9 @@ export class GithubService { let oldDocument; try { - oldDocument = await this.astraService.get(id).toPromise(); + oldDocument = await this.astraService + .get(id, keyspaceName, 'github') + .toPromise(); } catch (e) { throw new HttpException( `no github-profile for ${id} found`, @@ -118,7 +123,7 @@ export class GithubService { let updateResponse; try { updateResponse = await this.astraService - .replace(id, updateGithubProfile) + .replace(id, updateGithubProfile, keyspaceName, 'github') .toPromise(); } catch (e) { throw new HttpException( @@ -130,30 +135,32 @@ export class GithubService { return updateResponse; } - remove(id: string): Observable { - return this.astraService.get(id).pipe( - catchError(() => { - throw new HttpException( - `no github-profile for ${id} found`, - HttpStatus.NOT_FOUND, - ); - }), - filter((data) => { - if (!data) { + remove(id: string, keyspaceName: string): Observable { + return this.astraService + .get(id, keyspaceName, 'github') + .pipe( + catchError(() => { throw new HttpException( `no github-profile for ${id} found`, HttpStatus.NOT_FOUND, ); - } - - return true; - }), - concatMap(() => - this.astraService - .delete(id) - .pipe(filter((data: deleteItem) => data.deleted === true)), - ), - ); + }), + filter((data) => { + if (!data) { + throw new HttpException( + `no github-profile for ${id} found`, + HttpStatus.NOT_FOUND, + ); + } + + return true; + }), + concatMap(() => + this.astraService + .delete(id, keyspaceName, 'github') + .pipe(filter((data: deleteItem) => data.deleted === true)), + ), + ); } private async createGithub(body: GithubDTO): Promise { From 63192392f4a5d338f3fe3031159a03ab6701c29e Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 18 Jul 2021 17:09:08 +0200 Subject: [PATCH 16/40] feat: add new logic to discord module --- src/discord/discord.controller.ts | 64 ++++++++++---- src/discord/discord.module.ts | 9 +- src/discord/discord.service.ts | 134 +++++++++++++++++------------- 3 files changed, 125 insertions(+), 82 deletions(-) diff --git a/src/discord/discord.controller.ts b/src/discord/discord.controller.ts index ea895978..7c5bd259 100644 --- a/src/discord/discord.controller.ts +++ b/src/discord/discord.controller.ts @@ -9,51 +9,83 @@ import { UseGuards, HttpCode, } from '@nestjs/common'; -import { ApiHeader, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + ApiHeader, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; import { TokenGuard } from '../auth/token.strategy'; import { Author, AuthorObject } from '../auth/author-headers'; import { DiscordService } from './discord.service'; import { DiscordDTO } from './dto/discord.dto'; +import { User } from '../auth/decorators/user.decorator'; +import { + ScopesDictionary, + TokenPayload, +} from '../auth/interfaces/token-payload.interface'; +import { JWTGuard } from '../auth/jwt.strategy'; +import { ScopesGuard } from '../auth/guards/scopes.guard'; +import { Scopes } from '../auth/decorators/scopes.decorator'; @ApiTags('Discord') @Controller('discord') export class DiscordController { constructor(private readonly discordService: DiscordService) {} @Post() - @UseGuards(TokenGuard) - @ApiSecurity('token') - create(@Body() createDiscordDto: DiscordDTO) { - return this.discordService.create(createDiscordDto); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) + create(@Body() createDiscordDto: DiscordDTO, @User() user: TokenPayload) { + return this.discordService.create(createDiscordDto, user.keyspace); } @Get() - findAll() { - return this.discordService.findAll(); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.READ) + findAll(@User() user: TokenPayload) { + return this.discordService.findAll(user.keyspace); } @Get(':id') - findOne(@Param('id') id: string) { - return this.discordService.findOne(id); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.READ) + findOne(@Param('id') id: string, @User() user: TokenPayload) { + return this.discordService.findOne(id, user.keyspace); } @Put(':id') - @ApiSecurity('token') - @UseGuards(TokenGuard) + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) @ApiHeader({ name: 'User-Uid', required: true }) update( @Param('id') id: string, @Body() updateDiscordDto: DiscordDTO, @AuthorObject() author: Author, + @User() user: TokenPayload, ) { - return this.discordService.update(id, updateDiscordDto, author); + return this.discordService.update( + id, + updateDiscordDto, + author, + user.keyspace, + ); } @Delete(':id') - @ApiSecurity('token') @HttpCode(204) @ApiHeader({ name: 'User-Uid', required: true }) - @UseGuards(TokenGuard) - remove(@Param('id') id: string, @AuthorObject() author: Author) { - return this.discordService.remove(id, author); + @UseGuards(JWTGuard, ScopesGuard) + @ApiBearerAuth() + @Scopes(ScopesDictionary.WRITE) + remove( + @Param('id') id: string, + @AuthorObject() author: Author, + @User() user: TokenPayload, + ) { + return this.discordService.remove(id, author, user.keyspace); } } diff --git a/src/discord/discord.module.ts b/src/discord/discord.module.ts index e757676c..b27db209 100644 --- a/src/discord/discord.module.ts +++ b/src/discord/discord.module.ts @@ -1,15 +1,12 @@ import { Module } from '@nestjs/common'; import { DiscordService } from './discord.service'; import { DiscordController } from './discord.controller'; -import { AstraModule } from '@cahllagerfeld/nestjs-astra'; import { AuthModule } from '../auth/auth.module'; +import { AstraService } from '../astra/astra.service'; @Module({ - imports: [ - AuthModule, - AstraModule.forFeature({ namespace: 'eddiehub', collection: 'discord' }), - ], + imports: [AuthModule], controllers: [DiscordController], - providers: [DiscordService], + providers: [DiscordService, AstraService], }) export class DiscordModule {} diff --git a/src/discord/discord.service.ts b/src/discord/discord.service.ts index 75673722..069315f1 100644 --- a/src/discord/discord.service.ts +++ b/src/discord/discord.service.ts @@ -1,8 +1,4 @@ -import { - AstraService, - deleteItem, - documentId, -} from '@cahllagerfeld/nestjs-astra'; +import { deleteItem, documentId } from '@cahllagerfeld/nestjs-astra'; import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { from, Observable } from 'rxjs'; import { catchError, concatMap, filter } from 'rxjs/operators'; @@ -10,6 +6,7 @@ import { ValidationService } from '../auth/header-validation.service'; import { Author } from '../auth/author-headers'; import { DiscordDTO } from './dto/discord.dto'; import { DiscordProfile } from './interfaces/discord.interface'; +import { AstraService } from '../astra/astra.service'; @Injectable() export class DiscordService { @@ -18,7 +15,11 @@ export class DiscordService { private readonly validationService: ValidationService, ) {} - create(createDiscordDto: DiscordDTO): Observable { + create( + createDiscordDto: DiscordDTO, + keyspaceName: string, + ): Observable { + console.log(keyspaceName); const discordUser: DiscordProfile = { author: { ...createDiscordDto.author }, bio: createDiscordDto.bio, @@ -27,40 +28,51 @@ export class DiscordService { updatedOn: new Date(), }; - return this.astraService.create(discordUser).pipe( - catchError(() => { - throw new HttpException( - 'Creation didnt pass as expected', - HttpStatus.BAD_REQUEST, - ); - }), - ); + return this.astraService + .create(discordUser, keyspaceName, 'discord') + .pipe( + catchError(() => { + throw new HttpException( + 'Creation didnt pass as expected', + HttpStatus.BAD_REQUEST, + ); + }), + ); } - findAll() { + findAll(keyspaceName: string) { return this.astraService - .find() + .find(keyspaceName, 'discord') .pipe(catchError(() => from([{}]))); } - findOne(id: string) { - return this.astraService.get(id).pipe( - catchError(() => { - throw new HttpException( - 'Creation didnt pass as expected', - HttpStatus.BAD_REQUEST, - ); - }), - ); + findOne(id: string, keyspaceName: string) { + return this.astraService + .get(id, keyspaceName, 'discord') + .pipe( + catchError(() => { + throw new HttpException( + 'Creation didnt pass as expected', + HttpStatus.BAD_REQUEST, + ); + }), + ); } - async update(id: string, updateDiscordDto: DiscordDTO, authorObject: Author) { + async update( + id: string, + updateDiscordDto: DiscordDTO, + authorObject: Author, + keyspaceName: string, + ) { const { author, bio, socials } = updateDiscordDto; let discordUser; try { - discordUser = await this.astraService.get(id).toPromise(); + discordUser = await this.astraService + .get(id, keyspaceName, 'discord') + .toPromise(); } catch (e) { throw new HttpException( `no discord-profile for ${id} found`, @@ -111,7 +123,7 @@ export class DiscordService { try { updateResponse = await this.astraService - .replace(id, updatedDiscord) + .replace(id, updatedDiscord, keyspaceName, 'discord') .toPromise(); } catch (e) { throw new HttpException( @@ -123,41 +135,43 @@ export class DiscordService { return updateResponse; } - remove(id: string, authorObject: Author) { - return this.astraService.get(id).pipe( - catchError(() => { - throw new HttpException( - `no discord-profile for ${id} found`, - HttpStatus.NOT_FOUND, - ); - }), - filter((data: DiscordProfile) => { - if (!data) { + remove(id: string, authorObject: Author, keyspaceName: string) { + return this.astraService + .get(id, keyspaceName, 'discord') + .pipe( + catchError(() => { throw new HttpException( `no discord-profile for ${id} found`, HttpStatus.NOT_FOUND, ); - } - - if ( - !this.validationService.validateAuthor( - data.author, - authorObject.uid, - 'discord', - ) - ) { - throw new HttpException( - "deletion failed: author doesn't match", - HttpStatus.BAD_REQUEST, - ); - } - return true; - }), - concatMap(() => - this.astraService - .delete(id) - .pipe(filter((data: deleteItem) => data.deleted === true)), - ), - ); + }), + filter((data: DiscordProfile) => { + if (!data) { + throw new HttpException( + `no discord-profile for ${id} found`, + HttpStatus.NOT_FOUND, + ); + } + + if ( + !this.validationService.validateAuthor( + data.author, + authorObject.uid, + 'discord', + ) + ) { + throw new HttpException( + "deletion failed: author doesn't match", + HttpStatus.BAD_REQUEST, + ); + } + return true; + }), + concatMap(() => + this.astraService + .delete(id, keyspaceName, 'discord') + .pipe(filter((data: deleteItem) => data.deleted === true)), + ), + ); } } From dc173082c08d29d7230aaeffdd8d3ff3247df730 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 18 Jul 2021 21:29:05 +0200 Subject: [PATCH 17/40] feat: add new auth logic to calendar module --- src/calendar/calendar.controller.ts | 55 +++++++---- src/calendar/calendar.module.ts | 9 +- src/calendar/calendar.service.ts | 139 +++++++++++++++------------- 3 files changed, 117 insertions(+), 86 deletions(-) diff --git a/src/calendar/calendar.controller.ts b/src/calendar/calendar.controller.ts index 98c11b46..4331f90e 100644 --- a/src/calendar/calendar.controller.ts +++ b/src/calendar/calendar.controller.ts @@ -9,9 +9,16 @@ import { Put, UseGuards, } from '@nestjs/common'; -import { ApiHeader, ApiSecurity, ApiTags } from '@nestjs/swagger'; -import { TokenGuard } from '../auth/token.strategy'; +import { ApiBearerAuth, ApiHeader, ApiTags } from '@nestjs/swagger'; import { Author, AuthorObject } from '../auth/author-headers'; +import { Scopes } from '../auth/decorators/scopes.decorator'; +import { User } from '../auth/decorators/user.decorator'; +import { ScopesGuard } from '../auth/guards/scopes.guard'; +import { + ScopesDictionary, + TokenPayload, +} from '../auth/interfaces/token-payload.interface'; +import { JWTGuard } from '../auth/jwt.strategy'; import { CalendarService } from './calendar.service'; import { CalendarEventDTO } from './dto/calendar.dto'; @@ -21,42 +28,56 @@ export class CalendarController { constructor(private readonly service: CalendarService) {} @Post() - @UseGuards(TokenGuard) - @ApiSecurity('token') - create(@Body() calendarEvent: CalendarEventDTO) { - return this.service.createCalendarEvent(calendarEvent); + @ApiBearerAuth() + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.WRITE) + create(@Body() calendarEvent: CalendarEventDTO, @User() user: TokenPayload) { + return this.service.createCalendarEvent(calendarEvent, user.keyspace); } @Get() - findAll() { - return this.service.findAllEvents(); + @ApiBearerAuth() + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.READ) + findAll(@User() user: TokenPayload) { + return this.service.findAllEvents(user.keyspace); } @Get(':id') - findOne(@Param('id') id: string) { - return this.service.findOne(id); + @ApiBearerAuth() + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.READ) + findOne(@Param('id') id: string, @User() user: TokenPayload) { + return this.service.findOne(id, user.keyspace); } @Put(':id') - @UseGuards(TokenGuard) - @ApiSecurity('token') + @ApiBearerAuth() + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.WRITE) @ApiHeader({ name: 'User-Uid', required: true }) @ApiHeader({ name: 'Platform', required: true }) updateOne( @Param('id') id: string, @Body() calendarEvent: CalendarEventDTO, @AuthorObject() author: Author, + @User() user: TokenPayload, ) { - return this.service.updateOne(id, calendarEvent, author); + return this.service.updateOne(id, calendarEvent, author, user.keyspace); } @Delete(':id') - @UseGuards(TokenGuard) + @ApiBearerAuth() + @UseGuards(JWTGuard, ScopesGuard) + @Scopes(ScopesDictionary.WRITE) @HttpCode(204) - @ApiSecurity('token') @ApiHeader({ name: 'User-Uid', required: true }) @ApiHeader({ name: 'Platform', required: true }) - remove(@Param('id') id: string, @AuthorObject() author: Author) { - return this.service.remove(id, author); + remove( + @Param('id') id: string, + @AuthorObject() author: Author, + @User() user: TokenPayload, + ) { + return this.service.remove(id, author, user.keyspace); } } diff --git a/src/calendar/calendar.module.ts b/src/calendar/calendar.module.ts index 913fed63..f23cb3ec 100644 --- a/src/calendar/calendar.module.ts +++ b/src/calendar/calendar.module.ts @@ -1,15 +1,12 @@ -import { AstraModule } from '@cahllagerfeld/nestjs-astra'; import { Module } from '@nestjs/common'; +import { AstraService } from '../astra/astra.service'; import { AuthModule } from '../auth/auth.module'; import { CalendarController } from './calendar.controller'; import { CalendarService } from './calendar.service'; @Module({ - imports: [ - AuthModule, - AstraModule.forFeature({ namespace: 'eddiehub', collection: 'calendar' }), - ], + imports: [AuthModule], controllers: [CalendarController], - providers: [CalendarService], + providers: [CalendarService, AstraService], }) export class CalendarModule {} diff --git a/src/calendar/calendar.service.ts b/src/calendar/calendar.service.ts index 59a218c1..cf709bcd 100644 --- a/src/calendar/calendar.service.ts +++ b/src/calendar/calendar.service.ts @@ -2,14 +2,11 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CalendarEventDTO } from './dto/calendar.dto'; import { CalendarEvent } from './interfaces/calendar.interface'; import { catchError, concatMap, filter } from 'rxjs/operators'; -import { - AstraService, - deleteItem, - documentId, -} from '@cahllagerfeld/nestjs-astra'; +import { deleteItem, documentId } from '@cahllagerfeld/nestjs-astra'; import { forkJoin, from, Observable } from 'rxjs'; import { Author } from '../auth/author-headers'; import { ValidationService } from '../auth/header-validation.service'; +import { AstraService } from '../astra/astra.service'; @Injectable() export class CalendarService { @@ -20,6 +17,7 @@ export class CalendarService { createCalendarEvent( calendarEventBody: CalendarEventDTO, + keyspaceName: string, ): Observable { const newEvent: CalendarEvent = { name: calendarEventBody.name, @@ -33,48 +31,61 @@ export class CalendarService { updatedOn: new Date(), }; - return this.astraService.create(newEvent).pipe( - catchError(() => { - throw new HttpException( - 'Creation didnt pass as expected', - HttpStatus.BAD_REQUEST, - ); - }), - ); + return this.astraService + .create(newEvent, keyspaceName, 'calendar') + .pipe( + catchError(() => { + throw new HttpException( + 'Creation didnt pass as expected', + HttpStatus.BAD_REQUEST, + ); + }), + ); } - findAllEvents() { - const future = this.astraService.find({ - startDate: { $gt: new Date() }, - endDate: { $gt: new Date() }, - }); + findAllEvents(keyspaceName: string) { + const future = this.astraService.find( + keyspaceName, + 'calendar', + { + startDate: { $gt: new Date() }, + endDate: { $gt: new Date() }, + }, + ); - const ongoing = this.astraService.find({ - startDate: { $lt: new Date() }, - endDate: { $gt: new Date() }, - }); + const ongoing = this.astraService.find( + keyspaceName, + 'calendar', + { + startDate: { $lt: new Date() }, + endDate: { $gt: new Date() }, + }, + ); return forkJoin({ future, ongoing }).pipe(catchError(() => from([{}]))); } - findOne(id: string) { - return this.astraService.get(id).pipe( - catchError(() => { - throw new HttpException( - 'Creation didnt pass as expected', - HttpStatus.BAD_REQUEST, - ); - }), - ); + findOne(id: string, keyspaceName: string) { + return this.astraService + .get(id, keyspaceName, 'calendar') + .pipe( + catchError(() => { + throw new HttpException( + 'Creation didnt pass as expected', + HttpStatus.BAD_REQUEST, + ); + }), + ); } async updateOne( id: string, calendarDTO: CalendarEventDTO, authorObject: Author, + keyspaceName: string, ) { const oldDocument = await this.astraService - .get(id) + .get(id, keyspaceName, 'calendar') .pipe( catchError(() => { throw new HttpException( @@ -142,7 +153,7 @@ export class CalendarService { updateEvent.updatedOn = new Date(); const updateResponse = await this.astraService - .replace(id, updateEvent) + .replace(id, updateEvent, keyspaceName, 'calendar') .pipe( catchError(() => { throw new HttpException( @@ -156,39 +167,41 @@ export class CalendarService { return updateResponse; } - remove(id: string, authorObject: Author) { - return this.astraService.get(id).pipe( - catchError(() => { - throw new HttpException( - `no event for ${id} found`, - HttpStatus.NOT_FOUND, - ); - }), - filter((data: CalendarEvent) => { - if (!data) { + remove(id: string, authorObject: Author, keyspaceName: string) { + return this.astraService + .get(id, keyspaceName, 'calendar') + .pipe( + catchError(() => { throw new HttpException( `no event for ${id} found`, HttpStatus.NOT_FOUND, ); - } - - if ( - !this.validationService.validateAuthor( - data.author, - authorObject.uid, - authorObject.platform, - ) - ) { - throw new HttpException( - "deletion failed: author doesn't match", - HttpStatus.BAD_REQUEST, - ); - } - - return true; - }), - concatMap(() => this.astraService.delete(id)), - filter((data: deleteItem) => data.deleted === true), - ); + }), + filter((data: CalendarEvent) => { + if (!data) { + throw new HttpException( + `no event for ${id} found`, + HttpStatus.NOT_FOUND, + ); + } + + if ( + !this.validationService.validateAuthor( + data.author, + authorObject.uid, + authorObject.platform, + ) + ) { + throw new HttpException( + "deletion failed: author doesn't match", + HttpStatus.BAD_REQUEST, + ); + } + + return true; + }), + concatMap(() => this.astraService.delete(id, keyspaceName, 'calendar')), + filter((data: deleteItem) => data.deleted === true), + ); } } From 2cddd684629b44605ce58345cefd1a3c67688e25 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 18 Jul 2021 21:38:42 +0200 Subject: [PATCH 18/40] docs: change order for swagger --- src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.module.ts b/src/app.module.ts index 1da1e2b2..a642911c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { AstraConfigService } from './astra/astra-config.service'; @Module({ imports: [ + AuthModule, AstraModule.forRootAsync({ useClass: AstraConfigService, }), @@ -22,7 +23,6 @@ import { AstraConfigService } from './astra/astra-config.service'; ConfigModule.forRoot({ isGlobal: true, }), - AuthModule, CalendarModule, ], controllers: [AppController], From cb523e01c122f2bd92ffb57e8a9107bd649f9923 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Mon, 19 Jul 2021 21:18:54 +0200 Subject: [PATCH 19/40] test: fix existing e2e tests --- test/features/calendar.feature | 483 +++++++++++++++++---------------- test/features/discord.feature | 260 +++++++++--------- test/features/github.feature | 376 ++++++++++++------------- 3 files changed, 560 insertions(+), 559 deletions(-) diff --git a/test/features/calendar.feature b/test/features/calendar.feature index 04efd8f6..f614adf5 100644 --- a/test/features/calendar.feature +++ b/test/features/calendar.feature @@ -1,253 +1,254 @@ -# @calendar -# Feature: calendar module +@calendar +Feature: calendar module -# Scenario: add a new event -# Given authorisation -# When make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a GET request to "/calendar/{id}" -# Then the response status code should be 200 -# And the response should contain: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# | createdOn | "TYPE:DATE" | -# | updatedOn | "TYPE:DATE" | + Scenario: add a new event + Given authorization with Writing-Scopes + When make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a GET request to "/calendar/{id}" + Then the response status code should be 200 + And the response should contain: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + | createdOn | "TYPE:DATE" | + | updatedOn | "TYPE:DATE" | -# Scenario: get list of events -# Given make a GET request to "/calendar" -# Then the response status code should be 200 -# And the response should contain: -# | future | {} | -# | ongoing | {} | + Scenario: get list of events + Given authorization with Writing-Scopes + When make a GET request to "/calendar" + Then the response status code should be 200 + And the response should contain: + | future | {} | + | ongoing | {} | -# Scenario: add an empty event -# Given authorisation -# And make a POST request to "/calendar" with: -# | test | "test" | -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | error | "Bad Request" | -# And the response property "message" has items: -# | name must be a string | -# | name should not be empty | -# | platform must be one of the following values: YouTube, Twitch | -# | platform must be a string | -# | platform should not be empty | -# | author should not be empty | -# | startDate should not be empty | -# | startDate must be a Date instance | -# | endDate must be a Date instance | -# | endDate should not be empty | + Scenario: add an empty event + Given authorization with Writing-Scopes + And make a POST request to "/calendar" with: + | test | "test" | + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | error | "Bad Request" | + And the response property "message" has items: + | name must be a string | + | name should not be empty | + | platform must be one of the following values: YouTube, Twitch | + | platform must be a string | + | platform should not be empty | + | author should not be empty | + | startDate should not be empty | + | startDate must be a Date instance | + | endDate must be a Date instance | + | endDate should not be empty | -# Scenario: update an event -# Given authorisation -# When make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# Then set header "User-Uid" with value "hubber" -# Then set header "Platform" with value "discord" -# When make a PUT request to "/calendar/{id}" with: -# | name | "Livestream YZ" | -# | description | "undescriptive Description" | -# | url | "https://mydomain.com" | -# | platform | "Twitch" | -# | author | {"platform":"discord","uid":"hubby"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# Then the response status code should be 200 -# When make a GET request to "/calendar/{id}" -# Then the response status code should be 200 -# And the response should contain: -# | name | "Livestream YZ" | -# | description | "undescriptive Description" | -# | url | "https://mydomain.com" | -# | platform | "Twitch" | -# | author | {"platform":"discord","uid":"hubby"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# | createdOn | "TYPE:DATE" | -# | updatedOn | "TYPE:DATE" | + Scenario: update an event + Given authorization with Writing-Scopes + When make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + And the response should contain: + | documentId | "TYPE:ID" | + Then set header "User-Uid" with value "hubber" + Then set header "Platform" with value "discord" + When make a PUT request to "/calendar/{id}" with: + | name | "Livestream YZ" | + | description | "undescriptive Description" | + | url | "https://mydomain.com" | + | platform | "Twitch" | + | author | {"platform":"discord","uid":"hubby"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + Then the response status code should be 200 + When make a GET request to "/calendar/{id}" + Then the response status code should be 200 + And the response should contain: + | name | "Livestream YZ" | + | description | "undescriptive Description" | + | url | "https://mydomain.com" | + | platform | "Twitch" | + | author | {"platform":"discord","uid":"hubby"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + | createdOn | "TYPE:DATE" | + | updatedOn | "TYPE:DATE" | -# Scenario: update an event with wrong author -# Given authorisation -# When make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a PUT request to "/calendar/{id}" with: -# | name | "Livestream YZ" | -# | description | "undescriptive Description" | -# | url | "https://mydomain.com" | -# | platform | "Twitch" | -# | author | {"platform":"discord","uid":"hubby"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | message | "update failed: author doesn't match" | + Scenario: update an event with wrong author + Given authorization with Writing-Scopes + When make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a PUT request to "/calendar/{id}" with: + | name | "Livestream YZ" | + | description | "undescriptive Description" | + | url | "https://mydomain.com" | + | platform | "Twitch" | + | author | {"platform":"discord","uid":"hubby"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | message | "update failed: author doesn't match" | -# Scenario: update an non-existing event -# Given authorisation -# When make a PUT request to "/calendar/321" with: -# | name | "Livestream YZ" | -# | description | "undescriptive Description" | -# | url | "https://mydomain.com" | -# | platform | "Twitch" | -# | author | {"platform":"discord","uid":"hubby"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# Then the response status code should be 404 -# And the response should contain: -# | message | "no event for 321 found" | -# | statusCode | 404 | + Scenario: update an non-existing event + Given authorization with Writing-Scopes + When make a PUT request to "/calendar/321" with: + | name | "Livestream YZ" | + | description | "undescriptive Description" | + | url | "https://mydomain.com" | + | platform | "Twitch" | + | author | {"platform":"discord","uid":"hubby"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + Then the response status code should be 404 + And the response should contain: + | message | "no event for 321 found" | + | statusCode | 404 | -# Scenario: delete an event -# Given authorisation -# When make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# Then set header "User-Uid" with value "hubber" -# Then set header "Platform" with value "discord" -# When make a DELETE request to "/calendar/{id}" -# Then the response status code should be 204 + Scenario: delete an event + Given authorization with Writing-Scopes + When make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + And the response should contain: + | documentId | "TYPE:ID" | + Then set header "User-Uid" with value "hubber" + Then set header "Platform" with value "discord" + When make a DELETE request to "/calendar/{id}" + Then the response status code should be 204 -# Scenario: delete an event with wrong author -# Given authorisation -# When make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a DELETE request to "/calendar/{id}" -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | message | "deletion failed: author doesn't match" | + Scenario: delete an event with wrong author + Given authorization with Writing-Scopes + When make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a DELETE request to "/calendar/{id}" + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | message | "deletion failed: author doesn't match" | -# Scenario: delete non-existing event -# Given authorisation -# When make a DELETE request to "/calendar/321" -# Then the response status code should be 404 -# And the response should contain: -# | statusCode | 404 | -# | message | "no event for 321 found" | + Scenario: delete non-existing event + Given authorization with Writing-Scopes + When make a DELETE request to "/calendar/321" + Then the response status code should be 404 + And the response should contain: + | statusCode | 404 | + | message | "no event for 321 found" | -# Scenario: get event with authenticated request -# Given authorisation -# When make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a GET request to "/calendar/{id}" -# Then the response status code should be 200 -# And the response should contain: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# | createdOn | "TYPE:DATE" | -# | updatedOn | "TYPE:DATE" | + Scenario: get event with authenticated request + Given authorization with Writing-Scopes + When make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a GET request to "/calendar/{id}" + Then the response status code should be 200 + And the response should contain: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + | createdOn | "TYPE:DATE" | + | updatedOn | "TYPE:DATE" | -# Scenario: create a event without authorization -# Given make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2021-01-01T00:00:00.000Z" | -# Then the response status code should be 401 -# And the response should contain: -# | statusCode | 401 | -# | message | "Unauthorized" | + Scenario: create a event without authorization + Given make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2021-01-01T00:00:00.000Z" | + Then the response status code should be 401 + And the response should contain: + | statusCode | 401 | + | message | "Unauthorized" | -# Scenario: get sorted ongoing and future events -# Given authorisation -# And make a POST request to "/calendar" with: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2022-01-01T00:00:00.000Z" | -# | endDate | "2023-01-01T00:00:00.000Z" | -# When make a POST request to "/calendar" with: -# | name | "Livestream YZ" | -# | description | "undescriptive Description" | -# | url | "https://mydomain.com" | -# | platform | "Twitch" | -# | author | {"platform":"discord","uid":"hubby"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2022-01-01T00:00:00.000Z" | -# When make a GET request to "/calendar" -# Then the response status code should be 200 -# And the response property "future" has a subobject with a field "name" that is equal to "Livestream XY" should contain: -# | name | "Livestream XY" | -# | description | "descriptive Description" | -# | url | "https://domain.com" | -# | platform | "YouTube" | -# | author | {"platform":"discord","uid":"hubber"} | -# | startDate | "2022-01-01T00:00:00.000Z" | -# | endDate | "2023-01-01T00:00:00.000Z" | -# | createdOn | "TYPE:DATE" | -# | updatedOn | "TYPE:DATE" | -# And the response property "ongoing" has a subobject with a field "name" that is equal to "Livestream YZ" should contain: -# | name | "Livestream YZ" | -# | description | "undescriptive Description" | -# | url | "https://mydomain.com" | -# | platform | "Twitch" | -# | author | {"platform":"discord","uid":"hubby"} | -# | startDate | "2021-01-01T00:00:00.000Z" | -# | endDate | "2022-01-01T00:00:00.000Z" | -# | createdOn | "TYPE:DATE" | -# | updatedOn | "TYPE:DATE" | + Scenario: get sorted ongoing and future events + Given authorization with Writing-Scopes + And make a POST request to "/calendar" with: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2022-01-01T00:00:00.000Z" | + | endDate | "2023-01-01T00:00:00.000Z" | + When make a POST request to "/calendar" with: + | name | "Livestream YZ" | + | description | "undescriptive Description" | + | url | "https://mydomain.com" | + | platform | "Twitch" | + | author | {"platform":"discord","uid":"hubby"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2022-01-01T00:00:00.000Z" | + When make a GET request to "/calendar" + Then the response status code should be 200 + And the response property "future" has a subobject with a field "name" that is equal to "Livestream XY" should contain: + | name | "Livestream XY" | + | description | "descriptive Description" | + | url | "https://domain.com" | + | platform | "YouTube" | + | author | {"platform":"discord","uid":"hubber"} | + | startDate | "2022-01-01T00:00:00.000Z" | + | endDate | "2023-01-01T00:00:00.000Z" | + | createdOn | "TYPE:DATE" | + | updatedOn | "TYPE:DATE" | + And the response property "ongoing" has a subobject with a field "name" that is equal to "Livestream YZ" should contain: + | name | "Livestream YZ" | + | description | "undescriptive Description" | + | url | "https://mydomain.com" | + | platform | "Twitch" | + | author | {"platform":"discord","uid":"hubby"} | + | startDate | "2021-01-01T00:00:00.000Z" | + | endDate | "2022-01-01T00:00:00.000Z" | + | createdOn | "TYPE:DATE" | + | updatedOn | "TYPE:DATE" | diff --git a/test/features/discord.feature b/test/features/discord.feature index 4b514192..7af369b6 100644 --- a/test/features/discord.feature +++ b/test/features/discord.feature @@ -1,140 +1,140 @@ -# @discord -# Feature: discord module +@discord +Feature: discord module -# Scenario: add a new user -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response status code should be 201 -# And the response should contain: -# | documentId | "TYPE:ID" | + Scenario: add a new user + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response status code should be 201 + And the response should contain: + | documentId | "TYPE:ID" | -# Scenario: get list of users -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response should contain: -# | documentId | "TYPE:ID" | -# When make a GET request to "/discord" -# Then the response status code should be 200 -# And the response in item where property "author" has a subobject "uid" which contains a field that is equal to "hubber" should contain: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# | updatedOn | "TYPE:DATE" | -# | createdOn | "TYPE:DATE" | + Scenario: get list of users + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response should contain: + | documentId | "TYPE:ID" | + When make a GET request to "/discord" + Then the response status code should be 200 + And the response in item where property "author" has a subobject "uid" which contains a field that is equal to "hubber" should contain: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + | updatedOn | "TYPE:DATE" | + | createdOn | "TYPE:DATE" | -# Scenario: add an empty user -# Given authorisation -# And make a POST request to "/discord" with: -# | test | "test" | -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | error | "Bad Request" | -# And the response property "message" has items: -# | author should not be empty | + Scenario: add an empty user + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | test | "test" | + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | error | "Bad Request" | + And the response property "message" has items: + | author should not be empty | -# Scenario: update a user -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response should contain: -# | documentId | "TYPE:ID" | -# When set header "User-Uid" with value "hubber" -# Then make a PUT request to "/discord/{id}" with: -# | author | {"platform":"discord","uid":"hubby"} | -# | bio | "Updated user bio" | -# | socials | {"discord":"update-user"} | -# When make a GET request to "/discord/{id}" -# Then the response status code should be 200 -# And the response should contain: -# | bio | "Updated user bio" | -# | author | {"platform":"discord","uid":"hubby"} | -# | socials | {"discord":"update-user","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# | updatedOn | "TYPE:DATE" | -# | createdOn | "TYPE:DATE" | + Scenario: update a user + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response should contain: + | documentId | "TYPE:ID" | + When set header "User-Uid" with value "hubber" + Then make a PUT request to "/discord/{id}" with: + | author | {"platform":"discord","uid":"hubby"} | + | bio | "Updated user bio" | + | socials | {"discord":"update-user"} | + When make a GET request to "/discord/{id}" + Then the response status code should be 200 + And the response should contain: + | bio | "Updated user bio" | + | author | {"platform":"discord","uid":"hubby"} | + | socials | {"discord":"update-user","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + | updatedOn | "TYPE:DATE" | + | createdOn | "TYPE:DATE" | -# Scenario: update a user with wrong author -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response should contain: -# | documentId | "TYPE:ID" | -# When make a PUT request to "/discord/{id}" with: -# | author | {"platform":"discord","uid":"hubby"} | -# | bio | "Updated user bio" | -# | socials | {"discord":"update-user"} | -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | message | "update failed: author doesn't match" | + Scenario: update a user with wrong author + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response should contain: + | documentId | "TYPE:ID" | + When make a PUT request to "/discord/{id}" with: + | author | {"platform":"discord","uid":"hubby"} | + | bio | "Updated user bio" | + | socials | {"discord":"update-user"} | + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | message | "update failed: author doesn't match" | -# Scenario: delete a user -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response should contain: -# | documentId | "TYPE:ID" | -# Then set header "User-Uid" with value "hubber" -# When make a DELETE request to "/discord/{id}" -# Then the response status code should be 204 + Scenario: delete a user + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response should contain: + | documentId | "TYPE:ID" | + Then set header "User-Uid" with value "hubber" + When make a DELETE request to "/discord/{id}" + Then the response status code should be 204 -# Scenario: delete a user with wrong author -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response should contain: -# | documentId | "TYPE:ID" | -# When make a DELETE request to "/discord/{id}" -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | message | "deletion failed: author doesn't match" | + Scenario: delete a user with wrong author + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response should contain: + | documentId | "TYPE:ID" | + When make a DELETE request to "/discord/{id}" + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | message | "deletion failed: author doesn't match" | -# Scenario: delete non-existing user -# Given authorisation -# When make a DELETE request to "/discord/321" -# Then the response status code should be 404 -# And the response should contain: -# | statusCode | 404 | -# | message | "no discord-profile for 321 found" | + Scenario: delete non-existing user + Given authorization with Writing-Scopes + When make a DELETE request to "/discord/321" + Then the response status code should be 404 + And the response should contain: + | statusCode | 404 | + | message | "no discord-profile for 321 found" | -# Scenario: get user with authenticated request -# Given authorisation -# And make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response should contain: -# | documentId | "TYPE:ID" | -# When make a GET request to "/discord/{id}" -# Then the response status code should be 200 -# And the response should contain: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# | updatedOn | "TYPE:DATE" | -# | createdOn | "TYPE:DATE" | + Scenario: get user with authenticated request + Given authorization with Writing-Scopes + And make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response should contain: + | documentId | "TYPE:ID" | + When make a GET request to "/discord/{id}" + Then the response status code should be 200 + And the response should contain: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + | updatedOn | "TYPE:DATE" | + | createdOn | "TYPE:DATE" | -# Scenario: create a user without authorization -# Given make a POST request to "/discord" with: -# | bio | "This is a GitHub Campus Expert" | -# | author | {"platform":"discord","uid":"hubber"} | -# | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | -# Then the response status code should be 401 -# And the response should contain: -# | statusCode | 401 | -# | message | "Unauthorized" | + Scenario: create a user without authorization + Given make a POST request to "/discord" with: + | bio | "This is a GitHub Campus Expert" | + | author | {"platform":"discord","uid":"hubber"} | + | socials | {"discord":"khattakdev","github":"khattakdev","linkedin":"khattakdev","twitter":"khattakdev"} | + Then the response status code should be 401 + And the response should contain: + | statusCode | 401 | + | message | "Unauthorized" | diff --git a/test/features/github.feature b/test/features/github.feature index e7f9019a..f2e1a354 100644 --- a/test/features/github.feature +++ b/test/features/github.feature @@ -1,197 +1,197 @@ -# @github -# Feature: github module +@github +Feature: github module -# Scenario: add a new githubprofile -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# Then the response status code should be 201 -# And the response should contain: -# | documentId | "TYPE:ID" | + Scenario: add a new githubprofile + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + Then the response status code should be 201 + And the response should contain: + | documentId | "TYPE:ID" | -# Scenario: get list of githubprofiles -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a GET request to "/github" -# Then the response status code should be 200 -# And the response in item where field "username" is equal to "eddiehubber" should contain: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | communityStats | {"push":1} | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | -# | updatedOn | "TYPE:DATE" | -# | createdOn | "TYPE:DATE" | + Scenario: get list of githubprofiles + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a GET request to "/github" + Then the response status code should be 200 + And the response in item where field "username" is equal to "eddiehubber" should contain: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | communityStats | {"push":1} | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | + | updatedOn | "TYPE:DATE" | + | createdOn | "TYPE:DATE" | -# Scenario: add an empty githubprofile -# Given authorisation -# And make a POST request to "/github" with: -# | test | "test" | -# Then the response status code should be 400 -# And the response should contain: -# | statusCode | 400 | -# | error | "Bad Request" | -# And the response property "message" has items: -# | username must be a string | -# | username should not be empty | + Scenario: add an empty githubprofile + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | test | "test" | + Then the response status code should be 400 + And the response should contain: + | statusCode | 400 | + | error | "Bad Request" | + And the response property "message" has items: + | username must be a string | + | username should not be empty | -# Scenario: delete a githubprofile -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a DELETE request to "/github/{id}" -# Then the response status code should be 204 + Scenario: delete a githubprofile + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a DELETE request to "/github/{id}" + Then the response status code should be 204 -# Scenario: delete non-existent githubprofile -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# Then make a DELETE request to "/github/66" -# Then the response status code should be 404 -# And the response should contain: -# | statusCode | 404 | -# | message | "no github-profile for 66 found" | + Scenario: delete non-existent githubprofile + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + And the response should contain: + | documentId | "TYPE:ID" | + Then make a DELETE request to "/github/66" + Then the response status code should be 404 + And the response should contain: + | statusCode | 404 | + | message | "no github-profile for 66 found" | -# Scenario: update githubprofile with previously used event -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# Then make a PUT request to "/github/{id}" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# | event | "push" | -# Then the response status code should be 200 -# And the response should contain: -# | documentId | "TYPE:ID" | + Scenario: update githubprofile with previously used event + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + And the response should contain: + | documentId | "TYPE:ID" | + Then make a PUT request to "/github/{id}" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + | event | "push" | + Then the response status code should be 200 + And the response should contain: + | documentId | "TYPE:ID" | -# Scenario: update githubprofile with previously unused event -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# Then make a PUT request to "/github/{id}" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# | event | "label" | -# Then the response status code should be 200 -# And the response should contain: -# | documentId | "TYPE:ID" | + Scenario: update githubprofile with previously unused event + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + And the response should contain: + | documentId | "TYPE:ID" | + Then make a PUT request to "/github/{id}" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + | event | "label" | + Then the response status code should be 200 + And the response should contain: + | documentId | "TYPE:ID" | -# Scenario: get githubprofile with authenticated request -# Given authorisation -# And make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# And the response should contain: -# | documentId | "TYPE:ID" | -# When make a GET request to "/github/{id}" -# Then the response status code should be 200 -# And the response should contain: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | communityStats | {"push":1} | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | -# | updatedOn | "TYPE:DATE" | -# | createdOn | "TYPE:DATE" | + Scenario: get githubprofile with authenticated request + Given authorization with Writing-Scopes + And make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + And the response should contain: + | documentId | "TYPE:ID" | + When make a GET request to "/github/{id}" + Then the response status code should be 200 + And the response should contain: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | communityStats | {"push":1} | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | {"provided": "London","lat": 51.5073219,"long": -0.1276474} | + | updatedOn | "TYPE:DATE" | + | createdOn | "TYPE:DATE" | -# Scenario: create a githubprofile without authentication -# Given make a POST request to "/github" with: -# | username | "eddiehubber" | -# | bio | "I love to code" | -# | avatarUrl | "https://dummy.com/avatar" | -# | followers | 500 | -# | repos | 32 | -# | event | "push" | -# | blog | "https://www.myBlog.com" | -# | organization | "Eddiehub" | -# | location | "London" | -# Then the response status code should be 401 -# And the response should contain: -# | statusCode | 401 | -# | message | "Unauthorized" | + Scenario: create a githubprofile without authentication + Given make a POST request to "/github" with: + | username | "eddiehubber" | + | bio | "I love to code" | + | avatarUrl | "https://dummy.com/avatar" | + | followers | 500 | + | repos | 32 | + | event | "push" | + | blog | "https://www.myBlog.com" | + | organization | "Eddiehub" | + | location | "London" | + Then the response status code should be 401 + And the response should contain: + | statusCode | 401 | + | message | "Unauthorized" | From d0ad64ae472669206bbd1b4dc8eaa4369fda1dea Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Fri, 23 Jul 2021 19:57:18 +0200 Subject: [PATCH 20/40] fix: add missing catch --- src/standup/standup.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/standup/standup.service.ts b/src/standup/standup.service.ts index 8d86db49..ac3853f3 100644 --- a/src/standup/standup.service.ts +++ b/src/standup/standup.service.ts @@ -62,6 +62,12 @@ export class StandupService { deleteStandup(id: string, authorObject: Author, keyspaceName: string) { return this.astraService.get(id, keyspaceName, 'standup').pipe( + catchError(() => { + throw new HttpException( + `no standup for ${id} found`, + HttpStatus.NOT_FOUND, + ); + }), filter((data: Standup) => { if (!data) { throw new HttpException( From ae18fa75fb7c71d05681d7079a66f4d6763ed828 Mon Sep 17 00:00:00 2001 From: Eddie Jaoude Date: Fri, 23 Jul 2021 19:49:45 +0100 Subject: [PATCH 21/40] fix: read/write scopes (#137) --- test/features/calendar.feature | 22 +++++++++++----------- test/features/discord.feature | 18 +++++++++--------- test/features/github.feature | 16 ++++++++-------- test/features/standup.feature | 21 +++++++++++---------- test/step-definitions/requests.ts | 31 +++++++++++++------------------ 5 files changed, 52 insertions(+), 56 deletions(-) diff --git a/test/features/calendar.feature b/test/features/calendar.feature index f614adf5..d9ca54a1 100644 --- a/test/features/calendar.feature +++ b/test/features/calendar.feature @@ -2,7 +2,7 @@ Feature: calendar module Scenario: add a new event - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | @@ -27,7 +27,7 @@ Feature: calendar module | updatedOn | "TYPE:DATE" | Scenario: get list of events - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a GET request to "/calendar" Then the response status code should be 200 And the response should contain: @@ -35,7 +35,7 @@ Feature: calendar module | ongoing | {} | Scenario: add an empty event - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/calendar" with: | test | "test" | Then the response status code should be 400 @@ -55,7 +55,7 @@ Feature: calendar module | endDate should not be empty | Scenario: update an event - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | @@ -91,7 +91,7 @@ Feature: calendar module | updatedOn | "TYPE:DATE" | Scenario: update an event with wrong author - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | @@ -116,7 +116,7 @@ Feature: calendar module | message | "update failed: author doesn't match" | Scenario: update an non-existing event - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a PUT request to "/calendar/321" with: | name | "Livestream YZ" | | description | "undescriptive Description" | @@ -131,7 +131,7 @@ Feature: calendar module | statusCode | 404 | Scenario: delete an event - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | @@ -148,7 +148,7 @@ Feature: calendar module Then the response status code should be 204 Scenario: delete an event with wrong author - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | @@ -166,7 +166,7 @@ Feature: calendar module | message | "deletion failed: author doesn't match" | Scenario: delete non-existing event - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a DELETE request to "/calendar/321" Then the response status code should be 404 And the response should contain: @@ -174,7 +174,7 @@ Feature: calendar module | message | "no event for 321 found" | Scenario: get event with authenticated request - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | @@ -213,7 +213,7 @@ Feature: calendar module | message | "Unauthorized" | Scenario: get sorted ongoing and future events - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/calendar" with: | name | "Livestream XY" | | description | "descriptive Description" | diff --git a/test/features/discord.feature b/test/features/discord.feature index 7af369b6..078c7372 100644 --- a/test/features/discord.feature +++ b/test/features/discord.feature @@ -2,7 +2,7 @@ Feature: discord module Scenario: add a new user - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | @@ -12,7 +12,7 @@ Feature: discord module | documentId | "TYPE:ID" | Scenario: get list of users - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | @@ -29,7 +29,7 @@ Feature: discord module | createdOn | "TYPE:DATE" | Scenario: add an empty user - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | test | "test" | Then the response status code should be 400 @@ -40,7 +40,7 @@ Feature: discord module | author should not be empty | Scenario: update a user - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | @@ -62,7 +62,7 @@ Feature: discord module | createdOn | "TYPE:DATE" | Scenario: update a user with wrong author - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | @@ -79,7 +79,7 @@ Feature: discord module | message | "update failed: author doesn't match" | Scenario: delete a user - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | @@ -91,7 +91,7 @@ Feature: discord module Then the response status code should be 204 Scenario: delete a user with wrong author - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | @@ -105,7 +105,7 @@ Feature: discord module | message | "deletion failed: author doesn't match" | Scenario: delete non-existing user - Given authorization with Writing-Scopes + Given authorization with "writing" permission When make a DELETE request to "/discord/321" Then the response status code should be 404 And the response should contain: @@ -113,7 +113,7 @@ Feature: discord module | message | "no discord-profile for 321 found" | Scenario: get user with authenticated request - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/discord" with: | bio | "This is a GitHub Campus Expert" | | author | {"platform":"discord","uid":"hubber"} | diff --git a/test/features/github.feature b/test/features/github.feature index f2e1a354..bd9cbe27 100644 --- a/test/features/github.feature +++ b/test/features/github.feature @@ -2,7 +2,7 @@ Feature: github module Scenario: add a new githubprofile - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | @@ -18,7 +18,7 @@ Feature: github module | documentId | "TYPE:ID" | Scenario: get list of githubprofiles - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | @@ -47,7 +47,7 @@ Feature: github module | createdOn | "TYPE:DATE" | Scenario: add an empty githubprofile - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | test | "test" | Then the response status code should be 400 @@ -59,7 +59,7 @@ Feature: github module | username should not be empty | Scenario: delete a githubprofile - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | @@ -76,7 +76,7 @@ Feature: github module Then the response status code should be 204 Scenario: delete non-existent githubprofile - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | @@ -96,7 +96,7 @@ Feature: github module | message | "no github-profile for 66 found" | Scenario: update githubprofile with previously used event - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | @@ -124,7 +124,7 @@ Feature: github module | documentId | "TYPE:ID" | Scenario: update githubprofile with previously unused event - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | @@ -152,7 +152,7 @@ Feature: github module | documentId | "TYPE:ID" | Scenario: get githubprofile with authenticated request - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/github" with: | username | "eddiehubber" | | bio | "I love to code" | diff --git a/test/features/standup.feature b/test/features/standup.feature index 446aa7f6..37a1b778 100644 --- a/test/features/standup.feature +++ b/test/features/standup.feature @@ -2,7 +2,7 @@ Feature: Standup module Scenario: add a new standup - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -12,14 +12,15 @@ Feature: Standup module | documentId | "TYPE:ID" | Scenario: search existing standup - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | | todayMessage | "Today I'll do this" | Then the response should contain: | documentId | "TYPE:ID" | - Then make a GET request to "/standup/search?uid=hubber" + Given authorization with "reading" permission + And make a GET request to "/standup/search?uid=hubber" Then the response status code should be 200 And the response in item where field "todayMessage" is equal to "Today I'll do this" should contain: | author | {"platform":"discord","uid":"hubber"} | @@ -28,7 +29,7 @@ Feature: Standup module | createdOn | "TYPE:DATE" | Scenario: search non-existing standup - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -40,7 +41,7 @@ Feature: Standup module And the response should be "{}" Scenario: provide no search context - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -54,7 +55,7 @@ Feature: Standup module | message | "Please provide search context" | Scenario: add an empty standup - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | test | "test" | Then the response status code should be 400 @@ -69,7 +70,7 @@ Feature: Standup module | todayMessage must be a string | Scenario: delete standup - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -82,7 +83,7 @@ Feature: Standup module Then the response status code should be 204 Scenario: delete standup with wrong credentials - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -96,7 +97,7 @@ Feature: Standup module | message | "deletion failed: author doesn't match" | Scenario: delete non-existent standup - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | @@ -110,7 +111,7 @@ Feature: Standup module | message | "no standup for 66 found" | Scenario: get standup with authenticated request - Given authorization with Writing-Scopes + Given authorization with "writing" permission And make a POST request to "/standup" with: | author | {"platform":"discord","uid":"hubber"} | | yesterdayMessage | "Yesterday I did this" | diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index a7d6acc8..a4e635ec 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -4,6 +4,7 @@ import { exec } from 'child_process'; import { BeforeAll, setDefaultTimeout } from 'cucumber'; import { before, binding, given, when } from 'cucumber-tsflow'; import { sign } from 'jsonwebtoken'; +import { Scopes } from 'src/auth/decorators/scopes.decorator'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; import Context from '../support/world'; @@ -53,24 +54,18 @@ export class requests { await this.context.app.init(); } - @given(/authorization with Reading-Scopes/) - public async generateReadToken() { - const token = sign( - { scopes: ['Data.Read'], serverId: 'eddiehub' }, - process.env.SECRET, - ); - this.context.bearerToken = token; - } - - @given(/authorization with Writing-Scopes/) - public async generateWriteToken() { - const token = sign( - { - scopes: ['Data.Read', 'Data.Write'], - keyspace: 'eddiehub', - }, - process.env.SECRET, - ); + @given(/authorization with "([^"]*)" permission/) + public async generateReadToken(scope: string) { + let scopes = []; + switch (scope) { + case 'writing': + scopes = ['Data.Write', 'Data.Read']; + break; + case 'reading': + scopes = ['Data.Read']; + break; + } + const token = sign({ scopes, keyspace: 'eddiehub' }, process.env.SECRET); this.context.bearerToken = token; } From 8e443218c83fd69292c2b81dc425aebe7afbdc0d Mon Sep 17 00:00:00 2001 From: Eddie Jaoude Date: Sat, 24 Jul 2021 03:25:09 +0100 Subject: [PATCH 22/40] fix: auth tests (#165) --- test/features/auth.feature | 38 +++++++++++++++++++++++++++++++ test/step-definitions/requests.ts | 17 +++++++++++++- test/support/regexes.ts | 5 ++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 test/features/auth.feature diff --git a/test/features/auth.feature b/test/features/auth.feature new file mode 100644 index 00000000..08f7279f --- /dev/null +++ b/test/features/auth.feature @@ -0,0 +1,38 @@ +@auth +Feature: auth module + + Scenario: fail to create a token with no authorisation + When make a POST request to "/auth" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 401 + + Scenario: create a token successfully + Given authorisation + When make a POST request to "/auth" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 201 + And the response should contain: + | clientId | "TYPE:ID" | + | keyspace | "TYPE:STRING" | + | scopes | ["Data.Read"] | + | accessToken | "TYPE:JWT" | + | expiresIn | "TYPE:NUMBER" | + + Scenario: restart the app and use existing token + Given authorisation + When make a POST request to "/auth" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 201 + And the response should contain: + | clientId | "TYPE:ID" | + | keyspace | "TYPE:STRING" | + | scopes | ["Data.Read"] | + | accessToken | "TYPE:JWT" | + | expiresIn | "TYPE:NUMBER" | + When add bearer token to the header + # And restart app + And make a GET request to "/calendar" + Then the response status code should be 200 diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index a4e635ec..4f9c7b39 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -4,7 +4,6 @@ import { exec } from 'child_process'; import { BeforeAll, setDefaultTimeout } from 'cucumber'; import { before, binding, given, when } from 'cucumber-tsflow'; import { sign } from 'jsonwebtoken'; -import { Scopes } from 'src/auth/decorators/scopes.decorator'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; import Context from '../support/world'; @@ -54,6 +53,17 @@ export class requests { await this.context.app.init(); } + @when(/restart app/) + public async restartApp(): Promise { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + this.context.app = moduleFixture.createNestApplication(); + this.context.app.useGlobalPipes(new ValidationPipe({ transform: true })); + await this.context.app.init(); + } + @given(/authorization with "([^"]*)" permission/) public async generateReadToken(scope: string) { let scopes = []; @@ -74,6 +84,11 @@ export class requests { this.context.token = 'abc'; } + @when(/add bearer token to the header/) + public async addBearerToken() { + this.context.bearerToken = this.context.response.body.accessToken; + } + @given(/make a GET request to "([^"]*)"/) public async getRequest(url: string) { url = this.prepareURL(url); diff --git a/test/support/regexes.ts b/test/support/regexes.ts index d5457884..357bd751 100644 --- a/test/support/regexes.ts +++ b/test/support/regexes.ts @@ -4,6 +4,9 @@ const numberRegex = new RegExp(/[0-9]+/); const dateRegex = new RegExp( /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/, ); +const jwt = new RegExp( + /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, +); const versionRegex = new RegExp( /Currently running version: \d{1,3}.\d{1,3}.\d{1,3}/, ); @@ -23,6 +26,8 @@ export function getRegex(type: string): RegExp { return dateRegex; case 'TYPE:VERSION': return versionRegex; + case 'TYPE:JWT': + return jwt; default: break; } From 4ee26c9d0a7f3e100288635cc1c5dfabbd968b5c Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 25 Jul 2021 16:01:18 +0200 Subject: [PATCH 23/40] feat: store tokens in db on register --- src/auth/auth.module.ts | 9 ++++++++- src/auth/auth.service.ts | 27 ++++++++++++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index b54d3474..d9473dd6 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -6,6 +6,7 @@ import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './jwt.strategy'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AstraService } from '../astra/astra.service'; @Module({ imports: [ @@ -17,7 +18,13 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; inject: [ConfigService], }), ], - providers: [TokenStrategy, ValidationService, AuthService, JwtStrategy], + providers: [ + TokenStrategy, + ValidationService, + AuthService, + JwtStrategy, + AstraService, + ], exports: [ValidationService], controllers: [AuthController], }) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index fa7a3beb..f689f7fc 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -3,29 +3,38 @@ import { TokenPayload } from './interfaces/token-payload.interface'; import { JwtService } from '@nestjs/jwt'; import { v4 as uuidv4 } from 'uuid'; import { AuthDTO } from './dto/auth.dto'; +import { AstraService } from '../astra/astra.service'; @Injectable() export class AuthService { //TODO move configCollection to database => own ConfigDataModule private configCollection: { [id: string]: { knownClients: string[] } } = {}; - constructor(private readonly jwtService: JwtService) {} + constructor( + private readonly jwtService: JwtService, + private readonly astraService: AstraService, + ) {} - public register(body: AuthDTO) { + public async register(body: AuthDTO) { const clientId = uuidv4(); const { serverId, scopes } = body; + let tokens: String[] = []; + try { + tokens = await this.astraService + .get('Benjamin', serverId, 'tokens') + .toPromise(); + } catch (e) { + tokens = []; + } const payload: TokenPayload = { clientId, keyspace: serverId, scopes, }; - if (!this.configCollection[serverId]) { - this.configCollection[serverId] = { knownClients: [] }; - } - this.configCollection[serverId].knownClients = [ - ...this.configCollection[serverId].knownClients, - clientId, - ]; + tokens = [...tokens, clientId]; + await this.astraService + .replace('Benjamin', tokens, serverId, 'tokens') + .toPromise(); //TODO token-expiry const signedToken = this.jwtService.sign(payload, { expiresIn: '1y' }); const decoded: any = this.jwtService.decode(signedToken); From a1efc99fa6565b44b9de977d8699cfa7777d3021 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 25 Jul 2021 16:25:41 +0200 Subject: [PATCH 24/40] feat: validate and delete to database --- src/auth/auth.service.ts | 47 +++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index f689f7fc..5d16b2f4 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -7,8 +7,6 @@ import { AstraService } from '../astra/astra.service'; @Injectable() export class AuthService { - //TODO move configCollection to database => own ConfigDataModule - private configCollection: { [id: string]: { knownClients: string[] } } = {}; constructor( private readonly jwtService: JwtService, private readonly astraService: AstraService, @@ -20,7 +18,7 @@ export class AuthService { let tokens: String[] = []; try { tokens = await this.astraService - .get('Benjamin', serverId, 'tokens') + .get('tokens', serverId, 'tokens') .toPromise(); } catch (e) { tokens = []; @@ -33,7 +31,7 @@ export class AuthService { }; tokens = [...tokens, clientId]; await this.astraService - .replace('Benjamin', tokens, serverId, 'tokens') + .replace('tokens', tokens, serverId, 'tokens') .toPromise(); //TODO token-expiry const signedToken = this.jwtService.sign(payload, { expiresIn: '1y' }); @@ -42,33 +40,48 @@ export class AuthService { return { ...payload, accessToken: signedToken, expiresIn }; } - public validateClient(payload: TokenPayload): boolean { + public async validateClient(payload: TokenPayload): Promise { const { keyspace, clientId } = payload; - if ( - this.configCollection[keyspace] && - this.configCollection[keyspace].knownClients.includes(clientId) - ) { + let tokens: String[]; + try { + tokens = await this.astraService + .get('tokens', keyspace, 'tokens') + .toPromise(); + } catch { + return false; + } + if (tokens && tokens.includes(clientId)) { return true; } return false; } - public removeClient(token: string) { + public async removeClient(token: string): Promise { + let tokens: String[] = null; if (!token) throw new HttpException('Please provide token', HttpStatus.BAD_REQUEST); const decoded = this.jwtService.decode(token) as TokenPayload; try { - this.configCollection[ - decoded.keyspace - ].knownClients = this.configCollection[ - decoded.keyspace - ].knownClients.filter((client) => client !== decoded.clientId); - console.log(this.configCollection); + tokens = await this.astraService + .get('tokens', decoded.keyspace, 'tokens') + .toPromise(); + } catch (e) { return; + } + + tokens = tokens.filter((clientID) => clientID !== decoded.clientId); + try { + await this.astraService + .replace('tokens', tokens, decoded.keyspace, 'tokens') + .toPromise(); } catch (e) { - throw new HttpException('Invalid client id', HttpStatus.BAD_REQUEST); + throw new HttpException( + 'Deleting Token went wrong', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } + return; } } From b1754a284564398b7201b599bd5236e16ead74ac Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 25 Jul 2021 16:35:41 +0200 Subject: [PATCH 25/40] style: update tests + lint --- src/app.controller.spec.ts | 5 +++++ src/auth/auth.controller.spec.ts | 16 ++++++++++++++-- src/auth/auth.service.spec.ts | 16 ++++++++++++++-- src/auth/auth.service.ts | 12 ++++++------ src/discord/discord.controller.ts | 24 +++++++++--------------- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index f5620234..3b408df8 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,7 +1,9 @@ +import { AstraModule } from '@cahllagerfeld/nestjs-astra'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AstraConfigService } from './astra/astra-config.service'; import { AuthModule } from './auth/auth.module'; describe('AppController', () => { @@ -14,6 +16,9 @@ describe('AppController', () => { ConfigModule.forRoot({ isGlobal: true, }), + AstraModule.forRootAsync({ + useClass: AstraConfigService, + }), ], controllers: [AppController], providers: [AppService], diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 9a025668..f188a839 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,5 +1,9 @@ +import { AstraModule } from '@cahllagerfeld/nestjs-astra'; +import { ConfigModule } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; +import { AstraConfigService } from '../astra/astra-config.service'; +import { AstraService } from '../astra/astra.service'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @@ -8,9 +12,17 @@ describe('AuthController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [JwtModule.register({ secret: 'Test' })], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + JwtModule.register({ secret: 'Test' }), + AstraModule.forRootAsync({ + useClass: AstraConfigService, + }), + ], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, AstraService], }).compile(); controller = module.get(AuthController); diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index b3788213..25d43af3 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -1,5 +1,9 @@ +import { AstraModule } from '@cahllagerfeld/nestjs-astra'; +import { ConfigModule } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; +import { AstraConfigService } from '../astra/astra-config.service'; +import { AstraService } from '../astra/astra.service'; import { AuthService } from './auth.service'; describe('AuthService', () => { @@ -7,8 +11,16 @@ describe('AuthService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [JwtModule.register({ secret: 'Test' })], - providers: [AuthService], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + JwtModule.register({ secret: 'Test' }), + AstraModule.forRootAsync({ + useClass: AstraConfigService, + }), + ], + providers: [AuthService, AstraService], }).compile(); service = module.get(AuthService); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5d16b2f4..ca00457b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -15,10 +15,10 @@ export class AuthService { public async register(body: AuthDTO) { const clientId = uuidv4(); const { serverId, scopes } = body; - let tokens: String[] = []; + let tokens: string[] = []; try { tokens = await this.astraService - .get('tokens', serverId, 'tokens') + .get('tokens', serverId, 'tokens') .toPromise(); } catch (e) { tokens = []; @@ -42,10 +42,10 @@ export class AuthService { public async validateClient(payload: TokenPayload): Promise { const { keyspace, clientId } = payload; - let tokens: String[]; + let tokens: string[]; try { tokens = await this.astraService - .get('tokens', keyspace, 'tokens') + .get('tokens', keyspace, 'tokens') .toPromise(); } catch { return false; @@ -57,7 +57,7 @@ export class AuthService { } public async removeClient(token: string): Promise { - let tokens: String[] = null; + let tokens: string[] = null; if (!token) throw new HttpException('Please provide token', HttpStatus.BAD_REQUEST); @@ -65,7 +65,7 @@ export class AuthService { try { tokens = await this.astraService - .get('tokens', decoded.keyspace, 'tokens') + .get('tokens', decoded.keyspace, 'tokens') .toPromise(); } catch (e) { return; diff --git a/src/discord/discord.controller.ts b/src/discord/discord.controller.ts index 7c5bd259..b9c15bb6 100644 --- a/src/discord/discord.controller.ts +++ b/src/discord/discord.controller.ts @@ -1,32 +1,26 @@ import { + Body, Controller, + Delete, Get, + HttpCode, + Param, Post, - Body, Put, - Param, - Delete, UseGuards, - HttpCode, } from '@nestjs/common'; -import { - ApiBearerAuth, - ApiHeader, - ApiSecurity, - ApiTags, -} from '@nestjs/swagger'; -import { TokenGuard } from '../auth/token.strategy'; +import { ApiBearerAuth, ApiHeader, ApiTags } from '@nestjs/swagger'; import { Author, AuthorObject } from '../auth/author-headers'; -import { DiscordService } from './discord.service'; -import { DiscordDTO } from './dto/discord.dto'; +import { Scopes } from '../auth/decorators/scopes.decorator'; import { User } from '../auth/decorators/user.decorator'; +import { ScopesGuard } from '../auth/guards/scopes.guard'; import { ScopesDictionary, TokenPayload, } from '../auth/interfaces/token-payload.interface'; import { JWTGuard } from '../auth/jwt.strategy'; -import { ScopesGuard } from '../auth/guards/scopes.guard'; -import { Scopes } from '../auth/decorators/scopes.decorator'; +import { DiscordService } from './discord.service'; +import { DiscordDTO } from './dto/discord.dto'; @ApiTags('Discord') @Controller('discord') export class DiscordController { From 6cefb7a5600e973b1aa093df14b8feea395a56eb Mon Sep 17 00:00:00 2001 From: Eddie Jaoude Date: Mon, 26 Jul 2021 00:17:34 +0100 Subject: [PATCH 26/40] fix: automated tests for tokens (#165) --- test/features/auth.feature | 58 +++++++++++++++++++++++++++++-- test/step-definitions/requests.ts | 7 +++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/test/features/auth.feature b/test/features/auth.feature index 08f7279f..666f21ae 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -1,12 +1,34 @@ @auth Feature: auth module + # Scenario: get list of all tokens in invalid keyspace + # Given authorisation + # When make a GET request to "/auth/invalid-keyspace" + # Then the response status code should be 200 + # And the response should be "[]" + + # Scenario: get list of all tokens in my keyspace + # Given authorisation + # And make a POST request to "/auth/tokens" with: + # | serverId | "eddiehub" | + # | scopes | ["Data.Read"] | + # When make a GET request to "/auth/tokens/eddiehub" + # Then the response status code should be 200 + # And the response should be "[{\"serverId\":\"eddiehub\", \"scopes\":\"[\"Data.Read\"]\"}]" + Scenario: fail to create a token with no authorisation When make a POST request to "/auth" with: | serverId | "eddiehub" | | scopes | ["Data.Read"] | Then the response status code should be 401 + Scenario: fail to create a token with invalid authorisation + Given invalid authorisation + When make a POST request to "/auth" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 401 + Scenario: create a token successfully Given authorisation When make a POST request to "/auth" with: @@ -20,7 +42,7 @@ Feature: auth module | accessToken | "TYPE:JWT" | | expiresIn | "TYPE:NUMBER" | - Scenario: restart the app and use existing token + Scenario: use the token Given authorisation When make a POST request to "/auth" with: | serverId | "eddiehub" | @@ -33,6 +55,38 @@ Feature: auth module | accessToken | "TYPE:JWT" | | expiresIn | "TYPE:NUMBER" | When add bearer token to the header - # And restart app And make a GET request to "/calendar" Then the response status code should be 200 + + # Scenario: validate token + # Given authorisation + # When make a POST request to "/auth" with: + # | serverId | "eddiehub" | + # | scopes | ["Data.Read"] | + # Then the response status code should be 201 + # When make a POST request to "/auth/tokens/validate" with: + # | keyspace | "eddiehub" | + # | clientId | "{clientId}" | + # Then the response status code should be 200 + + # Scenario: invalid validation of token + # Given authorisation + # When make a POST request to "/auth" with: + # | serverId | "eddiehub" | + # | scopes | ["Data.Read"] | + # Then the response status code should be 201 + # When make a POST request to "/auth/validate" with: + # | keyspace | "eddiehub" | + # | clientId | "xxxxxxxx" | + # Then the response status code should be 400 + + Scenario: delete token + Given authorisation + When make a POST request to "/auth" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 201 + When make a DELETE request to "/auth/{clientId}" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 204 diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 4f9c7b39..43158507 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -79,11 +79,16 @@ export class requests { this.context.bearerToken = token; } - @given(/authorisation/) + @given(/^authorisation$/) public async authorisation() { this.context.token = 'abc'; } + @given(/^invalid authorisation$/) + public async invalidAuthorisation() { + this.context.token = 'xxx'; + } + @when(/add bearer token to the header/) public async addBearerToken() { this.context.bearerToken = this.context.response.body.accessToken; From 14dc6b588d381996c3350f1fdce5aebe76c90394 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 31 Jul 2021 10:45:24 +0200 Subject: [PATCH 27/40] feat: token to database --- src/auth/auth.service.ts | 73 +++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index ca00457b..121b0d27 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -15,24 +15,23 @@ export class AuthService { public async register(body: AuthDTO) { const clientId = uuidv4(); const { serverId, scopes } = body; - let tokens: string[] = []; - try { - tokens = await this.astraService - .get('tokens', serverId, 'tokens') - .toPromise(); - } catch (e) { - tokens = []; - } const payload: TokenPayload = { clientId, keyspace: serverId, scopes, }; - tokens = [...tokens, clientId]; - await this.astraService - .replace('tokens', tokens, serverId, 'tokens') - .toPromise(); + try { + await this.astraService + .create({ clientId }, serverId, 'tokens', clientId) + .toPromise(); + } catch (error) { + throw new HttpException( + "Token coundn't be created in the database", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + //TODO token-expiry const signedToken = this.jwtService.sign(payload, { expiresIn: '1y' }); const decoded: any = this.jwtService.decode(signedToken); @@ -42,46 +41,42 @@ export class AuthService { public async validateClient(payload: TokenPayload): Promise { const { keyspace, clientId } = payload; - let tokens: string[]; + let token: string = null; try { - tokens = await this.astraService - .get('tokens', keyspace, 'tokens') + token = await this.astraService + .get(clientId, keyspace, 'tokens') .toPromise(); } catch { return false; } - if (tokens && tokens.includes(clientId)) { - return true; + if (!token) { + return false; } - return false; + return true; } - public async removeClient(token: string): Promise { - let tokens: string[] = null; + public async removeClient(token: string) { + let deleteSuccess = false; + let clientId: string; + let keyspace: string; + if (!token) throw new HttpException('Please provide token', HttpStatus.BAD_REQUEST); - - const decoded = this.jwtService.decode(token) as TokenPayload; - - try { - tokens = await this.astraService - .get('tokens', decoded.keyspace, 'tokens') - .toPromise(); - } catch (e) { - return; - } - - tokens = tokens.filter((clientID) => clientID !== decoded.clientId); try { - await this.astraService - .replace('tokens', tokens, decoded.keyspace, 'tokens') - .toPromise(); - } catch (e) { + ({ clientId, keyspace } = this.jwtService.verify(token)); + } catch (error) { throw new HttpException( - 'Deleting Token went wrong', - HttpStatus.INTERNAL_SERVER_ERROR, + "Token couldn't be verified", + HttpStatus.BAD_REQUEST, ); } - return; + try { + await this.astraService.delete(clientId, keyspace, 'tokens').toPromise(); + deleteSuccess = true; + } catch (error) { + throw new Error("Token couldn't be deleted"); + } + + return deleteSuccess; } } From e3db00eaaeb9097182ec50a688ee481de608ae84 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 31 Jul 2021 10:49:04 +0200 Subject: [PATCH 28/40] test: disable delete token test temporarily --- test/features/auth.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/test/features/auth.feature b/test/features/auth.feature index 666f21ae..1a5cf936 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -80,6 +80,7 @@ Feature: auth module # | clientId | "xxxxxxxx" | # Then the response status code should be 400 + # TODO: Fix Delete Token Test Scenario: delete token Given authorisation When make a POST request to "/auth" with: From 4e082455af63cdb7275fb413ac6376b2d159e4d0 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 31 Jul 2021 10:51:23 +0200 Subject: [PATCH 29/40] test: disable delete token test temporarily --- test/features/auth.feature | 62 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/features/auth.feature b/test/features/auth.feature index 1a5cf936..5cba29f6 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -58,36 +58,36 @@ Feature: auth module And make a GET request to "/calendar" Then the response status code should be 200 - # Scenario: validate token - # Given authorisation - # When make a POST request to "/auth" with: - # | serverId | "eddiehub" | - # | scopes | ["Data.Read"] | - # Then the response status code should be 201 - # When make a POST request to "/auth/tokens/validate" with: - # | keyspace | "eddiehub" | - # | clientId | "{clientId}" | - # Then the response status code should be 200 +# Scenario: validate token +# Given authorisation +# When make a POST request to "/auth" with: +# | serverId | "eddiehub" | +# | scopes | ["Data.Read"] | +# Then the response status code should be 201 +# When make a POST request to "/auth/tokens/validate" with: +# | keyspace | "eddiehub" | +# | clientId | "{clientId}" | +# Then the response status code should be 200 - # Scenario: invalid validation of token - # Given authorisation - # When make a POST request to "/auth" with: - # | serverId | "eddiehub" | - # | scopes | ["Data.Read"] | - # Then the response status code should be 201 - # When make a POST request to "/auth/validate" with: - # | keyspace | "eddiehub" | - # | clientId | "xxxxxxxx" | - # Then the response status code should be 400 +# Scenario: invalid validation of token +# Given authorisation +# When make a POST request to "/auth" with: +# | serverId | "eddiehub" | +# | scopes | ["Data.Read"] | +# Then the response status code should be 201 +# When make a POST request to "/auth/validate" with: +# | keyspace | "eddiehub" | +# | clientId | "xxxxxxxx" | +# Then the response status code should be 400 - # TODO: Fix Delete Token Test - Scenario: delete token - Given authorisation - When make a POST request to "/auth" with: - | serverId | "eddiehub" | - | scopes | ["Data.Read"] | - Then the response status code should be 201 - When make a DELETE request to "/auth/{clientId}" with: - | serverId | "eddiehub" | - | scopes | ["Data.Read"] | - Then the response status code should be 204 +# TODO: Fix Delete Token Test +# Scenario: delete token +# Given authorisation +# When make a POST request to "/auth" with: +# | serverId | "eddiehub" | +# | scopes | ["Data.Read"] | +# Then the response status code should be 201 +# When make a DELETE request to "/auth/{clientId}" with: +# | serverId | "eddiehub" | +# | scopes | ["Data.Read"] | +# Then the response status code should be 204 From 405fabe4d8311bc00e4b7f9eaeefa3e509d1a65d Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sat, 31 Jul 2021 21:45:21 +0200 Subject: [PATCH 30/40] feat: get all clients endpoint --- src/auth/auth.controller.ts | 14 +++++++------- src/auth/auth.service.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 1048e2e9..9a68859c 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -18,13 +18,13 @@ import { TokenGuard } from './token.strategy'; export class AuthController { constructor(private readonly authService: AuthService) {} - // @Get() - // @UseGuards(TokenGuard) - // @ApiSecurity('token') - // @ApiQuery({ name: 'keyspace', required: true }) - // getTokens(@Query('keyspace') keyspace: string) { - // return this.authService.getClientIds(keyspace); - // } + @Get() + @ApiSecurity('token') + @ApiSecurity('token') + @ApiQuery({ name: 'keyspace', required: true }) + getTokens(@Query('keyspace') keyspace: string) { + return this.authService.getClientIds(keyspace); + } @Post() @UseGuards(TokenGuard) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 121b0d27..00b52618 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -79,4 +79,17 @@ export class AuthService { return deleteSuccess; } + + public async getClientIds(keyspace: string) { + let clients; + try { + clients = await this.astraService.find(keyspace, 'tokens').toPromise(); + } catch (error) { + throw new HttpException( + 'Clients couldnt be retrieved', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + return { clients: Object.keys(clients) }; + } } From e85cb775b56a36d9e5f6333859fdbf918e0b2ca4 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 1 Aug 2021 08:12:12 +0200 Subject: [PATCH 31/40] fix: set Guard --- src/auth/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 9a68859c..3ddc7a20 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -19,7 +19,7 @@ export class AuthController { constructor(private readonly authService: AuthService) {} @Get() - @ApiSecurity('token') + @UseGuards(TokenGuard) @ApiSecurity('token') @ApiQuery({ name: 'keyspace', required: true }) getTokens(@Query('keyspace') keyspace: string) { From 9f53daf3f5ea76e2c251275e7e259d4bd521a564 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 1 Aug 2021 14:52:39 +0200 Subject: [PATCH 32/40] test: fix delete token test --- src/auth/auth.controller.ts | 13 +++--- test/features/auth.feature | 71 +++++++++++++++---------------- test/step-definitions/requests.ts | 5 ++- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 3ddc7a20..db382136 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -4,11 +4,12 @@ import { Delete, Get, HttpCode, + Param, Post, Query, UseGuards, } from '@nestjs/common'; -import { ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { ApiParam, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { AuthDTO } from './dto/auth.dto'; import { TokenGuard } from './token.strategy'; @@ -18,22 +19,22 @@ import { TokenGuard } from './token.strategy'; export class AuthController { constructor(private readonly authService: AuthService) {} - @Get() + @Get('token/:keyspace') @UseGuards(TokenGuard) @ApiSecurity('token') - @ApiQuery({ name: 'keyspace', required: true }) - getTokens(@Query('keyspace') keyspace: string) { + @ApiParam({ name: 'keyspace', required: true }) + getTokens(@Param('keyspace') keyspace: string) { return this.authService.getClientIds(keyspace); } - @Post() + @Post('token') @UseGuards(TokenGuard) @ApiSecurity('token') register(@Body() body: AuthDTO) { return this.authService.register(body); } - @Delete() + @Delete('token') @UseGuards(TokenGuard) @ApiSecurity('token') @ApiQuery({ diff --git a/test/features/auth.feature b/test/features/auth.feature index 5cba29f6..ae454de5 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -9,7 +9,7 @@ Feature: auth module # Scenario: get list of all tokens in my keyspace # Given authorisation - # And make a POST request to "/auth/tokens" with: + # And make a POST request to "/auth" with: # | serverId | "eddiehub" | # | scopes | ["Data.Read"] | # When make a GET request to "/auth/tokens/eddiehub" @@ -17,21 +17,21 @@ Feature: auth module # And the response should be "[{\"serverId\":\"eddiehub\", \"scopes\":\"[\"Data.Read\"]\"}]" Scenario: fail to create a token with no authorisation - When make a POST request to "/auth" with: + When make a POST request to "/auth/token" with: | serverId | "eddiehub" | | scopes | ["Data.Read"] | Then the response status code should be 401 Scenario: fail to create a token with invalid authorisation Given invalid authorisation - When make a POST request to "/auth" with: + When make a POST request to "/auth/token" with: | serverId | "eddiehub" | | scopes | ["Data.Read"] | Then the response status code should be 401 Scenario: create a token successfully Given authorisation - When make a POST request to "/auth" with: + When make a POST request to "/auth/token" with: | serverId | "eddiehub" | | scopes | ["Data.Read"] | Then the response status code should be 201 @@ -44,7 +44,7 @@ Feature: auth module Scenario: use the token Given authorisation - When make a POST request to "/auth" with: + When make a POST request to "/auth/token" with: | serverId | "eddiehub" | | scopes | ["Data.Read"] | Then the response status code should be 201 @@ -58,36 +58,35 @@ Feature: auth module And make a GET request to "/calendar" Then the response status code should be 200 -# Scenario: validate token -# Given authorisation -# When make a POST request to "/auth" with: -# | serverId | "eddiehub" | -# | scopes | ["Data.Read"] | -# Then the response status code should be 201 -# When make a POST request to "/auth/tokens/validate" with: -# | keyspace | "eddiehub" | -# | clientId | "{clientId}" | -# Then the response status code should be 200 + # Scenario: validate token + # Given authorisation + # When make a POST request to "/auth" with: + # | serverId | "eddiehub" | + # | scopes | ["Data.Read"] | + # Then the response status code should be 201 + # When make a POST request to "/auth/tokens/validate" with: + # | keyspace | "eddiehub" | + # | clientId | "{clientId}" | + # Then the response status code should be 200 + + # Scenario: invalid validation of token + # Given authorisation + # When make a POST request to "/auth" with: + # | serverId | "eddiehub" | + # | scopes | ["Data.Read"] | + # Then the response status code should be 201 + # When make a POST request to "/auth/validate" with: + # | keyspace | "eddiehub" | + # | clientId | "xxxxxxxx" | + # Then the response status code should be 400 -# Scenario: invalid validation of token -# Given authorisation -# When make a POST request to "/auth" with: -# | serverId | "eddiehub" | -# | scopes | ["Data.Read"] | -# Then the response status code should be 201 -# When make a POST request to "/auth/validate" with: -# | keyspace | "eddiehub" | -# | clientId | "xxxxxxxx" | -# Then the response status code should be 400 -# TODO: Fix Delete Token Test -# Scenario: delete token -# Given authorisation -# When make a POST request to "/auth" with: -# | serverId | "eddiehub" | -# | scopes | ["Data.Read"] | -# Then the response status code should be 201 -# When make a DELETE request to "/auth/{clientId}" with: -# | serverId | "eddiehub" | -# | scopes | ["Data.Read"] | -# Then the response status code should be 204 + Scenario: delete token + Given authorisation + When make a POST request to "/auth/token" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 201 + Then add bearer token to the header + When make a DELETE request to "/auth/token?token={bearer}" + Then the response status code should be 204 diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 16895cae..ac33d200 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -36,9 +36,12 @@ export class requests { constructor(protected context: Context) {} private prepareURL(url: string): string { - if (/\/{id}/.test(url)) { + if (/{id}/.test(url)) { url = url.replace(/{id}/, this.context.documentId); } + if (/{bearer}/.test(url)) { + url = url.replace(/{bearer}/, this.context.bearerToken); + } return url; } From 78c863b14fb5586b6ef731a2ad1def2bd147930c Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 1 Aug 2021 15:02:46 +0200 Subject: [PATCH 33/40] feat: token validation endpoint --- src/auth/auth.controller.ts | 15 +++++++++++++-- src/auth/auth.service.ts | 9 +++++++++ src/auth/dto/auth.dto.ts | 7 +++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index db382136..4d7d9e28 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -9,9 +9,15 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { ApiParam, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { + ApiBody, + ApiParam, + ApiQuery, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; import { AuthService } from './auth.service'; -import { AuthDTO } from './dto/auth.dto'; +import { AuthDTO, TokenValidationDTO } from './dto/auth.dto'; import { TokenGuard } from './token.strategy'; @ApiTags('Auth') @@ -46,4 +52,9 @@ export class AuthController { deleteClient(@Query() query) { return this.authService.removeClient(query.token); } + + @Post('validate') + validateToken(@Body() body: TokenValidationDTO) { + return this.authService.validateToken(body.token); + } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 00b52618..8d4d32f4 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -92,4 +92,13 @@ export class AuthService { } return { clients: Object.keys(clients) }; } + + public validateToken(token: string) { + try { + this.jwtService.verify(token); + return { valid: true }; + } catch (error) { + return { valid: false }; + } + } } diff --git a/src/auth/dto/auth.dto.ts b/src/auth/dto/auth.dto.ts index 41f98373..a404b57c 100644 --- a/src/auth/dto/auth.dto.ts +++ b/src/auth/dto/auth.dto.ts @@ -20,3 +20,10 @@ export class AuthDTO { @ApiProperty({ required: true }) serverId: string; } + +export class TokenValidationDTO { + @IsNotEmpty() + @IsString() + @ApiProperty({ required: true }) + token: string; +} From 5c2c3234a6d94ccf4c621341afb064c9c4b027d5 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 1 Aug 2021 15:24:03 +0200 Subject: [PATCH 34/40] test: add tests for token validation --- src/auth/auth.controller.ts | 14 ++++------ src/auth/auth.service.ts | 7 ++--- test/features/auth.feature | 44 +++++++++++++++++-------------- test/step-definitions/requests.ts | 8 +++++- 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 4d7d9e28..ef85ba04 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -7,15 +7,11 @@ import { Param, Post, Query, + Res, UseGuards, } from '@nestjs/common'; -import { - ApiBody, - ApiParam, - ApiQuery, - ApiSecurity, - ApiTags, -} from '@nestjs/swagger'; +import { ApiParam, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; import { AuthService } from './auth.service'; import { AuthDTO, TokenValidationDTO } from './dto/auth.dto'; import { TokenGuard } from './token.strategy'; @@ -54,7 +50,7 @@ export class AuthController { } @Post('validate') - validateToken(@Body() body: TokenValidationDTO) { - return this.authService.validateToken(body.token); + validateToken(@Body() body: TokenValidationDTO, @Res() response: Response) { + this.authService.validateToken(body.token, response); } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 8d4d32f4..f931aa0b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -4,6 +4,7 @@ import { JwtService } from '@nestjs/jwt'; import { v4 as uuidv4 } from 'uuid'; import { AuthDTO } from './dto/auth.dto'; import { AstraService } from '../astra/astra.service'; +import { Response } from 'express'; @Injectable() export class AuthService { @@ -93,12 +94,12 @@ export class AuthService { return { clients: Object.keys(clients) }; } - public validateToken(token: string) { + public validateToken(token: string, response: Response) { try { this.jwtService.verify(token); - return { valid: true }; + response.status(HttpStatus.OK).json({ valid: true }); } catch (error) { - return { valid: false }; + response.status(HttpStatus.BAD_REQUEST).json({ valid: false }); } } } diff --git a/test/features/auth.feature b/test/features/auth.feature index ae454de5..3c5f8e7a 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -58,27 +58,31 @@ Feature: auth module And make a GET request to "/calendar" Then the response status code should be 200 - # Scenario: validate token - # Given authorisation - # When make a POST request to "/auth" with: - # | serverId | "eddiehub" | - # | scopes | ["Data.Read"] | - # Then the response status code should be 201 - # When make a POST request to "/auth/tokens/validate" with: - # | keyspace | "eddiehub" | - # | clientId | "{clientId}" | - # Then the response status code should be 200 + Scenario: validate token + Given authorisation + When make a POST request to "/auth/token" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 201 + Then add bearer token to the header + When make a POST request to "/auth/validate" with: + | token | "{BEARER}" | + Then the response status code should be 200 + And the response should contain: + | valid | true | - # Scenario: invalid validation of token - # Given authorisation - # When make a POST request to "/auth" with: - # | serverId | "eddiehub" | - # | scopes | ["Data.Read"] | - # Then the response status code should be 201 - # When make a POST request to "/auth/validate" with: - # | keyspace | "eddiehub" | - # | clientId | "xxxxxxxx" | - # Then the response status code should be 400 + Scenario: invalid validation of token + Given authorisation + When make a POST request to "/auth/token" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + Then the response status code should be 201 + Then add bearer token to the header + When make a POST request to "/auth/validate" with: + | token | "XXXXX" | + Then the response status code should be 400 + And the response should contain: + | valid | false | Scenario: delete token diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index ac33d200..8472bc94 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -117,6 +117,12 @@ export class requests { url = this.prepareURL(url); const post = request(this.context.app.getHttpServer()).post(url); + let body = this.context.tableToObject(table); + Object.keys(body).forEach((key) => { + if (/{BEARER}/.test(body[key])) { + body[key] = this.context.bearerToken; + } + }); if (this.context.token) { post.set('Client-Token', this.context.token); @@ -125,7 +131,7 @@ export class requests { if (this.context.bearerToken) { post.set('Authorization', `Bearer ${this.context.bearerToken}`); } - this.context.response = await post.send(this.context.tableToObject(table)); + this.context.response = await post.send(body); } @when(/clear the bearer token/) From 261bcf342ebdb0a8f8d752b5a6529bf857e9678b Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Sun, 1 Aug 2021 15:25:56 +0200 Subject: [PATCH 35/40] style: const instead of let --- test/step-definitions/requests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 8472bc94..9a9bc39d 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -117,7 +117,7 @@ export class requests { url = this.prepareURL(url); const post = request(this.context.app.getHttpServer()).post(url); - let body = this.context.tableToObject(table); + const body = this.context.tableToObject(table); Object.keys(body).forEach((key) => { if (/{BEARER}/.test(body[key])) { body[key] = this.context.bearerToken; From 3232fe382ef8d4cf56f1fae89d88d3c1433521e6 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Mon, 2 Aug 2021 22:14:14 +0200 Subject: [PATCH 36/40] test: update auth tests --- src/auth/auth.service.ts | 5 +---- test/features/auth.feature | 27 ++++++++++++++------------- test/step-definitions/requests.ts | 4 ++++ test/step-definitions/responses.ts | 9 +++++++++ test/support/regexes.ts | 5 +++++ 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index f931aa0b..0c58a52e 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -86,10 +86,7 @@ export class AuthService { try { clients = await this.astraService.find(keyspace, 'tokens').toPromise(); } catch (error) { - throw new HttpException( - 'Clients couldnt be retrieved', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + return { clients: [] }; } return { clients: Object.keys(clients) }; } diff --git a/test/features/auth.feature b/test/features/auth.feature index 3c5f8e7a..e783c129 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -1,20 +1,21 @@ @auth Feature: auth module - # Scenario: get list of all tokens in invalid keyspace - # Given authorisation - # When make a GET request to "/auth/invalid-keyspace" - # Then the response status code should be 200 - # And the response should be "[]" + Scenario: get list of all tokens in invalid keyspace + Given authorisation + When make a GET request to "/auth/token/invalid-keyspace" + Then the response status code should be 200 + And the response should contain: + | clients | [] | - # Scenario: get list of all tokens in my keyspace - # Given authorisation - # And make a POST request to "/auth" with: - # | serverId | "eddiehub" | - # | scopes | ["Data.Read"] | - # When make a GET request to "/auth/tokens/eddiehub" - # Then the response status code should be 200 - # And the response should be "[{\"serverId\":\"eddiehub\", \"scopes\":\"[\"Data.Read\"]\"}]" + Scenario: get list of all tokens in my keyspace + Given authorisation + And make a POST request to "/auth/token" with: + | serverId | "eddiehub" | + | scopes | ["Data.Read"] | + When make a GET request to "/auth/token/eddiehub" + Then the response status code should be 200 + And the response property "clients" should be of type "TYPE:UUID" Scenario: fail to create a token with no authorisation When make a POST request to "/auth/token" with: diff --git a/test/step-definitions/requests.ts b/test/step-definitions/requests.ts index 9a9bc39d..b5e33757 100644 --- a/test/step-definitions/requests.ts +++ b/test/step-definitions/requests.ts @@ -109,6 +109,10 @@ export class requests { if (this.context.bearerToken) { get.set('Authorization', `Bearer ${this.context.bearerToken}`); } + + if (this.context.token) { + get.set('Client-Token', this.context.token); + } this.context.response = await get.send(); } diff --git a/test/step-definitions/responses.ts b/test/step-definitions/responses.ts index c4209027..d25ee559 100644 --- a/test/step-definitions/responses.ts +++ b/test/step-definitions/responses.ts @@ -195,4 +195,13 @@ export class responses { } expect(responseString).to.equal(text); } + + @then(/the response property "([^"]*)" should be of type "([^"]*)"/) + public validateArrayProperty(property: string, type: string) { + const responseProperty = JSON.parse(this.context.response.text)[property]; + const regex = getRegex(type); + if (responseProperty instanceof Array) { + responseProperty.every((element) => expect(element).to.match(regex)); + } + } } diff --git a/test/support/regexes.ts b/test/support/regexes.ts index 357bd751..d2c9875c 100644 --- a/test/support/regexes.ts +++ b/test/support/regexes.ts @@ -10,6 +10,9 @@ const jwt = new RegExp( const versionRegex = new RegExp( /Currently running version: \d{1,3}.\d{1,3}.\d{1,3}/, ); +const v4 = new RegExp( + /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, +); export function getRegex(type: string): RegExp { switch (type) { @@ -28,6 +31,8 @@ export function getRegex(type: string): RegExp { return versionRegex; case 'TYPE:JWT': return jwt; + case 'TYPE:UUID': + return v4; default: break; } From 1a2e05889783b3af34a2546ed0d9bc607d557a6e Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Mon, 2 Aug 2021 22:42:25 +0200 Subject: [PATCH 37/40] feat: add guard to validation endpoint --- src/auth/auth.controller.ts | 2 ++ src/auth/token.strategy.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index ef85ba04..111926fc 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -50,6 +50,8 @@ export class AuthController { } @Post('validate') + @UseGuards(TokenGuard) + @ApiSecurity('token') validateToken(@Body() body: TokenValidationDTO, @Res() response: Response) { this.authService.validateToken(body.token, response); } diff --git a/src/auth/token.strategy.ts b/src/auth/token.strategy.ts index 1ce4b8af..92182577 100644 --- a/src/auth/token.strategy.ts +++ b/src/auth/token.strategy.ts @@ -13,6 +13,7 @@ export class TokenStrategy extends PassportStrategy( super({ tokenHeader: 'Client-Token', tokenQuery: 'Client-Token', + tokenField: 'Client-Token', passReqToCallback: true, }); } From a46c5bb73b0daa041490ce9c3fab0914b7c83e88 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Tue, 3 Aug 2021 10:07:17 +0200 Subject: [PATCH 38/40] refactor: response handling in controller --- src/auth/auth.controller.ts | 7 ++++++- src/auth/auth.service.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 111926fc..247ad3c6 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -4,6 +4,7 @@ import { Delete, Get, HttpCode, + HttpStatus, Param, Post, Query, @@ -53,6 +54,10 @@ export class AuthController { @UseGuards(TokenGuard) @ApiSecurity('token') validateToken(@Body() body: TokenValidationDTO, @Res() response: Response) { - this.authService.validateToken(body.token, response); + const valid = this.authService.validateToken(body.token, response); + if (!valid) { + response.status(HttpStatus.BAD_REQUEST).json({ valid }); + } + response.status(HttpStatus.OK).json({ valid }); } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 0c58a52e..328dc11e 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -94,9 +94,9 @@ export class AuthService { public validateToken(token: string, response: Response) { try { this.jwtService.verify(token); - response.status(HttpStatus.OK).json({ valid: true }); + return true; } catch (error) { - response.status(HttpStatus.BAD_REQUEST).json({ valid: false }); + return false; } } } From fc167bc773964a17b8d4d70e9d753e9c036c43a7 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Tue, 3 Aug 2021 10:16:43 +0200 Subject: [PATCH 39/40] fix: add missing return --- src/auth/auth.controller.ts | 3 ++- src/auth/auth.service.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 247ad3c6..30bd473a 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -54,9 +54,10 @@ export class AuthController { @UseGuards(TokenGuard) @ApiSecurity('token') validateToken(@Body() body: TokenValidationDTO, @Res() response: Response) { - const valid = this.authService.validateToken(body.token, response); + const valid = this.authService.validateToken(body.token); if (!valid) { response.status(HttpStatus.BAD_REQUEST).json({ valid }); + return; } response.status(HttpStatus.OK).json({ valid }); } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 328dc11e..02332525 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -91,7 +91,7 @@ export class AuthService { return { clients: Object.keys(clients) }; } - public validateToken(token: string, response: Response) { + public validateToken(token: string) { try { this.jwtService.verify(token); return true; From 7ca72aac6c97d03555a6bf5e669f06220dccbc05 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:00:35 +0200 Subject: [PATCH 40/40] test: rename step-def --- test/features/auth.feature | 2 +- test/step-definitions/responses.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/features/auth.feature b/test/features/auth.feature index e783c129..0d03a90c 100644 --- a/test/features/auth.feature +++ b/test/features/auth.feature @@ -15,7 +15,7 @@ Feature: auth module | scopes | ["Data.Read"] | When make a GET request to "/auth/token/eddiehub" Then the response status code should be 200 - And the response property "clients" should be of type "TYPE:UUID" + And the response property "clients" should have a collection of type "TYPE:UUID" Scenario: fail to create a token with no authorisation When make a POST request to "/auth/token" with: diff --git a/test/step-definitions/responses.ts b/test/step-definitions/responses.ts index d25ee559..ed2eb2d1 100644 --- a/test/step-definitions/responses.ts +++ b/test/step-definitions/responses.ts @@ -196,7 +196,9 @@ export class responses { expect(responseString).to.equal(text); } - @then(/the response property "([^"]*)" should be of type "([^"]*)"/) + @then( + /the response property "([^"]*)" should have a collection of type "([^"]*)"/, + ) public validateArrayProperty(property: string, type: string) { const responseProperty = JSON.parse(this.context.response.text)[property]; const regex = getRegex(type);