diff --git a/tests/cookies.test.ts b/tests/cookies.test.ts index 7f7e646..85a7b47 100644 --- a/tests/cookies.test.ts +++ b/tests/cookies.test.ts @@ -136,4 +136,44 @@ describe('cookies', () => { ); }); }); + + describe('extractCookiesFromChrome', () => { + it('returns cookies when sqlite yields hex values', async () => { + const fs = await import('node:fs'); + const { execSync } = await import('node:child_process'); + // Pretend Chrome cookie DB exists + (fs.existsSync as unknown as vi.Mock).mockImplementation((path: string) => path.includes('Cookies')); + (fs.copyFileSync as unknown as vi.Mock).mockImplementation(() => {}); + // sqlite output with hex strings ("test_auth", "test_ct0") + (execSync as unknown as vi.Mock).mockImplementation((cmd: string) => { + if (cmd.includes('sqlite3')) { + return 'auth_token|746573745f61757468\nct0|746573745f637430'; + } + return ''; + }); + + const { extractCookiesFromChrome } = await import('../src/lib/cookies.js'); + const result = await extractCookiesFromChrome('Default'); + + expect(result.cookies.authToken).toBe('test_auth'); + expect(result.cookies.ct0).toBe('test_ct0'); + expect(result.cookies.source).toContain('Chrome'); + expect(result.warnings).toHaveLength(0); + }); + + it('warns when Chrome DB exists but contains no cookies', async () => { + const fs = await import('node:fs'); + const { execSync } = await import('node:child_process'); + (fs.existsSync as unknown as vi.Mock).mockImplementation((path: string) => path.includes('Cookies')); + (fs.copyFileSync as unknown as vi.Mock).mockImplementation(() => {}); + (execSync as unknown as vi.Mock).mockReturnValue(''); + + const { extractCookiesFromChrome } = await import('../src/lib/cookies.js'); + const result = await extractCookiesFromChrome('Default'); + + expect(result.cookies.authToken).toBeNull(); + expect(result.cookies.ct0).toBeNull(); + expect(result.warnings.some((w) => w.includes('No Twitter cookies found in Chrome'))).toBe(true); + }); + }); }); diff --git a/tests/twitter-client.test.ts b/tests/twitter-client.test.ts index 13f878d..d7dad16 100644 --- a/tests/twitter-client.test.ts +++ b/tests/twitter-client.test.ts @@ -374,4 +374,116 @@ describe('TwitterClient', () => { expect(mockFetch).toHaveBeenCalledTimes(5); }); }); + + describe('conversation helpers', () => { + let mockFetch: ReturnType; + + beforeEach(() => { + mockFetch = vi.fn(); + global.fetch = mockFetch as unknown as typeof fetch; + }); + + const makeConversationPayload = () => ({ + data: { + threaded_conversation_with_injections_v2: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + tweet_results: { + result: { + rest_id: '1', + legacy: { + full_text: 'root', + created_at: '2024-01-01T00:00:00Z', + reply_count: 0, + retweet_count: 0, + favorite_count: 0, + conversation_id_str: '1', + }, + core: { user_results: { result: { legacy: { screen_name: 'root', name: 'Root' } } } }, + }, + }, + }, + }, + }, + { + content: { + itemContent: { + tweet_results: { + result: { + rest_id: '2', + legacy: { + full_text: 'child reply', + created_at: '2024-01-02T00:00:00Z', + reply_count: 0, + retweet_count: 0, + favorite_count: 0, + conversation_id_str: '1', + in_reply_to_status_id_str: '1', + }, + core: { user_results: { result: { legacy: { screen_name: 'child', name: 'Child' } } } }, + }, + }, + }, + }, + }, + ], + }, + ], + }, + }, + }); + + it('getReplies returns only replies to tweet', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => makeConversationPayload(), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getReplies('1'); + + expect(result.success).toBe(true); + expect(result.tweets?.length).toBe(1); + expect(result.tweets?.[0].id).toBe('2'); + }); + + it('getThread returns sorted thread by createdAt', async () => { + const payload = makeConversationPayload(); + // swap dates to verify sorting + payload.data.threaded_conversation_with_injections_v2.instructions[0]?.entries?.[0]?.content?.itemContent?.tweet_results?.result?.legacy && + ((payload.data.threaded_conversation_with_injections_v2.instructions[0]!.entries![0]!.content!.itemContent! + .tweet_results!.result!.legacy!.created_at = '2024-01-03T00:00:00Z')); + + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => payload, + }); + + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getThread('2'); + + expect(result.success).toBe(true); + expect(result.tweets?.map((t) => t.id)).toEqual(['2', '1']); // sorted by createdAt asc + }); + + it('propagates fetchTweetDetail errors', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + text: async () => 'oops', + }); + + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getThread('1'); + + expect(result.success).toBe(false); + expect(result.error).toContain('HTTP 500'); + }); + }); });