From db612031f15c625041572496f8f41a8ac8e99fc6 Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Thu, 9 Jan 2025 20:19:24 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feature:=20e2e=20TEST=20=EC=85=8B?= =?UTF-8?q?=ED=8C=85=20=EC=A4=80=EB=B9=84=20(=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- src/auth/application/jwt-refresh.strategy.ts | 9 +-- src/auth/auth.module.ts | 5 +- src/common/filters/exception.filters.ts | 7 +- src/pet/pet.module.ts | 4 +- src/pet/presentation/pet.controller.ts | 32 ++------ .../pre-registration-survey-use-case.ts | 2 +- .../pre-registration-servey.module.ts | 2 +- test/app.e2e-spec.ts | 24 ------ test/medium/auth.controller.test.ts | 73 +++++++++++++++++++ test/setup/jest.setup.ts | 1 + yarn.lock | 42 +++++------ 12 files changed, 120 insertions(+), 89 deletions(-) delete mode 100644 test/app.e2e-spec.ts create mode 100644 test/medium/auth.controller.test.ts create mode 100644 test/setup/jest.setup.ts diff --git a/package.json b/package.json index 1a2301d..f012097 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "jest": "^29.5.0", "prettier": "^3.0.0", "source-map-support": "^0.5.21", - "supertest": "^6.3.3", + "supertest": "^7.0.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", @@ -89,6 +89,12 @@ "json", "ts" ], + "setupFiles": [ + "/test/setup/jest.setup.ts" + ], + "setupFilesAfterEnv": [ + "/test/setup/jest.setup.ts" + ], "roots": [ "/src", "/test" diff --git a/src/auth/application/jwt-refresh.strategy.ts b/src/auth/application/jwt-refresh.strategy.ts index 3931b0b..39e7d11 100644 --- a/src/auth/application/jwt-refresh.strategy.ts +++ b/src/auth/application/jwt-refresh.strategy.ts @@ -1,12 +1,9 @@ import { ConfigService } from '@nestjs/config'; -import { - BadRequestException, - UnauthorizedException, -} from '@nestjs/common/exceptions'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common/exceptions'; import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { Strategy, ExtractJwt } from 'passport-jwt'; -import { CustomerService } from 'src/customer/application/customer.service'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { CustomerService } from '../../customer/application/customer.service'; import { BusinessService } from '../../business/application/business.service'; import { DriverService } from '../../driver/application/driver.service'; import { IUserService } from '../user.interface'; diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 89c9856..aabb93c 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,9 +1,9 @@ import { Global, Module } from '@nestjs/common'; import { AuthService } from './application/auth.service'; import { AuthController } from './presentation/auth.controller'; -import { CustomerModule } from 'src/customer/customer.module'; +import { CustomerModule } from '../customer/customer.module'; import { JwtModule } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtAccessStrategy } from './application/jwt-access.strategy'; import { JwtRefreshStrategy } from './application/jwt-refresh.strategy'; import { PassportModule } from '@nestjs/passport'; @@ -17,6 +17,7 @@ import { SmsModule } from '../common/sender/sms/sms.module'; @Global() @Module({ imports: [ + ConfigModule, JwtModule.registerAsync({ useFactory: async (configService: ConfigService) => ({ secret: configService.get('jwt/access/secret'), diff --git a/src/common/filters/exception.filters.ts b/src/common/filters/exception.filters.ts index 92d43ed..b418687 100644 --- a/src/common/filters/exception.filters.ts +++ b/src/common/filters/exception.filters.ts @@ -6,14 +6,11 @@ import { HttpException, HttpStatus, Logger, - NotFoundException, + NotFoundException } from '@nestjs/common'; import { ResponseEntity } from '../dto/response.entity'; import { Response } from 'express'; -import { - BadRequestException, - UnauthorizedException, -} from '@nestjs/common/exceptions'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common/exceptions'; import { EntityNotFoundError } from 'typeorm'; @Catch() diff --git a/src/pet/pet.module.ts b/src/pet/pet.module.ts index 869dd1c..57434c6 100644 --- a/src/pet/pet.module.ts +++ b/src/pet/pet.module.ts @@ -2,8 +2,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { PetService } from './application/pet.service'; import { PetController } from './presentation/pet.controller'; -import { Pet } from 'src/schemas/pets.entity'; -import { Breed } from 'src/schemas/breed.entity'; +import { Pet } from '../schemas/pets.entity'; +import { Breed } from '../schemas/breed.entity'; import { PetChecklist } from '../schemas/pet-checklist.entity'; import { CustomerModule } from '../customer/customer.module'; import { PetChecklistChoiceAnswer } from '../schemas/pet-checklist-chocie-answer.entity'; diff --git a/src/pet/presentation/pet.controller.ts b/src/pet/presentation/pet.controller.ts index 69bf005..b722829 100644 --- a/src/pet/presentation/pet.controller.ts +++ b/src/pet/presentation/pet.controller.ts @@ -1,30 +1,12 @@ import { PetService } from '../application/pet.service'; -import { - Body, - Controller, - Delete, - Get, - Param, - Post, - Put, - Query, -} from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { PetChecklistAnswerDto, PetChecklistDto, PetDto } from './pet.dto'; -import { - ApiBody, - ApiCreatedResponse, - ApiOkResponse, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; -import { GroupValidation } from 'src/common/validation/validation.decorator'; -import { CrudGroup } from 'src/common/validation/validation.data'; -import { CustomerEntity } from 'src/schemas/customer.entity'; -import { Auth, CurrentCustomer } from 'src/auth/decorator/auth.decorator'; -import { - ChecklistType, - PetChecklistCategory, -} from '../../schemas/pet-checklist.entity'; +import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { GroupValidation } from '../../common/validation/validation.decorator'; +import { CrudGroup } from '../../common/validation/validation.data'; +import { CustomerEntity } from '../../schemas/customer.entity'; +import { Auth, CurrentCustomer } from '../../auth/decorator/auth.decorator'; +import { ChecklistType, PetChecklistCategory } from '../../schemas/pet-checklist.entity'; import { ResponseEntity } from '../../common/dto/response.entity'; @ApiTags('반려동물 관련 API') diff --git a/src/pre-registration-servey/application/pre-registration-survey-use-case.ts b/src/pre-registration-servey/application/pre-registration-survey-use-case.ts index a13cb2c..d4f9236 100644 --- a/src/pre-registration-servey/application/pre-registration-survey-use-case.ts +++ b/src/pre-registration-servey/application/pre-registration-survey-use-case.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PreRegistrationSurveyRequest } from './pre-registration-survey-request'; -import { EmailService } from 'src/email/email.service'; +import { EmailService } from '../../email/email.service'; import { EmailContentGenerator } from './email-content-generator'; @Injectable() diff --git a/src/pre-registration-servey/pre-registration-servey.module.ts b/src/pre-registration-servey/pre-registration-servey.module.ts index 5e248a0..ac9908a 100644 --- a/src/pre-registration-servey/pre-registration-servey.module.ts +++ b/src/pre-registration-servey/pre-registration-servey.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { PreRegistrationSurveyUseCase } from './application/pre-registration-survey-use-case'; import { PreRegistrationSurveyController } from './presentation/pre-registration-survey-controller'; -import { EmailModule } from 'src/email/email.module'; +import { EmailModule } from '../email/email.module'; @Module({ imports: [EmailModule], diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 50cda62..0000000 --- a/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts new file mode 100644 index 0000000..641b9de --- /dev/null +++ b/test/medium/auth.controller.test.ts @@ -0,0 +1,73 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthModule } from '../../src/auth/auth.module'; +import { ImageModule } from '../../src/common/image/image.module'; +import { CloudModule } from '../../src/common/cloud/cloud.module'; +import { RedisModule } from '@liaoliaots/nestjs-redis'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; + +describe('AuthController E2E 테스트 (e2e)', () => { + let app: INestApplication; + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + AuthModule, + ImageModule, + CloudModule, + RedisModule.forRootAsync({ + useFactory: async (configService: ConfigService) => { + return { + config: {}, + readyLog: true, + }; + }, + inject: [ConfigService], + }), + TypeOrmModule.forRootAsync({ + useFactory: async (configService: ConfigService) => { + const datasource = { + host: 'localhost', + port: 5432, + username: 'test', + password: 'test', + database: 'test', + logging: false, + }; + + return { + type: 'postgres', + host: datasource.host, + port: datasource.port, + username: datasource.username, + password: datasource.password, + database: datasource.database, + logging: datasource.logging, + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: false, + namingStrategy: new SnakeNamingStrategy(), + }; + }, + inject: [ConfigService], + }), + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/test/setup/jest.setup.ts b/test/setup/jest.setup.ts new file mode 100644 index 0000000..d2c9bc6 --- /dev/null +++ b/test/setup/jest.setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/yarn.lock b/yarn.lock index f0c3213..08b47f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4408,15 +4408,14 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formidable@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" - integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== +formidable@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.2.tgz#207c33fecdecb22044c82ba59d0c63a12fb81d77" + integrity sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg== dependencies: dezalgo "^1.0.4" - hexoid "^1.0.0" + hexoid "^2.0.0" once "^1.4.0" - qs "^6.11.0" forwarded@0.2.0: version "0.2.0" @@ -4665,10 +4664,10 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -hexoid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" - integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== +hexoid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-2.0.0.tgz#fb36c740ebbf364403fa1ec0c7efd268460ec5b9" + integrity sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw== highlight.js@^10.7.1: version "10.7.3" @@ -6622,7 +6621,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: +semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -6978,29 +6977,28 @@ strnum@^1.0.5: resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== -superagent@^8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" - integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== +superagent@^9.0.1: + version "9.0.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-9.0.2.tgz#a18799473fc57557289d6b63960610e358bdebc1" + integrity sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w== dependencies: component-emitter "^1.3.0" cookiejar "^2.1.4" debug "^4.3.4" fast-safe-stringify "^2.1.1" form-data "^4.0.0" - formidable "^2.1.2" + formidable "^3.5.1" methods "^1.1.2" mime "2.6.0" qs "^6.11.0" - semver "^7.3.8" -supertest@^6.3.3: - version "6.3.4" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.4.tgz#2145c250570c2ea5d337db3552dbfb78a2286218" - integrity sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw== +supertest@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.0.0.tgz#cac53b3d6872a0b317980b2b0cfa820f09cd7634" + integrity sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA== dependencies: methods "^1.1.2" - superagent "^8.1.2" + superagent "^9.0.1" supports-color@^5.3.0: version "5.5.0" From 2e756fc61778487c0a5396c63d7f11b686156b07 Mon Sep 17 00:00:00 2001 From: emibgo2 Date: Sun, 12 Jan 2025 19:02:00 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feature:=20redis=20testcontainers=20?= =?UTF-8?q?=ED=99=94,=20pg-mem=20=EC=A0=81=EC=9A=A9=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/app.controller.spec.ts | 14 +- test/medium/auth.controller.test.ts | 132 +++-- test/mock/fake.config.service.ts | 2 + test/mock/pg.helper.ts | 44 ++ yarn.lock | 743 +++++++++++++++++++++++++++- 6 files changed, 872 insertions(+), 65 deletions(-) create mode 100644 test/mock/pg.helper.ts diff --git a/package.json b/package.json index f012097..eb47109 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.10", "@ssut/nestjs-sqs": "^2.2.0", + "@testcontainers/redis": "^10.16.0", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", "@types/socket.io": "^3.0.2", @@ -49,6 +50,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pg": "^8.11.5", + "pg-mem": "^3.0.4", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index d22f389..e18ce46 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,17 +1,29 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; describe('AppController', () => { let appController: AppController; + let app: INestApplication; beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ + const moduleFutures = await Test.createTestingModule({ controllers: [AppController], providers: [AppService], }).compile(); + app = moduleFutures.createNestApplication(); appController = app.get(AppController); + await app.init(); + }); + + test('integration test ', async () => { + await request(app.getHttpServer()) + .get('/auth') + .expect(200) + .expect('Hello World!'); }); describe('root', () => { diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index 641b9de..fd3e645 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -1,72 +1,104 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; import * as request from 'supertest'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AuthModule } from '../../src/auth/auth.module'; +import { INestApplication } from '@nestjs/common'; +import { AuthController } from '../../src/auth/presentation/auth.controller'; +import { AuthService } from '../../src/auth/application/auth.service'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule } from '@nestjs/config'; +import { PassportModule } from '@nestjs/passport'; +import { SmsModule } from '../../src/common/sender/sms/sms.module'; +import { UserModule } from '../../src/auth/user.module'; +import { CacheModule } from '../../src/common/cache/cache.module'; +import { DriverModule } from '../../src/driver/driver.module'; +import { SecurityModule } from '../../src/auth/application/security.module'; +import { BusinessModule } from '../../src/business/business.module'; +import { CustomerModule } from '../../src/customer/customer.module'; +import { RedisModule } from '@liaoliaots/nestjs-redis'; import { ImageModule } from '../../src/common/image/image.module'; +import { CLOUD_STORAGE } from '../../src/common/cloud/aws/s3/application/s3.service'; +import { FakeCloudStorageService } from '../mock/fake.cloud-storage.service'; import { CloudModule } from '../../src/common/cloud/cloud.module'; -import { RedisModule } from '@liaoliaots/nestjs-redis'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis'; +import { IBackup, newDb } from 'pg-mem'; -describe('AuthController E2E 테스트 (e2e)', () => { +describe('AuthController E2E 테스트', () => { + let redisContainer: StartedRedisContainer; let app: INestApplication; + let backup: IBackup; + beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ + jest.setTimeout(30000); // 30초 타임아웃 설정 (기본값은 5초) + // Redis Testcontainers 설정 + redisContainer = await new RedisContainer().start(); + const redisHost = redisContainer.getHost(); + const redisPort = redisContainer.getMappedPort(6379); + + // pg-mem 설정 + const db = newDb(); + db.public.registerFunction({ + name: 'current_database', + implementation: () => 'test_database', + }); + + db.public.registerFunction({ + name: 'version', + implementation: () => '13.3', + }); + + const dataSource = await db.adapters.createTypeormDataSource({ + type: 'postgres', + entities: [__dirname + '/../../src/**/*.entity.{ts,js}'], + synchronize: false, + }); + await dataSource.initialize(); + + const moduleFutures = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true }), - AuthModule, - ImageModule, - CloudModule, - RedisModule.forRootAsync({ - useFactory: async (configService: ConfigService) => { - return { - config: {}, - readyLog: true, - }; - }, - inject: [ConfigService], + JwtModule.registerAsync({ + useFactory: async () => ({ secret: 'test-secret' }), }), - TypeOrmModule.forRootAsync({ - useFactory: async (configService: ConfigService) => { - const datasource = { - host: 'localhost', - port: 5432, - username: 'test', - password: 'test', - database: 'test', - logging: false, - }; - - return { - type: 'postgres', - host: datasource.host, - port: datasource.port, - username: datasource.username, - password: datasource.password, - database: datasource.database, - logging: datasource.logging, - entities: [__dirname + '/**/*.entity{.ts,.js}'], - synchronize: false, - namingStrategy: new SnakeNamingStrategy(), - }; - }, - inject: [ConfigService], + PassportModule.register({ defaultStrategy: 'access' }), + SmsModule, + UserModule, + CacheModule, + RedisModule.forRootAsync({ + useFactory: async () => ({ + config: { host: redisHost, port: redisPort }, + readyLog: true, + }), }), + DriverModule, + SecurityModule, + BusinessModule, + CustomerModule, + ImageModule, + CloudModule, ], - }).compile(); + controllers: [AuthController], + providers: [AuthService], + }) + .overrideProvider(CLOUD_STORAGE) + .useClass(FakeCloudStorageService) + .overrideProvider('TypeORMDataSource') // 데이터베이스 프로바이더 오버라이드 + .useValue(dataSource) // pg-mem 데이터 소스 주입 + .compile(); + + backup = db.backup(); - app = moduleFixture.createNestApplication(); + app = moduleFutures.createNestApplication(); await app.init(); }); afterAll(async () => { + await redisContainer.stop(); await app.close(); + backup.restore(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') + test('e2e test ', async () => { + await request(app.getHttpServer()) + .post('/v1/auth/login') .expect(200) .expect('Hello World!'); }); diff --git a/test/mock/fake.config.service.ts b/test/mock/fake.config.service.ts index 07cf7ae..9d5f4a4 100644 --- a/test/mock/fake.config.service.ts +++ b/test/mock/fake.config.service.ts @@ -12,6 +12,8 @@ export class FakeConfigService extends ConfigService { this.configMap['jwt/access/strategy'] = 'unique'; this.configMap['jwt/refresh/secret'] = 'refresh_secret'; this.configMap['jwt/refresh/expire'] = 60 * 60 * 24 * 14; + this.configMap['datasource/redis'] = '{"host":"localhost","port":6379}'; + this.configMap['datasource/db'] = '{"type":"mysql","host":"localhost","port":3306,"username":"root","password":"root","database":"test"}'; } override get(key: string): any { diff --git a/test/mock/pg.helper.ts b/test/mock/pg.helper.ts new file mode 100644 index 0000000..8f49f86 --- /dev/null +++ b/test/mock/pg.helper.ts @@ -0,0 +1,44 @@ +import { IBackup, IMemoryDb, newDb } from 'pg-mem'; +import { Connection } from 'typeorm'; + +export class PgTestHelper { + db: IMemoryDb; + connection: Connection; + backup: IBackup; + + async connect(entities?: any[]) { + this.db = newDb({ autoCreateForeignKeyIndices: true }); + this.db.public.registerFunction({ + implementation: () => 'test', + name: 'current_database', + }); + this.db.public.registerFunction({ + implementation: () => 'test', + name: 'version', + }); + this.connection = await this.db.adapters.createTypeormDataSource({ + type: 'postgres', + username: 'test', + password: 'test', + entities: entities ?? __dirname + '/../../src/**/*.entity.{ts,js}', + logger: 'advanced-console', + logging: true, + synchronize: true, + }); + await this.sync(); + this.backup = this.db.backup(); + return this.connection; + } + + restore() { + this.backup.restore(); + } + + async disconnect() { + await this.connection.close(); + } + + async sync() { + await this.connection.synchronize(); + } +} diff --git a/yarn.lock b/yarn.lock index 08b47f8..a88eecc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,6 +1197,11 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1246,6 +1251,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + "@golevelup/nestjs-discovery@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz#d48f334760c55457e7b98f3d3b78ee99195bb4f5" @@ -2329,6 +2339,13 @@ sqs-consumer "^7.0.3" sqs-producer "^3.1.1" +"@testcontainers/redis@^10.16.0": + version "10.16.0" + resolved "https://registry.yarnpkg.com/@testcontainers/redis/-/redis-10.16.0.tgz#e3eeeb1ec1194842e050525474a2dc3bc18f899f" + integrity sha512-+NJO1tfMXvUQiMfa9Y8qqwaFP6yV0BBg2cNA+iNz+Zt6kzTyxMapBiKCL8pWsCqWolz2mqZwuDtfcHBxdoCzAw== + dependencies: + testcontainers "^10.16.0" + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -2421,6 +2438,23 @@ dependencies: "@types/node" "*" +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/docker-modem/-/docker-modem-3.0.6.tgz#1f9262fcf85425b158ca725699a03eb23cddbf87" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.29": + version "3.3.34" + resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.34.tgz#1cef62f1b98f80bd4460961dd8aac99b95a0fb6e" + integrity sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" + "@types/ssh2" "*" + "@types/eslint-scope@^3.7.3": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -2564,6 +2598,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^18.11.18": + version "18.19.70" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.70.tgz#5a77508f5568d16fcd3b711c8102d7a430a04df7" + integrity sha512-RE+K0+KZoEpDUbGGctnGdkrLFwi1eYKTlIHNl2Um98mUkGsm1u2Ff6Ltd0e8DktTtC98uy7rSj+hO8t/QuLoVQ== + dependencies: + undici-types "~5.26.4" + "@types/nodemailer@^6.4.16": version "6.4.16" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.16.tgz#db006abcb1e1c8e6ea2fb53b27fefec3c03eaa6c" @@ -2640,6 +2681,28 @@ dependencies: "@types/node" "*" +"@types/ssh2-streams@*": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f" + integrity sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg== + dependencies: + "@types/node" "*" + +"@types/ssh2@*": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.15.3.tgz#dc67c0336d04e1f3b5ae500b0c1455676ab49193" + integrity sha512-pUhXytp7aRAj1AKCQpdKNKGb32e53MUGPu186U9cm8mbX6kxF0UcngI/RVoKvJTWdHqDFiNrSDiPR/JZWexcCQ== + dependencies: + "@types/node" "^18.11.18" + +"@types/ssh2@^0.5.48": + version "0.5.52" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.52.tgz#9dbd8084e2a976e551d5e5e70b978ed8b5965741" + integrity sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg== + dependencies: + "@types/node" "*" + "@types/ssh2-streams" "*" + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" @@ -2911,6 +2974,13 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3052,6 +3122,32 @@ append-field@^1.0.0: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +archiver-utils@^5.0.0, archiver-utils@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" + integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== + dependencies: + glob "^10.0.0" + graceful-fs "^4.2.0" + is-stream "^2.0.1" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +archiver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" + integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== + dependencies: + archiver-utils "^5.0.2" + async "^3.2.4" + buffer-crc32 "^1.0.0" + readable-stream "^4.0.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^6.0.1" + are-we-there-yet@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" @@ -3097,6 +3193,23 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +async-lock@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f" + integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== + +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -3134,6 +3247,11 @@ axios@^1.7.2: form-data "^4.0.0" proxy-from-env "^1.1.0" +b4a@^1.6.4: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -3199,6 +3317,39 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-events@^2.0.0, bare-events@^2.2.0: + version "2.5.4" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.4.tgz#16143d435e1ed9eafd1ab85f12b89b3357a41745" + integrity sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA== + +bare-fs@^2.1.1: + version "2.3.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a" + integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9" + integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.1.tgz#b3b9874fab05b662c9aea2706a12fb0698c46836" + integrity sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g== + dependencies: + streamx "^2.21.0" + base32.js@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba" @@ -3214,6 +3365,13 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + bcrypt@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" @@ -3232,7 +3390,7 @@ bintrees@1.0.2: resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== -bl@^4.1.0: +bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== @@ -3310,6 +3468,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -3345,6 +3508,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + builder-pattern@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/builder-pattern/-/builder-pattern-2.2.0.tgz#f1e56f889282486fb5b3852947e91cd09cfe7b79" @@ -3357,11 +3525,24 @@ busboy@^1.0.0: dependencies: streamsearch "^1.1.0" +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -3373,6 +3554,24 @@ call-bind@^1.0.2, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3440,6 +3639,11 @@ chokidar@3.6.0, chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -3596,7 +3800,7 @@ commander@4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^2.20.0: +commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3617,6 +3821,17 @@ component-emitter@^1.3.0: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== +compress-commons@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" + integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^6.0.0" + is-stream "^2.0.1" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -3702,6 +3917,27 @@ cosmiconfig@^8.2.0: parse-json "^5.2.0" path-type "^4.0.0" +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" + integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== + dependencies: + crc-32 "^1.2.0" + readable-stream "^4.0.0" + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" @@ -3774,6 +4010,13 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" +debug@^4.3.5: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" @@ -3877,6 +4120,37 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== + +docker-compose@^0.24.8: + version "0.24.8" + resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.24.8.tgz#6c125e6b9e04cf68ced47e2596ef2bb93ee9694e" + integrity sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw== + dependencies: + yaml "^2.2.2" + +docker-modem@^3.0.0: + version "3.0.8" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.8.tgz#ef62c8bdff6e8a7d12f0160988c295ea8705e77a" + integrity sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.11.0" + +dockerode@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.5.tgz#7ae3f40f2bec53ae5e9a741ce655fff459745629" + integrity sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^3.0.0" + tar-fs "~2.0.1" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -3894,6 +4168,15 @@ dotenv@16.4.5, dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3936,6 +4219,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + engine.io-parser@~5.2.1: version "5.2.2" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" @@ -3979,6 +4269,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -3989,6 +4284,13 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236" integrity sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw== +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -4145,12 +4447,17 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + events@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -4242,6 +4549,11 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -4427,6 +4739,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^10.0.0: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -4463,6 +4780,11 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + gauge@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" @@ -4499,11 +4821,40 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" + integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + function-bind "^1.1.2" + get-proto "^1.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-proto@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -4539,6 +4890,18 @@ glob@10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" +glob@^10.0.0: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^10.3.10: version "10.3.15" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.15.tgz#e72bc61bc3038c90605f5dd48543dc67aaf3b50d" @@ -4603,6 +4966,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -4645,6 +5013,11 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -4657,7 +5030,7 @@ has-unicode@^2.0.1: resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -hasown@^2.0.0: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -4732,6 +5105,11 @@ image-to-base64@^2.2.0: dependencies: node-fetch "^2.6.0" +immutable@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== + import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4909,7 +5287,7 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-stream@^2.0.0: +is-stream@^2.0.0, is-stream@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== @@ -4936,6 +5314,11 @@ isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5008,6 +5391,15 @@ jackspeak@^2.3.5, jackspeak@^2.3.6: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" @@ -5430,6 +5822,17 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz#addb683c2b78014d0b78d704c2fcbdf0695a60e2" + integrity sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -5454,6 +5857,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonwebtoken@9.0.2, jsonwebtoken@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" @@ -5499,6 +5907,13 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -5596,7 +6011,7 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@4.17.21, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5661,6 +6076,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5742,6 +6162,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimatch@^8.0.2: version "8.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" @@ -5756,6 +6183,13 @@ minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -5788,6 +6222,11 @@ minipass@^7.0.4: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.1.tgz#f7f85aff59aa22f110b20e27692465cf3bf89481" integrity sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -5796,6 +6235,11 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -5803,7 +6247,7 @@ mkdirp@^0.5.4: dependencies: minimist "^1.2.6" -mkdirp@^1.0.3: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -5813,6 +6257,16 @@ mkdirp@^2.1.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== +moment@^2.27.0: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +moo@^0.5.0, moo@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -5860,6 +6314,11 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nan@^2.19.0, nan@^2.20.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== + nanoid@^3.3.4: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -5870,6 +6329,16 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +nearley@^2.19.5: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -5963,11 +6432,21 @@ object-hash@3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== +object-hash@^2.0.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -5975,7 +6454,7 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -6054,6 +6533,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6143,7 +6627,7 @@ path-scurry@^1.10.1, path-scurry@^1.6.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-scurry@^1.11.0: +path-scurry@^1.11.0, path-scurry@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -6186,6 +6670,19 @@ pg-int8@1.0.1: resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== +pg-mem@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/pg-mem/-/pg-mem-3.0.4.tgz#6158e9ea9ce74ad1b9581e815b5fb81fa3cd62e6" + integrity sha512-1eA4/tuJLxNK/aT/vyS9ZfesCvly4KsG9J1YDHEUzDBl37koNJgldv/qytwxF9bJLCyAFHolfU374OX9fsbHEg== + dependencies: + functional-red-black-tree "^1.0.1" + immutable "^4.3.4" + json-stable-stringify "^1.0.1" + lru-cache "^6.0.0" + moment "^2.27.0" + object-hash "^2.0.3" + pgsql-ast-parser "^12.0.1" + pg-pool@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" @@ -6227,6 +6724,14 @@ pgpass@1.x: dependencies: split2 "^4.1.0" +pgsql-ast-parser@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/pgsql-ast-parser/-/pgsql-ast-parser-12.0.1.tgz#7b9d9880cb62df4b4c595c258b8e7a4b5f44ce53" + integrity sha512-pe8C6Zh5MsS+o38WlSu18NhrTjAv1UNMeDTs2/Km2ZReZdYBYtwtbWGZKK2BM2izv5CrQpbmP0oI10wvHOwv4A== + dependencies: + moo "^0.5.1" + nearley "^2.19.5" + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -6317,6 +6822,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prom-client@^15.1.3: version "15.1.3" resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2" @@ -6333,6 +6843,22 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + +properties-reader@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/properties-reader/-/properties-reader-2.3.0.tgz#f3ab84224c9535a7a36e011ae489a79a13b472b2" + integrity sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw== + dependencies: + mkdirp "^1.0.4" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -6346,6 +6872,14 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pump@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -6385,6 +6919,24 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -6412,7 +6964,7 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -readable-stream@^2.2.2: +readable-stream@^2.0.5, readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -6425,7 +6977,7 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -6434,6 +6986,24 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -6539,6 +7109,16 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -6592,7 +7172,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -6669,7 +7249,7 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-function-length@^1.2.1: +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -6835,6 +7415,11 @@ speakeasy@^2.0.0: dependencies: base32.js "0.0.1" +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + split2@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" @@ -6860,6 +7445,25 @@ sqs-producer@^3.1.1: dependencies: "@aws-sdk/client-sqs" "^3.428.0" +ssh-remote-port-forward@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz#72b0c5df8ec27ca300c75805cc6b266dee07e298" + integrity sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ== + dependencies: + "@types/ssh2" "^0.5.48" + ssh2 "^1.4.0" + +ssh2@^1.11.0, ssh2@^1.4.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.16.0.tgz#79221d40cbf4d03d07fe881149de0a9de928c9f0" + integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.20.0" + stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -6882,6 +7486,17 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +streamx@^2.15.0, streamx@^2.21.0: + version "2.21.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.1.tgz#f02979d8395b6b637d08a589fb514498bed55845" + integrity sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -6917,7 +7532,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -7049,6 +7664,47 @@ tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-fs@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" + +tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.0.0, tar-stream@^3.1.5: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + tar@^6.1.11: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" @@ -7098,6 +7754,34 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +testcontainers@^10.16.0: + version "10.16.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.16.0.tgz#8a7e69ada5cd2c6cce1c6db72b3a3e8e412fcaf6" + integrity sha512-oxPLuOtrRWS11A+Yn0+zXB7GkmNarflWqmy6CQJk8KJ75LZs2/zlUXDpizTbPpCGtk4kE2EQYwFZjrE967F8Wg== + dependencies: + "@balena/dockerignore" "^1.0.2" + "@types/dockerode" "^3.3.29" + archiver "^7.0.1" + async-lock "^1.4.1" + byline "^5.0.0" + debug "^4.3.5" + docker-compose "^0.24.8" + dockerode "^3.3.5" + get-port "^5.1.1" + proper-lockfile "^4.1.2" + properties-reader "^2.3.0" + ssh-remote-port-forward "^1.0.4" + tar-fs "^3.0.6" + tmp "^0.2.3" + undici "^5.28.4" + +text-decoder@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== + dependencies: + b4a "^1.6.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -7129,6 +7813,11 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -7253,6 +7942,11 @@ tslib@^1.11.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -7341,6 +8035,13 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici@^5.28.4: + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + dependencies: + "@fastify/busboy" "^2.0.0" + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -7616,6 +8317,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.2.2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== + yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -7661,3 +8367,12 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zip-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" + integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== + dependencies: + archiver-utils "^5.0.0" + compress-commons "^6.0.2" + readable-stream "^4.0.0" From 2c61a5e117b19da79faf69b4fb121643c19aaed3 Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Mon, 13 Jan 2025 15:34:32 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feature:=20pg-mem,=20testcontainers/redis?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/broker/consumer/consumer.module.ts | 22 +++--- .../image/application/image.consumer.ts | 3 +- test/medium/auth.controller.test.ts | 74 ++++++++----------- test/mock/fake.config.service.ts | 3 +- test/mock/pg.helper.ts | 44 ----------- test/mock/pg.test-helper.ts | 63 ++++++++++++++++ test/mock/redis.test-helper.ts | 44 +++++++++++ 7 files changed, 155 insertions(+), 98 deletions(-) delete mode 100644 test/mock/pg.helper.ts create mode 100644 test/mock/pg.test-helper.ts create mode 100644 test/mock/redis.test-helper.ts diff --git a/src/common/broker/consumer/consumer.module.ts b/src/common/broker/consumer/consumer.module.ts index 320ba08..df924b1 100644 --- a/src/common/broker/consumer/consumer.module.ts +++ b/src/common/broker/consumer/consumer.module.ts @@ -23,16 +23,18 @@ import { SqsOptions } from '@ssut/nestjs-sqs/dist/sqs.types'; }); return { - consumers: [ - { - name: 's3-image-object-created', - queueUrl: ( - configService.get(`sqs/url/${sqsName.s3ImageCreated}`) - ), - region: configService.get('AWS_REGION'), - sqs: sqsClient, - }, - ], + consumers: configService.get(`sqs/url/${sqsName.s3ImageCreated}`) + ? [ + { + name: 's3-image-object-created', + queueUrl: ( + configService.get(`sqs/url/${sqsName.s3ImageCreated}`) + ), + region: configService.get('AWS_REGION'), + sqs: sqsClient, + }, + ] + : [], }; }, }), diff --git a/src/common/image/application/image.consumer.ts b/src/common/image/application/image.consumer.ts index 43ee38f..cf3f48d 100644 --- a/src/common/image/application/image.consumer.ts +++ b/src/common/image/application/image.consumer.ts @@ -88,8 +88,9 @@ export class ImageConsumer { @SqsConsumerEventHandler('s3-image-object-created', 'error') public async errorHandler(error: Error, message: Message): Promise { + this.logger.error(`Error: ${error.message}`) this.logger.error( - `Error occurred while processing message: ${message.MessageId}`, + `Error occurred while processing message: ${message?.MessageId}`, ); } diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index fd3e645..b4a995b 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -4,7 +4,7 @@ import { INestApplication } from '@nestjs/common'; import { AuthController } from '../../src/auth/presentation/auth.controller'; import { AuthService } from '../../src/auth/application/auth.service'; import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { PassportModule } from '@nestjs/passport'; import { SmsModule } from '../../src/common/sender/sms/sms.module'; import { UserModule } from '../../src/auth/user.module'; @@ -13,44 +13,31 @@ import { DriverModule } from '../../src/driver/driver.module'; import { SecurityModule } from '../../src/auth/application/security.module'; import { BusinessModule } from '../../src/business/business.module'; import { CustomerModule } from '../../src/customer/customer.module'; -import { RedisModule } from '@liaoliaots/nestjs-redis'; import { ImageModule } from '../../src/common/image/image.module'; import { CLOUD_STORAGE } from '../../src/common/cloud/aws/s3/application/s3.service'; import { FakeCloudStorageService } from '../mock/fake.cloud-storage.service'; import { CloudModule } from '../../src/common/cloud/cloud.module'; -import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis'; -import { IBackup, newDb } from 'pg-mem'; +import { DataSource } from 'typeorm'; +import { FakeConfigService } from '../mock/fake.config.service'; +import { HttpStatusCode } from 'axios'; +import { PgTestHelper } from '../mock/pg.test-helper'; +import { RedisTestHelper } from '../mock/redis.test-helper'; describe('AuthController E2E 테스트', () => { - let redisContainer: StartedRedisContainer; let app: INestApplication; - let backup: IBackup; + + let redisTestHelper: RedisTestHelper; + let pgTestHelper: PgTestHelper; beforeEach(async () => { - jest.setTimeout(30000); // 30초 타임아웃 설정 (기본값은 5초) // Redis Testcontainers 설정 - redisContainer = await new RedisContainer().start(); - const redisHost = redisContainer.getHost(); - const redisPort = redisContainer.getMappedPort(6379); - - // pg-mem 설정 - const db = newDb(); - db.public.registerFunction({ - name: 'current_database', - implementation: () => 'test_database', - }); + redisTestHelper = new RedisTestHelper(); + await redisTestHelper.start(); - db.public.registerFunction({ - name: 'version', - implementation: () => '13.3', - }); + pgTestHelper = new PgTestHelper(); - const dataSource = await db.adapters.createTypeormDataSource({ - type: 'postgres', - entities: [__dirname + '/../../src/**/*.entity.{ts,js}'], - synchronize: false, - }); - await dataSource.initialize(); + // pg-mem 설정 + const { db, dataSource } = await pgTestHelper.connect(); const moduleFutures = await Test.createTestingModule({ imports: [ @@ -59,15 +46,11 @@ describe('AuthController E2E 테스트', () => { useFactory: async () => ({ secret: 'test-secret' }), }), PassportModule.register({ defaultStrategy: 'access' }), + await pgTestHelper.module(), + await redisTestHelper.module(), SmsModule, UserModule, CacheModule, - RedisModule.forRootAsync({ - useFactory: async () => ({ - config: { host: redisHost, port: redisPort }, - readyLog: true, - }), - }), DriverModule, SecurityModule, BusinessModule, @@ -78,28 +61,35 @@ describe('AuthController E2E 테스트', () => { controllers: [AuthController], providers: [AuthService], }) + .overrideProvider(ConfigService) + .useClass(FakeConfigService) .overrideProvider(CLOUD_STORAGE) .useClass(FakeCloudStorageService) - .overrideProvider('TypeORMDataSource') // 데이터베이스 프로바이더 오버라이드 + .overrideProvider(DataSource) // 데이터베이스 프로바이더 오버라이드 .useValue(dataSource) // pg-mem 데이터 소스 주입 .compile(); - backup = db.backup(); - app = moduleFutures.createNestApplication(); await app.init(); }); + afterEach(async () => { + pgTestHelper.restore(); + }); + afterAll(async () => { - await redisContainer.stop(); + await redisTestHelper.stop(); + await pgTestHelper.disconnect(); await app.close(); - backup.restore(); }); - test('e2e test ', async () => { - await request(app.getHttpServer()) + test('빈값으로 보내면 Bad Request가 반환된다.', async () => { + const response = await request(app.getHttpServer()) .post('/v1/auth/login') - .expect(200) - .expect('Hello World!'); + .expect(HttpStatusCode.BadRequest); + + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); }); }); diff --git a/test/mock/fake.config.service.ts b/test/mock/fake.config.service.ts index 9d5f4a4..1a67193 100644 --- a/test/mock/fake.config.service.ts +++ b/test/mock/fake.config.service.ts @@ -13,7 +13,8 @@ export class FakeConfigService extends ConfigService { this.configMap['jwt/refresh/secret'] = 'refresh_secret'; this.configMap['jwt/refresh/expire'] = 60 * 60 * 24 * 14; this.configMap['datasource/redis'] = '{"host":"localhost","port":6379}'; - this.configMap['datasource/db'] = '{"type":"mysql","host":"localhost","port":3306,"username":"root","password":"root","database":"test"}'; + this.configMap['datasource/db'] = + '{"type":"mysql","host":"localhost","port":3306,"username":"root","password":"root","database":"test"}'; } override get(key: string): any { diff --git a/test/mock/pg.helper.ts b/test/mock/pg.helper.ts deleted file mode 100644 index 8f49f86..0000000 --- a/test/mock/pg.helper.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IBackup, IMemoryDb, newDb } from 'pg-mem'; -import { Connection } from 'typeorm'; - -export class PgTestHelper { - db: IMemoryDb; - connection: Connection; - backup: IBackup; - - async connect(entities?: any[]) { - this.db = newDb({ autoCreateForeignKeyIndices: true }); - this.db.public.registerFunction({ - implementation: () => 'test', - name: 'current_database', - }); - this.db.public.registerFunction({ - implementation: () => 'test', - name: 'version', - }); - this.connection = await this.db.adapters.createTypeormDataSource({ - type: 'postgres', - username: 'test', - password: 'test', - entities: entities ?? __dirname + '/../../src/**/*.entity.{ts,js}', - logger: 'advanced-console', - logging: true, - synchronize: true, - }); - await this.sync(); - this.backup = this.db.backup(); - return this.connection; - } - - restore() { - this.backup.restore(); - } - - async disconnect() { - await this.connection.close(); - } - - async sync() { - await this.connection.synchronize(); - } -} diff --git a/test/mock/pg.test-helper.ts b/test/mock/pg.test-helper.ts new file mode 100644 index 0000000..68081ab --- /dev/null +++ b/test/mock/pg.test-helper.ts @@ -0,0 +1,63 @@ +import { IBackup, IMemoryDb, newDb } from 'pg-mem'; +import { DataSource } from 'typeorm'; +import { DynamicModule } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +export class PgTestHelper { + db: IMemoryDb; + dataSource: DataSource; + backup: IBackup; + + async connect( + entities?: [any], + synchronize: boolean = false, + ): Promise<{ db: IMemoryDb; dataSource: any }> { + // pg-mem 설정 + this.db = newDb(); + this.db.public.registerFunction({ + name: 'current_database', + implementation: () => 'test_database', + }); + + this.db.public.registerFunction({ + name: 'version', + implementation: () => '13.3', + }); + const dataSource = await this.db.adapters.createTypeormDataSource({ + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'test', + password: 'test', + database: 'test', + entities: entities ?? [__dirname + '/../../src/**/*.entity.{ts,js}'], + synchronize: synchronize, + }); + + this.backup = this.db.backup(); + + return { db: this.db, dataSource: dataSource }; + } + + restore() { + this.backup.restore(); + } + + async disconnect() { + await this.dataSource.destroy(); + } + + async sync() { + await this.dataSource.synchronize(); + } + + async module(): Promise { + return TypeOrmModule.forRootAsync({ + useFactory: async () => { + return { + type: 'postgres', + }; + }, + }); + } +} diff --git a/test/mock/redis.test-helper.ts b/test/mock/redis.test-helper.ts new file mode 100644 index 0000000..9811930 --- /dev/null +++ b/test/mock/redis.test-helper.ts @@ -0,0 +1,44 @@ +import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis'; +import { RedisModule } from '@liaoliaots/nestjs-redis'; +import { DynamicModule } from '@nestjs/common'; + +export class RedisTestHelper { + redisContainer: StartedRedisContainer; + host: string; + port: number; + + constructor() {} + + async start(): Promise<{ + container: StartedRedisContainer; + host: string; + port: number; + }> { + // Redis Testcontainers 설정 + this.redisContainer = await new RedisContainer().start(); + this.host = this.redisContainer.getHost(); + this.port = this.redisContainer.getMappedPort(6379); + + return { + container: this.redisContainer, + host: this.host, + port: this.port, + }; + } + + async stop() { + await this.redisContainer.stop(); + } + + async module(): Promise { + return RedisModule.forRootAsync({ + useFactory: async () => ({ + config: { + host: this.host, + port: this.port, + }, + readyLog: true, + }), + }); + } +} From 51c03d906b173895aa3a8a1b7e8e4444c763d153 Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Tue, 14 Jan 2025 12:14:54 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feature:=20pg-mem=20=EC=A2=85=EB=A3=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20schema=20init=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/application/auth.service.spec.ts | 2 +- src/auth/application/security.service.ts | 2 +- src/common/entity/user.entity.ts | 13 ++ src/customer/customer.domain.ts | 3 +- src/schemas/business.entity.ts | 21 +-- src/schemas/customer.entity.ts | 10 +- src/schemas/drivers.entity.ts | 25 +--- test/medium/auth.controller.test.ts | 69 ++++++++- test/mock/pg.test-helper.ts | 172 ++++++++++++++++++++-- test/mock/redis.test-helper.ts | 13 +- 10 files changed, 274 insertions(+), 56 deletions(-) create mode 100644 src/common/entity/user.entity.ts diff --git a/src/auth/application/auth.service.spec.ts b/src/auth/application/auth.service.spec.ts index e40c293..de68a78 100644 --- a/src/auth/application/auth.service.spec.ts +++ b/src/auth/application/auth.service.spec.ts @@ -57,7 +57,7 @@ describe('AuthService', () => { .spyOn(jwtService, 'sign') .mockReturnValueOnce('test-access-token') .mockReturnValueOnce('test-refresh-token'); - jest.spyOn(cacheService, 'set').mockResolvedValueOnce(undefined); + jest.spyOn(cacheService, 'set').mockResolvedValueOnce('OK'); jest.spyOn(userService, 'update').mockResolvedValueOnce(userDto); const result = await service.login(userDto); diff --git a/src/auth/application/security.service.ts b/src/auth/application/security.service.ts index 5581db6..6ad535b 100644 --- a/src/auth/application/security.service.ts +++ b/src/auth/application/security.service.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import * as speakeasy from 'speakeasy'; import { ConfigService } from '@nestjs/config'; -export const SECURITY_SERVICE = 'SECURITY_SERVICE'; +export const SECURITY_SERVICE = Symbol('SECURITY_SERVICE'); export interface ISecurityService { encrypt(text: string): string | undefined; diff --git a/src/common/entity/user.entity.ts b/src/common/entity/user.entity.ts new file mode 100644 index 0000000..925ed9f --- /dev/null +++ b/src/common/entity/user.entity.ts @@ -0,0 +1,13 @@ +import { Column } from 'typeorm'; +import { HasUuid } from './parent.entity'; +import { AuthProvider } from '../../auth/presentation/user.dto'; + +export abstract class UserEntity extends HasUuid { + @Column({ + type: 'enum', + enum: AuthProvider, + enumName: 'auth_provider', + nullable: false, + }) + authProvider: AuthProvider; +} diff --git a/src/customer/customer.domain.ts b/src/customer/customer.domain.ts index 8712775..f170be3 100644 --- a/src/customer/customer.domain.ts +++ b/src/customer/customer.domain.ts @@ -12,6 +12,7 @@ import { IUUIDHolder } from '../common/holder/uuid.holders'; import { IDateHolder } from '../common/holder/date.holder'; import { PresignedUrlDto } from '../common/cloud/aws/s3/presentation/presigned-url.dto'; import { ImageDto } from '../common/image/presentation/image.dto'; +import { BadRequestException } from '@nestjs/common/exceptions'; export interface ICustomer extends AuthDto { customerId?: number; @@ -59,7 +60,7 @@ export class Customer implements ICustomer, UserDto { dateHolder: IDateHolder, ): Customer { if ((!customer.customerName && !customer.name) || !customer.authProvider) { - throw new Error('필수 정보가 누락되었습니다.'); + throw new BadRequestException('필수 정보가 누락되었습니다.'); } return Builder() diff --git a/src/schemas/business.entity.ts b/src/schemas/business.entity.ts index 1547493..3c19023 100644 --- a/src/schemas/business.entity.ts +++ b/src/schemas/business.entity.ts @@ -1,12 +1,12 @@ import { - Entity, - PrimaryColumn, Column, - OneToMany, CreateDateColumn, DeleteDateColumn, - UpdateDateColumn, + Entity, + OneToMany, Point, + PrimaryColumn, + UpdateDateColumn } from 'typeorm'; import { Appointment } from './appointments.entity'; import { BusinessBadge } from './business-badges.entity'; @@ -17,11 +17,10 @@ import { Favorite } from './favorites.entity'; import { Review } from './reviews.entity'; import { ServiceOption } from './service-options.entity'; import { BusinessChatRoom } from './business-chat-room.entity'; -import { HasUuid } from '../common/entity/parent.entity'; -import { AuthProvider } from '../auth/presentation/user.dto'; +import { UserEntity } from '../common/entity/user.entity'; @Entity({ name: 'business' }) -export class BusinessEntity extends HasUuid { +export class BusinessEntity extends UserEntity { @PrimaryColumn() businessId: number; @@ -94,14 +93,6 @@ export class BusinessEntity extends HasUuid { @OneToMany(() => BusinessChatRoom, (chatRooms) => chatRooms.chatRoom) chatRooms: BusinessChatRoom[]; - @Column({ - type: 'enum', - enum: AuthProvider, - enumName: 'auth_provider', - nullable: false, - }) - authProvider: AuthProvider; - @Column({ length: 20, unique: true, nullable: true }) refreshToken?: string; } diff --git a/src/schemas/customer.entity.ts b/src/schemas/customer.entity.ts index c514f1e..ddccbc3 100644 --- a/src/schemas/customer.entity.ts +++ b/src/schemas/customer.entity.ts @@ -6,23 +6,22 @@ import { OneToMany, Point, PrimaryGeneratedColumn, - UpdateDateColumn, + UpdateDateColumn } from 'typeorm'; import { Appointment } from './appointments.entity'; import { Favorite } from './favorites.entity'; import { Pet } from './pets.entity'; import { Review } from './reviews.entity'; import { CustomerChatRoom } from './customer-chat-room.entity'; -import { HasUuid } from '../common/entity/parent.entity'; -import { AuthProvider } from '../auth/presentation/user.dto'; import { ImageEntity } from './image.entity'; import { Customer } from '../customer/customer.domain'; import { Builder } from 'builder-pattern'; import { IUUIDHolder } from '../common/holder/uuid.holders'; import { IDateHolder } from '../common/holder/date.holder'; +import { UserEntity } from '../common/entity/user.entity'; @Entity({ name: 'customers' }) -export class CustomerEntity extends HasUuid { +export class CustomerEntity extends UserEntity { @PrimaryGeneratedColumn() customerId: number; @@ -46,9 +45,6 @@ export class CustomerEntity extends HasUuid { }) customerLocation?: Point; - @Column({ type: 'enum', enum: AuthProvider, nullable: false }) - authProvider: AuthProvider; - @CreateDateColumn({ nullable: false }) createdAt: Date; diff --git a/src/schemas/drivers.entity.ts b/src/schemas/drivers.entity.ts index 85ed4e7..54a017b 100644 --- a/src/schemas/drivers.entity.ts +++ b/src/schemas/drivers.entity.ts @@ -1,22 +1,21 @@ import { - Entity, - PrimaryColumn, Column, - ManyToOne, - JoinColumn, - OneToMany, CreateDateColumn, DeleteDateColumn, - UpdateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryColumn, + UpdateDateColumn } from 'typeorm'; import { Appointment } from './appointments.entity'; import { BusinessEntity } from './business.entity'; import { DriverChatRoom } from './driver-chat-room.entity'; -import { HasUuid } from '../common/entity/parent.entity'; -import { AuthProvider } from '../auth/presentation/user.dto'; +import { UserEntity } from '../common/entity/user.entity'; @Entity({ name: 'drivers' }) -export class DriverEntity extends HasUuid { +export class DriverEntity extends UserEntity { @PrimaryColumn() driverId: number; @@ -45,14 +44,6 @@ export class DriverEntity extends HasUuid { @JoinColumn({ name: 'business_id' }) business: BusinessEntity; - @Column({ - type: 'enum', - enum: AuthProvider, - enumName: 'auth_provider', - nullable: false, - }) - authProvider: AuthProvider; - @Column({ length: 20, unique: true, nullable: true }) refreshToken?: string; } diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index b4a995b..a7ddd7b 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -28,8 +28,9 @@ describe('AuthController E2E 테스트', () => { let redisTestHelper: RedisTestHelper; let pgTestHelper: PgTestHelper; + let dataSource: DataSource; - beforeEach(async () => { + beforeAll(async () => { // Redis Testcontainers 설정 redisTestHelper = new RedisTestHelper(); await redisTestHelper.start(); @@ -37,7 +38,9 @@ describe('AuthController E2E 테스트', () => { pgTestHelper = new PgTestHelper(); // pg-mem 설정 - const { db, dataSource } = await pgTestHelper.connect(); + const db = await pgTestHelper.connect(); + + dataSource = db.dataSource; const moduleFutures = await Test.createTestingModule({ imports: [ @@ -91,5 +94,67 @@ describe('AuthController E2E 테스트', () => { const body = response.body; expect(body.error).toEqual('Bad Request'); expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('userType이 없으면 Bad Request가 반환된다.', async () => { + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + phoneNumber: '01012345678', + password: 'password', + }) + .expect(HttpStatusCode.BadRequest); + + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('name이 없으면 Bad Request가 반환된다.', async () => { + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + userType: 'customer', + phoneNumber: '01012345678', + }) + .expect(HttpStatusCode.BadRequest); + + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('phoneNumber이 없으면 Bad Request가 반환된다.', async () => { + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + userType: 'customer', + name: 'test', + }) + .expect(HttpStatusCode.BadRequest); + + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('사용자가 없으면 사용자가 생성된다.', async () => { + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + userType: 'customer', + uuid: 'test', + name: 'test', + }) + .expect(HttpStatusCode.Created); + + const body = response.body; + expect(body.accessToken).toBeDefined(); + expect(body.refreshToken).toBeDefined(); + console.table(body); }); }); diff --git a/test/mock/pg.test-helper.ts b/test/mock/pg.test-helper.ts index 68081ab..6840cc0 100644 --- a/test/mock/pg.test-helper.ts +++ b/test/mock/pg.test-helper.ts @@ -1,17 +1,20 @@ -import { IBackup, IMemoryDb, newDb } from 'pg-mem'; +import { DataType, IBackup, IMemoryDb, newDb } from 'pg-mem'; import { DataSource } from 'typeorm'; -import { DynamicModule } from '@nestjs/common'; +import { DynamicModule, Logger } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; export class PgTestHelper { + private readonly logger = new Logger(PgTestHelper.name); db: IMemoryDb; dataSource: DataSource; backup: IBackup; async connect( - entities?: [any], + entities?: any[], synchronize: boolean = false, ): Promise<{ db: IMemoryDb; dataSource: any }> { + this.logger.log('Connecting to the database'); // pg-mem 설정 this.db = newDb(); this.db.public.registerFunction({ @@ -19,11 +22,7 @@ export class PgTestHelper { implementation: () => 'test_database', }); - this.db.public.registerFunction({ - name: 'version', - implementation: () => '13.3', - }); - const dataSource = await this.db.adapters.createTypeormDataSource({ + this.dataSource = await this.db.adapters.createTypeormDataSource({ type: 'postgres', host: 'localhost', port: 5432, @@ -32,23 +31,175 @@ export class PgTestHelper { database: 'test', entities: entities ?? [__dirname + '/../../src/**/*.entity.{ts,js}'], synchronize: synchronize, + namingStrategy: new SnakeNamingStrategy(), + }); + + this.db.registerExtension('postgis', (schema) => { + schema.registerEquivalentType({ + name: 'geometry', + equivalentTo: DataType.text, + isValid: (geo: any) => typeof geo === 'object' && geo.type === 'Point', + }); + + // Register geography type with point and SRID 4326 + schema.registerEquivalentType({ + name: 'geography(Point,4326)', + equivalentTo: DataType.jsonb, + isValid: (geo: any) => typeof geo === 'object' && geo.type === 'Point', + }); + + // Register Point type explicitly + schema.registerEquivalentType({ + name: 'point', + equivalentTo: DataType.jsonb, + isValid: (geo: any) => typeof geo === 'object' && geo.type === 'Point', + }); + + schema.registerFunction({ + name: 'ST_AsGeoJSON', + implementation: (geo: any) => geo, + }); + }); + + this.db.public.registerFunction({ + name: 'version', + implementation: () => '13.3', }); + this.db.public.registerFunction({ + name: 'ST_AsGeoJSON', + implementation: (geo: any) => geo, + }); + + this.db.public.none(` + CREATE TYPE auth_provider AS ENUM ('KAKAO', 'APPLE', 'GOOGLE', 'BASIC'); +CREATE TYPE geometry AS ENUM ('POINT', 'LINESTRING', 'POLYGON', 'MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION'); + +CREATE TYPE gender AS ENUM ('MALE', 'FEMALE'); + + +CREATE TYPE message_type AS ENUM ('TEXT', 'IMAGE', 'VIDEO', 'AUDIO'); + + +CREATE TYPE appointment_status AS ENUM ('CONFIRMED', 'COMPLETED', 'CANCELLED'); + + +CREATE TYPE checklist_type AS ENUM ('choice', 'answer'); + + +CREATE TYPE pet_checklist_category AS ENUM ('health', 'food', 'grooming', 'personality', 'other'); + + +CREATE TABLE IF NOT EXISTS spatial_ref_sys +( + srid INTEGER NOT NULL + PRIMARY KEY + CONSTRAINT spatial_ref_sys_srid_check + CHECK ((srid > 0) AND (srid <= 998999)), + auth_name VARCHAR(256), + auth_srid INTEGER, + srtext VARCHAR(2048), + proj4text VARCHAR(2048) +); + +CREATE TABLE IF NOT EXISTS images +( + image_id SERIAL + PRIMARY KEY, + uuid VARCHAR(44) NOT NULL, + image_url TEXT NOT NULL + CONSTRAINT images_image_link_unique + UNIQUE, + created_at TIMESTAMP DEFAULT NOW() NOT NULL +); + + +CREATE TABLE IF NOT EXISTS customers +( + customer_id SERIAL + PRIMARY KEY, + uuid VARCHAR(44) NOT NULL + UNIQUE, + customer_name VARCHAR(30) NOT NULL, + customer_phone_number TEXT, + customer_location geometry, + auth_provider auth_provider NOT NULL, + created_at TIMESTAMP DEFAULT NOW() NOT NULL, + modified_at TIMESTAMP DEFAULT NOW() NOT NULL, + deleted_at TIMESTAMP, + refresh_token TEXT, + customer_detail_address TEXT, + customer_address TEXT +); + +COMMENT ON TABLE customers IS '고객'; + +COMMENT ON COLUMN customers.customer_id IS '고객ID'; + +COMMENT ON COLUMN customers.uuid IS '식별자 / 외부 서비스에서 사용'; + +COMMENT ON COLUMN customers.customer_name IS '고객이름'; + +COMMENT ON COLUMN customers.customer_phone_number IS '고객 연락처'; + +COMMENT ON COLUMN customers.customer_location IS '고객위치'; + +COMMENT ON COLUMN customers.auth_provider IS '인증 제공자 (OAuth)'; + +COMMENT ON COLUMN customers.created_at IS '생성일시'; + +COMMENT ON COLUMN customers.modified_at IS '수정일시'; + +COMMENT ON COLUMN customers.deleted_at IS '삭제일시'; + +COMMENT ON COLUMN customers.refresh_token IS 'JWT 리프레시 토큰'; + +COMMENT ON COLUMN customers.customer_detail_address IS '고객 상세 주소'; + +COMMENT ON COLUMN customers.customer_address IS '고객 주소'; + + +CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_refresh_token + ON customers (refresh_token); + + `); + + if (!this.dataSource.isInitialized) { + await this.dataSource.initialize(); + this.logger.log('Initialized the database'); + } + this.backup = this.db.backup(); - return { db: this.db, dataSource: dataSource }; + this.logger.log('Connected to the database'); + + return { db: this.db, dataSource: this.dataSource }; + } + + async query(query: string) { + const client = this.db.adapters.createPgPromise(); + + await client.query(query); } restore() { + this.logger.log('Restoring the database'); this.backup.restore(); + this.logger.log('Restored the database'); } async disconnect() { - await this.dataSource.destroy(); + if (this.dataSource.isInitialized) { + this.logger.log('Disconnecting from the database'); + await this.dataSource.destroy(); + this.logger.log('Disconnected from the database'); + } } async sync() { + this.logger.log('Synchronizing the database'); await this.dataSource.synchronize(); + this.logger.log('Synchronized the database'); } async module(): Promise { @@ -56,6 +207,7 @@ export class PgTestHelper { useFactory: async () => { return { type: 'postgres', + logging: true, }; }, }); diff --git a/test/mock/redis.test-helper.ts b/test/mock/redis.test-helper.ts index 9811930..3e364bc 100644 --- a/test/mock/redis.test-helper.ts +++ b/test/mock/redis.test-helper.ts @@ -1,8 +1,9 @@ import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis'; import { RedisModule } from '@liaoliaots/nestjs-redis'; -import { DynamicModule } from '@nestjs/common'; +import { DynamicModule, Logger } from '@nestjs/common'; export class RedisTestHelper { + private readonly logger = new Logger(RedisTestHelper.name); redisContainer: StartedRedisContainer; host: string; port: number; @@ -14,11 +15,13 @@ export class RedisTestHelper { host: string; port: number; }> { + this.logger.log('Starting Redis Testcontainers'); // Redis Testcontainers 설정 this.redisContainer = await new RedisContainer().start(); this.host = this.redisContainer.getHost(); this.port = this.redisContainer.getMappedPort(6379); + this.logger.log('Started Redis Testcontainers'); return { container: this.redisContainer, host: this.host, @@ -27,7 +30,13 @@ export class RedisTestHelper { } async stop() { - await this.redisContainer.stop(); + this.logger.log('Stopping Redis Testcontainers'); + try { + await this.redisContainer.stop(); + this.logger.log('Stopped Redis Testcontainers'); + } catch (error) { + this.logger.error('Error stopping Redis Testcontainers', error); + } } async module(): Promise { From 609f561c4208da0836e4fb273a4eee2f67666d52 Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Tue, 14 Jan 2025 13:11:59 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feature:=20PostgreSQL=20->=20TestContaine?= =?UTF-8?q?rs=ED=99=94=20(Postgis=20=EB=93=B1=20pg-mem=EC=9D=80=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=EC=A0=81=EC=9E=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/auth/application/auth.service.ts | 26 +---- src/auth/presentation/auth.controller.ts | 11 +- src/customer/application/customer.service.ts | 14 +-- src/customer/customer.domain.ts | 4 +- src/schemas/customer.entity.ts | 1 + test/medium/auth.controller.test.ts | 103 ++++++++++++------- test/mock/fake.config.service.ts | 6 ++ test/mock/pg.test-helper.ts | 61 +++++++++++ test/unit/customer/customer.domain.test.ts | 6 +- yarn.lock | 7 ++ 11 files changed, 155 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index eb47109..c8a1995 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.10", "@ssut/nestjs-sqs": "^2.2.0", + "@testcontainers/postgresql": "^10.16.0", "@testcontainers/redis": "^10.16.0", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", diff --git a/src/auth/application/auth.service.ts b/src/auth/application/auth.service.ts index 2ca276b..1ce1595 100644 --- a/src/auth/application/auth.service.ts +++ b/src/auth/application/auth.service.ts @@ -2,20 +2,14 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { CACHE_SERVICE, ICacheService } from '../../common/cache/cache.service'; -import { - BadRequestException, - UnauthorizedException, -} from '@nestjs/common/exceptions'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common/exceptions'; import { UserService } from './user.service'; import { UserDto } from '../presentation/user.dto'; import { AuthDto } from '../presentation/auth.dto'; import { Builder } from 'builder-pattern'; import { ISecurityService, SECURITY_SERVICE } from './security.service'; import { Sender } from '../../common/sender/sender.interface'; -import { - ISmsService, - SMS_SERVICE, -} from '../../common/sender/sms/application/sms.service'; +import { ISmsService, SMS_SERVICE } from '../../common/sender/sms/application/sms.service'; @Injectable() export class AuthService { @@ -175,20 +169,11 @@ export class AuthService { if (existingToken) await this.cacheService.del(existingToken); } - await this.cacheService.set( - key, - accessToken, + const expiresIn = Math.floor( (this.accessTokenOption.expiresIn as number) / 1000, ); - await this.cacheService.set( - accessToken, - JSON.stringify({ - ...user, - refreshToken: undefined, - }), - (this.accessTokenOption.expiresIn as number) / 1000, - ); + await this.cacheService.set(key, accessToken, expiresIn); await this.cacheService.set( accessToken, @@ -196,10 +181,9 @@ export class AuthService { ...user, refreshToken: undefined, }), - (this.accessTokenOption.expiresIn as number) / 1000, + expiresIn, ); } - async getUser(token: string): Promise { const payload = await this.jwtService.verify(token); if (!payload) { diff --git a/src/auth/presentation/auth.controller.ts b/src/auth/presentation/auth.controller.ts index c874183..9d984d7 100644 --- a/src/auth/presentation/auth.controller.ts +++ b/src/auth/presentation/auth.controller.ts @@ -1,14 +1,7 @@ import { Body, Controller, HttpStatus, Post, Query, Req } from '@nestjs/common'; import { AuthService } from '../application/auth.service'; import { AuthDto, OtpRequestDto, OtpResponseDto } from './auth.dto'; -import { - ApiCreatedResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiResponse, - ApiTags, -} from '@nestjs/swagger'; +import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Auth, CurrentUser } from '../decorator/auth.decorator'; import { UserDto, UserGroup } from './user.dto'; import { GroupValidation } from '../../common/validation/validation.decorator'; @@ -38,7 +31,7 @@ export class AuthController { @Post('/login') @GroupValidation([UserGroup.login]) async login(@Body() dto: UserDto): Promise> { - return ResponseEntity.OK(await this.authService.login(dto)); + return ResponseEntity.CREATED(await this.authService.login(dto)); } @ApiOperation({ diff --git a/src/customer/application/customer.service.ts b/src/customer/application/customer.service.ts index 365cb3e..d60ea7a 100644 --- a/src/customer/application/customer.service.ts +++ b/src/customer/application/customer.service.ts @@ -3,17 +3,11 @@ import { CustomerDto } from '../presentation/customer.dto'; import { IUserService } from '../../auth/user.interface'; import { AuthDto } from '../../auth/presentation/auth.dto'; import { UserDto, UserType } from '../../auth/presentation/user.dto'; -import { - ISecurityService, - SECURITY_SERVICE, -} from '../../auth/application/security.service'; +import { ISecurityService, SECURITY_SERVICE } from '../../auth/application/security.service'; import { ImageService } from '../../common/image/application/image.service'; import { BadRequestException } from '@nestjs/common/exceptions'; import { Customer, ICustomer } from '../customer.domain'; -import { - CUSTOMER_REPOSITORY, - ICustomerRepository, -} from '../port/customer.repository'; +import { CUSTOMER_REPOSITORY, ICustomerRepository } from '../port/customer.repository'; import { IUUIDHolder, UUID_HOLDER } from '../../common/holder/uuid.holders'; import { DATE_HOLDER, IDateHolder } from '../../common/holder/date.holder'; @@ -50,9 +44,7 @@ export class CustomerService implements IUserService { } return await this.customerRepository.save( - this.customerRepository.create( - Customer.from(dto, this.uuidHolder, this.dateHolder), - ), + Customer.create(dto, this.uuidHolder, this.dateHolder), ); } diff --git a/src/customer/customer.domain.ts b/src/customer/customer.domain.ts index f170be3..224e96e 100644 --- a/src/customer/customer.domain.ts +++ b/src/customer/customer.domain.ts @@ -54,7 +54,7 @@ export class Customer implements ICustomer, UserDto { presignedUrlDto?: PresignedUrlDto; profileImage?: ImageDto; - static from( + static create( customer: CustomerDto, uuidHolder: IUUIDHolder, dateHolder: IDateHolder, @@ -64,7 +64,7 @@ export class Customer implements ICustomer, UserDto { } return Builder() - .uuid(uuidHolder.generatedUuid()) + .uuid(customer.uuid ?? uuidHolder.generatedUuid()) .customerName(customer.customerName ?? customer.name) .authProvider(customer.authProvider) .createdAt(dateHolder.now()) diff --git a/src/schemas/customer.entity.ts b/src/schemas/customer.entity.ts index ddccbc3..2ae888f 100644 --- a/src/schemas/customer.entity.ts +++ b/src/schemas/customer.entity.ts @@ -97,6 +97,7 @@ export class CustomerEntity extends UserEntity { static toModel(customer: CustomerEntity): Customer { return Builder() + .uuid(customer.uuid) .customerId(customer.customerId) .customerName(customer.customerName) .customerPhoneNumber(customer.customerPhoneNumber) diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index a7ddd7b..4f54716 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -17,30 +17,28 @@ import { ImageModule } from '../../src/common/image/image.module'; import { CLOUD_STORAGE } from '../../src/common/cloud/aws/s3/application/s3.service'; import { FakeCloudStorageService } from '../mock/fake.cloud-storage.service'; import { CloudModule } from '../../src/common/cloud/cloud.module'; -import { DataSource } from 'typeorm'; import { FakeConfigService } from '../mock/fake.config.service'; import { HttpStatusCode } from 'axios'; -import { PgTestHelper } from '../mock/pg.test-helper'; +import { PostGisHelper } from '../mock/pg.test-helper'; import { RedisTestHelper } from '../mock/redis.test-helper'; +import { UUID_HOLDER } from '../../src/common/holder/uuid.holders'; +import { FakeUuidHolder } from '../mock/fake.holder'; describe('AuthController E2E 테스트', () => { let app: INestApplication; let redisTestHelper: RedisTestHelper; - let pgTestHelper: PgTestHelper; - let dataSource: DataSource; + let pgTestHelper: PostGisHelper; + const uuidHolder = new FakeUuidHolder(); beforeAll(async () => { // Redis Testcontainers 설정 redisTestHelper = new RedisTestHelper(); await redisTestHelper.start(); - pgTestHelper = new PgTestHelper(); - - // pg-mem 설정 - const db = await pgTestHelper.connect(); - - dataSource = db.dataSource; + // PostGis Testcontainers 설정 + pgTestHelper = new PostGisHelper(); + await pgTestHelper.start(); const moduleFutures = await Test.createTestingModule({ imports: [ @@ -64,33 +62,31 @@ describe('AuthController E2E 테스트', () => { controllers: [AuthController], providers: [AuthService], }) + .overrideProvider(UUID_HOLDER) + .useValue(uuidHolder) .overrideProvider(ConfigService) .useClass(FakeConfigService) .overrideProvider(CLOUD_STORAGE) .useClass(FakeCloudStorageService) - .overrideProvider(DataSource) // 데이터베이스 프로바이더 오버라이드 - .useValue(dataSource) // pg-mem 데이터 소스 주입 .compile(); app = moduleFutures.createNestApplication(); await app.init(); - }); - - afterEach(async () => { - pgTestHelper.restore(); - }); + }, 30000); afterAll(async () => { await redisTestHelper.stop(); - await pgTestHelper.disconnect(); + await pgTestHelper.stop(); await app.close(); }); test('빈값으로 보내면 Bad Request가 반환된다.', async () => { + // Given const response = await request(app.getHttpServer()) .post('/v1/auth/login') .expect(HttpStatusCode.BadRequest); + // Then const body = response.body; expect(body.error).toEqual('Bad Request'); expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); @@ -98,14 +94,19 @@ describe('AuthController E2E 테스트', () => { }); test('userType이 없으면 Bad Request가 반환된다.', async () => { + // Given + const user = { + phoneNumber: '01012345678', + password: 'password', + }; + + // When const response = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ - phoneNumber: '01012345678', - password: 'password', - }) + .send(user) .expect(HttpStatusCode.BadRequest); + // Then const body = response.body; expect(body.error).toEqual('Bad Request'); expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); @@ -113,14 +114,19 @@ describe('AuthController E2E 테스트', () => { }); test('name이 없으면 Bad Request가 반환된다.', async () => { + // Given + const user = { + userType: 'customer', + phoneNumber: '01012345678', + }; + + // When const response = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ - userType: 'customer', - phoneNumber: '01012345678', - }) + .send(user) .expect(HttpStatusCode.BadRequest); + // Then const body = response.body; expect(body.error).toEqual('Bad Request'); expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); @@ -128,33 +134,52 @@ describe('AuthController E2E 테스트', () => { }); test('phoneNumber이 없으면 Bad Request가 반환된다.', async () => { + // Given + const user = { + userType: 'customer', + name: 'test', + }; + + // When const response = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ - userType: 'customer', - name: 'test', - }) + .send(user) .expect(HttpStatusCode.BadRequest); + // Then const body = response.body; expect(body.error).toEqual('Bad Request'); expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); console.table(body); }); - test('사용자가 없으면 사용자가 생성된다.', async () => { + test('정상적인 요청이면 Created가 반환된다.', async () => { + // Given + const user = { + authProvider: 'KAKAO', + userType: 'customer', + uuid: 'test', + name: 'test', + }; + + // when const response = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ - userType: 'customer', - uuid: 'test', - name: 'test', - }) + .send(user) .expect(HttpStatusCode.Created); const body = response.body; - expect(body.accessToken).toBeDefined(); - expect(body.refreshToken).toBeDefined(); - console.table(body); + const data = body.data; + + // then + expect(data).toBeDefined(); + expect(data.uuid).toBe(user.uuid); + expect(data.name).toBe(user.name); + expect(data.userId).toBeDefined(); + expect(data.userType).toBe(user.userType); + expect(data.authProvider).toBe(user.authProvider); + expect(data.accessToken).toBeDefined(); + expect(data.refreshToken).toBeDefined(); + console.table(data); }); }); diff --git a/test/mock/fake.config.service.ts b/test/mock/fake.config.service.ts index 1a67193..c34e541 100644 --- a/test/mock/fake.config.service.ts +++ b/test/mock/fake.config.service.ts @@ -15,6 +15,12 @@ export class FakeConfigService extends ConfigService { this.configMap['datasource/redis'] = '{"host":"localhost","port":6379}'; this.configMap['datasource/db'] = '{"type":"mysql","host":"localhost","port":3306,"username":"root","password":"root","database":"test"}'; + + this.configMap['jwt/access/secret'] = 'test-access'; + this.configMap['jwt/access/expire'] = 3600; + this.configMap['jwt/refresh/secret'] = 'test-refresh'; + this.configMap['jwt/refresh/expire'] = 60 * 60 * 24 * 14; + } override get(key: string): any { diff --git a/test/mock/pg.test-helper.ts b/test/mock/pg.test-helper.ts index 6840cc0..eb0341a 100644 --- a/test/mock/pg.test-helper.ts +++ b/test/mock/pg.test-helper.ts @@ -3,6 +3,7 @@ import { DataSource } from 'typeorm'; import { DynamicModule, Logger } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; export class PgTestHelper { private readonly logger = new Logger(PgTestHelper.name); @@ -213,3 +214,63 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_refresh_token }); } } + +export class PostGisHelper { + private readonly logger = new Logger(PostGisHelper.name); + postgisContainer: StartedPostgreSqlContainer; + host: string; + port: number; + + constructor() {} + + async start(): Promise<{ + container: StartedPostgreSqlContainer; + host: string; + port: number; + }> { + this.logger.log('Starting PostGIS Testcontainers'); + // PostGIS Testcontainers 설정 + this.postgisContainer = await new PostgreSqlContainer( + 'postgis/postgis:12-3.0', + ).start(); + + this.host = this.postgisContainer.getHost(); + this.port = this.postgisContainer.getMappedPort(5432); + + this.logger.log('Started PostGIS Testcontainers'); + + return { + container: this.postgisContainer, + host: this.host, + port: this.port, + }; + } + + async stop() { + this.logger.log('Stopping PostGIS Testcontainers'); + try { + await this.postgisContainer.stop(); + this.logger.log('Stopped PostGIS Testcontainers'); + } catch (error) { + this.logger.error('Error stopping PostGIS Testcontainers', error); + } + } + + async module(): Promise { + return TypeOrmModule.forRootAsync({ + useFactory: async () => ({ + type: 'postgres', + host: this.host, + port: this.port, + username: 'test', + password: 'test', + database: 'test', + entities: [__dirname + '/../../src/**/*.entity.{ts,js}'], + logger: 'advanced-console', + logging: 'all', + synchronize: true, + namingStrategy: new SnakeNamingStrategy(), + }), + }); + } +} diff --git a/test/unit/customer/customer.domain.test.ts b/test/unit/customer/customer.domain.test.ts index e17c843..837bac2 100644 --- a/test/unit/customer/customer.domain.test.ts +++ b/test/unit/customer/customer.domain.test.ts @@ -16,7 +16,7 @@ describe('Customer', () => { dateHolder = new FakeDateHolder(date); }); - describe('from', () => { + describe('create', () => { it('CustomerDto로부터 Customer 객체를 생성한다', () => { const customerDto: CustomerDto = Builder() .customerName('홍길동') @@ -25,7 +25,7 @@ describe('Customer', () => { .customerDetailAddress('역삼동') .authProvider(AuthProvider.BASIC) .build(); - const customer = Customer.from(customerDto, uuidHolder, dateHolder); + const customer = Customer.create(customerDto, uuidHolder, dateHolder); expect(customer).toBeDefined(); expect(customer.uuid).toBe('test-uuid-1'); @@ -44,7 +44,7 @@ describe('Customer', () => { .build(); expect(() => - Customer.from(customerDto, uuidHolder, dateHolder), + Customer.create(customerDto, uuidHolder, dateHolder), ).toThrow(); }); }); diff --git a/yarn.lock b/yarn.lock index a88eecc..4fc78b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2339,6 +2339,13 @@ sqs-consumer "^7.0.3" sqs-producer "^3.1.1" +"@testcontainers/postgresql@^10.16.0": + version "10.16.0" + resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.16.0.tgz#0437a9b426d64ea958e745a0e2ae19462b786f81" + integrity sha512-zWFQI+3QxlEELRvVv27i6zlVEPNUz9zKaSh7iWmFlCdfhcyr78daS0FG8FIfdQ79VK7YXA4jv+dTYXa2SwXu/w== + dependencies: + testcontainers "^10.16.0" + "@testcontainers/redis@^10.16.0": version "10.16.0" resolved "https://registry.yarnpkg.com/@testcontainers/redis/-/redis-10.16.0.tgz#e3eeeb1ec1194842e050525474a2dc3bc18f899f" From 11cbfbb9906782ada7be97e868f9e3d0a47727bc Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Tue, 14 Jan 2025 15:13:14 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feature:=20Data=20seeding=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/auth/application/auth.service.ts | 4 +- src/auth/presentation/user.dto.ts | 6 +- src/customer/application/customer.service.ts | 5 +- src/schemas/customer.entity.ts | 1 + test/medium/auth.controller.test.ts | 275 ++++++++++++------- test/mock/pg.test-helper.ts | 11 +- test/mock/seeders.ts | 39 +++ yarn.lock | 156 ++++++++++- 9 files changed, 382 insertions(+), 116 deletions(-) create mode 100644 test/mock/seeders.ts diff --git a/package.json b/package.json index c8a1995..50f62dc 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "speakeasy": "^2.0.0", "tsid-ts": "^0.0.9", "typeorm": "^0.3.20", + "typeorm-extension": "^3.6.3", "typeorm-naming-strategies": "^4.1.0" }, "devDependencies": { diff --git a/src/auth/application/auth.service.ts b/src/auth/application/auth.service.ts index 1ce1595..fc89238 100644 --- a/src/auth/application/auth.service.ts +++ b/src/auth/application/auth.service.ts @@ -56,7 +56,7 @@ export class AuthService { return user ?? (await this.userService.create(dto)); }); - user.userId = user.customerId ?? user.driverId ?? user.businessId; + user.userId = user.userId ?? user.customerId ?? user.driverId ?? user.businessId; user.userType = dto.userType; @@ -95,7 +95,7 @@ export class AuthService { return Builder(AuthDto) .uuid(user.uuid) - .name(dto.name) + .name(user.name ?? dto.name) .userId(user.userId) .userType(user.userType) .phoneNumber(user.phoneNumber) diff --git a/src/auth/presentation/user.dto.ts b/src/auth/presentation/user.dto.ts index 8bc5a59..e494e0e 100644 --- a/src/auth/presentation/user.dto.ts +++ b/src/auth/presentation/user.dto.ts @@ -68,11 +68,13 @@ export class UserDto { static from(customer: Customer): UserDto { return Builder(UserDto) - .userId(customer.customerId) + .uuid(customer.uuid) .name(customer.customerName) + .userId(customer.customerId) + .customerId(customer.customerId) + .userType('customer') .phoneNumber(customer.customerPhoneNumber) .authProvider(customer.authProvider) - .uuid(customer.uuid) .build(); } } diff --git a/src/customer/application/customer.service.ts b/src/customer/application/customer.service.ts index d60ea7a..f60bd2a 100644 --- a/src/customer/application/customer.service.ts +++ b/src/customer/application/customer.service.ts @@ -48,8 +48,9 @@ export class CustomerService implements IUserService { ); } - findOne(dto: Partial): Promise { - return this.customerRepository.findOne(dto); + async findOne(dto: Partial): Promise { + const customer = await this.customerRepository.findOne(dto); + return customer ? UserDto.from(customer) : null; } async getOne( diff --git a/src/schemas/customer.entity.ts b/src/schemas/customer.entity.ts index 2ae888f..76e1e38 100644 --- a/src/schemas/customer.entity.ts +++ b/src/schemas/customer.entity.ts @@ -98,6 +98,7 @@ export class CustomerEntity extends UserEntity { static toModel(customer: CustomerEntity): Customer { return Builder() .uuid(customer.uuid) + .customerName(customer.customerName) .customerId(customer.customerId) .customerName(customer.customerName) .customerPhoneNumber(customer.customerPhoneNumber) diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index 4f54716..16bd530 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -22,14 +22,23 @@ import { HttpStatusCode } from 'axios'; import { PostGisHelper } from '../mock/pg.test-helper'; import { RedisTestHelper } from '../mock/redis.test-helper'; import { UUID_HOLDER } from '../../src/common/holder/uuid.holders'; -import { FakeUuidHolder } from '../mock/fake.holder'; +import { FakeDateHolder, FakeUuidHolder } from '../mock/fake.holder'; +import { DataSource } from 'typeorm'; +import { CustomerSeeder } from '../mock/seeders'; +import { AuthProvider } from '../../src/auth/presentation/user.dto'; +import { CustomerService } from '../../src/customer/application/customer.service'; describe('AuthController E2E 테스트', () => { let app: INestApplication; - let redisTestHelper: RedisTestHelper; let pgTestHelper: PostGisHelper; + + let customerService: CustomerService; + const uuidHolder = new FakeUuidHolder(); + const date = new Date('2021-01-01T00:00:00Z'); + const dateHolder = new FakeDateHolder(date); + beforeAll(async () => { // Redis Testcontainers 설정 @@ -44,7 +53,7 @@ describe('AuthController E2E 테스트', () => { imports: [ ConfigModule.forRoot({ isGlobal: true }), JwtModule.registerAsync({ - useFactory: async () => ({ secret: 'test-secret' }), + useFactory: async () => ({ secret: 'test-secret' }) }), PassportModule.register({ defaultStrategy: 'access' }), await pgTestHelper.module(), @@ -57,10 +66,10 @@ describe('AuthController E2E 테스트', () => { BusinessModule, CustomerModule, ImageModule, - CloudModule, + CloudModule ], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService] }) .overrideProvider(UUID_HOLDER) .useValue(uuidHolder) @@ -70,8 +79,15 @@ describe('AuthController E2E 테스트', () => { .useClass(FakeCloudStorageService) .compile(); + app = moduleFutures.createNestApplication(); await app.init(); + + const dataSource = app.get(DataSource); + customerService = app.get(CustomerService); + + await pgTestHelper.seed(new CustomerSeeder(dateHolder), dataSource); + }, 30000); afterAll(async () => { @@ -80,106 +96,153 @@ describe('AuthController E2E 테스트', () => { await app.close(); }); - test('빈값으로 보내면 Bad Request가 반환된다.', async () => { - // Given - const response = await request(app.getHttpServer()) - .post('/v1/auth/login') - .expect(HttpStatusCode.BadRequest); - - // Then - const body = response.body; - expect(body.error).toEqual('Bad Request'); - expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); - console.table(body); - }); - - test('userType이 없으면 Bad Request가 반환된다.', async () => { - // Given - const user = { - phoneNumber: '01012345678', - password: 'password', - }; - - // When - const response = await request(app.getHttpServer()) - .post('/v1/auth/login') - .send(user) - .expect(HttpStatusCode.BadRequest); - - // Then - const body = response.body; - expect(body.error).toEqual('Bad Request'); - expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); - console.table(body); - }); - - test('name이 없으면 Bad Request가 반환된다.', async () => { - // Given - const user = { - userType: 'customer', - phoneNumber: '01012345678', - }; - - // When - const response = await request(app.getHttpServer()) - .post('/v1/auth/login') - .send(user) - .expect(HttpStatusCode.BadRequest); - - // Then - const body = response.body; - expect(body.error).toEqual('Bad Request'); - expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); - console.table(body); - }); - - test('phoneNumber이 없으면 Bad Request가 반환된다.', async () => { - // Given - const user = { - userType: 'customer', - name: 'test', - }; - - // When - const response = await request(app.getHttpServer()) - .post('/v1/auth/login') - .send(user) - .expect(HttpStatusCode.BadRequest); - - // Then - const body = response.body; - expect(body.error).toEqual('Bad Request'); - expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); - console.table(body); - }); - - test('정상적인 요청이면 Created가 반환된다.', async () => { - // Given - const user = { - authProvider: 'KAKAO', - userType: 'customer', - uuid: 'test', - name: 'test', - }; - - // when - const response = await request(app.getHttpServer()) - .post('/v1/auth/login') - .send(user) - .expect(HttpStatusCode.Created); - - const body = response.body; - const data = body.data; - - // then - expect(data).toBeDefined(); - expect(data.uuid).toBe(user.uuid); - expect(data.name).toBe(user.name); - expect(data.userId).toBeDefined(); - expect(data.userType).toBe(user.userType); - expect(data.authProvider).toBe(user.authProvider); - expect(data.accessToken).toBeDefined(); - expect(data.refreshToken).toBeDefined(); - console.table(data); + describe('POST /v1/auth/login', () => { + test('빈값으로 보내면 Bad Request가 반환된다.', async () => { + // Given + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .expect(HttpStatusCode.BadRequest); + + // Then + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('userType이 없으면 Bad Request가 반환된다.', async () => { + // Given + const user = { + phoneNumber: '01012345678', + password: 'password' + }; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(user) + .expect(HttpStatusCode.BadRequest); + + // Then + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('name이 없으면 Bad Request가 반환된다.', async () => { + // Given + const user = { + userType: 'customer', + phoneNumber: '01012345678' + }; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(user) + .expect(HttpStatusCode.BadRequest); + + // Then + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('authProvider가 없으면 Bad Request가 반환된다.', async () => { + // Given + const user = { + userType: 'customer', + name: 'test' + }; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(user) + .expect(HttpStatusCode.BadRequest); + + // Then + const body = response.body; + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + console.table(body); + }); + + test('정상적인 요청이면 Created가 반환된다.', async () => { + // Given + const user = { + authProvider: 'KAKAO', + userType: 'customer', + uuid: 'test', + name: 'test' + }; + + // when + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(user) + .expect(HttpStatusCode.Created); + + const body = response.body; + const data = body.data; + + // then + expect(data).toBeDefined(); + expect(data.uuid).toBe(user.uuid); + expect(data.name).toBe(user.name); + expect(data.userId).toBeDefined(); + expect(data.userType).toBe(user.userType); + expect(data.authProvider).toBe(user.authProvider); + expect(data.accessToken).toBeDefined(); + expect(data.refreshToken).toBeDefined(); + console.table(data); + }); + + test('이미 가입된 사용자라면 entity를 생성하지 않는다.', async () => { + // Given + const seedCustomer = { + uuid: 'customer-seed-uuid', + userType: 'customer', + authProvider: AuthProvider.KAKAO, + customerId: 1 + }; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(seedCustomer) + .expect(HttpStatusCode.Created); + + // Then + + const body = response.body; + const data = body.data; + expect(data).toBeDefined(); + expect(data.uuid).toBe(seedCustomer.uuid); + expect(data.userId).toEqual(1); + expect(data.userType).toBe(seedCustomer.userType); + expect(data.authProvider).toBe(seedCustomer.authProvider); + expect(data.accessToken).toBeDefined(); + expect(data.refreshToken).toBeDefined(); + + console.log('data'); + console.table(data); + + await customerService + .findOne({ uuid: seedCustomer.uuid }) + .then((customer) => { + expect(customer).toBeDefined(); + expect(customer?.userId).toEqual(1); + expect(customer?.customerId).toEqual(1); + expect(customer?.uuid).toEqual(seedCustomer.uuid); + expect(customer?.authProvider).toEqual(seedCustomer.authProvider); + expect(customer?.name).toEqual('customer-seed-name'); + expect(customer?.phoneNumber).toEqual('customer-seed-phone'); + console.log('customer'); + console.table(customer); + }); + }); }); }); diff --git a/test/mock/pg.test-helper.ts b/test/mock/pg.test-helper.ts index eb0341a..13f1dfc 100644 --- a/test/mock/pg.test-helper.ts +++ b/test/mock/pg.test-helper.ts @@ -4,6 +4,7 @@ import { DynamicModule, Logger } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; export class PgTestHelper { private readonly logger = new Logger(PgTestHelper.name); @@ -236,7 +237,6 @@ export class PostGisHelper { this.host = this.postgisContainer.getHost(); this.port = this.postgisContainer.getMappedPort(5432); - this.logger.log('Started PostGIS Testcontainers'); return { @@ -256,6 +256,14 @@ export class PostGisHelper { } } + async seed( + seeder: Seeder, + dataSource:DataSource, + seederFactory : SeederFactoryManager = new SeederFactoryManager(), + ){ + await seeder.run(dataSource, seederFactory); + } + async module(): Promise { return TypeOrmModule.forRootAsync({ useFactory: async () => ({ @@ -266,6 +274,7 @@ export class PostGisHelper { password: 'test', database: 'test', entities: [__dirname + '/../../src/**/*.entity.{ts,js}'], + migrations: [__dirname + './mock/seeders.ts'], logger: 'advanced-console', logging: 'all', synchronize: true, diff --git a/test/mock/seeders.ts b/test/mock/seeders.ts new file mode 100644 index 0000000..9a0e81d --- /dev/null +++ b/test/mock/seeders.ts @@ -0,0 +1,39 @@ +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { DataSource } from 'typeorm'; +import { CustomerEntity } from '../../src/schemas/customer.entity'; +import { AuthProvider } from '../../src/auth/presentation/user.dto'; +import { DateHolder } from '../../src/common/holder/date.holder'; + +export class CustomerSeeder implements Seeder { + dateHolder: DateHolder; + + constructor(dateHolder: DateHolder) { + this.dateHolder = dateHolder; + } + + async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager + ): Promise { + const customerRepository = dataSource.getRepository(CustomerEntity); + + const seedCustomer = customerRepository.create({ + appointments: [], + authProvider: AuthProvider.KAKAO, + chatRooms: [], + favorites: [], + pets: [], + reviews: [], + uuid: 'customer-seed-uuid', + customerId: 1, + customerName: 'customer-seed-name', + customerAddress: 'customer-seed-address', + customerPhoneNumber: 'customer-seed-phone', + customerDetailAddress: 'customer-seed-detail-address', + createdAt: this.dateHolder.now(), + modifiedAt: this.dateHolder.now(), + }); + + const customer = await customerRepository.save(seedCustomer); + } +} diff --git a/yarn.lock b/yarn.lock index 4fc78b3..950101f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,6 +1251,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@faker-js/faker@^8.4.1": + version "8.4.1" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.4.1.tgz#5d5e8aee8fce48f5e189bf730ebd1f758f491451" + integrity sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg== + "@fastify/busboy@^2.0.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" @@ -3451,6 +3456,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browserslist@^4.21.10, browserslist@^4.22.2: version "4.23.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" @@ -3859,6 +3871,11 @@ consola@^2.15.0: resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== +consola@^3.2.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.0.tgz#4cfc9348fd85ed16a17940b3032765e31061ab88" + integrity sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA== + console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" @@ -4087,6 +4104,11 @@ depd@2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +destr@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" + integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -4189,6 +4211,18 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ebec@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ebec/-/ebec-1.1.1.tgz#683a0dc82a77e86349e05b24c43d7233a6d0f840" + integrity sha512-JZ1vcvPQtR+8LGbZmbjG21IxLQq/v47iheJqn2F6yB2CgnGfn8ZVg3myHrf3buIZS8UCwQK0jOSIb3oHX7aH8g== + dependencies: + smob "^1.4.0" + +ebec@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/ebec/-/ebec-2.3.0.tgz#9b5b423f1196a0cd414e041111f2b1d146308bd5" + integrity sha512-bt+0tSL7223VU3PSVi0vtNLZ8pO1AfWolcPPMk2a/a5H+o/ZU9ky0n3A0zhrR4qzJTN61uPsGIO4ShhOukdzxA== + ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -4262,6 +4296,13 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: graceful-fs "^4.2.4" tapable "^2.2.0" +envix@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/envix/-/envix-1.5.0.tgz#bbb7ceba6f098a272ef2044e606789844d08c1db" + integrity sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w== + dependencies: + std-env "^3.7.0" + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -4572,6 +4613,17 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4637,6 +4689,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -4675,6 +4734,11 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^3.2.9: version "3.3.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" @@ -5774,6 +5838,11 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.7.0" +jiti@^2.4.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560" + integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== + jmespath@0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" @@ -5963,6 +6032,18 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +locter@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/locter/-/locter-2.1.5.tgz#b16306a321b40f579f1124069618c41612420502" + integrity sha512-eI57PuVxigQ0GBscGIIFGPB467E5zKODHD3XGuknzLvf7HdnvRw3GdZVGj1J8XKsKOYovZQesX/oOdTwbdjwuQ== + dependencies: + destr "^2.0.3" + ebec "^2.3.0" + fast-glob "^3.3.2" + flat "^5.0.2" + jiti "^2.4.0" + yaml "^2.6.0" + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -6031,6 +6112,13 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lru-cache@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" @@ -6128,6 +6216,14 @@ micromatch@^4.0.0, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -6356,6 +6452,14 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -6584,6 +6688,14 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + passport-jwt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" @@ -6956,6 +7068,14 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +rapiq@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/rapiq/-/rapiq-0.9.0.tgz#0f427b1a459064a488ae86d2fb91f14524369240" + integrity sha512-k4oT4RarFBrlLMJ49xUTeQpa/us0uU4I70D/UEnK3FWQ4GENzei01rEQAmvPKAIzACo4NMW+YcYJ7EVfSa7EFg== + dependencies: + ebec "^1.1.0" + smob "^1.4.0" + raw-body@2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" @@ -7052,7 +7172,7 @@ redis@^3.0.0: redis-errors "^1.2.0" redis-parser "^3.0.0" -reflect-metadata@^0.2.0, reflect-metadata@^0.2.1: +reflect-metadata@^0.2.0, reflect-metadata@^0.2.1, reflect-metadata@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== @@ -7332,6 +7452,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +smob@^1.4.0, smob@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" + integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== + socket.io-adapter@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz#43af9157c4609e74b8addc6867873ac7eb48fda2" @@ -7488,6 +7613,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +std-env@^3.7.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -7949,6 +8079,11 @@ tslib@^1.11.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.3: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tweetnacl@^0.14.3: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -7989,6 +8124,21 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== +typeorm-extension@^3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/typeorm-extension/-/typeorm-extension-3.6.3.tgz#555cb5786eddf39315bfef9453257bd13dea86d9" + integrity sha512-AE+8KqBphlBdVz5JS77o6LZzzi+b+YFFt8So4Qu/KRo/iynAwekrx98Oxuu3FAYNm6DUKDcubOBMZsJeiRvHkA== + dependencies: + "@faker-js/faker" "^8.4.1" + consola "^3.2.3" + envix "^1.5.0" + locter "^2.1.5" + pascal-case "^3.1.2" + rapiq "^0.9.0" + reflect-metadata "^0.2.2" + smob "^1.5.0" + yargs "^17.7.2" + typeorm-naming-strategies@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/typeorm-naming-strategies/-/typeorm-naming-strategies-4.1.0.tgz#1ec6eb296c8d7b69bb06764d5b9083ff80e814a9" @@ -8324,7 +8474,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.2.2: +yaml@^2.2.2, yaml@^2.6.0: version "2.7.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== @@ -8352,7 +8502,7 @@ yargs@^16.0.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.3.1, yargs@^17.6.2: +yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From db75bf4624b31479420e507bb804e43c9f2a257a Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Tue, 14 Jan 2025 19:20:12 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feature:=20Refresh=20Strategy=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/medium/auth.controller.test.ts | 53 +++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index 16bd530..0fbb77b 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -27,6 +27,7 @@ import { DataSource } from 'typeorm'; import { CustomerSeeder } from '../mock/seeders'; import { AuthProvider } from '../../src/auth/presentation/user.dto'; import { CustomerService } from '../../src/customer/application/customer.service'; +import { JwtRefreshStrategy } from '../../src/auth/application/jwt-refresh.strategy'; describe('AuthController E2E 테스트', () => { let app: INestApplication; @@ -39,7 +40,6 @@ describe('AuthController E2E 테스트', () => { const date = new Date('2021-01-01T00:00:00Z'); const dateHolder = new FakeDateHolder(date); - beforeAll(async () => { // Redis Testcontainers 설정 redisTestHelper = new RedisTestHelper(); @@ -53,7 +53,7 @@ describe('AuthController E2E 테스트', () => { imports: [ ConfigModule.forRoot({ isGlobal: true }), JwtModule.registerAsync({ - useFactory: async () => ({ secret: 'test-secret' }) + useFactory: async () => ({ secret: 'test-secret' }), }), PassportModule.register({ defaultStrategy: 'access' }), await pgTestHelper.module(), @@ -66,10 +66,10 @@ describe('AuthController E2E 테스트', () => { BusinessModule, CustomerModule, ImageModule, - CloudModule + CloudModule, ], controllers: [AuthController], - providers: [AuthService] + providers: [AuthService, JwtRefreshStrategy], }) .overrideProvider(UUID_HOLDER) .useValue(uuidHolder) @@ -79,7 +79,6 @@ describe('AuthController E2E 테스트', () => { .useClass(FakeCloudStorageService) .compile(); - app = moduleFutures.createNestApplication(); await app.init(); @@ -87,7 +86,6 @@ describe('AuthController E2E 테스트', () => { customerService = app.get(CustomerService); await pgTestHelper.seed(new CustomerSeeder(dateHolder), dataSource); - }, 30000); afterAll(async () => { @@ -114,7 +112,7 @@ describe('AuthController E2E 테스트', () => { // Given const user = { phoneNumber: '01012345678', - password: 'password' + password: 'password', }; // When @@ -134,7 +132,7 @@ describe('AuthController E2E 테스트', () => { // Given const user = { userType: 'customer', - phoneNumber: '01012345678' + phoneNumber: '01012345678', }; // When @@ -154,7 +152,7 @@ describe('AuthController E2E 테스트', () => { // Given const user = { userType: 'customer', - name: 'test' + name: 'test', }; // When @@ -176,7 +174,7 @@ describe('AuthController E2E 테스트', () => { authProvider: 'KAKAO', userType: 'customer', uuid: 'test', - name: 'test' + name: 'test', }; // when @@ -206,7 +204,7 @@ describe('AuthController E2E 테스트', () => { uuid: 'customer-seed-uuid', userType: 'customer', authProvider: AuthProvider.KAKAO, - customerId: 1 + customerId: 1, }; // When @@ -245,4 +243,37 @@ describe('AuthController E2E 테스트', () => { }); }); }); + + describe('POST /v1/auth/refresh', () => { + test('Authorization 헤더를 빈값으로 보내면 Unauthorized 가 반환된다.', async () => { + // Given + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/refresh') + .expect(HttpStatusCode.Unauthorized); + + // Then + const body = response.body; + expect(body.message).toEqual('Unauthorized'); + expect(body.statusCode).toEqual(HttpStatusCode.Unauthorized); + console.table(body); + }); + + test('refreshToken이 Bearer 토큰이 아니면 Unauthorized가 반환된다', async () => { + // Given + const authorization = 'test'; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/refresh') + .set('Authorization', authorization) + .expect(HttpStatusCode.Unauthorized); + + // Then + const body = response.body; + expect(body.message).toEqual('Unauthorized'); + expect(body.statusCode).toEqual(HttpStatusCode.Unauthorized); + console.table(body); + }); + }); }); From bd2bbf46bce86d6c0ccf934023831021bdd6da8c Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Tue, 14 Jan 2025 19:28:37 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feature:=20Jest=20forceExit=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 50f62dc..f6c7a04 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest --verbose", + "test": "jest --verbose --detectOpenHandles --forceExit", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", From 47ea2745b5e234c1c2bcd4b4c9c0d679090bfa85 Mon Sep 17 00:00:00 2001 From: emibgo2 Date: Thu, 16 Jan 2025 07:57:25 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feature:=20/v1/auth/refresh=20api=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/medium/auth.controller.test.ts | 79 +++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index 0fbb77b..40b56eb 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -28,6 +28,7 @@ import { CustomerSeeder } from '../mock/seeders'; import { AuthProvider } from '../../src/auth/presentation/user.dto'; import { CustomerService } from '../../src/customer/application/customer.service'; import { JwtRefreshStrategy } from '../../src/auth/application/jwt-refresh.strategy'; +import clearAllTimers = jest.clearAllTimers; describe('AuthController E2E 테스트', () => { let app: INestApplication; @@ -245,6 +246,38 @@ describe('AuthController E2E 테스트', () => { }); describe('POST /v1/auth/refresh', () => { + let accessToken: string; + let refreshToken: string; + + beforeAll(async () => { + const user = { + authProvider: 'KAKAO', + userType: 'customer', + uuid: 'customer-seed-uuid', + name: 'test', + }; + + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(user) + .expect(HttpStatusCode.Created); + + const body = response.body; + const data = body.data; + + expect(data).toBeDefined(); + expect(data.accessToken).toBeDefined(); + expect(data.refreshToken).toBeDefined(); + accessToken = data.accessToken; + refreshToken = data.refreshToken; + + console.table(data); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + test('Authorization 헤더를 빈값으로 보내면 Unauthorized 가 반환된다.', async () => { // Given // When @@ -275,5 +308,51 @@ describe('AuthController E2E 테스트', () => { expect(body.statusCode).toEqual(HttpStatusCode.Unauthorized); console.table(body); }); + + test('refreshToken이 만료되면 Unauthorized가 반환된다.', async () => { + // Given + jest.useFakeTimers({ + now: new Date().getTime() + 1000 * 60 * 60 * 24 * 31, // 31일 후 + }); + console.log('now', new Date()); + console.log('accessToken', accessToken); + console.log('refreshToken', refreshToken); + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/refresh') + .set('Authorization', `Bearer ${refreshToken}`) + .expect(HttpStatusCode.Unauthorized); + + // Then + const body = response.body; + expect(body.message).toEqual('Unauthorized'); + expect(body.statusCode).toEqual(HttpStatusCode.Unauthorized); + console.table(body); + clearAllTimers(); + }); + + test('refreshToken이 정상적이면 accessToken과 refreshToken 모두 갱신한다.', async () => { + // Given + console.log('now', new Date()); + console.log('accessToken', accessToken); + console.log('refreshToken', refreshToken); + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/refresh') + .set('Authorization', `Bearer ${refreshToken}`) + .expect(HttpStatusCode.Created); + + // Then + const body = response.body; + const data = body.data; + expect(data).toBeDefined(); + expect(data.accessToken).toBeDefined(); + expect(data.refreshToken).toBeDefined(); + expect(data.accessToken).not.toEqual(accessToken); + expect(data.refreshToken).not.toEqual(refreshToken); + console.table(data); + }); }); }); From 37b63789e2e29198426dad06c3e121a357337ea0 Mon Sep 17 00:00:00 2001 From: Jihoon Ko Date: Thu, 16 Jan 2025 15:58:47 +0900 Subject: [PATCH 10/11] =?UTF-8?q?test:=20/v1/auth/otp/verification=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/presentation/auth.controller.ts | 5 +- src/auth/presentation/auth.dto.ts | 7 + test/medium/auth.controller.test.ts | 196 ++++++++++++++++-- test/mock/fake.config.service.ts | 4 +- .../auth/application/security.service.test.ts | 99 ++++++--- 5 files changed, 257 insertions(+), 54 deletions(-) diff --git a/src/auth/presentation/auth.controller.ts b/src/auth/presentation/auth.controller.ts index 9d984d7..c33f67c 100644 --- a/src/auth/presentation/auth.controller.ts +++ b/src/auth/presentation/auth.controller.ts @@ -106,7 +106,10 @@ export class AuthController { ); return ResponseEntity.CREATED( - Builder().otp(generatedOtpNumber).build(), + Builder() + .otp(generatedOtpNumber) + .sendType(sendType) + .build(), ); } diff --git a/src/auth/presentation/auth.dto.ts b/src/auth/presentation/auth.dto.ts index 0b2d51a..cc2ac44 100644 --- a/src/auth/presentation/auth.dto.ts +++ b/src/auth/presentation/auth.dto.ts @@ -66,4 +66,11 @@ export class OtpResponseDto implements OtpResponse { readOnly: true, }) verified: boolean; + + @ApiProperty({ + description: '전송 매체', + type: String, + readOnly: true, + }) + sendType: string; } diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index 40b56eb..06fcd2e 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -28,7 +28,10 @@ import { CustomerSeeder } from '../mock/seeders'; import { AuthProvider } from '../../src/auth/presentation/user.dto'; import { CustomerService } from '../../src/customer/application/customer.service'; import { JwtRefreshStrategy } from '../../src/auth/application/jwt-refresh.strategy'; -import clearAllTimers = jest.clearAllTimers; +import { AuthDto } from '../../src/auth/presentation/auth.dto'; +import { SMS_SERVICE } from '../../src/common/sender/sms/application/sms.service'; +import { FakeSmsService } from '../mock/fake.sms.service'; +import { JwtAccessStrategy } from '../../src/auth/application/jwt-access.strategy'; describe('AuthController E2E 테스트', () => { let app: INestApplication; @@ -70,10 +73,12 @@ describe('AuthController E2E 테스트', () => { CloudModule, ], controllers: [AuthController], - providers: [AuthService, JwtRefreshStrategy], + providers: [AuthService, JwtAccessStrategy, JwtRefreshStrategy], }) .overrideProvider(UUID_HOLDER) .useValue(uuidHolder) + .overrideProvider(SMS_SERVICE) + .useClass(FakeSmsService) .overrideProvider(ConfigService) .useClass(FakeConfigService) .overrideProvider(CLOUD_STORAGE) @@ -246,11 +251,10 @@ describe('AuthController E2E 테스트', () => { }); describe('POST /v1/auth/refresh', () => { - let accessToken: string; - let refreshToken: string; + let user: AuthDto; beforeAll(async () => { - const user = { + const loginUser = { authProvider: 'KAKAO', userType: 'customer', uuid: 'customer-seed-uuid', @@ -259,7 +263,7 @@ describe('AuthController E2E 테스트', () => { const response = await request(app.getHttpServer()) .post('/v1/auth/login') - .send(user) + .send(loginUser) .expect(HttpStatusCode.Created); const body = response.body; @@ -268,8 +272,7 @@ describe('AuthController E2E 테스트', () => { expect(data).toBeDefined(); expect(data.accessToken).toBeDefined(); expect(data.refreshToken).toBeDefined(); - accessToken = data.accessToken; - refreshToken = data.refreshToken; + user = data; console.table(data); }); @@ -315,13 +318,13 @@ describe('AuthController E2E 테스트', () => { now: new Date().getTime() + 1000 * 60 * 60 * 24 * 31, // 31일 후 }); console.log('now', new Date()); - console.log('accessToken', accessToken); - console.log('refreshToken', refreshToken); + console.log('accessToken', user.accessToken); + console.log('refreshToken', user.refreshToken); // When const response = await request(app.getHttpServer()) .post('/v1/auth/refresh') - .set('Authorization', `Bearer ${refreshToken}`) + .set('Authorization', `Bearer ${user.refreshToken}`) .expect(HttpStatusCode.Unauthorized); // Then @@ -329,29 +332,188 @@ describe('AuthController E2E 테스트', () => { expect(body.message).toEqual('Unauthorized'); expect(body.statusCode).toEqual(HttpStatusCode.Unauthorized); console.table(body); - clearAllTimers(); + jest.clearAllTimers(); }); test('refreshToken이 정상적이면 accessToken과 refreshToken 모두 갱신한다.', async () => { // Given console.log('now', new Date()); - console.log('accessToken', accessToken); - console.log('refreshToken', refreshToken); + console.log('accessToken', user.accessToken); + console.log('refreshToken', user.refreshToken); // When const response = await request(app.getHttpServer()) .post('/v1/auth/refresh') - .set('Authorization', `Bearer ${refreshToken}`) + .set('Authorization', `Bearer ${user.refreshToken}`) .expect(HttpStatusCode.Created); // Then const body = response.body; const data = body.data; + expect(data).toBeDefined(); expect(data.accessToken).toBeDefined(); expect(data.refreshToken).toBeDefined(); - expect(data.accessToken).not.toEqual(accessToken); - expect(data.refreshToken).not.toEqual(refreshToken); + expect(data.accessToken).not.toEqual(user.accessToken); + expect(data.refreshToken).not.toEqual(user.refreshToken); + expect(data.userId).toEqual(user.userId); + expect(data.uuid).toEqual(user.uuid); + expect(data.userType).toEqual(user.userType); + expect(data.authProvider).toEqual(user.authProvider); + console.log('data'); + console.table(data); + }); + }); + + describe('POST /v1/auth/otp', () => { + let user: AuthDto; + + beforeAll(async () => { + const loginUser = { + authProvider: 'KAKAO', + userType: 'customer', + uuid: 'customer-seed-uuid', + name: 'test', + }; + + const response = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send(loginUser) + .expect(HttpStatusCode.Created); + + const body = response.body; + const data = body.data; + + expect(data).toBeDefined(); + expect(data.accessToken).toBeDefined(); + expect(data.refreshToken).toBeDefined(); + user = data; + + console.table(data); + }); + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(Date.now()); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + test('OTP 요청시 secret이 없으면 BadRequest가 반환된다', async () => { + // Given + const otpRequest = {}; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/otp') + .set('Authorization', `Bearer ${user.accessToken}`) + .send(otpRequest) + .expect(HttpStatusCode.BadRequest); + + // Then + const body = response.body; + const data = response.body.data; + + expect(body.error).toEqual('Bad Request'); + expect(body.statusCode).toEqual(HttpStatusCode.BadRequest); + expect(data).toBeUndefined(); + }); + + test('OTP를 생성하면 생성된 OTP는 6자리이고 Created가 반환된다.', async () => { + // Given + const otpRequest = { + secret: '01012345678', + }; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/otp') + .set('Authorization', `Bearer ${user.accessToken}`) + .send(otpRequest) + .expect(HttpStatusCode.Created); + + // Then + const body = response.body; + const data = response.body.data; + + expect(body.statusCode).toEqual(HttpStatusCode.Created); + expect(data).toBeDefined(); + expect(data.otp).toBeDefined(); + expect(data.otp.length).toEqual(6); + expect(data.sendType).toBeUndefined(); + + console.table(data); + }); + + test('OTP 생성 요청시 sendType을 지정하면 해당 OTP가 해당 sendType으로 전송된다.', async () => { + // Given + const otpRequest = { + secret: '01012345678', + }; + + // When + const response = await request(app.getHttpServer()) + .post('/v1/auth/otp') + .set('Authorization', `Bearer ${user.accessToken}`) + .query({ sendType: 'sms' }) + .send(otpRequest) + .expect(HttpStatusCode.Created); + + // Then + const body = response.body; + const data = response.body.data; + + expect(body.statusCode).toEqual(HttpStatusCode.Created); + expect(data).toBeDefined(); + expect(data.otp).toBeDefined(); + expect(data.otp.length).toEqual(6); + expect(data.sendType).toEqual('sms'); + + console.table(data); + }); + + test('OTP는 생성 후 10분이 지나면 유효하지 않다.', async () => { + // Given + const otpRequest = { + secret: '01012345678', + }; + + const otpResponse = await request(app.getHttpServer()) + .post('/v1/auth/otp') + .set('Authorization', `Bearer ${user.accessToken}`) + .send(otpRequest) + .expect(HttpStatusCode.Created); + + const otp = otpResponse.body.data.otp; + + // When + await jest.advanceTimersByTimeAsync(1000 * 60 * 11); // 11분 후 + + console.log('now', new Date()); + + const response = await request(app.getHttpServer()) + .post('/v1/auth/otp/verification') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + secret: otpRequest.secret, + otp: otp, + }) + .expect(HttpStatusCode.Ok); + + // Then + const body = response.body; + const data = response.body.data; + + expect(body.statusCode).toEqual(HttpStatusCode.Ok); + expect(data).toBeDefined(); + expect(data.otp).toBeDefined(); + expect(data.otp.length).toEqual(6); + expect(data.verified).toBe(false); + + console.log('data'); console.table(data); }); }); diff --git a/test/mock/fake.config.service.ts b/test/mock/fake.config.service.ts index c34e541..bd3910d 100644 --- a/test/mock/fake.config.service.ts +++ b/test/mock/fake.config.service.ts @@ -8,10 +8,10 @@ export class FakeConfigService extends ConfigService { this.configMap['security/crypto/algorithm'] = 'aes-256-cbc'; this.configMap['security/crypto/key'] = 'mgmg_crypto_HSphC1OlzYwJmSS1Or1K'; this.configMap['jwt/access/secret'] = 'access_secret'; - this.configMap['jwt/access/expire'] = 3600; + this.configMap['jwt/access/expire'] = 3600; // 1 hour this.configMap['jwt/access/strategy'] = 'unique'; this.configMap['jwt/refresh/secret'] = 'refresh_secret'; - this.configMap['jwt/refresh/expire'] = 60 * 60 * 24 * 14; + this.configMap['jwt/refresh/expire'] = 60 * 60 * 24 * 14; // 14 days this.configMap['datasource/redis'] = '{"host":"localhost","port":6379}'; this.configMap['datasource/db'] = '{"type":"mysql","host":"localhost","port":3306,"username":"root","password":"root","database":"test"}'; diff --git a/test/unit/auth/application/security.service.test.ts b/test/unit/auth/application/security.service.test.ts index b10531f..7ed1c01 100644 --- a/test/unit/auth/application/security.service.test.ts +++ b/test/unit/auth/application/security.service.test.ts @@ -8,49 +8,80 @@ describe('SecurityService Test', () => { service = new SecurityService(new FakeConfigService()); jest.useFakeTimers(); + jest.setSystemTime(Date.now()); }); afterEach(() => { - jest.useRealTimers(); + jest.clearAllTimers(); }); - test('암호화', () => { - const text = 'test'; - const encrypted = service.encrypt(text); - console.table({ text, encrypted }); - expect(encrypted).toBeDefined(); - expect(encrypted).not.toBe(text); - }); + describe('Encrypt/Decrypt', () => { + test('암호화', () => { + // Given + const text = 'test'; - test('복호화', () => { - const text = 'test'; - const encrypted = service.encrypt(text); - const decrypted = service.decrypt(encrypted!); - console.table({ text, encrypted, decrypted }); - expect(decrypted).toBe(text); - }); + // When + const encrypted = service.encrypt(text); - test('otp 생성', async () => { - const otp = await service.generateOtp('010-1111-1111'); - console.table({ otp }); - expect(otp).toBeDefined(); - expect(otp.length).toBe(6); - expect(otp).toMatch(/^[0-9]{6}$/); - }); + // Then + console.table({ text, encrypted }); + expect(encrypted).toBeDefined(); + expect(encrypted).not.toBe(text); + }); - test('otp는 10분이 지나기전에 말료되지 않는다', async () => { - const otp = await service.generateOtp('010-1111-1111'); - const result = await service.validateOtp('010-1111-1111', otp); - console.table({ otp, result }); - expect(result).toBe(true); - }); + test('복호화', () => { + // Given + const text = 'test'; - test('otp는 10분 후 만료 된다.', async () => { - const otp = await service.generateOtp('010-1111-1111'); - jest.advanceTimersByTime(600000); - const result = await service.validateOtp('010-1111-1111', otp); - console.table({ otp, result }); - expect(result).toBe(false); + // When + const encrypted = service.encrypt(text); + const decrypted = service.decrypt(encrypted!); + + // Then + console.table({ text, encrypted, decrypted }); + expect(decrypted).toBe(text); + }); }); + describe('OTP', () => { + const testOtpSecrets = ['01012345678', '010-8765-4321', '010-1111-1111']; + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(Date.now()); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + test.each(testOtpSecrets)('otp 생성 - %s', async (secret) => { + // Given + const otp = await service.generateOtp(secret); + + // When + // Then + console.table({ secret, otp }); + expect(otp).toBeDefined(); + expect(otp.length).toBe(6); + expect(otp).toMatch(/^[0-9]{6}$/); + }); + + test.each(testOtpSecrets)( + 'otp는 10분 후 만료 된다 for %s', + async (secret: string) => { + // Given + const otp = await service.generateOtp(secret); + + // When + // 11분 후 + jest.advanceTimersByTime(1000 * 60 * 11); + + // Then + const result = await service.validateOtp(secret, otp); + console.table({ otp, result }); + expect(result).toBe(false); + }, + ); + }); }); From 906a6c80a6ff0d7277ecf3389d5635a3980b8b7c Mon Sep 17 00:00:00 2001 From: emibgo2 Date: Fri, 28 Feb 2025 19:53:43 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20Conflict=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/medium/auth.controller.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/medium/auth.controller.test.ts b/test/medium/auth.controller.test.ts index 06fcd2e..7845791 100644 --- a/test/medium/auth.controller.test.ts +++ b/test/medium/auth.controller.test.ts @@ -15,7 +15,6 @@ import { BusinessModule } from '../../src/business/business.module'; import { CustomerModule } from '../../src/customer/customer.module'; import { ImageModule } from '../../src/common/image/image.module'; import { CLOUD_STORAGE } from '../../src/common/cloud/aws/s3/application/s3.service'; -import { FakeCloudStorageService } from '../mock/fake.cloud-storage.service'; import { CloudModule } from '../../src/common/cloud/cloud.module'; import { FakeConfigService } from '../mock/fake.config.service'; import { HttpStatusCode } from 'axios'; @@ -32,6 +31,7 @@ import { AuthDto } from '../../src/auth/presentation/auth.dto'; import { SMS_SERVICE } from '../../src/common/sender/sms/application/sms.service'; import { FakeSmsService } from '../mock/fake.sms.service'; import { JwtAccessStrategy } from '../../src/auth/application/jwt-access.strategy'; +import { FakeCloudStorage } from '../mock/fake.cloud-storage'; describe('AuthController E2E 테스트', () => { let app: INestApplication; @@ -82,7 +82,7 @@ describe('AuthController E2E 테스트', () => { .overrideProvider(ConfigService) .useClass(FakeConfigService) .overrideProvider(CLOUD_STORAGE) - .useClass(FakeCloudStorageService) + .useClass(FakeCloudStorage) .compile(); app = moduleFutures.createNestApplication();