import { describe, it, expect, vi, beforeEach } from 'vitest'; import { JMAPClient } from '../jmap/client'; const mockIdentity = { id: 'id-1', name: 'Test User', email: 'test@example.com', mayDelete: true, }; function createClient(): JMAPClient { const client = new JMAPClient('https://jmap.example.com', 'user', 'pass'); // Set internal state so request() doesn't throw "Not connected" Object.assign(client, { apiUrl: 'https://jmap.example.com/api', accountId: 'account-1', }); return client; } function mockFetch(response: object, ok = true, status = 200) { return vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok, status, text: () => Promise.resolve(JSON.stringify(response)), json: () => Promise.resolve(response), } as Response); } describe('JMAPClient identity methods', () => { beforeEach(() => { vi.restoreAllMocks(); }); describe('getIdentities', () => { it('should return identities from server', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/get', { list: [mockIdentity] }, '0']], }); const result = await client.getIdentities(); expect(result).toHaveLength(1); expect(result[0].id).toBe('id-1'); expect(result[0].email).toBe('test@example.com'); }); it('should return empty array when no identities', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/get', { list: [] }, '0']], }); const result = await client.getIdentities(); expect(result).toEqual([]); }); it('should return empty array on network error', async () => { const client = createClient(); vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Network error')); const result = await client.getIdentities(); expect(result).toEqual([]); }); it('should return empty array for unexpected response', async () => { const client = createClient(); mockFetch({ methodResponses: [['SomethingElse', {}, '0']], }); const result = await client.getIdentities(); expect(result).toEqual([]); }); it('should handle missing list property', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/get', {}, '0']], }); const result = await client.getIdentities(); expect(result).toEqual([]); }); }); describe('createIdentity', () => { it('should create identity and return full object', async () => { const client = createClient(); const fetchSpy = vi.spyOn(globalThis, 'fetch'); // First call: Identity/set returns created id fetchSpy.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(JSON.stringify({ methodResponses: [['Identity/set', { created: { 'new-identity': { id: 'new-id' } } }, '0']], })), json: () => Promise.resolve({ methodResponses: [['Identity/set', { created: { 'new-identity': { id: 'new-id' } } }, '0']], }), } as Response); // Second call: getIdentities fetches full object fetchSpy.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(JSON.stringify({ methodResponses: [['Identity/get', { list: [{ ...mockIdentity, id: 'new-id' }] }, '0']], })), json: () => Promise.resolve({ methodResponses: [['Identity/get', { list: [{ ...mockIdentity, id: 'new-id' }] }, '0']], }), } as Response); const result = await client.createIdentity('Test User', 'test@example.com'); expect(result.id).toBe('new-id'); }); it('should throw on forbidden error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notCreated: { 'new-identity': { type: 'forbidden' } }, }, '0']], }); await expect(client.createIdentity('Test', 'test@example.com')) .rejects.toThrow('not authorized'); }); it('should throw on generic creation error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notCreated: { 'new-identity': { type: 'invalidProperties', description: 'Bad input' } }, }, '0']], }); await expect(client.createIdentity('Test', 'test@example.com')) .rejects.toThrow('Bad input'); }); it('should throw on unexpected response', async () => { const client = createClient(); mockFetch({ methodResponses: [['SomethingElse', {}, '0']], }); await expect(client.createIdentity('Test', 'test@example.com')) .rejects.toThrow('unexpected'); }); it('should pass all parameters to the request', async () => { const client = createClient(); const fetchSpy = vi.spyOn(globalThis, 'fetch'); fetchSpy.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(JSON.stringify({ methodResponses: [['Identity/set', { created: { 'new-identity': { id: 'new-id' } } }, '0']], })), json: () => Promise.resolve({ methodResponses: [['Identity/set', { created: { 'new-identity': { id: 'new-id' } } }, '0']], }), } as Response); fetchSpy.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(JSON.stringify({ methodResponses: [['Identity/get', { list: [{ ...mockIdentity, id: 'new-id' }] }, '0']], })), json: () => Promise.resolve({ methodResponses: [['Identity/get', { list: [{ ...mockIdentity, id: 'new-id' }] }, '0']], }), } as Response); const replyTo = [{ name: 'Reply', email: 'reply@example.com' }]; const bcc = [{ email: 'bcc@example.com' }]; await client.createIdentity('Test', 'test@example.com', replyTo, bcc, 'text sig', 'html sig'); const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); const createArgs = body.methodCalls[0][1].create['new-identity']; expect(createArgs.name).toBe('Test'); expect(createArgs.email).toBe('test@example.com'); expect(createArgs.replyTo).toEqual(replyTo); expect(createArgs.bcc).toEqual(bcc); expect(createArgs.textSignature).toBe('text sig'); expect(createArgs.htmlSignature).toBe('html sig'); }); }); describe('updateIdentity', () => { it('should update identity successfully', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { updated: { 'id-1': null } }, '0']], }); await expect(client.updateIdentity('id-1', { name: 'New Name' })).resolves.toBeUndefined(); }); it('should throw on notFound error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notUpdated: { 'id-1': { type: 'notFound' } }, }, '0']], }); await expect(client.updateIdentity('id-1', { name: 'X' })) .rejects.toThrow('not found'); }); it('should throw on forbidden error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notUpdated: { 'id-1': { type: 'forbidden' } }, }, '0']], }); await expect(client.updateIdentity('id-1', { name: 'X' })) .rejects.toThrow('not authorized'); }); it('should throw on generic update error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notUpdated: { 'id-1': { type: 'other', description: 'Server error' } }, }, '0']], }); await expect(client.updateIdentity('id-1', { name: 'X' })) .rejects.toThrow('Server error'); }); it('should throw on unexpected response', async () => { const client = createClient(); mockFetch({ methodResponses: [['SomethingElse', {}, '0']], }); await expect(client.updateIdentity('id-1', { name: 'X' })) .rejects.toThrow('unexpected'); }); }); describe('deleteIdentity', () => { it('should delete identity successfully', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { destroyed: ['id-1'] }, '0']], }); await expect(client.deleteIdentity('id-1')).resolves.toBeUndefined(); }); it('should throw on forbidden error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notDestroyed: { 'id-1': { type: 'forbidden' } }, }, '0']], }); await expect(client.deleteIdentity('id-1')) .rejects.toThrow('cannot be deleted'); }); it('should throw on notFound error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notDestroyed: { 'id-1': { type: 'notFound' } }, }, '0']], }); await expect(client.deleteIdentity('id-1')) .rejects.toThrow('not found'); }); it('should throw on generic delete error', async () => { const client = createClient(); mockFetch({ methodResponses: [['Identity/set', { notDestroyed: { 'id-1': { type: 'other', description: 'Cannot remove' } }, }, '0']], }); await expect(client.deleteIdentity('id-1')) .rejects.toThrow('Cannot remove'); }); it('should throw on unexpected response', async () => { const client = createClient(); mockFetch({ methodResponses: [['SomethingElse', {}, '0']], }); await expect(client.deleteIdentity('id-1')) .rejects.toThrow('unexpected'); }); }); });