mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
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>
244 lines
9.9 KiB
TypeScript
244 lines
9.9 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|