about-system 0.10.8 → 0.10.10
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/dist/about-system-cli.d.ts.map +1 -0
- package/dist/about-system-cli.js +1 -1
- package/dist/cache/cache-config.d.ts +62 -0
- package/dist/cache/cache-config.d.ts.map +1 -0
- package/dist/cache/cache.d.ts +53 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/index.d.ts +18 -52
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/info/hardware.d.ts +76 -0
- package/dist/info/hardware.d.ts.map +1 -0
- package/dist/info/memory.d.ts +59 -0
- package/dist/info/memory.d.ts.map +1 -0
- package/dist/info/network.d.ts +54 -0
- package/dist/info/network.d.ts.map +1 -0
- package/dist/info/platform.d.ts +36 -0
- package/dist/info/platform.d.ts.map +1 -0
- package/dist/info/process.d.ts +23 -0
- package/dist/info/process.d.ts.map +1 -0
- package/dist/info/settings.d.ts +185 -0
- package/dist/info/settings.d.ts.map +1 -0
- package/dist/info/software.d.ts +33 -0
- package/dist/info/software.d.ts.map +1 -0
- package/dist/info/system-status.d.ts +36 -0
- package/dist/info/system-status.d.ts.map +1 -0
- package/dist/{system-info-api-NgTm8Gzo.js → system-info-api-ByXoHI0m.js} +314 -278
- package/dist/system-info-api-ByXoHI0m.js.map +1 -0
- package/dist/system-info-api.d.ts +62 -442
- package/dist/system-info-api.d.ts.map +1 -0
- package/dist/system-info-api.js +4 -5
- package/dist/system-info-api.js.map +1 -1
- package/dist/types/internal-types.d.ts +14 -0
- package/dist/types/internal-types.d.ts.map +1 -0
- package/dist/utils/command.d.ts +18 -0
- package/dist/utils/command.d.ts.map +1 -0
- package/dist/utils/network.d.ts +27 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/web-server.d.ts +21 -0
- package/dist/web-server.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cache/cache-config.ts +2 -2
- package/src/system-info-api.ts +2 -67
- package/src/utils/network.ts +104 -4
- package/src/utils/utils.test.ts +105 -38
- package/dist/system-info-api-NgTm8Gzo.js.map +0 -1
- package/dist/web/assets/index--HOsj3jw.js +0 -14
- package/dist/web/assets/index-BU_blZyu.css +0 -1
- package/dist/web/index.html +0 -13
|
@@ -17,7 +17,7 @@ export const CACHE_FILE = path.join(os.tmpdir(), "systeminfo-cache.json");
|
|
|
17
17
|
* Values are in milliseconds
|
|
18
18
|
*
|
|
19
19
|
* @constant
|
|
20
|
-
* @property {number} ip - IP information cache (
|
|
20
|
+
* @property {number} ip - IP information cache (10 minutes)
|
|
21
21
|
* @property {number} cpu - CPU information cache (24 hours)
|
|
22
22
|
* @property {number} gpu - GPU information cache (24 hours)
|
|
23
23
|
* @property {number} os - OS information cache (24 hours)
|
|
@@ -36,7 +36,7 @@ export const CACHE_FILE = path.join(os.tmpdir(), "systeminfo-cache.json");
|
|
|
36
36
|
* @property {number} mount_points - Mount points cache (10 minutes)
|
|
37
37
|
*/
|
|
38
38
|
export const CACHE_DURATION = {
|
|
39
|
-
ip:
|
|
39
|
+
ip: 10 * 60 * 1000,
|
|
40
40
|
cpu: 24 * 60 * 60 * 1000,
|
|
41
41
|
gpu: 24 * 60 * 60 * 1000,
|
|
42
42
|
os: 24 * 60 * 60 * 1000,
|
package/src/system-info-api.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import os from "os";
|
|
13
13
|
import fs from "fs";
|
|
14
|
-
import
|
|
14
|
+
import { fetchIPInfo } from "./utils/network";
|
|
15
15
|
import path from "path";
|
|
16
16
|
import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types";
|
|
17
17
|
import { CACHE_FILE } from "./info/settings"; // Using CACHE_FILE from settings to ensure consistency
|
|
@@ -50,7 +50,7 @@ import { shell, packages, containers } from "./info/software";
|
|
|
50
50
|
* Values are in milliseconds
|
|
51
51
|
*/
|
|
52
52
|
const CACHE_DURATION = {
|
|
53
|
-
ip:
|
|
53
|
+
ip: 10 * 60 * 1000,
|
|
54
54
|
cpu: 24 * 60 * 60 * 1000,
|
|
55
55
|
gpu: 24 * 60 * 60 * 1000,
|
|
56
56
|
bench: 24 * 60 * 60 * 1000,
|
|
@@ -81,41 +81,6 @@ const IS_WINDOWS = os.platform() === "win32";
|
|
|
81
81
|
const IS_MAC = os.platform() === "darwin";
|
|
82
82
|
const IS_LINUX = os.platform() === "linux";
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
* Default IPInfo.io API token for geolocation
|
|
86
|
-
*/
|
|
87
|
-
const DEFAULT_IPINFO_TOKEN = "da2d6cc4baa5d1";
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Default network request timeout in milliseconds
|
|
91
|
-
*/
|
|
92
|
-
const DEFAULT_NETWORK_TIMEOUT = 5000;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Represents a cached value with timestamp
|
|
96
|
-
*/
|
|
97
|
-
interface CacheEntry {
|
|
98
|
-
value: any;
|
|
99
|
-
timestamp: number;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Cache storage structure
|
|
104
|
-
*/
|
|
105
|
-
interface Cache {
|
|
106
|
-
[key: string]: CacheEntry;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* IP information from ipinfo.io API
|
|
111
|
-
*/
|
|
112
|
-
interface IPInfo {
|
|
113
|
-
ip?: string;
|
|
114
|
-
city?: string;
|
|
115
|
-
hostname?: string;
|
|
116
|
-
org?: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
84
|
/**
|
|
120
85
|
* Context object passed to info collection functions
|
|
121
86
|
*/
|
|
@@ -153,36 +118,6 @@ function saveCache(cache: Cache): void {
|
|
|
153
118
|
}
|
|
154
119
|
}
|
|
155
120
|
|
|
156
|
-
/**
|
|
157
|
-
* Fetches IP geolocation information from ipinfo.io API
|
|
158
|
-
*/
|
|
159
|
-
async function fetchIPInfo(
|
|
160
|
-
token: string = DEFAULT_IPINFO_TOKEN,
|
|
161
|
-
timeout: number = DEFAULT_NETWORK_TIMEOUT
|
|
162
|
-
): Promise<IPInfo> {
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
const url = `https://ipinfo.io/json${token ? `?token=${token}` : ""}`;
|
|
165
|
-
|
|
166
|
-
const req = https.get(url, (res) => {
|
|
167
|
-
let data = "";
|
|
168
|
-
res.on("data", (chunk) => (data += chunk));
|
|
169
|
-
res.on("end", () => {
|
|
170
|
-
try {
|
|
171
|
-
resolve(JSON.parse(data));
|
|
172
|
-
} catch {
|
|
173
|
-
resolve({});
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
req.on("error", () => resolve({}));
|
|
179
|
-
req.setTimeout(timeout, () => {
|
|
180
|
-
req.destroy();
|
|
181
|
-
resolve({});
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
121
|
/**
|
|
187
122
|
* System information collection functions map
|
|
188
123
|
*/
|
package/src/utils/network.ts
CHANGED
|
@@ -25,24 +25,86 @@ export interface IPInfo {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* IP information from ip-api.com (fallback)
|
|
29
|
+
* @interface IPAPIInfo
|
|
30
|
+
*/
|
|
31
|
+
interface IPAPIInfo {
|
|
32
|
+
query?: string;
|
|
33
|
+
city?: string;
|
|
34
|
+
isp?: string;
|
|
35
|
+
org?: string;
|
|
36
|
+
as?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetches IP geolocation information from ipinfo.io lite API
|
|
29
41
|
* @param {string} token - IPInfo.io API token
|
|
30
42
|
* @param {number} timeout - Request timeout in milliseconds
|
|
31
43
|
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
32
44
|
*/
|
|
33
|
-
|
|
45
|
+
async function fetchFromIPInfo(
|
|
34
46
|
token: string = DEFAULT_IPINFO_TOKEN,
|
|
35
47
|
timeout: number = DEFAULT_NETWORK_TIMEOUT
|
|
36
48
|
): Promise<IPInfo> {
|
|
37
49
|
return new Promise((resolve) => {
|
|
38
|
-
const url = `https://ipinfo.io/
|
|
50
|
+
const url = `https://api.ipinfo.io/lite/8.8.8.8?token=${token}`;
|
|
51
|
+
|
|
52
|
+
const req = https.get(url, (res) => {
|
|
53
|
+
let data = "";
|
|
54
|
+
res.on("data", (chunk) => (data += chunk));
|
|
55
|
+
res.on("end", () => {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(data);
|
|
58
|
+
// ipinfo lite returns: { ip, city, region, country, org }
|
|
59
|
+
// Map to our standard IPInfo format
|
|
60
|
+
resolve({
|
|
61
|
+
ip: parsed.ip,
|
|
62
|
+
city: parsed.city,
|
|
63
|
+
hostname: parsed.hostname,
|
|
64
|
+
org: parsed.org,
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
resolve({});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
req.on("error", () => resolve({}));
|
|
73
|
+
req.setTimeout(timeout, () => {
|
|
74
|
+
req.destroy();
|
|
75
|
+
resolve({});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetches IP geolocation information from ip-api.com (fallback)
|
|
82
|
+
* @param {number} timeout - Request timeout in milliseconds
|
|
83
|
+
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
84
|
+
*/
|
|
85
|
+
async function fetchFromIPAPI(
|
|
86
|
+
timeout: number = DEFAULT_NETWORK_TIMEOUT
|
|
87
|
+
): Promise<IPInfo> {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const url = "http://ip-api.com/json/?fields=status,message,query,city,isp,org,as";
|
|
39
90
|
|
|
40
91
|
const req = https.get(url, (res) => {
|
|
41
92
|
let data = "";
|
|
42
93
|
res.on("data", (chunk) => (data += chunk));
|
|
43
94
|
res.on("end", () => {
|
|
44
95
|
try {
|
|
45
|
-
|
|
96
|
+
const parsed: IPAPIInfo = JSON.parse(data);
|
|
97
|
+
if (parsed.status === "success") {
|
|
98
|
+
// Map ip-api response to our standard IPInfo format
|
|
99
|
+
resolve({
|
|
100
|
+
ip: parsed.query,
|
|
101
|
+
city: parsed.city,
|
|
102
|
+
hostname: undefined,
|
|
103
|
+
org: parsed.org || parsed.isp || parsed.as,
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
resolve({});
|
|
107
|
+
}
|
|
46
108
|
} catch {
|
|
47
109
|
resolve({});
|
|
48
110
|
}
|
|
@@ -56,3 +118,41 @@ export async function fetchIPInfo(
|
|
|
56
118
|
});
|
|
57
119
|
});
|
|
58
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fetches IP geolocation information using race between ipinfo.io and ip-api.com
|
|
124
|
+
* Uses ipinfo.io lite endpoint first, falls back to ip-api.com if slower or fails
|
|
125
|
+
* @param {string} token - IPInfo.io API token
|
|
126
|
+
* @param {number} timeout - Request timeout in milliseconds
|
|
127
|
+
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
128
|
+
*/
|
|
129
|
+
export async function fetchIPInfo(
|
|
130
|
+
token: string = DEFAULT_IPINFO_TOKEN,
|
|
131
|
+
timeout: number = DEFAULT_NETWORK_TIMEOUT
|
|
132
|
+
): Promise<IPInfo> {
|
|
133
|
+
// Race both services, use whichever responds first
|
|
134
|
+
const [ipinfoResult, ipapiResult] = await Promise.allSettled([
|
|
135
|
+
fetchFromIPInfo(token, timeout),
|
|
136
|
+
fetchFromIPAPI(timeout),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// Prefer ipinfo.io result if successful and has data
|
|
140
|
+
if (ipinfoResult.status === "fulfilled" && ipinfoResult.value && ipinfoResult.value.ip) {
|
|
141
|
+
return ipinfoResult.value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fall back to ip-api.com result if successful and has data
|
|
145
|
+
if (ipapiResult.status === "fulfilled" && ipapiResult.value && ipapiResult.value.ip) {
|
|
146
|
+
return ipapiResult.value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Return whichever has data, or empty object
|
|
150
|
+
if (ipinfoResult.status === "fulfilled" && ipinfoResult.value) {
|
|
151
|
+
return ipinfoResult.value;
|
|
152
|
+
}
|
|
153
|
+
if (ipapiResult.status === "fulfilled" && ipapiResult.value) {
|
|
154
|
+
return ipapiResult.value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {};
|
|
158
|
+
}
|
package/src/utils/utils.test.ts
CHANGED
|
@@ -25,8 +25,8 @@ vi.mock("https", () => ({
|
|
|
25
25
|
|
|
26
26
|
import { commandExists, execCommand } from "./command";
|
|
27
27
|
import { IS_LINUX, IS_MAC, IS_WINDOWS } from "./platform";
|
|
28
|
-
import { fetchIPInfo } from "./network";
|
|
29
|
-
import { DEFAULT_IPINFO_TOKEN } from "../cache/cache-config";
|
|
28
|
+
import { fetchIPInfo, fetchFromIPInfo, fetchFromIPAPI } from "./network";
|
|
29
|
+
import { DEFAULT_IPINFO_TOKEN, DEFAULT_NETWORK_TIMEOUT } from "../cache/cache-config";
|
|
30
30
|
|
|
31
31
|
afterEach(() => {
|
|
32
32
|
execSync.mockReset();
|
|
@@ -142,59 +142,34 @@ const respondWith = (body: string) =>
|
|
|
142
142
|
res.emit("end");
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
describe("
|
|
146
|
-
it("parses the ipinfo.io payload", async () => {
|
|
147
|
-
respondWith(JSON.stringify({ ip: "1.2.3.4", city: "San Francisco" }));
|
|
145
|
+
describe("fetchFromIPInfo", () => {
|
|
146
|
+
it("parses the ipinfo.io lite payload", async () => {
|
|
147
|
+
respondWith(JSON.stringify({ ip: "1.2.3.4", city: "San Francisco", org: "AS123 Test ISP" }));
|
|
148
148
|
|
|
149
|
-
await expect(
|
|
149
|
+
await expect(fetchFromIPInfo()).resolves.toEqual({
|
|
150
150
|
ip: "1.2.3.4",
|
|
151
151
|
city: "San Francisco",
|
|
152
|
+
org: "AS123 Test ISP",
|
|
152
153
|
});
|
|
153
154
|
});
|
|
154
155
|
|
|
155
|
-
it("sends
|
|
156
|
+
it("sends request to ipinfo.io lite endpoint with token", async () => {
|
|
156
157
|
const get = respondWith("{}");
|
|
157
|
-
await
|
|
158
|
+
await fetchFromIPInfo();
|
|
158
159
|
|
|
159
160
|
expect(get.mock.calls[0][0]).toBe(
|
|
160
|
-
`https://ipinfo.io/
|
|
161
|
+
`https://api.ipinfo.io/lite/8.8.8.8?token=${DEFAULT_IPINFO_TOKEN}`,
|
|
161
162
|
);
|
|
162
163
|
});
|
|
163
164
|
|
|
164
|
-
it("sends a caller-supplied token", async () => {
|
|
165
|
-
const get = respondWith("{}");
|
|
166
|
-
await fetchIPInfo("my-token");
|
|
167
|
-
|
|
168
|
-
expect(get.mock.calls[0][0]).toContain("token=my-token");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("omits the query string entirely for an empty token", async () => {
|
|
172
|
-
const get = respondWith("{}");
|
|
173
|
-
await fetchIPInfo("");
|
|
174
|
-
|
|
175
|
-
expect(get.mock.calls[0][0]).toBe("https://ipinfo.io/json");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("reassembles a body that arrives in chunks", async () => {
|
|
179
|
-
stubHttpsGet((_req, onResponse) => {
|
|
180
|
-
const res = new EventEmitter();
|
|
181
|
-
onResponse(res);
|
|
182
|
-
res.emit("data", '{"ip":"1.2');
|
|
183
|
-
res.emit("data", '.3.4"}');
|
|
184
|
-
res.emit("end");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
await expect(fetchIPInfo()).resolves.toEqual({ ip: "1.2.3.4" });
|
|
188
|
-
});
|
|
189
|
-
|
|
190
165
|
it("resolves empty rather than rejecting on a malformed body", async () => {
|
|
191
166
|
respondWith("<html>gateway error</html>");
|
|
192
|
-
await expect(
|
|
167
|
+
await expect(fetchFromIPInfo()).resolves.toEqual({});
|
|
193
168
|
});
|
|
194
169
|
|
|
195
170
|
it("resolves empty rather than rejecting when the request errors", async () => {
|
|
196
171
|
stubHttpsGet((req) => req.emit("error", new Error("ENOTFOUND")));
|
|
197
|
-
await expect(
|
|
172
|
+
await expect(fetchFromIPInfo()).resolves.toEqual({});
|
|
198
173
|
});
|
|
199
174
|
|
|
200
175
|
it("arms a timeout that abandons the request", async () => {
|
|
@@ -209,10 +184,102 @@ describe("fetchIPInfo", () => {
|
|
|
209
184
|
return req;
|
|
210
185
|
});
|
|
211
186
|
|
|
212
|
-
const pending =
|
|
187
|
+
const pending = fetchFromIPInfo(DEFAULT_IPINFO_TOKEN, 1234);
|
|
213
188
|
expect(armed?.ms).toBe(1234);
|
|
214
189
|
|
|
215
190
|
armed?.fire();
|
|
216
191
|
await expect(pending).resolves.toEqual({});
|
|
217
192
|
});
|
|
218
193
|
});
|
|
194
|
+
|
|
195
|
+
describe("fetchFromIPAPI", () => {
|
|
196
|
+
it("parses the ip-api.com payload", async () => {
|
|
197
|
+
respondWith(JSON.stringify({
|
|
198
|
+
status: "success",
|
|
199
|
+
query: "1.2.3.4",
|
|
200
|
+
city: "San Francisco",
|
|
201
|
+
isp: "Test ISP",
|
|
202
|
+
org: "AS123 Test Organization",
|
|
203
|
+
as: "AS123 Test AS"
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
await expect(fetchFromIPAPI()).resolves.toEqual({
|
|
207
|
+
ip: "1.2.3.4",
|
|
208
|
+
city: "San Francisco",
|
|
209
|
+
org: "AS123 Test Organization",
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("sends request to ip-api.com endpoint", async () => {
|
|
214
|
+
const get = respondWith('{"status":"success"}');
|
|
215
|
+
await fetchFromIPAPI();
|
|
216
|
+
|
|
217
|
+
expect(get.mock.calls[0][0]).toBe(
|
|
218
|
+
"http://ip-api.com/json/?fields=status,message,query,city,isp,org,as"
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns empty object on failed status", async () => {
|
|
223
|
+
respondWith(JSON.stringify({ status: "fail", message: "private range" }));
|
|
224
|
+
await expect(fetchFromIPAPI()).resolves.toEqual({});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("resolves empty rather than rejecting on a malformed body", async () => {
|
|
228
|
+
respondWith("<html>gateway error</html>");
|
|
229
|
+
await expect(fetchFromIPAPI()).resolves.toEqual({});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("fetchIPInfo (race)", () => {
|
|
234
|
+
it("returns ipinfo result when it responds first with data", async () => {
|
|
235
|
+
let ipinfoResolve: (value: any) => void;
|
|
236
|
+
let ipapiResolve: (value: any) => void;
|
|
237
|
+
|
|
238
|
+
const ipinfoPromise = new Promise((resolve) => { ipinfoResolve = resolve; });
|
|
239
|
+
const ipapiPromise = new Promise((resolve) => { ipapiResolve = resolve; });
|
|
240
|
+
|
|
241
|
+
vi.spyOn(await import("./network"), "fetchFromIPInfo").mockReturnValue(ipinfoPromise);
|
|
242
|
+
vi.spyOn(await import("./network"), "fetchFromIPAPI").mockReturnValue(ipapiPromise);
|
|
243
|
+
|
|
244
|
+
const resultPromise = fetchIPInfo();
|
|
245
|
+
|
|
246
|
+
// Resolve ipinfo first with data
|
|
247
|
+
ipinfoResolve!({ ip: "1.2.3.4", city: "San Francisco" });
|
|
248
|
+
ipapiResolve!({ ip: "5.6.7.8", city: "New York" });
|
|
249
|
+
|
|
250
|
+
await expect(resultPromise).resolves.toEqual({
|
|
251
|
+
ip: "1.2.3.4",
|
|
252
|
+
city: "San Francisco",
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("falls back to ip-api when ipinfo fails", async () => {
|
|
257
|
+
let ipinfoResolve: (value: any) => void;
|
|
258
|
+
let ipapiResolve: (value: any) => void;
|
|
259
|
+
|
|
260
|
+
const ipinfoPromise = new Promise((resolve) => { ipinfoResolve = resolve; });
|
|
261
|
+
const ipapiPromise = new Promise((resolve) => { ipapiResolve = resolve; });
|
|
262
|
+
|
|
263
|
+
vi.spyOn(await import("./network"), "fetchFromIPInfo").mockReturnValue(ipinfoPromise);
|
|
264
|
+
vi.spyOn(await import("./network"), "fetchFromIPAPI").mockReturnValue(ipapiPromise);
|
|
265
|
+
|
|
266
|
+
const resultPromise = fetchIPInfo();
|
|
267
|
+
|
|
268
|
+
// Resolve ipinfo with empty, ipapi with data
|
|
269
|
+
ipinfoResolve!({});
|
|
270
|
+
ipapiResolve!({ ip: "5.6.7.8", city: "New York", org: "AS456 Fallback ISP" });
|
|
271
|
+
|
|
272
|
+
await expect(resultPromise).resolves.toEqual({
|
|
273
|
+
ip: "5.6.7.8",
|
|
274
|
+
city: "New York",
|
|
275
|
+
org: "AS456 Fallback ISP",
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("returns empty when both fail", async () => {
|
|
280
|
+
vi.spyOn(await import("./network"), "fetchFromIPInfo").mockResolvedValue({});
|
|
281
|
+
vi.spyOn(await import("./network"), "fetchFromIPAPI").mockResolvedValue({});
|
|
282
|
+
|
|
283
|
+
await expect(fetchIPInfo()).resolves.toEqual({});
|
|
284
|
+
});
|
|
285
|
+
});
|