import { UnauthorizedException } from '@nestjs/common'; // @nestjs/jwt v12 ships ESM only, which this CommonJS jest setup cannot load. // The service is constructed with a hand-built JwtService double anyway, so the // real module is never used here — stubbing it keeps the suite runnable without // touching the shared jest config (D19: lowest-risk option). jest.mock('@nestjs/jwt', () => ({ JwtService: class {} })); import { CustomerAuthService, masterOtpCode, masterOtpEnabled } from './customer-auth.service'; /** * D20 — the delivery master OTP. * * The three cases the owner specified: * 1. with OTP_MASTER_CODE set, that code signs an active customer in; * 2. with the variable absent, the same code is rejected; * 3. the real, stored code still works. * * Plus the guards that must survive the bypass: an inactive or archived * customer is still refused, and an unknown mobile is still refused. * * The service is driven with hand-built doubles rather than a Nest testing * module, so these run without a database. */ describe('OTP_MASTER_CODE (D20)', () => { const saved = { ...process.env }; /** An active customer whose stored challenge holds the real code "4321". */ const CUSTOMER = { id: 'cus_1', mobile: '09121112233', isActive: true, isArchived: false }; let prisma: { customer: { findFirst: jest.Mock }; customerOtp: { findFirst: jest.Mock; update: jest.Mock; create: jest.Mock; count: jest.Mock }; }; let jwt: { signAsync: jest.Mock }; let sms: { send: jest.Mock }; let service: CustomerAuthService; beforeEach(() => { delete process.env.OTP_MASTER_CODE; process.env.JWT_CUSTOMER_SECRET = 'test-secret'; prisma = { customer: { findFirst: jest.fn().mockResolvedValue(CUSTOMER) }, customerOtp: { findFirst: jest.fn().mockResolvedValue(null), update: jest.fn().mockResolvedValue({}), create: jest.fn().mockResolvedValue({}), count: jest.fn().mockResolvedValue(0), }, }; jwt = { signAsync: jest.fn().mockResolvedValue('signed.jwt.token') }; sms = { send: jest.fn().mockResolvedValue({ ok: true }) }; service = new CustomerAuthService( prisma as never, jwt as never, sms as never, ); // The bypass logs every use; keep the test output clean. jest.spyOn(service['logger'], 'warn').mockImplementation(() => undefined); }); afterAll(() => { process.env = { ...saved }; }); describe('the env switch itself', () => { it('is disabled when the variable is absent', () => { expect(masterOtpCode()).toBeNull(); expect(masterOtpEnabled()).toBe(false); }); it('is disabled when the variable is empty or only whitespace', () => { process.env.OTP_MASTER_CODE = ''; expect(masterOtpEnabled()).toBe(false); process.env.OTP_MASTER_CODE = ' '; expect(masterOtpEnabled()).toBe(false); }); it('is enabled when the variable holds a code', () => { process.env.OTP_MASTER_CODE = '1379'; expect(masterOtpCode()).toBe('1379'); expect(masterOtpEnabled()).toBe(true); }); }); describe('1 — the master code signs an active customer in', () => { it('issues a token without any stored challenge', async () => { process.env.OTP_MASTER_CODE = '1379'; const result = await service.verifyOtp(CUSTOMER.mobile, '1379', '127.0.0.1'); expect(result.token).toBe('signed.jwt.token'); expect(result.customer).toEqual({ id: CUSTOMER.id, mobile: CUSTOMER.mobile }); // It must not depend on a CustomerOtp row existing. expect(prisma.customerOtp.findFirst).not.toHaveBeenCalled(); }); it('accepts the master code typed with Persian digits', async () => { // faToEn runs before the comparison, as it does for a real code. process.env.OTP_MASTER_CODE = '1379'; await expect(service.verifyOtp(CUSTOMER.mobile, '۱۳۷۹', undefined)).resolves.toMatchObject({ token: 'signed.jwt.token', }); }); it('logs the bypass with the customer mobile', async () => { process.env.OTP_MASTER_CODE = '1379'; const warn = jest.spyOn(service['logger'], 'warn'); await service.verifyOtp(CUSTOMER.mobile, '1379', undefined); expect(warn).toHaveBeenCalledWith(expect.stringContaining(CUSTOMER.mobile)); expect(warn).toHaveBeenCalledWith(expect.stringContaining('OTP_MASTER_CODE')); }); it('still refuses an inactive or archived customer', async () => { process.env.OTP_MASTER_CODE = '1379'; // The lookup filters on isActive/isArchived, so it finds nothing. prisma.customer.findFirst.mockResolvedValue(null); await expect(service.verifyOtp(CUSTOMER.mobile, '1379', undefined)).rejects.toBeInstanceOf( UnauthorizedException, ); }); it('still refuses a mobile that belongs to no customer', async () => { process.env.OTP_MASTER_CODE = '1379'; prisma.customer.findFirst.mockResolvedValue(null); await expect(service.verifyOtp('09990000000', '1379', undefined)).rejects.toBeInstanceOf( UnauthorizedException, ); }); }); describe('2 — without the env var the same code is rejected', () => { it('falls through to the normal path and rejects it', async () => { // No OTP_MASTER_CODE, and no stored challenge to match. await expect(service.verifyOtp(CUSTOMER.mobile, '1379', undefined)).rejects.toBeInstanceOf( UnauthorizedException, ); // It took the ordinary route, which does consult the challenge table. expect(prisma.customerOtp.findFirst).toHaveBeenCalled(); expect(jwt.signAsync).not.toHaveBeenCalled(); }); it('rejects a code that merely resembles the master one while it is armed', async () => { process.env.OTP_MASTER_CODE = '1379'; await expect(service.verifyOtp(CUSTOMER.mobile, '1378', undefined)).rejects.toBeInstanceOf( UnauthorizedException, ); expect(jwt.signAsync).not.toHaveBeenCalled(); }); }); describe('3 — the real code still works', () => { /** bcrypt hash of "4321", generated once for this test. */ const bcrypt = require('bcryptjs') as typeof import('bcryptjs'); const challengeWith = async (code: string) => ({ id: 'otp_1', codeHash: await bcrypt.hash(code, 4), expiresAt: new Date(Date.now() + 60_000), attempts: 0, customer: CUSTOMER, }); it('verifies a stored code while the master code is armed', async () => { process.env.OTP_MASTER_CODE = '1379'; prisma.customerOtp.findFirst.mockResolvedValue(await challengeWith('4321')); const result = await service.verifyOtp(CUSTOMER.mobile, '4321', undefined); expect(result.token).toBe('signed.jwt.token'); // The real path burns the challenge; the master path never touches it. expect(prisma.customerOtp.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ consumedAt: expect.any(Date) }) }), ); }); it('verifies a stored code while the master code is absent', async () => { prisma.customerOtp.findFirst.mockResolvedValue(await challengeWith('4321')); await expect(service.verifyOtp(CUSTOMER.mobile, '4321', undefined)).resolves.toMatchObject({ token: 'signed.jwt.token', }); }); it('still rejects a wrong code and counts the attempt', async () => { prisma.customerOtp.findFirst.mockResolvedValue(await challengeWith('4321')); await expect(service.verifyOtp(CUSTOMER.mobile, '0000', undefined)).rejects.toBeInstanceOf( UnauthorizedException, ); expect(prisma.customerOtp.update).toHaveBeenCalledWith( expect.objectContaining({ data: { attempts: { increment: 1 } } }), ); }); }); });