hridoy-ultimate 9.0.0 → 9.0.2

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 (46) hide show
  1. package/CHANGELOG.md +346 -69
  2. package/{docs/UPSTREAM_API_DOCS.md → DOCS.md} +3 -3
  3. package/README.md +536 -161
  4. package/func/checkUpdate.js +1 -0
  5. package/index.d.ts +13 -74
  6. package/index.js +0 -6
  7. package/module/config.js +2 -5
  8. package/module/login.js +1 -2
  9. package/module/loginHelper.js +9 -41
  10. package/module/options.js +6 -6
  11. package/package.json +44 -39
  12. package/src/api/action/logout.js +0 -19
  13. package/src/api/messaging/sendMessage.js +1 -1
  14. package/src/api/messaging/sessionGuard.js +8 -0
  15. package/src/api/messaging/setMessageReaction.js +2 -7
  16. package/src/api/socket/core/connectMqtt.js +72 -99
  17. package/src/api/socket/core/emitAuth.js +17 -9
  18. package/src/api/socket/e2ee/index.js +15 -33
  19. package/src/api/socket/e2ee/native/NOTICE.md +2 -2
  20. package/src/api/socket/e2ee/native/lib/index.mjs +1 -2
  21. package/src/api/socket/e2ee/native/nativeMediaBridge.js +4 -4
  22. package/src/api/socket/listenE2EE.js +1 -3
  23. package/src/api/socket/listenMqtt.js +60 -67
  24. package/src/utils/antiSuspension.js +3 -3
  25. package/src/utils/fingerprint.js +35 -16
  26. package/src/utils/headers.js +1 -1
  27. package/src/utils/versionCheck.js +47 -0
  28. package/LICENSE +0 -23
  29. package/MERGE_REPORT.md +0 -54
  30. package/THIRD_PARTY_NOTICES.md +0 -36
  31. package/docs/TEST_RESULTS.md +0 -68
  32. package/scripts/run-tests.js +0 -19
  33. package/scripts/syntax-check.js +0 -22
  34. package/src/api/socket/core/lifecycle.js +0 -117
  35. package/src/api/socket/e2ee/proto/proto-writer.ts +0 -76
  36. package/src/utils/typingIndicator.js +0 -202
  37. package/test/helpers/stub-modules.js +0 -122
  38. package/test/integration/logout.test.js +0 -69
  39. package/test/integration/mqtt-lifecycle.test.js +0 -195
  40. package/test/integration/package.test.js +0 -116
  41. package/test/integration/typing-wiring.test.js +0 -71
  42. package/test/unit/fingerprint.test.js +0 -19
  43. package/test/unit/humanizer.test.js +0 -32
  44. package/test/unit/lifecycle.test.js +0 -92
  45. package/test/unit/parsers.test.js +0 -43
  46. package/test/unit/typing.test.js +0 -136
@@ -1,195 +0,0 @@
1
- "use strict";
2
- /**
3
- * MOCKED integration tests: real listenMqtt/connectMqtt/emitAuth code driven
4
- * against a fake mqtt client (test/helpers/stub-modules.js). They prove lifecycle
5
- * logic, ordering and cleanup. They do NOT prove Facebook accepts the traffic.
6
- * Set HRIDOY_TEST_ROOT=<other package dir> to run the same file against another tree
7
- * (used to demonstrate the regressions fail on the original 1.1.9 code).
8
- */
9
- const test = require("node:test");
10
- const assert = require("node:assert/strict");
11
- const path = require("path");
12
- const os = require("os");
13
- const fs = require("fs");
14
- const root = process.env.HRIDOY_TEST_ROOT || path.join(__dirname, "..", "..");
15
- const stubs = require("../helpers/stub-modules");
16
- stubs.install();
17
- process.chdir(fs.mkdtempSync(path.join(os.tmpdir(), "hridoy-test-"))); // library creates ./Fca_Database on import
18
-
19
- const tick = () => new Promise((r) => setImmediate(r));
20
- const flush = async (n = 6) => { for (let i = 0; i < n; i++) await tick(); };
21
-
22
- // stopListening uses setTimeout internally; when timers are mocked we must tick them.
23
- async function stop(s, t) {
24
- let done = false;
25
- const p = new Promise((r) => s.emitter.stopListening(() => { done = true; r(); }));
26
- for (let i = 0; i < 50 && !done; i++) {
27
- if (t && t.mock && t.mock.timers && t.mock.timers.tick) { try { t.mock.timers.tick(10); } catch (_) { /* timers not mocked */ } }
28
- await tick();
29
- }
30
- await p;
31
- }
32
-
33
- function setup(opts = {}) {
34
- if (opts.t) opts.t.mock.timers.enable({ apis: ["setTimeout", "setInterval"] }); // BEFORE listen so its timers are mocked
35
- stubs.purge(root);
36
- stubs.FakeMqttClient.instances.length = 0;
37
- const calls = { get: 0, post: 0 };
38
- const defaultFuncs = {
39
- get: () => { calls.get++; return Promise.resolve({ data: "" }); },
40
- post: () => { calls.post++; return Promise.reject(new Error("network down (test)")); }
41
- };
42
- const api = { getCookies: () => "c_user=1; xs=abc", refreshFb_dtsg: () => Promise.resolve() };
43
- const ctx = {
44
- userID: "100000000000001", clientId: "abcd1234", loggedIn: true, firstListen: true, lastSeqId: 42,
45
- globalOptions: Object.assign({ autoReconnect: true, emitReady: true, userAgent: "UA" }, opts.globalOptions || {}),
46
- tasks: new Map(), _mqttOpt: opts.mqtt || {}
47
- };
48
- const factory = require(path.join(root, "src/api/socket/listenMqtt.js"));
49
- const listen = factory(defaultFuncs, api, ctx, opts.mqtt);
50
- const events = [];
51
- const emitter = listen((err, msg) => events.push({ err, msg }), opts.listenOptions);
52
- const clients = stubs.FakeMqttClient.instances;
53
- return { ctx, api, calls, events, emitter, clients, listen, defaultFuncs };
54
- }
55
-
56
- test("[mocked] sync queue is published only after every SUBACK", async (t) => {
57
- const s = setup();
58
- assert.equal(s.clients.length, 1);
59
- const c = s.clients[0];
60
- c.simulateConnect();
61
- assert.equal(c.published.filter((p) => p.topic.startsWith("/messenger_sync")).length, 0, "must not publish before SUBACK");
62
- await flush();
63
- const log = c.log;
64
- const firstPublish = log.findIndex((e) => e.startsWith("publish:/messenger_sync"));
65
- const lastSuback = log.map((e, i) => (e.startsWith("suback:") ? i : -1)).filter((i) => i >= 0).pop();
66
- assert.ok(firstPublish > -1, "sync publish happened");
67
- assert.ok(lastSuback < firstPublish, `SUBACKs (${lastSuback}) must precede first publish (${firstPublish}): ${log.join(",")}`);
68
- await stop(s, t);
69
- });
70
-
71
- test("[mocked] reconnect after socket close uses backoff, then recovers", async (t) => {
72
- const s = setup({ mqtt: { reconnectDelayMs: 1000, reconnectJitter: 0, maxReconnectAttempts: 5 } });
73
- s.clients[0].simulateConnect(); await flush();
74
- t.mock.timers.enable({ apis: ["setTimeout"] });
75
- s.clients[0].simulateClose();
76
- assert.ok(s.ctx._reconnectTimer, "reconnect timer armed");
77
- t.mock.timers.tick(999); assert.equal(s.clients.length, 1, "not before the delay");
78
- t.mock.timers.tick(1); assert.equal(s.clients.length, 2, "reconnected after 1000ms");
79
- // second failure waits twice as long
80
- s.clients[1].simulateConnect(); await flush();
81
- s.clients[1].simulateClose();
82
- t.mock.timers.tick(1999); assert.equal(s.clients.length, 2);
83
- t.mock.timers.tick(1); assert.equal(s.clients.length, 3, "second attempt after 2000ms (backoff doubled)");
84
- t.mock.timers.reset();
85
- await stop(s, t);
86
- });
87
-
88
- test("[mocked] stopListening during a cycle must not be revived by a pending retry (race regression)", async (t) => {
89
- // Original 1.1.9: after stopListening() completed, endQuietly reset _ending=false and the untracked
90
- // reconnect/getSeqID retry timers kept firing (observed: 30 getSeqID calls + a session refresh in 60s).
91
- const s = setup({ t, mqtt: { cycleMs: 1000, reconnectDelayMs: 2000, reconnectJitter: 0 } });
92
- s.clients[0].simulateConnect(); await flush();
93
- t.mock.timers.tick(1000); // auto-cycle fires -> forceCycle
94
- for (let i = 0; i < 8; i++) { await tick(); t.mock.timers.tick(5); }
95
- assert.equal(s.clients[0].ended, true, "precondition: the cycle really started");
96
- await stop(s, t); // user stops inside the reconnect window
97
- const before = { get: s.calls.get, post: s.calls.post, n: s.clients.length };
98
- for (let i = 0; i < 30; i++) { t.mock.timers.tick(2000); await tick(); await tick(); }
99
- assert.equal(s.calls.get, before.get, "no session refresh after stop");
100
- assert.equal(s.calls.post, before.post, "no getSeqID retries after stop");
101
- assert.equal(s.clients.length, before.n, "no new MQTT client after stop");
102
- });
103
-
104
- test("[mocked] stopListening clears timers, ends the client and is safe to call twice", async (t) => {
105
- const s = setup();
106
- s.clients[0].simulateConnect(); await flush();
107
- await stop(s, t);
108
- assert.equal(s.clients[0].ended, true);
109
- assert.equal(s.ctx.mqttClient, undefined);
110
- for (const k of ["_heartbeatInterval", "_sessionKeeperInterval", "_reconnectTimer", "_rTimeout", "_autoCycleTimer"]) {
111
- assert.ok(!s.ctx[k], `${k} cleared`);
112
- }
113
- await stop(s, t); // second call: must complete, not hang/throw
114
- });
115
-
116
- test("[mocked] late socket 'error' after stopListening does not throw", async (t) => {
117
- const s = setup();
118
- s.clients[0].simulateConnect(); await flush();
119
- await stop(s, t);
120
- assert.doesNotThrow(() => s.clients[0].emit("error", new Error("late ECONNRESET")));
121
- });
122
-
123
- test("[mocked] calling listen twice replaces the old client (no duplicate connection)", async (t) => {
124
- const s = setup();
125
- s.clients[0].simulateConnect(); await flush();
126
- const second = s.listen(() => {});
127
- assert.equal(s.clients.length, 2);
128
- assert.equal(s.clients[0].ended, true, "old client ended");
129
- assert.equal(s.ctx.mqttClient, s.clients[1]);
130
- // events from the stale client are inert
131
- const timersBefore = s.ctx._reconnectTimer;
132
- s.clients[0].simulateClose();
133
- assert.equal(s.ctx._reconnectTimer, timersBefore, "stale close must not schedule a reconnect");
134
- await stop({ emitter: second }, t);
135
- });
136
-
137
- test("[mocked] gives up after maxReconnectAttempts and reports stop_listen (no infinite loop)", async (t) => {
138
- const s = setup({ mqtt: { reconnectDelayMs: 10, reconnectJitter: 0, maxReconnectAttempts: 2 } });
139
- t.mock.timers.enable({ apis: ["setTimeout"] });
140
- s.clients[0].simulateClose(); t.mock.timers.tick(10); // attempt 1
141
- assert.equal(s.clients.length, 2);
142
- s.clients[1].simulateClose(); t.mock.timers.tick(20); // attempt 2
143
- assert.equal(s.clients.length, 3);
144
- s.clients[2].simulateClose(); // budget exhausted
145
- t.mock.timers.tick(100000);
146
- assert.equal(s.clients.length, 3, "no further reconnects");
147
- const stop = s.events.find((e) => e.err && e.err.type === "stop_listen");
148
- assert.ok(stop, "stop_listen surfaced to the caller: " + JSON.stringify(s.events.map((e) => e.err && e.err.type)));
149
- t.mock.timers.reset();
150
- });
151
-
152
- test("[mocked] a healthy first sync resets the backoff counter", async (t) => {
153
- const s = setup({ globalOptions: { emitReady: true } });
154
- const c = s.clients[0];
155
- s.ctx._reconnectAttempt = 4;
156
- c.simulateConnect(); await flush();
157
- assert.equal(typeof s.ctx.tmsWait, "function");
158
- c.emit("message", "/t_ms", Buffer.from(JSON.stringify({ firstDeltaSeqId: 1, syncToken: "tok", deltas: [] })));
159
- assert.equal(s.ctx._reconnectAttempt, 0);
160
- assert.ok(s.events.some((e) => e.err && e.err.type === "ready"), "ready event emitted: " + JSON.stringify(s.events));
161
- await stop(s, t);
162
- });
163
-
164
- test("[mocked] humanizer middleware is NOT installed by default, and is when opted in", async (t) => {
165
- let s = setup();
166
- assert.equal(s.api.listMiddleware().length, 0, "default: no hidden middleware");
167
- await stop(s, t);
168
- s = setup({ globalOptions: { humanize: true } });
169
- assert.ok(JSON.stringify(s.api.listMiddleware()).includes("humanizer"), "opt-in installs it");
170
- await stop(s, t);
171
- });
172
-
173
- test("[mocked] emitAuth stops everything and reports account_inactive", async (t) => {
174
- const s = setup();
175
- s.clients[0].simulateConnect(); await flush();
176
- const createEmitAuth = require(path.join(root, "src/api/socket/core/emitAuth"));
177
- const seen = [];
178
- createEmitAuth({ logger: () => {} })(s.ctx, s.api, (e) => seen.push(e), "not_logged_in", "test");
179
- assert.equal(seen[0].type, "account_inactive");
180
- assert.equal(s.ctx.loggedIn, false);
181
- assert.equal(s.ctx.mqttClient, undefined);
182
- assert.ok(!s.ctx._heartbeatInterval && !s.ctx._sessionKeeperInterval && !s.ctx._reconnectTimer);
183
- assert.doesNotThrow(() => s.clients[0].emit("error", new Error("late")));
184
- });
185
-
186
- test("[mocked] listen(callback, options) applies reconnect options per call", async (t) => {
187
- const s = setup({ t, listenOptions: { reconnectDelayMs: 50, reconnectJitter: 0, maxReconnectAttempts: 1 } });
188
- assert.equal(s.ctx._mqttOpt.maxReconnectAttempts, 1);
189
- assert.equal(s.ctx._mqttOpt.reconnectDelayMs, 50);
190
- s.clients[0].simulateClose(); t.mock.timers.tick(50);
191
- assert.equal(s.clients.length, 2);
192
- s.clients[1].simulateClose(); t.mock.timers.tick(100000);
193
- assert.equal(s.clients.length, 2, "budget of 1 attempt honoured");
194
- assert.ok(s.events.some((e) => e.err && e.err.type === "stop_listen"));
195
- });
@@ -1,116 +0,0 @@
1
- "use strict";
2
- /** Package-surface tests. Third-party modules are stubbed (see stub-modules.js) so these are MOCKED loads. */
3
- const test = require("node:test");
4
- const assert = require("node:assert/strict");
5
- const path = require("path");
6
- const os = require("os");
7
- const fs = require("fs");
8
- const { builtinModules } = require("module");
9
- const { spawnSync } = require("child_process");
10
- const root = path.join(__dirname, "..", "..");
11
- const pkg = require(path.join(root, "package.json"));
12
-
13
- function walk(dir, out = []) {
14
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
15
- if (["node_modules", "Fca_Database", "test", ".git"].includes(e.name)) continue;
16
- const p = path.join(dir, e.name);
17
- e.isDirectory() ? walk(p, out) : out.push(p);
18
- }
19
- return out;
20
- }
21
-
22
- test("package metadata: name, version, owner, entry points exist", () => {
23
- assert.equal(pkg.name, "hridoy-ultimate");
24
- assert.equal(pkg.version, "9.0.0");
25
- assert.equal(pkg.author.name, "HR ID OY");
26
- assert.ok(pkg.maintainers.some((m) => m.name === "HR ID OY"));
27
- assert.ok(fs.existsSync(path.join(root, pkg.main)));
28
- assert.ok(fs.existsSync(path.join(root, pkg.types)));
29
- assert.ok(!Object.values(pkg.dependencies).some((v) => /^(git|github)/.test(v)), "no git dependencies in hard deps");
30
- });
31
-
32
- test("regression: every third-party module the source requires is declared", () => {
33
- // 1.1.9 required request/tough-cookie/form-data/undici without declaring them.
34
- const declared = new Set([...Object.keys(pkg.dependencies), ...Object.keys(pkg.optionalDependencies || {})]);
35
- const builtins = new Set(builtinModules.flatMap((m) => [m, "node:" + m]));
36
- const re = /require\(\s*["']([^"'./][^"']*)["']\s*\)|^\s*(?:import|export)[^;\n]*?\sfrom\s+["']([^"'./][^"']*)["']|\bimport\(\s*["']([^"'./][^"']*)["']\s*\)/gm;
37
- const missing = new Map();
38
- for (const f of walk(root).filter((p) => /\.(js|cjs|mjs)$/.test(p))) {
39
- if (f.includes(`${path.sep}vendor${path.sep}`)) continue; // bundled third-party build, checked separately below
40
- const src = fs.readFileSync(f, "utf8");
41
- for (const m of src.matchAll(re)) {
42
- let name = m[1] || m[2] || m[3];
43
- if (builtins.has(name)) continue;
44
- name = name.startsWith("@") ? name.split("/").slice(0, 2).join("/") : name.split("/")[0];
45
- if (builtins.has(name) || declared.has(name)) continue;
46
- missing.set(name, path.relative(root, f));
47
- }
48
- }
49
- assert.deepEqual([...missing], [], "undeclared modules: " + JSON.stringify([...missing]));
50
- });
51
-
52
- test("vendored E2EE bundle: its external requires are declared too", () => {
53
- const f = path.join(root, "src/api/socket/e2ee/vendor/fme/dist/index.cjs");
54
- const declared = new Set([...Object.keys(pkg.dependencies), ...Object.keys(pkg.optionalDependencies || {})]);
55
- const names = new Set([...fs.readFileSync(f, "utf8").matchAll(/require\("([^".][^"]*)"\)/g)].map((m) => m[1]).filter((n) => !n.startsWith("node:") && !builtinModules.includes(n)));
56
- for (const n of names) assert.ok(declared.has(n), `${n} used by vendor bundle but undeclared`);
57
- });
58
-
59
- test("only the pinned messagix binaries ship, native stays opt-in, no default third-party credential host", () => {
60
- const crypto = require("crypto");
61
- const files = walk(root);
62
- const bin = files.filter((p) => /\.(so|dll|dylib|node|exe)$/.test(p)).map((p) => path.relative(root, p).split(path.sep).join("/")).sort();
63
- assert.deepEqual(bin, [
64
- "src/api/socket/e2ee/native/build/messagix.dll",
65
- "src/api/socket/e2ee/native/build/messagix.so"
66
- ]);
67
- const sha = (f) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, f))).digest("hex");
68
- assert.equal(sha(bin[1]), "6249f50cc23de0b11a710266a2323c306d44cf46530880e58e0cd1cf4b5bf8d9");
69
- assert.equal(sha(bin[0]), "f8597ba86c90c39cec19b660e75eba59f2022678eb20866b740cfa321036885e");
70
- const e2ee = fs.readFileSync(path.join(root, "src/api/socket/e2ee/index.js"), "utf8");
71
- assert.match(e2ee, /process\.env\.FCA_E2EE_NATIVE === "1" && _nativeLibAvailable\(\)/, "native engine must stay opt-in");
72
- const hits = files.filter((p) => /\.(js|cjs|mjs|json)$/.test(p) && fs.readFileSync(p, "utf8").includes("minhdong.site"));
73
- assert.deepEqual(hits.map((p) => path.relative(root, p)), []);
74
- const cfg = fs.readFileSync(path.join(root, "module/config.js"), "utf8");
75
- assert.match(cfg, /apiServer:\s*""/);
76
- assert.match(cfg, /autoUpdate:\s*false/);
77
- assert.match(cfg, /autoLogin:\s*false/);
78
- });
79
-
80
- test("every JS file in the package parses", () => {
81
- const r = spawnSync(process.execPath, [path.join(root, "scripts/syntax-check.js")], { encoding: "utf8" });
82
- assert.equal(r.status, 0, r.stdout + r.stderr);
83
- });
84
-
85
- test("[mocked] package loads with consistent exports", () => {
86
- const stubs = require("../helpers/stub-modules");
87
- stubs.install();
88
- process.chdir(fs.mkdtempSync(path.join(os.tmpdir(), "hridoy-pkg-")));
89
- const m = require(root);
90
- assert.equal(typeof m.login, "function");
91
- assert.equal(m.default, m.login);
92
- const expected = ["login", "default", "createFcaClient", "MessengerBot", "createMessengerBot", "MessengerContext",
93
- "attachThreadInfoRealtimeSync", "applyThreadInfoRealtimeEvent", "CommandHumanizer", "createCommandHumanizer",
94
- "createHumanizerMiddleware", "humanizer", "TypingIndicator", "createTypingIndicator", "typingIndicator"];
95
- assert.deepEqual(Object.keys(m).sort(), expected.sort(), "export surface changed: update README + this list");
96
- for (const k of expected) assert.notEqual(m[k], undefined, `${k} is undefined`);
97
- });
98
-
99
- test("[mocked] login with empty options reports an error via callback and never throws", async () => {
100
- const stubs = require("../helpers/stub-modules");
101
- stubs.install();
102
- process.chdir(fs.mkdtempSync(path.join(os.tmpdir(), "hridoy-login-")));
103
- const m = require(root);
104
- const rejections = [];
105
- const onRej = (e) => rejections.push(e);
106
- process.on("unhandledRejection", onRej);
107
- const result = await new Promise((resolve) => {
108
- const t = setTimeout(() => resolve({ timeout: true }), 5000);
109
- m.login({}, {}, (err, api) => { clearTimeout(t); resolve({ err, api }); });
110
- });
111
- process.removeListener("unhandledRejection", onRej);
112
- assert.ok(!result.timeout, "callback was invoked");
113
- assert.ok(result.err, "error delivered");
114
- assert.equal(result.api, undefined);
115
- assert.deepEqual(rejections, []);
116
- });
@@ -1,71 +0,0 @@
1
- "use strict";
2
- /**
3
- * MOCKED integration: the typing indicator is wired into the real listenMqtt factory through the
4
- * shared ctx._middleware (also used by listenE2EE) and is torn down by stopListening().
5
- */
6
- const test = require("node:test");
7
- const assert = require("node:assert/strict");
8
- const path = require("path");
9
- const os = require("os");
10
- const fs = require("fs");
11
- const root = process.env.HRIDOY_TEST_ROOT || path.join(__dirname, "..", "..");
12
- const stubs = require("../helpers/stub-modules");
13
- stubs.install();
14
- process.chdir(fs.mkdtempSync(path.join(os.tmpdir(), "hridoy-typing-")));
15
-
16
- function build({ withTyping = true, globalOptions = {} } = {}) {
17
- stubs.purge(root);
18
- stubs.FakeMqttClient.instances.length = 0;
19
- const { TypingIndicator } = require(path.join(root, "src/utils/typingIndicator"));
20
- const sent = [];
21
- const api = {
22
- getCookies: () => "c_user=1; xs=abc",
23
- refreshFb_dtsg: () => Promise.resolve(),
24
- sendTypingIndicator: (tid, state) => { sent.push([tid, state]); return Promise.resolve(); }
25
- };
26
- const ctx = {
27
- userID: "100000000000001", clientId: "abcd1234", loggedIn: true, firstListen: true, lastSeqId: 42,
28
- globalOptions: Object.assign({ autoReconnect: true, emitReady: true, userAgent: "UA" }, globalOptions),
29
- tasks: new Map(), _mqttOpt: {}
30
- };
31
- if (withTyping) ctx._typing = new TypingIndicator(api, ctx, undefined);
32
- const defaultFuncs = { get: () => Promise.resolve({ data: "" }), post: () => Promise.reject(new Error("network down (test)")) };
33
- const factory = require(path.join(root, "src/api/socket/listenMqtt.js"));
34
- const listen = factory(defaultFuncs, api, ctx);
35
- const events = [];
36
- const emitter = listen((err, msg) => events.push({ err, msg }));
37
- return { ctx, api, sent, events, emitter };
38
- }
39
-
40
- const stopAndWait = (s) => new Promise((r) => s.emitter.stopListening(r));
41
- const msg = (over) => Object.assign({ type: "message", threadID: "t9", senderID: "42", body: "/ping", isGroup: false }, over);
42
-
43
- test("[mocked] typing middleware is installed by default and shows typing for a command without delaying delivery", async () => {
44
- const s = build();
45
- assert.ok(s.ctx._middleware.list().includes("typing"));
46
- let delivered = null;
47
- s.ctx._middleware.process(msg(), (err, ev) => { delivered = ev; });
48
- assert.ok(delivered && delivered.body === "/ping", "event delivered synchronously");
49
- assert.deepEqual(s.sent, [["t9", true]]);
50
- await stopAndWait(s);
51
- });
52
-
53
- test("[mocked] plain chat does not trigger typing, and stopListening clears pending typing timers", async () => {
54
- const s = build();
55
- s.ctx._middleware.process(msg({ body: "hello everyone" }), () => {});
56
- assert.deepEqual(s.sent, []);
57
- s.ctx._middleware.process(msg(), () => {});
58
- assert.equal(s.ctx._typing.isTyping("t9"), true);
59
- await stopAndWait(s);
60
- assert.equal(s.ctx._typing.isTyping("t9"), false, "stopListening tears typing down");
61
- });
62
-
63
- test("[mocked] typing middleware is NOT installed when the humanizer owns typing, or when no controller exists", async () => {
64
- const hz = build({ globalOptions: { humanize: true } });
65
- assert.ok(!hz.ctx._middleware.list().includes("typing"));
66
- assert.ok(hz.ctx._middleware.list().includes("humanizer"));
67
- await stopAndWait(hz);
68
- const none = build({ withTyping: false });
69
- assert.ok(!none.ctx._middleware.list().includes("typing"));
70
- await stopAndWait(none);
71
- });
@@ -1,19 +0,0 @@
1
- "use strict";
2
- const test = require("node:test");
3
- const assert = require("node:assert/strict");
4
- const path = require("path");
5
- const root = process.env.HRIDOY_TEST_ROOT || path.join(__dirname, "..", "..");
6
- const fp = require(path.join(root, "src/utils/fingerprint"));
7
-
8
- test("fingerprint: UA is the reduced Chrome/<major>.0.0.0 form and the full build only appears in client hints", () => {
9
- const ua = fp.getDefaultUserAgent();
10
- const m = ua.match(/Chrome\/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
11
- assert.ok(m, ua);
12
- assert.deepEqual([m[2], m[3], m[4]], ["0", "0", "0"], "UA must not leak the full build");
13
- const { version, major } = fp.getFingerprint();
14
- assert.equal(String(major), m[1]);
15
- const h = fp.clientHintsFromUA(ua);
16
- assert.ok(h["sec-ch-ua-full-version-list"].includes(`"Google Chrome";v="${version}"`));
17
- assert.ok(h["sec-ch-ua"].includes(`"Google Chrome";v="${major}"`));
18
- assert.ok(major >= 149, "pool must not fall back to years-old Chrome builds");
19
- });
@@ -1,32 +0,0 @@
1
- "use strict";
2
- const test = require("node:test");
3
- const assert = require("node:assert/strict");
4
- const path = require("path");
5
- const root = process.env.HRIDOY_TEST_ROOT || path.join(__dirname, "..", "..");
6
- const { CommandHumanizer } = require(path.join(root, "src/utils/humanizer"));
7
-
8
- test("humanizer: first command accepted with a bounded delay, burst inside cooldown dropped", (t) => {
9
- t.mock.timers.enable({ apis: ["Date"], now: 1_000_000 });
10
- const h = new CommandHumanizer({ reactDelayMs: [2000, 3000], cooldownMs: 5500 });
11
- const ev = { body: "/help", threadID: "t1" };
12
- const first = h.shouldProcess(ev);
13
- assert.equal(first.ok, true);
14
- assert.ok(first.delayMs >= 2000 && first.delayMs <= 3000, `delay ${first.delayMs}`);
15
- t.mock.timers.tick(100);
16
- assert.equal(h.shouldProcess(ev).ok, false, "burst command dropped");
17
- t.mock.timers.tick(6000);
18
- assert.equal(h.shouldProcess(ev).ok, true, "accepted again after cooldown/reset");
19
- });
20
-
21
- test("humanizer: non-command messages pass untouched; per-thread isolation", (t) => {
22
- t.mock.timers.enable({ apis: ["Date"], now: 5_000_000 });
23
- const h = new CommandHumanizer({});
24
- assert.equal(h.isCommand({ body: "hello", threadID: "a" }), false);
25
- assert.equal(h.shouldProcess({ body: "/x", threadID: "a" }).ok, true);
26
- assert.equal(h.shouldProcess({ body: "/x", threadID: "b" }).ok, true, "other thread unaffected");
27
- });
28
-
29
- test("humanizer: enabled:false never delays or drops", () => {
30
- const h = new CommandHumanizer({ enabled: false });
31
- for (let i = 0; i < 50; i++) assert.deepEqual(h.shouldProcess({ body: "/x", threadID: "a" }), { ok: true, delayMs: 0 });
32
- });
@@ -1,92 +0,0 @@
1
- "use strict";
2
- const test = require("node:test");
3
- const assert = require("node:assert/strict");
4
- const EventEmitter = require("events");
5
- const path = require("path");
6
- const root = process.env.HRIDOY_TEST_ROOT || path.join(__dirname, "..", "..");
7
- const L = require(path.join(root, "src/api/socket/core/lifecycle"));
8
-
9
- test("computeBackoff grows exponentially, is bounded and never negative", () => {
10
- const rand = () => 0.5; // zero jitter offset
11
- const seq = [0, 1, 2, 3, 4, 5, 6, 7, 8, 20, 1000].map(a => L.computeBackoff(a, { baseMs: 1000, maxMs: 60000, jitter: 0.3, random: rand }));
12
- assert.deepEqual(seq.slice(0, 7), [1000, 2000, 4000, 8000, 16000, 32000, 60000]);
13
- for (const v of seq) { assert.ok(v >= 0 && v <= 60000, `out of bounds: ${v}`); }
14
- assert.equal(seq[seq.length - 1], 60000);
15
- });
16
-
17
- test("computeBackoff jitter stays within +/- jitter and clamps to max", () => {
18
- const lo = L.computeBackoff(2, { baseMs: 1000, maxMs: 1e9, jitter: 0.3, random: () => 0 });
19
- const hi = L.computeBackoff(2, { baseMs: 1000, maxMs: 1e9, jitter: 0.3, random: () => 1 });
20
- assert.equal(lo, 2800); assert.equal(hi, 5200);
21
- assert.equal(L.computeBackoff(10, { baseMs: 1000, maxMs: 5000, jitter: 1, random: () => 1 }), 5000);
22
- });
23
-
24
- test("computeBackoff tolerates garbage input", () => {
25
- for (const bad of [undefined, null, NaN, -5, "x"]) {
26
- const v = L.computeBackoff(bad, { baseMs: bad, maxMs: bad, jitter: bad });
27
- assert.ok(Number.isFinite(v) && v >= 0);
28
- }
29
- });
30
-
31
- test("scheduleReconnect: single timer, debounce, stop, exhaustion", async (t) => {
32
- t.mock.timers.enable({ apis: ["setTimeout"] });
33
- const ctx = { _mqttOpt: { reconnectDelayMs: 100, maxReconnectAttempts: 2, reconnectJitter: 0 } };
34
- let fired = 0;
35
- const fn = () => { fired++; };
36
- assert.equal(L.scheduleReconnect(ctx, null, fn, "a"), "scheduled");
37
- assert.equal(L.scheduleReconnect(ctx, null, fn, "b"), "debounced");
38
- t.mock.timers.tick(100);
39
- assert.equal(fired, 1);
40
- assert.equal(ctx._reconnectTimer, null);
41
- assert.equal(L.scheduleReconnect(ctx, null, fn, "c"), "scheduled"); // attempt 2 -> delay 200
42
- t.mock.timers.tick(200);
43
- assert.equal(fired, 2);
44
- assert.equal(L.scheduleReconnect(ctx, null, fn, "d"), "exhausted");
45
- ctx._stopped = true;
46
- assert.equal(L.scheduleReconnect(ctx, null, fn, "e"), "stopped");
47
- });
48
-
49
- test("scheduleReconnect: a timer that fires after stop does nothing (race regression)", (t) => {
50
- t.mock.timers.enable({ apis: ["setTimeout"] });
51
- const ctx = { _mqttOpt: { reconnectDelayMs: 100, reconnectJitter: 0 } };
52
- let fired = 0;
53
- L.scheduleReconnect(ctx, null, () => fired++, "x");
54
- ctx._stopped = true; // stop WITHOUT clearing the timer, e.g. a path that forgot to
55
- t.mock.timers.tick(10000);
56
- assert.equal(fired, 0);
57
- });
58
-
59
- test("maxReconnectAttempts: 0 means unlimited only when explicitly set", () => {
60
- const ctx = { _reconnectAttempt: 9999, _mqttOpt: { maxReconnectAttempts: 0 } };
61
- assert.equal(L.scheduleReconnect(ctx, null, () => {}, "u"), "scheduled");
62
- clearTimeout(ctx._reconnectTimer);
63
- const ctx2 = { _reconnectAttempt: 10, _mqttOpt: {} };
64
- assert.equal(L.scheduleReconnect(ctx2, null, () => {}, "default"), "exhausted"); // default cap = 10
65
- });
66
-
67
- test("clearListenerTimers is idempotent and clears every owned timer", () => {
68
- const ctx = {
69
- _autoCycleTimer: setInterval(() => {}, 1e6), _heartbeatInterval: setInterval(() => {}, 1e6),
70
- _sessionKeeperInterval: setTimeout(() => {}, 1e6), _rTimeout: setTimeout(() => {}, 1e6),
71
- _reconnectTimer: setTimeout(() => {}, 1e6), _dtsgRefreshInterval: setInterval(() => {}, 1e6)
72
- };
73
- L.clearListenerTimers(ctx, false);
74
- assert.equal(ctx._autoCycleTimer, null); assert.equal(ctx._heartbeatInterval, null);
75
- assert.equal(ctx._sessionKeeperInterval, null); assert.equal(ctx._rTimeout, null); assert.equal(ctx._reconnectTimer, null);
76
- assert.notEqual(ctx._dtsgRefreshInterval, null, "session timer kept unless includeSession");
77
- L.clearListenerTimers(ctx, true);
78
- assert.equal(ctx._dtsgRefreshInterval, null);
79
- assert.doesNotThrow(() => L.clearListenerTimers(ctx, true));
80
- });
81
-
82
- test("detachClient: a late 'error' after detach must not throw", () => {
83
- const c = new EventEmitter();
84
- c.on("error", () => {});
85
- L.detachClient(c);
86
- assert.equal(c.listenerCount("message"), 0);
87
- assert.doesNotThrow(() => c.emit("error", new Error("late socket error")));
88
- assert.doesNotThrow(() => L.detachClient(null));
89
- // contrast: plain removeAllListeners (the old behaviour) DOES throw
90
- const d = new EventEmitter(); d.removeAllListeners();
91
- assert.throws(() => d.emit("error", new Error("boom")));
92
- });
@@ -1,43 +0,0 @@
1
- "use strict";
2
- const test = require("node:test");
3
- const assert = require("node:assert/strict");
4
- const path = require("path");
5
- const root = process.env.HRIDOY_TEST_ROOT || path.join(__dirname, "..", "..");
6
- const { extractDtsg } = require(path.join(root, "src/utils/loginParser/extractDtsg"));
7
- const fp = require(path.join(root, "src/utils/fingerprint"));
8
-
9
- test("extractDtsg handles each supported markup shape", () => {
10
- const cases = [
11
- ['<script>["DTSGInitialData",[],{"token":"TOK_A"}]</script>', "TOK_A"],
12
- ['x["DTSGInitData",[],{"token":"TOK_B","async_get_token":"zz"}]', "TOK_B"],
13
- ['{"dtsg":{"token":"TOK_C"}}', "TOK_C"],
14
- ['{"async_get_token":"TOK_D"}', "TOK_D"],
15
- ['<input name="fb_dtsg" value="TOK_E">', "TOK_E"]
16
- ];
17
- for (const [html, want] of cases) assert.equal(extractDtsg(html).fb_dtsg, want, html);
18
- });
19
-
20
- test("extractDtsg: jazoest forms, empty and non-string input", () => {
21
- assert.equal(extractDtsg('<input name="jazoest" value="2987">').jazoest, "2987");
22
- assert.equal(extractDtsg("a?jazoest=2123&b").jazoest, "2123");
23
- assert.deepEqual(extractDtsg(""), { fb_dtsg: null, jazoest: null });
24
- assert.deepEqual(extractDtsg(null), { fb_dtsg: null, jazoest: null });
25
- assert.deepEqual(extractDtsg("<html>nothing here</html>"), { fb_dtsg: null, jazoest: null });
26
- });
27
-
28
- test("fingerprint: default UA and client hints are mutually consistent", () => {
29
- const ua = fp.getDefaultUserAgent();
30
- assert.match(ua, /^Mozilla\/5\.0 /);
31
- const h = fp.clientHintsFromUA(ua);
32
- assert.equal(h["sec-ch-ua-mobile"], /Mobile|Android|iPhone/.test(ua) ? "?1" : "?0");
33
- const m = ua.match(/Chrome\/(\d+)/);
34
- if (m) assert.ok(h["sec-ch-ua"].includes(`v="${m[1]}"`), "hint major must match UA major");
35
- });
36
-
37
- test("fingerprint: Android UA yields mobile Android hints (no desktop contradiction)", () => {
38
- const ua = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Mobile Safari/537.36";
39
- const h = fp.clientHintsFromUA(ua);
40
- assert.equal(h["sec-ch-ua-mobile"], "?1");
41
- assert.equal(h["sec-ch-ua-platform"], '"Android"');
42
- assert.equal(h["sec-ch-ua-arch"], undefined);
43
- });