import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
import { WebexClient } from '../client'
import { toRestId } from '../id-normalizer'
import { WebexError } from '../types'
const messageId = toRestId('msg_123', 'MESSAGE')
const message2Id = toRestId('msg_124', 'MESSAGE')
const roomId = toRestId('space_456', 'ROOM')
const personId = toRestId('person_789', 'PEOPLE')
const mockMessage = {
id: messageId,
ref: 'msg_123',
roomId,
roomRef: 'space_456',
roomType: 'group' as const,
text: 'Hello world',
html: '
Hello world
',
personId,
personRef: 'person_789',
personEmail: 'user@example.com',
created: '2025-01-29T10:00:00.000Z',
}
const mockMessage2 = {
id: message2Id,
ref: 'msg_124',
roomId,
roomRef: 'space_456',
roomType: 'group' as const,
text: 'Second message',
html: 'Second message
',
personId,
personRef: 'person_789',
personEmail: 'user@example.com',
created: '2025-01-29T10:01:00.000Z',
}
import { deleteAction, dmAction, editAction, getAction, listAction, sendAction, typingAction } from './message'
let mockSendMessage: ReturnType
let mockSendDirectMessage: ReturnType
let mockListMessages: ReturnType
let mockGetMessage: ReturnType
let mockDeleteMessage: ReturnType
let mockEditMessage: ReturnType
let mockSetTyping: ReturnType
let mockLogin: ReturnType
let mockDispose: ReturnType
let consoleLogSpy: ReturnType
let consoleErrorSpy: ReturnType
let processExitSpy: ReturnType
const protoSpies: ReturnType[] = []
function protoSpy(method: keyof WebexClient) {
const s = spyOn(WebexClient.prototype, method as never)
protoSpies.push(s)
return s
}
beforeEach(() => {
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
return this
})
mockDispose = protoSpy('dispose').mockResolvedValue(undefined)
mockSendMessage = protoSpy('sendMessage').mockResolvedValue(mockMessage)
mockSendDirectMessage = protoSpy('sendDirectMessage').mockResolvedValue(mockMessage)
mockListMessages = protoSpy('listMessages').mockResolvedValue([mockMessage, mockMessage2])
mockGetMessage = protoSpy('getMessage').mockResolvedValue(mockMessage)
mockDeleteMessage = protoSpy('deleteMessage').mockResolvedValue(undefined)
mockEditMessage = protoSpy('editMessage').mockResolvedValue({ ...mockMessage, text: 'Updated message' })
mockSetTyping = protoSpy('setTyping').mockResolvedValue(undefined)
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
})
afterEach(() => {
consoleLogSpy.mockRestore()
consoleErrorSpy.mockRestore()
processExitSpy.mockRestore()
for (const s of protoSpies) s.mockRestore()
protoSpies.length = 0
})
it('calls sendMessage with correct args and outputs result', async () => {
await sendAction('space_456', 'Hello world', { pretty: false })
expect(mockSendMessage).toHaveBeenCalledWith('space_456', 'Hello world', {
markdown: undefined,
})
expect(consoleLogSpy).toHaveBeenCalled()
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output.id).toBe(messageId)
expect(output.ref).toBe('msg_123')
expect(output.roomId).toBe(roomId)
expect(output.roomRef).toBe('space_456')
expect(output.personEmail).toBe('user@example.com')
expect(mockDispose).toHaveBeenCalled()
})
it('passes markdown option when --markdown flag is set on send', async () => {
await sendAction('space_456', '**bold**', { markdown: true, pretty: false })
expect(mockSendMessage).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
})
it('disposes the client when send fails', async () => {
mockSendMessage.mockRejectedValue(new WebexError('Send failed', 'send_failed'))
await sendAction('space_456', 'Hello world', { pretty: false })
expect(mockDispose).toHaveBeenCalled()
})
it('exits with code 1 when not authenticated on send', async () => {
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
await sendAction('space_456', 'Hello', { pretty: false })
expect(mockSendMessage).not.toHaveBeenCalled()
expect(processExitSpy).toHaveBeenCalledWith(1)
})
it('calls sendDirectMessage with email and text', async () => {
await dmAction('alice@example.com', 'Hello!', { pretty: false })
expect(mockSendDirectMessage).toHaveBeenCalledWith('alice@example.com', 'Hello!', {
markdown: undefined,
})
expect(consoleLogSpy).toHaveBeenCalled()
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output.ref).toBe('msg_123')
})
it('passes markdown option to sendDirectMessage when --markdown flag is set', async () => {
await dmAction('alice@example.com', '**bold**', { markdown: true, pretty: false })
expect(mockSendDirectMessage).toHaveBeenCalledWith('alice@example.com', '**bold**', {
markdown: true,
})
})
it('calls listMessages with limit and outputs array', async () => {
await listAction('space_456', { limit: 50, pretty: false })
expect(mockListMessages).toHaveBeenCalledWith('space_456', { max: 50 })
expect(consoleLogSpy).toHaveBeenCalled()
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output[0].ref).toBe('msg_123')
expect(output[1].ref).toBe('msg_124')
expect(output[0].html).toContain('https://example.com')
})
it('calls getMessage with correct id and outputs result', async () => {
await getAction('msg_123', { pretty: false })
expect(mockGetMessage).toHaveBeenCalledWith('msg_123')
expect(consoleLogSpy).toHaveBeenCalled()
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output.ref).toBe('msg_123')
expect(output.personEmail).toBe('user@example.com')
expect(output.html).toContain('https://example.com')
})
it('calls deleteMessage and outputs deleted id when --force flag is set', async () => {
await deleteAction('msg_123', { force: true, pretty: false })
expect(mockDeleteMessage).toHaveBeenCalledWith('msg_123')
expect(consoleLogSpy).toHaveBeenCalled()
const output = consoleLogSpy.mock.calls[0][0]
expect(output).toContain('deleted')
expect(output).toContain('msg_123')
})
it('shows warning and does not delete without --force flag', async () => {
try {
await deleteAction('msg_123', { force: false, pretty: false })
} catch {}
expect(mockDeleteMessage).not.toHaveBeenCalled()
expect(consoleLogSpy).toHaveBeenCalled()
const output = consoleLogSpy.mock.calls[0][0]
expect(output).toContain('warning')
expect(output).toContain('--force')
})
it('calls editMessage with roomId in args and outputs result', async () => {
await editAction('msg_123', 'space_456', 'Updated message', { pretty: false })
expect(mockEditMessage).toHaveBeenCalledWith('msg_123', 'space_456', 'Updated message', {
markdown: undefined,
})
expect(consoleLogSpy).toHaveBeenCalled()
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output.ref).toBe('msg_123')
expect(output.text).toBe('Updated message')
expect(mockDispose).toHaveBeenCalled()
})
it('passes markdown option to editMessage when --markdown flag is set', async () => {
await editAction('msg_123', 'space_456', '**updated**', { markdown: true, pretty: false })
expect(mockEditMessage).toHaveBeenCalledWith('msg_123', 'space_456', '**updated**', {
markdown: true,
})
})
it('calls setTyping with start-typing by default and outputs result', async () => {
await typingAction('space_456', { pretty: false })
expect(mockSetTyping).toHaveBeenCalledWith('space_456', true)
expect(consoleLogSpy).toHaveBeenCalled()
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output).toEqual({ spaceId: 'space_456', typing: true })
expect(mockDispose).toHaveBeenCalled()
})
it('calls setTyping with stop-typing when --stop flag is set', async () => {
await typingAction('space_456', { stop: true, pretty: false })
expect(mockSetTyping).toHaveBeenCalledWith('space_456', false)
const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
expect(output).toEqual({ spaceId: 'space_456', typing: false })
})
it('disposes the client when setTyping fails', async () => {
mockSetTyping.mockRejectedValue(new WebexError('Typing failed', 'unsupported'))
await typingAction('space_456', { pretty: false })
expect(mockDispose).toHaveBeenCalled()
})