@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,529 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
extractPdfText,
|
|
7
|
-
passwordsFromHints,
|
|
8
|
-
} = require("../../lib/adapters/email-imap/pdf-extractor");
|
|
9
|
-
|
|
10
|
-
const { extractTransactions } = require("../../lib/adapters/email-imap/transactions");
|
|
11
|
-
|
|
12
|
-
// ─── pdf-extractor (password trial loop) ─────────────────────────────────
|
|
13
|
-
|
|
14
|
-
function makeMockPdfParse(spec = {}) {
|
|
15
|
-
// spec: { needsPassword?: string, text?: string, throwsOnPassword?: Function }
|
|
16
|
-
const calls = [];
|
|
17
|
-
const fn = async (buf, opts = {}) => {
|
|
18
|
-
calls.push({ pwd: opts.password, bufLen: buf.length });
|
|
19
|
-
if (spec.throwsAlways) {
|
|
20
|
-
const err = new Error(spec.throwsAlways);
|
|
21
|
-
throw err;
|
|
22
|
-
}
|
|
23
|
-
if (spec.needsPassword != null && opts.password !== spec.needsPassword) {
|
|
24
|
-
const err = new Error("PasswordException: incorrect password");
|
|
25
|
-
throw err;
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
text: spec.text || "extracted content",
|
|
29
|
-
numpages: spec.numpages || 1,
|
|
30
|
-
info: { IsEncrypted: spec.needsPassword != null },
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
fn.calls = calls;
|
|
34
|
-
return fn;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("extractPdfText — password trial loop", () => {
|
|
38
|
-
it("returns text when no password is required (empty-password first)", async () => {
|
|
39
|
-
const mockParse = makeMockPdfParse({ text: "hello world" });
|
|
40
|
-
const r = await extractPdfText(Buffer.from("FAKE PDF"), {
|
|
41
|
-
passwords: ["abc", "def"],
|
|
42
|
-
pdfParseImpl: mockParse,
|
|
43
|
-
});
|
|
44
|
-
expect(r.decrypted).toBe(true);
|
|
45
|
-
expect(r.text).toBe("hello world");
|
|
46
|
-
expect(r.attempted).toBe(1); // empty-string trial succeeds first
|
|
47
|
-
expect(r.password).toBeUndefined();
|
|
48
|
-
expect(mockParse.calls[0].pwd).toBe("");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("succeeds on the second password in the list", async () => {
|
|
52
|
-
const mockParse = makeMockPdfParse({ needsPassword: "OPENME-mock", text: "decrypted!" });
|
|
53
|
-
const r = await extractPdfText(Buffer.from("FAKE"), {
|
|
54
|
-
passwords: ["wrong1", "OPENME-mock", "wrong2"],
|
|
55
|
-
pdfParseImpl: mockParse,
|
|
56
|
-
});
|
|
57
|
-
expect(r.decrypted).toBe(true);
|
|
58
|
-
expect(r.text).toBe("decrypted!");
|
|
59
|
-
expect(r.password).toBe("OPENME-mock");
|
|
60
|
-
// Attempts: "" → "wrong1" → "OPENME-mock" = 3
|
|
61
|
-
expect(r.attempted).toBe(3);
|
|
62
|
-
expect(r.wasEncrypted).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("returns decrypted:false when no password works", async () => {
|
|
66
|
-
const mockParse = makeMockPdfParse({ needsPassword: "nobody-knows" });
|
|
67
|
-
const r = await extractPdfText(Buffer.from("FAKE"), {
|
|
68
|
-
passwords: ["a", "b", "c"],
|
|
69
|
-
pdfParseImpl: mockParse,
|
|
70
|
-
});
|
|
71
|
-
expect(r.decrypted).toBe(false);
|
|
72
|
-
expect(r.text).toBe("");
|
|
73
|
-
expect(r.attempted).toBe(4); // "" + 3 user passwords
|
|
74
|
-
expect(r.wasEncrypted).toBe(true);
|
|
75
|
-
expect(r.error).toMatch(/password/i);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("deduplicates passwords + always tries empty-string first", async () => {
|
|
79
|
-
const mockParse = makeMockPdfParse({ needsPassword: "match" });
|
|
80
|
-
const r = await extractPdfText(Buffer.from("FAKE"), {
|
|
81
|
-
passwords: ["", "match", "", "match"], // duplicates + empty
|
|
82
|
-
pdfParseImpl: mockParse,
|
|
83
|
-
});
|
|
84
|
-
expect(r.decrypted).toBe(true);
|
|
85
|
-
expect(r.attempted).toBe(2); // "" → "match"
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("non-password error short-circuits (no further trials)", async () => {
|
|
89
|
-
const mockParse = makeMockPdfParse({ throwsAlways: "corrupt PDF stream" });
|
|
90
|
-
const r = await extractPdfText(Buffer.from("FAKE"), {
|
|
91
|
-
passwords: ["a", "b", "c"],
|
|
92
|
-
pdfParseImpl: mockParse,
|
|
93
|
-
});
|
|
94
|
-
expect(r.decrypted).toBe(false);
|
|
95
|
-
expect(r.attempted).toBe(1); // gave up on first non-password error
|
|
96
|
-
expect(r.error).toContain("corrupt PDF stream");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("rejects non-Buffer input gracefully", async () => {
|
|
100
|
-
const r = await extractPdfText("not-a-buffer");
|
|
101
|
-
expect(r.decrypted).toBe(false);
|
|
102
|
-
expect(r.error).toMatch(/buffer/i);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("truncates extracted text at maxTextChars", async () => {
|
|
106
|
-
const longText = "x".repeat(300_000);
|
|
107
|
-
const mockParse = makeMockPdfParse({ text: longText });
|
|
108
|
-
const r = await extractPdfText(Buffer.from("F"), {
|
|
109
|
-
passwords: [],
|
|
110
|
-
maxTextChars: 1000,
|
|
111
|
-
pdfParseImpl: mockParse,
|
|
112
|
-
});
|
|
113
|
-
expect(r.decrypted).toBe(true);
|
|
114
|
-
expect(r.text.length).toBeLessThan(longText.length);
|
|
115
|
-
expect(r.text).toMatch(/truncated/);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("passwordsFromHints — hint ordering", () => {
|
|
120
|
-
it("returns priority order: idCard → phone → cardLast6 → cardLast4 → dob", () => {
|
|
121
|
-
const out = passwordsFromHints({
|
|
122
|
-
cardLast4: "1234",
|
|
123
|
-
idCardLast6: "987654",
|
|
124
|
-
dobYYYYMMDD: "19900101",
|
|
125
|
-
phoneLast6: "555000",
|
|
126
|
-
cardLast6: "555111",
|
|
127
|
-
});
|
|
128
|
-
expect(out).toEqual(["987654", "555000", "555111", "1234", "19900101"]);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("dedups + skips empty / non-string", () => {
|
|
132
|
-
const out = passwordsFromHints({
|
|
133
|
-
idCardLast6: "111111",
|
|
134
|
-
phoneLast6: "111111", // duplicate
|
|
135
|
-
cardLast4: "",
|
|
136
|
-
cardLast6: undefined,
|
|
137
|
-
});
|
|
138
|
-
expect(out).toEqual(["111111"]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("returns [] for empty input", () => {
|
|
142
|
-
expect(passwordsFromHints({})).toEqual([]);
|
|
143
|
-
expect(passwordsFromHints()).toEqual([]);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// ─── transactions extractor ──────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
describe("extractTransactions — Chinese bank statements", () => {
|
|
150
|
-
it("CMB (招商银行): YYYY-MM-DD whitespace-column format", () => {
|
|
151
|
-
const text = [
|
|
152
|
-
"招商银行信用卡 11 月对账单",
|
|
153
|
-
"账单周期: 2026-10-26 至 2026-11-25",
|
|
154
|
-
"",
|
|
155
|
-
"2026-10-30 星巴克 上海中山公园店 -39.00 1,234.56",
|
|
156
|
-
"2026-11-05 京东商城 -899.00 335.56",
|
|
157
|
-
"2026-11-12 退款 淘宝 +50.00 385.56",
|
|
158
|
-
"",
|
|
159
|
-
"第 1 页 共 2 页",
|
|
160
|
-
].join("\n");
|
|
161
|
-
const out = extractTransactions(text);
|
|
162
|
-
expect(out).toHaveLength(3);
|
|
163
|
-
expect(out[0].amount.value).toBe(39);
|
|
164
|
-
expect(out[0].amount.direction).toBe("out");
|
|
165
|
-
expect(out[0].balance.value).toBe(1234.56);
|
|
166
|
-
expect(out[0].description).toMatch(/星巴克/);
|
|
167
|
-
expect(out[2].amount.direction).toBe("in"); // "+" prefix
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("ICBC (工商银行): YYYY/MM/DD with 借/贷 prefix", () => {
|
|
171
|
-
const text = [
|
|
172
|
-
"工商银行信用卡账单",
|
|
173
|
-
"2026/04/15 借 39.00 星巴克 CNY 1234.56",
|
|
174
|
-
"2026/04/16 贷 200.00 退款 ABC CNY 1434.56",
|
|
175
|
-
].join("\n");
|
|
176
|
-
const out = extractTransactions(text);
|
|
177
|
-
expect(out).toHaveLength(2);
|
|
178
|
-
expect(out[0].amount.direction).toBe("out"); // 借 → out
|
|
179
|
-
expect(out[1].amount.direction).toBe("in"); // 贷 → in
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("中文日期: YYYY年MM月DD日 + 支出/收入 keywords", () => {
|
|
183
|
-
const text = [
|
|
184
|
-
"2026年04月15日 星巴克 上海中山公园店 支出 39.00 余额 1234.56",
|
|
185
|
-
"2026年04月16日 工资到账 公司财务 收入 8000.00 余额 9234.56",
|
|
186
|
-
].join("\n");
|
|
187
|
-
const out = extractTransactions(text);
|
|
188
|
-
expect(out).toHaveLength(2);
|
|
189
|
-
expect(out[0].amount.value).toBe(39);
|
|
190
|
-
expect(out[0].amount.direction).toBe("out");
|
|
191
|
-
expect(out[1].amount.direction).toBe("in");
|
|
192
|
-
expect(out[1].balance.value).toBeCloseTo(9234.56);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("skips header / footer / legalese lines", () => {
|
|
196
|
-
const text = [
|
|
197
|
-
"中国银行 月度账单",
|
|
198
|
-
"声明: 本邮件由系统自动发送",
|
|
199
|
-
"温馨提示: 请按时还款",
|
|
200
|
-
"第 1 页",
|
|
201
|
-
"===============",
|
|
202
|
-
"2026-05-01 测试商户 100.00 1000.00",
|
|
203
|
-
].join("\n");
|
|
204
|
-
const out = extractTransactions(text);
|
|
205
|
-
expect(out).toHaveLength(1);
|
|
206
|
-
expect(out[0].description).toMatch(/测试商户/);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("returns [] for non-statement text (no dates)", () => {
|
|
210
|
-
expect(extractTransactions("This is a marketing email, no statement rows.")).toEqual([]);
|
|
211
|
-
expect(extractTransactions("")).toEqual([]);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("caps at maxRows", () => {
|
|
215
|
-
const lines = Array.from(
|
|
216
|
-
{ length: 50 },
|
|
217
|
-
(_, i) => `2026-05-${String((i % 28) + 1).padStart(2, "0")} merchant${i} 10.00 100.00`,
|
|
218
|
-
);
|
|
219
|
-
const out = extractTransactions(lines.join("\n"), { maxRows: 5 });
|
|
220
|
-
expect(out).toHaveLength(5);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("each transaction includes a unique occurredAtMs", () => {
|
|
224
|
-
const text = [
|
|
225
|
-
"2026-05-01 a 10.00 100.00",
|
|
226
|
-
"2026-05-02 b 20.00 80.00",
|
|
227
|
-
"2026-05-03 c 30.00 50.00",
|
|
228
|
-
].join("\n");
|
|
229
|
-
const out = extractTransactions(text);
|
|
230
|
-
expect(out).toHaveLength(3);
|
|
231
|
-
expect(out[0].occurredAtMs).toBeLessThan(out[1].occurredAtMs);
|
|
232
|
-
expect(out[1].occurredAtMs).toBeLessThan(out[2].occurredAtMs);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("description excludes amount/balance/direction-keyword noise", () => {
|
|
236
|
-
const text = "2026-05-15 星巴克 支出 39.00 余额 1234.56";
|
|
237
|
-
const out = extractTransactions(text);
|
|
238
|
-
expect(out[0].description).toBe("星巴克");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("single-amount line treats it as the transaction amount (no balance)", () => {
|
|
242
|
-
const text = "2026-05-15 ATM withdrawal -500.00";
|
|
243
|
-
const out = extractTransactions(text);
|
|
244
|
-
expect(out).toHaveLength(1);
|
|
245
|
-
expect(out[0].amount.value).toBe(500);
|
|
246
|
-
expect(out[0].balance).toBeUndefined();
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// ─── EmailAdapter Phase 5.5 integration ──────────────────────────────────
|
|
251
|
-
|
|
252
|
-
const { EmailAdapter } = require("../../lib/adapters/email-imap/email-adapter");
|
|
253
|
-
|
|
254
|
-
function makeSession(envelopes) {
|
|
255
|
-
return (_opts) => ({
|
|
256
|
-
async connect() {},
|
|
257
|
-
async openMailbox(_name) {
|
|
258
|
-
return { uidValidity: 1, uidNext: 9999, exists: envelopes.length };
|
|
259
|
-
},
|
|
260
|
-
async *fetchFullSince(sinceUid = 0) {
|
|
261
|
-
for (const env of envelopes) {
|
|
262
|
-
if (env.uid > sinceUid) yield { ...env, source: env.source || Buffer.alloc(0) };
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
async close() {},
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const billEnv = (uid = 1) => ({
|
|
270
|
-
uid,
|
|
271
|
-
internalDate: new Date("2026-05-01T10:00:00Z"),
|
|
272
|
-
flags: ["\\Seen"],
|
|
273
|
-
messageId: `<bill-${uid}@x>`,
|
|
274
|
-
subject: "招商银行信用卡 11 月对账单",
|
|
275
|
-
from: [{ name: "招商银行", address: "ebank@cmbchina.com" }],
|
|
276
|
-
to: [{ address: "me@example.com" }],
|
|
277
|
-
cc: [],
|
|
278
|
-
date: new Date("2026-05-01T10:00:00Z"),
|
|
279
|
-
size: 4096,
|
|
280
|
-
source: Buffer.from("RAW", "utf8"),
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const PDF_TEXT = [
|
|
284
|
-
"招商银行信用卡 11 月对账单",
|
|
285
|
-
"尾号 1234 最后还款日 2026-12-05 应还金额 ¥3,000.00",
|
|
286
|
-
"",
|
|
287
|
-
"2026-11-05 星巴克 上海中山公园店 -39.00 2961.00",
|
|
288
|
-
"2026-11-15 京东自营 -899.00 2062.00",
|
|
289
|
-
"2026-11-20 退款 淘宝 +50.00 2112.00",
|
|
290
|
-
].join("\n");
|
|
291
|
-
|
|
292
|
-
describe("EmailAdapter — Phase 5.5 PDF extraction integration", () => {
|
|
293
|
-
it("decrypts bill PDF + extracts transactions into fields.transactions", async () => {
|
|
294
|
-
const factory = makeSession([billEnv()]);
|
|
295
|
-
const a = new EmailAdapter({
|
|
296
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x", folders: ["INBOX"] },
|
|
297
|
-
sessionFactory: factory,
|
|
298
|
-
// Force-create a "decrypted" PDF attachment via a custom parser
|
|
299
|
-
parser: async () => ({
|
|
300
|
-
textBody: "尾号 1234 应还金额 ¥3,000",
|
|
301
|
-
attachments: [{
|
|
302
|
-
filename: "statement.pdf",
|
|
303
|
-
contentType: "application/pdf",
|
|
304
|
-
contentDisposition: "attachment",
|
|
305
|
-
size: 12345,
|
|
306
|
-
sha256: "abc",
|
|
307
|
-
isInline: false,
|
|
308
|
-
isEncrypted: true,
|
|
309
|
-
buffer: Buffer.from("FAKE PDF BYTES"),
|
|
310
|
-
}],
|
|
311
|
-
}),
|
|
312
|
-
pdfExtractor: async (buf, _opts) => ({
|
|
313
|
-
decrypted: true,
|
|
314
|
-
text: PDF_TEXT,
|
|
315
|
-
password: "987654",
|
|
316
|
-
attempted: 2,
|
|
317
|
-
wasEncrypted: true,
|
|
318
|
-
pageCount: 2,
|
|
319
|
-
}),
|
|
320
|
-
pdfPasswords: ["987654"],
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
const raws = [];
|
|
324
|
-
for await (const r of a.sync()) raws.push(r);
|
|
325
|
-
expect(raws).toHaveLength(1);
|
|
326
|
-
const ext = raws[0].payload.extraction;
|
|
327
|
-
expect(ext.template).toBe("bill");
|
|
328
|
-
expect(ext.fields.transactions).toBeDefined();
|
|
329
|
-
expect(ext.fields.transactions.length).toBe(3);
|
|
330
|
-
expect(ext.fields.transactions[0].amount.value).toBe(39);
|
|
331
|
-
expect(ext.fields.transactions[0].amount.direction).toBe("out");
|
|
332
|
-
expect(ext.fields.transactions[2].amount.direction).toBe("in");
|
|
333
|
-
expect(ext.pdfExtraction).toBeDefined();
|
|
334
|
-
expect(ext.pdfExtraction[0].decrypted).toBe(true);
|
|
335
|
-
expect(ext.pdfExtraction[0].transactionsExtracted).toBe(3);
|
|
336
|
-
// Real password value must NEVER be persisted (only masked)
|
|
337
|
-
expect(ext.pdfExtraction[0].passwordUsed).toBe("***");
|
|
338
|
-
expect(JSON.stringify(ext.pdfExtraction)).not.toMatch(/987654/);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("normalize copies transactions into extra.fields.transactions", async () => {
|
|
342
|
-
const factory = makeSession([billEnv()]);
|
|
343
|
-
const a = new EmailAdapter({
|
|
344
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x", folders: ["INBOX"] },
|
|
345
|
-
sessionFactory: factory,
|
|
346
|
-
parser: async () => ({
|
|
347
|
-
textBody: "尾号 1234",
|
|
348
|
-
attachments: [{
|
|
349
|
-
filename: "stmt.pdf",
|
|
350
|
-
contentType: "application/pdf",
|
|
351
|
-
size: 100,
|
|
352
|
-
sha256: "x",
|
|
353
|
-
buffer: Buffer.from("FAKE"),
|
|
354
|
-
}],
|
|
355
|
-
}),
|
|
356
|
-
pdfExtractor: async () => ({
|
|
357
|
-
decrypted: true,
|
|
358
|
-
text: "2026-05-01 星巴克 -39.00 1000.00",
|
|
359
|
-
attempted: 1,
|
|
360
|
-
wasEncrypted: true,
|
|
361
|
-
pageCount: 1,
|
|
362
|
-
}),
|
|
363
|
-
});
|
|
364
|
-
const raws = [];
|
|
365
|
-
for await (const r of a.sync()) raws.push(r);
|
|
366
|
-
const batch = a.normalize(raws[0]);
|
|
367
|
-
expect(batch.events).toHaveLength(1);
|
|
368
|
-
expect(batch.events[0].extra.fields.transactions).toBeDefined();
|
|
369
|
-
expect(batch.events[0].extra.fields.transactions).toHaveLength(1);
|
|
370
|
-
expect(batch.events[0].extra.pdfExtraction).toBeDefined();
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it("decrypt failure: pdfExtraction.error populated; no transactions", async () => {
|
|
374
|
-
const factory = makeSession([billEnv()]);
|
|
375
|
-
const a = new EmailAdapter({
|
|
376
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x", folders: ["INBOX"] },
|
|
377
|
-
sessionFactory: factory,
|
|
378
|
-
parser: async () => ({
|
|
379
|
-
textBody: "stmt",
|
|
380
|
-
attachments: [{
|
|
381
|
-
filename: "x.pdf",
|
|
382
|
-
contentType: "application/pdf",
|
|
383
|
-
size: 100,
|
|
384
|
-
sha256: "h",
|
|
385
|
-
buffer: Buffer.from("F"),
|
|
386
|
-
}],
|
|
387
|
-
}),
|
|
388
|
-
pdfExtractor: async () => ({
|
|
389
|
-
decrypted: false,
|
|
390
|
-
text: "",
|
|
391
|
-
attempted: 5,
|
|
392
|
-
wasEncrypted: true,
|
|
393
|
-
pageCount: 0,
|
|
394
|
-
error: "all passwords failed",
|
|
395
|
-
}),
|
|
396
|
-
});
|
|
397
|
-
const raws = [];
|
|
398
|
-
for await (const r of a.sync()) raws.push(r);
|
|
399
|
-
const ext = raws[0].payload.extraction;
|
|
400
|
-
expect(ext.fields.transactions).toBeUndefined();
|
|
401
|
-
expect(ext.pdfExtraction[0].decrypted).toBe(false);
|
|
402
|
-
expect(ext.pdfExtraction[0].error).toContain("all passwords failed");
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("disablePdfExtraction skips the decryption pass", async () => {
|
|
406
|
-
const factory = makeSession([billEnv()]);
|
|
407
|
-
let extractorCalled = false;
|
|
408
|
-
const a = new EmailAdapter({
|
|
409
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x", folders: ["INBOX"] },
|
|
410
|
-
sessionFactory: factory,
|
|
411
|
-
parser: async () => ({
|
|
412
|
-
textBody: "stmt",
|
|
413
|
-
attachments: [{
|
|
414
|
-
filename: "x.pdf",
|
|
415
|
-
contentType: "application/pdf",
|
|
416
|
-
size: 100,
|
|
417
|
-
sha256: "h",
|
|
418
|
-
buffer: Buffer.from("F"),
|
|
419
|
-
}],
|
|
420
|
-
}),
|
|
421
|
-
pdfExtractor: async () => {
|
|
422
|
-
extractorCalled = true;
|
|
423
|
-
return { decrypted: true, text: "txt", attempted: 1, wasEncrypted: false, pageCount: 1 };
|
|
424
|
-
},
|
|
425
|
-
disablePdfExtraction: true,
|
|
426
|
-
});
|
|
427
|
-
const raws = [];
|
|
428
|
-
for await (const r of a.sync()) raws.push(r);
|
|
429
|
-
expect(extractorCalled).toBe(false);
|
|
430
|
-
expect(raws[0].payload.extraction.pdfExtraction).toBeUndefined();
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
it("non-bill email does NOT trigger PDF extraction even when PDF attached", async () => {
|
|
434
|
-
const factory = makeSession([{
|
|
435
|
-
...billEnv(),
|
|
436
|
-
subject: "Welcome",
|
|
437
|
-
from: [{ address: "noreply@example.com" }],
|
|
438
|
-
}]);
|
|
439
|
-
let extractorCalled = false;
|
|
440
|
-
const a = new EmailAdapter({
|
|
441
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x", folders: ["INBOX"] },
|
|
442
|
-
sessionFactory: factory,
|
|
443
|
-
parser: async () => ({
|
|
444
|
-
textBody: "Welcome to our service!",
|
|
445
|
-
attachments: [{
|
|
446
|
-
filename: "brochure.pdf",
|
|
447
|
-
contentType: "application/pdf",
|
|
448
|
-
size: 100,
|
|
449
|
-
sha256: "b",
|
|
450
|
-
buffer: Buffer.from("F"),
|
|
451
|
-
}],
|
|
452
|
-
}),
|
|
453
|
-
pdfExtractor: async () => {
|
|
454
|
-
extractorCalled = true;
|
|
455
|
-
return { decrypted: true, text: "", attempted: 1, wasEncrypted: false, pageCount: 1 };
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
const raws = [];
|
|
459
|
-
for await (const r of a.sync()) raws.push(r);
|
|
460
|
-
expect(extractorCalled).toBe(false);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("attachment buffers are STRIPPED from the emitted RawEvent payload", async () => {
|
|
464
|
-
const factory = makeSession([billEnv()]);
|
|
465
|
-
const a = new EmailAdapter({
|
|
466
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x", folders: ["INBOX"] },
|
|
467
|
-
sessionFactory: factory,
|
|
468
|
-
parser: async () => ({
|
|
469
|
-
textBody: "stmt",
|
|
470
|
-
attachments: [{
|
|
471
|
-
filename: "x.pdf",
|
|
472
|
-
contentType: "application/pdf",
|
|
473
|
-
size: 100,
|
|
474
|
-
sha256: "h",
|
|
475
|
-
buffer: Buffer.from("SECRET PDF BYTES"),
|
|
476
|
-
}],
|
|
477
|
-
}),
|
|
478
|
-
pdfExtractor: async () => ({ decrypted: true, text: "", attempted: 1, wasEncrypted: false, pageCount: 1 }),
|
|
479
|
-
});
|
|
480
|
-
const raws = [];
|
|
481
|
-
for await (const r of a.sync()) raws.push(r);
|
|
482
|
-
// The buffer must not survive into the serialized payload
|
|
483
|
-
const serialized = JSON.stringify(raws[0]);
|
|
484
|
-
expect(serialized).not.toMatch(/SECRET PDF BYTES/);
|
|
485
|
-
// Attachment metadata still present
|
|
486
|
-
expect(raws[0].payload.parsedBody.attachments[0].filename).toBe("x.pdf");
|
|
487
|
-
expect(raws[0].payload.parsedBody.attachments[0].buffer).toBeUndefined();
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it("capability flag: decrypt:pdf-bills present by default, absent when disabled", () => {
|
|
491
|
-
const a = new EmailAdapter({
|
|
492
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x" },
|
|
493
|
-
sessionFactory: makeSession([]),
|
|
494
|
-
});
|
|
495
|
-
expect(a.capabilities).toContain("decrypt:pdf-bills");
|
|
496
|
-
|
|
497
|
-
const b = new EmailAdapter({
|
|
498
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x" },
|
|
499
|
-
sessionFactory: makeSession([]),
|
|
500
|
-
disablePdfExtraction: true,
|
|
501
|
-
});
|
|
502
|
-
expect(b.capabilities).not.toContain("decrypt:pdf-bills");
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it("pdfPasswordHints + pdfPasswords are merged + deduped", () => {
|
|
506
|
-
const a = new EmailAdapter({
|
|
507
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x" },
|
|
508
|
-
sessionFactory: makeSession([]),
|
|
509
|
-
pdfPasswords: ["custom1", "custom2"],
|
|
510
|
-
pdfPasswordHints: { idCardLast6: "111111", phoneLast6: "custom1" }, // dup
|
|
511
|
-
});
|
|
512
|
-
// Internal field — surface via the password list passed to pdfExtractor
|
|
513
|
-
let receivedPasswords = null;
|
|
514
|
-
a._pdfExtractor = async (_buf, opts) => {
|
|
515
|
-
receivedPasswords = opts.passwords;
|
|
516
|
-
return { decrypted: false, attempted: 0, text: "", wasEncrypted: false, pageCount: 0 };
|
|
517
|
-
};
|
|
518
|
-
// No need to run sync; verify the internal list directly:
|
|
519
|
-
expect(a._pdfPasswords).toEqual(["custom1", "custom2", "111111"]);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it("version reflects 0.6.0", () => {
|
|
523
|
-
const a = new EmailAdapter({
|
|
524
|
-
account: { provider: "qq", email: "u@qq.com", authCode: "x" },
|
|
525
|
-
sessionFactory: makeSession([]),
|
|
526
|
-
});
|
|
527
|
-
expect(a.version).toBe("0.7.0");
|
|
528
|
-
});
|
|
529
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
PROVIDERS,
|
|
7
|
-
resolveProvider,
|
|
8
|
-
} = require("../../lib/adapters/email-imap/providers");
|
|
9
|
-
|
|
10
|
-
describe("EMAIL_PROVIDERS preset table", () => {
|
|
11
|
-
it("exposes qq / 189 / 163 / outlook / gmail", () => {
|
|
12
|
-
expect(PROVIDERS.qq.host).toBe("imap.qq.com");
|
|
13
|
-
expect(PROVIDERS["189"].host).toBe("imap.189.cn");
|
|
14
|
-
expect(PROVIDERS["163"].host).toBe("imap.163.com");
|
|
15
|
-
expect(PROVIDERS.outlook.host).toBe("outlook.office365.com");
|
|
16
|
-
expect(PROVIDERS.gmail.host).toBe("imap.gmail.com");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("every preset uses port 993 + TLS by default", () => {
|
|
20
|
-
for (const p of Object.values(PROVIDERS)) {
|
|
21
|
-
expect(p.port).toBe(993);
|
|
22
|
-
expect(p.secure).toBe(true);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("each preset advertises an authNote pointing at authorization-code, not password", () => {
|
|
27
|
-
for (const p of Object.values(PROVIDERS)) {
|
|
28
|
-
expect(p.authNote.length).toBeGreaterThan(10);
|
|
29
|
-
}
|
|
30
|
-
expect(PROVIDERS.qq.authNote).toMatch(/授权码|auth.*code|app password/i);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("resolveProvider", () => {
|
|
35
|
-
it("returns preset config when no overrides", () => {
|
|
36
|
-
const r = resolveProvider({ provider: "qq" });
|
|
37
|
-
expect(r.host).toBe("imap.qq.com");
|
|
38
|
-
expect(r.port).toBe(993);
|
|
39
|
-
expect(r.secure).toBe(true);
|
|
40
|
-
expect(r.folders).toEqual(["INBOX", "Sent Messages"]);
|
|
41
|
-
expect(r.providerId).toBe("qq");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("respects user overrides", () => {
|
|
45
|
-
const r = resolveProvider({
|
|
46
|
-
provider: "qq",
|
|
47
|
-
host: "imap.example.com",
|
|
48
|
-
port: 143,
|
|
49
|
-
secure: false,
|
|
50
|
-
folders: ["INBOX", "Drafts", "Custom"],
|
|
51
|
-
displayName: "My Mailbox",
|
|
52
|
-
});
|
|
53
|
-
expect(r.host).toBe("imap.example.com");
|
|
54
|
-
expect(r.port).toBe(143);
|
|
55
|
-
expect(r.secure).toBe(false);
|
|
56
|
-
expect(r.folders).toEqual(["INBOX", "Drafts", "Custom"]);
|
|
57
|
-
expect(r.displayName).toBe("My Mailbox");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("rejects unknown provider", () => {
|
|
61
|
-
expect(() => resolveProvider({ provider: "myproto" })).toThrow(/unknown provider/i);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("custom provider requires host", () => {
|
|
65
|
-
expect(() => resolveProvider({ provider: "custom" })).toThrow(/host/);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("custom provider applies sensible defaults", () => {
|
|
69
|
-
const r = resolveProvider({
|
|
70
|
-
provider: "custom",
|
|
71
|
-
host: "mail.acme.test",
|
|
72
|
-
});
|
|
73
|
-
expect(r.host).toBe("mail.acme.test");
|
|
74
|
-
expect(r.port).toBe(993);
|
|
75
|
-
expect(r.secure).toBe(true);
|
|
76
|
-
expect(r.folders).toEqual(["INBOX"]);
|
|
77
|
-
expect(r.providerId).toBe("custom");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("rejects null / wrong-type account", () => {
|
|
81
|
-
expect(() => resolveProvider()).toThrow();
|
|
82
|
-
expect(() => resolveProvider(null)).toThrow();
|
|
83
|
-
});
|
|
84
|
-
});
|