import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:test' const originalConsoleLog = console.log import type { Command } from 'commander' import { InstagramClient } from '../client' import * as sharedModule from './shared' const mockGetMessages = mock(() => Promise.resolve([{ id: 'msg-1', text: 'Hello' }])) const mockSendMessage = mock(() => Promise.resolve({ id: 'msg-2', text: 'Sent' })) const mockSendMessageToUser = mock(() => Promise.resolve({ id: 'msg-3', text: 'Sent to user' })) const mockSearchMessages = mock(() => Promise.resolve([{ id: 'msg-4', text: 'Found' }])) const mockSearchUsers = mock(() => Promise.resolve([{ pk: '999', username: 'targetuser' }])) const mockClient = { getMessages: mockGetMessages, sendMessage: mockSendMessage, sendMessageToUser: mockSendMessageToUser, searchMessages: mockSearchMessages, searchUsers: mockSearchUsers, } import { messageCommand } from './message' function resetCommandState(cmd: Command): void { for (const sub of cmd.commands) { ;( sub as unknown as { _optionValues: Record; _optionValueSources: Record } )._optionValues = {} ;( sub as unknown as { _optionValues: Record; _optionValueSources: Record } )._optionValueSources = {} } } describe('message commands', () => { let consoleLogSpy: ReturnType let processExitSpy: ReturnType let withInstagramClientSpy: ReturnType beforeEach(() => { resetCommandState(messageCommand) mockGetMessages.mockReset() mockSendMessage.mockReset() mockSendMessageToUser.mockReset() mockSearchMessages.mockReset() mockSearchUsers.mockReset() mockGetMessages.mockImplementation(() => Promise.resolve([{ id: 'msg-1', text: 'Hello' }])) mockSendMessage.mockImplementation(() => Promise.resolve({ id: 'msg-2', text: 'Sent' })) mockSendMessageToUser.mockImplementation(() => Promise.resolve({ id: 'msg-3', text: 'Sent to user' })) mockSearchMessages.mockImplementation(() => Promise.resolve([{ id: 'msg-4', text: 'Found' }])) mockSearchUsers.mockImplementation(() => Promise.resolve([{ pk: '999', username: 'targetuser' }])) consoleLogSpy = mock((..._args: unknown[]) => {}) console.log = consoleLogSpy withInstagramClientSpy = spyOn(sharedModule, 'withInstagramClient').mockImplementation(async (_options, fn) => { return fn(Object.assign(Object.create(InstagramClient.prototype), mockClient) as InstagramClient) }) processExitSpy = spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called') }) }) afterEach(() => { console.log = originalConsoleLog withInstagramClientSpy.mockRestore() processExitSpy.mockRestore() }) describe('list', () => { it('lists messages from a thread', async () => { await expect(messageCommand.parseAsync(['list', 'thread-123'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockGetMessages).toHaveBeenCalledWith('thread-123', 25) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toEqual([{ id: 'msg-1', text: 'Hello' }]) }) it('passes custom limit', async () => { await expect( messageCommand.parseAsync(['list', 'thread-123', '--limit', '10'], { from: 'user' }), ).rejects.toThrow('process.exit called') expect(mockGetMessages).toHaveBeenCalledWith('thread-123', 10) }) }) describe('send', () => { it('sends a message to a thread', async () => { await expect(messageCommand.parseAsync(['send', 'thread-123', 'Hello world'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockSendMessage).toHaveBeenCalledWith('thread-123', 'Hello world') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toEqual({ id: 'msg-2', text: 'Sent' }) }) }) describe('send-to', () => { it('sends a message to a user by username', async () => { await expect(messageCommand.parseAsync(['send-to', 'targetuser', 'Hi there'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockSearchUsers).toHaveBeenCalledWith('targetuser') expect(mockSendMessageToUser).toHaveBeenCalledWith('999', 'Hi there') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toEqual({ id: 'msg-3', text: 'Sent to user' }) }) it('strips @ prefix from username', async () => { await expect(messageCommand.parseAsync(['send-to', '@targetuser', 'Hi there'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(mockSearchUsers).toHaveBeenCalledWith('targetuser') }) it('handles user not found error', async () => { mockSearchUsers.mockImplementation(() => Promise.resolve([])) try { await messageCommand.parseAsync(['send-to', 'unknownuser', 'Hi'], { from: 'user' }) } catch { /* empty */ } expect(mockSendMessageToUser).not.toHaveBeenCalled() }) }) describe('search', () => { it('searches messages by query', async () => { await expect(messageCommand.parseAsync(['search', 'hello'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockSearchMessages).toHaveBeenCalledWith('hello', { threadId: undefined, limit: 20 }) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toEqual([{ id: 'msg-4', text: 'Found' }]) }) it('passes thread option to search', async () => { await expect( messageCommand.parseAsync(['search', 'hello', '--thread', 'thread-456'], { from: 'user' }), ).rejects.toThrow('process.exit called') expect(mockSearchMessages).toHaveBeenCalledWith('hello', { threadId: 'thread-456', limit: 20 }) }) it('passes limit option to search', async () => { await expect(messageCommand.parseAsync(['search', 'hello2', '--limit', '5'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(mockSearchMessages).toHaveBeenCalledWith('hello2', { threadId: undefined, limit: 5 }) }) }) describe('search-users', () => { it('searches users by query', async () => { await expect(messageCommand.parseAsync(['search-users', 'target'], { from: 'user' })).rejects.toThrow( 'process.exit called', ) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockSearchUsers).toHaveBeenCalledWith('target') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toEqual([{ pk: '999', username: 'targetuser' }]) }) }) })