@chainlesschain/personal-data-hub 0.4.18 → 0.4.24

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.
Files changed (53) hide show
  1. package/__tests__/adapters/bank-family.test.js +125 -0
  2. package/__tests__/adapters/biz-tianyancha.test.js +159 -0
  3. package/__tests__/adapters/car-mercedesme.test.js +74 -0
  4. package/__tests__/adapters/doc-camscanner.test.js +147 -0
  5. package/__tests__/adapters/finance-dcep.test.js +74 -0
  6. package/__tests__/adapters/fitness-joyrun.test.js +82 -0
  7. package/__tests__/adapters/gov-12123.test.js +103 -0
  8. package/__tests__/adapters/gov-ixiamen.test.js +150 -0
  9. package/__tests__/adapters/gov-tax.test.js +135 -0
  10. package/__tests__/adapters/health-meiyou.test.js +125 -0
  11. package/__tests__/adapters/music-qq.test.js +112 -0
  12. package/__tests__/adapters/reading-family.test.js +108 -0
  13. package/__tests__/adapters/social-dongchedi.test.js +165 -0
  14. package/__tests__/adapters/travel-didi-consumer.test.js +66 -0
  15. package/__tests__/adapters/video-xigua.test.js +106 -0
  16. package/__tests__/adapters/wework-pc.test.js +124 -0
  17. package/__tests__/audio-ximalaya-snapshot.test.js +279 -0
  18. package/__tests__/fitness-keep-snapshot.test.js +224 -0
  19. package/__tests__/shopping-eleme-snapshot.test.js +454 -0
  20. package/__tests__/shopping-vipshop-snapshot.test.js +425 -0
  21. package/__tests__/shopping-xianyu-snapshot.test.js +451 -0
  22. package/__tests__/social-douban-snapshot.test.js +351 -0
  23. package/lib/adapter-guide.js +31 -3
  24. package/lib/adapters/_bank-base.js +405 -0
  25. package/lib/adapters/_reading-base.js +315 -0
  26. package/lib/adapters/audio-ximalaya/index.js +414 -0
  27. package/lib/adapters/bank-bankcomm/index.js +27 -0
  28. package/lib/adapters/bank-boc/index.js +26 -0
  29. package/lib/adapters/bank-cmbc/index.js +26 -0
  30. package/lib/adapters/bank-icbc/index.js +27 -0
  31. package/lib/adapters/biz-tianyancha/index.js +348 -0
  32. package/lib/adapters/car-mercedesme/index.js +225 -0
  33. package/lib/adapters/doc-camscanner/index.js +102 -0
  34. package/lib/adapters/finance-dcep/index.js +302 -0
  35. package/lib/adapters/fitness-joyrun/index.js +295 -0
  36. package/lib/adapters/fitness-keep/index.js +343 -0
  37. package/lib/adapters/gov-12123/index.js +391 -0
  38. package/lib/adapters/gov-ixiamen/index.js +380 -0
  39. package/lib/adapters/gov-tax/index.js +451 -0
  40. package/lib/adapters/health-meiyou/index.js +393 -0
  41. package/lib/adapters/music-qq/index.js +372 -0
  42. package/lib/adapters/reading-fanqie/index.js +61 -0
  43. package/lib/adapters/reading-qimao/index.js +61 -0
  44. package/lib/adapters/shopping-eleme/index.js +441 -0
  45. package/lib/adapters/shopping-vipshop/index.js +429 -0
  46. package/lib/adapters/shopping-xianyu/index.js +454 -0
  47. package/lib/adapters/social-dongchedi/index.js +360 -0
  48. package/lib/adapters/social-douban/index.js +564 -0
  49. package/lib/adapters/travel-didi-consumer/index.js +148 -0
  50. package/lib/adapters/video-xigua/index.js +68 -0
  51. package/lib/adapters/wework-pc/index.js +31 -0
  52. package/lib/index.js +52 -0
  53. package/package.json +1 -1
@@ -0,0 +1,125 @@
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 base = require("../../lib/adapters/_bank-base");
10
+ const cmbc = require("../../lib/adapters/bank-cmbc");
11
+ const boc = require("../../lib/adapters/bank-boc");
12
+ const bankcomm = require("../../lib/adapters/bank-bankcomm");
13
+ const icbc = require("../../lib/adapters/bank-icbc");
14
+
15
+ function writeTmp(content) {
16
+ const p = path.join(os.tmpdir(), `cc-bank-${crypto.randomUUID()}.json`);
17
+ fs.writeFileSync(p, content, "utf-8");
18
+ return p;
19
+ }
20
+ async function collect(gen) {
21
+ const out = [];
22
+ for await (const x of gen) out.push(x);
23
+ return out;
24
+ }
25
+ const COOKIES = "MBANK_SSO=abc; sid=xyz";
26
+
27
+ describe("_bank-base helpers", () => {
28
+ it("normDirection: keyword + amount-sign fallback", () => {
29
+ expect(base.normDirection({ direction: "收入" })).toBe("credit");
30
+ expect(base.normDirection({ flag: "支出" })).toBe("debit");
31
+ expect(base.normDirection({ amount: -50 })).toBe("debit");
32
+ expect(base.normDirection({ amount: 50 })).toBe("credit");
33
+ });
34
+ it("mapTransaction / mapCard aliases; no id → null", () => {
35
+ const tx = base.mapTransaction({ serialNo: "T1", tranTime: 1716383000, tranAmount: "-1,234.56", oppName: "星巴克", abstract: "消费", bal: 9999 });
36
+ expect(tx).toMatchObject({ txId: "T1", direction: "debit", counterparty: "星巴克", summary: "消费" });
37
+ expect(tx.amount).toBeCloseTo(1234.56, 2);
38
+ expect(tx.balance).toBe(9999);
39
+ expect(base.mapTransaction({ amount: 1 })).toBe(null);
40
+ const card = base.mapCard({ billId: "B1", billMonth: "2025-03", statementAmount: 3210, minPayment: 321, status: "已出账" });
41
+ expect(card).toMatchObject({ billId: "B1", billMonth: "2025-03", statementAmount: 3210, status: "已出账" });
42
+ expect(base.mapCard({ status: "x" })).toBe(null);
43
+ });
44
+ it("billMonthToMs", () => {
45
+ expect(base.billMonthToMs("2025-03")).toBe(Date.parse("2025-03-01T00:00:00Z"));
46
+ });
47
+ });
48
+
49
+ describe("bank wrappers identity", () => {
50
+ it("each wrapper has its own name + adapter class", () => {
51
+ expect(cmbc.NAME).toBe("bank-cmbc");
52
+ expect(boc.NAME).toBe("bank-boc");
53
+ expect(bankcomm.NAME).toBe("bank-bankcomm");
54
+ expect(icbc.NAME).toBe("bank-icbc");
55
+ expect(new cmbc.CmbcBankAdapter().name).toBe("bank-cmbc");
56
+ expect(new boc.BocBankAdapter().name).toBe("bank-boc");
57
+ expect(new bankcomm.BankcommBankAdapter().name).toBe("bank-bankcomm");
58
+ expect(new icbc.IcbcBankAdapter().name).toBe("bank-icbc");
59
+ });
60
+ it("all gated high sensitivity + legalGate", () => {
61
+ for (const A of [new cmbc.CmbcBankAdapter(), new boc.BocBankAdapter(), new bankcomm.BankcommBankAdapter(), new icbc.IcbcBankAdapter()]) {
62
+ expect(A.dataDisclosure.sensitivity).toBe("high");
63
+ expect(A.dataDisclosure.legalGate).toBe(true);
64
+ }
65
+ });
66
+ });
67
+
68
+ describe("CmbcBankAdapter (via _bank-base)", () => {
69
+ const SNAP = JSON.stringify({
70
+ schemaVersion: 1,
71
+ snapshottedAt: 1716383000000,
72
+ account: { userId: "u1" },
73
+ events: [
74
+ { kind: "transaction", id: "tx-T1", txId: "T1", time: 1716383000, amount: -88.5, direction: "支出", counterparty: "全家", summary: "消费", balance: 1200 },
75
+ { kind: "card", id: "card-C1", billId: "C1", billMonth: "2025-03", statementAmount: 3210, minPayment: 321, status: "已出账" },
76
+ ],
77
+ });
78
+
79
+ it("transaction → PAYMENT event; card → OTHER event", async () => {
80
+ const p = writeTmp(SNAP);
81
+ try {
82
+ const a = new cmbc.CmbcBankAdapter();
83
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
84
+ const items = await collect(a.sync({ inputPath: p }));
85
+ expect(items).toHaveLength(2);
86
+ const tx = a.normalize(items[0]);
87
+ expect(tx.events[0].subtype).toBe("payment");
88
+ expect(tx.events[0].content.title).toContain("支出");
89
+ expect(tx.events[0].extra.amount).toBe(88.5);
90
+ expect(tx.events[0].extra.direction).toBe("debit");
91
+ expect(items[0].originalId).toBe("cmbc:transaction:T1");
92
+ const card = a.normalize(items[1]);
93
+ expect(card.events[0].subtype).toBe("other");
94
+ expect(card.events[0].extra.billMonth).toBe("2025-03");
95
+ expect(items[1].originalId).toBe("cmbc:card:C1");
96
+ } finally {
97
+ fs.unlinkSync(p);
98
+ }
99
+ });
100
+
101
+ it("cookie-api: best-effort fetch + unverified + sign seam", async () => {
102
+ let signed = 0;
103
+ const a = new cmbc.CmbcBankAdapter({
104
+ account: { cookies: COOKIES, userId: "u1" },
105
+ signProvider: async () => { signed += 1; return "sig"; },
106
+ fetchFn: async ({ url, query }) => {
107
+ if (query.page > 1) return { list: [] };
108
+ if (url.includes("transactions")) return { list: [{ txId: "T9", time: 1716383000, amount: 500, direction: "收入", summary: "工资" }] };
109
+ return { list: [{ billId: "C9", billMonth: "2025-02", statementAmount: 1000 }] };
110
+ },
111
+ });
112
+ expect(await a.authenticate()).toMatchObject({ ok: true, mode: "cookie", unverified: true });
113
+ const items = await collect(a.sync({}));
114
+ expect(items).toHaveLength(2);
115
+ expect(items.map((i) => i.originalId).sort()).toEqual(["cmbc:card:C9", "cmbc:transaction:T9"]);
116
+ expect(signed).toBeGreaterThan(0);
117
+ });
118
+
119
+ it("default fetch throws; no input throws", async () => {
120
+ const a = new cmbc.CmbcBankAdapter({ account: { cookies: COOKIES } });
121
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
122
+ const b = new cmbc.CmbcBankAdapter();
123
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
124
+ });
125
+ });
@@ -0,0 +1,159 @@
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
+ TianyanchaAdapter,
11
+ extractData,
12
+ NAME,
13
+ VERSION,
14
+ SNAPSHOT_SCHEMA_VERSION,
15
+ } = require("../../lib/adapters/biz-tianyancha");
16
+
17
+ function writeTmp(content) {
18
+ const p = path.join(os.tmpdir(), `cc-tyc-${crypto.randomUUID()}.json`);
19
+ fs.writeFileSync(p, content, "utf-8");
20
+ return p;
21
+ }
22
+ async function collect(gen) {
23
+ const out = [];
24
+ for await (const x of gen) out.push(x);
25
+ return out;
26
+ }
27
+
28
+ const COOKIES = "auth_token=abc; TYCID=xyz";
29
+
30
+ const SNAP = JSON.stringify({
31
+ schemaVersion: 1,
32
+ snapshottedAt: 1716383000000,
33
+ account: { userId: "u1" },
34
+ events: [
35
+ { kind: "monitor", id: "mon-G1", companyId: "G1", companyName: "字节跳动有限公司", legalPerson: "张利东", regStatus: "存续", capturedAt: 1716300000000 },
36
+ { kind: "search", id: "s-1", query: "小米科技", companyName: "小米科技有限责任公司", capturedAt: 1716320000000 },
37
+ ],
38
+ });
39
+
40
+ describe("constants + extractData", () => {
41
+ it("name/version/schema", () => {
42
+ expect(NAME).toBe("biz-tianyancha");
43
+ expect(VERSION).toBe("0.1.0");
44
+ expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
45
+ });
46
+ it("extractData tolerant", () => {
47
+ expect(extractData({ data: [{ id: 1 }] })).toHaveLength(1);
48
+ expect(extractData({ data: { resultList: [{ id: 1 }] } })).toHaveLength(1);
49
+ expect(extractData({ list: [{ id: 1 }] })).toHaveLength(1);
50
+ expect(extractData({})).toEqual([]);
51
+ });
52
+ });
53
+
54
+ describe("TianyanchaAdapter snapshot mode", () => {
55
+ it("authenticate validates inputPath", async () => {
56
+ const p = writeTmp(SNAP);
57
+ try {
58
+ const a = new TianyanchaAdapter();
59
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
60
+ expect((await a.authenticate({ inputPath: path.join(os.tmpdir(), "no-tyc.json") })).reason).toBe("INPUT_PATH_UNREADABLE");
61
+ } finally {
62
+ fs.unlinkSync(p);
63
+ }
64
+ });
65
+
66
+ it("sync 2 kinds + normalize monitor→like / search→interaction", async () => {
67
+ const p = writeTmp(SNAP);
68
+ try {
69
+ const a = new TianyanchaAdapter();
70
+ const items = await collect(a.sync({ inputPath: p }));
71
+ expect(items.map((x) => x.kind)).toEqual(["monitor", "search"]);
72
+
73
+ const mon = a.normalize(items[0]);
74
+ expect(mon.events[0].subtype).toBe("like");
75
+ expect(mon.events[0].content.title).toBe("关注公司: 字节跳动有限公司");
76
+ expect(mon.events[0].extra.legalPerson).toBe("张利东");
77
+ expect(mon.events[0].extra.regStatus).toBe("存续");
78
+
79
+ const search = a.normalize(items[1]);
80
+ expect(search.events[0].subtype).toBe("interaction");
81
+ expect(search.events[0].content.title).toBe("搜索企业: 小米科技");
82
+ expect(search.events[0].extra.companyName).toBe("小米科技有限责任公司");
83
+ } finally {
84
+ fs.unlinkSync(p);
85
+ }
86
+ });
87
+
88
+ it("include + limit + schema mismatch + unknown kind", async () => {
89
+ const p = writeTmp(SNAP);
90
+ try {
91
+ const a = new TianyanchaAdapter();
92
+ expect((await collect(a.sync({ inputPath: p, include: { monitor: false } }))).map((x) => x.kind)).toEqual(["search"]);
93
+ expect(await collect(a.sync({ inputPath: p, limit: 1 }))).toHaveLength(1);
94
+ expect(() => a.normalize({ kind: "bogus", payload: {} })).toThrow(/unknown kind/);
95
+ } finally {
96
+ fs.unlinkSync(p);
97
+ }
98
+ const bad = writeTmp(JSON.stringify({ schemaVersion: 9, events: [] }));
99
+ try {
100
+ const a = new TianyanchaAdapter();
101
+ await expect(collect(a.sync({ inputPath: bad }))).rejects.toThrow(/schemaVersion mismatch/);
102
+ } finally {
103
+ fs.unlinkSync(bad);
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("TianyanchaAdapter cookie-api mode", () => {
109
+ it("authenticate cookie (userId optional)", async () => {
110
+ const a = new TianyanchaAdapter({ account: { cookies: COOKIES } });
111
+ expect(await a.authenticate()).toEqual({ ok: true, account: null, mode: "cookie" });
112
+ });
113
+
114
+ it("sync fetches monitor + search, normalizes", async () => {
115
+ const byUrl = (u) => (u.includes("monitor") ? "monitor" : "search");
116
+ const data = {
117
+ monitor: [{ graphId: "G9", companyName: "腾讯科技", legalPersonName: "马化腾", createTime: 1716300000 }],
118
+ search: [{ id: "h1", keyword: "阿里巴巴", searchTime: 1716320000 }],
119
+ };
120
+ const calls = [];
121
+ const a = new TianyanchaAdapter({
122
+ account: { cookies: COOKIES, userId: "u1" },
123
+ fetchFn: async ({ url, cookies, query, sign }) => {
124
+ const k = byUrl(url);
125
+ calls.push({ k, cookies, pageNum: query.pageNum, sign });
126
+ return { data: { list: query.pageNum === 1 ? data[k] : [] } };
127
+ },
128
+ });
129
+ const items = await collect(a.sync({}));
130
+ expect(items.map((x) => x.kind).sort()).toEqual(["monitor", "search"]);
131
+ expect(calls.every((c) => c.cookies === COOKIES && c.sign === null)).toBe(true);
132
+ const mon = a.normalize(items.find((x) => x.kind === "monitor"));
133
+ expect(mon.events[0].content.title).toBe("关注公司: 腾讯科技");
134
+ expect(mon.events[0].extra.legalPerson).toBe("马化腾");
135
+ const search = a.normalize(items.find((x) => x.kind === "search"));
136
+ expect(search.events[0].content.title).toBe("搜索企业: 阿里巴巴");
137
+ });
138
+
139
+ it("invokes signProvider + limit + empty + default fetch + no input", async () => {
140
+ const signCalls = [];
141
+ const a = new TianyanchaAdapter({
142
+ account: { cookies: COOKIES },
143
+ fetchFn: async ({ query }) => ({ data: { list: query.pageNum === 1 ? [{ graphId: "G1", companyName: "a" }, { graphId: "G2", companyName: "b" }] : [] } }),
144
+ signProvider: async (ctx) => { signCalls.push(ctx); return "sig"; },
145
+ });
146
+ expect(await collect(a.sync({ limit: 1, include: { search: false } }))).toHaveLength(1);
147
+ expect(signCalls.length).toBeGreaterThan(0);
148
+ expect(signCalls[0].cookies).toBe(COOKIES);
149
+
150
+ const a2 = new TianyanchaAdapter({ account: { cookies: COOKIES }, fetchFn: async () => "<html>login</html>" });
151
+ expect(await collect(a2.sync({}))).toEqual([]);
152
+
153
+ const a3 = new TianyanchaAdapter({ account: { cookies: COOKIES } });
154
+ await expect(collect(a3.sync({}))).rejects.toThrow(/no fetchFn configured/);
155
+
156
+ const a4 = new TianyanchaAdapter();
157
+ await expect(collect(a4.sync({}))).rejects.toThrow(/needs opts.inputPath/);
158
+ });
159
+ });
@@ -0,0 +1,74 @@
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 mb = require("../../lib/adapters/car-mercedesme");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-mb-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+ const COOKIES = "mb_token=abc";
22
+
23
+ describe("car-mercedesme", () => {
24
+ it("name + tripToRecord (km direct + meters fallback; no id → null)", () => {
25
+ expect(mb.NAME).toBe("car-mercedesme");
26
+ const r = mb.tripToRecord({ tripId: "T1", startTime: 1716383000, endTime: 1716385000, startAddress: "家", endAddress: "公司", distanceKm: 12.4, plate: "京A12345" });
27
+ expect(r).toMatchObject({ vendorId: "mercedesme", recordId: "T1", vehicleType: "car", carrier: "Mercedes me", vehicleNumber: "京A12345" });
28
+ expect(r.from.name).toBe("家");
29
+ expect(r.to.name).toBe("公司");
30
+ expect(r.extras.distanceKm).toBeCloseTo(12.4, 2);
31
+ // meters fallback
32
+ expect(mb.tripToRecord({ tripId: "T2", distanceMeters: 5230 }).extras.distanceKm).toBeCloseTo(5.23, 2);
33
+ expect(mb.tripToRecord({ startAddress: "x" })).toBe(null);
34
+ });
35
+ it("extractTrips tolerant", () => {
36
+ expect(mb.extractTrips({ trips: [{ tripId: 1 }] })).toHaveLength(1);
37
+ expect(mb.extractTrips({ data: { list: [{ tripId: 1 }] } })).toHaveLength(1);
38
+ expect(mb.extractTrips({})).toEqual([]);
39
+ });
40
+
41
+ it("snapshot trip → car travel event", async () => {
42
+ const SNAP = JSON.stringify({ trips: [{ tripId: "T1", startTime: 1716383000, endTime: 1716385000, startAddress: "家", endAddress: "公司", distanceKm: 12.4 }] });
43
+ const p = writeTmp(SNAP);
44
+ try {
45
+ const a = new mb.MercedesMeAdapter();
46
+ const items = await collect(a.sync({ inputPath: p }));
47
+ expect(items).toHaveLength(1);
48
+ expect(items[0].originalId).toBe("T1");
49
+ const b = a.normalize(items[0]);
50
+ expect(b.events[0].source.adapter).toBe("car-mercedesme");
51
+ } finally {
52
+ fs.unlinkSync(p);
53
+ }
54
+ });
55
+
56
+ it("cookie-api: unverified + sign seam", async () => {
57
+ let signed = 0;
58
+ const a = new mb.MercedesMeAdapter({
59
+ account: { cookies: COOKIES, userId: "u1" },
60
+ signProvider: async () => { signed += 1; return "sig"; },
61
+ fetchFn: async ({ query }) => (query.page > 1 ? { trips: [] } : { trips: [{ tripId: "T9", startTime: 1716383000, distanceMeters: 8000 }] }),
62
+ });
63
+ expect(await a.authenticate()).toMatchObject({ ok: true, mode: "cookie", unverified: true });
64
+ const items = await collect(a.sync({}));
65
+ expect(items).toHaveLength(1);
66
+ expect(items[0].originalId).toBe("T9");
67
+ expect(signed).toBeGreaterThan(0);
68
+ });
69
+
70
+ it("medium sensitivity; default fetch throws", async () => {
71
+ expect(new mb.MercedesMeAdapter().dataDisclosure.sensitivity).toBe("medium");
72
+ await expect(collect(new mb.MercedesMeAdapter({ account: { cookies: COOKIES } }).sync({}))).rejects.toThrow(/no fetchFn/);
73
+ });
74
+ });
@@ -0,0 +1,147 @@
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 cs = require("../../lib/adapters/doc-camscanner");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-cs-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+
22
+ const COOKIES = "INTSIG_TOKEN=abc; PHPSESSID=xyz";
23
+
24
+ describe("doc-camscanner mappers", () => {
25
+ it("name/version", () => {
26
+ expect(cs.NAME).toBe("doc-camscanner");
27
+ expect(cs.VERSION).toBe("0.1.0");
28
+ });
29
+ it("mapDoc maps CamScanner fields; doc_type + extension → docType", () => {
30
+ const rec = cs.mapDoc({
31
+ sync_doc_id: "DOC123",
32
+ title: "营业执照",
33
+ doc_type: 2,
34
+ page_num: 3,
35
+ create_time: 1716383000,
36
+ modify_time: 1716390000,
37
+ pdf_url: "https://cs/DOC123.pdf",
38
+ tags: ["证件"],
39
+ });
40
+ expect(rec).toMatchObject({ docId: "DOC123", title: "营业执照", docType: "certificate" });
41
+ expect(rec.createdMs).toBe(1716383000000);
42
+ expect(rec.updatedMs).toBe(1716390000000);
43
+ expect(rec.url).toBe("https://cs/DOC123.pdf");
44
+ expect(rec.extra.pageNum).toBe(3);
45
+ expect(rec.extra.tags).toEqual(["证件"]);
46
+ // extension fallback when no doc_type
47
+ expect(cs.mapDoc({ doc_id: 1, title: "报表.xlsx" }).docType).toBe("excel");
48
+ expect(cs.mapDoc({ doc_id: 2, title: "合同.pdf" }).docType).toBe("pdf");
49
+ // default → scan
50
+ expect(cs.mapDoc({ doc_id: 3, title: "随手拍" }).docType).toBe("scan");
51
+ // no id → null
52
+ expect(cs.mapDoc({ title: "noid" })).toBe(null);
53
+ });
54
+ it("extractDocs tolerant across shapes", () => {
55
+ expect(cs.extractDocs({ docs: [{ doc_id: 1 }] })).toHaveLength(1);
56
+ expect(cs.extractDocs({ list: [{ doc_id: 1 }] })).toHaveLength(1);
57
+ expect(cs.extractDocs({ data: { docs: [{ doc_id: 1 }] } })).toHaveLength(1);
58
+ expect(cs.extractDocs({})).toEqual([]);
59
+ });
60
+ });
61
+
62
+ describe("CamScannerDocAdapter (via _document-base)", () => {
63
+ const SNAP = JSON.stringify({
64
+ schemaVersion: 1,
65
+ snapshottedAt: 1716383000000,
66
+ account: { userId: "u1" },
67
+ events: [
68
+ {
69
+ kind: "document",
70
+ id: "doc-S1",
71
+ docId: "S1",
72
+ title: "身份证扫描",
73
+ docType: "certificate",
74
+ updatedTime: 1716383000,
75
+ url: "https://cs/S1.pdf",
76
+ },
77
+ ],
78
+ });
79
+
80
+ it("snapshot sync + normalize → event(post)+item(document)", async () => {
81
+ const p = writeTmp(SNAP);
82
+ try {
83
+ const a = new cs.CamScannerDocAdapter();
84
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
85
+ const items = await collect(a.sync({ inputPath: p }));
86
+ expect(items).toHaveLength(1);
87
+ const batch = a.normalize(items[0]);
88
+ expect(batch.events[0].subtype).toBe("post");
89
+ expect(batch.items[0].subtype).toBe("document");
90
+ expect(batch.items[0].name).toBe("身份证扫描");
91
+ expect(batch.items[0].extra.platform).toBe("camscanner");
92
+ } finally {
93
+ fs.unlinkSync(p);
94
+ }
95
+ });
96
+
97
+ it("cookie-api: fetch + paginate + normalize", async () => {
98
+ const pages = [
99
+ { docs: [{ sync_doc_id: 7, title: "发票.pdf", doc_type: "pdf", modify_time: 1716383000 }] },
100
+ { docs: [] },
101
+ ];
102
+ const calls = [];
103
+ const a = new cs.CamScannerDocAdapter({
104
+ account: { cookies: COOKIES, userId: "u1" },
105
+ fetchFn: async ({ url, cookies, query, sign }) => {
106
+ calls.push({ url, cookies, offset: query.offset, sign });
107
+ return query.offset === 0 ? pages[0] : pages[1];
108
+ },
109
+ });
110
+ expect(await a.authenticate()).toEqual({ ok: true, account: "u1", mode: "cookie" });
111
+ const items = await collect(a.sync({}));
112
+ expect(items).toHaveLength(1);
113
+ expect(items[0].originalId).toBe("camscanner:document:7");
114
+ expect(calls[0].cookies).toBe(COOKIES);
115
+ expect(calls[0].sign).toBe(null);
116
+ const batch = a.normalize(items[0]);
117
+ expect(batch.items[0].name).toBe("发票.pdf");
118
+ expect(batch.items[0].extra.docType).toBe("pdf");
119
+ });
120
+
121
+ it("cookie-api: signProvider seam invoked when present", async () => {
122
+ let seen = null;
123
+ const a = new cs.CamScannerDocAdapter({
124
+ account: { cookies: COOKIES, userId: "u1" },
125
+ signProvider: async ({ url, query }) => {
126
+ seen = { url, offset: query.offset };
127
+ return "sig-abc";
128
+ },
129
+ fetchFn: async ({ sign, query }) => {
130
+ return query.offset === 0
131
+ ? { docs: [{ sync_doc_id: "X", title: "t", _sign: sign }] }
132
+ : { docs: [] };
133
+ },
134
+ });
135
+ const items = await collect(a.sync({}));
136
+ expect(items).toHaveLength(1);
137
+ expect(seen.offset).toBe(0);
138
+ expect(seen.url).toContain("intsig");
139
+ });
140
+
141
+ it("default fetch throws; no input throws", async () => {
142
+ const a = new cs.CamScannerDocAdapter({ account: { cookies: COOKIES } });
143
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
144
+ const b = new cs.CamScannerDocAdapter();
145
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
146
+ });
147
+ });
@@ -0,0 +1,74 @@
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 dc = require("../../lib/adapters/finance-dcep");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-dcep-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+ const COOKIES = "DCEP_SSO=abc";
22
+
23
+ describe("finance-dcep", () => {
24
+ it("name/version + mappers", () => {
25
+ expect(dc.NAME).toBe("finance-dcep");
26
+ expect(dc.normDirection({ direction: "收款" })).toBe("receive");
27
+ expect(dc.normDirection({ amount: -5 })).toBe("pay");
28
+ const t = dc.mapTx({ txId: "X1", time: 1716383000, amount: -12.5, merchant: "便利店", subWallet: "中行子钱包" });
29
+ expect(t).toMatchObject({ txId: "X1", direction: "pay", counterparty: "便利店", walletType: "中行子钱包" });
30
+ expect(t.amount).toBe(12.5);
31
+ expect(dc.mapTx({ amount: 1 })).toBe(null);
32
+ });
33
+
34
+ it("snapshot → PAYMENT event; cookie-api unverified + sign", async () => {
35
+ const SNAP = JSON.stringify({
36
+ schemaVersion: 1,
37
+ snapshottedAt: 1716383000000,
38
+ account: { userId: "u1" },
39
+ events: [{ kind: "transaction", id: "tx-X1", txId: "X1", time: 1716383000, amount: 12.5, direction: "pay", counterparty: "便利店" }],
40
+ });
41
+ const p = writeTmp(SNAP);
42
+ try {
43
+ const a = new dc.DcepAdapter();
44
+ const items = await collect(a.sync({ inputPath: p }));
45
+ expect(items).toHaveLength(1);
46
+ const b = a.normalize(items[0]);
47
+ expect(b.events[0].subtype).toBe("payment");
48
+ expect(b.events[0].content.title).toContain("付款");
49
+ expect(b.events[0].extra.amount).toBe(12.5);
50
+ expect(items[0].originalId).toBe("dcep:transaction:X1");
51
+ } finally {
52
+ fs.unlinkSync(p);
53
+ }
54
+
55
+ let signed = 0;
56
+ const a = new dc.DcepAdapter({
57
+ account: { cookies: COOKIES, userId: "u1" },
58
+ signProvider: async () => { signed += 1; return "sig"; },
59
+ fetchFn: async ({ query }) => (query.page > 1 ? { list: [] } : { list: [{ txId: "X9", time: 1716383000, amount: 9.9, direction: "receive" }] }),
60
+ });
61
+ expect(await a.authenticate()).toMatchObject({ ok: true, mode: "cookie", unverified: true });
62
+ const items = await collect(a.sync({}));
63
+ expect(items).toHaveLength(1);
64
+ expect(items[0].originalId).toBe("dcep:transaction:X9");
65
+ expect(signed).toBeGreaterThan(0);
66
+ });
67
+
68
+ it("high sensitivity + legalGate; default fetch / no input throw", async () => {
69
+ expect(new dc.DcepAdapter().dataDisclosure.sensitivity).toBe("high");
70
+ expect(new dc.DcepAdapter().dataDisclosure.legalGate).toBe(true);
71
+ await expect(collect(new dc.DcepAdapter({ account: { cookies: COOKIES } }).sync({}))).rejects.toThrow(/no fetchFn/);
72
+ await expect(collect(new dc.DcepAdapter().sync({}))).rejects.toThrow(/needs opts.inputPath/);
73
+ });
74
+ });
@@ -0,0 +1,82 @@
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 jr = require("../../lib/adapters/fitness-joyrun");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-jr-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+ const COOKIES = "joyrun_sid=abc";
22
+
23
+ describe("fitness-joyrun", () => {
24
+ it("name + mapRun (meter wins; km*1000 fallback; no id → null)", () => {
25
+ expect(jr.NAME).toBe("fitness-joyrun");
26
+ const r = jr.mapRun({ fid: "R1", starttime: 1716383000, meter: 5230, second: 1800, pace: 344, dohas: 320, stepcount: 6400 });
27
+ expect(r).toMatchObject({ runId: "R1", distanceMeters: 5230, durationSec: 1800, paceSecPerKm: 344, calories: 320, steps: 6400 });
28
+ expect(r.timeMs).toBe(1716383000000);
29
+ // distance given as km (5.23) with no meter field → *1000
30
+ expect(jr.mapRun({ id: "R2", distance: 5.23 }).distanceMeters).toBeCloseTo(5230, 0);
31
+ expect(jr.mapRun({ meter: 100 })).toBe(null);
32
+ });
33
+ it("extractList tolerant", () => {
34
+ expect(jr.extractList({ data: { runs: [{ fid: 1 }] } })).toHaveLength(1);
35
+ expect(jr.extractList({ list: [{ fid: 1 }] })).toHaveLength(1);
36
+ expect(jr.extractList({})).toEqual([]);
37
+ });
38
+
39
+ it("snapshot → OTHER event with km title + run extras", async () => {
40
+ const SNAP = JSON.stringify({
41
+ schemaVersion: 1,
42
+ snapshottedAt: 1716383000000,
43
+ account: { userId: "u1" },
44
+ events: [{ kind: "run", id: "r-R1", runId: "R1", time: 1716383000, distanceMeters: 5230, durationSec: 1800, paceSecPerKm: 344, calories: 320 }],
45
+ });
46
+ const p = writeTmp(SNAP);
47
+ try {
48
+ const a = new jr.JoyrunAdapter();
49
+ const items = await collect(a.sync({ inputPath: p }));
50
+ expect(items).toHaveLength(1);
51
+ const b = a.normalize(items[0]);
52
+ expect(b.events[0].subtype).toBe("other");
53
+ expect(b.events[0].content.title).toBe("跑步 5.23 km");
54
+ expect(b.events[0].extra.distanceMeters).toBe(5230);
55
+ expect(b.events[0].extra.calories).toBe(320);
56
+ expect(items[0].originalId).toBe("joyrun:run:R1");
57
+ } finally {
58
+ fs.unlinkSync(p);
59
+ }
60
+ });
61
+
62
+ it("cookie-api: unverified flag + sign seam + paginate", async () => {
63
+ let signed = 0;
64
+ const a = new jr.JoyrunAdapter({
65
+ account: { cookies: COOKIES, userId: "u1" },
66
+ signProvider: async () => { signed += 1; return "sig"; },
67
+ fetchFn: async ({ query }) => (query.page > 1 ? { list: [] } : { data: { runs: [{ fid: "R9", starttime: 1716383000, meter: 10000, second: 3600 }] } }),
68
+ });
69
+ expect(await a.authenticate()).toMatchObject({ ok: true, mode: "cookie", unverified: true });
70
+ const items = await collect(a.sync({}));
71
+ expect(items).toHaveLength(1);
72
+ expect(items[0].originalId).toBe("joyrun:run:R9");
73
+ expect(signed).toBeGreaterThan(0);
74
+ });
75
+
76
+ it("medium sensitivity (GPS route); default fetch / no input throw", async () => {
77
+ expect(new jr.JoyrunAdapter().dataDisclosure.sensitivity).toBe("medium");
78
+ expect(new jr.JoyrunAdapter().dataDisclosure.legalGate).toBe(false);
79
+ await expect(collect(new jr.JoyrunAdapter({ account: { cookies: COOKIES } }).sync({}))).rejects.toThrow(/no fetchFn/);
80
+ await expect(collect(new jr.JoyrunAdapter().sync({}))).rejects.toThrow(/needs opts.inputPath/);
81
+ });
82
+ });