@prosopo/ipinfo 0.2.40 → 0.3.0

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 (52) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +12 -10
  2. package/.turbo/turbo-build$colon$tsc.log +10 -10
  3. package/.turbo/turbo-build.log +14 -11
  4. package/CHANGELOG.md +19 -0
  5. package/dist/IpInfoService.d.ts +8 -1
  6. package/dist/IpInfoService.d.ts.map +1 -1
  7. package/dist/IpInfoService.js +77 -84
  8. package/dist/IpInfoService.js.map +1 -1
  9. package/dist/_virtual/_rolldown/runtime.js +3 -0
  10. package/dist/backends/ipapi.d.ts +6 -0
  11. package/dist/backends/ipapi.d.ts.map +1 -1
  12. package/dist/backends/ipapi.js +117 -104
  13. package/dist/backends/ipapi.js.map +1 -1
  14. package/dist/backends/maxmind.d.ts +4 -0
  15. package/dist/backends/maxmind.d.ts.map +1 -1
  16. package/dist/backends/maxmind.js +126 -145
  17. package/dist/backends/maxmind.js.map +1 -1
  18. package/dist/cjs/IpInfoService.cjs +80 -86
  19. package/dist/cjs/backends/ipapi.cjs +118 -104
  20. package/dist/cjs/backends/maxmind.cjs +124 -165
  21. package/dist/cjs/index.cjs +7 -3
  22. package/dist/index.d.ts +6 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/tests/ipInfoService.unit.test.d.ts +2 -0
  27. package/dist/tests/ipInfoService.unit.test.d.ts.map +1 -0
  28. package/dist/tests/ipInfoService.unit.test.js +244 -0
  29. package/dist/tests/ipInfoService.unit.test.js.map +1 -0
  30. package/dist/tests/ipapi.unit.test.d.ts +2 -0
  31. package/dist/tests/ipapi.unit.test.d.ts.map +1 -0
  32. package/dist/tests/ipapi.unit.test.js +337 -0
  33. package/dist/tests/ipapi.unit.test.js.map +1 -0
  34. package/dist/tests/ipinfo.test-d.d.ts +2 -0
  35. package/dist/tests/ipinfo.test-d.d.ts.map +1 -0
  36. package/dist/tests/ipinfo.test-d.js +97 -0
  37. package/dist/tests/ipinfo.test-d.js.map +1 -0
  38. package/dist/tests/maxmind.unit.test.d.ts +2 -0
  39. package/dist/tests/maxmind.unit.test.d.ts.map +1 -0
  40. package/dist/tests/maxmind.unit.test.js +391 -0
  41. package/dist/tests/maxmind.unit.test.js.map +1 -0
  42. package/package.json +10 -8
  43. package/src/IpInfoService.ts +19 -2
  44. package/src/backends/ipapi.ts +57 -10
  45. package/src/backends/maxmind.ts +22 -4
  46. package/src/index.ts +9 -1
  47. package/src/tests/ipInfoService.unit.test.ts +422 -0
  48. package/src/tests/ipapi.unit.test.ts +509 -0
  49. package/src/tests/ipinfo.test-d.ts +189 -0
  50. package/src/tests/maxmind.unit.test.ts +562 -0
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/vite.test.config.ts +18 -0
@@ -0,0 +1,562 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // Testing strategy: the reader is the seam. Shipping a real .mmdb fixture would
16
+ // make these tests depend on MaxMind's data staying constant, and would leave
17
+ // the failure paths — a missing database, a corrupt one, an IP absent from it —
18
+ // impossible to reach at all. The reader is therefore injected, and everything
19
+ // the backend does with its output runs for real.
20
+
21
+ import type { Asn, City, ReaderModel } from "@maxmind/geoip2-node";
22
+ import type { IPInfoResult } from "@prosopo/types";
23
+ import { describe, expect, it, vi } from "vitest";
24
+ import {
25
+ MaxMindBackend,
26
+ type MaxMindUserType,
27
+ type OpenReader,
28
+ } from "../backends/maxmind.js";
29
+
30
+ const IP = "8.8.8.8";
31
+ const CITY_DB = "/dbs/GeoLite2-City.mmdb";
32
+ const ASN_DB = "/dbs/GeoLite2-ASN.mmdb";
33
+
34
+ /** Builds a reader that answers city/asn lookups however the test needs. */
35
+ const reader = (behaviour: {
36
+ city?: () => City;
37
+ asn?: () => Asn;
38
+ }): ReaderModel => {
39
+ const notSupported = (): never => {
40
+ // The real Reader throws when asked for a database it did not open.
41
+ throw new Error("database does not support this lookup");
42
+ };
43
+ return {
44
+ city: behaviour.city ?? notSupported,
45
+ asn: behaviour.asn ?? notSupported,
46
+ } as unknown as ReaderModel;
47
+ };
48
+
49
+ const cityData = (overrides: Partial<City> = {}): City =>
50
+ ({
51
+ country: { isoCode: "US", names: { en: "United States" } },
52
+ city: { names: { en: "Mountain View" } },
53
+ subdivisions: [{ names: { en: "California" } }],
54
+ location: {
55
+ latitude: 37.4,
56
+ longitude: -122.1,
57
+ timeZone: "America/Los_Angeles",
58
+ },
59
+ traits: {},
60
+ ...overrides,
61
+ }) as City;
62
+
63
+ const asnData = (overrides: Partial<Asn> = {}): Asn =>
64
+ ({
65
+ autonomousSystemNumber: 15169,
66
+ autonomousSystemOrganization: "Google LLC",
67
+ ...overrides,
68
+ }) as Asn;
69
+
70
+ /** An opener that returns the given reader for whichever path it is asked for. */
71
+ const opens = (readers: {
72
+ city?: ReaderModel;
73
+ asn?: ReaderModel;
74
+ }): OpenReader => {
75
+ return async (dbPath: string): Promise<ReaderModel> => {
76
+ const model = dbPath === CITY_DB ? readers.city : readers.asn;
77
+ if (undefined === model) {
78
+ throw new Error(`no database at ${dbPath}`);
79
+ }
80
+ return model;
81
+ };
82
+ };
83
+
84
+ const expectValid = (
85
+ response: Awaited<ReturnType<MaxMindBackend["lookup"]>>,
86
+ ): IPInfoResult => {
87
+ if (!response.isValid) {
88
+ throw new Error(`expected a valid result, got: ${response.error}`);
89
+ }
90
+ return response;
91
+ };
92
+
93
+ describe("MaxMindBackend.initialize", () => {
94
+ it("opens only the databases that are configured", async () => {
95
+ const openReader = vi.fn<OpenReader>(
96
+ opens({ city: reader({ city: () => cityData() }) }),
97
+ );
98
+ const backend = new MaxMindBackend({ cityDbPath: CITY_DB, openReader });
99
+
100
+ await backend.initialize();
101
+
102
+ expect(openReader).toHaveBeenCalledTimes(1);
103
+ expect(openReader).toHaveBeenCalledWith(CITY_DB);
104
+ });
105
+
106
+ it("opens neither database when no paths are configured", async () => {
107
+ const openReader = vi.fn<OpenReader>(opens({}));
108
+ await new MaxMindBackend({ openReader }).initialize();
109
+
110
+ expect(openReader).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it("logs the readers it did open", async () => {
114
+ const info = vi.fn();
115
+ await new MaxMindBackend({
116
+ cityDbPath: CITY_DB,
117
+ asnDbPath: ASN_DB,
118
+ openReader: opens({
119
+ city: reader({ city: () => cityData() }),
120
+ asn: reader({ asn: () => asnData() }),
121
+ }),
122
+ logger: { info, warn: vi.fn(), debug: vi.fn() } as never,
123
+ }).initialize();
124
+
125
+ const messages = info.mock.calls.map(
126
+ (call: unknown[]): string => (call[0] as () => { msg: string })().msg,
127
+ );
128
+ expect(messages).toEqual([
129
+ "MaxMind City reader initialized",
130
+ "MaxMind ASN reader initialized",
131
+ ]);
132
+ });
133
+
134
+ it("keeps the ASN database when the City database fails to open", async () => {
135
+ // Deployments routinely ship one and not the other. A failure to open one
136
+ // must not take the other down with it.
137
+ const backend = new MaxMindBackend({
138
+ cityDbPath: CITY_DB,
139
+ asnDbPath: ASN_DB,
140
+ openReader: opens({ asn: reader({ asn: () => asnData() }) }),
141
+ });
142
+
143
+ await backend.initialize();
144
+
145
+ expect(backend.isAvailable()).toBe(true);
146
+ });
147
+
148
+ it("does not throw when every database fails to open", async () => {
149
+ // A missing .mmdb is a deployment mistake, but it must degrade to an
150
+ // unavailable backend rather than crash the process at startup.
151
+ const backend = new MaxMindBackend({
152
+ cityDbPath: CITY_DB,
153
+ asnDbPath: ASN_DB,
154
+ openReader: opens({}),
155
+ });
156
+
157
+ await expect(backend.initialize()).resolves.toBeUndefined();
158
+ expect(backend.isAvailable()).toBe(false);
159
+ });
160
+
161
+ it("logs a warning for each database it could not open", async () => {
162
+ const warn = vi.fn();
163
+ const backend = new MaxMindBackend({
164
+ cityDbPath: CITY_DB,
165
+ asnDbPath: ASN_DB,
166
+ openReader: opens({}),
167
+ logger: { warn, info: vi.fn(), debug: vi.fn() } as never,
168
+ });
169
+
170
+ await backend.initialize();
171
+
172
+ expect(warn).toHaveBeenCalledTimes(2);
173
+ // The payloads are built lazily, so evaluate them: a broken builder would
174
+ // otherwise only surface while someone is debugging a missing database.
175
+ const messages = warn.mock.calls.map(
176
+ (call: unknown[]): string => (call[0] as () => { msg: string })().msg,
177
+ );
178
+ expect(messages).toEqual([
179
+ "Failed to initialize MaxMind City reader",
180
+ "Failed to initialize MaxMind ASN reader",
181
+ ]);
182
+ });
183
+
184
+ it("is safe to call twice", async () => {
185
+ // Callers may initialize defensively; the second call must not leave the
186
+ // backend worse off than the first.
187
+ const backend = new MaxMindBackend({
188
+ cityDbPath: CITY_DB,
189
+ openReader: opens({ city: reader({ city: () => cityData() }) }),
190
+ });
191
+
192
+ await backend.initialize();
193
+ await backend.initialize();
194
+
195
+ expect(backend.isAvailable()).toBe(true);
196
+ });
197
+ });
198
+
199
+ describe("MaxMindBackend.isAvailable", () => {
200
+ it("is unavailable before initialize, even when paths are configured", async () => {
201
+ // Configured is not the same as ready: the databases are opened from disk
202
+ // asynchronously, so a caller that skips initialize gets nothing.
203
+ const backend = new MaxMindBackend({
204
+ cityDbPath: CITY_DB,
205
+ openReader: opens({ city: reader({ city: () => cityData() }) }),
206
+ });
207
+
208
+ expect(backend.isAvailable()).toBe(false);
209
+ await backend.initialize();
210
+ expect(backend.isAvailable()).toBe(true);
211
+ });
212
+ });
213
+
214
+ describe("MaxMindBackend.lookup", () => {
215
+ const initialised = async (readers: {
216
+ city?: ReaderModel;
217
+ asn?: ReaderModel;
218
+ }): Promise<MaxMindBackend> => {
219
+ const backend = new MaxMindBackend({
220
+ cityDbPath: readers.city ? CITY_DB : undefined,
221
+ asnDbPath: readers.asn ? ASN_DB : undefined,
222
+ openReader: opens(readers),
223
+ });
224
+ await backend.initialize();
225
+ return backend;
226
+ };
227
+
228
+ it("refuses to look up before the databases are open", async () => {
229
+ const backend = new MaxMindBackend({ cityDbPath: CITY_DB });
230
+
231
+ await expect(backend.lookup(IP)).resolves.toEqual({
232
+ isValid: false,
233
+ error: "MaxMind readers not initialized",
234
+ ip: IP,
235
+ });
236
+ });
237
+
238
+ it("maps city data onto the shared result shape", async () => {
239
+ const backend = await initialised({
240
+ city: reader({ city: () => cityData() }),
241
+ });
242
+
243
+ const result = expectValid(await backend.lookup(IP));
244
+
245
+ expect(result).toMatchObject({
246
+ ip: IP,
247
+ isValid: true,
248
+ country: "United States",
249
+ countryCode: "US",
250
+ region: "California",
251
+ city: "Mountain View",
252
+ latitude: 37.4,
253
+ longitude: -122.1,
254
+ timezone: "America/Los_Angeles",
255
+ });
256
+ });
257
+
258
+ it("reports threat indicators the free databases never populate as false", async () => {
259
+ // GeoLite2 does not carry these traits. They must read false rather than
260
+ // undefined, or every consumer has to re-decide what absence means.
261
+ const backend = await initialised({
262
+ city: reader({ city: () => cityData() }),
263
+ });
264
+
265
+ const result = expectValid(await backend.lookup(IP));
266
+
267
+ expect(result.isVPN).toBe(false);
268
+ expect(result.isTor).toBe(false);
269
+ expect(result.isProxy).toBe(false);
270
+ expect(result.isAbuser).toBe(false);
271
+ expect(result.isMobile).toBe(false);
272
+ expect(result.isCrawler).toBe(false);
273
+ });
274
+
275
+ it("reads the anonymity traits when a paid database supplies them", async () => {
276
+ const backend = await initialised({
277
+ city: reader({
278
+ city: () =>
279
+ cityData({
280
+ traits: {
281
+ isAnonymousVpn: true,
282
+ isTorExitNode: true,
283
+ isHostingProvider: true,
284
+ isSatelliteProvider: true,
285
+ },
286
+ } as Partial<City>),
287
+ }),
288
+ });
289
+
290
+ const result = expectValid(await backend.lookup(IP));
291
+
292
+ expect(result.isVPN).toBe(true);
293
+ expect(result.isTor).toBe(true);
294
+ expect(result.isDatacenter).toBe(true);
295
+ expect(result.isSatellite).toBe(true);
296
+ });
297
+
298
+ it("treats a residential proxy as a proxy", async () => {
299
+ // Two distinct traits collapse into one flag; missing either would let a
300
+ // proxied request through unflagged.
301
+ const residential = await initialised({
302
+ city: reader({
303
+ city: () =>
304
+ cityData({ traits: { isResidentialProxy: true } } as Partial<City>),
305
+ }),
306
+ });
307
+ const publicProxy = await initialised({
308
+ city: reader({
309
+ city: () =>
310
+ cityData({ traits: { isPublicProxy: true } } as Partial<City>),
311
+ }),
312
+ });
313
+
314
+ expect(expectValid(await residential.lookup(IP)).isProxy).toBe(true);
315
+ expect(expectValid(await publicProxy.lookup(IP)).isProxy).toBe(true);
316
+ });
317
+
318
+ it("falls back to the ASN database for the AS number", async () => {
319
+ const backend = await initialised({
320
+ city: reader({ city: () => cityData() }),
321
+ asn: reader({ asn: () => asnData() }),
322
+ });
323
+
324
+ const result = expectValid(await backend.lookup(IP));
325
+
326
+ expect(result.asnNumber).toBe(15169);
327
+ expect(result.asnOrganization).toBe("Google LLC");
328
+ });
329
+
330
+ it("prefers the City database's ASN traits over the ASN database", async () => {
331
+ // The City database is the more specific source when it carries traits.
332
+ const backend = await initialised({
333
+ city: reader({
334
+ city: () =>
335
+ cityData({
336
+ traits: {
337
+ autonomousSystemNumber: 111,
338
+ autonomousSystemOrganization: "From City",
339
+ },
340
+ } as Partial<City>),
341
+ }),
342
+ asn: reader({ asn: () => asnData({ autonomousSystemNumber: 222 }) }),
343
+ });
344
+
345
+ const result = expectValid(await backend.lookup(IP));
346
+
347
+ expect(result.asnNumber).toBe(111);
348
+ expect(result.asnOrganization).toBe("From City");
349
+ });
350
+
351
+ it("returns ASN-only data when no City database is configured", async () => {
352
+ const backend = await initialised({
353
+ asn: reader({ asn: () => asnData() }),
354
+ });
355
+
356
+ const result = expectValid(await backend.lookup(IP));
357
+
358
+ expect(result.asnNumber).toBe(15169);
359
+ expect(result.country).toBeUndefined();
360
+ });
361
+
362
+ it("reports no data when the IP is in neither database", async () => {
363
+ // The real Reader throws AddressNotFoundError for an unknown IP. Both
364
+ // lookups failing is not an error — it is simply an absent answer.
365
+ const missing = (): never => {
366
+ throw new Error("address not found");
367
+ };
368
+ const backend = await initialised({
369
+ city: reader({ city: missing }),
370
+ asn: reader({ asn: missing }),
371
+ });
372
+
373
+ await expect(backend.lookup(IP)).resolves.toEqual({
374
+ isValid: false,
375
+ error: "No MaxMind data available for IP",
376
+ ip: IP,
377
+ });
378
+ });
379
+
380
+ it("still answers from the ASN database when the City lookup throws", async () => {
381
+ // Partial data beats no data: an IP present in one database and not the
382
+ // other is common.
383
+ const backend = await initialised({
384
+ city: reader({
385
+ city: (): never => {
386
+ throw new Error("address not found");
387
+ },
388
+ }),
389
+ asn: reader({ asn: () => asnData() }),
390
+ });
391
+
392
+ const result = expectValid(await backend.lookup(IP));
393
+
394
+ expect(result.asnNumber).toBe(15169);
395
+ expect(result.country).toBeUndefined();
396
+ });
397
+
398
+ it("maps a hosting user type onto the provider type", async () => {
399
+ const backend = await initialised({
400
+ city: reader({
401
+ city: () =>
402
+ cityData({ traits: { userType: "hosting" } } as Partial<City>),
403
+ }),
404
+ });
405
+
406
+ expect(expectValid(await backend.lookup(IP)).providerType).toBe("hosting");
407
+ });
408
+
409
+ it("maps every user type MaxMind can emit", async () => {
410
+ // Exhaustive on purpose: each case is a routing decision downstream, and
411
+ // a type silently falling through to undefined is indistinguishable from
412
+ // "no data" at the consumer.
413
+ const forUserType = async (
414
+ userType: string,
415
+ ): Promise<string | undefined> => {
416
+ const backend = await initialised({
417
+ city: reader({
418
+ city: () =>
419
+ cityData({ traits: { userType } } as unknown as Partial<City>),
420
+ }),
421
+ });
422
+ return expectValid(await backend.lookup(IP)).providerType;
423
+ };
424
+
425
+ // Typed as a total Record over the union, so adding a user type to
426
+ // MaxMindUserType without deciding its mapping fails the type check
427
+ // rather than silently falling through to undefined at runtime.
428
+ const expected: Record<MaxMindUserType, string | undefined> = {
429
+ hosting: "hosting",
430
+ content_delivery_network: "hosting",
431
+ college: "education",
432
+ school: "education",
433
+ library: "education",
434
+ government: "government",
435
+ military: "government",
436
+ business: "business",
437
+ residential: "isp",
438
+ cellular: "isp",
439
+ dialup: "isp",
440
+ cafe: "isp",
441
+ traveler: "isp",
442
+ router: "isp",
443
+ // Deliberately unmapped: neither is a provider type the consumers
444
+ // route on, so both reach the `default` arm and report "no data".
445
+ consumer_privacy_network: undefined,
446
+ search_engine_spider: undefined,
447
+ };
448
+
449
+ for (const [userType, providerType] of Object.entries(expected)) {
450
+ expect(await forUserType(userType), userType).toBe(providerType);
451
+ }
452
+ });
453
+
454
+ it("logs each failed database lookup with the IP and the cause", async () => {
455
+ // The log lines are built lazily; if the payload builder is wrong nobody
456
+ // finds out until an incident, so it is evaluated here.
457
+ const debug = vi.fn();
458
+ const missing = (): never => {
459
+ throw new Error("address not found");
460
+ };
461
+ const backend = new MaxMindBackend({
462
+ cityDbPath: CITY_DB,
463
+ asnDbPath: ASN_DB,
464
+ openReader: opens({
465
+ city: reader({ city: missing }),
466
+ asn: reader({ asn: missing }),
467
+ }),
468
+ logger: { debug, warn: vi.fn(), info: vi.fn() } as never,
469
+ });
470
+ await backend.initialize();
471
+
472
+ await backend.lookup(IP);
473
+
474
+ expect(debug).toHaveBeenCalledTimes(2);
475
+ type DebugPayload = { msg: string; data: { ip: string } };
476
+ const payloads: DebugPayload[] = debug.mock.calls.map(
477
+ (call: unknown[]): DebugPayload => (call[0] as () => DebugPayload)(),
478
+ );
479
+ expect(payloads.map((p) => p.msg)).toEqual([
480
+ "MaxMind City lookup failed",
481
+ "MaxMind ASN lookup failed",
482
+ ]);
483
+ expect(payloads.every((p) => p.data.ip === IP)).toBe(true);
484
+ });
485
+
486
+ it("leaves the provider type unset for a user type it does not map", async () => {
487
+ // e.g. "search_engine_spider" — deliberately unmapped rather than forced
488
+ // into a category it does not belong to.
489
+ const backend = await initialised({
490
+ city: reader({
491
+ city: () =>
492
+ cityData({
493
+ traits: { userType: "search_engine_spider" },
494
+ } as unknown as Partial<City>),
495
+ }),
496
+ });
497
+
498
+ expect(expectValid(await backend.lookup(IP)).providerType).toBeUndefined();
499
+ });
500
+
501
+ it("leaves the provider type unset when there is no user type at all", async () => {
502
+ const backend = await initialised({
503
+ city: reader({ city: () => cityData() }),
504
+ });
505
+
506
+ expect(expectValid(await backend.lookup(IP)).providerType).toBeUndefined();
507
+ });
508
+
509
+ it("turns a failure during mapping into an error response", async () => {
510
+ // The per-database try blocks only cover the lookup call itself. A throw
511
+ // while reading the returned record — here a getter that fails, standing
512
+ // in for any malformed decode — happens outside them, and must still not
513
+ // escape to the caller.
514
+ const hostile = {
515
+ get traits(): never {
516
+ throw new Error("corrupt record");
517
+ },
518
+ };
519
+ const backend = await initialised({
520
+ city: reader({ city: () => hostile as unknown as City }),
521
+ });
522
+
523
+ await expect(backend.lookup(IP)).resolves.toEqual({
524
+ isValid: false,
525
+ error: "MaxMind lookup error: corrupt record",
526
+ ip: IP,
527
+ });
528
+ });
529
+
530
+ it("describes a non-Error failure without printing [object Object]", async () => {
531
+ const hostile = {
532
+ get traits(): never {
533
+ // The case under test is a non-Error throw, hence the string.
534
+ throw "just a string";
535
+ },
536
+ };
537
+ const backend = await initialised({
538
+ city: reader({ city: () => hostile as unknown as City }),
539
+ });
540
+
541
+ const response = await backend.lookup(IP);
542
+
543
+ expect(response).toMatchObject({
544
+ isValid: false,
545
+ error: "MaxMind lookup error: just a string",
546
+ });
547
+ });
548
+ });
549
+
550
+ describe("MaxMindBackend default reader", () => {
551
+ it("does not throw when the real database file is missing", async () => {
552
+ // Exercises the un-injected path: the lazy import of the MaxMind reader
553
+ // runs for real, and its failure to open a nonexistent file must be
554
+ // contained exactly like an injected one.
555
+ const backend = new MaxMindBackend({
556
+ cityDbPath: "/nonexistent/prosopo-test/GeoLite2-City.mmdb",
557
+ });
558
+
559
+ await expect(backend.initialize()).resolves.toBeUndefined();
560
+ expect(backend.isAvailable()).toBe(false);
561
+ });
562
+ });