about-system 0.0.51 → 0.0.66
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/README.md +63 -0
- package/dist/about-system-cli.js +117 -99
- package/dist/about-system-cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/system-info-api-C_y-O4VW.js +2409 -0
- package/dist/system-info-api-C_y-O4VW.js.map +1 -0
- package/dist/system-info-api.js +1 -1
- package/package.json +17 -6
- package/src/about-system-cli.ts +31 -6
- package/src/cache/cache.test.ts +235 -0
- package/src/info/platform.test.ts +190 -0
- package/src/info/settings.ts +38 -10
- package/src/utils/utils.test.ts +218 -0
- package/dist/system-info-api-CChFyamn.js +0 -2396
- package/dist/system-info-api-CChFyamn.js.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Covers the shell and network helpers every info module is
|
|
3
|
+
* built on. Both are written to never throw — `about-system` prints a partial
|
|
4
|
+
* readout rather than crashing when a tool is missing or the network is down —
|
|
5
|
+
* so the failure paths are the point of these tests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
// `command.ts` binds `execSync` as a named ESM import, so spying on the
|
|
12
|
+
// child_process namespace object would not reach it — the module has to be
|
|
13
|
+
// mocked outright.
|
|
14
|
+
const execSync = vi.hoisted(() => vi.fn());
|
|
15
|
+
vi.mock("child_process", () => ({
|
|
16
|
+
execSync,
|
|
17
|
+
default: { execSync },
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const httpsGet = vi.hoisted(() => vi.fn());
|
|
21
|
+
vi.mock("https", () => ({
|
|
22
|
+
get: httpsGet,
|
|
23
|
+
default: { get: httpsGet },
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { commandExists, execCommand } from "./command";
|
|
27
|
+
import { IS_LINUX, IS_MAC, IS_WINDOWS } from "./platform";
|
|
28
|
+
import { fetchIPInfo } from "./network";
|
|
29
|
+
import { DEFAULT_IPINFO_TOKEN } from "../cache/cache-config";
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
execSync.mockReset();
|
|
33
|
+
httpsGet.mockReset();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("platform flags", () => {
|
|
38
|
+
it("identifies exactly one platform", () => {
|
|
39
|
+
expect([IS_WINDOWS, IS_MAC, IS_LINUX].filter(Boolean).length).toBeLessThanOrEqual(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("agrees with the running platform", () => {
|
|
43
|
+
expect(IS_LINUX).toBe(process.platform === "linux");
|
|
44
|
+
expect(IS_MAC).toBe(process.platform === "darwin");
|
|
45
|
+
expect(IS_WINDOWS).toBe(process.platform === "win32");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("execCommand", () => {
|
|
50
|
+
it("returns the command's trimmed output", () => {
|
|
51
|
+
execSync.mockReturnValue(" hello \n");
|
|
52
|
+
expect(execCommand("echo hello")).toBe("hello");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns an empty string instead of throwing when the command fails", () => {
|
|
56
|
+
execSync.mockImplementation(() => {
|
|
57
|
+
throw new Error("command not found");
|
|
58
|
+
});
|
|
59
|
+
expect(execCommand("nope")).toBe("");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("never waits forever on a hung command", () => {
|
|
63
|
+
execSync.mockReturnValue("");
|
|
64
|
+
execCommand("sleep 999");
|
|
65
|
+
|
|
66
|
+
expect(execSync.mock.calls[0][1]).toMatchObject({ timeout: 10_000 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("discards the command's stderr so it cannot corrupt the readout", () => {
|
|
70
|
+
execSync.mockReturnValue("");
|
|
71
|
+
execCommand("noisy");
|
|
72
|
+
|
|
73
|
+
expect((execSync.mock.calls[0][1] as { stdio: string[] }).stdio).toEqual([
|
|
74
|
+
"pipe",
|
|
75
|
+
"pipe",
|
|
76
|
+
"ignore",
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("lets the caller override the default options", () => {
|
|
81
|
+
execSync.mockReturnValue("");
|
|
82
|
+
execCommand("slow", { timeout: 100 });
|
|
83
|
+
|
|
84
|
+
expect(execSync.mock.calls[0][1]).toMatchObject({ timeout: 100 });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("handles a Buffer result from execSync", () => {
|
|
88
|
+
execSync.mockReturnValue(Buffer.from(" buffered \n"));
|
|
89
|
+
expect(execCommand("x")).toBe("buffered");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("commandExists", () => {
|
|
94
|
+
it("reports a command that resolves", () => {
|
|
95
|
+
execSync.mockReturnValue("");
|
|
96
|
+
expect(commandExists("node")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("reports a command that does not resolve", () => {
|
|
100
|
+
execSync.mockImplementation(() => {
|
|
101
|
+
throw new Error("not found");
|
|
102
|
+
});
|
|
103
|
+
expect(commandExists("definitely-not-a-real-command")).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("looks the command up with the platform's own resolver", () => {
|
|
107
|
+
execSync.mockReturnValue("");
|
|
108
|
+
commandExists("git");
|
|
109
|
+
|
|
110
|
+
expect(execSync.mock.calls[0][0]).toBe(IS_WINDOWS ? "where git" : "which git");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("silences the resolver's own output", () => {
|
|
114
|
+
execSync.mockReturnValue("");
|
|
115
|
+
commandExists("git");
|
|
116
|
+
|
|
117
|
+
expect(execSync.mock.calls[0][1]).toMatchObject({ stdio: "ignore" });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** A fake `https.get` whose response body and failure mode a test controls. */
|
|
122
|
+
function stubHttpsGet(behavior: (req: EventEmitter & { setTimeout: (ms: number, fn: () => void) => void; destroy: () => void }, onResponse: (res: EventEmitter) => void) => void) {
|
|
123
|
+
httpsGet.mockImplementation(
|
|
124
|
+
(_url: string, callback: (res: EventEmitter) => void) => {
|
|
125
|
+
const req = Object.assign(new EventEmitter(), {
|
|
126
|
+
setTimeout: vi.fn(),
|
|
127
|
+
destroy: vi.fn(),
|
|
128
|
+
});
|
|
129
|
+
queueMicrotask(() => behavior(req, callback));
|
|
130
|
+
return req;
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
return httpsGet;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Emits a complete response body on the next tick. */
|
|
137
|
+
const respondWith = (body: string) =>
|
|
138
|
+
stubHttpsGet((_req, onResponse) => {
|
|
139
|
+
const res = new EventEmitter();
|
|
140
|
+
onResponse(res);
|
|
141
|
+
res.emit("data", body);
|
|
142
|
+
res.emit("end");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("fetchIPInfo", () => {
|
|
146
|
+
it("parses the ipinfo.io payload", async () => {
|
|
147
|
+
respondWith(JSON.stringify({ ip: "1.2.3.4", city: "San Francisco" }));
|
|
148
|
+
|
|
149
|
+
await expect(fetchIPInfo()).resolves.toEqual({
|
|
150
|
+
ip: "1.2.3.4",
|
|
151
|
+
city: "San Francisco",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("sends the default token when none is given", async () => {
|
|
156
|
+
const get = respondWith("{}");
|
|
157
|
+
await fetchIPInfo();
|
|
158
|
+
|
|
159
|
+
expect(get.mock.calls[0][0]).toBe(
|
|
160
|
+
`https://ipinfo.io/json?token=${DEFAULT_IPINFO_TOKEN}`,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
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
|
+
it("resolves empty rather than rejecting on a malformed body", async () => {
|
|
191
|
+
respondWith("<html>gateway error</html>");
|
|
192
|
+
await expect(fetchIPInfo()).resolves.toEqual({});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("resolves empty rather than rejecting when the request errors", async () => {
|
|
196
|
+
stubHttpsGet((req) => req.emit("error", new Error("ENOTFOUND")));
|
|
197
|
+
await expect(fetchIPInfo()).resolves.toEqual({});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("arms a timeout that abandons the request", async () => {
|
|
201
|
+
let armed: { ms: number; fire: () => void } | undefined;
|
|
202
|
+
httpsGet.mockImplementation(() => {
|
|
203
|
+
const req = Object.assign(new EventEmitter(), {
|
|
204
|
+
setTimeout: (ms: number, fire: () => void) => {
|
|
205
|
+
armed = { ms, fire };
|
|
206
|
+
},
|
|
207
|
+
destroy: vi.fn(),
|
|
208
|
+
});
|
|
209
|
+
return req;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const pending = fetchIPInfo("t", 1234);
|
|
213
|
+
expect(armed?.ms).toBe(1234);
|
|
214
|
+
|
|
215
|
+
armed?.fire();
|
|
216
|
+
await expect(pending).resolves.toEqual({});
|
|
217
|
+
});
|
|
218
|
+
});
|