diff --git a/funpets-server/main/function.json b/funpets-server/main/function.json index 1e5c89e..46922ba 100644 --- a/funpets-server/main/function.json +++ b/funpets-server/main/function.json @@ -1,7 +1,7 @@ { "bindings": [ { - "authLevel": "anonymous", + "authLevel": "function", "type": "httpTrigger", "direction": "in", "name": "req", diff --git a/funpets-server/package-lock.json b/funpets-server/package-lock.json index 582bc31..1d36cee 100644 --- a/funpets-server/package-lock.json +++ b/funpets-server/package-lock.json @@ -2024,6 +2024,11 @@ } } }, + "@types/validator": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.3.tgz", + "integrity": "sha512-GKF2VnEkMmEeEGvoo03ocrP9ySMuX1ypKazIYMlsjfslfBMhOAtC5dmEWKdJioW4lJN7MZRS88kalTsVClyQ9w==" + }, "@types/webpack": { "version": "4.41.5", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.5.tgz", @@ -3454,6 +3459,11 @@ "safe-buffer": "^5.0.1" } }, + "class-transformer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.2.3.tgz", + "integrity": "sha512-qsP+0xoavpOlJHuYsQJsN58HXSl8Jvveo+T37rEvCEeRfMWoytAyR0Ua/YsFgpM6AZYZ/og2PJwArwzJl1aXtQ==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -3477,6 +3487,23 @@ } } }, + "class-validator": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.11.0.tgz", + "integrity": "sha512-niAmmSPFku9xsnpYYrddy8NZRrCX3yyoZ/rgPKOilE5BG0Ma1eVCIxpR4X0LasL/6BzbYzsutG+mSbAXlh4zNw==", + "requires": { + "@types/validator": "10.11.3", + "google-libphonenumber": "^3.1.6", + "validator": "12.0.0" + }, + "dependencies": { + "validator": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-12.0.0.tgz", + "integrity": "sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng==" + } + } + }, "cli-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz", @@ -5815,6 +5842,11 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "google-libphonenumber": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.7.tgz", + "integrity": "sha512-8Es+gIaGoBddq/tqegG52iuzHOV+VU+de2mvkIOVlDkg/bnJlNdVZcof6iMPChy9Dte+si7BJeDaaueBtFoV6Q==" + }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", diff --git a/funpets-server/package.json b/funpets-server/package.json index 4e97762..1e07f84 100644 --- a/funpets-server/package.json +++ b/funpets-server/package.json @@ -24,12 +24,14 @@ "@azure/functions": "^1.0.3", "@nestjs/azure-database": "^1.0.0", "@nestjs/azure-func-http": "^0.4.1", - "@nestjs/azure-storage": "^2.1.0", + "@nestjs/azure-storage": "^2.1.1", "@nestjs/common": "^6.10.14", "@nestjs/core": "^6.10.14", "@nestjs/platform-express": "^6.10.14", "@nestjs/typeorm": "^6.3.4", "mongodb": "^3.5.5", + "class-transformer": "^0.2.3", + "class-validator": "^0.11.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.5.4", diff --git a/funpets-server/src/main.azure.ts b/funpets-server/src/main.azure.ts index 435edcf..5dd63c7 100644 --- a/funpets-server/src/main.azure.ts +++ b/funpets-server/src/main.azure.ts @@ -1,11 +1,12 @@ -import { INestApplication } from '@nestjs/common'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; export async function createApp(): Promise { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); - + app.useGlobalPipes(new ValidationPipe()); + await app.init(); return app; } diff --git a/funpets-server/src/main.ts b/funpets-server/src/main.ts index 1374fc5..7c0be4f 100644 --- a/funpets-server/src/main.ts +++ b/funpets-server/src/main.ts @@ -1,9 +1,12 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); + app.useGlobalPipes(new ValidationPipe()); + await app.listen(3000); } bootstrap(); diff --git a/funpets-server/src/stories/stories.controller.spec.ts b/funpets-server/src/stories/stories.controller.spec.ts index d826ade..3ae1c79 100644 --- a/funpets-server/src/stories/stories.controller.spec.ts +++ b/funpets-server/src/stories/stories.controller.spec.ts @@ -1,5 +1,20 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StoriesController } from './stories.controller'; +import { AzureStorageService } from '@nestjs/azure-storage'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Story } from './story.entity'; + +jest.mock('@nestjs/azure-storage', () => ({ + // Use Jest automatic mock generation + ...jest.genMockFromModule('@nestjs/azure-storage'), + + // Interceptor mock needs to be done manually + AzureStorageFileInterceptor: jest.fn(() => ({ + intercept: (context, next) => next.handle(), + })), +})); + +const mockRepository = jest.genMockFromModule('typeorm').MongoRepository; describe('Stories Controller', () => { let controller: StoriesController; @@ -7,6 +22,10 @@ describe('Stories Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [StoriesController], + providers: [ + AzureStorageService, + { provide: getRepositoryToken(Story), useValue: mockRepository }, + ], }).compile(); controller = module.get(StoriesController); diff --git a/funpets-server/src/stories/stories.controller.ts b/funpets-server/src/stories/stories.controller.ts index 9c05e60..f683b10 100644 --- a/funpets-server/src/stories/stories.controller.ts +++ b/funpets-server/src/stories/stories.controller.ts @@ -9,6 +9,7 @@ import { Body, UploadedFile, UnsupportedMediaTypeException, + BadRequestException, } from '@nestjs/common'; import { AzureStorageFileInterceptor, @@ -17,7 +18,9 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { MongoRepository } from 'typeorm'; import { ObjectID } from 'mongodb'; +import { Validator } from 'class-validator'; import { Story } from './story.entity'; +import { StoryDto } from './story.dto'; // Some cat facts, courtesy of https://catfact.ninja const funFacts = [ @@ -89,7 +92,7 @@ export class StoriesController { @UseInterceptors(AzureStorageFileInterceptor('file', fileUploadOptions)) async createStory( @Body() - data: Partial, + data: StoryDto, @UploadedFile() file: UploadedFileMetadata, ): Promise { @@ -100,6 +103,10 @@ export class StoriesController { if (file) { story.imageUrl = file.storageUrl || null; } + const validator = new Validator(); + if (validator.isEmpty(story.description) && validator.isEmpty(story.imageUrl)) { + throw new BadRequestException('Either description or image file must be provided'); + } return await this.storiesRepository.save(story); } } diff --git a/funpets-server/src/stories/story.dto.ts b/funpets-server/src/stories/story.dto.ts new file mode 100644 index 0000000..151f22c --- /dev/null +++ b/funpets-server/src/stories/story.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty, IsOptional, IsDate, MaxLength } from 'class-validator'; + +export class StoryDto { + @IsNotEmpty() + animal: string; + + @IsOptional() + @MaxLength(240) + description: string; + + @IsOptional() + @IsDate() + createdAt: Date; +}