import { describe, expect, it } from 'vitest'; import { CommandExecutionError } from '@jackwener/opencli/errors'; import { assertReadableUserSnapshot } from './user.js'; import { buildXhsNoteUrl, extractXhsUserNotes, flattenXhsNoteGroups, normalizeXhsUserId, } from './user-helpers.js'; describe('normalizeXhsUserId', () => { it('extracts the profile id from a full Xiaohongshu URL', () => { expect(normalizeXhsUserId('https://www.xiaohongshu.com/user/profile/615529370000000002026001?xsec_source=pc_search')).toBe('615529370000000002026001'); }); it('keeps a bare profile id unchanged', () => { expect(normalizeXhsUserId('615529370000000002026001')).toBe('615529370000000002026001'); }); }); describe('flattenXhsNoteGroups', () => { it('flattens grouped note arrays and ignores empty groups', () => { expect(flattenXhsNoteGroups([[{ id: 'a' }], [], null, [{ id: 'b' }]])).toEqual([ { id: 'a' }, { id: 'b' }, ]); }); }); describe('buildXhsNoteUrl', () => { it('includes xsec token when available', () => { expect(buildXhsNoteUrl('user123', 'note456', 'token789')).toBe('https://www.xiaohongshu.com/user/profile/user123/note456?xsec_token=token789&xsec_source=pc_user'); }); it('emits a rednote URL when webHost is overridden', () => { expect(buildXhsNoteUrl('user123', 'note456', 'token789', 'www.rednote.com')).toBe('https://www.rednote.com/user/profile/user123/note456?xsec_token=token789&xsec_source=pc_user'); }); }); describe('extractXhsUserNotes', () => { it('normalizes grouped note cards into CLI rows', () => { const rows = extractXhsUserNotes({ noteGroups: [ [ { id: 'note-1', xsecToken: 'abc', noteCard: { noteId: 'note-1', displayTitle: 'First note', type: 'video', interactInfo: { likedCount: '4.6万' }, user: { userId: 'user-1' }, }, }, { noteCard: { note_id: 'note-2', display_title: 'Second note', type: 'normal', interact_info: { liked_count: 42 }, }, }, ], [], ], }, 'fallback-user'); expect(rows).toEqual([ { id: 'note-1', title: 'First note', type: 'video', likes: '4.6万', cover: '', url: 'https://www.xiaohongshu.com/user/profile/user-1/note-1?xsec_token=abc&xsec_source=pc_user', }, { id: 'note-2', title: 'Second note', type: 'normal', likes: '42', cover: '', url: 'https://www.xiaohongshu.com/user/profile/fallback-user/note-2', }, ]); }); it('extracts cover urls with fallback priority urlDefault -> urlPre -> url', () => { const rows = extractXhsUserNotes({ noteGroups: [ [ { noteCard: { noteId: 'cover-1', cover: { urlDefault: 'https://img.example/default.jpg', urlPre: 'https://img.example/pre.jpg', url: 'https://img.example/raw.jpg' } } }, { noteCard: { noteId: 'cover-2', cover: { urlPre: 'https://img.example/pre-only.jpg', url: 'https://img.example/raw-only.jpg' } } }, { noteCard: { noteId: 'cover-3', cover: { url: 'https://img.example/raw-fallback.jpg' } } }, ], ], }, 'fallback-user'); expect(rows.map(row => row.cover)).toEqual([ 'https://img.example/default.jpg', 'https://img.example/pre-only.jpg', 'https://img.example/raw-fallback.jpg', ]); }); it('deduplicates repeated notes by note id', () => { const rows = extractXhsUserNotes({ noteGroups: [ [ { noteCard: { noteId: 'dup-1', displayTitle: 'keep me' } }, { noteCard: { noteId: 'dup-1', displayTitle: 'drop me' } }, ], ], }, 'fallback-user'); expect(rows).toHaveLength(1); expect(rows[0]?.title).toBe('keep me'); }); it('emits rednote-hosted URLs when webHost is overridden', () => { const rows = extractXhsUserNotes({ noteGroups: [ [ { xsecToken: 'tok', noteCard: { noteId: 'note-red', displayTitle: 'rednote note', user: { userId: 'user-red' }, }, }, ], ], }, 'fallback-user', 'www.rednote.com'); expect(rows[0]?.url).toBe('https://www.rednote.com/user/profile/user-red/note-red?xsec_token=tok&xsec_source=pc_user'); }); }); describe('assertReadableUserSnapshot', () => { it('accepts an explicit empty notes array from a readable user store', () => { expect(() => assertReadableUserSnapshot({ storePresent: true, notesPresent: true, pageDataPresent: false, noteGroups: [], pageData: {}, })).not.toThrow(); }); it('fails typed when the user store is missing instead of treating parser drift as empty', () => { expect(() => assertReadableUserSnapshot({ storePresent: false, notesPresent: false, pageDataPresent: false, noteGroups: [], pageData: {}, })).toThrow(CommandExecutionError); }); it('fails typed when profile metadata exists but the notes array is missing', () => { expect(() => assertReadableUserSnapshot({ storePresent: true, notesPresent: false, pageDataPresent: true, noteGroups: [], pageData: { user: { nickname: 'Alice' } }, })).toThrow(CommandExecutionError); }); it('fails typed when notesPresent metadata and cloned noteGroups disagree', () => { expect(() => assertReadableUserSnapshot({ storePresent: true, notesPresent: true, pageDataPresent: false, noteGroups: null, pageData: {}, })).toThrow(CommandExecutionError); }); });