good forst commit

This commit is contained in:
2026-06-09 02:02:40 +05:30
parent 801c1d7121
commit 249d759e6a
215 changed files with 15425 additions and 1240 deletions
@@ -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' }),
);
});
});
});