@7365admin1/core 3.42.0 → 3.42.1
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/CHANGELOG.md +18 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3520 -3364
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3548 -3392
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
- package/test/e2e/harness.mjs +426 -0
- package/test/e2e/service-provider-invite.e2e.test.mjs +471 -0
- package/test/service-provider-invite.test.mjs +198 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.42.
|
|
4
|
+
"version": "3.42.1",
|
|
5
5
|
"author": "7365admin1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
|
|
14
14
|
"release": "yarn run build && changeset publish",
|
|
15
|
-
"lint": "tsc"
|
|
15
|
+
"lint": "tsc",
|
|
16
|
+
"test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@changesets/cli": "^2.26.0",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"@types/node-cron": "^3.0.11",
|
|
25
26
|
"@types/nodemailer": "^8.0.0",
|
|
26
27
|
"@types/xml2js": "^0.4.14",
|
|
28
|
+
"mongodb-memory-server": "^10.1.4",
|
|
27
29
|
"shx": "^0.4.0",
|
|
28
30
|
"tsup": "^6.5.0",
|
|
29
31
|
"typescript": "^4.9.4"
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// Throwaway end-to-end harness for the service-provider invite flow.
|
|
2
|
+
//
|
|
3
|
+
// Everything it talks to is created here and thrown away at the end of the run:
|
|
4
|
+
// an in-process MongoDB replica set (mongodb-memory-server, replica set because
|
|
5
|
+
// the services use transactions), a minimal Redis server on a loopback port, and
|
|
6
|
+
// a minimal SMTP server on a loopback port that swallows every message and hands
|
|
7
|
+
// it back for assertions.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here reaches a shared database, a shared Redis, or a real mail server.
|
|
10
|
+
// The mailer is pointed at 127.0.0.1, so a real email cannot be sent even by
|
|
11
|
+
// mistake.
|
|
12
|
+
//
|
|
13
|
+
// The Express wiring below is a verbatim mirror of the route files in
|
|
14
|
+
// iservice365-API-core (named above each block). The middleware (`requireAuth`),
|
|
15
|
+
// the error handler, the controllers and the services are the real published
|
|
16
|
+
// ones — only the `app.use(...)` mounting is restated here, because the route
|
|
17
|
+
// files live in the API repository and cannot be imported from this package.
|
|
18
|
+
|
|
19
|
+
import net from "node:net";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import crypto from "node:crypto";
|
|
24
|
+
import { once } from "node:events";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
|
|
27
|
+
import express from "express";
|
|
28
|
+
import { MongoMemoryReplSet } from "mongodb-memory-server";
|
|
29
|
+
import { ObjectId } from "mongodb";
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
|
|
33
|
+
/* -------------------------------------------------------------- fake Redis */
|
|
34
|
+
|
|
35
|
+
// ponytail: only the commands this flow actually uses, and TTLs are ignored —
|
|
36
|
+
// a run lasts seconds. If a test ever needs expiry, store an expiry timestamp
|
|
37
|
+
// alongside the value and check it in GET.
|
|
38
|
+
function startRedis() {
|
|
39
|
+
const strings = new Map();
|
|
40
|
+
const sets = new Map();
|
|
41
|
+
|
|
42
|
+
const enc = (v) => {
|
|
43
|
+
if (v === null || v === undefined) return "$-1\r\n";
|
|
44
|
+
if (typeof v === "number") return `:${v}\r\n`;
|
|
45
|
+
if (Array.isArray(v)) return `*${v.length}\r\n` + v.map(enc).join("");
|
|
46
|
+
if (typeof v === "object" && v.simple) return `+${v.simple}\r\n`;
|
|
47
|
+
const b = Buffer.from(String(v));
|
|
48
|
+
return `$${b.length}\r\n${b.toString()}\r\n`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const run = (args) => {
|
|
52
|
+
const cmd = String(args[0] ?? "").toUpperCase();
|
|
53
|
+
const key = args[1];
|
|
54
|
+
|
|
55
|
+
switch (cmd) {
|
|
56
|
+
case "PING":
|
|
57
|
+
return { simple: "PONG" };
|
|
58
|
+
case "INFO":
|
|
59
|
+
return "# Server\r\nredis_version:7.2.0\r\n# Persistence\r\nloading:0\r\n";
|
|
60
|
+
case "GET":
|
|
61
|
+
return strings.has(key) ? strings.get(key) : null;
|
|
62
|
+
case "SET":
|
|
63
|
+
case "SETEX":
|
|
64
|
+
strings.set(key, cmd === "SET" ? args[2] : args[3]);
|
|
65
|
+
return { simple: "OK" };
|
|
66
|
+
case "DEL":
|
|
67
|
+
case "UNLINK": {
|
|
68
|
+
let n = 0;
|
|
69
|
+
for (const k of args.slice(1)) {
|
|
70
|
+
if (strings.delete(k)) n++;
|
|
71
|
+
if (sets.delete(k)) n++;
|
|
72
|
+
}
|
|
73
|
+
return n;
|
|
74
|
+
}
|
|
75
|
+
case "EXISTS":
|
|
76
|
+
return args.slice(1).filter((k) => strings.has(k) || sets.has(k)).length;
|
|
77
|
+
case "SADD": {
|
|
78
|
+
const s = sets.get(key) ?? new Set();
|
|
79
|
+
const before = s.size;
|
|
80
|
+
for (const m of args.slice(2)) s.add(m);
|
|
81
|
+
sets.set(key, s);
|
|
82
|
+
return s.size - before;
|
|
83
|
+
}
|
|
84
|
+
case "SMEMBERS":
|
|
85
|
+
return [...(sets.get(key) ?? [])];
|
|
86
|
+
case "KEYS":
|
|
87
|
+
return [...strings.keys()];
|
|
88
|
+
case "EXPIRE":
|
|
89
|
+
case "TTL":
|
|
90
|
+
return 1;
|
|
91
|
+
default:
|
|
92
|
+
// AUTH, SELECT, CLIENT, COMMAND, QUIT, ...
|
|
93
|
+
return { simple: "OK" };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const server = net.createServer((sock) => {
|
|
98
|
+
let buf = Buffer.alloc(0);
|
|
99
|
+
|
|
100
|
+
sock.on("data", (chunk) => {
|
|
101
|
+
buf = Buffer.concat([buf, chunk]);
|
|
102
|
+
|
|
103
|
+
for (;;) {
|
|
104
|
+
const parsed = parseCommand(buf);
|
|
105
|
+
if (!parsed) return;
|
|
106
|
+
buf = parsed.rest;
|
|
107
|
+
sock.write(enc(run(parsed.args)));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
sock.on("error", () => {});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// RESP: *<n>\r\n then n bulk strings ($<len>\r\n<bytes>\r\n)
|
|
114
|
+
function parseCommand(b) {
|
|
115
|
+
const text = b.toString("utf8");
|
|
116
|
+
if (!text.startsWith("*")) return null;
|
|
117
|
+
|
|
118
|
+
let i = text.indexOf("\r\n");
|
|
119
|
+
if (i < 0) return null;
|
|
120
|
+
|
|
121
|
+
const count = Number(text.slice(1, i));
|
|
122
|
+
let pos = i + 2;
|
|
123
|
+
const args = [];
|
|
124
|
+
|
|
125
|
+
for (let n = 0; n < count; n++) {
|
|
126
|
+
if (text[pos] !== "$") return null;
|
|
127
|
+
const j = text.indexOf("\r\n", pos);
|
|
128
|
+
if (j < 0) return null;
|
|
129
|
+
const len = Number(text.slice(pos + 1, j));
|
|
130
|
+
const start = j + 2;
|
|
131
|
+
if (text.length < start + len + 2) return null;
|
|
132
|
+
args.push(text.slice(start, start + len));
|
|
133
|
+
pos = start + len + 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { args, rest: Buffer.from(text.slice(pos), "utf8") };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
server.listen(0, "127.0.0.1", () =>
|
|
141
|
+
resolve({ port: server.address().port, stop: () => server.close() }),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* --------------------------------------------------------------- mail sink */
|
|
147
|
+
|
|
148
|
+
const decodeQuotedPrintable = (s) =>
|
|
149
|
+
s
|
|
150
|
+
.replace(/=\r?\n/g, "")
|
|
151
|
+
.replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
152
|
+
|
|
153
|
+
function parseMessage(raw) {
|
|
154
|
+
const split = raw.indexOf("\r\n\r\n");
|
|
155
|
+
const head = raw.slice(0, split).replace(/\r\n[ \t]+/g, " ");
|
|
156
|
+
let body = raw.slice(split + 4);
|
|
157
|
+
|
|
158
|
+
const header = (name) =>
|
|
159
|
+
head.match(new RegExp(`^${name}:\\s*(.*)$`, "im"))?.[1]?.trim() ?? "";
|
|
160
|
+
|
|
161
|
+
if (/quoted-printable/i.test(header("Content-Transfer-Encoding"))) {
|
|
162
|
+
body = decodeQuotedPrintable(body);
|
|
163
|
+
} else if (/base64/i.test(header("Content-Transfer-Encoding"))) {
|
|
164
|
+
body = Buffer.from(body.replace(/\r\n/g, ""), "base64").toString("utf8");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { to: header("To"), from: header("From"), subject: header("Subject"), body };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function startMailSink() {
|
|
171
|
+
const messages = [];
|
|
172
|
+
|
|
173
|
+
const server = net.createServer((sock) => {
|
|
174
|
+
let buf = "";
|
|
175
|
+
let inData = false;
|
|
176
|
+
|
|
177
|
+
sock.write("220 e2e.invalid ESMTP\r\n");
|
|
178
|
+
|
|
179
|
+
sock.on("data", (chunk) => {
|
|
180
|
+
buf += chunk.toString("utf8");
|
|
181
|
+
|
|
182
|
+
for (;;) {
|
|
183
|
+
if (inData) {
|
|
184
|
+
const end = buf.indexOf("\r\n.\r\n");
|
|
185
|
+
if (end < 0) return;
|
|
186
|
+
messages.push(parseMessage(buf.slice(0, end)));
|
|
187
|
+
buf = buf.slice(end + 5);
|
|
188
|
+
inData = false;
|
|
189
|
+
sock.write("250 2.0.0 Ok\r\n");
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const nl = buf.indexOf("\r\n");
|
|
194
|
+
if (nl < 0) return;
|
|
195
|
+
const line = buf.slice(0, nl);
|
|
196
|
+
buf = buf.slice(nl + 2);
|
|
197
|
+
|
|
198
|
+
const cmd = line.split(" ")[0].toUpperCase();
|
|
199
|
+
if (cmd === "EHLO" || cmd === "HELO") {
|
|
200
|
+
sock.write("250-e2e.invalid\r\n250-AUTH PLAIN\r\n250 8BITMIME\r\n");
|
|
201
|
+
} else if (cmd === "DATA") {
|
|
202
|
+
inData = true;
|
|
203
|
+
sock.write("354 End data with <CR><LF>.<CR><LF>\r\n");
|
|
204
|
+
} else if (cmd === "QUIT") {
|
|
205
|
+
sock.write("221 Bye\r\n");
|
|
206
|
+
sock.end();
|
|
207
|
+
return;
|
|
208
|
+
} else {
|
|
209
|
+
sock.write("250 2.0.0 Ok\r\n"); // AUTH, MAIL FROM, RCPT TO, RSET, NOOP
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
sock.on("error", () => {});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
server.listen(0, "127.0.0.1", () =>
|
|
218
|
+
resolve({ port: server.address().port, messages, stop: () => server.close() }),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ------------------------------------------------------------------- app */
|
|
224
|
+
|
|
225
|
+
function buildApp(nsu, core) {
|
|
226
|
+
const { requireAuth, errorHandler } = nsu;
|
|
227
|
+
const app = express();
|
|
228
|
+
app.use(express.json());
|
|
229
|
+
|
|
230
|
+
// mirrors iservice365-API-core/src/routes/auth.route.ts
|
|
231
|
+
const auth = express.Router();
|
|
232
|
+
{
|
|
233
|
+
const { login } = core.useAuthController();
|
|
234
|
+
auth.post("/", login);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// mirrors iservice365-API-core/src/routes/auth-v2.route.ts
|
|
238
|
+
const authV2 = express.Router();
|
|
239
|
+
{
|
|
240
|
+
const { signUp } = core.useAuthControllerV2();
|
|
241
|
+
const { verify } = core.useVerificationControllerV2();
|
|
242
|
+
authV2.post("/sign-up", signUp);
|
|
243
|
+
authV2.get("/verify/:verificationCode", verify);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// mirrors iservice365-API-core/src/routes/user-v2.route.ts
|
|
247
|
+
const usersV2 = express.Router();
|
|
248
|
+
{
|
|
249
|
+
const { createUserByVerification } = core.useUserControllerV2();
|
|
250
|
+
usersV2.post("/invite/:id", createUserByVerification);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// mirrors iservice365-API-core/src/routes/service-provider.route.ts
|
|
254
|
+
const serviceProviders = express.Router();
|
|
255
|
+
{
|
|
256
|
+
const { createServiceProviderInvite } = core.useVerificationController();
|
|
257
|
+
serviceProviders.post("/invite", requireAuth, createServiceProviderInvite);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// mirrors iservice365-API-core/src/routes/customer-site.route.ts
|
|
261
|
+
const customerSites = express.Router();
|
|
262
|
+
{
|
|
263
|
+
const { getAll, addViaInvite } = core.useCustomerSiteController();
|
|
264
|
+
customerSites.get("/", requireAuth, getAll);
|
|
265
|
+
customerSites.post("/invite/:id", addViaInvite); // no requireAuth — as shipped
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// mirrors iservice365-API-core/src/routes/index.ts mount points
|
|
269
|
+
app.use("/api/auth", auth);
|
|
270
|
+
app.use("/api/auth/v2", authV2);
|
|
271
|
+
app.use("/api/users/v2", usersV2);
|
|
272
|
+
app.use("/api/service-providers", serviceProviders);
|
|
273
|
+
app.use("/api/customer-sites", customerSites);
|
|
274
|
+
|
|
275
|
+
app.use(errorHandler);
|
|
276
|
+
return app;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ------------------------------------------------------------ working dir */
|
|
280
|
+
|
|
281
|
+
// src/utils/rsa-encryption.ts reads the Entrypass key pair from
|
|
282
|
+
// `<cwd>/src/public/rsa-keys/` at module load, and those keys are secrets that
|
|
283
|
+
// are not in the repository — so simply requiring the package fails anywhere
|
|
284
|
+
// they are absent. The invite flow never touches Entrypass, so the harness runs
|
|
285
|
+
// from a temporary directory holding a freshly generated throwaway pair. It
|
|
286
|
+
// also keeps the run away from any local .env.
|
|
287
|
+
function makeWorkDir() {
|
|
288
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "iservice365-e2e-"));
|
|
289
|
+
const keys = path.join(dir, "src", "public", "rsa-keys");
|
|
290
|
+
fs.mkdirSync(keys, { recursive: true });
|
|
291
|
+
|
|
292
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
293
|
+
modulusLength: 512,
|
|
294
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
295
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
fs.writeFileSync(path.join(keys, "new_rsa_512_pub.pem"), publicKey);
|
|
299
|
+
fs.writeFileSync(path.join(keys, "new_rsa_512_priv.pem"), privateKey);
|
|
300
|
+
|
|
301
|
+
const previous = process.cwd();
|
|
302
|
+
process.chdir(dir);
|
|
303
|
+
|
|
304
|
+
return () => {
|
|
305
|
+
process.chdir(previous);
|
|
306
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* --------------------------------------------------------------- harness */
|
|
311
|
+
|
|
312
|
+
export async function startHarness() {
|
|
313
|
+
const redis = await startRedis();
|
|
314
|
+
const mail = await startMailSink();
|
|
315
|
+
const mongo = await MongoMemoryReplSet.create({
|
|
316
|
+
replSet: { count: 1, storageEngine: "wiredTiger" },
|
|
317
|
+
});
|
|
318
|
+
const restoreCwd = makeWorkDir();
|
|
319
|
+
|
|
320
|
+
// config.ts reads these once, at import time — they must be set before the
|
|
321
|
+
// package is required.
|
|
322
|
+
Object.assign(process.env, {
|
|
323
|
+
NODE_ENV: "development",
|
|
324
|
+
MONGO_URI: mongo.getUri(),
|
|
325
|
+
MONGO_DB: "e2e",
|
|
326
|
+
REDIS_HOST: "127.0.0.1",
|
|
327
|
+
REDIS_PORT: String(redis.port),
|
|
328
|
+
REDIS_PASSWORD: "",
|
|
329
|
+
MAILER_TRANSPORT_HOST: "127.0.0.1",
|
|
330
|
+
MAILER_TRANSPORT_PORT: String(mail.port),
|
|
331
|
+
MAILER_TRANSPORT_SECURE: "false",
|
|
332
|
+
MAILER_EMAIL: "no-reply@e2e.example.com",
|
|
333
|
+
MAILER_PASSWORD: "e2e",
|
|
334
|
+
APP_MAIN: "http://app.e2e.example.com",
|
|
335
|
+
DOMAIN: "localhost",
|
|
336
|
+
// the S3 client is constructed eagerly by the user service; this flow never
|
|
337
|
+
// uploads anything, and the endpoint is deliberately unroutable.
|
|
338
|
+
SPACES_ACCESS_KEY: "e2e",
|
|
339
|
+
SPACES_SECRET_KEY: "e2e",
|
|
340
|
+
SPACES_ENDPOINT: "http://127.0.0.1:1",
|
|
341
|
+
SPACES_REGION: "us-east-1",
|
|
342
|
+
SPACES_BUCKET: "e2e",
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const nsu = require("@7365admin1/node-server-utils");
|
|
346
|
+
await nsu.useAtlas.initialize({ uri: mongo.getUri(), db: "e2e" });
|
|
347
|
+
nsu.useRedis().init({ host: "127.0.0.1", port: redis.port });
|
|
348
|
+
|
|
349
|
+
const core = require("../../dist/index.js");
|
|
350
|
+
const db = nsu.useAtlas.getDb();
|
|
351
|
+
|
|
352
|
+
const server = buildApp(nsu, core).listen(0, "127.0.0.1");
|
|
353
|
+
await once(server, "listening");
|
|
354
|
+
const base = `http://127.0.0.1:${server.address().port}/api`;
|
|
355
|
+
|
|
356
|
+
async function api(path, { method = "GET", body, sid } = {}) {
|
|
357
|
+
const res = await fetch(`${base}${path}`, {
|
|
358
|
+
method,
|
|
359
|
+
headers: {
|
|
360
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
361
|
+
...(sid ? { authorization: `Bearer ${sid}` } : {}),
|
|
362
|
+
},
|
|
363
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const text = await res.text();
|
|
367
|
+
let json;
|
|
368
|
+
try {
|
|
369
|
+
json = JSON.parse(text);
|
|
370
|
+
} catch {
|
|
371
|
+
json = text;
|
|
372
|
+
}
|
|
373
|
+
return { status: res.status, body: json };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// issueSession caches the session without awaiting it, so a login can return
|
|
377
|
+
// a moment before requireAuth can see it. Log in through the real endpoint,
|
|
378
|
+
// then wait for the session to land.
|
|
379
|
+
async function login(email, password) {
|
|
380
|
+
const res = await api("/auth", { method: "POST", body: { email, password } });
|
|
381
|
+
if (res.status !== 200) {
|
|
382
|
+
throw new Error(`login failed for ${email}: ${res.status} ${JSON.stringify(res.body)}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const sid = res.body.sid;
|
|
386
|
+
for (let i = 0; i < 100; i++) {
|
|
387
|
+
const probe = await api("/customer-sites?org=" + new ObjectId().toString() + "&status=active", { sid });
|
|
388
|
+
if (probe.status !== 401) return sid;
|
|
389
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
390
|
+
}
|
|
391
|
+
throw new Error(`session for ${email} never became usable`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// some senders fire the mail off without awaiting it, so a test that looks
|
|
395
|
+
// straight after the response can be ahead of the sink.
|
|
396
|
+
async function mailAt(index, timeoutMs = 5000) {
|
|
397
|
+
const until = Date.now() + timeoutMs;
|
|
398
|
+
while (mail.messages.length <= index) {
|
|
399
|
+
if (Date.now() > until) throw new Error(`no email arrived at index ${index}`);
|
|
400
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
401
|
+
}
|
|
402
|
+
return mail.messages[index];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function stop() {
|
|
406
|
+
server.close();
|
|
407
|
+
await nsu.useAtlas.close();
|
|
408
|
+
try {
|
|
409
|
+
nsu.useRedis().getClient().disconnect();
|
|
410
|
+
} catch {}
|
|
411
|
+
redis.stop();
|
|
412
|
+
mail.stop();
|
|
413
|
+
await mongo.stop();
|
|
414
|
+
restoreCwd();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
api,
|
|
419
|
+
login,
|
|
420
|
+
mailAt,
|
|
421
|
+
db,
|
|
422
|
+
mails: mail.messages,
|
|
423
|
+
hashPassword: nsu.hashPassword,
|
|
424
|
+
stop,
|
|
425
|
+
};
|
|
426
|
+
}
|