From 97c73e939faffb2e4a17c6ca42c95a5c4ff6f036 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 11:25:32 +0100 Subject: [PATCH] test: cover following/followers --- tests/cli-args.test.ts | 4 + tests/twitter-client-coverage.api.test.ts | 63 ++++++++ tests/twitter-client.test.ts | 189 ++++++++++++++++++++++ 3 files changed, 256 insertions(+) diff --git a/tests/cli-args.test.ts b/tests/cli-args.test.ts index 6aeef7b..1927203 100644 --- a/tests/cli-args.test.ts +++ b/tests/cli-args.test.ts @@ -11,6 +11,10 @@ describe('cli-args', () => { 'thread', 'search', 'mentions', + 'bookmarks', + 'following', + 'followers', + 'likes', 'help', 'whoami', 'check', diff --git a/tests/twitter-client-coverage.api.test.ts b/tests/twitter-client-coverage.api.test.ts index 7083dc6..0938077 100644 --- a/tests/twitter-client-coverage.api.test.ts +++ b/tests/twitter-client-coverage.api.test.ts @@ -17,6 +17,8 @@ type ResponseLike = { type TwitterClientApiPrivate = TwitterClient & { getBookmarksQueryIds: () => Promise; + getFollowingQueryIds: () => Promise; + getFollowersQueryIds: () => Promise; }; const makeResponse = (overrides: Partial = {}): ResponseLike => ({ @@ -204,4 +206,65 @@ describe('TwitterClient API coverage', () => { expect(result.error).toBe('HTTP 404'); }); }); + + describe('following/followers error paths', () => { + 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.getFollowingQueryIds = async () => ['test']; + + const result = await client.getFollowing('123', 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.getFollowersQueryIds = async () => ['test']; + + const result = await client.getFollowers('123', 1); + + expect(result.success).toBe(false); + expect(result.error).toBe('bad'); + }); + + 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.getFollowingQueryIds = async () => []; + + const result = await client.getFollowing('123', 1); + + expect(result.success).toBe(false); + expect(result.error).toBe('Unknown error fetching following'); + }); + + 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.getFollowersQueryIds = async () => ['test']; + + const result = await client.getFollowers('123', 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..78346e8 100644 --- a/tests/twitter-client.test.ts +++ b/tests/twitter-client.test.ts @@ -1144,6 +1144,195 @@ describe('TwitterClient', () => { }); }); + describe('following/followers', () => { + let mockFetch: ReturnType; + + const makeUserResult = (id: string, username: string, name = username) => ({ + __typename: 'User', + rest_id: id, + legacy: { + screen_name: username, + name, + description: `bio-${id}`, + followers_count: 10, + friends_count: 5, + profile_image_url_https: `https://example.com/${id}.jpg`, + created_at: '2024-01-01T00:00:00Z', + }, + }); + + beforeEach(() => { + mockFetch = vi.fn(); + global.fetch = mockFetch as unknown as typeof fetch; + }); + + it('fetches following users and filters invalid entries', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: { + user: { + result: { + timeline: { + timeline: { + instructions: [ + { + type: 'TimelineAddEntries', + entries: [ + { + content: { + itemContent: { + user_results: { + result: makeUserResult('1', 'alpha', 'Alpha'), + }, + }, + }, + }, + { + content: { + itemContent: { + user_results: { + result: { __typename: 'User', rest_id: '2' }, + }, + }, + }, + }, + { + content: { + itemContent: { + user_results: { + result: { __typename: 'TimelineUser' }, + }, + }, + }, + }, + ], + }, + ], + }, + }, + }, + }, + }, + }), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClient & { getFollowingQueryIds: () => Promise }; + clientPrivate.getFollowingQueryIds = async () => ['test']; + + const result = await client.getFollowing('123', 2); + + expect(result.success).toBe(true); + expect(result.users?.length).toBe(1); + expect(result.users?.[0].username).toBe('alpha'); + const [url] = mockFetch.mock.calls[0]; + expect(String(url)).toContain('/Following?'); + }); + + it('fetches followers and unwraps visibility results', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: { + user: { + result: { + timeline: { + timeline: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + user_results: { + result: { + __typename: 'UserWithVisibilityResults', + user: makeUserResult('9', 'vis', 'Visible'), + }, + }, + }, + }, + }, + ], + }, + ], + }, + }, + }, + }, + }, + }), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClient & { getFollowersQueryIds: () => Promise }; + clientPrivate.getFollowersQueryIds = async () => ['test']; + + const result = await client.getFollowers('123', 1); + + expect(result.success).toBe(true); + expect(result.users?.[0].username).toBe('vis'); + const [url] = mockFetch.mock.calls[0]; + expect(String(url)).toContain('/Followers?'); + }); + + it('refreshes query IDs after 404s', async () => { + mockFetch + .mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'nope' }) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: { + user: { + result: { + timeline: { + timeline: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + user_results: { + result: makeUserResult('1', 'alpha', 'Alpha'), + }, + }, + }, + }, + ], + }, + ], + }, + }, + }, + }, + }, + }), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClient & { + getFollowingQueryIds: () => Promise; + refreshQueryIds: () => Promise; + }; + clientPrivate.getFollowingQueryIds = async () => ['test']; + let refreshed = false; + clientPrivate.refreshQueryIds = async () => { + refreshed = true; + }; + + const result = await client.getFollowing('123', 1); + + expect(refreshed).toBe(true); + expect(result.success).toBe(true); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + }); + describe('conversation helpers', () => { let mockFetch: ReturnType;