From a3b084f8275fc6505dce52415de7226502036148 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Sun, 17 May 2026 13:24:15 +0530 Subject: [PATCH 1/2] fix(version-check): validate registry response and clean up getUpdateType - Parse registry response as unknown and validate 'version' field. - Resolve duplicate getUpdateType declarations. - Export semver utilities for testing. - Add unit tests for version comparison and update logic. --- src/__tests__/version-check.test.ts | 94 +++++++++++++++++++++++++++++ src/utils/version-check.ts | 21 +++---- 2 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/version-check.test.ts diff --git a/src/__tests__/version-check.test.ts b/src/__tests__/version-check.test.ts new file mode 100644 index 0000000..41e4ec7 --- /dev/null +++ b/src/__tests__/version-check.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { compareSemver, getUpdateType, checkForUpdates } from '../utils/version-check'; +import { logger } from '../utils/logger'; + +vi.mock('../utils/logger', () => ({ + logger: { + error: vi.fn(), + }, +})); + +describe('version-check utilities', () => { + describe('compareSemver', () => { + it('should return "lt" if a < b', () => { + expect(compareSemver('1.0.0', '1.0.1')).toBe('lt'); + expect(compareSemver('1.0.0', '1.1.0')).toBe('lt'); + expect(compareSemver('1.0.0', '2.0.0')).toBe('lt'); + }); + + it('should return "gt" if a > b', () => { + expect(compareSemver('1.0.1', '1.0.0')).toBe('gt'); + expect(compareSemver('1.1.0', '1.0.0')).toBe('gt'); + expect(compareSemver('2.0.0', '1.0.0')).toBe('gt'); + }); + + it('should return "eq" if a === b', () => { + expect(compareSemver('1.0.0', '1.0.0')).toBe('eq'); + expect(compareSemver('v1.0.0', '1.0.0')).toBe('eq'); + }); + }); + + describe('getUpdateType', () => { + it('should return "major" for major updates', () => { + expect(getUpdateType('1.0.0', '2.0.0')).toBe('major'); + }); + + it('should return "minor" for minor updates', () => { + expect(getUpdateType('1.0.0', '1.1.0')).toBe('minor'); + }); + + it('should return "patch" for patch updates', () => { + expect(getUpdateType('1.0.0', '1.0.1')).toBe('patch'); + }); + + it('should return empty string if versions are equal', () => { + expect(getUpdateType('1.0.0', '1.0.0')).toBe(''); + }); + }); + + describe('checkForUpdates', () => { + const fetchMock = vi.fn(); + + beforeEach(() => { + vi.stubGlobal('fetch', fetchMock); + vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + it('should log an error if registry response is invalid', async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ not_a_version: '1.2.3' }), + }); + + await checkForUpdates(); + + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Invalid registry response')); + }); + + it('should handle fetch timeout/error gracefully', async () => { + fetchMock.mockRejectedValue(new Error('Timeout')); + + await checkForUpdates(); + // Should not throw and should not log error (it fails silently in the catch block for network errors) + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('should log update message if a newer version is available', async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ version: '9.9.9' }), + }); + + const consoleSpy = vi.spyOn(console, 'log'); + await checkForUpdates(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('update available!')); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('v9.9.9')); + }); + }); +}); diff --git a/src/utils/version-check.ts b/src/utils/version-check.ts index 618ed32..aa1cf25 100644 --- a/src/utils/version-check.ts +++ b/src/utils/version-check.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; +import { logger } from './logger'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -18,7 +19,7 @@ function getInstalledVersion(): string { } // Compare two semver strings; returns 'lt' if a < b, 'gt' if a > b, 'eq' if equal -function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' { +export function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' { const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number); const [a1, a2, a3] = parse(a); const [b1, b2, b3] = parse(b); @@ -29,13 +30,7 @@ function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' { } // Determine update type for messaging -function getUpdateType(installed: string, latest: string): string { - const cmp = compareSemver(installed, latest); - if (cmp === 'eq') return ''; - const [i1] = installed.replace(/^v/, '').split('.').map(Number); - const [l1] = latest.replace(/^v/, '').split('.').map(Number); - if (l1 > i1) return 'major'; -function getUpdateType(installed: string, latest: string): string { +export function getUpdateType(installed: string, latest: string): string { const cmp = compareSemver(installed, latest); if (cmp === 'eq') return ''; const [i1 = 0, i2 = 0] = installed.replace(/^v/, '').split('.').map(Number); @@ -44,8 +39,6 @@ function getUpdateType(installed: string, latest: string): string { if (l2 > i2) return 'minor'; return 'patch'; } - return 'patch'; -} export async function checkForUpdates(): Promise { try { @@ -54,7 +47,13 @@ export async function checkForUpdates(): Promise { }); if (!response.ok) return; - const data = await response.json() as { version: string }; + const data = (await response.json()) as unknown; + + if (!data || typeof data !== 'object' || !('version' in data) || typeof data.version !== 'string') { + logger.error('Invalid registry response: version not found'); + return; + } + const latestVersion = data.version; const installedVersion = getInstalledVersion(); From f689f9c880808d2baf789c4753cbacb9dbc68e1c Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Sun, 17 May 2026 13:54:57 +0530 Subject: [PATCH 2/2] fix(version-check): improve test isolation and handle downgrades - Update afterEach to use vi.restoreAllMocks(). - Update getUpdateType to return empty string for downgrades. - Add test case for downgrades in getUpdateType. --- src/__tests__/version-check.test.ts | 8 +++++++- src/utils/version-check.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/__tests__/version-check.test.ts b/src/__tests__/version-check.test.ts index 41e4ec7..d5d30c1 100644 --- a/src/__tests__/version-check.test.ts +++ b/src/__tests__/version-check.test.ts @@ -44,6 +44,12 @@ describe('version-check utilities', () => { it('should return empty string if versions are equal', () => { expect(getUpdateType('1.0.0', '1.0.0')).toBe(''); }); + + it('should return empty string for downgrades', () => { + expect(getUpdateType('2.0.0', '1.0.0')).toBe(''); + expect(getUpdateType('1.1.0', '1.0.0')).toBe(''); + expect(getUpdateType('1.0.1', '1.0.0')).toBe(''); + }); }); describe('checkForUpdates', () => { @@ -56,7 +62,7 @@ describe('version-check utilities', () => { afterEach(() => { vi.unstubAllGlobals(); - vi.clearAllMocks(); + vi.restoreAllMocks(); }); it('should log an error if registry response is invalid', async () => { diff --git a/src/utils/version-check.ts b/src/utils/version-check.ts index aa1cf25..528f1f1 100644 --- a/src/utils/version-check.ts +++ b/src/utils/version-check.ts @@ -32,7 +32,7 @@ export function compareSemver(a: string, b: string): 'lt' | 'gt' | 'eq' { // Determine update type for messaging export function getUpdateType(installed: string, latest: string): string { const cmp = compareSemver(installed, latest); - if (cmp === 'eq') return ''; + if (cmp === 'eq' || cmp === 'gt') return ''; const [i1 = 0, i2 = 0] = installed.replace(/^v/, '').split('.').map(Number); const [l1 = 0, l2 = 0] = latest.replace(/^v/, '').split('.').map(Number); if (l1 > i1) return 'major';