fix(ci): fix E2E job env to unblock every PR — GOO-136

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 <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-23 21:45:44 +07:00
parent 6b23bfb756
commit bafc3ddc2f
4 changed files with 77 additions and 7 deletions

View File

@@ -195,8 +195,8 @@ jobs:
ports: ports:
- 9000:9000 - 9000:9000
env: env:
MINIO_ROOT_USER: ci_minio_user MINIO_ROOT_USER: ${{ vars.CI_MINIO_ACCESS_KEY || 'ci_minio_user' }}
MINIO_ROOT_PASSWORD: ci_minio_secret_key_32chars!! MINIO_ROOT_PASSWORD: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }}
options: >- options: >-
--health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1" --health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1"
--health-interval 10s --health-interval 10s
@@ -206,22 +206,41 @@ jobs:
env: env:
DATABASE_URL: postgresql://goodgo:goodgo_test_secret@localhost:5432/goodgo_test DATABASE_URL: postgresql://goodgo:goodgo_test_secret@localhost:5432/goodgo_test
REDIS_URL: redis://localhost:6379 REDIS_URL: redis://localhost:6379
REDIS_HOST: localhost
REDIS_PORT: 6379
TYPESENSE_URL: http://localhost:8108 TYPESENSE_URL: http://localhost:8108
TYPESENSE_HOST: localhost TYPESENSE_HOST: localhost
TYPESENSE_PORT: 8108 TYPESENSE_PORT: 8108
TYPESENSE_PROTOCOL: http
TYPESENSE_API_KEY: ts_ci_key TYPESENSE_API_KEY: ts_ci_key
MINIO_ENDPOINT: localhost MINIO_ENDPOINT: localhost
MINIO_PORT: 9000 MINIO_PORT: 9000
MINIO_ACCESS_KEY: ci_minio_user MINIO_ACCESS_KEY: ${{ vars.CI_MINIO_ACCESS_KEY || 'ci_minio_user' }}
MINIO_SECRET_KEY: ci_minio_secret_key_32chars!! MINIO_SECRET_KEY: ${{ vars.CI_MINIO_SECRET_KEY || 'ci_minio_secret_key_32chars!!' }}
MINIO_BUCKET: goodgo-uploads MINIO_BUCKET: goodgo-uploads
NODE_ENV: test NODE_ENV: test
JWT_SECRET: e2e-test-jwt-secret-key CI: true
JWT_REFRESH_SECRET: e2e-test-refresh-secret-key # 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_TMN_CODE: TESTCODE
VNPAY_HASH_SECRET: TESTHASHSECRET VNPAY_HASH_SECRET: TESTHASHSECRETTESTHASHSECRETTEST
VNPAY_URL: https://sandbox.vnpayment.vn/paymentv2/vpcpay.html VNPAY_URL: https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
VNPAY_RETURN_URL: http://localhost:3000/payment/return 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: steps:
- name: Checkout - name: Checkout

View File

@@ -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,
) {}
}

View File

@@ -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<void> {
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,
),
);
}
}

View File

@@ -8,6 +8,7 @@ describe('CreatePaymentHandler', () => {
let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> }; let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
let mockGateway: { createPaymentUrl: ReturnType<typeof vi.fn>; verifyCallback: ReturnType<typeof vi.fn>; refund: ReturnType<typeof vi.fn> }; let mockGateway: { createPaymentUrl: ReturnType<typeof vi.fn>; verifyCallback: ReturnType<typeof vi.fn>; refund: ReturnType<typeof vi.fn> };
let mockEventBus: { publish: ReturnType<typeof vi.fn> }; let mockEventBus: { publish: ReturnType<typeof vi.fn> };
let mockSbvCompliance: { checkPaymentCompliance: ReturnType<typeof vi.fn> };
beforeEach(() => { beforeEach(() => {
mockPaymentRepo = { mockPaymentRepo = {
@@ -35,11 +36,16 @@ describe('CreatePaymentHandler', () => {
mockEventBus = { publish: vi.fn() }; 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() }; const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
handler = new CreatePaymentHandler( handler = new CreatePaymentHandler(
mockPaymentRepo as any, mockPaymentRepo as any,
mockGatewayFactory as any, mockGatewayFactory as any,
mockSbvCompliance as any,
mockEventBus as any, mockEventBus as any,
mockLogger as any, mockLogger as any,
); );