@chainlesschain/personal-data-hub 0.4.28 → 0.4.30
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/README.md +13 -5
- package/lib/adapters/social-douyin-adb/usage-profile-reader.js +253 -0
- package/lib/adapters/social-douyin-adb/watch-history-reader.js +104 -31
- package/lib/adapters/social-toutiao-adb/article-reader.js +202 -0
- package/lib/analysis-skills/overview.js +24 -4
- package/lib/analysis-skills/spending.js +63 -2
- package/lib/analysis-skills/timeline.js +11 -6
- package/lib/prompt-builder.js +15 -1
- package/lib/query-parser.js +38 -8
- package/package.json +4 -1
- package/__tests__/adapter-guide.test.js +0 -47
- package/__tests__/adapter-spec.test.js +0 -78
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
- package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
- package/__tests__/adapters/ai-chat-history.test.js +0 -396
- package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
- package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
- package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
- package/__tests__/adapters/apple-health.test.js +0 -95
- package/__tests__/adapters/bank-family.test.js +0 -125
- package/__tests__/adapters/biz-tianyancha.test.js +0 -159
- package/__tests__/adapters/browser-history-chrome.test.js +0 -377
- package/__tests__/adapters/browser-history-edge.test.js +0 -159
- package/__tests__/adapters/car-mercedesme.test.js +0 -74
- package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
- package/__tests__/adapters/doc-camscanner.test.js +0 -147
- package/__tests__/adapters/doc-platforms.test.js +0 -177
- package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
- package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
- package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
- package/__tests__/adapters/email-adapter.test.js +0 -742
- package/__tests__/adapters/email-classifier.test.js +0 -347
- package/__tests__/adapters/email-imap-session.test.js +0 -334
- package/__tests__/adapters/email-parser.test.js +0 -244
- package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
- package/__tests__/adapters/email-providers.test.js +0 -84
- package/__tests__/adapters/email-retry-progress.test.js +0 -294
- package/__tests__/adapters/email-templates.test.js +0 -822
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
- package/__tests__/adapters/finance-alipay-live.test.js +0 -258
- package/__tests__/adapters/finance-dcep.test.js +0 -74
- package/__tests__/adapters/fitness-joyrun.test.js +0 -82
- package/__tests__/adapters/game-genshin-live.test.js +0 -238
- package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
- package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
- package/__tests__/adapters/git-activity.test.js +0 -222
- package/__tests__/adapters/gov-12123.test.js +0 -103
- package/__tests__/adapters/gov-ixiamen.test.js +0 -150
- package/__tests__/adapters/gov-tax.test.js +0 -135
- package/__tests__/adapters/health-meiyou.test.js +0 -125
- package/__tests__/adapters/local-files.test.js +0 -264
- package/__tests__/adapters/local-im-pc.test.js +0 -154
- package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
- package/__tests__/adapters/music-kugou.test.js +0 -187
- package/__tests__/adapters/music-qq.test.js +0 -112
- package/__tests__/adapters/netease-music-live.test.js +0 -244
- package/__tests__/adapters/netease-music.test.js +0 -74
- package/__tests__/adapters/pc-local-discovery.test.js +0 -141
- package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
- package/__tests__/adapters/reading-family.test.js +0 -108
- package/__tests__/adapters/recruit-boss.test.js +0 -180
- package/__tests__/adapters/shell-history.test.js +0 -180
- package/__tests__/adapters/shopping-base.test.js +0 -179
- package/__tests__/adapters/shopping-dianping.test.js +0 -239
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
- package/__tests__/adapters/social-csdn.test.js +0 -175
- package/__tests__/adapters/social-dongchedi.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
- package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -192
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
- package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
- package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
- package/__tests__/adapters/social-zhihu.test.js +0 -246
- package/__tests__/adapters/system-data-adapter.test.js +0 -443
- package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
- package/__tests__/adapters/system-data-android.test.js +0 -519
- package/__tests__/adapters/system-data-disclosure.test.js +0 -153
- package/__tests__/adapters/travel-12306.test.js +0 -512
- package/__tests__/adapters/travel-amap.test.js +0 -219
- package/__tests__/adapters/travel-baidu-map.test.js +0 -305
- package/__tests__/adapters/travel-base.test.js +0 -205
- package/__tests__/adapters/travel-ctrip.test.js +0 -377
- package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
- package/__tests__/adapters/travel-didi.test.js +0 -204
- package/__tests__/adapters/travel-tencent-map.test.js +0 -207
- package/__tests__/adapters/travel-tongcheng.test.js +0 -289
- package/__tests__/adapters/video-platforms.test.js +0 -152
- package/__tests__/adapters/video-xigua.test.js +0 -106
- package/__tests__/adapters/vscode.test.js +0 -299
- package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
- package/__tests__/adapters/wechat-env-probe.test.js +0 -162
- package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
- package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
- package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
- package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
- package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
- package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
- package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
- package/__tests__/adapters/weread.test.js +0 -123
- package/__tests__/adapters/wework-pc.test.js +0 -124
- package/__tests__/adapters/win-recent.test.js +0 -192
- package/__tests__/analysis-skills.test.js +0 -679
- package/__tests__/analysis.test.js +0 -1845
- package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
- package/__tests__/batch.test.js +0 -133
- package/__tests__/bridges-cc-kg.test.js +0 -231
- package/__tests__/bridges-cc-llm.test.js +0 -191
- package/__tests__/bridges-cc-rag.test.js +0 -162
- package/__tests__/categories.test.js +0 -92
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
- package/__tests__/e2e/full-user-journey.test.js +0 -188
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
- package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
- package/__tests__/entity-resolver-stages.test.js +0 -411
- package/__tests__/entity-resolver-vault.test.js +0 -249
- package/__tests__/entity-resolver.test.js +0 -526
- package/__tests__/fitness-keep-snapshot.test.js +0 -224
- package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
- package/__tests__/ids.test.js +0 -45
- package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
- package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
- package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
- package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
- package/__tests__/key-providers.test.js +0 -126
- package/__tests__/kg-derive.test.js +0 -219
- package/__tests__/llm-client.test.js +0 -122
- package/__tests__/longtail-adapters.test.js +0 -281
- package/__tests__/messaging-qq-snapshot.test.js +0 -294
- package/__tests__/mobile-extractor-encrypted.test.js +0 -460
- package/__tests__/mobile-extractor.test.js +0 -288
- package/__tests__/mock-adapter.test.js +0 -93
- package/__tests__/prompt-builder.test.js +0 -249
- package/__tests__/query-parser.test.js +0 -302
- package/__tests__/rag-derive.test.js +0 -169
- package/__tests__/registry-readiness.test.js +0 -292
- package/__tests__/registry.test.js +0 -420
- package/__tests__/salvage-ingest.test.js +0 -97
- package/__tests__/schemas.test.js +0 -331
- package/__tests__/shopping-adapters.test.js +0 -392
- package/__tests__/shopping-eleme-snapshot.test.js +0 -454
- package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
- package/__tests__/shopping-snapshot.test.js +0 -438
- package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
- package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
- package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
- package/__tests__/sidecar-supervisor.test.js +0 -128
- package/__tests__/sign-providers.test.js +0 -62
- package/__tests__/social-adapters.test.js +0 -280
- package/__tests__/social-bilibili-snapshot.test.js +0 -278
- package/__tests__/social-douban-snapshot.test.js +0 -351
- package/__tests__/social-douyin-im-direct-read.test.js +0 -377
- package/__tests__/social-douyin-salvage-collector.test.js +0 -98
- package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
- package/__tests__/social-douyin-snapshot.test.js +0 -256
- package/__tests__/social-kuaishou-snapshot.test.js +0 -362
- package/__tests__/social-toutiao-snapshot.test.js +0 -366
- package/__tests__/social-weibo-snapshot.test.js +0 -234
- package/__tests__/social-weibo-sqlite-device.test.js +0 -174
- package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
- package/__tests__/sqlite-leaf-salvage.test.js +0 -97
- package/__tests__/travel-adapters.test.js +0 -483
- package/__tests__/travel-maps-snapshot.test.js +0 -426
- package/__tests__/vault-driver-error.test.js +0 -74
- package/__tests__/vault-search-helpers.test.js +0 -104
- package/__tests__/vault-search.test.js +0 -423
- package/__tests__/vault.test.js +0 -767
- package/__tests__/wechat-adapter.test.js +0 -594
- package/__tests__/whatsapp-adapter.test.js +0 -138
- package/scripts/_make-fixture-all.js +0 -126
- package/scripts/_make-fixture-contacts.js +0 -84
- package/scripts/evaluate-entity-resolver.js +0 -213
- package/scripts/run-native-tests-sandbox.sh +0 -55
- package/scripts/smoke-phase-5-5.js +0 -196
- package/scripts/smoke-phase-5-7.js +0 -181
- package/scripts/smoke-system-data-contacts.js +0 -309
- package/scripts/smoke-system-data.js +0 -312
- package/vitest.config.js +0 -88
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
SystemDataAdapter,
|
|
7
|
-
SOURCE_DESCRIPTORS,
|
|
8
|
-
SOURCE_KEYS,
|
|
9
|
-
sanitizeInclude,
|
|
10
|
-
resolveRetentionMs,
|
|
11
|
-
checkDisclosureCoverage,
|
|
12
|
-
buildDisclosurePayload,
|
|
13
|
-
} = require("../../lib/adapters/system-data");
|
|
14
|
-
|
|
15
|
-
class StubSupervisor {
|
|
16
|
-
invoke() { return Promise.resolve({}); }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
describe("SOURCE_DESCRIPTORS", () => {
|
|
22
|
-
it("declares all four sources with required metadata", () => {
|
|
23
|
-
expect(SOURCE_KEYS).toEqual(["contacts", "calllog", "sms", "wifi"]);
|
|
24
|
-
for (const key of SOURCE_KEYS) {
|
|
25
|
-
const d = SOURCE_DESCRIPTORS[key];
|
|
26
|
-
expect(d.label).toBeTruthy();
|
|
27
|
-
expect(Array.isArray(d.fields)).toBe(true);
|
|
28
|
-
expect(d.fields.length).toBeGreaterThan(0);
|
|
29
|
-
expect(["low", "medium", "high"]).toContain(d.sensitivity);
|
|
30
|
-
expect(typeof d.defaultEnabled).toBe("boolean");
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("SMS is the only opt-out source", () => {
|
|
35
|
-
const optOut = SOURCE_KEYS.filter((k) => !SOURCE_DESCRIPTORS[k].defaultEnabled);
|
|
36
|
-
expect(optOut).toEqual(["sms"]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("SMS carries a third-party-content warning", () => {
|
|
40
|
-
expect(SOURCE_DESCRIPTORS.sms.warning).toMatch(/他人/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("WiFi excludes password from collected fields", () => {
|
|
44
|
-
expect(SOURCE_DESCRIPTORS.wifi.excludedFields).toContain("password");
|
|
45
|
-
expect(SOURCE_DESCRIPTORS.wifi.fields).not.toContain("password");
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
describe("sanitizeInclude", () => {
|
|
52
|
-
it("returns descriptor defaults when input is empty", () => {
|
|
53
|
-
expect(sanitizeInclude({})).toEqual({
|
|
54
|
-
contacts: true, calllog: true, sms: false, wifi: true,
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns descriptor defaults when input is null", () => {
|
|
59
|
-
expect(sanitizeInclude(null)).toEqual({
|
|
60
|
-
contacts: true, calllog: true, sms: false, wifi: true,
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("coerces truthy non-boolean values to true", () => {
|
|
65
|
-
const out = sanitizeInclude({ sms: 1, contacts: "yes", wifi: 0 });
|
|
66
|
-
expect(out.sms).toBe(true);
|
|
67
|
-
expect(out.contacts).toBe(true);
|
|
68
|
-
expect(out.wifi).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("drops unknown keys silently", () => {
|
|
72
|
-
const out = sanitizeInclude({ contacts: true, MALICIOUS_KEY: true });
|
|
73
|
-
expect(out).not.toHaveProperty("MALICIOUS_KEY");
|
|
74
|
-
expect(Object.keys(out).sort()).toEqual(["calllog", "contacts", "sms", "wifi"]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("missing keys fall back to source defaults — sms stays false", () => {
|
|
78
|
-
const out = sanitizeInclude({ contacts: true });
|
|
79
|
-
expect(out.sms).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
describe("resolveRetentionMs", () => {
|
|
86
|
-
it("returns null when policy is missing", () => {
|
|
87
|
-
expect(resolveRetentionMs(null)).toBeNull();
|
|
88
|
-
expect(resolveRetentionMs({})).toBeNull();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("returns null when retentionDays is not positive", () => {
|
|
92
|
-
expect(resolveRetentionMs({ retentionDays: 0 })).toBeNull();
|
|
93
|
-
expect(resolveRetentionMs({ retentionDays: -10 })).toBeNull();
|
|
94
|
-
expect(resolveRetentionMs({ retentionDays: "abc" })).toBeNull();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("converts days to milliseconds", () => {
|
|
98
|
-
expect(resolveRetentionMs({ retentionDays: 1 })).toBe(24 * 60 * 60 * 1000);
|
|
99
|
-
expect(resolveRetentionMs({ retentionDays: 30 })).toBe(30 * 24 * 60 * 60 * 1000);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
describe("checkDisclosureCoverage", () => {
|
|
106
|
-
it("ok when adapter declares every descriptor field", () => {
|
|
107
|
-
const adapter = new SystemDataAdapter({ supervisor: new StubSupervisor() });
|
|
108
|
-
const result = checkDisclosureCoverage(adapter.dataDisclosure.fields);
|
|
109
|
-
if (!result.ok) {
|
|
110
|
-
console.error("missing fields:", result.missing);
|
|
111
|
-
}
|
|
112
|
-
expect(result.ok).toBe(true);
|
|
113
|
-
expect(result.missing).toEqual([]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("flags missing source", () => {
|
|
117
|
-
const result = checkDisclosureCoverage(["contacts:name", "calllog:number", "wifi:ssid"]);
|
|
118
|
-
// Missing sms entirely + missing fields under contacts/calllog/wifi
|
|
119
|
-
expect(result.ok).toBe(false);
|
|
120
|
-
expect(result.missing.some((m) => m.startsWith("sms:"))).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("flags missing per-source fields", () => {
|
|
124
|
-
const result = checkDisclosureCoverage([
|
|
125
|
-
"contacts:name", // missing phone/email/...
|
|
126
|
-
"calllog:number,duration,timestamp,type,name",
|
|
127
|
-
"sms:address,body,timestamp,type,threadId,isRead",
|
|
128
|
-
"wifi:ssid,securityType,hidden",
|
|
129
|
-
]);
|
|
130
|
-
expect(result.ok).toBe(false);
|
|
131
|
-
expect(result.missing).toContain("contacts:phone");
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
describe("buildDisclosurePayload", () => {
|
|
138
|
-
it("returns a payload that the UI can render directly", () => {
|
|
139
|
-
const payload = buildDisclosurePayload();
|
|
140
|
-
expect(payload.adapter).toBe("system-data");
|
|
141
|
-
expect(payload.sensitivity).toBe("high");
|
|
142
|
-
expect(payload.legalGate).toBe(true);
|
|
143
|
-
expect(payload.sources).toHaveLength(4);
|
|
144
|
-
expect(payload.sources.map((s) => s.key)).toEqual(["contacts", "calllog", "sms", "wifi"]);
|
|
145
|
-
expect(payload.legalDeclaration).toMatch(/合法使用者/);
|
|
146
|
-
expect(payload.legalDeclaration).toMatch(/不向任何第三方分发/);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("notice mentions the no-upload promise", () => {
|
|
150
|
-
const payload = buildDisclosurePayload();
|
|
151
|
-
expect(payload.notice).toMatch(/不向任何服务器上传/);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
const fs = require("node:fs");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
const os = require("node:os");
|
|
7
|
-
const crypto = require("node:crypto");
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
Train12306Adapter,
|
|
11
|
-
parseRecords,
|
|
12
|
-
ticketsFromOrder,
|
|
13
|
-
extractCompletedOrders,
|
|
14
|
-
extractPendingOrders,
|
|
15
|
-
parse12306DateTime,
|
|
16
|
-
parseYyyymmdd,
|
|
17
|
-
NAME,
|
|
18
|
-
VERSION,
|
|
19
|
-
SNAPSHOT_SCHEMA_VERSION,
|
|
20
|
-
VALID_SNAPSHOT_KINDS,
|
|
21
|
-
} = require("../../lib/adapters/travel-12306");
|
|
22
|
-
|
|
23
|
-
function writeTmp(content) {
|
|
24
|
-
const p = path.join(os.tmpdir(), `cc-12306-test-${crypto.randomUUID()}.json`);
|
|
25
|
-
fs.writeFileSync(p, content, "utf-8");
|
|
26
|
-
return p;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function collect(gen) {
|
|
30
|
-
const out = [];
|
|
31
|
-
for await (const x of gen) out.push(x);
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const TICKET_EVENT = {
|
|
36
|
-
kind: "ticket",
|
|
37
|
-
id: "ticket-SEQ1:0",
|
|
38
|
-
capturedAt: 1716383021000,
|
|
39
|
-
orderSequenceNo: "SEQ1",
|
|
40
|
-
ticketNumber: "E123456789",
|
|
41
|
-
passengerName: "张三",
|
|
42
|
-
passengerIdLast6: "123456",
|
|
43
|
-
trainNumber: "G35",
|
|
44
|
-
fromStation: "上海虹桥",
|
|
45
|
-
toStation: "北京南",
|
|
46
|
-
departureMs: 1716383021000,
|
|
47
|
-
arrivalMs: 1716401021000,
|
|
48
|
-
seatTypeName: "二等座",
|
|
49
|
-
coachNo: "05",
|
|
50
|
-
seatNo: "12A",
|
|
51
|
-
ticketPrice: 553.5,
|
|
52
|
-
orderDateMs: 1716000000000,
|
|
53
|
-
orderTotalPrice: 553.5,
|
|
54
|
-
isCompleted: true,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function makeSnapshot(events, extra = {}) {
|
|
58
|
-
return JSON.stringify({
|
|
59
|
-
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
60
|
-
snapshottedAt: 1716383021000,
|
|
61
|
-
vendor: "12306",
|
|
62
|
-
events,
|
|
63
|
-
...extra,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
describe("constants", () => {
|
|
68
|
-
it("exposes name/version/schema", () => {
|
|
69
|
-
expect(NAME).toBe("travel-12306");
|
|
70
|
-
expect(VERSION).toBe("0.7.0");
|
|
71
|
-
expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
|
|
72
|
-
expect([...VALID_SNAPSHOT_KINDS]).toEqual(["ticket"]);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("authenticate", () => {
|
|
77
|
-
it("snapshot mode ok when inputPath readable", async () => {
|
|
78
|
-
const p = writeTmp(makeSnapshot([]));
|
|
79
|
-
try {
|
|
80
|
-
const a = new Train12306Adapter();
|
|
81
|
-
expect(await a.authenticate({ inputPath: p })).toEqual({
|
|
82
|
-
ok: true,
|
|
83
|
-
mode: "snapshot-file",
|
|
84
|
-
});
|
|
85
|
-
} finally {
|
|
86
|
-
fs.unlinkSync(p);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("snapshot mode fails INPUT_PATH_UNREADABLE on missing file", async () => {
|
|
91
|
-
const a = new Train12306Adapter();
|
|
92
|
-
const r = await a.authenticate({
|
|
93
|
-
inputPath: path.join(os.tmpdir(), "nonexistent-12306.json"),
|
|
94
|
-
});
|
|
95
|
-
expect(r.ok).toBe(false);
|
|
96
|
-
expect(r.reason).toBe("INPUT_PATH_UNREADABLE");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("file-import mode requires account.username", async () => {
|
|
100
|
-
const noAccount = new Train12306Adapter({ dataPath: "x.json" });
|
|
101
|
-
expect((await noAccount.authenticate({})).reason).toBe(
|
|
102
|
-
"NO_ACCOUNT_USERNAME",
|
|
103
|
-
);
|
|
104
|
-
const withAccount = new Train12306Adapter({
|
|
105
|
-
dataPath: "x.json",
|
|
106
|
-
account: { username: "alice" },
|
|
107
|
-
});
|
|
108
|
-
expect(await withAccount.authenticate({})).toEqual({
|
|
109
|
-
ok: true,
|
|
110
|
-
account: "alice",
|
|
111
|
-
mode: "file-import",
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("fails NO_INPUT when neither path given", async () => {
|
|
116
|
-
const a = new Train12306Adapter();
|
|
117
|
-
expect((await a.authenticate({})).reason).toBe("NO_INPUT");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("sync — snapshot mode", () => {
|
|
122
|
-
it("yields ticket events with stable originalId", async () => {
|
|
123
|
-
const p = writeTmp(makeSnapshot([TICKET_EVENT]));
|
|
124
|
-
try {
|
|
125
|
-
const a = new Train12306Adapter();
|
|
126
|
-
const items = await collect(a.sync({ inputPath: p }));
|
|
127
|
-
expect(items).toHaveLength(1);
|
|
128
|
-
expect(items[0]).toMatchObject({
|
|
129
|
-
adapter: NAME,
|
|
130
|
-
kind: "ticket",
|
|
131
|
-
originalId: "12306:ticket:ticket-SEQ1:0",
|
|
132
|
-
capturedAt: 1716383021000,
|
|
133
|
-
});
|
|
134
|
-
expect(items[0].payload.snapshot).toBe(true);
|
|
135
|
-
expect(items[0].payload.trainNumber).toBe("G35");
|
|
136
|
-
} finally {
|
|
137
|
-
fs.unlinkSync(p);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("throws on schemaVersion mismatch", async () => {
|
|
142
|
-
const p = writeTmp(JSON.stringify({ schemaVersion: 99, events: [] }));
|
|
143
|
-
try {
|
|
144
|
-
const a = new Train12306Adapter();
|
|
145
|
-
await expect(collect(a.sync({ inputPath: p }))).rejects.toThrow(
|
|
146
|
-
/schemaVersion mismatch/,
|
|
147
|
-
);
|
|
148
|
-
} finally {
|
|
149
|
-
fs.unlinkSync(p);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("skips unknown kinds + honors include gate + limit", async () => {
|
|
154
|
-
const p = writeTmp(
|
|
155
|
-
makeSnapshot([
|
|
156
|
-
{ kind: "alien", id: "x" },
|
|
157
|
-
TICKET_EVENT,
|
|
158
|
-
{ ...TICKET_EVENT, id: "ticket-SEQ1:1" },
|
|
159
|
-
]),
|
|
160
|
-
);
|
|
161
|
-
try {
|
|
162
|
-
const a = new Train12306Adapter();
|
|
163
|
-
const all = await collect(a.sync({ inputPath: p }));
|
|
164
|
-
expect(all).toHaveLength(2);
|
|
165
|
-
const limited = await collect(a.sync({ inputPath: p, limit: 1 }));
|
|
166
|
-
expect(limited).toHaveLength(1);
|
|
167
|
-
const gated = await collect(
|
|
168
|
-
a.sync({ inputPath: p, include: { ticket: false } }),
|
|
169
|
-
);
|
|
170
|
-
expect(gated).toHaveLength(0);
|
|
171
|
-
} finally {
|
|
172
|
-
fs.unlinkSync(p);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("falls back capturedAt to departureMs then snapshottedAt", async () => {
|
|
177
|
-
const noCaptured = { ...TICKET_EVENT, capturedAt: undefined };
|
|
178
|
-
const noTimes = {
|
|
179
|
-
...TICKET_EVENT,
|
|
180
|
-
capturedAt: undefined,
|
|
181
|
-
departureMs: undefined,
|
|
182
|
-
};
|
|
183
|
-
const p = writeTmp(makeSnapshot([noCaptured, noTimes]));
|
|
184
|
-
try {
|
|
185
|
-
const a = new Train12306Adapter();
|
|
186
|
-
const items = await collect(a.sync({ inputPath: p }));
|
|
187
|
-
expect(items[0].capturedAt).toBe(TICKET_EVENT.departureMs);
|
|
188
|
-
expect(items[1].capturedAt).toBe(1716383021000); // snapshottedAt
|
|
189
|
-
} finally {
|
|
190
|
-
fs.unlinkSync(p);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("normalize — snapshot payload", () => {
|
|
196
|
-
it("maps ticket event to trip with traveler/cost/seat extras", () => {
|
|
197
|
-
const a = new Train12306Adapter();
|
|
198
|
-
const batch = a.normalize({
|
|
199
|
-
payload: { ...TICKET_EVENT, snapshot: true },
|
|
200
|
-
});
|
|
201
|
-
const ev = batch.events[0];
|
|
202
|
-
expect(ev.subtype).toBe("trip");
|
|
203
|
-
expect(ev.content.title).toBe("train: 上海虹桥 → 北京南");
|
|
204
|
-
expect(ev.content.amount).toEqual({
|
|
205
|
-
value: 553.5,
|
|
206
|
-
currency: "CNY",
|
|
207
|
-
direction: "out",
|
|
208
|
-
});
|
|
209
|
-
expect(ev.extra.vehicleNumber).toBe("G35");
|
|
210
|
-
expect(ev.extra.confirmationCode).toBe("E123456789");
|
|
211
|
-
expect(ev.extra.vendorExtras).toMatchObject({
|
|
212
|
-
seat: "二等座",
|
|
213
|
-
coachNo: "05",
|
|
214
|
-
seatNumber: "12A",
|
|
215
|
-
isCompleted: true,
|
|
216
|
-
idLast6: "123456",
|
|
217
|
-
});
|
|
218
|
-
const traveler = batch.persons.find((p) => p.subtype === "contact");
|
|
219
|
-
expect(traveler.names).toEqual(["张三"]);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("throws on missing payload / record", () => {
|
|
223
|
-
const a = new Train12306Adapter();
|
|
224
|
-
expect(() => a.normalize(null)).toThrow(/payload missing/);
|
|
225
|
-
expect(() => a.normalize({ payload: {} })).toThrow(
|
|
226
|
-
/payload\.record missing/,
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe("sync + parseRecords — file-import mode", () => {
|
|
232
|
-
const ORDER = {
|
|
233
|
-
orderId: "O1",
|
|
234
|
-
fromStation: "上海虹桥",
|
|
235
|
-
toStation: "北京南",
|
|
236
|
-
departureTime: 1716383021000,
|
|
237
|
-
trainNumber: "G35",
|
|
238
|
-
price: "553.5",
|
|
239
|
-
passengerName: "张三",
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
it("requires account.username at sync time", async () => {
|
|
243
|
-
const a = new Train12306Adapter({ dataPath: "whatever.json" });
|
|
244
|
-
await expect(collect(a.sync({}))).rejects.toThrow(
|
|
245
|
-
/account\.username required/,
|
|
246
|
-
);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("parses JSON array / {orders} / JSONL shapes", () => {
|
|
250
|
-
expect(parseRecords(JSON.stringify([ORDER]))).toHaveLength(1);
|
|
251
|
-
expect(parseRecords(JSON.stringify({ orders: [ORDER] }))).toHaveLength(1);
|
|
252
|
-
const jsonl = `${JSON.stringify(ORDER)}\n# comment\n${JSON.stringify({ ...ORDER, orderId: "O2" })}`;
|
|
253
|
-
const recs = parseRecords(jsonl);
|
|
254
|
-
expect(recs.map((r) => r.recordId)).toEqual(["O1", "O2"]);
|
|
255
|
-
expect(recs[0].vehicleType).toBe("train");
|
|
256
|
-
expect(recs[0].totalCost).toEqual({ value: 553.5, currency: "CNY" });
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("drops orders without any id", () => {
|
|
260
|
-
expect(parseRecords(JSON.stringify([{ price: "1" }]))).toHaveLength(0);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("yields from a real dump file end-to-end", async () => {
|
|
264
|
-
const p = writeTmp(JSON.stringify([ORDER]));
|
|
265
|
-
try {
|
|
266
|
-
const a = new Train12306Adapter({
|
|
267
|
-
dataPath: p,
|
|
268
|
-
account: { username: "alice" },
|
|
269
|
-
});
|
|
270
|
-
const items = await collect(a.sync({}));
|
|
271
|
-
expect(items).toHaveLength(1);
|
|
272
|
-
expect(items[0].originalId).toBe("O1");
|
|
273
|
-
const batch = a.normalize(items[0]);
|
|
274
|
-
expect(batch.events[0].content.title).toBe("train: 上海虹桥 → 北京南");
|
|
275
|
-
} finally {
|
|
276
|
-
fs.unlinkSync(p);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("sync throws when neither inputPath nor dataPath", async () => {
|
|
281
|
-
const a = new Train12306Adapter();
|
|
282
|
-
await expect(collect(a.sync({}))).rejects.toThrow(/needs opts\.inputPath/);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ─── §2.5 v0.3 cookie-api live fetch ────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
// One raw 12306 order object (queryMyOrder shape) carrying a single ticket.
|
|
289
|
-
function rawOrder(seq, overrides = {}) {
|
|
290
|
-
return {
|
|
291
|
-
sequence_no: seq,
|
|
292
|
-
order_date: "20240315",
|
|
293
|
-
ticket_total_price: "553.5",
|
|
294
|
-
tickets: [
|
|
295
|
-
{
|
|
296
|
-
ticket_no: "E123456789",
|
|
297
|
-
passenger_name: "张三",
|
|
298
|
-
passenger_id_no: "310101199001011234", // last6 = 011234
|
|
299
|
-
train_code: "G35",
|
|
300
|
-
from_station_name: "上海虹桥",
|
|
301
|
-
to_station_name: "北京南",
|
|
302
|
-
start_train_date_page: "2024-03-20 09:00",
|
|
303
|
-
arrive_train_date_page: "2024-03-20 14:00",
|
|
304
|
-
seat_type_name: "二等座",
|
|
305
|
-
coach_no: "05",
|
|
306
|
-
seat_no: "12A",
|
|
307
|
-
ticket_price: "553.5",
|
|
308
|
-
...overrides,
|
|
309
|
-
},
|
|
310
|
-
],
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const COOKIE = "tk=abc; JSESSIONID=xyz; RAIL_DEVICEID=dev";
|
|
315
|
-
|
|
316
|
-
describe("cookie-api helpers", () => {
|
|
317
|
-
it("extractCompletedOrders tolerates shape variants + junk", () => {
|
|
318
|
-
expect(extractCompletedOrders({ data: { OrderDTODataList: [1, 2] } })).toEqual([1, 2]);
|
|
319
|
-
expect(extractCompletedOrders({ data: { orderDTODataList: [3] } })).toEqual([3]);
|
|
320
|
-
expect(extractCompletedOrders(null)).toEqual([]);
|
|
321
|
-
expect(extractCompletedOrders({ data: {} })).toEqual([]);
|
|
322
|
-
expect(extractCompletedOrders("nope")).toEqual([]);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("extractPendingOrders tolerates shape variants + junk", () => {
|
|
326
|
-
expect(extractPendingOrders({ data: { orderDBList: [1] } })).toEqual([1]);
|
|
327
|
-
expect(extractPendingOrders({ data: { orderDbList: [2] } })).toEqual([2]);
|
|
328
|
-
expect(extractPendingOrders({})).toEqual([]);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("ticketsFromOrder flattens tickets with snake/camel fallbacks", () => {
|
|
332
|
-
const evs = ticketsFromOrder(rawOrder("SEQ1"), true);
|
|
333
|
-
expect(evs).toHaveLength(1);
|
|
334
|
-
expect(evs[0]).toMatchObject({
|
|
335
|
-
kind: "ticket",
|
|
336
|
-
id: "ticket-SEQ1:0",
|
|
337
|
-
orderSequenceNo: "SEQ1",
|
|
338
|
-
ticketNumber: "E123456789",
|
|
339
|
-
passengerName: "张三",
|
|
340
|
-
passengerIdLast6: "011234",
|
|
341
|
-
trainNumber: "G35",
|
|
342
|
-
fromStation: "上海虹桥",
|
|
343
|
-
toStation: "北京南",
|
|
344
|
-
seatTypeName: "二等座",
|
|
345
|
-
coachNo: "05",
|
|
346
|
-
seatNo: "12A",
|
|
347
|
-
ticketPrice: 553.5,
|
|
348
|
-
isCompleted: true,
|
|
349
|
-
capturedVia: "cookie-api",
|
|
350
|
-
});
|
|
351
|
-
// camelCase + *_page fallbacks
|
|
352
|
-
const camel = ticketsFromOrder(
|
|
353
|
-
{
|
|
354
|
-
sequenceNo: "SEQ2",
|
|
355
|
-
tickets: [
|
|
356
|
-
{
|
|
357
|
-
stationTrainCode: "D7",
|
|
358
|
-
passengerName: "李四",
|
|
359
|
-
from_station_name_page: "杭州东",
|
|
360
|
-
to_station_name_page: "南京南",
|
|
361
|
-
ticketPrice: "100",
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
},
|
|
365
|
-
false,
|
|
366
|
-
);
|
|
367
|
-
expect(camel[0]).toMatchObject({
|
|
368
|
-
trainNumber: "D7",
|
|
369
|
-
fromStation: "杭州东",
|
|
370
|
-
toStation: "南京南",
|
|
371
|
-
ticketPrice: 100,
|
|
372
|
-
isCompleted: false,
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it("ticketsFromOrder drops incomplete tickets + bad orders", () => {
|
|
377
|
-
expect(ticketsFromOrder(null, true)).toEqual([]);
|
|
378
|
-
expect(ticketsFromOrder({ tickets: [] }, true)).toEqual([]); // no sequence_no
|
|
379
|
-
// ticket missing passenger / train / stations → dropped
|
|
380
|
-
const evs = ticketsFromOrder(
|
|
381
|
-
{ sequence_no: "S", tickets: [{ passenger_name: "x" }] },
|
|
382
|
-
true,
|
|
383
|
-
);
|
|
384
|
-
expect(evs).toEqual([]);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it("parse12306DateTime handles date / date-time / Chinese / blank", () => {
|
|
388
|
-
// 2024-03-20 09:00 Shanghai = 2024-03-20 01:00 UTC
|
|
389
|
-
expect(parse12306DateTime("2024-03-20 09:00")).toBe(Date.UTC(2024, 2, 20, 1, 0));
|
|
390
|
-
expect(parse12306DateTime("2024-03-20")).toBe(Date.UTC(2024, 2, 19, 16, 0));
|
|
391
|
-
expect(parse12306DateTime("")).toBeNull();
|
|
392
|
-
expect(parse12306DateTime(null)).toBeNull();
|
|
393
|
-
expect(parse12306DateTime("garbage")).toBeNull();
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("parseYyyymmdd parses compact dates", () => {
|
|
397
|
-
expect(parseYyyymmdd("20240315")).toBe(Date.UTC(2024, 2, 14, 16, 0));
|
|
398
|
-
expect(parseYyyymmdd("2024-03-15")).toBeNull();
|
|
399
|
-
expect(parseYyyymmdd(null)).toBeNull();
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
describe("authenticate — cookie-api mode", () => {
|
|
404
|
-
it("ok when account.cookies present (username optional)", async () => {
|
|
405
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE } });
|
|
406
|
-
expect(await a.authenticate({})).toEqual({
|
|
407
|
-
ok: true,
|
|
408
|
-
account: null,
|
|
409
|
-
mode: "cookie",
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it("carries username when supplied alongside cookies", async () => {
|
|
414
|
-
const a = new Train12306Adapter({
|
|
415
|
-
account: { cookies: COOKIE, username: "alice" },
|
|
416
|
-
});
|
|
417
|
-
expect((await a.authenticate({})).account).toBe("alice");
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("cookie mode takes precedence over file-import", async () => {
|
|
421
|
-
const a = new Train12306Adapter({
|
|
422
|
-
account: { cookies: COOKIE },
|
|
423
|
-
dataPath: "x.json",
|
|
424
|
-
});
|
|
425
|
-
expect((await a.authenticate({})).mode).toBe("cookie");
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
describe("sync — cookie-api mode", () => {
|
|
430
|
-
it("yields flattened ticket events from completed + pending", async () => {
|
|
431
|
-
const calls = [];
|
|
432
|
-
const fetchFn = async ({ url, cookies, form }) => {
|
|
433
|
-
calls.push({ url, cookies, form });
|
|
434
|
-
if (url.includes("NoComplete")) {
|
|
435
|
-
return { data: { orderDBList: [rawOrder("PEND1")] } };
|
|
436
|
-
}
|
|
437
|
-
// single completed page (< PAGE_SIZE → stops)
|
|
438
|
-
return { data: { OrderDTODataList: [rawOrder("SEQ1")] } };
|
|
439
|
-
};
|
|
440
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE }, fetchFn });
|
|
441
|
-
const items = await collect(a.sync({}));
|
|
442
|
-
expect(items.map((i) => i.originalId)).toEqual([
|
|
443
|
-
"12306:ticket:ticket-SEQ1:0",
|
|
444
|
-
"12306:ticket:ticket-PEND1:0",
|
|
445
|
-
]);
|
|
446
|
-
expect(items[0].payload.snapshot).toBe(true);
|
|
447
|
-
expect(items[0].payload.capturedVia).toBe("cookie-api");
|
|
448
|
-
expect(items[1].payload.isCompleted).toBe(false);
|
|
449
|
-
// cookie header forwarded + completed form carries pagination params
|
|
450
|
-
expect(calls[0].cookies).toBe(COOKIE);
|
|
451
|
-
expect(calls[0].form).toMatchObject({
|
|
452
|
-
come_from_flag: "my_order",
|
|
453
|
-
pageSize: "50",
|
|
454
|
-
pageIndex: "1",
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it("paginates completed orders until a short page", async () => {
|
|
459
|
-
const fetchFn = async ({ url, form }) => {
|
|
460
|
-
if (url.includes("NoComplete")) return { data: { orderDBList: [] } };
|
|
461
|
-
const page = parseInt(form.pageIndex, 10);
|
|
462
|
-
if (page === 1) {
|
|
463
|
-
// exactly PAGE_SIZE (50) → triggers page 2
|
|
464
|
-
const orders = Array.from({ length: 50 }, (_, i) => rawOrder(`P1-${i}`));
|
|
465
|
-
return { data: { OrderDTODataList: orders } };
|
|
466
|
-
}
|
|
467
|
-
if (page === 2) return { data: { OrderDTODataList: [rawOrder("P2-0")] } };
|
|
468
|
-
return { data: { OrderDTODataList: [] } };
|
|
469
|
-
};
|
|
470
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE }, fetchFn });
|
|
471
|
-
const items = await collect(a.sync({}));
|
|
472
|
-
expect(items).toHaveLength(51); // 50 + 1, then short page stops
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it("honors limit + include gate", async () => {
|
|
476
|
-
const fetchFn = async ({ url }) =>
|
|
477
|
-
url.includes("NoComplete")
|
|
478
|
-
? { data: { orderDBList: [] } }
|
|
479
|
-
: { data: { OrderDTODataList: [rawOrder("A"), rawOrder("B")] } };
|
|
480
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE }, fetchFn });
|
|
481
|
-
expect(await collect(a.sync({ limit: 1 }))).toHaveLength(1);
|
|
482
|
-
expect(await collect(a.sync({ include: { ticket: false } }))).toHaveLength(0);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("empty responses yield nothing (expired cookie / no orders)", async () => {
|
|
486
|
-
const fetchFn = async () => ({});
|
|
487
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE }, fetchFn });
|
|
488
|
-
expect(await collect(a.sync({}))).toHaveLength(0);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it("throws when cookie mode active but no fetchFn injected", async () => {
|
|
492
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE } });
|
|
493
|
-
await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it("normalize maps a cookie ticket event → trip with capturedVia", async () => {
|
|
497
|
-
const fetchFn = async ({ url }) =>
|
|
498
|
-
url.includes("NoComplete")
|
|
499
|
-
? { data: { orderDBList: [] } }
|
|
500
|
-
: { data: { OrderDTODataList: [rawOrder("SEQ1")] } };
|
|
501
|
-
const a = new Train12306Adapter({ account: { cookies: COOKIE }, fetchFn });
|
|
502
|
-
const [item] = await collect(a.sync({}));
|
|
503
|
-
const batch = a.normalize(item);
|
|
504
|
-
const ev = batch.events[0];
|
|
505
|
-
expect(ev.subtype).toBe("trip");
|
|
506
|
-
expect(ev.content.title).toBe("train: 上海虹桥 → 北京南");
|
|
507
|
-
expect(ev.extra.vendorExtras).toMatchObject({
|
|
508
|
-
capturedVia: "cookie-api",
|
|
509
|
-
idLast6: "011234",
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
});
|