Files
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

84 lines
3.8 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { mapFeeds, parseQzoneCallback } from '@snowluma/protocol/web/qzone';
// Real feeds3_html_more response captured live (issue #182). Its `data` value
// is a JS object literal (unquoted keys, single-quoted strings, \xNN escapes,
// trailing `undefined`) — JSON.parse throws on it; parseQzoneCallback must not.
const SAMPLE = readFileSync(
fileURLToPath(new URL('./fixtures/qzone-feeds-sample.jsonp', import.meta.url)),
'utf8',
);
interface RawFeeds {
code?: number;
data?: { data?: unknown[]; hasmore?: number | string };
}
describe('qzone feeds JS-literal parsing (issue #182)', () => {
it('JSON.parse chokes on the real body (regression baseline)', () => {
const inner = SAMPLE.slice(SAMPLE.indexOf('{'), SAMPLE.lastIndexOf('}') + 1);
expect(() => JSON.parse(inner)).toThrow();
});
it('parseQzoneCallback evaluates the JS-literal payload', () => {
const data = parseQzoneCallback<RawFeeds>(SAMPLE);
expect(data.code).toBe(0);
expect(Array.isArray(data.data?.data)).toBe(true);
// The array carries a trailing `undefined` hole alongside the real feed.
expect(data.data!.data!.length).toBe(2);
expect(data.data!.data!.some((x) => x === undefined)).toBe(true);
});
it('mapFeeds drops the undefined hole and normalises the feed', () => {
const data = parseQzoneCallback(SAMPLE);
const result = mapFeeds(data as any);
expect(result.feeds).toHaveLength(1);
const f = result.feeds[0]!;
expect(f.uin).toBe(20050606);
expect(f.nickname).toBe('官方Qzone');
expect(f.appid).toBe(311);
expect(f.time).toBe(1780711209);
expect(f.key).toBe('aef23101297f236ae4f10600');
// \xNN escapes decoded by the JS eval, html passed through verbatim.
expect(f.html).toContain('<li class="f-single f-s-s"');
expect(f.html).toContain('官方Qzone');
expect(f.tid).toBe(f.key);
expect(f.content).toContain('QQ空间');
expect(f.content).not.toContain('展开全文');
expect(f.content_complete).toBe(false);
expect(f.images).toEqual([]);
expect(f.like_count).toBe(3173513);
expect(f.comment_count).toBeNull();
expect(result.has_more).toBe(false);
});
it('parses as inert data — never resolves identifiers or executes code', () => {
// The parser has no notion of globals, member access, or calls: any such
// token is rejected, so a tampered body cannot reach `process`/`Function`
// etc. (the whole reason we do NOT eval this payload).
expect(() => parseQzoneCallback('_preloadCallback({x: process})')).toThrow();
expect(() => parseQzoneCallback('_preloadCallback({x: callback.constructor})')).toThrow();
expect(() =>
parseQzoneCallback('_preloadCallback({x: (function(){return 1})()})'),
).toThrow();
expect(() => parseQzoneCallback('_preloadCallback(eval("1+1"))')).toThrow();
});
it('does not pollute Object.prototype via a __proto__ key', () => {
parseQzoneCallback('_preloadCallback({"__proto__":{"polluted":1},"code":0})');
expect(({} as any).polluted).toBeUndefined();
});
it('decodes JS string escapes (\\xNN, \\/) literally, not as markup', () => {
const d = parseQzoneCallback<{ s?: string }>("_preloadCallback({s:'\\x3Cb\\x3E&\\/x'})");
expect(d.s).toBe('<b>&/x');
});
it('rejects a body that captures no object', () => {
expect(() => parseQzoneCallback('_preloadCallback(42)')).toThrow('invalid feeds response');
expect(() => parseQzoneCallback('void 0')).toThrow('invalid feeds response');
});
});