From 9cfeda13b2e9baa070e081942d66a4568ab77e51 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 06:44:42 +0000 Subject: [PATCH] test: add search CLI pagination checks --- tests/commands.search.test.ts | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/commands.search.test.ts diff --git a/tests/commands.search.test.ts b/tests/commands.search.test.ts new file mode 100644 index 0000000..3163829 --- /dev/null +++ b/tests/commands.search.test.ts @@ -0,0 +1,119 @@ +import { Command } from 'commander'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { CliContext } from '../src/cli/shared.js'; +import { registerSearchCommands } from '../src/commands/search.js'; +import { TwitterClient } from '../src/lib/twitter-client.js'; + +describe('search command', () => { + let program: Command; + let mockContext: Partial; + + beforeEach(() => { + program = new Command(); + mockContext = { + resolveTimeoutFromOptions: () => undefined, + resolveQuoteDepthFromOptions: () => undefined, + resolveCredentialsFromOptions: async () => ({ + cookies: { authToken: 'auth', ct0: 'ct0', cookieHeader: 'auth=auth; ct0=ct0' }, + warnings: [], + }), + p: () => '', + printTweetsResult: vi.fn(), + }; + }); + + it('requires --all or --cursor when --max-pages is provided', async () => { + registerSearchCommands(program, mockContext as CliContext); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`exit ${code}`); + }) as never); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + try { + await expect(program.parseAsync(['node', 'bird', 'search', 'cats', '--max-pages', '2'])).rejects.toThrow( + 'exit 1', + ); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--max-pages requires --all or --cursor')); + } finally { + exitSpy.mockRestore(); + errorSpy.mockRestore(); + } + }); + + it('requires positive --count when not paging', async () => { + registerSearchCommands(program, mockContext as CliContext); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`exit ${code}`); + }) as never); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + try { + await expect(program.parseAsync(['node', 'bird', 'search', 'cats', '--count', '0'])).rejects.toThrow('exit 1'); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --count. Expected a positive integer.')); + } finally { + exitSpy.mockRestore(); + errorSpy.mockRestore(); + } + }); + + it('requires positive --max-pages when paging', async () => { + registerSearchCommands(program, mockContext as CliContext); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`exit ${code}`); + }) as never); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + try { + await expect(program.parseAsync(['node', 'bird', 'search', 'cats', '--all', '--max-pages', '0'])).rejects.toThrow( + 'exit 1', + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid --max-pages. Expected a positive integer.'), + ); + } finally { + exitSpy.mockRestore(); + errorSpy.mockRestore(); + } + }); + + it('uses paged search when --all is set', async () => { + registerSearchCommands(program, mockContext as CliContext); + const getAllSpy = vi + .spyOn(TwitterClient.prototype, 'getAllSearchResults') + .mockResolvedValue({ success: true, tweets: [] }); + const searchSpy = vi.spyOn(TwitterClient.prototype, 'search').mockResolvedValue({ success: true, tweets: [] }); + + try { + await program.parseAsync(['node', 'bird', 'search', 'cats', '--all', '--json']); + expect(getAllSpy).toHaveBeenCalledWith('cats', expect.objectContaining({ includeRaw: false })); + expect(searchSpy).not.toHaveBeenCalled(); + expect(mockContext.printTweetsResult).toHaveBeenCalledWith(expect.objectContaining({ tweets: [] }), { + json: true, + usePagination: true, + emptyMessage: 'No tweets found.', + }); + } finally { + getAllSpy.mockRestore(); + searchSpy.mockRestore(); + } + }); + + it('uses cursor pagination when --cursor is set', async () => { + registerSearchCommands(program, mockContext as CliContext); + const getAllSpy = vi + .spyOn(TwitterClient.prototype, 'getAllSearchResults') + .mockResolvedValue({ success: true, tweets: [] }); + + try { + await program.parseAsync(['node', 'bird', 'search', 'cats', '--cursor', 'cursor-1']); + expect(getAllSpy).toHaveBeenCalledWith('cats', expect.objectContaining({ cursor: 'cursor-1' })); + expect(mockContext.printTweetsResult).toHaveBeenCalledWith(expect.objectContaining({ tweets: [] }), { + json: false, + usePagination: true, + emptyMessage: 'No tweets found.', + }); + } finally { + getAllSpy.mockRestore(); + } + }); +});