From 1e6dda961dd2b5490b39396c75d319a217ef0a21 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 09:54:53 +0100 Subject: [PATCH] test: cover likes timeline handling --- tests/twitter-client-coverage.api.test.ts | 91 +++++++++++++++++++ tests/twitter-client.test.ts | 104 ++++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/tests/twitter-client-coverage.api.test.ts b/tests/twitter-client-coverage.api.test.ts index 7083dc6..38027c0 100644 --- a/tests/twitter-client-coverage.api.test.ts +++ b/tests/twitter-client-coverage.api.test.ts @@ -17,6 +17,12 @@ type ResponseLike = { type TwitterClientApiPrivate = TwitterClient & { getBookmarksQueryIds: () => Promise; + getLikesQueryIds: () => Promise; + getCurrentUser: () => Promise<{ + success: boolean; + user?: { id: string; username: string; name: string }; + error?: string; + }>; }; const makeResponse = (overrides: Partial = {}): ResponseLike => ({ @@ -204,4 +210,89 @@ describe('TwitterClient API coverage', () => { expect(result.error).toBe('HTTP 404'); }); }); + + describe('likes error paths', () => { + const stubCurrentUser = async () => ({ + success: true, + user: { id: '123', username: 'tester', name: 'Tester' }, + }); + + it('returns an error for non-ok responses', async () => { + const mockFetch = vi + .fn() + .mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' })); + global.fetch = mockFetch as unknown as typeof fetch; + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientApiPrivate; + clientPrivate.getCurrentUser = stubCurrentUser; + clientPrivate.getLikesQueryIds = async () => ['test']; + + const result = await client.getLikes(1); + + expect(result.success).toBe(false); + expect(result.error).toContain('HTTP 500'); + }); + + it('returns API errors from payloads', async () => { + const mockFetch = vi.fn().mockResolvedValueOnce( + makeResponse({ + json: async () => ({ errors: [{ message: 'bad' }] }), + }), + ); + global.fetch = mockFetch as unknown as typeof fetch; + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientApiPrivate; + clientPrivate.getCurrentUser = stubCurrentUser; + clientPrivate.getLikesQueryIds = async () => ['test']; + + const result = await client.getLikes(1); + + expect(result.success).toBe(false); + expect(result.error).toBe('bad'); + }); + + it('returns an error when fetching throws', async () => { + const mockFetch = vi.fn().mockRejectedValue(new Error('boom')); + global.fetch = mockFetch as unknown as typeof fetch; + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientApiPrivate; + clientPrivate.getCurrentUser = stubCurrentUser; + clientPrivate.getLikesQueryIds = async () => ['test']; + + const result = await client.getLikes(1); + + expect(result.success).toBe(false); + expect(result.error).toContain('boom'); + }); + + it('returns unknown error when no query ids are available', async () => { + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientApiPrivate; + clientPrivate.getCurrentUser = stubCurrentUser; + clientPrivate.getLikesQueryIds = async () => []; + + const result = await client.getLikes(1); + + expect(result.success).toBe(false); + expect(result.error).toBe('Unknown error fetching likes'); + }); + + it('returns the second attempt error after 404s', async () => { + const mockFetch = vi.fn().mockResolvedValue(makeResponse({ ok: false, status: 404, text: async () => 'nope' })); + global.fetch = mockFetch as unknown as typeof fetch; + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientApiPrivate; + clientPrivate.getCurrentUser = stubCurrentUser; + clientPrivate.getLikesQueryIds = async () => ['test']; + + const result = await client.getLikes(1); + + expect(result.success).toBe(false); + expect(result.error).toBe('HTTP 404'); + }); + }); }); diff --git a/tests/twitter-client.test.ts b/tests/twitter-client.test.ts index f4f3fe9..ef410c1 100644 --- a/tests/twitter-client.test.ts +++ b/tests/twitter-client.test.ts @@ -9,6 +9,14 @@ describe('TwitterClient', () => { cookieHeader: 'auth_token=test_auth_token; ct0=test_ct0_token', source: 'test', }; + type TwitterClientPrivate = TwitterClient & { + getCurrentUser: () => Promise<{ + success: boolean; + user?: { id: string; username: string; name: string }; + error?: string; + }>; + getLikesQueryIds: () => Promise; + }; afterEach(() => { global.fetch = originalFetch; @@ -1144,6 +1152,102 @@ describe('TwitterClient', () => { }); }); + describe('likes', () => { + let mockFetch: ReturnType; + + beforeEach(() => { + mockFetch = vi.fn(); + global.fetch = mockFetch as unknown as typeof fetch; + }); + + it('fetches likes and parses tweet results', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: { + user: { + result: { + timeline: { + timeline: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + tweet_results: { + result: { + rest_id: '2', + legacy: { + full_text: 'liked', + created_at: '2024-01-01T00:00:00Z', + reply_count: 0, + retweet_count: 0, + favorite_count: 0, + conversation_id_str: '2', + }, + core: { + user_results: { + result: { + rest_id: 'u2', + legacy: { screen_name: 'root', name: 'Root' }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, + ], + }, + }, + }, + }, + }, + }), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientPrivate; + clientPrivate.getCurrentUser = async () => ({ + success: true, + user: { id: '42', username: 'tester', name: 'Tester' }, + }); + clientPrivate.getLikesQueryIds = async () => ['test']; + + const result = await client.getLikes(2); + + expect(result.success).toBe(true); + expect(result.tweets?.[0].id).toBe('2'); + expect(mockFetch).toHaveBeenCalledTimes(1); + + const [url, options] = mockFetch.mock.calls[0]; + expect(options.method).toBe('GET'); + expect(String(url)).toContain('/Likes?'); + const parsedVars = JSON.parse(new URL(url as string).searchParams.get('variables') as string); + expect(parsedVars.userId).toBe('42'); + expect(parsedVars.count).toBe(2); + const parsedFeatures = JSON.parse(new URL(url as string).searchParams.get('features') as string); + expect(parsedFeatures.graphql_timeline_v2_bookmark_timeline).toBeUndefined(); + }); + + it('returns an error when current user is unavailable', async () => { + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientPrivate; + clientPrivate.getCurrentUser = async () => ({ success: false, error: 'no user' }); + + const result = await client.getLikes(1); + + expect(result.success).toBe(false); + expect(result.error).toBe('no user'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + describe('conversation helpers', () => { let mockFetch: ReturnType;