@chainlesschain/personal-data-hub 0.3.1 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/adapters/email-adapter-snapshot.test.js +237 -0
- package/__tests__/adapters/email-adapter.test.js +1 -1
- package/__tests__/adapters/email-pdf-extractor.test.js +1 -1
- package/__tests__/adapters/email-retry-progress.test.js +1 -1
- package/__tests__/adapters/email-templates.test.js +1 -1
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +721 -0
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +346 -0
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +284 -0
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +343 -0
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +296 -0
- package/__tests__/adapters/social-douyin-adb-collector.test.js +254 -0
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +304 -0
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +216 -0
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +432 -0
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +276 -0
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +141 -0
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +178 -0
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +537 -0
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +285 -0
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +163 -0
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +196 -0
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +362 -0
- package/__tests__/adapters/social-weibo-adb-collector.test.js +201 -0
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +189 -0
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +207 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +351 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +130 -0
- package/__tests__/adapters/system-data-android.test.js +32 -1
- package/__tests__/longtail-adapters.test.js +15 -2
- package/__tests__/shopping-adapters.test.js +96 -0
- package/__tests__/sign-providers.test.js +62 -0
- package/__tests__/travel-adapters.test.js +66 -0
- package/__tests__/whatsapp-adapter.test.js +5 -2
- package/lib/adapters/browser-history-chrome/chrome-db-reader.js +11 -1
- package/lib/adapters/email-imap/email-adapter.js +224 -17
- package/lib/adapters/messaging-telegram/index.js +15 -12
- package/lib/adapters/messaging-whatsapp/index.js +15 -12
- package/lib/adapters/shopping-taobao/index.js +161 -21
- package/lib/adapters/social-bilibili-adb/api-client.js +555 -0
- package/lib/adapters/social-bilibili-adb/chromium-cookies-reader.js +296 -0
- package/lib/adapters/social-bilibili-adb/collector.js +190 -0
- package/lib/adapters/social-bilibili-adb/cookies-extension.js +250 -0
- package/lib/adapters/social-bilibili-adb/index.js +51 -0
- package/lib/adapters/social-bilibili-adb/snapshot-builder.js +197 -0
- package/lib/adapters/social-douyin/index.js +4 -0
- package/lib/adapters/social-douyin-adb/collector.js +165 -0
- package/lib/adapters/social-douyin-adb/db-extension.js +281 -0
- package/lib/adapters/social-douyin-adb/im-db-parser.js +287 -0
- package/lib/adapters/social-douyin-adb/index.js +57 -0
- package/lib/adapters/social-douyin-adb/snapshot-builder.js +174 -0
- package/lib/adapters/social-kuaishou-adb/api-client.js +397 -0
- package/lib/adapters/social-kuaishou-adb/collector.js +196 -0
- package/lib/adapters/social-kuaishou-adb/cookies-extension.js +261 -0
- package/lib/adapters/social-kuaishou-adb/index.js +53 -0
- package/lib/adapters/social-kuaishou-adb/snapshot-builder.js +145 -0
- package/lib/adapters/social-toutiao-adb/api-client.js +377 -0
- package/lib/adapters/social-toutiao-adb/collector.js +200 -0
- package/lib/adapters/social-toutiao-adb/cookies-extension.js +266 -0
- package/lib/adapters/social-toutiao-adb/index.js +52 -0
- package/lib/adapters/social-toutiao-adb/snapshot-builder.js +148 -0
- package/lib/adapters/social-weibo-adb/api-client.js +281 -0
- package/lib/adapters/social-weibo-adb/collector.js +169 -0
- package/lib/adapters/social-weibo-adb/cookies-extension.js +251 -0
- package/lib/adapters/social-weibo-adb/index.js +55 -0
- package/lib/adapters/social-weibo-adb/snapshot-builder.js +145 -0
- package/lib/adapters/social-xiaohongshu-adb/api-client.js +309 -0
- package/lib/adapters/social-xiaohongshu-adb/collector.js +209 -0
- package/lib/adapters/social-xiaohongshu-adb/cookies-extension.js +211 -0
- package/lib/adapters/social-xiaohongshu-adb/index.js +50 -0
- package/lib/adapters/social-xiaohongshu-adb/sign.js +90 -0
- package/lib/adapters/social-xiaohongshu-adb/snapshot-builder.js +126 -0
- package/lib/adapters/system-data-android/adapter.js +77 -3
- package/lib/adapters/travel-amap/index.js +16 -10
- package/lib/adapters/travel-ctrip/index.js +25 -9
- package/lib/adapters/vscode/vscode-reader.js +7 -1
- package/lib/sign-providers/index.js +20 -0
- package/lib/sign-providers/interface.js +82 -0
- package/lib/sign-providers/null-sign-provider.js +30 -0
- package/package.json +10 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 3a — Weibo Node API client unit cover.
|
|
5
|
+
*
|
|
6
|
+
* Same fake-fetch pattern as social-bilibili-adb-api-client.test.js.
|
|
7
|
+
* Byte-parity check against WeiboApiClient.kt (Kotlin) is in
|
|
8
|
+
* `android-app/.../WeiboApiClient*Test.kt`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi } from "vitest";
|
|
12
|
+
|
|
13
|
+
const { WeiboApiClient, _internals } = require(
|
|
14
|
+
"../../lib/adapters/social-weibo-adb/api-client",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function makeClient(responses) {
|
|
18
|
+
const calls = [];
|
|
19
|
+
const fakeFetch = async (urlStr, opts) => {
|
|
20
|
+
calls.push({ url: urlStr, opts });
|
|
21
|
+
for (const [pattern, payload] of responses) {
|
|
22
|
+
if (urlStr.includes(pattern)) {
|
|
23
|
+
const resolved =
|
|
24
|
+
typeof payload === "function" ? await payload(urlStr, opts) : payload;
|
|
25
|
+
return {
|
|
26
|
+
ok: resolved.status == null || resolved.status === 200,
|
|
27
|
+
status: resolved.status || 200,
|
|
28
|
+
text: async () => resolved.body,
|
|
29
|
+
json: async () => JSON.parse(resolved.body),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error("fake fetch: no scripted response for " + urlStr);
|
|
34
|
+
};
|
|
35
|
+
return { client: new WeiboApiClient({ fetch: fakeFetch }), calls };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const FAKE_COOKIE = "SUB=abc; SUBP=def; _T_WM=tw1; MLOGIN=1";
|
|
39
|
+
|
|
40
|
+
// ─── _internals.parseWeiboTime ──────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe("parseWeiboTime", () => {
|
|
43
|
+
it("parses 'EEE MMM dd HH:mm:ss Z yyyy' format", () => {
|
|
44
|
+
const t = _internals.parseWeiboTime("Sun Jan 12 13:45:00 +0800 2026");
|
|
45
|
+
// Sunday 2026-01-12 13:45:00 +0800 = 2026-01-12 05:45:00 UTC
|
|
46
|
+
expect(t).toBeGreaterThan(0);
|
|
47
|
+
const d = new Date(t);
|
|
48
|
+
expect(d.getUTCFullYear()).toBe(2026);
|
|
49
|
+
expect(d.getUTCMonth()).toBe(0);
|
|
50
|
+
expect(d.getUTCDate()).toBe(12);
|
|
51
|
+
expect(d.getUTCHours()).toBe(5);
|
|
52
|
+
expect(d.getUTCMinutes()).toBe(45);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("digits-only treated as unix-seconds (< 1e12)", () => {
|
|
56
|
+
expect(_internals.parseWeiboTime("1716383021")).toBe(1716383021000);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("digits-only treated as ms (> 1e12)", () => {
|
|
60
|
+
expect(_internals.parseWeiboTime("1716383021000")).toBe(1716383021000);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns 0 for empty / null / unparseable", () => {
|
|
64
|
+
expect(_internals.parseWeiboTime("")).toBe(0);
|
|
65
|
+
expect(_internals.parseWeiboTime(null)).toBe(0);
|
|
66
|
+
expect(_internals.parseWeiboTime(undefined)).toBe(0);
|
|
67
|
+
expect(_internals.parseWeiboTime("not a date")).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ─── _internals.stripHtml ───────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe("stripHtml", () => {
|
|
74
|
+
it("strips <a> / <span> tags", () => {
|
|
75
|
+
expect(
|
|
76
|
+
_internals.stripHtml(
|
|
77
|
+
'<a href="x">hello</a> <span class="y">world</span>',
|
|
78
|
+
),
|
|
79
|
+
).toBe("hello world");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("decodes / & / < / > / "", () => {
|
|
83
|
+
expect(_internals.stripHtml("a b&c<d>e"f")).toBe(
|
|
84
|
+
'a b&c<d>e"f',
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("handles Chinese + emoji", () => {
|
|
89
|
+
expect(_internals.stripHtml("<p>你好 👋</p>")).toBe("你好 👋");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns empty for null / empty", () => {
|
|
93
|
+
expect(_internals.stripHtml("")).toBe("");
|
|
94
|
+
expect(_internals.stripHtml(null)).toBe("");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── fetchUid ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("WeiboApiClient.fetchUid", () => {
|
|
101
|
+
it("returns numeric UID when login=true", async () => {
|
|
102
|
+
const { client } = makeClient([
|
|
103
|
+
[
|
|
104
|
+
"api/config",
|
|
105
|
+
{ body: JSON.stringify({ ok: 1, data: { login: true, uid: "1234567890" } }) },
|
|
106
|
+
],
|
|
107
|
+
]);
|
|
108
|
+
const uid = await client.fetchUid(FAKE_COOKIE);
|
|
109
|
+
expect(uid).toBe(1234567890);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns null when login=false", async () => {
|
|
113
|
+
const { client } = makeClient([
|
|
114
|
+
[
|
|
115
|
+
"api/config",
|
|
116
|
+
{ body: JSON.stringify({ ok: 1, data: { login: false } }) },
|
|
117
|
+
],
|
|
118
|
+
]);
|
|
119
|
+
expect(await client.fetchUid(FAKE_COOKIE)).toBe(null);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns null on non-JSON (cookie expired login redirect)", async () => {
|
|
123
|
+
const { client } = makeClient([
|
|
124
|
+
["api/config", { body: "<html>login redirect</html>" }],
|
|
125
|
+
]);
|
|
126
|
+
expect(await client.fetchUid(FAKE_COOKIE)).toBe(null);
|
|
127
|
+
expect(client.lastErrorCode).toBe(-4);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("returns null on HTTP error", async () => {
|
|
131
|
+
const { client } = makeClient([
|
|
132
|
+
["api/config", { status: 503, body: "Service Unavailable" }],
|
|
133
|
+
]);
|
|
134
|
+
expect(await client.fetchUid(FAKE_COOKIE)).toBe(null);
|
|
135
|
+
expect(client.lastErrorCode).toBe(503);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("sends required browser headers", async () => {
|
|
139
|
+
const { client, calls } = makeClient([
|
|
140
|
+
[
|
|
141
|
+
"api/config",
|
|
142
|
+
{ body: JSON.stringify({ ok: 1, data: { login: true, uid: "1" } }) },
|
|
143
|
+
],
|
|
144
|
+
]);
|
|
145
|
+
await client.fetchUid(FAKE_COOKIE);
|
|
146
|
+
const h = calls[0].opts.headers;
|
|
147
|
+
expect(h["User-Agent"]).toContain("Mozilla");
|
|
148
|
+
expect(h.Referer).toBe("https://m.weibo.cn/");
|
|
149
|
+
expect(h["X-Requested-With"]).toBe("XMLHttpRequest");
|
|
150
|
+
expect(h["MWeibo-Pwa"]).toBe("1");
|
|
151
|
+
expect(h.Cookie).toBe(FAKE_COOKIE);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── fetchPosts ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
describe("WeiboApiClient.fetchPosts", () => {
|
|
158
|
+
it("parses card_type=9 mblog cards", async () => {
|
|
159
|
+
const { client } = makeClient([
|
|
160
|
+
[
|
|
161
|
+
"api/container/getIndex",
|
|
162
|
+
{
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
ok: 1,
|
|
165
|
+
data: {
|
|
166
|
+
cards: [
|
|
167
|
+
{
|
|
168
|
+
card_type: 9,
|
|
169
|
+
mblog: {
|
|
170
|
+
mid: "MBLOG_001",
|
|
171
|
+
text: '<a href="/user/x">@friend</a> hello 你好',
|
|
172
|
+
created_at: "Sun Jan 12 13:45:00 +0800 2026",
|
|
173
|
+
source: '<a href="x">iPhone 16</a>',
|
|
174
|
+
reposts_count: 5,
|
|
175
|
+
comments_count: 10,
|
|
176
|
+
attitudes_count: 100,
|
|
177
|
+
pic_num: 2,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{ card_type: 11 /* banner — should skip */ },
|
|
181
|
+
{
|
|
182
|
+
card_type: 9,
|
|
183
|
+
mblog: {
|
|
184
|
+
id: 12345, // alt id field
|
|
185
|
+
text: "second post",
|
|
186
|
+
created_at: "1716383021",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
]);
|
|
195
|
+
const posts = await client.fetchPosts(FAKE_COOKIE, 1234567890);
|
|
196
|
+
expect(posts).toHaveLength(2);
|
|
197
|
+
expect(posts[0]).toMatchObject({
|
|
198
|
+
mid: "MBLOG_001",
|
|
199
|
+
text: "@friend hello 你好",
|
|
200
|
+
// source field preserves raw HTML (Kotlin byte-parity — WeiboApiClient.kt
|
|
201
|
+
// doesn't strip it; UI can strip if it wants)
|
|
202
|
+
source: '<a href="x">iPhone 16</a>',
|
|
203
|
+
repostsCount: 5,
|
|
204
|
+
commentsCount: 10,
|
|
205
|
+
likesCount: 100,
|
|
206
|
+
picCount: 2,
|
|
207
|
+
});
|
|
208
|
+
expect(posts[1].mid).toBe("12345");
|
|
209
|
+
expect(posts[1].text).toBe("second post");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("respects limit", async () => {
|
|
213
|
+
const items = Array.from({ length: 100 }, (_, i) => ({
|
|
214
|
+
card_type: 9,
|
|
215
|
+
mblog: { mid: `M${i}`, text: `t${i}`, created_at: "1716383021" },
|
|
216
|
+
}));
|
|
217
|
+
const { client } = makeClient([
|
|
218
|
+
[
|
|
219
|
+
"api/container/getIndex",
|
|
220
|
+
{ body: JSON.stringify({ ok: 1, data: { cards: items } }) },
|
|
221
|
+
],
|
|
222
|
+
]);
|
|
223
|
+
const posts = await client.fetchPosts(FAKE_COOKIE, 1, { limit: 5 });
|
|
224
|
+
expect(posts).toHaveLength(5);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("uses containerid=107603<uid>", async () => {
|
|
228
|
+
const { client, calls } = makeClient([
|
|
229
|
+
[
|
|
230
|
+
"api/container/getIndex",
|
|
231
|
+
{ body: JSON.stringify({ ok: 1, data: { cards: [] } }) },
|
|
232
|
+
],
|
|
233
|
+
]);
|
|
234
|
+
await client.fetchPosts(FAKE_COOKIE, 99);
|
|
235
|
+
expect(calls[0].url).toContain("containerid=10760399");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ─── fetchFavourites ───────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("WeiboApiClient.fetchFavourites", () => {
|
|
242
|
+
it("parses status nested rows", async () => {
|
|
243
|
+
const { client } = makeClient([
|
|
244
|
+
[
|
|
245
|
+
"api/favorites",
|
|
246
|
+
{
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
ok: 1,
|
|
249
|
+
data: {
|
|
250
|
+
favorites: [
|
|
251
|
+
{
|
|
252
|
+
favorited_time: "Sun Jan 12 14:00:00 +0800 2026",
|
|
253
|
+
status: {
|
|
254
|
+
mid: "FAV_001",
|
|
255
|
+
text: "interesting <em>thing</em>",
|
|
256
|
+
user: { screen_name: "@famous" },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
]);
|
|
265
|
+
const favs = await client.fetchFavourites(FAKE_COOKIE);
|
|
266
|
+
expect(favs).toHaveLength(1);
|
|
267
|
+
expect(favs[0]).toMatchObject({
|
|
268
|
+
mid: "FAV_001",
|
|
269
|
+
text: "interesting thing",
|
|
270
|
+
authorScreenName: "@famous",
|
|
271
|
+
});
|
|
272
|
+
expect(favs[0].favAt).toBeGreaterThan(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("falls back to status.created_at when favorited_time absent", async () => {
|
|
276
|
+
const { client } = makeClient([
|
|
277
|
+
[
|
|
278
|
+
"api/favorites",
|
|
279
|
+
{
|
|
280
|
+
body: JSON.stringify({
|
|
281
|
+
ok: 1,
|
|
282
|
+
data: {
|
|
283
|
+
favorites: [
|
|
284
|
+
{
|
|
285
|
+
status: {
|
|
286
|
+
mid: "X",
|
|
287
|
+
text: "y",
|
|
288
|
+
created_at: "Sun Jan 12 13:45:00 +0800 2026",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
]);
|
|
297
|
+
const favs = await client.fetchFavourites(FAKE_COOKIE);
|
|
298
|
+
expect(favs[0].favAt).toBeGreaterThan(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ─── fetchFollows ──────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
describe("WeiboApiClient.fetchFollows", () => {
|
|
305
|
+
it("parses users list", async () => {
|
|
306
|
+
const { client } = makeClient([
|
|
307
|
+
[
|
|
308
|
+
"api/friendships/friends",
|
|
309
|
+
{
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
ok: 1,
|
|
312
|
+
data: {
|
|
313
|
+
users: [
|
|
314
|
+
{
|
|
315
|
+
id: 42,
|
|
316
|
+
screen_name: "Friend1",
|
|
317
|
+
description: "hi",
|
|
318
|
+
profile_image_url: "https://x.png",
|
|
319
|
+
},
|
|
320
|
+
{ id: 0, screen_name: "Ghost" }, // skip
|
|
321
|
+
{ id: 99, screen_name: "Friend2" },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
}),
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
]);
|
|
328
|
+
const fols = await client.fetchFollows(FAKE_COOKIE, 1234);
|
|
329
|
+
expect(fols).toHaveLength(2);
|
|
330
|
+
expect(fols[0].uid).toBe(42);
|
|
331
|
+
expect(fols[1].uid).toBe(99);
|
|
332
|
+
expect(fols[0].screenName).toBe("Friend1");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("uid sent as vmid query param", async () => {
|
|
336
|
+
const { client, calls } = makeClient([
|
|
337
|
+
[
|
|
338
|
+
"api/friendships/friends",
|
|
339
|
+
{ body: JSON.stringify({ ok: 1, data: { users: [] } }) },
|
|
340
|
+
],
|
|
341
|
+
]);
|
|
342
|
+
await client.fetchFollows(FAKE_COOKIE, 9999);
|
|
343
|
+
expect(calls[0].url).toContain("uid=9999");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ─── error code propagation ─────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
describe("WeiboApiClient — error propagation", () => {
|
|
350
|
+
it("ok != 1 → returns [] + sets lastError", async () => {
|
|
351
|
+
const { client } = makeClient([
|
|
352
|
+
[
|
|
353
|
+
"api/container/getIndex",
|
|
354
|
+
{ body: JSON.stringify({ ok: -100, msg: "anti-bot" }) },
|
|
355
|
+
],
|
|
356
|
+
]);
|
|
357
|
+
const r = await client.fetchPosts(FAKE_COOKIE, 1);
|
|
358
|
+
expect(r).toEqual([]);
|
|
359
|
+
expect(client.lastErrorCode).toBe(-100);
|
|
360
|
+
expect(client.lastErrorMessage).toBe("anti-bot");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
4
|
+
import { existsSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
collect,
|
|
10
|
+
collectAndSync,
|
|
11
|
+
} = require("../../lib/adapters/social-weibo-adb/collector");
|
|
12
|
+
|
|
13
|
+
let stagingDir;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
stagingDir = mkdtempSync(join(tmpdir(), "cc-weibo-test-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
try {
|
|
21
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
22
|
+
} catch (_e) {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeFakeBridge({ cookieResult, throwOnInvoke } = {}) {
|
|
28
|
+
return {
|
|
29
|
+
invoke: vi.fn(async (_method) => {
|
|
30
|
+
if (throwOnInvoke) throw throwOnInvoke;
|
|
31
|
+
return cookieResult;
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeFakeApiClient({
|
|
37
|
+
uid = 1234567890,
|
|
38
|
+
posts = [],
|
|
39
|
+
favourites = [],
|
|
40
|
+
follows = [],
|
|
41
|
+
lastErrorCode = 0,
|
|
42
|
+
lastErrorMessage = null,
|
|
43
|
+
} = {}) {
|
|
44
|
+
return {
|
|
45
|
+
fetchUid: vi.fn(async () => uid),
|
|
46
|
+
fetchPosts: vi.fn(async () => posts),
|
|
47
|
+
fetchFavourites: vi.fn(async () => favourites),
|
|
48
|
+
fetchFollows: vi.fn(async () => follows),
|
|
49
|
+
lastErrorCode,
|
|
50
|
+
lastErrorMessage,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("collect — happy path", () => {
|
|
55
|
+
it("invokes cookies, fetchUid, 3 endpoints, writes snapshot", async () => {
|
|
56
|
+
const bridge = makeFakeBridge({
|
|
57
|
+
cookieResult: {
|
|
58
|
+
cookie: "SUB=abc",
|
|
59
|
+
diagnostic: { cookieCount: 5, hasSub: true },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const apiClient = makeFakeApiClient({
|
|
63
|
+
uid: 1234567890,
|
|
64
|
+
posts: [{ mid: "P1", text: "p", createdAt: 1 }],
|
|
65
|
+
favourites: [{ mid: "F1", text: "f", favAt: 2 }],
|
|
66
|
+
follows: [{ uid: 99, screenName: "x", followedAt: 0 }],
|
|
67
|
+
});
|
|
68
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
69
|
+
|
|
70
|
+
expect(bridge.invoke).toHaveBeenCalledWith("weibo.cookies");
|
|
71
|
+
expect(apiClient.fetchUid).toHaveBeenCalledWith("SUB=abc");
|
|
72
|
+
expect(apiClient.fetchPosts).toHaveBeenCalledWith(
|
|
73
|
+
"SUB=abc",
|
|
74
|
+
1234567890,
|
|
75
|
+
expect.any(Object),
|
|
76
|
+
);
|
|
77
|
+
expect(result.uid).toBe(1234567890);
|
|
78
|
+
expect(result.eventCounts).toEqual({
|
|
79
|
+
post: 1,
|
|
80
|
+
favourite: 1,
|
|
81
|
+
follow: 1,
|
|
82
|
+
total: 3,
|
|
83
|
+
});
|
|
84
|
+
expect(existsSync(result.snapshotPath)).toBe(true);
|
|
85
|
+
const snap = JSON.parse(readFileSync(result.snapshotPath, "utf-8"));
|
|
86
|
+
expect(snap.schemaVersion).toBe(1);
|
|
87
|
+
expect(snap.events).toHaveLength(3);
|
|
88
|
+
expect(result.uidFetchFailed).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("collect — uid fetch failure (cookie expired)", () => {
|
|
93
|
+
it("returns empty-event snapshot + uidFetchFailed=true", async () => {
|
|
94
|
+
const bridge = makeFakeBridge({
|
|
95
|
+
cookieResult: { cookie: "SUB=expired", diagnostic: {} },
|
|
96
|
+
});
|
|
97
|
+
const apiClient = makeFakeApiClient({
|
|
98
|
+
uid: null,
|
|
99
|
+
lastErrorCode: -4,
|
|
100
|
+
lastErrorMessage: "non-json (cookie expired?)",
|
|
101
|
+
});
|
|
102
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
103
|
+
expect(result.uid).toBe(null);
|
|
104
|
+
expect(result.uidFetchFailed).toBe(true);
|
|
105
|
+
expect(result.eventCounts.total).toBe(0);
|
|
106
|
+
expect(result.lastErrorCode).toBe(-4);
|
|
107
|
+
// fetchPosts/Favourites/Follows must NOT be called when uid failed
|
|
108
|
+
expect(apiClient.fetchPosts).not.toHaveBeenCalled();
|
|
109
|
+
// Snapshot file still written so downstream caller doesn't crash on
|
|
110
|
+
// missing inputPath
|
|
111
|
+
expect(existsSync(result.snapshotPath)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("collect — failure modes", () => {
|
|
116
|
+
it("propagates bridge.invoke errors", async () => {
|
|
117
|
+
const bridge = makeFakeBridge({
|
|
118
|
+
throwOnInvoke: new Error("WEIBO_NO_ROOT: phone isn't rooted"),
|
|
119
|
+
});
|
|
120
|
+
await expect(collect(bridge, { stagingDir })).rejects.toThrow(/WEIBO_NO_ROOT/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("throws TypeError when bridge missing invoke", async () => {
|
|
124
|
+
await expect(collect(null, { stagingDir })).rejects.toThrow(TypeError);
|
|
125
|
+
await expect(collect({}, { stagingDir })).rejects.toThrow(TypeError);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("throws on malformed cookieResult", async () => {
|
|
129
|
+
const bridge = makeFakeBridge({ cookieResult: { cookie: null } });
|
|
130
|
+
await expect(collect(bridge, { stagingDir })).rejects.toThrow(/malformed payload/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("collect — partial result tolerance", () => {
|
|
135
|
+
it("0 events when uid OK but all 3 endpoints empty", async () => {
|
|
136
|
+
const bridge = makeFakeBridge({ cookieResult: { cookie: "SUB=a" } });
|
|
137
|
+
const apiClient = makeFakeApiClient();
|
|
138
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
139
|
+
expect(result.eventCounts.total).toBe(0);
|
|
140
|
+
expect(existsSync(result.snapshotPath)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("partial event set when one endpoint fails", async () => {
|
|
144
|
+
const bridge = makeFakeBridge({ cookieResult: { cookie: "SUB=a" } });
|
|
145
|
+
const apiClient = makeFakeApiClient({
|
|
146
|
+
posts: [{ mid: "P", text: "x", createdAt: 1 }],
|
|
147
|
+
favourites: [], // simulated failure
|
|
148
|
+
follows: [{ uid: 1, screenName: "x" }],
|
|
149
|
+
});
|
|
150
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
151
|
+
expect(result.eventCounts).toEqual({ post: 1, favourite: 0, follow: 1, total: 2 });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("collectAndSync", () => {
|
|
156
|
+
it("calls registry.syncAdapter + cleans up on success", async () => {
|
|
157
|
+
const bridge = makeFakeBridge({ cookieResult: { cookie: "SUB=a" } });
|
|
158
|
+
const apiClient = makeFakeApiClient({
|
|
159
|
+
posts: [{ mid: "P", createdAt: 1 }],
|
|
160
|
+
});
|
|
161
|
+
let syncedPath = null;
|
|
162
|
+
const registry = {
|
|
163
|
+
syncAdapter: vi.fn(async (name, opts) => {
|
|
164
|
+
if (name !== "social-weibo") throw new Error("wrong name");
|
|
165
|
+
syncedPath = opts.inputPath;
|
|
166
|
+
return { adapter: name, status: "ok", rawCount: 1 };
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
const report = await collectAndSync(bridge, registry, {
|
|
170
|
+
apiClient,
|
|
171
|
+
stagingDir,
|
|
172
|
+
});
|
|
173
|
+
expect(report.status).toBe("ok");
|
|
174
|
+
expect(report.weibo.uid).toBe(1234567890);
|
|
175
|
+
expect(report.weibo.eventCounts.post).toBe(1);
|
|
176
|
+
expect(existsSync(syncedPath)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("cleanup even on syncAdapter throw", async () => {
|
|
180
|
+
const bridge = makeFakeBridge({ cookieResult: { cookie: "SUB=a" } });
|
|
181
|
+
const apiClient = makeFakeApiClient();
|
|
182
|
+
let syncedPath = null;
|
|
183
|
+
const registry = {
|
|
184
|
+
syncAdapter: vi.fn(async (_n, opts) => {
|
|
185
|
+
syncedPath = opts.inputPath;
|
|
186
|
+
throw new Error("registry exploded");
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
await expect(
|
|
190
|
+
collectAndSync(bridge, registry, { apiClient, stagingDir }),
|
|
191
|
+
).rejects.toThrow("registry exploded");
|
|
192
|
+
expect(existsSync(syncedPath)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("rejects missing syncAdapter", async () => {
|
|
196
|
+
const bridge = makeFakeBridge({ cookieResult: { cookie: "SUB=a" } });
|
|
197
|
+
await expect(
|
|
198
|
+
collectAndSync(bridge, null, { apiClient: makeFakeApiClient() }),
|
|
199
|
+
).rejects.toThrow(TypeError);
|
|
200
|
+
});
|
|
201
|
+
});
|