mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
test: unit tests for Ozonetel integration, caller resolution, team, missed calls
48 tests across 5 new spec files, all passing in <1s: - ozonetel-agent.service.spec: agent auth (login/logout/retry), manual dial, set disposition, change state, token caching (10 tests) - missed-call-webhook.spec: webhook payload parsing, IST→UTC conversion, duration parsing, CallerID handling, JSON-wrapped body (9 tests) - missed-queue.spec: abandon call polling, PENDING_CALLBACK status, UCID dedup, phone normalization, istToUtc utility (8 tests) - caller-resolution.spec: phone→lead+patient resolution (4 paths: both exist, lead only, patient only, neither), caching, phone normalization, link-if-unlinked (9 tests) - team.spec: 5-step member creation flow, SIP seat linking, validation, temp password Redis cache, email normalization, workspace context caching (8 tests) Fixtures: ozonetel-payloads.ts with accurate Ozonetel API shapes from official docs — webhook payloads, CDR records, abandon calls, disposition responses, auth responses. QA coverage: TC-MC-01/02/03, TC-IB-05/06/07, backs TC-IB/OB-01→06 via the Ozonetel service layer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
243
src/team/team.spec.ts
Normal file
243
src/team/team.spec.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Team Service — unit tests
|
||||
*
|
||||
* Tests the in-place workspace member creation flow (no email invites):
|
||||
* 1. signUpInWorkspace → creates user + workspace member
|
||||
* 2. updateWorkspaceMember → set name
|
||||
* 3. updateWorkspaceMemberRole → assign role
|
||||
* 4. (optional) updateAgent → link SIP seat
|
||||
*
|
||||
* Platform GraphQL + SessionService are mocked.
|
||||
*/
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { TeamService, CreateMemberInput } from './team.service';
|
||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||
import { SessionService } from '../auth/session.service';
|
||||
|
||||
describe('TeamService', () => {
|
||||
let service: TeamService;
|
||||
let platform: jest.Mocked<PlatformGraphqlService>;
|
||||
let session: jest.Mocked<SessionService>;
|
||||
|
||||
const mockWorkspace = {
|
||||
id: '42424242-1c25-4d02-bf25-6aeccf7ea419',
|
||||
inviteHash: 'test-invite-hash',
|
||||
isPublicInviteLinkEnabled: true,
|
||||
};
|
||||
|
||||
const mockMemberId = 'member-new-001';
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
TeamService,
|
||||
{
|
||||
provide: PlatformGraphqlService,
|
||||
useValue: {
|
||||
query: jest.fn().mockImplementation((query: string) => {
|
||||
if (query.includes('currentWorkspace')) {
|
||||
return Promise.resolve({ currentWorkspace: mockWorkspace });
|
||||
}
|
||||
if (query.includes('signUpInWorkspace')) {
|
||||
return Promise.resolve({ signUpInWorkspace: { workspace: { id: mockWorkspace.id } } });
|
||||
}
|
||||
if (query.includes('workspaceMembers')) {
|
||||
return Promise.resolve({
|
||||
workspaceMembers: {
|
||||
edges: [{
|
||||
node: {
|
||||
id: mockMemberId,
|
||||
userId: 'user-new-001',
|
||||
userEmail: 'ccagent@ramaiahcare.com',
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (query.includes('updateWorkspaceMember')) {
|
||||
return Promise.resolve({ updateWorkspaceMember: { id: mockMemberId } });
|
||||
}
|
||||
if (query.includes('updateWorkspaceMemberRole')) {
|
||||
return Promise.resolve({ updateWorkspaceMemberRole: { id: mockMemberId } });
|
||||
}
|
||||
if (query.includes('updateAgent')) {
|
||||
return Promise.resolve({ updateAgent: { id: 'agent-001', workspaceMemberId: mockMemberId } });
|
||||
}
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SessionService,
|
||||
useValue: {
|
||||
setCache: jest.fn().mockResolvedValue(undefined),
|
||||
getCache: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(TeamService);
|
||||
platform = module.get(PlatformGraphqlService);
|
||||
session = module.get(SessionService);
|
||||
});
|
||||
|
||||
// ── Create member (standard flow) ────────────────────────────
|
||||
|
||||
it('should create a workspace member with correct 5-step flow', async () => {
|
||||
const input: CreateMemberInput = {
|
||||
firstName: 'CC',
|
||||
lastName: 'Agent',
|
||||
email: 'ccagent@ramaiahcare.com',
|
||||
password: 'CcRamaiah@2026',
|
||||
roleId: 'role-agent-001',
|
||||
};
|
||||
|
||||
const result = await service.createMember(input);
|
||||
|
||||
expect(result.id).toBe(mockMemberId);
|
||||
expect(result.userEmail).toBe('ccagent@ramaiahcare.com');
|
||||
|
||||
// Verify the 5-step call chain
|
||||
const calls = platform.query.mock.calls.map(c => c[0] as string);
|
||||
|
||||
// Step 1: fetch workspace context (inviteHash)
|
||||
expect(calls.some(q => q.includes('currentWorkspace'))).toBe(true);
|
||||
// Step 2: signUpInWorkspace
|
||||
expect(calls.some(q => q.includes('signUpInWorkspace'))).toBe(true);
|
||||
// Step 3: find member by email
|
||||
expect(calls.some(q => q.includes('workspaceMembers'))).toBe(true);
|
||||
// Step 4: set name
|
||||
expect(calls.some(q => q.includes('updateWorkspaceMember'))).toBe(true);
|
||||
// Step 5: assign role
|
||||
expect(calls.some(q => q.includes('updateWorkspaceMemberRole'))).toBe(true);
|
||||
});
|
||||
|
||||
// ── Create member with SIP seat link ─────────────────────────
|
||||
|
||||
it('should link agent SIP seat when agentId is provided', async () => {
|
||||
const input: CreateMemberInput = {
|
||||
firstName: 'CC',
|
||||
lastName: 'Agent',
|
||||
email: 'ccagent@ramaiahcare.com',
|
||||
password: 'CcRamaiah@2026',
|
||||
roleId: 'role-agent-001',
|
||||
agentId: 'agent-sip-001',
|
||||
};
|
||||
|
||||
const result = await service.createMember(input);
|
||||
|
||||
// Step 6: updateAgent (links workspaceMemberId)
|
||||
const agentCall = platform.query.mock.calls.find(
|
||||
c => (c[0] as string).includes('updateAgent'),
|
||||
);
|
||||
expect(agentCall).toBeDefined();
|
||||
expect(agentCall![1]).toMatchObject({
|
||||
id: 'agent-sip-001',
|
||||
data: { workspaceMemberId: mockMemberId },
|
||||
});
|
||||
});
|
||||
|
||||
// ── Validation ───────────────────────────────────────────────
|
||||
|
||||
it('should reject missing required fields', async () => {
|
||||
await expect(
|
||||
service.createMember({
|
||||
firstName: '',
|
||||
lastName: 'Test',
|
||||
email: 'test@ramaiahcare.com',
|
||||
password: 'pass',
|
||||
roleId: 'role-001',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should reject missing email', async () => {
|
||||
await expect(
|
||||
service.createMember({
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
email: '',
|
||||
password: 'pass',
|
||||
roleId: 'role-001',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
// ── Temp password caching ────────────────────────────────────
|
||||
|
||||
it('should cache temp password in Redis with 24h TTL', async () => {
|
||||
const input: CreateMemberInput = {
|
||||
firstName: 'CC',
|
||||
lastName: 'Agent',
|
||||
email: 'ccagent@ramaiahcare.com',
|
||||
password: 'CcRamaiah@2026',
|
||||
roleId: 'role-001',
|
||||
};
|
||||
|
||||
await service.createMember(input);
|
||||
|
||||
// Redis setCache should be called with the temp password
|
||||
expect(session.setCache).toHaveBeenCalledWith(
|
||||
expect.stringContaining('team:tempPassword:'),
|
||||
'CcRamaiah@2026',
|
||||
86400, // 24 hours
|
||||
);
|
||||
});
|
||||
|
||||
// ── Email normalization ──────────────────────────────────────
|
||||
|
||||
it('should lowercase email before signUp', async () => {
|
||||
const input: CreateMemberInput = {
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
email: 'CcAgent@RamaiahCare.COM',
|
||||
password: 'pass',
|
||||
roleId: 'role-001',
|
||||
};
|
||||
|
||||
await service.createMember(input);
|
||||
|
||||
const signUpCall = platform.query.mock.calls.find(
|
||||
c => (c[0] as string).includes('signUpInWorkspace'),
|
||||
);
|
||||
expect(signUpCall![1]?.email).toBe('ccagent@ramaiahcare.com');
|
||||
});
|
||||
|
||||
// ── Workspace context caching ────────────────────────────────
|
||||
|
||||
it('should fetch workspace context only once for multiple creates', async () => {
|
||||
let callCount = 0;
|
||||
|
||||
platform.query.mockImplementation((query: string) => {
|
||||
if (query.includes('currentWorkspace')) {
|
||||
return Promise.resolve({ currentWorkspace: mockWorkspace });
|
||||
}
|
||||
if (query.includes('signUpInWorkspace')) {
|
||||
return Promise.resolve({ signUpInWorkspace: { workspace: { id: mockWorkspace.id } } });
|
||||
}
|
||||
if (query.includes('workspaceMembers')) {
|
||||
callCount++;
|
||||
// Return matching email based on call order
|
||||
const email = callCount <= 1 ? 'a@test.com' : 'b@test.com';
|
||||
return Promise.resolve({
|
||||
workspaceMembers: {
|
||||
edges: [{ node: { id: `member-${callCount}`, userId: 'u', userEmail: email } }],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (query.includes('updateWorkspaceMember')) return Promise.resolve({ updateWorkspaceMember: { id: 'member-x' } });
|
||||
if (query.includes('updateWorkspaceMemberRole')) return Promise.resolve({ updateWorkspaceMemberRole: { id: 'member-x' } });
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
await service.createMember({ firstName: 'A', lastName: 'B', email: 'a@test.com', password: 'p', roleId: 'r' });
|
||||
await service.createMember({ firstName: 'C', lastName: 'D', email: 'b@test.com', password: 'p', roleId: 'r' });
|
||||
|
||||
const wsCalls = platform.query.mock.calls.filter(
|
||||
c => (c[0] as string).includes('currentWorkspace'),
|
||||
);
|
||||
// Should only fetch workspace context once (cached)
|
||||
expect(wsCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user