good forst commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OnboardingService } from './onboarding.service';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UnauthorizedException, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
const PEPPER = 'pepper-secret-must-be-32-chars-min';
|
||||
const TEST_PHONE = '+19198765432';
|
||||
const TEST_PHONE_HASH = createHash('sha256').update(`${PEPPER}:${TEST_PHONE}`).digest('hex');
|
||||
|
||||
describe('OnboardingService', () => {
|
||||
let service: OnboardingService;
|
||||
const mockPrisma: any = {
|
||||
group: { findUnique: jest.fn() },
|
||||
tenant: { findUnique: jest.fn() },
|
||||
otpChallenge: { create: jest.fn(), findUnique: jest.fn(), update: jest.fn() },
|
||||
towerUser: { upsert: jest.fn() },
|
||||
consentRecord: { findFirst: jest.fn(), create: jest.fn(), update: jest.fn() },
|
||||
};
|
||||
const mockJwt = { signAsync: jest.fn().mockResolvedValue('member-jwt'), verify: jest.fn() };
|
||||
const mockAudit = { log: jest.fn() };
|
||||
const mockConfig = { get: jest.fn().mockReturnValue('pepper-secret-must-be-32-chars-min') };
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
OnboardingService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
{ provide: JwtService, useValue: mockJwt },
|
||||
{ provide: AuditService, useValue: mockAudit },
|
||||
{ provide: ConfigService, useValue: mockConfig },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<OnboardingService>(OnboardingService);
|
||||
});
|
||||
|
||||
function validToken(): string {
|
||||
return Buffer.from(
|
||||
JSON.stringify({ tenantId: 'tnt-1', groupId: 'grp-1', jid: '1234@s.whatsapp.net' }),
|
||||
'utf8',
|
||||
).toString('base64url');
|
||||
}
|
||||
|
||||
describe('decodeOnboardingToken', () => {
|
||||
it('rejects garbage', () => {
|
||||
expect(() => service.decodeOnboardingToken('!!!')).toThrow(UnauthorizedException);
|
||||
});
|
||||
it('rejects missing fields', () => {
|
||||
const tok = Buffer.from(JSON.stringify({ groupId: 'x' }), 'utf8').toString('base64url');
|
||||
expect(() => service.decodeOnboardingToken(tok)).toThrow(UnauthorizedException);
|
||||
});
|
||||
it('decodes a valid token', () => {
|
||||
const out = service.decodeOnboardingToken(validToken());
|
||||
expect(out.tenantId).toBe('tnt-1');
|
||||
expect(out.jid).toBe('1234@s.whatsapp.net');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOnboardInfo', () => {
|
||||
it('throws when group is not claimed', async () => {
|
||||
mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', name: 'Foo', tenantId: null });
|
||||
await expect(service.getOnboardInfo(validToken())).rejects.toThrow(ConflictException);
|
||||
});
|
||||
it('returns group + tenant + policy info', async () => {
|
||||
mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', name: 'UP Parivar', tenantId: 'tnt-1' });
|
||||
mockPrisma.tenant.findUnique.mockResolvedValue({ id: 'tnt-1', name: 'UP Parivar Dallas' });
|
||||
const res = await service.getOnboardInfo(validToken());
|
||||
expect(res.groupName).toBe('UP Parivar');
|
||||
expect(res.tenantName).toBe('UP Parivar Dallas');
|
||||
expect(res.defaultScopes).toContain('INGEST');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestOtp', () => {
|
||||
it('creates a challenge with a 6-digit code', async () => {
|
||||
mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', tenantId: 'tnt-1' });
|
||||
mockPrisma.otpChallenge.create.mockResolvedValue({ id: 'ch-1' });
|
||||
const res = await service.requestOtp(validToken(), '+19198765432');
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.challengeId).toBeTruthy();
|
||||
expect(res.expiresInSeconds).toBe(300);
|
||||
expect(mockPrisma.otpChallenge.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
jid: '1234@s.whatsapp.net',
|
||||
policyVersion: 'v1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects if group is not claimable', async () => {
|
||||
mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', tenantId: null });
|
||||
await expect(service.requestOtp(validToken(), '+19198765432')).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyOtp', () => {
|
||||
it('rejects unknown challenge', async () => {
|
||||
mockPrisma.otpChallenge.findUnique.mockResolvedValue(null);
|
||||
await expect(
|
||||
service.verifyOtp(validToken(), 'ch-x', '+19198765432', '123456', [], undefined),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('rejects consumed challenge', async () => {
|
||||
mockPrisma.otpChallenge.findUnique.mockResolvedValue({
|
||||
id: 'ch-1',
|
||||
code: '123456',
|
||||
consumedAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 60000),
|
||||
phoneHash: 'a',
|
||||
});
|
||||
await expect(
|
||||
service.verifyOtp(validToken(), 'ch-1', '+19198765432', '123456', [], undefined),
|
||||
).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('rejects wrong code', async () => {
|
||||
mockPrisma.otpChallenge.findUnique.mockResolvedValue({
|
||||
id: 'ch-1',
|
||||
code: '111111',
|
||||
consumedAt: null,
|
||||
expiresAt: new Date(Date.now() + 60000),
|
||||
phoneHash: 'computed-hash',
|
||||
});
|
||||
await expect(
|
||||
service.verifyOtp(validToken(), 'ch-1', '+19198765432', '123456', [], undefined),
|
||||
).rejects.toThrow(/Invalid code/);
|
||||
});
|
||||
|
||||
it('creates TowerUser, ConsentRecord, and member JWT on success', async () => {
|
||||
mockPrisma.otpChallenge.findUnique.mockResolvedValue({
|
||||
id: 'ch-1',
|
||||
code: '123456',
|
||||
consumedAt: null,
|
||||
expiresAt: new Date(Date.now() + 60000),
|
||||
phoneHash: TEST_PHONE_HASH,
|
||||
tenantId: 'tnt-1',
|
||||
jid: '1234@s.whatsapp.net',
|
||||
groupId: 'grp-1',
|
||||
});
|
||||
mockPrisma.towerUser.upsert.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
tenantId: 'tnt-1',
|
||||
jid: '1234@s.whatsapp.net',
|
||||
displayName: '1234@s.whatsapp.net',
|
||||
});
|
||||
mockPrisma.consentRecord.findFirst.mockResolvedValue(null);
|
||||
mockPrisma.consentRecord.create.mockResolvedValue({
|
||||
id: 'consent-1',
|
||||
scopes: ['INGEST', 'DISPLAY'],
|
||||
retentionDays: 90,
|
||||
policyVersion: 'v1',
|
||||
});
|
||||
|
||||
const res = await service.verifyOtp(validToken(), 'ch-1', '+19198765432', '123456', [], undefined);
|
||||
expect(res.memberToken).toBe('member-jwt');
|
||||
expect(res.user.id).toBe('user-1');
|
||||
expect(res.consent.scopes).toContain('INGEST');
|
||||
expect(mockPrisma.otpChallenge.update).toHaveBeenCalledWith({
|
||||
where: { id: 'ch-1' },
|
||||
data: { consumedAt: expect.any(Date) },
|
||||
});
|
||||
expect(mockAudit.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: 'MEMBER_ONBOARDED' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user