UNPKG

@accounter/server

Version:
258 lines (228 loc) • 7.52 kB
import { GraphQLError, GraphQLResolveInfo } from 'graphql'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { invitationsResolvers } from '../resolvers/invitations.resolver.js'; import { AuthContextProvider } from '../providers/auth-context.provider.js'; import { Auth0ManagementProvider } from '../providers/auth0-management.provider.js'; import { InvitationsProvider } from '../providers/invitations.provider.js'; const createInvitation = invitationsResolvers.Mutation?.createInvitation as ( source: unknown, args: { email: string; roleId: string }, context: { injector: { get<T>(token: unknown): T } }, info: GraphQLResolveInfo, ) => Promise<{ id: string; email: string; roleId: string; expiresAt: Date; token: string; }>; const mockInfo = {} as GraphQLResolveInfo; describe('createInvitation resolver', () => { let mockAuthProvider: { getAuthContext: ReturnType<typeof vi.fn> }; let mockAuth0ManagementProvider: { createBlockedUser: ReturnType<typeof vi.fn>; deleteUser: ReturnType<typeof vi.fn>; }; let mockInvitationsProvider: { getInvitationByEmailLoader: { load: ReturnType<typeof vi.fn> }; insertInvitation: ReturnType<typeof vi.fn>; }; beforeEach(() => { mockAuthProvider = { getAuthContext: vi.fn().mockResolvedValue({ authType: 'jwt', user: { userId: 'user-123', roleId: 'business_owner', email: 'owner@example.com', permissions: [], emailVerified: true, permissionsVersion: 1, auth0UserId: 'auth0|owner-123', }, tenant: { businessId: 'business-123', roleId: 'business_owner', }, }), }; mockAuth0ManagementProvider = { createBlockedUser: vi.fn().mockResolvedValue('auth0|new-user-123'), deleteUser: vi.fn().mockResolvedValue(undefined), }; mockInvitationsProvider = { getInvitationByEmailLoader: { load: vi.fn().mockResolvedValue([]), }, insertInvitation: vi.fn().mockResolvedValue({ id: 'inv-123', email: 'new.user@example.com', roleId: 'employee', expiresAt: new Date('2030-01-01T00:00:00.000Z'), token: 'invitation-token-abc', }), }; }); afterEach(() => { vi.clearAllMocks(); }); function createContext() { return { injector: { get<T>(token: unknown): T { if (token === AuthContextProvider) { return mockAuthProvider as T; } if (token === Auth0ManagementProvider) { return mockAuth0ManagementProvider as T; } if (token === InvitationsProvider) { return mockInvitationsProvider as T; } throw new Error(`Unexpected token requested: ${String(token)}`); }, }, }; } it('business_owner can create invitation (happy path)', async () => { const result = await createInvitation( {}, { email: 'new.user@example.com', roleId: 'employee' }, createContext(), mockInfo, ); expect(result).toEqual({ id: 'inv-123', email: 'new.user@example.com', roleId: 'employee', expiresAt: new Date('2030-01-01T00:00:00.000Z'), token: 'invitation-token-abc', }); }); it('Auth0 Management API called with correct parameters (mock)', async () => { await createInvitation( {}, { email: 'INVITEE@EXAMPLE.COM', roleId: 'scraper' }, createContext(), mockInfo, ); expect(mockAuth0ManagementProvider.createBlockedUser).toHaveBeenCalledWith('invitee@example.com'); }); it('invitation record inserted with correct fields', async () => { await createInvitation( {}, { email: 'new.user@example.com', roleId: 'accountant' }, createContext(), mockInfo, ); expect(mockInvitationsProvider.insertInvitation).toHaveBeenCalledTimes(1); const call = mockInvitationsProvider.insertInvitation.mock.calls[0][0]; expect(call.email).toBe('new.user@example.com'); expect(call.roleId).toBe('accountant'); expect(call.auth0UserId).toBe('auth0|new-user-123'); expect(call.invitedByUserId).toBe('user-123'); expect(call.ownerId).toBe('business-123'); }); it('cleans up Auth0 user when DB insert fails', async () => { const dbError = new Error('DB insert failed'); mockInvitationsProvider.insertInvitation.mockRejectedValueOnce(dbError); await expect( createInvitation( {}, { email: 'new.user@example.com', roleId: 'employee' }, createContext(), mockInfo, ), ).rejects.toBe(dbError); expect(mockAuth0ManagementProvider.deleteUser).toHaveBeenCalledWith('auth0|new-user-123'); }); it('duplicate active invitation rejected', async () => { mockInvitationsProvider.getInvitationByEmailLoader.load.mockResolvedValueOnce([ { id: 'existing-invitation', email: 'new.user@example.com', }, ]); await expect( createInvitation( {}, { email: 'new.user@example.com', roleId: 'employee' }, createContext(), mockInfo, ), ).rejects.toMatchObject({ extensions: { code: 'INVITATION_ALREADY_PENDING' }, }); expect(mockAuth0ManagementProvider.createBlockedUser).not.toHaveBeenCalled(); expect(mockInvitationsProvider.insertInvitation).not.toHaveBeenCalled(); }); it('Auth0 rate limit handled gracefully (mocked 429)', async () => { mockAuth0ManagementProvider.createBlockedUser.mockRejectedValue({ statusCode: 429, retryAfter: 60, message: 'Too many requests', }); await expect( createInvitation( {}, { email: 'new.user@example.com', roleId: 'employee' }, createContext(), mockInfo, ), ).rejects.toMatchObject({ extensions: { code: 'RATE_LIMITED', retryAfter: 60, }, }); expect(mockInvitationsProvider.insertInvitation).not.toHaveBeenCalled(); }); it('Auth0 user already exists returns USER_ALREADY_EXISTS', async () => { mockAuth0ManagementProvider.createBlockedUser.mockRejectedValue({ statusCode: 409, message: 'The user already exists', }); await expect( createInvitation( {}, { email: 'new.user@example.com', roleId: 'employee' }, createContext(), mockInfo, ), ).rejects.toMatchObject({ extensions: { code: 'USER_ALREADY_EXISTS', }, }); expect(mockInvitationsProvider.insertInvitation).not.toHaveBeenCalled(); }); it('employee calling mutation gets FORBIDDEN', async () => { mockAuthProvider.getAuthContext.mockResolvedValueOnce({ authType: 'jwt', user: { userId: 'user-employee', roleId: 'employee', email: 'employee@example.com', permissions: [], emailVerified: true, permissionsVersion: 1, auth0UserId: 'auth0|employee', }, tenant: { businessId: 'business-123', roleId: 'employee', }, }); await expect( createInvitation( {}, { email: 'new.user@example.com', roleId: 'employee' }, createContext(), mockInfo, ), ).rejects.toSatisfy(error => { return error instanceof GraphQLError && error.extensions?.code === 'FORBIDDEN'; }); expect(mockInvitationsProvider.insertInvitation).not.toHaveBeenCalled(); }); });