From 25480727be3189697fc27a193d19a0b7c8aadc01 Mon Sep 17 00:00:00 2001 From: nattadex Date: Fri, 15 Mar 2024 16:27:53 +0800 Subject: [PATCH] added server testing app --- apps/server/package.json | 2 + apps/server/src/MarbleFiLsdServerApp.ts | 19 ++- apps/server/src/modules/BaseModule.ts | 32 +++++ apps/server/src/user/UserController.spec.ts | 112 ++++++++++-------- .../test/MarbleFiLsdServerTestingApp.ts | 52 ++++++++ apps/server/test/TestingModule.ts | 50 ++++---- pnpm-lock.yaml | 30 ++++- 7 files changed, 222 insertions(+), 75 deletions(-) create mode 100644 apps/server/src/modules/BaseModule.ts create mode 100644 apps/server/test/MarbleFiLsdServerTestingApp.ts diff --git a/apps/server/package.json b/apps/server/package.json index 84517efc..8e476d46 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -25,9 +25,11 @@ "prettier": "@waveshq/standard-prettier", "dependencies": { "@prisma/client": "^5.6.0", + "@stickyjs/testcontainers": "^1.3.10", "@waveshq/standard-api-fastify": "^3.0.1", "class-validator": "^0.14.1", "joi": "^17.12.2", + "light-my-request": "^5.12.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" }, diff --git a/apps/server/src/MarbleFiLsdServerApp.ts b/apps/server/src/MarbleFiLsdServerApp.ts index bd03a26e..fed6856a 100644 --- a/apps/server/src/MarbleFiLsdServerApp.ts +++ b/apps/server/src/MarbleFiLsdServerApp.ts @@ -1,7 +1,10 @@ -import { Request, Response, NextFunction } from "express"; -import { INestApplication } from "@nestjs/common"; +import { NextFunction, Request, Response } from "express"; +import { INestApplication, NestApplicationOptions } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; -import { NestFastifyApplication } from "@nestjs/platform-fastify"; +import { + FastifyAdapter, + NestFastifyApplication, +} from "@nestjs/platform-fastify"; import { AppModule } from "./AppModule"; @@ -15,6 +18,16 @@ export class MarbleFiLsdServerApp< constructor(protected readonly module: any) {} + get nestApplicationOptions(): NestApplicationOptions { + return { + bufferLogs: true, + }; + } + + get fastifyAdapter(): FastifyAdapter { + return new FastifyAdapter(); + } + async createNestApp(): Promise { const app = await NestFactory.create(AppModule); await this.configureApp(app); diff --git a/apps/server/src/modules/BaseModule.ts b/apps/server/src/modules/BaseModule.ts new file mode 100644 index 00000000..66c05b46 --- /dev/null +++ b/apps/server/src/modules/BaseModule.ts @@ -0,0 +1,32 @@ +import { DynamicModule, Global, Module, ModuleMetadata } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { LoggerModule } from "nestjs-pino"; + +/** + * Baseline module for any Bridge nest applications. + * + * - `@nestjs/config`, nestjs ConfigModule + * - `nestjs-pino`, the Pino logger for NestJS + * - `joi`, for validation of environment variables + */ +@Global() +@Module({ + imports: [ + LoggerModule.forRoot({ + exclude: ["/health", "/version", "/settings"], + }), + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + }), + ], +}) +export class BaseModule { + static with(metadata: ModuleMetadata): DynamicModule { + return { + module: BaseModule, + global: true, + ...metadata, + }; + } +} diff --git a/apps/server/src/user/UserController.spec.ts b/apps/server/src/user/UserController.spec.ts index 70798c0a..c9abf523 100644 --- a/apps/server/src/user/UserController.spec.ts +++ b/apps/server/src/user/UserController.spec.ts @@ -1,57 +1,75 @@ -import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@stickyjs/testcontainers'; -import { UserController} from "./UserController"; -import { UserService} from "./UserService"; -import {PrismaService} from "../PrismaService"; - -import {buildTestConfig, TestingModule } from "../../test/TestingModule"; -import {MarbleFiLsdServerTestingApp} from "../../dist/test-i9n/MarbleFiLsdServerTestingApp"; - -describe('UserController', () => { - let testing: MarbleFiLsdServerTestingApp - let userController: UserController; - let userService: UserService; - let prismaService: PrismaService - let startedPostgresContainer: StartedPostgreSqlContainer; - - beforeAll(async () => { - startedPostgresContainer = await new PostgreSqlContainer().start(); - testing = new MarbleFiLsdServerTestingApp( - TestingModule.register( - buildTestConfig({startedPostgresContainer}), - ), - ); - const app = await testing.start(); +import { HttpStatus } from "@nestjs/common"; +import { + PostgreSqlContainer, + StartedPostgreSqlContainer, +} from "@stickyjs/testcontainers"; +import { UserController } from "./UserController"; +import { UserService } from "./UserService"; +import { PrismaService } from "../PrismaService"; - // init postgres database - prismaService = app.get(PrismaService); - userService = new UserService(prismaService); - userController = new UserController(userService); - }); +import { buildTestConfig, TestingModule } from "../../test/TestingModule"; +import { MarbleFiLsdServerTestingApp } from "../../test/MarbleFiLsdServerTestingApp"; + +describe("UserController", () => { + let testing: MarbleFiLsdServerTestingApp; + let userController: UserController; + let userService: UserService; + let prismaService: PrismaService; + let startedPostgresContainer: StartedPostgreSqlContainer; + + beforeAll(async () => { + startedPostgresContainer = await new PostgreSqlContainer().start(); + testing = new MarbleFiLsdServerTestingApp( + TestingModule.register(buildTestConfig({ startedPostgresContainer })), + ); + const app = await testing.start(); - describe('create user', () => { - it('should create an active user in db', async () => { - const res = await userController.create("test@example.com", 'ACTIVE'); + // init postgres database + prismaService = app.get(PrismaService); + userService = new UserService(prismaService); + userController = new UserController(userService); + }); - expect(res).toEqual({ id: 1, email: 'test@example.com', status: 'ACTIVE' }); - }); + describe("create user", () => { + it("should create an active user in db", async () => { + const res = await userController.create("test@example.com", "ACTIVE"); - it('should create an inactive user in db', async () => { - const res = await userController.create("test2@example.com", 'INACTIVE'); + expect(res).toEqual({ + id: 1, + email: "test@example.com", + status: "ACTIVE", + }); + }); - expect(res).toEqual({ id: 2, email: 'test2@example.com', status: 'INACTIVE' }); - }); + it("should create an inactive user in db", async () => { + const res = await userController.create("test2@example.com", "INACTIVE"); - it('should create an active user by default', async () => { - const res = await userController.create("test3@example.com"); + expect(res).toEqual({ + id: 2, + email: "test2@example.com", + status: "INACTIVE", + }); + }); - expect(res).toEqual({ id: 3, email: 'test3@example.com', status: 'ACTIVE' }); - }); + it("should create an active user by default", async () => { + const res = await userController.create("test3@example.com"); - it('should not create a user with same email', async () => { - const res = await userController.create("test@example.com"); + expect(res).toEqual({ + id: 3, + email: "test3@example.com", + status: "ACTIVE", + }); + }); - console.log(res) - // expect(res).toEqual({ id: 3, email: 'test@example.com', status: 'ACTIVE' }); - }); + it("should not create a user with same email", async () => { + try { + await userController.create("test@example.com"); + } catch (e) { + expect(e.response.statusCode).toStrictEqual(HttpStatus.BAD_REQUEST); + expect(e.response.message).toStrictEqual( + `Duplicate email 'test@example.com' found in database`, + ); + } }); -}); \ No newline at end of file + }); +}); diff --git a/apps/server/test/MarbleFiLsdServerTestingApp.ts b/apps/server/test/MarbleFiLsdServerTestingApp.ts new file mode 100644 index 00000000..0c9c8c53 --- /dev/null +++ b/apps/server/test/MarbleFiLsdServerTestingApp.ts @@ -0,0 +1,52 @@ +import { NestFastifyApplication } from "@nestjs/platform-fastify"; +import { Test, TestingModule } from "@nestjs/testing"; +import { + Chain as LightMyRequestChain, + InjectOptions, + Response as LightMyRequestResponse, +} from "light-my-request"; + +import { MarbleFiLsdServerApp } from "../src/MarbleFiLsdServerApp"; +import { BaseModule } from "../src/modules/BaseModule"; + +/** + * Testing app used for testing MarbleFi Server App behaviour through integration tests + */ +export class MarbleFiLsdServerTestingApp extends MarbleFiLsdServerApp { + async createTestingModule(): Promise { + return Test.createTestingModule({ + imports: [ + BaseModule.with({ + imports: [this.module], + }), + ], + }).compile(); + } + + override async createNestApp(): Promise { + const module = await this.createTestingModule(); + return module.createNestApplication( + this.fastifyAdapter, + this.nestApplicationOptions, + ); + } + + async start(): Promise { + return this.init(); + } + + /** + * A wrapper function around native `fastify.inject()` method. + * @returns {void} + */ + inject(): LightMyRequestChain; + inject(opts: InjectOptions | string): Promise; + inject( + opts?: InjectOptions | string, + ): LightMyRequestChain | Promise { + if (opts === undefined) { + return this.app!.inject(); + } + return this.app!.inject(opts); + } +} diff --git a/apps/server/test/TestingModule.ts b/apps/server/test/TestingModule.ts index d4d8c875..d200c320 100644 --- a/apps/server/test/TestingModule.ts +++ b/apps/server/test/TestingModule.ts @@ -1,39 +1,41 @@ -import * as child_process from 'node:child_process'; +import * as child_process from "node:child_process"; -import { DynamicModule, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { StartedPostgreSqlContainer } from '@stickyjs/testcontainers'; +import { DynamicModule, Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { StartedPostgreSqlContainer } from "@stickyjs/testcontainers"; -import { AppConfig, DeepPartial} from "../src/AppConfig"; -import { AppModule} from "../src/AppModule"; +import { AppConfig, DeepPartial } from "../src/AppConfig"; +import { AppModule } from "../src/AppModule"; @Module({}) export class TestingModule { - static register(config: AppConfig): DynamicModule { - return { - module: TestingModule, - imports: [AppModule, ConfigModule.forFeature(() => config)], - }; - } + static register(config: AppConfig): DynamicModule { + return { + module: TestingModule, + imports: [AppModule, ConfigModule.forFeature(() => config)], + }; + } } export function buildTestConfig({ - startedPostgresContainer, - }: BuildTestConfigParams) { - if (startedPostgresContainer === undefined) { - throw Error('Must pass in StartedPostgresContainer'); - } - const dbUrl = `postgres://${startedPostgresContainer.getUsername()}:${startedPostgresContainer.getPassword()}@${startedPostgresContainer.getHost()}:${startedPostgresContainer.getPort()}`; - child_process.execSync(`export DATABASE_URL=${dbUrl} && pnpm prisma migrate deploy`); - return { - dbUrl: dbUrl ?? '' - }; + startedPostgresContainer, +}: BuildTestConfigParams) { + if (startedPostgresContainer === undefined) { + throw Error("Must pass in StartedPostgresContainer"); + } + const dbUrl = `postgres://${startedPostgresContainer.getUsername()}:${startedPostgresContainer.getPassword()}@${startedPostgresContainer.getHost()}:${startedPostgresContainer.getPort()}`; + child_process.execSync( + `export DATABASE_URL=${dbUrl} && pnpm prisma migrate deploy`, + ); + return { + dbUrl: dbUrl ?? "", + }; } type BuildTestConfigParams = DeepPartial & { - startedPostgresContainer: StartedPostgreSqlContainer; + startedPostgresContainer: StartedPostgreSqlContainer; }; type OptionalBuildTestConfigParams = { - dbUrl: string; + dbUrl: string; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29b685b5..52b5ad18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@prisma/client': specifier: ^5.6.0 version: 5.10.2(prisma@5.10.2) + '@stickyjs/testcontainers': + specifier: ^1.3.10 + version: 1.3.10 '@waveshq/standard-api-fastify': specifier: ^3.0.1 version: 3.0.1(@prisma/client@5.10.2)(class-validator@0.14.1)(pino-http@8.6.1)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typescript@5.3.3) @@ -29,6 +32,9 @@ importers: joi: specifier: ^17.12.2 version: 17.12.2 + light-my-request: + specifier: ^5.12.0 + version: 5.12.0 reflect-metadata: specifier: ^0.1.13 version: 0.1.13 @@ -4833,6 +4839,15 @@ packages: - ts-node - typescript + /@stickyjs/testcontainers@1.3.10: + resolution: {integrity: sha512-bsqeFYB/gM7tA5R6irOXiVYdSgwOdZWSspPEBFHViU2B8UNCMATyb9tYRpf3Yx4OLnimAmYjNu1+u2JWPrqpxw==} + dependencies: + testcontainers: 9.12.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@stickyjs/turbo-jest@1.3.10(@babel/core@7.23.9)(@types/node@20.11.1)(ts-node@10.9.2)(turbo@1.11.3)(typanion@3.14.0)(typescript@5.3.3): resolution: {integrity: sha512-ojHHwWEWL2BAJ5GDO/MyvbhbNL/cMR2+VA7aLE7q/d3EzQFP3eDei7b5DMJh/ApbfIC7yBaFoy5iuecVCCTJJA==} dependencies: @@ -8615,6 +8630,11 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true @@ -10608,7 +10628,7 @@ packages: fast-content-type-parse: 1.1.0 fast-json-stringify: 5.12.0 find-my-way: 8.1.0 - light-my-request: 5.11.0 + light-my-request: 5.12.0 pino: 8.19.0 process-warning: 3.0.0 proxy-addr: 2.0.7 @@ -13062,6 +13082,14 @@ packages: set-cookie-parser: 2.6.0 dev: false + /light-my-request@5.12.0: + resolution: {integrity: sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==} + dependencies: + cookie: 0.6.0 + process-warning: 3.0.0 + set-cookie-parser: 2.6.0 + dev: false + /lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} dependencies: