Skip to content

Commit 81a9b40

Browse files
committed
improve testability of versionCheck.ts
1 parent 700d101 commit 81a9b40

File tree

2 files changed

+194
-22
lines changed

2 files changed

+194
-22
lines changed

src/utils/versionCheck.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import {
3+
isGlobalPackage,
4+
generateUpgradeMessage,
5+
fetchLatestVersion,
6+
getPackageInfo,
7+
checkForUpdates,
8+
} from "./versionCheck.js";
9+
10+
// eslint-disable-next-line max-lines-per-function
11+
describe("versionCheck", () => {
12+
describe("isGlobalPackage", () => {
13+
const originalEnv = process.env;
14+
15+
beforeEach(() => {
16+
process.env = { ...originalEnv };
17+
});
18+
19+
afterEach(() => {
20+
process.env = originalEnv;
21+
});
22+
23+
it("returns true when npm_config_global is set", () => {
24+
process.env.npm_config_global = "true";
25+
expect(isGlobalPackage()).toBe(true);
26+
});
27+
28+
it("returns false when npm_config_global is not set", () => {
29+
delete process.env.npm_config_global;
30+
expect(isGlobalPackage()).toBe(false);
31+
});
32+
});
33+
34+
describe("generateUpgradeMessage", () => {
35+
it("returns null when versions are the same", () => {
36+
expect(generateUpgradeMessage("1.0.0", "1.0.0", "test-package")).toBe(
37+
null,
38+
);
39+
});
40+
41+
it("returns upgrade message when versions differ", () => {
42+
const message = generateUpgradeMessage("1.0.0", "1.1.0", "test-package");
43+
expect(message).toContain("Update available: 1.0.0 → 1.1.0");
44+
expect(message).toContain("Run 'npm install -g test-package' to update");
45+
});
46+
});
47+
48+
describe("fetchLatestVersion", () => {
49+
const mockFetch = vi.fn();
50+
const originalFetch = global.fetch;
51+
52+
beforeEach(() => {
53+
global.fetch = mockFetch;
54+
});
55+
56+
afterEach(() => {
57+
global.fetch = originalFetch;
58+
vi.clearAllMocks();
59+
});
60+
61+
it("returns version when fetch succeeds", async () => {
62+
mockFetch.mockResolvedValueOnce({
63+
ok: true,
64+
json: () => Promise.resolve({ version: "1.1.0" }),
65+
});
66+
67+
const version = await fetchLatestVersion("test-package");
68+
expect(version).toBe("1.1.0");
69+
expect(mockFetch).toHaveBeenCalledWith(
70+
"https://registry.npmjs.org/test-package/latest",
71+
);
72+
});
73+
74+
it("returns null when fetch fails", async () => {
75+
mockFetch.mockResolvedValueOnce({
76+
ok: false,
77+
statusText: "Not Found",
78+
});
79+
80+
const version = await fetchLatestVersion("test-package");
81+
expect(version).toBe(null);
82+
});
83+
});
84+
85+
describe("getPackageInfo", () => {
86+
it("returns package info from package.json", () => {
87+
const info = getPackageInfo();
88+
expect(info).toHaveProperty("name");
89+
expect(info).toHaveProperty("version");
90+
expect(typeof info.name).toBe("string");
91+
expect(typeof info.version).toBe("string");
92+
});
93+
});
94+
95+
describe("checkForUpdates", () => {
96+
const mockFetch = vi.fn();
97+
const originalFetch = global.fetch;
98+
const originalEnv = process.env;
99+
100+
beforeEach(() => {
101+
global.fetch = mockFetch;
102+
process.env = { ...originalEnv };
103+
});
104+
105+
afterEach(() => {
106+
global.fetch = originalFetch;
107+
process.env = originalEnv;
108+
vi.clearAllMocks();
109+
});
110+
111+
it("returns null when not running as global package", async () => {
112+
delete process.env.npm_config_global;
113+
const result = await checkForUpdates();
114+
expect(result).toBe(null);
115+
expect(mockFetch).not.toHaveBeenCalled();
116+
});
117+
118+
it("returns upgrade message when update available", async () => {
119+
process.env.npm_config_global = "true";
120+
mockFetch.mockResolvedValueOnce({
121+
ok: true,
122+
json: () => Promise.resolve({ version: "999.0.0" }), // Much higher version
123+
});
124+
125+
const result = await checkForUpdates();
126+
expect(result).toContain("Update available");
127+
});
128+
});
129+
});

src/utils/versionCheck.ts

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,67 @@ import { createRequire } from "module";
33
import type { PackageJson } from "type-fest";
44

55
const require = createRequire(import.meta.url);
6-
const packageInfo = require("../../package.json") as PackageJson;
76
const logger = new Logger({ name: "version-check" });
87

8+
/**
9+
* Gets the current package info from package.json
10+
*/
11+
export function getPackageInfo(): {
12+
name: string | undefined;
13+
version: string | undefined;
14+
} {
15+
const packageInfo = require("../../package.json") as PackageJson;
16+
return {
17+
name: packageInfo.name,
18+
version: packageInfo.version,
19+
};
20+
}
21+
22+
/**
23+
* Checks if the package is running as a global npm package
24+
*/
25+
export function isGlobalPackage(): boolean {
26+
return !!process.env.npm_config_global;
27+
}
28+
29+
/**
30+
* Fetches the latest version of a package from npm registry
31+
*/
32+
export async function fetchLatestVersion(
33+
packageName: string,
34+
): Promise<string | null> {
35+
try {
36+
const registryUrl = `https://registry.npmjs.org/${packageName}/latest`;
37+
const response = await fetch(registryUrl);
38+
39+
if (!response.ok) {
40+
throw new Error(`Failed to fetch version info: ${response.statusText}`);
41+
}
42+
43+
const data = (await response.json()) as { version: string | undefined };
44+
return data.version ?? null;
45+
} catch (error) {
46+
logger.warn(
47+
"Error fetching latest version:",
48+
error instanceof Error ? error.message : String(error),
49+
);
50+
return null;
51+
}
52+
}
53+
54+
/**
55+
* Generates an upgrade message if versions differ
56+
*/
57+
export function generateUpgradeMessage(
58+
currentVersion: string,
59+
latestVersion: string,
60+
packageName: string,
61+
): string | null {
62+
return currentVersion !== latestVersion
63+
? `Update available: ${currentVersion}${latestVersion}\nRun 'npm install -g ${packageName}' to update`
64+
: null;
65+
}
66+
967
/**
1068
* Checks if a newer version of the package is available on npm.
1169
* Only runs check when package is installed globally.
@@ -15,45 +73,30 @@ const logger = new Logger({ name: "version-check" });
1573
export async function checkForUpdates(): Promise<string | null> {
1674
try {
1775
// Only check for updates if running as global package
18-
if (!process.env.npm_config_global) {
76+
if (!isGlobalPackage()) {
1977
return null;
2078
}
2179

22-
const packageName = packageInfo.name;
23-
const currentVersion = packageInfo.version;
80+
const { name: packageName, version: currentVersion } = getPackageInfo();
2481

2582
if (!packageName || !currentVersion) {
2683
logger.warn("Unable to determine current package name or version");
2784
return null;
2885
}
2986

30-
// Fetch latest version from npm registry
31-
const registryUrl = `https://registry.npmjs.org/${packageName}/latest`;
32-
const response = await fetch(registryUrl);
33-
34-
if (!response.ok) {
35-
throw new Error(`Failed to fetch version info: ${response.statusText}`);
36-
}
37-
38-
const data = (await response.json()) as { version: string | undefined };
39-
const latestVersion = data.version;
87+
const latestVersion = await fetchLatestVersion(packageName);
4088

4189
if (!latestVersion) {
42-
logger.warn("Unable to determine determine latest published version");
90+
logger.warn("Unable to determine latest published version");
4391
return null;
4492
}
4593

46-
// Compare versions
47-
if (currentVersion !== latestVersion) {
48-
return `Update available: ${currentVersion}${latestVersion}\nRun 'npm install -g ${packageName}' to update`;
49-
}
50-
51-
return null;
94+
return generateUpgradeMessage(currentVersion, latestVersion, packageName);
5295
} catch (error) {
5396
// Log error but don't throw to handle gracefully
5497
logger.warn(
5598
"Error checking for updates:",
56-
error instanceof Error ? error.message : String(error)
99+
error instanceof Error ? error.message : String(error),
57100
);
58101
return null;
59102
}

0 commit comments

Comments
 (0)