From bafc3ddc2f40c35387e3edb251db4ee348b854dd Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 23 Apr 2026 21:45:44 +0700 Subject: [PATCH] =?UTF-8?q?fix(ci):=20fix=20E2E=20job=20env=20to=20unblock?= =?UTF-8?q?=20every=20PR=20=E2=80=94=20GOO-136?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the ci.yml e2e job had JWT_SECRET (23 chars) and JWT_REFRESH_SECRET (27 chars), both below the MIN_SECRET_LENGTH=32 enforced by env-validation.ts. The API webServer refused to start with "Insecure JWT secret configuration", so every E2E test failed with ERR_CONNECTION_REFUSED before a single assertion ran. Fixes in this commit: 1. .github/workflows/ci.yml — sync e2e job env with the working standalone e2e.yml: • JWT secrets lengthened past 32-char minimum • Add API_PORT / WEB_PORT / *_BASE_URL for Playwright webServer • Add REDIS_HOST/PORT, TYPESENSE_PROTOCOL, JWT_EXPIRES_IN, BCRYPT_ROUNDS, GOOGLE_*/ZALO_* OAuth stubs • VNPAY_HASH_SECRET lengthened to 32 chars • MINIO creds use vars fallback pattern (matches e2e.yml) 2. phone-login-otp-requested.event.ts + .listener.ts — the notifications module imported PhoneLoginOtpRequestedListener but neither the event class nor the listener file existed, causing module-resolution errors in four unrelated test suites that import notifications.module.ts. 3. create-payment.handler.spec.ts — inject mockSbvCompliance so the two handler tests that were TypeError-ing on undefined.error() pass. Co-Authored-By: Paperclip --- .github/workflows/ci.yml | 33 +++++++++++++++---- .../events/phone-login-otp-requested.event.ts | 12 +++++++ .../phone-login-otp-requested.listener.ts | 33 +++++++++++++++++++ .../__tests__/create-payment.handler.spec.ts | 6 ++++ 4 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/modules/auth/domain/events/phone-login-otp-requested.event.ts create mode 100644 apps/api/src/modules/notifications/application/listeners/phone-login-otp-requested.listener.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43a8ebb..209676a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,8 +195,8 @@ jobs: ports: - 9000:9000 env: - MINIO_ROOT_USER: ci_minio_user - MINIO_ROOT_PASSWORD: ci_minio_secret_key_32chars!! + MINIO_ROOT_USER: ${{ vars.CI_MINIO_ACCESS_KEY || 'ci_minio_user' }} + MINIO_ROOT_PASSWORD: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }} options: >- --health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1" --health-interval 10s @@ -206,22 +206,41 @@ jobs: env: DATABASE_URL: postgresql://goodgo:goodgo_test_secret@localhost:5432/goodgo_test REDIS_URL: redis://localhost:6379 + REDIS_HOST: localhost + REDIS_PORT: 6379 TYPESENSE_URL: http://localhost:8108 TYPESENSE_HOST: localhost TYPESENSE_PORT: 8108 + TYPESENSE_PROTOCOL: http TYPESENSE_API_KEY: ts_ci_key MINIO_ENDPOINT: localhost MINIO_PORT: 9000 - MINIO_ACCESS_KEY: ci_minio_user - MINIO_SECRET_KEY: ci_minio_secret_key_32chars!! + MINIO_ACCESS_KEY: ${{ vars.CI_MINIO_ACCESS_KEY || 'ci_minio_user' }} + MINIO_SECRET_KEY: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }} MINIO_BUCKET: goodgo-uploads NODE_ENV: test - JWT_SECRET: e2e-test-jwt-secret-key - JWT_REFRESH_SECRET: e2e-test-refresh-secret-key + CI: true + # API and Web ports for Playwright webServer + API_PORT: 3001 + WEB_PORT: 3000 + API_BASE_URL: http://localhost:3001/api/v1/ + WEB_BASE_URL: http://localhost:3000 + NEXT_PUBLIC_API_URL: http://localhost:3001/api/v1 + JWT_SECRET: e2e-test-jwt-secret-key-minimum-32-chars-long-enough + JWT_REFRESH_SECRET: e2e-test-refresh-secret-key-minimum-32-chars-ok + JWT_EXPIRES_IN: 15m + JWT_REFRESH_EXPIRES_IN: 7d + BCRYPT_ROUNDS: 4 VNPAY_TMN_CODE: TESTCODE - VNPAY_HASH_SECRET: TESTHASHSECRET + VNPAY_HASH_SECRET: TESTHASHSECRETTESTHASHSECRETTEST VNPAY_URL: https://sandbox.vnpayment.vn/paymentv2/vpcpay.html VNPAY_RETURN_URL: http://localhost:3000/payment/return + GOOGLE_CLIENT_ID: test-google-client-id + GOOGLE_CLIENT_SECRET: test-google-client-secret + GOOGLE_CALLBACK_URL: http://localhost:3001/api/v1/auth/google/callback + ZALO_APP_ID: test-zalo-app-id + ZALO_APP_SECRET: test-zalo-app-secret + ZALO_CALLBACK_URL: http://localhost:3001/api/v1/auth/zalo/callback steps: - name: Checkout diff --git a/apps/api/src/modules/auth/domain/events/phone-login-otp-requested.event.ts b/apps/api/src/modules/auth/domain/events/phone-login-otp-requested.event.ts new file mode 100644 index 0000000..457454c --- /dev/null +++ b/apps/api/src/modules/auth/domain/events/phone-login-otp-requested.event.ts @@ -0,0 +1,12 @@ +import { type DomainEvent } from '@modules/shared'; + +export class PhoneLoginOtpRequestedEvent implements DomainEvent { + readonly eventName = 'user.phone_login_otp'; + readonly occurredAt = new Date(); + + constructor( + public readonly aggregateId: string, + public readonly phone: string, + public readonly otpCode: string, + ) {} +} diff --git a/apps/api/src/modules/notifications/application/listeners/phone-login-otp-requested.listener.ts b/apps/api/src/modules/notifications/application/listeners/phone-login-otp-requested.listener.ts new file mode 100644 index 0000000..919f276 --- /dev/null +++ b/apps/api/src/modules/notifications/application/listeners/phone-login-otp-requested.listener.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { OnEvent } from '@nestjs/event-emitter'; +import { type PhoneLoginOtpRequestedEvent } from '@modules/auth/domain/events/phone-login-otp-requested.event'; +import { LoggerService } from '@modules/shared'; +import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; + +@Injectable() +export class PhoneLoginOtpRequestedListener { + constructor( + private readonly commandBus: CommandBus, + private readonly logger: LoggerService, + ) {} + + @OnEvent('user.phone_login_otp', { async: true }) + async handle(event: PhoneLoginOtpRequestedEvent): Promise { + this.logger.log( + `Handling phone login OTP for user ${event.aggregateId}`, + 'PhoneLoginOtpRequestedListener', + ); + + await this.commandBus.execute( + new SendNotificationCommand( + event.aggregateId, + 'SMS', + 'user.phone_login_otp', + { otpCode: event.otpCode }, + undefined, + event.phone, + ), + ); + } +} diff --git a/apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts b/apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts index 7ef657a..21638aa 100644 --- a/apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts +++ b/apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts @@ -8,6 +8,7 @@ describe('CreatePaymentHandler', () => { let mockGatewayFactory: { getGateway: ReturnType }; let mockGateway: { createPaymentUrl: ReturnType; verifyCallback: ReturnType; refund: ReturnType }; let mockEventBus: { publish: ReturnType }; + let mockSbvCompliance: { checkPaymentCompliance: ReturnType }; beforeEach(() => { mockPaymentRepo = { @@ -35,11 +36,16 @@ describe('CreatePaymentHandler', () => { mockEventBus = { publish: vi.fn() }; + mockSbvCompliance = { + checkPaymentCompliance: vi.fn().mockReturnValue({ allowed: true, sbvLargeTransaction: false, amlFlagged: false }), + }; + const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() }; handler = new CreatePaymentHandler( mockPaymentRepo as any, mockGatewayFactory as any, + mockSbvCompliance as any, mockEventBus as any, mockLogger as any, );