@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,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach } 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
|
+
buildSnapshot,
|
|
10
|
+
writeSnapshotJson,
|
|
11
|
+
cleanupSnapshotJson,
|
|
12
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
13
|
+
} = require("../../lib/adapters/social-weibo-adb/snapshot-builder");
|
|
14
|
+
|
|
15
|
+
let tmpDir;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = mkdtempSync(join(tmpdir(), "cc-weibo-snap-test-"));
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
try {
|
|
21
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
} catch (_e) {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("buildSnapshot — schema", () => {
|
|
28
|
+
it("returns documented shape", () => {
|
|
29
|
+
const s = buildSnapshot({
|
|
30
|
+
uid: 1234567890,
|
|
31
|
+
displayName: "alice",
|
|
32
|
+
snapshottedAt: 1716383021000,
|
|
33
|
+
});
|
|
34
|
+
expect(s.schemaVersion).toBe(1);
|
|
35
|
+
expect(s.schemaVersion).toBe(SNAPSHOT_SCHEMA_VERSION);
|
|
36
|
+
expect(s.snapshottedAt).toBe(1716383021000);
|
|
37
|
+
expect(s.account).toEqual({ uid: "1234567890", displayName: "alice" });
|
|
38
|
+
expect(s.events).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects non-positive uid", () => {
|
|
42
|
+
expect(() => buildSnapshot({ uid: 0 })).toThrow(TypeError);
|
|
43
|
+
expect(() => buildSnapshot({ uid: -1 })).toThrow(TypeError);
|
|
44
|
+
expect(() => buildSnapshot({})).toThrow(TypeError);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defaults displayName + snapshottedAt", () => {
|
|
48
|
+
const before = Date.now();
|
|
49
|
+
const s = buildSnapshot({ uid: 1 });
|
|
50
|
+
const after = Date.now();
|
|
51
|
+
expect(s.account.displayName).toBe("");
|
|
52
|
+
expect(s.snapshottedAt).toBeGreaterThanOrEqual(before);
|
|
53
|
+
expect(s.snapshottedAt).toBeLessThanOrEqual(after);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("buildSnapshot — events", () => {
|
|
58
|
+
it("maps posts → kind=post", () => {
|
|
59
|
+
const s = buildSnapshot({
|
|
60
|
+
uid: 1,
|
|
61
|
+
posts: [
|
|
62
|
+
{
|
|
63
|
+
mid: "M1",
|
|
64
|
+
text: "hi",
|
|
65
|
+
createdAt: 1716383021000,
|
|
66
|
+
source: "iPhone",
|
|
67
|
+
repostsCount: 5,
|
|
68
|
+
commentsCount: 10,
|
|
69
|
+
likesCount: 100,
|
|
70
|
+
picCount: 2,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
expect(s.events).toHaveLength(1);
|
|
75
|
+
expect(s.events[0]).toMatchObject({
|
|
76
|
+
kind: "post",
|
|
77
|
+
id: "post-M1",
|
|
78
|
+
capturedAt: 1716383021000,
|
|
79
|
+
text: "hi",
|
|
80
|
+
mid: "M1",
|
|
81
|
+
source: "iPhone",
|
|
82
|
+
repostsCount: 5,
|
|
83
|
+
commentsCount: 10,
|
|
84
|
+
likesCount: 100,
|
|
85
|
+
picCount: 2,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("maps favourites → kind=favourite", () => {
|
|
90
|
+
const s = buildSnapshot({
|
|
91
|
+
uid: 1,
|
|
92
|
+
favourites: [
|
|
93
|
+
{ mid: "F1", text: "fav text", favAt: 1716383022000, authorScreenName: "x" },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
expect(s.events[0]).toMatchObject({
|
|
97
|
+
kind: "favourite",
|
|
98
|
+
id: "fav-F1",
|
|
99
|
+
capturedAt: 1716383022000,
|
|
100
|
+
mid: "F1",
|
|
101
|
+
authorScreenName: "x",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("maps follows → kind=follow with snapshottedAt fallback", () => {
|
|
106
|
+
const s = buildSnapshot({
|
|
107
|
+
uid: 1,
|
|
108
|
+
snapshottedAt: 999,
|
|
109
|
+
follows: [
|
|
110
|
+
{
|
|
111
|
+
uid: 42,
|
|
112
|
+
screenName: "Friend",
|
|
113
|
+
description: "hi",
|
|
114
|
+
avatarUrl: "https://x.png",
|
|
115
|
+
followedAt: 0, // /api/friendships/friends doesn't return follow time
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
expect(s.events[0]).toMatchObject({
|
|
120
|
+
kind: "follow",
|
|
121
|
+
id: "follow-42",
|
|
122
|
+
uid: 42,
|
|
123
|
+
screenName: "Friend",
|
|
124
|
+
capturedAt: 999, // fallback to snapshottedAt
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("uses index fallback id when mid/uid missing", () => {
|
|
129
|
+
const s = buildSnapshot({
|
|
130
|
+
uid: 1,
|
|
131
|
+
posts: [{ text: "no mid" }],
|
|
132
|
+
favourites: [{ text: "no mid" }],
|
|
133
|
+
follows: [{ screenName: "no uid" }],
|
|
134
|
+
});
|
|
135
|
+
expect(s.events.map((e) => e.id)).toEqual(["post-0", "fav-0", "follow-0"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("preserves order post → favourite → follow", () => {
|
|
139
|
+
const s = buildSnapshot({
|
|
140
|
+
uid: 1,
|
|
141
|
+
posts: [{ mid: "P", createdAt: 1 }],
|
|
142
|
+
favourites: [{ mid: "F", favAt: 2 }],
|
|
143
|
+
follows: [{ uid: 1, followedAt: 3 }],
|
|
144
|
+
});
|
|
145
|
+
expect(s.events.map((e) => e.kind)).toEqual(["post", "favourite", "follow"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("handles non-array fields", () => {
|
|
149
|
+
const s = buildSnapshot({
|
|
150
|
+
uid: 1,
|
|
151
|
+
posts: "not array",
|
|
152
|
+
favourites: null,
|
|
153
|
+
follows: {},
|
|
154
|
+
});
|
|
155
|
+
expect(s.events).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("skips null/non-object items", () => {
|
|
159
|
+
const s = buildSnapshot({
|
|
160
|
+
uid: 1,
|
|
161
|
+
posts: [null, { mid: "P", createdAt: 1 }, "junk"],
|
|
162
|
+
});
|
|
163
|
+
expect(s.events).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("writeSnapshotJson + cleanupSnapshotJson", () => {
|
|
168
|
+
it("writes to default tmpdir", () => {
|
|
169
|
+
const s = buildSnapshot({ uid: 1 });
|
|
170
|
+
const p = writeSnapshotJson(s);
|
|
171
|
+
expect(existsSync(p)).toBe(true);
|
|
172
|
+
expect(p).toContain("cc-weibo-snapshot-");
|
|
173
|
+
const parsed = JSON.parse(readFileSync(p, "utf-8"));
|
|
174
|
+
expect(parsed.schemaVersion).toBe(1);
|
|
175
|
+
cleanupSnapshotJson(p);
|
|
176
|
+
expect(existsSync(p)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("rejects path separators in fileName", () => {
|
|
180
|
+
expect(() => writeSnapshotJson({ schemaVersion: 1 }, { fileName: "../evil.json" })).toThrow(
|
|
181
|
+
/must be a basename/,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("cleanupSnapshotJson tolerates missing / null", () => {
|
|
186
|
+
cleanupSnapshotJson(null);
|
|
187
|
+
cleanupSnapshotJson(join(tmpDir, "nonexistent.json"));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
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-xiaohongshu-adb/collector");
|
|
12
|
+
|
|
13
|
+
let stagingDir;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
stagingDir = mkdtempSync(join(tmpdir(), "cc-xhs-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
|
+
me = { userId: "5e8c8f7e1234abcdef", nickname: "Alice" },
|
|
38
|
+
notes = [],
|
|
39
|
+
liked = [],
|
|
40
|
+
follows = [],
|
|
41
|
+
lastErrorCode = 0,
|
|
42
|
+
lastErrorMessage = null,
|
|
43
|
+
} = {}) {
|
|
44
|
+
return {
|
|
45
|
+
fetchMe: vi.fn(async () => me),
|
|
46
|
+
fetchNotes: vi.fn(async () => notes),
|
|
47
|
+
fetchLiked: vi.fn(async () => liked),
|
|
48
|
+
fetchFollows: vi.fn(async () => follows),
|
|
49
|
+
lastErrorCode,
|
|
50
|
+
lastErrorMessage,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("collect — happy path", () => {
|
|
55
|
+
it("invokes cookies, fetchMe, 3 endpoints, writes snapshot", async () => {
|
|
56
|
+
const bridge = makeFakeBridge({
|
|
57
|
+
cookieResult: {
|
|
58
|
+
cookie: "a1=fp; web_session=tok",
|
|
59
|
+
a1: "fp",
|
|
60
|
+
diagnostic: { cookieCount: 5 },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const apiClient = makeFakeApiClient({
|
|
64
|
+
notes: [{ noteId: "N1", title: "note", createdAt: 1716383021000 }],
|
|
65
|
+
liked: [{ noteId: "L1", title: "liked" }],
|
|
66
|
+
follows: [{ userId: "U1", nickname: "Friend" }],
|
|
67
|
+
});
|
|
68
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
69
|
+
|
|
70
|
+
expect(bridge.invoke).toHaveBeenCalledWith("xhs.cookies");
|
|
71
|
+
expect(apiClient.fetchMe).toHaveBeenCalledWith("a1=fp; web_session=tok");
|
|
72
|
+
expect(apiClient.fetchNotes).toHaveBeenCalledWith(
|
|
73
|
+
"a1=fp; web_session=tok",
|
|
74
|
+
"fp",
|
|
75
|
+
"5e8c8f7e1234abcdef",
|
|
76
|
+
expect.any(Object),
|
|
77
|
+
);
|
|
78
|
+
expect(result.userId).toBe("5e8c8f7e1234abcdef");
|
|
79
|
+
expect(result.nickname).toBe("Alice");
|
|
80
|
+
expect(result.eventCounts).toEqual({
|
|
81
|
+
note: 1,
|
|
82
|
+
liked: 1,
|
|
83
|
+
follow: 1,
|
|
84
|
+
total: 3,
|
|
85
|
+
});
|
|
86
|
+
expect(existsSync(result.snapshotPath)).toBe(true);
|
|
87
|
+
const snap = JSON.parse(readFileSync(result.snapshotPath, "utf-8"));
|
|
88
|
+
expect(snap.schemaVersion).toBe(1);
|
|
89
|
+
expect(snap.events).toHaveLength(3);
|
|
90
|
+
expect(result.meFetchFailed).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("collect — meFetchFailed (cookie expired)", () => {
|
|
95
|
+
it("returns empty-event snapshot + meFetchFailed=true", async () => {
|
|
96
|
+
const bridge = makeFakeBridge({
|
|
97
|
+
cookieResult: { cookie: "a1=fp; web_session=expired", a1: "fp" },
|
|
98
|
+
});
|
|
99
|
+
const apiClient = makeFakeApiClient({
|
|
100
|
+
me: null,
|
|
101
|
+
lastErrorCode: -7,
|
|
102
|
+
lastErrorMessage: "/user/me user_id blank",
|
|
103
|
+
});
|
|
104
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
105
|
+
expect(result.userId).toBe(null);
|
|
106
|
+
expect(result.meFetchFailed).toBe(true);
|
|
107
|
+
expect(result.eventCounts.total).toBe(0);
|
|
108
|
+
expect(apiClient.fetchNotes).not.toHaveBeenCalled();
|
|
109
|
+
expect(existsSync(result.snapshotPath)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("collect — failure modes", () => {
|
|
114
|
+
it("propagates bridge.invoke errors", async () => {
|
|
115
|
+
const bridge = makeFakeBridge({
|
|
116
|
+
throwOnInvoke: new Error("XHS_NO_ROOT: phone isn't rooted"),
|
|
117
|
+
});
|
|
118
|
+
await expect(collect(bridge, { stagingDir })).rejects.toThrow(/XHS_NO_ROOT/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws TypeError when bridge missing invoke", async () => {
|
|
122
|
+
await expect(collect(null, { stagingDir })).rejects.toThrow(TypeError);
|
|
123
|
+
await expect(collect({}, { stagingDir })).rejects.toThrow(TypeError);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("throws on malformed cookieResult (missing a1)", async () => {
|
|
127
|
+
const bridge = makeFakeBridge({
|
|
128
|
+
cookieResult: { cookie: "web_session=s", a1: null },
|
|
129
|
+
});
|
|
130
|
+
await expect(collect(bridge, { stagingDir })).rejects.toThrow(/malformed payload/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("collect — partial result (X-S signing best-effort)", () => {
|
|
135
|
+
it("0 events when X-S sign rejected on all 3 endpoints", async () => {
|
|
136
|
+
const bridge = makeFakeBridge({
|
|
137
|
+
cookieResult: { cookie: "a1=fp; web_session=tok", a1: "fp" },
|
|
138
|
+
});
|
|
139
|
+
const apiClient = makeFakeApiClient({
|
|
140
|
+
// me OK (no X-S) but all 3 X-S endpoints return [] (461 rejection)
|
|
141
|
+
lastErrorCode: 461,
|
|
142
|
+
lastErrorMessage: "X-S validation failed",
|
|
143
|
+
});
|
|
144
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
145
|
+
expect(result.eventCounts.total).toBe(0);
|
|
146
|
+
expect(result.lastErrorCode).toBe(461);
|
|
147
|
+
expect(result.meFetchFailed).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("partial event set when one endpoint X-S signed OK", async () => {
|
|
151
|
+
const bridge = makeFakeBridge({
|
|
152
|
+
cookieResult: { cookie: "a1=fp; web_session=tok", a1: "fp" },
|
|
153
|
+
});
|
|
154
|
+
const apiClient = makeFakeApiClient({
|
|
155
|
+
notes: [{ noteId: "N1", title: "x" }],
|
|
156
|
+
liked: [], // 461
|
|
157
|
+
follows: [], // 461
|
|
158
|
+
});
|
|
159
|
+
const result = await collect(bridge, { apiClient, stagingDir });
|
|
160
|
+
expect(result.eventCounts).toEqual({ note: 1, liked: 0, follow: 0, total: 1 });
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("collectAndSync", () => {
|
|
165
|
+
it("calls registry.syncAdapter + cleans up on success", async () => {
|
|
166
|
+
const bridge = makeFakeBridge({
|
|
167
|
+
cookieResult: { cookie: "a1=fp; web_session=tok", a1: "fp" },
|
|
168
|
+
});
|
|
169
|
+
const apiClient = makeFakeApiClient({
|
|
170
|
+
notes: [{ noteId: "N1" }],
|
|
171
|
+
});
|
|
172
|
+
let syncedPath = null;
|
|
173
|
+
const registry = {
|
|
174
|
+
syncAdapter: vi.fn(async (name, opts) => {
|
|
175
|
+
if (name !== "social-xiaohongshu") throw new Error("wrong name");
|
|
176
|
+
syncedPath = opts.inputPath;
|
|
177
|
+
return { adapter: name, status: "ok", rawCount: 1 };
|
|
178
|
+
}),
|
|
179
|
+
};
|
|
180
|
+
const report = await collectAndSync(bridge, registry, {
|
|
181
|
+
apiClient,
|
|
182
|
+
stagingDir,
|
|
183
|
+
});
|
|
184
|
+
expect(report.status).toBe("ok");
|
|
185
|
+
expect(report.xhs.userId).toBe("5e8c8f7e1234abcdef");
|
|
186
|
+
expect(report.xhs.eventCounts.note).toBe(1);
|
|
187
|
+
expect(existsSync(syncedPath)).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("cleanup even on syncAdapter throw", async () => {
|
|
191
|
+
const bridge = makeFakeBridge({
|
|
192
|
+
cookieResult: { cookie: "a1=fp; web_session=tok", a1: "fp" },
|
|
193
|
+
});
|
|
194
|
+
const apiClient = makeFakeApiClient();
|
|
195
|
+
let syncedPath = null;
|
|
196
|
+
const registry = {
|
|
197
|
+
syncAdapter: vi.fn(async (_n, opts) => {
|
|
198
|
+
syncedPath = opts.inputPath;
|
|
199
|
+
throw new Error("registry exploded");
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
await expect(
|
|
203
|
+
collectAndSync(bridge, registry, { apiClient, stagingDir }),
|
|
204
|
+
).rejects.toThrow("registry exploded");
|
|
205
|
+
expect(existsSync(syncedPath)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|