1
2import assert from 'node:assert/strict';
3import { after, afterEach, describe, it } from 'node:test';
4
5import type { MockHandler, MockServer } from './helpers.js';
6import { removeStorage, runActor, startMockTomba, startStandbyActor, totalCharges } from './helpers.js';
7
8const SOURCES = [
9 {
10 uri: 'https://stripe.com/blog/team',
11 website_url: 'stripe.com',
12 extracted_on: '2024-09-17T11:26:56+02:00',
13 last_seen_on: '2025-09-06T04:51:06+02:00',
14 still_on_page: true,
15 },
16];
17
18
19function found(domain: string, first: string, last: string): Record<string, unknown> {
20 return {
21 email: `${first.toLowerCase()}.${last.toLowerCase()}@${domain}`,
22 first_name: first,
23 last_name: last,
24 full_name: `${first} ${last}`,
25 country: 'US',
26 gender: 'male',
27 phone_number: false,
28 position: 'Head of Sales',
29 twitter: null,
30 linkedin: `https://www.linkedin.com/in/${first.toLowerCase()}${last.toLowerCase()}`,
31 disposable: false,
32 webmail: false,
33 accept_all: false,
34 company: 'Stripe',
35 website_url: domain,
36 score: 97,
37 verification: { date: '2025-10-17T00:00:00+02:00', status: 'valid' },
38 sources: SOURCES,
39 };
40}
41
42
43const PHONES: Record<string, Record<string, unknown>[]> = {
44 Jane: [
45 { number: '+14155550123', type: 'mobile' },
46 { number: '+14155550199', type: 'work' },
47 ],
48 Phoneless: [],
49};
50const phonesFor = (first: string) => PHONES[first] ?? [{ number: '+14155550123', type: 'mobile' }];
51
52
53
54
55
56
57
58const tomba: MockHandler = (req) => {
59 assert.equal(req.method, 'GET');
60 assert.equal(req.path, '/email-finder');
61 const { company, full_name: fullName, enrich_mobile: enrichMobile } = req.query;
62 const domain = req.query.domain ?? `${company.toLowerCase()}.com`;
63 const [first, last] = fullName ? fullName.split(' ') : [req.query.first_name, req.query.last_name];
64 const phones = enrichMobile === 'true' ? { phone_data: phonesFor(first) } : {};
65 if (domain === 'empty.com') return { body: { data: null } };
66 if (domain === 'invalid.com') return { status: 422, body: { errors: { message: 'Invalid domain' } } };
67 if (domain === 'html.com') return { raw: '<html>Bad gateway</html>' };
68 if (first === 'Nobody') {
69 return {
70 body: {
71 data: {
72 email: null,
73 first_name: first,
74 last_name: last,
75 full_name: null,
76 website_url: domain,
77 score: 0,
78 verification: { date: null, status: null },
79 sources: [],
80 ...phones,
81 },
82 },
83 };
84 }
85 return { body: { data: { ...found(domain, first, last), ...phones } } };
86};
87
88const servers: MockServer[] = [];
89const dirs: string[] = [];
90
91async function mock(handler: MockHandler = tomba): Promise<MockServer> {
92 const server = await startMockTomba(handler);
93 servers.push(server);
94 return server;
95}
96
97async function run(...args: Parameters<typeof runActor>) {
98 const result = await runActor(...args);
99 dirs.push(result.storageDir);
100 return result;
101}
102
103const person = (domain: string, firstName = 'John', lastName = 'Doe') => ({ domain, firstName, lastName });
104
105afterEach(async () => {
106 await Promise.all(servers.splice(0).map(async (s) => s.close()));
107});
108
109after(async () => {
110 await Promise.all(dirs.map(removeStorage));
111});
112
113describe('email-finder', () => {
114 it('returns the email and charges one event per billable request', async () => {
115 const server = await mock();
116 const result = await run({
117 input: { requests: [person('stripe.com'), person('empty.com')] },
118 endpoint: server.url,
119 });
120
121 assert.equal(result.code, 0, result.output);
122 const hit = result.items.find((i) => i.domain === 'stripe.com');
123 assert.deepEqual(hit, {
124 ...found('stripe.com', 'John', 'Doe'),
125 domain: 'stripe.com',
126 firstName: 'John',
127 lastName: 'Doe',
128 source: 'tomba_email_finder',
129 phoneNumbers: 0,
130 charged: true,
131 chargedCredits: 1,
132 cached: false,
133 });
134
135 const empty = result.items.find((i) => i.domain === 'empty.com');
136 assert.deepEqual(empty, {
137 domain: 'empty.com',
138 firstName: 'John',
139 lastName: 'Doe',
140 source: 'tomba_email_finder',
141 phoneNumbers: 0,
142 charged: false,
143 chargedCredits: 0,
144 cached: false,
145 error: 'No email found for this person',
146 });
147
148 assert.deepEqual(result.chargeCounts, { 'tomba-request': 1 });
149 });
150
151 it('sends the built-in credentials and the expected query to Tomba', async () => {
152 const server = await mock();
153 await run({ input: { requests: [person('stripe.com')] }, endpoint: server.url });
154 assert.equal(server.requests.length, 1);
155 assert.equal(server.requests[0].headers['x-tomba-key'], 'ta_test_key');
156 assert.equal(server.requests[0].headers['x-tomba-secret'], 'ts_test_secret');
157 assert.deepEqual(server.requests[0].query, { domain: 'stripe.com', first_name: 'John', last_name: 'Doe' });
158 });
159
160 it('normalizes domains, trims names and deduplicates requests', async () => {
161 const server = await mock();
162 const result = await run({
163 input: {
164 requests: [
165 person('https://www.Stripe.com/about', ' John ', ' Doe '),
166 person('stripe.com', 'john', 'DOE'),
167 person('STRIPE.COM', 'John', 'Doe'),
168 ],
169 },
170 endpoint: server.url,
171 });
172 assert.deepEqual(
173 server.requests.map((r) => r.query),
174 [{ domain: 'stripe.com', first_name: 'John', last_name: 'Doe' }],
175 );
176 assert.equal(result.items.length, 1);
177 });
178
179 it('charges a negative answer (person found, no email)', async () => {
180 const server = await mock();
181 const result = await run({
182 input: { requests: [person('stripe.com', 'Nobody', 'Here')] },
183 endpoint: server.url,
184 });
185 assert.equal(result.items.length, 1);
186 assert.equal(result.items[0].email, null);
187 assert.equal(result.items[0].charged, true);
188 assert.equal(result.items[0].error, undefined);
189 assert.deepEqual(result.chargeCounts, { 'tomba-request': 1 });
190 });
191
192 it('does not charge Tomba error statuses and does not retry them', async () => {
193 const server = await mock();
194 const result = await run({ input: { requests: [person('invalid.com')] }, endpoint: server.url });
195 assert.equal(result.code, 0, result.output);
196 assert.equal(server.requests.length, 1);
197 assert.equal(result.items[0].charged, false);
198 assert.match(String(result.items[0].error), /422: Invalid domain/);
199 assert.equal(totalCharges(result), 0);
200 });
201
202 it('does not charge a non-JSON body', async () => {
203 const server = await mock();
204 const result = await run({ input: { requests: [person('html.com')] }, endpoint: server.url });
205 assert.equal(result.items[0].charged, false);
206 assert.match(String(result.items[0].error), /Invalid response/);
207 assert.equal(totalCharges(result), 0);
208 });
209
210 it('retries 429 and 5xx responses, then charges the success once', async () => {
211 let calls = 0;
212 const server = await mock(async (req) => {
213 calls++;
214 if (calls === 1)
215 return {
216 status: 429,
217 body: { errors: { message: 'Too many requests' } },
218 headers: { 'retry-after': '1' },
219 };
220 if (calls === 2) return { status: 503, body: {} };
221 return tomba(req);
222 });
223 const result = await run({ input: { requests: [person('stripe.com')], maxRetries: 3 }, endpoint: server.url });
224 assert.equal(server.requests.length, 3);
225 assert.equal(result.items[0].charged, true);
226 assert.equal(result.items[0].email, 'john.doe@stripe.com');
227 assert.deepEqual(result.chargeCounts, { 'tomba-request': 1 });
228 });
229
230 it('serves repeated runs from the cache for free', async () => {
231 const server = await mock();
232 const first = await run({ input: { requests: [person('stripe.com')] }, endpoint: server.url });
233 const second = await run({
234 input: { requests: [person('stripe.com')] },
235 endpoint: server.url,
236 storageDir: first.storageDir,
237 });
238
239 assert.equal(server.requests.length, 1);
240 assert.equal(second.items.length, 1);
241 assert.equal(second.items[0].email, 'john.doe@stripe.com');
242 assert.equal(second.items[0].cached, true);
243 assert.equal(second.items[0].charged, false);
244 assert.equal(totalCharges(second), 0);
245 });
246
247 it('calls Tomba again when the cache is disabled', async () => {
248 const server = await mock();
249 const input = { requests: [person('stripe.com')], useCache: false };
250 const first = await run({ input, endpoint: server.url });
251 await run({ input, endpoint: server.url, storageDir: first.storageDir });
252 assert.equal(server.requests.length, 2);
253 });
254
255 it('stops at the max charge limit and resumes without reprocessing', async () => {
256 const server = await mock();
257 const domains = ['a.com', 'b.com', 'c.com', 'd.com', 'e.com'];
258 const input = { requests: domains.map((d) => person(d)), maxConcurrency: 1, useCache: false };
259
260
261 const first = await run({ input, endpoint: server.url, maxTotalChargeUsd: 2 });
262 assert.equal(first.code, 0, first.output);
263 assert.equal(totalCharges(first), 2);
264 assert.equal(server.requests.length, 2);
265 assert.equal(first.items.length, 2);
266
267 const second = await run({ input, endpoint: server.url, storageDir: first.storageDir, keepStorage: true });
268 assert.equal(second.code, 0, second.output);
269
270 assert.equal(totalCharges(second), 5);
271 assert.equal(second.items.length, 5);
272 assert.deepEqual(
273 server.requests.map((r) => r.query.domain),
274 domains,
275 );
276 });
277
278 it('respects maxResults', async () => {
279 const server = await mock();
280 const result = await run({
281 input: { requests: [person('a.com'), person('b.com'), person('c.com')], maxResults: 2 },
282 endpoint: server.url,
283 });
284 assert.equal(server.requests.length, 2);
285 assert.equal(result.items.length, 2);
286 assert.equal(totalCharges(result), 2);
287 });
288
289 it('runs requests in parallel', async () => {
290 let active = 0;
291 let peak = 0;
292 const server = await mock(async (req) => {
293 active++;
294 peak = Math.max(peak, active);
295 await new Promise((r) => {
296 setTimeout(r, 100);
297 });
298 active--;
299 return tomba(req);
300 });
301 const requests = Array.from({ length: 8 }, (_, i) => person(`site${i}.com`));
302 await run({ input: { requests, maxConcurrency: 4 }, endpoint: server.url });
303 assert.equal(server.requests.length, 8);
304 assert.ok(peak > 1 && peak <= 4, `peak concurrency ${peak}`);
305 });
306
307 it('fails without Tomba credentials and never calls the API', async () => {
308 const server = await mock();
309 const result = await run({
310 input: { requests: [person('stripe.com')] },
311 endpoint: server.url,
312 withCredentials: false,
313 });
314 assert.notEqual(result.code, 0);
315 assert.match(result.output, /misconfigured/);
316 assert.doesNotMatch(result.output, /ta_test_key|ts_test_secret/);
317 assert.equal(server.requests.length, 0);
318 });
319
320 it('never sends requests with a missing field and does not charge them', async () => {
321 const server = await mock();
322 const result = await run({
323 input: {
324 requests: [
325 { domain: 'stripe.com', firstName: 'John' },
326 { domain: '', firstName: 'Jane', lastName: 'Smith' },
327 { firstName: ' ', lastName: 'Smith', domain: 'stripe.com' },
328 person('stripe.com', 'Jane', 'Smith'),
329 ],
330 },
331 endpoint: server.url,
332 });
333 assert.equal(result.code, 0, result.output);
334 assert.deepEqual(
335 server.requests.map((r) => r.query),
336 [{ domain: 'stripe.com', first_name: 'Jane', last_name: 'Smith' }],
337 );
338 const invalid = result.items.filter((i) => i.error);
339 assert.equal(invalid.length, 3);
340 for (const item of invalid) {
341 assert.equal(item.charged, false);
342 assert.match(String(item.error), /Missing required fields/);
343 }
344 assert.deepEqual(result.chargeCounts, { 'tomba-request': 1 });
345 });
346
347 it('sends enrich_mobile and webhook_url only when they are set', async () => {
348 const server = await mock();
349 await run({ input: { requests: [person('stripe.com')], enrichMobile: false }, endpoint: server.url });
350 await run({
351 input: {
352 requests: [person('shopify.com')],
353 enrichMobile: true,
354 webhookUrl: 'https://hooks.example.com/tomba',
355 },
356 endpoint: server.url,
357 });
358 assert.deepEqual(
359 server.requests.map((r) => r.query),
360 [
361 { domain: 'stripe.com', first_name: 'John', last_name: 'Doe' },
362 {
363 domain: 'shopify.com',
364 first_name: 'John',
365 last_name: 'Doe',
366 enrich_mobile: 'true',
367 webhook_url: 'https://hooks.example.com/tomba',
368 },
369 ],
370 );
371 });
372
373 it('fails on a webhook URL that is not http(s) and never calls the API', async () => {
374 const server = await mock();
375 const result = await run({
376 input: { requests: [person('stripe.com')], webhookUrl: 'ftp://hooks.example.com' },
377 endpoint: server.url,
378 });
379 assert.notEqual(result.code, 0);
380 assert.equal(server.requests.length, 0);
381 });
382
383 it('charges 1 credit without enrichMobile and returns no phone data', async () => {
384 const server = await mock();
385 const result = await run({
386 input: { requests: [person('stripe.com', 'Jane', 'Smith')] },
387 endpoint: server.url,
388 });
389 assert.equal(result.items[0].phone_data, undefined);
390 assert.equal(result.items[0].phoneNumbers, 0);
391 assert.equal(result.items[0].chargedCredits, 1);
392 assert.deepEqual(result.chargeCounts, { 'tomba-request': 1 });
393 });
394
395 it('charges 6 credits per result with phone data, whatever the number of phones', async () => {
396 const server = await mock();
397 const result = await run({
398 input: {
399 requests: [person('stripe.com', 'John', 'Doe'), person('stripe.com', 'Jane', 'Smith')],
400 enrichMobile: true,
401 },
402 endpoint: server.url,
403 });
404 assert.equal(result.code, 0, result.output);
405 const john = result.items.find((i) => i.firstName === 'John');
406 const jane = result.items.find((i) => i.firstName === 'Jane');
407 assert.deepEqual(john?.phone_data, [{ number: '+14155550123', type: 'mobile' }]);
408 assert.equal(john?.phoneNumbers, 1);
409 assert.equal(john?.chargedCredits, 6);
410 assert.equal(john?.charged, true);
411 assert.equal(jane?.phoneNumbers, 2);
412 assert.equal(jane?.chargedCredits, 6);
413 assert.deepEqual(result.chargeCounts, { 'tomba-request': 12 });
414 });
415
416 it('charges 1 credit with enrichMobile when no phone number is found', async () => {
417 const server = await mock();
418 const result = await run({
419 input: { requests: [person('stripe.com', 'Phoneless', 'Person')], enrichMobile: true },
420 endpoint: server.url,
421 });
422 assert.deepEqual(result.items[0].phone_data, []);
423 assert.equal(result.items[0].phoneNumbers, 0);
424 assert.equal(result.items[0].chargedCredits, 1);
425 assert.deepEqual(result.chargeCounts, { 'tomba-request': 1 });
426 });
427
428 it('serves phone data from the cache for free', async () => {
429 const server = await mock();
430 const input = { requests: [person('stripe.com', 'Jane', 'Smith')], enrichMobile: true };
431 const first = await run({ input, endpoint: server.url });
432 assert.equal(totalCharges(first), 6);
433 const second = await run({ input, endpoint: server.url, storageDir: first.storageDir });
434 assert.equal(server.requests.length, 1);
435 assert.equal(second.items[0].cached, true);
436 assert.equal(second.items[0].charged, false);
437 assert.equal(second.items[0].chargedCredits, 0);
438 assert.equal(second.items[0].phoneNumbers, 2);
439 assert.equal(totalCharges(second), 0);
440 });
441
442 it('does not reuse a cached result without phone data when enrichMobile is turned on', async () => {
443 const server = await mock();
444 const first = await run({ input: { requests: [person('stripe.com')] }, endpoint: server.url });
445 const second = await run({
446 input: { requests: [person('stripe.com')], enrichMobile: true },
447 endpoint: server.url,
448 storageDir: first.storageDir,
449 });
450 assert.equal(server.requests.length, 2);
451 assert.equal(second.items[0].phoneNumbers, 1);
452 assert.equal(totalCharges(second), 6);
453 });
454
455 it('accepts a company name instead of a domain and a full name instead of first and last name', async () => {
456 const server = await mock();
457 const result = await run({
458 input: {
459 requests: [
460 { company: ' Stripe ', fullName: ' John Doe ' },
461 { domain: 'https://www.shopify.com/', fullName: 'Jane Smith' },
462 { company: 'Acme', firstName: 'Bob', lastName: 'Ray' },
463 { company: 'stripe', fullName: 'john doe' },
464 ],
465 maxConcurrency: 1,
466 },
467 endpoint: server.url,
468 });
469 assert.equal(result.code, 0, result.output);
470 assert.deepEqual(
471 server.requests.map((r) => r.query),
472 [
473 { company: 'Stripe', full_name: 'John Doe' },
474 { domain: 'shopify.com', full_name: 'Jane Smith' },
475 { company: 'Acme', first_name: 'Bob', last_name: 'Ray' },
476 ],
477 );
478 assert.equal(result.items.length, 3);
479 const john = result.items.find((i) => i.fullName === 'John Doe');
480 assert.equal(john?.email, 'john.doe@stripe.com');
481 assert.equal(john?.inputCompany, 'Stripe');
482 assert.equal(john?.company, 'Stripe');
483 assert.equal(john?.domain, '');
484 const bob = result.items.find((i) => i.inputCompany === 'Acme');
485 assert.equal(bob?.email, 'bob.ray@acme.com');
486 assert.equal(bob?.fullName, undefined);
487 assert.deepEqual(result.chargeCounts, { 'tomba-request': 3 });
488 });
489
490 it('rejects requests without a company or without a name, for free', async () => {
491 const server = await mock();
492 const result = await run({
493 input: {
494 requests: [
495 { company: 'Stripe' },
496 { fullName: 'John Doe' },
497 { company: 'Stripe', firstName: 'John' },
498 { company: ' ', domain: '', fullName: 'Jane Doe' },
499 { company: 'Stripe', fullName: ' ', lastName: 'Doe' },
500 ],
501 },
502 endpoint: server.url,
503 });
504 assert.equal(result.code, 0, result.output);
505 assert.equal(server.requests.length, 0);
506 assert.equal(result.items.length, 5);
507 for (const item of result.items) {
508 assert.equal(item.charged, false);
509 assert.equal(item.chargedCredits, 0);
510 assert.equal(item.phoneNumbers, 0);
511 assert.match(String(item.error), /Missing required fields/);
512 }
513 assert.equal(totalCharges(result), 0);
514 });
515
516 it('fails on empty input', async () => {
517 const server = await mock();
518 const result = await run({ input: { requests: [] }, endpoint: server.url });
519 assert.notEqual(result.code, 0);
520 assert.equal(server.requests.length, 0);
521 });
522});
523
524describe('email-finder standby (real-time API)', () => {
525 it('answers the readiness probe and a bare GET with usage info', async () => {
526 const server = await mock();
527 const actor = await startStandbyActor({ endpoint: server.url });
528 try {
529 const probe = await actor.call('/', { headers: { 'x-apify-container-server-readiness-probe': '1' } });
530 assert.equal(probe.status, 200);
531 const usage = await actor.call('/');
532 assert.equal(usage.status, 200);
533 assert.match(String(usage.body.usage), /GET/);
534 assert.equal(server.requests.length, 0);
535 } finally {
536 await actor.stop();
537 }
538 });
539
540 it('looks up one person from GET query parameters and charges per credit', async () => {
541 const server = await mock();
542 const actor = await startStandbyActor({ endpoint: server.url });
543 let stopped;
544 try {
545 const res = await actor.call('/?domain=https://www.Stripe.com/team&first_name=John&last_name=Doe');
546 assert.equal(res.status, 200);
547 assert.deepEqual(res.body.items, [
548 {
549 ...found('stripe.com', 'John', 'Doe'),
550 domain: 'stripe.com',
551 firstName: 'John',
552 lastName: 'Doe',
553 source: 'tomba_email_finder',
554 phoneNumbers: 0,
555 charged: true,
556 chargedCredits: 1,
557 cached: false,
558 },
559 ]);
560
561 const phones = await actor.call('/?company=Stripe&full_name=Jane%20Smith&enrichMobile=true');
562 const [item] = phones.body.items as Record<string, unknown>[];
563 assert.equal(item.inputCompany, 'Stripe');
564 assert.equal(item.fullName, 'Jane Smith');
565 assert.equal(item.phoneNumbers, 2);
566 assert.equal(item.chargedCredits, 6);
567 assert.equal(server.requests[1].query.enrich_mobile, 'true');
568
569 const empty = await actor.call('/?domain=empty.com&firstName=John&lastName=Doe');
570 const [miss] = empty.body.items as Record<string, unknown>[];
571 assert.equal(miss.error, 'No email found for this person');
572 assert.equal(miss.charged, false);
573 } finally {
574 stopped = await actor.stop();
575 }
576 assert.deepEqual(stopped.chargeCounts, { 'tomba-request': 7 });
577 });
578
579 it('accepts a POST with the same JSON input as a normal run', async () => {
580 const server = await mock();
581 const actor = await startStandbyActor({ endpoint: server.url });
582 try {
583 const res = await actor.call('/', {
584 body: { requests: [person('stripe.com'), person('shopify.com', 'Jane', 'Smith'), { company: 'X' }] },
585 });
586 assert.equal(res.status, 200);
587 const items = res.body.items as Record<string, unknown>[];
588 assert.equal(items.length, 3);
589 assert.equal(items.filter((i) => i.email).length, 2);
590 assert.equal(server.requests.length, 2);
591 } finally {
592 await actor.stop();
593 }
594 });
595
596 it('serves repeated requests from the cache for free', async () => {
597 const server = await mock();
598 const actor = await startStandbyActor({ endpoint: server.url });
599 let stopped;
600 try {
601 await actor.call('/?domain=stripe.com&fullName=John%20Doe');
602 const second = await actor.call('/?domain=stripe.com&fullName=John%20Doe');
603 assert.ok((second.body.items as Record<string, unknown>[]).every((i) => i.cached === true));
604 assert.equal(server.requests.length, 1);
605 } finally {
606 stopped = await actor.stop();
607 }
608 assert.deepEqual(stopped.chargeCounts, { 'tomba-request': 1 });
609 });
610
611 it('keeps serving after a request hits maxResults', async () => {
612 const server = await mock();
613 const actor = await startStandbyActor({ endpoint: server.url });
614 try {
615 const first = await actor.call('/', {
616 body: { requests: [person('a.com'), person('b.com')], maxResults: 1 },
617 });
618 assert.equal((first.body.items as unknown[]).length, 1);
619 const second = await actor.call('/?domain=c.com&firstName=John&lastName=Doe');
620 assert.equal((second.body.items as unknown[]).length, 1);
621 } finally {
622 await actor.stop();
623 }
624 });
625
626 it('rejects invalid input with 400 and unknown paths with 404', async () => {
627 const server = await mock();
628 const actor = await startStandbyActor({ endpoint: server.url });
629 try {
630 assert.equal((await actor.call('/', { body: {} })).status, 400);
631 assert.equal((await actor.call('/', { body: 'not json' })).status, 400);
632 assert.equal((await actor.call('/?maxResults=abc&domain=a.com')).status, 400);
633 assert.equal((await actor.call('/?enrichMobile=maybe&domain=a.com&fullName=John%20Doe')).status, 400);
634 assert.equal((await actor.call('/?domain=a.com&fullName=John%20Doe&webhookUrl=ftp://x')).status, 400);
635 assert.equal((await actor.call('/nope')).status, 404);
636 assert.equal((await actor.call('/', { method: 'DELETE' })).status, 405);
637 assert.equal(server.requests.length, 0);
638 } finally {
639 await actor.stop();
640 }
641 });
642
643 it('returns 402 once the max charge limit is reached', async () => {
644 const server = await mock();
645 const actor = await startStandbyActor({ endpoint: server.url, maxTotalChargeUsd: 1 });
646 try {
647 const first = await actor.call('/?domain=a.com&firstName=John&lastName=Doe');
648 assert.equal(first.status, 200);
649 const second = await actor.call('/?domain=b.com&firstName=John&lastName=Doe');
650 assert.equal(second.status, 402);
651 assert.equal(server.requests.length, 1);
652 } finally {
653 await actor.stop();
654 }
655 });
656});