@accounter/server
Version:
Accounter GraphQL server
162 lines • 7.43 kB
JavaScript
import { GraphQLError } 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;
const mockInfo = {};
describe('createInvitation resolver', () => {
let mockAuthProvider;
let mockAuth0ManagementProvider;
let mockInvitationsProvider;
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(token) {
if (token === AuthContextProvider) {
return mockAuthProvider;
}
if (token === Auth0ManagementProvider) {
return mockAuth0ManagementProvider;
}
if (token === InvitationsProvider) {
return mockInvitationsProvider;
}
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();
});
});
//# sourceMappingURL=create-invitation.test.js.map