From 4b7378a492074741af08012e075a8cb4e1071ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AF=BB?= Date: Fri, 22 Oct 2021 21:47:53 +0800 Subject: [PATCH] dev/snippet (#130) --- .eslintrc.js | 1 + .npmrc | 1 + README.md | 3 +- package.json | 3 +- pnpm-lock.yaml | 11 ++ src/app.module.ts | 10 +- src/common/guard/auth.guard.ts | 8 ++ .../middlewares/attach-auth.middleware.ts | 23 ++++ src/mock/user.mock.ts | 21 +++ src/modules/page/page.model.ts | 8 -- src/modules/snippet/snippet.controller.ts | 95 ++++++++++++++ src/modules/snippet/snippet.dto.ts | 0 src/modules/snippet/snippet.model.ts | 73 +++++++++++ src/modules/snippet/snippet.module.ts | 13 ++ src/modules/snippet/snippet.readme.md | 44 +++++++ src/modules/snippet/snippet.service.ts | 99 ++++++++++++++ src/processors/database/database.module.ts | 2 + src/shared/model/base.model.ts | 1 + src/utils/index.util.ts | 30 +---- test/helper/register-app.helper.ts | 22 ++++ test/helper/utils.helper.ts | 5 + .../snippet/snippet.controller.e2e-spec.ts | 121 ++++++++++++++++++ .../user.controller.e2e-spec.ts} | 0 .../user.controller.spec.ts} | 0 .../user.service.spec.ts} | 0 tsconfig.build.json | 4 - tsconfig.json | 4 + 27 files changed, 555 insertions(+), 47 deletions(-) create mode 100644 src/common/middlewares/attach-auth.middleware.ts create mode 100644 src/mock/user.mock.ts create mode 100644 src/modules/snippet/snippet.controller.ts create mode 100644 src/modules/snippet/snippet.dto.ts create mode 100644 src/modules/snippet/snippet.model.ts create mode 100644 src/modules/snippet/snippet.module.ts create mode 100644 src/modules/snippet/snippet.readme.md create mode 100644 src/modules/snippet/snippet.service.ts create mode 100644 test/helper/register-app.helper.ts create mode 100644 test/helper/utils.helper.ts create mode 100644 test/src/modules/snippet/snippet.controller.e2e-spec.ts rename test/src/modules/{users/users.controller.e2e-spec.ts => user/user.controller.e2e-spec.ts} (100%) rename test/src/modules/{users/users.controller.spec.ts => user/user.controller.spec.ts} (100%) rename test/src/modules/{users/users.service.spec.ts => user/user.service.spec.ts} (100%) diff --git a/.eslintrc.js b/.eslintrc.js index f34b4030..4de4c227 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,5 +2,6 @@ module.exports = { extends: ['@innei-util/eslint-config-ts'], rules: { 'no-empty': 'warn', + 'no-fallthrough': 'error', }, } diff --git a/.npmrc b/.npmrc index 63126b92..5bde9a45 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ public-hoist-pattern[]=*fastify* public-hoist-pattern[]=mongodb +public-hoist-pattern[]=*eslint* diff --git a/README.md b/README.md index 802261f5..831c9092 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ docker-compose up -d ├── app.module.ts # 主程序根模块,负责各业务模块的聚合 ├── app.resolver.ts # 主程序根 GraphQL Resolver ├── common # 存放中间件 -│ ├── adapt # Fastify 适配器的配置 +│ ├── adapters # Fastify 适配器的配置 │ ├── decorator # 业务装饰器 │ ├── exceptions # 自定义异常 │ ├── filters # 异常处理器 @@ -105,6 +105,7 @@ docker-compose up -d │ ├── recently │ ├── say │ ├── search +| ├── snippet │ ├── sitemap │ ├── tool │ └── user diff --git a/package.json b/package.json index 1b615660..daa7c09a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "build": "nest build", "bundle": "rimraf out && yarn run build && cd dist/src && npx ncc build main.js -o ../../out -m", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "cross-env NODE_ENV=development nest start -w", + "start": "cross-env NODE_ENV=development nest start -w --path tsconfig.json", "start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", "start:prod": "cross-env NODE_ENV=production node dist/main", @@ -145,6 +145,7 @@ "ioredis": "*", "jest": "27.3.1", "lint-staged": "11.2.3", + "mockingoose": "2.15.2", "prettier": "2.4.1", "rimraf": "3.0.2", "run-script-webpack-plugin": "0.0.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77a5d32b..9aa474fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,7 @@ specifiers: lodash: '*' marked: 3.0.7 mkdirp: '*' + mockingoose: 2.15.2 mongoose: '*' mongoose-lean-id: 0.2.0 mongoose-lean-virtuals: 0.9.0 @@ -193,6 +194,7 @@ devDependencies: ioredis: 4.28.0 jest: 27.3.1_ts-node@10.3.1 lint-staged: 11.2.3 + mockingoose: 2.15.2_mongoose@5.13.8 prettier: 2.4.1 rimraf: 3.0.2 run-script-webpack-plugin: 0.0.11 @@ -6213,6 +6215,15 @@ packages: hasBin: true dev: false + /mockingoose/2.15.2_mongoose@5.13.8: + resolution: {integrity: sha512-50mtbAk29Go5hdhzqTmjmE67Z/cB0yPz45u2jrHoGm4nkYnnBq224viWgyKwnxzWw8birnqn98viM2cRBTnJvw==} + engines: {node: '>=6.4.0'} + peerDependencies: + mongoose: '>=4.9.10' + dependencies: + mongoose: 5.13.8 + dev: true + /moment-timezone/0.5.33: resolution: {integrity: sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==} dependencies: diff --git a/src/app.module.ts b/src/app.module.ts index 54bcdd6e..10dccdab 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor' import { CountingInterceptor } from './common/interceptors/counting.interceptor' import { JSONSerializeInterceptor } from './common/interceptors/json-serialize.interceptor' import { ResponseInterceptor } from './common/interceptors/response.interceptor' +import { AttachHeaderTokenMiddleware } from './common/middlewares/attach-auth.middleware' import { DATA_DIR, LOGGER_DIR, @@ -40,6 +41,7 @@ import { RecentlyModule } from './modules/recently/recently.module' import { SayModule } from './modules/say/say.module' import { SearchModule } from './modules/search/search.module' import { SitemapModule } from './modules/sitemap/sitemap.module' +import { SnippetModule } from './modules/snippet/snippet.module' import { ToolModule } from './modules/tool/tool.module' import { UserModule } from './modules/user/user.module' import { CacheModule } from './processors/cache/cache.module' @@ -103,6 +105,7 @@ mkdirs() GatewayModule, HelperModule, LoggerModule, + SnippetModule, ], controllers: [AppController], providers: [ @@ -141,11 +144,6 @@ mkdirs() }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { - // FIXME: nestjs 8 middleware bug - // consumer - // .apply(AnalyzeMiddleware) - // .forRoutes({ path: '(.*?)', method: RequestMethod.GET }) - // .apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware) - // .forRoutes({ path: '(.*?)', method: RequestMethod.ALL }) + consumer.apply(AttachHeaderTokenMiddleware).forRoutes('*') } } diff --git a/src/common/guard/auth.guard.ts b/src/common/guard/auth.guard.ts index e5a56762..8e524258 100644 --- a/src/common/guard/auth.guard.ts +++ b/src/common/guard/auth.guard.ts @@ -1,5 +1,7 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' import { AuthGuard as _AuthGuard } from '@nestjs/passport' +import { mockUser1 } from '~/mock/user.mock' +import { isTest } from '~/utils/index.util' import { getNestExecutionContextRequest } from '~/utils/nest.util' /** @@ -15,6 +17,12 @@ export class JWTAuthGuard extends _AuthGuard('jwt') implements CanActivate { return true } + /// for e2e-test mock user + if (isTest) { + request.user = { ...mockUser1 } + return true + } + return super.canActivate(context) } diff --git a/src/common/middlewares/attach-auth.middleware.ts b/src/common/middlewares/attach-auth.middleware.ts new file mode 100644 index 00000000..34bf0a9d --- /dev/null +++ b/src/common/middlewares/attach-auth.middleware.ts @@ -0,0 +1,23 @@ +/** + * 把 URL Search 上的 `token` 附加到 Header Authorization 上 + * @author Innei + */ + +import { Injectable, NestMiddleware } from '@nestjs/common' +import { IncomingMessage, ServerResponse } from 'http' +import { parseRelativeUrl } from '~/utils/ip.util' + +@Injectable() +export class AttachHeaderTokenMiddleware implements NestMiddleware { + async use(req: IncomingMessage, res: ServerResponse, next: () => void) { + // @ts-ignore + const url = req.originalUrl?.replace(/^\/api(\/v\d)?/, '') + const parser = parseRelativeUrl(url) + + if (parser.searchParams.get('token')) { + req.headers.authorization = parser.searchParams.get('token') + } + + next() + } +} diff --git a/src/mock/user.mock.ts b/src/mock/user.mock.ts new file mode 100644 index 00000000..a663f473 --- /dev/null +++ b/src/mock/user.mock.ts @@ -0,0 +1,21 @@ +import { UserModel } from '~/modules/user/user.model' + +export const mockUser1: UserModel = { + id: '1', + name: 'John Doe', + mail: 'example@ee.com', + password: '**********', + authCode: '*****', + username: 'johndoe', + created: new Date('2021/1/1 10:00:11'), +} + +export const mockUser2: UserModel = { + id: '2', + name: 'Shawn Carter', + mail: 'example@ee.com', + password: '**********', + authCode: '*****', + username: 'shawn', + created: new Date('2020/10/10 19:22:22'), +} diff --git a/src/modules/page/page.model.ts b/src/modules/page/page.model.ts index cc98d4aa..0667a735 100644 --- a/src/modules/page/page.model.ts +++ b/src/modules/page/page.model.ts @@ -1,5 +1,4 @@ import { PartialType } from '@nestjs/mapped-types' -import { ApiProperty } from '@nestjs/swagger' import { modelOptions, prop } from '@typegoose/typegoose' import { Transform } from 'class-transformer' import { @@ -24,20 +23,17 @@ export enum PageType { }, }) export class PageModel extends WriteBaseModel { - @ApiProperty({ description: 'Slug', required: true }) @prop({ trim: 1, index: true, required: true, unique: true }) @IsString() @IsNotEmpty() slug!: string - @ApiProperty({ description: 'SubTitle', required: false }) @prop({ trim: true }) @IsString() @IsOptional() @IsNilOrString() subtitle?: string | null - @ApiProperty({ description: 'Order', required: false }) @prop({ default: 1 }) @IsInt() @Min(0) @@ -45,10 +41,6 @@ export class PageModel extends WriteBaseModel { @Transform(({ value }) => parseInt(value)) order!: number - @ApiProperty({ - enum: PageType, - required: false, - }) @prop({ default: 'md' }) @IsEnum(PageType) @IsOptional() diff --git a/src/modules/snippet/snippet.controller.ts b/src/modules/snippet/snippet.controller.ts new file mode 100644 index 00000000..53fcda72 --- /dev/null +++ b/src/modules/snippet/snippet.controller.ts @@ -0,0 +1,95 @@ +import { + Body, + Controller, + Delete, + ForbiddenException, + Get, + Param, + Post, + Put, + Query, +} from '@nestjs/common' +import { Auth } from '~/common/decorator/auth.decorator' +import { ApiName } from '~/common/decorator/openapi.decorator' +import { IsMaster } from '~/common/decorator/role.decorator' +import { MongoIdDto } from '~/shared/dto/id.dto' +import { PagerDto } from '~/shared/dto/pager.dto' +import { transformDataToPaginate } from '~/utils/transfrom.util' +import { SnippetModel } from './snippet.model' +import { SnippetService } from './snippet.service' + +@ApiName +@Controller('snippets') +export class SnippetController { + constructor(private readonly snippetService: SnippetService) {} + + @Get('/') + @Auth() + async getList(@Query() query: PagerDto) { + const { page, size, select = '' } = query + + return transformDataToPaginate( + await this.snippetService.model.paginate( + {}, + { page, limit: size, select }, + ), + ) + } + + @Post('/') + @Auth() + async create(@Body() body: SnippetModel) { + return await this.snippetService.create(body) + } + + @Get('/:id') + async getSnippetById( + @Param() param: MongoIdDto, + @IsMaster() isMaster: boolean, + ) { + const { id } = param + const snippet = await this.snippetService.getSnippetById(id) + if (snippet.private && !isMaster) { + throw new ForbiddenException('snippet is private') + } + return snippet + } + + @Get('/:reference/:name') + async getSnippetByName( + @Param('name') name: string, + @Param('reference') reference: string, + @IsMaster() isMaster: boolean, + ) { + if (typeof name !== 'string') { + throw new ForbiddenException('name should be string') + } + + if (typeof reference !== 'string') { + throw new ForbiddenException('reference should be string') + } + + const snippet = await this.snippetService.getSnippetByName(name, reference) + + if (snippet.private && !isMaster) { + throw new ForbiddenException('snippet is private') + } + return snippet + } + + @Put('/:id') + @Auth() + async update(@Param() param: MongoIdDto, @Body() body: SnippetModel) { + const { id } = param + + await this.snippetService.update(id, body) + return await this.snippetService.getSnippetById(id) + } + + @Delete('/:id') + @Auth() + async delete(@Param() param: MongoIdDto) { + const { id } = param + await this.snippetService.delete(id) + } +} diff --git a/src/modules/snippet/snippet.dto.ts b/src/modules/snippet/snippet.dto.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/modules/snippet/snippet.model.ts b/src/modules/snippet/snippet.model.ts new file mode 100644 index 00000000..10f32ba4 --- /dev/null +++ b/src/modules/snippet/snippet.model.ts @@ -0,0 +1,73 @@ +import { index, modelOptions, prop } from '@typegoose/typegoose' +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + Matches, + MaxLength, +} from 'class-validator' +import { BaseModel } from '~/shared/model/base.model' + +export enum SnippetType { + JSON = 'json', + Function = 'function', + Text = 'text', +} + +@modelOptions({ + options: { + customName: 'snippet', + }, + schemaOptions: { + timestamps: { + createdAt: 'created', + updatedAt: 'updated', + }, + }, +}) +@index({ name: 1, reference: 1 }) +@index({ type: 1 }) +export class SnippetModel extends BaseModel { + @prop({ default: SnippetType['JSON'] }) + @IsEnum(SnippetType) + type: SnippetType + + @prop({ default: false }) + @IsBoolean() + @IsOptional() + private: boolean + + @prop({ require: true }) + @IsString() + @IsNotEmpty() + raw: string + + @prop({ require: true, trim: true }) + @IsNotEmpty() + @Matches(/^[a-zA-Z0-9_]{1,30}$/, { + message: 'name 只能使用英文字母和数字下划线且不超过 30 个字符', + }) + name: string + + // 适用于 + @prop({ default: 'root' }) + @IsString() + @IsOptional() + @IsNotEmpty() + reference: string + + // 注释 + @prop({}) + @IsString() + @IsOptional() + comment?: string + + // 类型注释 + @prop({ maxlength: 20 }) + @MaxLength(20) + @IsString() + @IsOptional() + metatype?: string +} diff --git a/src/modules/snippet/snippet.module.ts b/src/modules/snippet/snippet.module.ts new file mode 100644 index 00000000..a865c3b6 --- /dev/null +++ b/src/modules/snippet/snippet.module.ts @@ -0,0 +1,13 @@ +/** + * 数据配置区块 + */ +import { Module } from '@nestjs/common' +import { SnippetController } from './snippet.controller' +import { SnippetService } from './snippet.service' + +@Module({ + controllers: [SnippetController], + exports: [SnippetService], + providers: [SnippetService], +}) +export class SnippetModule {} diff --git a/src/modules/snippet/snippet.readme.md b/src/modules/snippet/snippet.readme.md new file mode 100644 index 00000000..d5ffe646 --- /dev/null +++ b/src/modules/snippet/snippet.readme.md @@ -0,0 +1,44 @@ + +# 数据区块 (Snippet) + +拟定于存储一些动态扩展配置. 一期实现存储 JSON 和 plain text 的区块 + +二期实现低配云函数 + +拟定: + +JSON: + +``` +input: json `{"foo":"bar"}` + + +output: + +{ + metatype: null, + type: 'json', + data: { + foo: 'bar' + }, + raw: '{"foo":"bar"}', + id: xx +} +``` + +serverless function + +``` +input: `module.exports = ctx => { return 'foo' }` + +output: + +{ + raw: `module.exports = ctx => { return 'foo' }`, + data: 'foo', + .... + +} + + +``` \ No newline at end of file diff --git a/src/modules/snippet/snippet.service.ts b/src/modules/snippet/snippet.service.ts new file mode 100644 index 00000000..23db8c4a --- /dev/null +++ b/src/modules/snippet/snippet.service.ts @@ -0,0 +1,99 @@ +import { BadRequestException, Injectable } from '@nestjs/common' +import { InjectModel } from 'nestjs-typegoose' +import { SnippetModel, SnippetType } from './snippet.model' + +@Injectable() +export class SnippetService { + constructor( + @InjectModel(SnippetModel) + private readonly snippetModel: MongooseModel, + ) {} + + get model() { + return this.snippetModel + } + + async create(model: SnippetModel) { + const isExist = await this.model.countDocuments({ + name: model.name, + reference: model.reference || 'root', + }) + + if (isExist) { + throw new BadRequestException('snippet is exist') + } + // 验证正确类型 + await this.validateType(model) + return await this.model.create({ ...model, created: new Date() }) + } + + async update(id: string, model: SnippetModel) { + await this.validateType(model) + + await this.model.updateOne( + { + _id: id, + }, + model, + ) + } + + async delete(id: string) { + await this.model.deleteOne({ _id: id }) + } + + private async validateType(model: SnippetModel) { + switch (model.type) { + case SnippetType.JSON: { + const isValidJSON = JSON.parse(model.raw) + if (!isValidJSON) { + throw new BadRequestException('content is not valid json') + } + break + } + case SnippetType.Function: + // TODO + throw new BadRequestException( + 'Serverless functions are not currently supported', + ) + + case SnippetType.Text: + default: { + break + } + } + } + + async getSnippetById(id: string) { + const doc = await this.model.findById(id).lean() + return this.attachSnippet(doc) + } + + /** + * + * @param name + * @param reference 引用类型, 可以理解为 type + * @returns + */ + async getSnippetByName(name: string, reference: string) { + const doc = await this.model.findOne({ name, reference }).lean() + return this.attachSnippet(doc) + } + + async attachSnippet(model: SnippetModel) { + switch (model.type) { + case SnippetType.JSON: { + Reflect.set(model, 'data', JSON.parse(model.raw)) + break + } + case SnippetType.Text: { + break + } + } + + return model + } + + // TODO serverless function + // async runSnippet(model: SnippetModel) {} +} diff --git a/src/processors/database/database.module.ts b/src/processors/database/database.module.ts index 7e06bfe5..5686c8a4 100644 --- a/src/processors/database/database.module.ts +++ b/src/processors/database/database.module.ts @@ -10,6 +10,7 @@ import { PageModel } from '~/modules/page/page.model' import { ProjectModel } from '~/modules/project/project.model' import { RecentlyModel } from '~/modules/recently/recently.model' import { SayModel } from '~/modules/say/say.model' +import { SnippetModel } from '~/modules/snippet/snippet.model' import { CategoryModel } from '../../modules/category/category.model' import { PostModel } from '../../modules/post/post.model' import { UserModel } from '../../modules/user/user.model' @@ -27,6 +28,7 @@ const models = TypegooseModule.forFeature([ ProjectModel, RecentlyModel, SayModel, + SnippetModel, UserModel, ]) @Module({ diff --git a/src/shared/model/base.model.ts b/src/shared/model/base.model.ts index d8afe596..d5ce26ca 100644 --- a/src/shared/model/base.model.ts +++ b/src/shared/model/base.model.ts @@ -25,6 +25,7 @@ export class BaseModel { @Field(() => Date) created?: Date @Field(() => ID) + @ApiHideProperty() id?: string static get protectedKeys() { diff --git a/src/utils/index.util.ts b/src/utils/index.util.ts index 45af0be1..639c7eac 100644 --- a/src/utils/index.util.ts +++ b/src/utils/index.util.ts @@ -3,8 +3,11 @@ import { isObject } from 'lodash' export * from './ip.util' export const isDev = process.env.NODE_ENV == 'development' +export const isTest = !!process.env.TEST + export const md5 = (text: string) => require('crypto').createHash('md5').update(text).digest('hex') + export function getAvatar(mail: string) { if (!mail) { return '' @@ -20,33 +23,6 @@ export function hasChinese(str: string) { return escape(str).indexOf('%u') < 0 ? false : true } -export const escapeShell = function (cmd: string) { - return '"' + cmd.replace(/(["\s'$`\\])/g, '\\$1') + '"' -} - -export function arrDifference(a1: string[], a2: string[]) { - const a = [], - diff = [] - - for (let i = 0; i < a1.length; i++) { - a[a1[i]] = true - } - - for (let i = 0; i < a2.length; i++) { - if (a[a2[i]]) { - delete a[a2[i]] - } else { - a[a2[i]] = true - } - } - - for (const k in a) { - diff.push(k) - } - - return diff -} - export function deleteKeys( target: T, keys: (keyof T)[], diff --git a/test/helper/register-app.helper.ts b/test/helper/register-app.helper.ts new file mode 100644 index 00000000..c4ec1926 --- /dev/null +++ b/test/helper/register-app.helper.ts @@ -0,0 +1,22 @@ +import { ValidationPipe } from '@nestjs/common' +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import { TestingModule } from '@nestjs/testing' +import { fastifyApp } from '~/common/adapters/fastify.adapter' + +export const setupApp = async (module: TestingModule) => { + const app = module.createNestApplication(fastifyApp) + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + errorHttpStatusCode: 422, + forbidUnknownValues: true, + enableDebugMessages: isDev, + stopAtFirstError: true, + }), + ) + + await app.init() + await app.getHttpAdapter().getInstance().ready() + return app +} diff --git a/test/helper/utils.helper.ts b/test/helper/utils.helper.ts new file mode 100644 index 00000000..6d9241f0 --- /dev/null +++ b/test/helper/utils.helper.ts @@ -0,0 +1,5 @@ +export const firstOfMap = (map: Map) => [...map.entries()]?.[0] +export const firstKeyOfMap = (map: Map) => + [...map.entries()]?.[0][0] +export const firstValueOfMap = (map: Map) => + [...map.entries()]?.[0][1] diff --git a/test/src/modules/snippet/snippet.controller.e2e-spec.ts b/test/src/modules/snippet/snippet.controller.e2e-spec.ts new file mode 100644 index 00000000..12c07ade --- /dev/null +++ b/test/src/modules/snippet/snippet.controller.e2e-spec.ts @@ -0,0 +1,121 @@ +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import { Test } from '@nestjs/testing' +import { getModelForClass } from '@typegoose/typegoose' +import { getModelToken } from 'nestjs-typegoose' +import { setupApp } from 'test/helper/register-app.helper' +import { firstKeyOfMap } from 'test/helper/utils.helper' +import { SnippetController } from '~/modules/snippet/snippet.controller' +import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model' +import { SnippetService } from '~/modules/snippet/snippet.service' +const mockingoose = require('mockingoose') + +describe.only('test /snippets', () => { + let app: NestFastifyApplication + const model = getModelForClass(SnippetModel) + + const mockTable = new Map() + + const mockPayload1: Partial = Object.freeze({ + name: 'Snippet_1', + private: false, + raw: JSON.stringify({ foo: 'bar' }), + type: SnippetType.JSON, + }) + + beforeAll(async () => { + const ref = await Test.createTestingModule({ + controllers: [SnippetController], + providers: [ + SnippetService, + { + provide: getModelToken(SnippetModel.name), + useValue: model, + }, + ], + }).compile() + + app = await setupApp(ref) + }) + + test('POST /snippets, should 422 with wrong name', async () => { + await app + .inject({ + method: 'POST', + url: '/snippets', + payload: { + name: 'Snippet-1', + private: false, + raw: JSON.stringify({ foo: 'bar' }), + type: SnippetType.JSON, + } as SnippetModel, + }) + .then((res) => { + // name is wrong format + expect(res.statusCode).toBe(422) + }) + }) + + test('POST /snippets, should return 201', async () => { + await app + .inject({ + method: 'POST', + url: '/snippets', + payload: mockPayload1, + }) + .then(async (res) => { + const json = res.json() + expect(res.statusCode).toBe(201) + expect(json).toBeDefined() + expect(json.name).toBe('Snippet_1') + // set mockingoose + mockingoose(model).toReturn( + { + ...mockPayload1, + _id: json._id, + }, + 'findOne', + ) + mockingoose(model).toReturn( + { + ...mockPayload1, + _id: json._id, + }, + 'countDocuments', + ) + mockTable.set(json._id, json) + }) + }) + + test('POST /snippets, re-create same of name should return 400', async () => { + await app + .inject({ + method: 'POST', + url: '/snippets', + payload: { + name: 'Snippet_1', + private: false, + raw: JSON.stringify({ foo: 'bar' }), + type: SnippetType.JSON, + } as SnippetModel, + }) + .then((res) => { + expect(res.statusCode).toBe(400) + }) + }) + + test('GET /snippets/:id, should return 200', async () => { + await app + .inject({ + method: 'GET', + url: '/snippets/' + firstKeyOfMap(mockTable), + }) + .then((res) => { + const json = res.json() + expect(res.statusCode).toBe(200) + expect(json.name).toBe('Snippet_1') + expect(json.raw).toBe(mockPayload1.raw) + + expect(json.data).toEqual(JSON.parse(mockPayload1.raw)) + }) + }) +}) diff --git a/test/src/modules/users/users.controller.e2e-spec.ts b/test/src/modules/user/user.controller.e2e-spec.ts similarity index 100% rename from test/src/modules/users/users.controller.e2e-spec.ts rename to test/src/modules/user/user.controller.e2e-spec.ts diff --git a/test/src/modules/users/users.controller.spec.ts b/test/src/modules/user/user.controller.spec.ts similarity index 100% rename from test/src/modules/users/users.controller.spec.ts rename to test/src/modules/user/user.controller.spec.ts diff --git a/test/src/modules/users/users.service.spec.ts b/test/src/modules/user/user.service.spec.ts similarity index 100% rename from test/src/modules/users/users.service.spec.ts rename to test/src/modules/user/user.service.spec.ts diff --git a/tsconfig.build.json b/tsconfig.build.json index 60bed82c..f78cd3ea 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,10 +4,6 @@ "declaration": false }, "extends": "./tsconfig.json", - "include": [ - "src/*", - "*.d.ts" - ], "exclude": [ "node_modules", "test", diff --git a/tsconfig.json b/tsconfig.json index 7101ee33..6435da4e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,10 @@ ] } }, + "include": [ + "src/*", + "*.d.ts" + ], "exclude": [ "dist", "tmp"