mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
60dbbd4baa
* perf: replace document.cookie reads with page.getCookies({domain}) (Tier 1 cookie API sweep)
Prior pattern in 25 adapter files round-tripped through `page.evaluate(\`document.cookie.split…\`)` to extract a single cookie value (CSRF token, session ID, etc.). CDP's `page.getCookies({domain})` reads the cookie store directly with zero JS-execution overhead.
Files touched (sites: twitter / linkedin / maimai / youtube):
- twitter (15): thread, timeline, list-add, bookmark-folders, following, list-tweets, bookmarks, list-remove, tweets, bookmark-folder, likes, lists, trending — direct 4-line replacement (cookie was outside `page.evaluate`); article, profile — hoisted ct0 read OUT of `page.evaluate` and threw `AuthRequiredError` upfront so unreachable in-evaluate auth branches got cleaned up too.
- linkedin/search.js — JSESSIONID was read inside the per-batch fetch loop's `page.evaluate`; hoisted once before the loop and pass `csrf` value into the template via `JSON.stringify`.
- maimai/search-talents.js — csrftoken cookie hoisted via getCookies; meta-tag fallback preserved inside `page.evaluate` (reached only when no cookie). Also converted the `page.evaluate(async (body) => …, body)` Playwright-style call to OpenCLI's template-string form so the helper actually runs.
- youtube — `SAPISID_HASH_FN` (used by like / unlike / subscribe / unsubscribe) reworked: sapisid is now passed in as a parameter; new `readYoutubeSapisid(page)` helper reads it via CDP. The HMAC-SHA1 compute still happens browser-side (Web Crypto), only the cookie read is hoisted.
Tests updated where mocks specifically referenced `document.cookie` (twitter following / bookmark-folder / bookmark-folders) to mock `getCookies` instead.
Verification:
- `npx tsc --noEmit` clean
- `npx vitest run clis/twitter clis/linkedin clis/youtube` → 264/264 pass
- typed-error-lint 189/189 (no new violations)
- silent-column-drop 103/103 (no new violations)
Scope notes (not in this PR):
- `goto + wait` redundancy and `browserSession: { reuse: 'site' }` rollout are scoped to follow-up PRs B and C per the #OpenCLI:3889b5cf thread plan.
- `document.cookie.match(...)` patterns (instagram 8 / xiaoe / qwen / hupu / tiktok / 1point3acres — ~13 files) are outside the original \`document.cookie.split\` audit scope and will follow as a Tier 1 expansion sweep.
* fix(adapters): read auth cookies by url scope
153 lines
5.8 KiB
JavaScript
153 lines
5.8 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import { __test__ } from './bookmark-folders.js';
|
|
|
|
const { parseBookmarkFolders, buildUrl } = __test__;
|
|
|
|
describe('twitter bookmark-folders parser', () => {
|
|
it('returns [] for empty payload', () => {
|
|
expect(parseBookmarkFolders({}, new Set())).toEqual([]);
|
|
expect(parseBookmarkFolders({ data: {} }, new Set())).toEqual([]);
|
|
});
|
|
|
|
it('extracts folders from the modern viewer.bookmark_collections_slice envelope', () => {
|
|
const data = {
|
|
data: {
|
|
viewer: {
|
|
bookmark_collections_slice: {
|
|
items: [
|
|
{
|
|
bookmarkCollection: {
|
|
id_str: '1234567890',
|
|
name: 'Reading list',
|
|
bookmarks_count: 42,
|
|
created_at: '2025-09-15T10:00:00.000Z',
|
|
},
|
|
},
|
|
{
|
|
bookmarkCollection: {
|
|
id_str: '9876543210',
|
|
name: 'Recipes',
|
|
bookmarks_count: 7,
|
|
created_at: '2026-01-03T03:14:00.000Z',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
expect(parseBookmarkFolders(data, new Set())).toEqual([
|
|
{ id: '1234567890', name: 'Reading list', items: 42, created_at: '2025-09-15T10:00:00.000Z' },
|
|
{ id: '9876543210', name: 'Recipes', items: 7, created_at: '2026-01-03T03:14:00.000Z' },
|
|
]);
|
|
});
|
|
|
|
it('falls back to legacy viewer_v2 envelope', () => {
|
|
const data = {
|
|
data: {
|
|
viewer_v2: {
|
|
user_results: {
|
|
result: {
|
|
bookmark_collections_slice: {
|
|
items: [{ bookmarkCollection: { id: 'abc', name: 'Old', count: 3 } }],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
expect(parseBookmarkFolders(data, new Set())).toEqual([
|
|
{ id: 'abc', name: 'Old', items: 3, created_at: '' },
|
|
]);
|
|
});
|
|
|
|
it('falls back to flat bookmark_collections_slice envelope', () => {
|
|
const data = {
|
|
data: {
|
|
bookmark_collections_slice: {
|
|
items: [{ id_str: '5', name: 'Flat', bookmarks_count: 1, created_at: '2024-01-01' }],
|
|
},
|
|
},
|
|
};
|
|
expect(parseBookmarkFolders(data, new Set())).toEqual([
|
|
{ id: '5', name: 'Flat', items: 1, created_at: '2024-01-01' },
|
|
]);
|
|
});
|
|
|
|
it('deduplicates folders by id across the seen Set', () => {
|
|
const data = {
|
|
data: {
|
|
viewer: {
|
|
bookmark_collections_slice: {
|
|
items: [
|
|
{ bookmarkCollection: { id_str: '1', name: 'A', bookmarks_count: 0 } },
|
|
{ bookmarkCollection: { id_str: '1', name: 'A again', bookmarks_count: 0 } },
|
|
{ bookmarkCollection: { id_str: '2', name: 'B', bookmarks_count: 0 } },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
expect(parseBookmarkFolders(data, new Set())).toEqual([
|
|
{ id: '1', name: 'A', items: 0, created_at: '' },
|
|
{ id: '2', name: 'B', items: 0, created_at: '' },
|
|
]);
|
|
});
|
|
|
|
it('coerces missing items count to 0', () => {
|
|
const data = {
|
|
data: {
|
|
viewer: {
|
|
bookmark_collections_slice: {
|
|
items: [{ bookmarkCollection: { id: '1', name: 'No count' } }],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
expect(parseBookmarkFolders(data, new Set())[0].items).toBe(0);
|
|
});
|
|
|
|
it('skips entries without an id', () => {
|
|
const data = {
|
|
data: {
|
|
viewer: {
|
|
bookmark_collections_slice: {
|
|
items: [
|
|
{ bookmarkCollection: { name: 'Anonymous' } },
|
|
{ bookmarkCollection: { id: '1', name: 'OK' } },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
expect(parseBookmarkFolders(data, new Set())).toEqual([
|
|
{ id: '1', name: 'OK', items: 0, created_at: '' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('twitter bookmark-folders URL builder', () => {
|
|
it('encodes the empty variables object and includes the queryId in the path', () => {
|
|
const url = buildUrl('queryid123');
|
|
expect(url).toContain('/i/api/graphql/queryid123/bookmarkFoldersSlice');
|
|
expect(url).toContain('variables=' + encodeURIComponent('{}'));
|
|
expect(url).toContain('features=');
|
|
});
|
|
});
|
|
|
|
describe('twitter bookmark-folders command (registry)', () => {
|
|
it('throws AuthRequiredError when ct0 cookie is missing', async () => {
|
|
const command = getRegistry().get('twitter/bookmark-folders');
|
|
expect(command?.func).toBeTypeOf('function');
|
|
const page = {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
getCookies: vi.fn().mockResolvedValue([]), // no ct0 cookie → AuthRequired
|
|
evaluate: vi.fn().mockResolvedValue(null),
|
|
};
|
|
await expect(command.func(page, {})).rejects.toThrow(/Not logged into x.com/);
|
|
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
|
|
});
|
|
});
|