UNPKG

@signalwire/js

Version:
593 lines (525 loc) 17.6 kB
import { Conversation } from './Conversation' import { ConversationAPI } from './ConversationAPI' import { HTTPClient } from './HTTPClient' import { WSClient } from './WSClient' import { uuid } from '@signalwire/core' // Mock HTTPClient jest.mock('./HTTPClient', () => { return { HTTPClient: jest.fn().mockImplementation(() => { return { fetch: jest.fn(), fetchSubscriberInfo: jest.fn(() => Promise.resolve({ id: 'subscriber-id' }) ), } }), } }) // Mock WSClient jest.mock('./WSClient', () => { return { WSClient: jest.fn().mockImplementation(() => { return { connect: jest.fn(), runWorker: jest.fn(), } }), } }) describe('Conversation', () => { let conversation: Conversation let httpClient: HTTPClient let wsClient: WSClient beforeEach(async () => { httpClient = new HTTPClient({ token: '....', }) wsClient = new WSClient({ token: '....', }) await wsClient.connect() conversation = new Conversation({ httpClient, wsClient }) }) describe('getConversations', () => { it('should fetch conversations', async () => { const conversations = [ { id: uuid(), last_message_at: Date.now(), created_at: Date.now(), metadata: {}, name: 'convo 1', }, { id: uuid(), last_message_at: Date.now(), created_at: Date.now(), metadata: {}, name: 'convo 2', }, ] ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: conversations, links: {} }, }) const result = await conversation.getConversations() result.data.forEach((item, index) => { expect(item).toBeInstanceOf(ConversationAPI) expect(item.id).toEqual(conversations[index].id) expect(item.name).toEqual(conversations[index].name) }) expect(result.hasNext).toBe(false) expect(result.hasPrev).toBe(false) expect(httpClient.fetch).toHaveBeenCalledWith( expect.stringContaining('/conversations') ) expect(result.data[0].sendMessage).not.toBeUndefined() expect(typeof result.data[0].sendMessage).toBe('function') }) it('should handle errors with getConversations', async () => { ;(httpClient.fetch as jest.Mock).mockRejectedValue( new Error('Network error') ) try { await conversation.getConversations() fail('Expected getConversations to throw an error.') } catch (error) { expect(error).toBeInstanceOf(Error) expect(error.message).toBe('Error fetching the conversation history!') } }) }) describe('getMessages', () => { it('should fetch conversation messages', async () => { ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: ['message1', 'message2'], links: { next: 'http://next.url', prev: 'http://prev.url', }, }, }) const result = await conversation.getMessages() expect(result.data).toEqual(['message1', 'message2']) expect(result.hasNext).toBe(true) expect(result.hasPrev).toBe(true) expect(httpClient.fetch).toHaveBeenCalledWith( expect.stringContaining('/message') ) }) it('should handle errors with getMessages', async () => { ;(httpClient.fetch as jest.Mock).mockRejectedValue( new Error('Network error') ) try { await conversation.getMessages() fail('Expected getMessages to throw an error.') } catch (error) { expect(error).toBeInstanceOf(Error) expect(error.message).toBe('Error fetching the conversation messages!') } }) }) describe('getConversationMessages', () => { it('should fetch conversation messages', async () => { ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: ['message1', 'message2'], links: { next: 'http://next.url', prev: 'http://prev.url', }, }, }) const result = await conversation.getConversationMessages({ addressId: '1234', }) expect(result.data).toEqual(['message1', 'message2']) expect(result.hasNext).toBe(true) expect(result.hasPrev).toBe(true) expect(httpClient.fetch).toHaveBeenCalledWith( expect.stringContaining('/conversations/1234/messages') ) }) it('should handle errors with getConversationMessages', async () => { ;(httpClient.fetch as jest.Mock).mockRejectedValue( new Error('Network error') ) try { await conversation.getConversationMessages({ addressId: '1234', }) fail('Expected getConversationMessages to throw an error.') } catch (error) { expect(error).toBeInstanceOf(Error) expect(error.message).toBe('Error fetching the conversation messages!') } }) }) describe('sendMessage', () => { it('should create a conversation message', async () => { const addressId = uuid() const text = 'test message' const expectedResponse = { table: { text, conversation_id: addressId, }, } ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: expectedResponse, }) // TODO: Test with payload const result = await conversation.sendMessage({ addressId, text, }) expect(result).toEqual(expectedResponse) expect(httpClient.fetch).toHaveBeenCalledWith('/api/fabric/messages', { method: 'POST', body: { conversation_id: addressId, text, }, }) }) it('should handles errors with createConversationMessage', async () => { ;(httpClient.fetch as jest.Mock).mockRejectedValue( new Error('Network error') ) try { await conversation.sendMessage({ text: 'text message', addressId: uuid(), }) fail('Expected sendMessage to throw error.') } catch (error) { expect(error).toBeInstanceOf(Error) expect(error.message).toBe('Error sending message to conversation!') } }) }) describe('joinConversation', () => { it('should join a conversation', async () => { const addressId = uuid() const expectedResponse = { table: { conversation_id: addressId, }, } ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: expectedResponse, }) const result = await conversation.joinConversation({ addressId, }) expect(result).toEqual(expectedResponse) expect(httpClient.fetch).toHaveBeenCalledWith( '/api/fabric/conversations/join', { method: 'POST', body: { conversation_id: addressId, }, } ) }) it('should handles errors with joinConversation', async () => { ;(httpClient.fetch as jest.Mock).mockRejectedValue( new Error('Network error') ) try { await conversation.joinConversation({ addressId: uuid(), }) fail('Expected joinConversation to throw error.') } catch (error) { expect(error).toBeInstanceOf(Error) expect(error.message).toBe('Error joining a conversation!') } }) }) describe('subscribe', () => { it('should register the callback', async () => { const callback = jest.fn() await conversation.subscribe(callback) expect(conversation['callbacks']).toContain(callback) }) it('should unsubscribe the correct callback', async () => { const callback1 = jest.fn() await conversation.subscribe(callback1) const callback2 = jest.fn() const { unsubscribe: unsubscribe2 } = await conversation.subscribe( callback2 ) const callback3 = jest.fn() await conversation.subscribe(callback3) expect(conversation['callbacks']).toContain(callback1) expect(conversation['callbacks']).toContain(callback2) expect(conversation['callbacks']).toContain(callback3) unsubscribe2() expect(conversation['callbacks']).toContain(callback1) expect(conversation['callbacks']).not.toContain(callback2) expect(conversation['callbacks']).toContain(callback3) }) }) describe('handleEvent', () => { it('should call all registered callbacks with the event', async () => { const mockCallback1 = jest.fn() const mockCallback2 = jest.fn() await conversation.subscribe(mockCallback1) await conversation.subscribe(mockCallback2) const event = { type: 'conversation.message', message: 'Test message', } // @ts-expect-error conversation.handleEvent(event) expect(mockCallback1).toHaveBeenCalledWith(event) expect(mockCallback2).toHaveBeenCalledWith(event) }) }) describe('Chat utilities', () => { it('Should return adresss chat messages only', async () => { ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: [ { subtype: 'log', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'xyz' }, ], links: {}, }, }) const addressId = 'abc' const messages = await conversation.getChatMessages({ addressId }) expect(messages.data).toHaveLength(1) expect(messages.data[0].conversation_id).toEqual(addressId) }) it('Should return 10(default page) adresss chat messages only', async () => { ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: [ { subtype: 'log', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'xyz' }, ], links: { next: 'http://next.url', prev: 'http://prev.url', }, }, }) const addressId = 'abc' const messages = await conversation.getChatMessages({ addressId }) expect(messages.data).toHaveLength(10) expect(messages.data.every((item) => item.subtype === 'chat')).toBe(true) expect( messages.data.every((item) => item.conversation_id === addressId) ).toBe(true) }) it('Should return 10(default page) adresses chat messages only, on next', async () => { ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: [ { subtype: 'log', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'xyz' }, ], links: { next: 'http://next.url', prev: 'http://prev.url', }, }, }) const addressId = 'abc' let messages = await conversation.getChatMessages({ addressId }) expect(messages.data).toHaveLength(10) expect(messages.data.every((item) => item.subtype === 'chat')).toBe(true) expect( messages.data.every((item) => item.conversation_id === addressId) ).toBe(true) //@ts-ignore messages = await messages.nextPage() expect(messages.data).toHaveLength(10) expect(messages.data.every((item) => item.subtype === 'chat')).toBe(true) expect( messages.data.every((item) => item.conversation_id === addressId) ).toBe(true) }) it('Should return 3 adresses chat messages only', async () => { ;(httpClient.fetch as jest.Mock).mockResolvedValue({ body: { data: [ { subtype: 'log', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'xyz' }, ], links: { next: 'http://next.url', prev: 'http://prev.url', }, }, }) const addressId = 'abc' const messages = await conversation.getChatMessages({ addressId, pageSize: 3, }) expect(messages.data).toHaveLength(3) expect(messages.data.every((item) => item.subtype === 'chat')).toBe(true) expect( messages.data.every((item) => item.conversation_id === addressId) ).toBe(true) }) it('Should return 3 adresss chat messages only', async () => { let count = 0 ;(httpClient.fetch as jest.Mock).mockImplementation(() => { ++count return { body: { data: [ { subtype: 'log', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'abc' }, { subtype: 'chat', conversation_id: 'xyz' }, ], links: { next: count < 3 ? 'http://next.url' : undefined, prev: count < 3 ? 'http://prev.url' : undefined, }, }, } }) const addressId = 'abc' const messages = await conversation.getChatMessages({ addressId }) expect(messages.data).toHaveLength(3) expect(messages.data.every((item) => item.subtype === 'chat')).toBe(true) expect( messages.data.every((item) => item.conversation_id === addressId) ).toBe(true) }) it('should get only address chat event', async () => { const mockCallback = jest.fn() const addressId = 'abc' await conversation.subscribeChatMessages({ addressId, onMessage: mockCallback, }) const valid = { type: 'message', subtype: 'chat', conversation_id: 'abc', text: 'text', } //@ts-expect-error conversation.handleEvent(valid) //@ts-expect-error conversation.handleEvent({ type: 'message', subtype: 'chat', conversation_id: 'xyz', text: 'text', }) //@ts-expect-error conversation.handleEvent({ type: 'message', subtype: 'log', conversation_id: 'abc', }) expect(mockCallback).toHaveBeenCalledWith(valid) }) it('should register the chat callback', async () => { const addressId = 'abc' const mockCallback = jest.fn() await conversation.subscribeChatMessages({ addressId, onMessage: mockCallback, }) expect(conversation['chatSubscriptions'][addressId]).toContain( mockCallback ) }) it('should cancel the correct chat subscription', async () => { const mockCallback1 = jest.fn() const mockCallback2 = jest.fn() const mockCallback3 = jest.fn() const addressId1 = 'abc' const addressId2 = 'xyz' await conversation.subscribeChatMessages({ addressId: addressId1, onMessage: mockCallback1, }) const subscription2 = await conversation.subscribeChatMessages({ addressId: addressId1, onMessage: mockCallback2, }) await conversation.subscribeChatMessages({ addressId: addressId2, onMessage: mockCallback3, }) expect(conversation['chatSubscriptions'][addressId1]).toContain( mockCallback1 ) expect(conversation['chatSubscriptions'][addressId1]).toContain( mockCallback2 ) expect(conversation['chatSubscriptions'][addressId1]).not.toContain( mockCallback3 ) expect(conversation['chatSubscriptions'][addressId2]).toContain( mockCallback3 ) const eventForAddressId1 = { conversation_id: addressId1, conversation_name: 'test_conversation_name', details: {}, hidden: false, id: 'test_id', kind: 'test_kind', metadata: {}, subtype: 'chat', type: 'message', text: 'test_text', ts: 1, user_id: 'test_user_id', user_name: 'test_user_name', address_id: 'text_address_id', from_address_id: 'test_from_address_id', } conversation.handleEvent(eventForAddressId1) conversation.handleEvent({ ...eventForAddressId1, conversation_id: 'different_id', subtype: 'chat', type: 'message', address_id: '', from_address_id: '', }) conversation.handleEvent({ ...eventForAddressId1, conversation_id: 'abc', subtype: 'log', type: 'message', address_id: '', from_address_id: '', }) expect(mockCallback1).toHaveBeenCalledWith(eventForAddressId1) expect(mockCallback1).toHaveBeenCalledTimes(1) expect(mockCallback2).toHaveBeenCalledWith(eventForAddressId1) expect(mockCallback2).toHaveBeenCalledTimes(1) expect(mockCallback3).not.toHaveBeenCalledWith(eventForAddressId1) expect(mockCallback3).toHaveBeenCalledTimes(0) subscription2.unsubscribe() conversation.handleEvent(eventForAddressId1) expect(mockCallback1).toHaveBeenCalledTimes(2) expect(mockCallback2).toHaveBeenCalledTimes(1) expect(mockCallback3).toHaveBeenCalledTimes(0) }) }) })