fix(smartupdate): improve update check caching, validation, and error handling
This commit is contained in:
+148
-61
@@ -1,126 +1,213 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import type * as smartnpm from '@push.rocks/smartnpm';
|
||||
import * as smartupdate from '../ts/index.js';
|
||||
|
||||
let testSmartUpdate: smartupdate.SmartUpdate;
|
||||
let silentSmartUpdate: smartupdate.SmartUpdate;
|
||||
const createMockedSmartUpdate = (versionsArg: Record<string, string>) => {
|
||||
const checkedPackages: string[] = [];
|
||||
const testSmartUpdate = new smartupdate.SmartUpdate({
|
||||
logLevel: 'SILENT',
|
||||
noColor: true,
|
||||
cacheDuration: { minutes: 30 },
|
||||
cacheStore: { storeType: 'ephemeral' },
|
||||
});
|
||||
|
||||
testSmartUpdate.npmRegistry.getPackageInfo = async (packageNameArg: string) => {
|
||||
checkedPackages.push(packageNameArg);
|
||||
const version = versionsArg[packageNameArg];
|
||||
if (!version) {
|
||||
throw new Error(`Package not found: ${packageNameArg}`);
|
||||
}
|
||||
return {
|
||||
name: packageNameArg,
|
||||
version,
|
||||
} as unknown as smartnpm.NpmPackage;
|
||||
};
|
||||
|
||||
return { testSmartUpdate, checkedPackages };
|
||||
};
|
||||
|
||||
// Test suite for backward compatibility
|
||||
tap.test('backward compatibility: should create an instance of SmartUpdate', async () => {
|
||||
testSmartUpdate = new smartupdate.SmartUpdate();
|
||||
const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' });
|
||||
expect(testSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate);
|
||||
});
|
||||
|
||||
tap.test('backward compatibility: should check for a npm module using old API', async () => {
|
||||
const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' });
|
||||
const result = await testSmartUpdate.check('lodash', '1.0.5');
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
// Test suite for new modern API
|
||||
tap.test('modern API: should create SmartUpdate with custom options', async () => {
|
||||
silentSmartUpdate = new smartupdate.SmartUpdate({
|
||||
logLevel: 'SILENT',
|
||||
noColor: true,
|
||||
cacheDuration: { minutes: 30 },
|
||||
});
|
||||
expect(silentSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate);
|
||||
});
|
||||
|
||||
tap.test('modern API: checkForUpdate should return rich result object', async () => {
|
||||
const result = await silentSmartUpdate.checkForUpdate({
|
||||
const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' });
|
||||
const result = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'lodash',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
// Verify result structure
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(result.status).toBeTypeOf('string');
|
||||
expect(result.status).toEqual('update-available');
|
||||
expect(result.packageName).toEqual('lodash');
|
||||
expect(result.currentVersion).toEqual('1.0.0');
|
||||
expect(result.latestVersion).toEqual('4.17.21');
|
||||
expect(result.checkTime).toBeInstanceOf(Date);
|
||||
expect(result.cacheHit).toBeTypeOf('boolean');
|
||||
|
||||
// Should have update available (1.0.0 is old)
|
||||
expect(result.status).toEqual('update-available');
|
||||
expect(result.latestVersion).toBeTypeOf('string');
|
||||
expect(result.cacheHit).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('modern API: checkForUpdate with up-to-date version', async () => {
|
||||
const result = await silentSmartUpdate.checkForUpdate({
|
||||
const { testSmartUpdate } = createMockedSmartUpdate({ '@push.rocks/smartversion': '3.1.0' });
|
||||
const result = await testSmartUpdate.checkForUpdate({
|
||||
packageName: '@push.rocks/smartversion',
|
||||
currentVersion: '999.999.999', // Future version
|
||||
currentVersion: '999.999.999',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
expect(result.status).toEqual('up-to-date');
|
||||
expect(result.latestVersion).toBeTypeOf('string');
|
||||
expect(result.latestVersion).toEqual('3.1.0');
|
||||
});
|
||||
|
||||
tap.test('modern API: getLatestVersion utility method', async () => {
|
||||
const latestVersion = await silentSmartUpdate.getLatestVersion('lodash');
|
||||
expect(latestVersion).toBeTypeOf('string');
|
||||
expect(latestVersion).toMatch(/^\d+\.\d+\.\d+/); // Semver format
|
||||
const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' });
|
||||
const latestVersion = await testSmartUpdate.getLatestVersion('lodash');
|
||||
expect(latestVersion).toEqual('4.17.21');
|
||||
});
|
||||
|
||||
tap.test('modern API: error handling for non-existent package', async () => {
|
||||
const result = await silentSmartUpdate.checkForUpdate({
|
||||
const { testSmartUpdate } = createMockedSmartUpdate({});
|
||||
const result = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'this-package-definitely-does-not-exist-12345',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
expect(result.status).toEqual('error');
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
expect(result.error).toBeInstanceOf(smartupdate.PackageNotFoundError);
|
||||
});
|
||||
|
||||
tap.test('modern API: cache strategy works', async () => {
|
||||
// Clear cache first to ensure clean state
|
||||
const testPackage = 'express';
|
||||
try {
|
||||
await silentSmartUpdate.clearCache(testPackage);
|
||||
} catch (e) {
|
||||
// Cache might not exist, that's fine
|
||||
}
|
||||
tap.test('modern API: invalid versions are reported before registry access', async () => {
|
||||
const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ lodash: '4.17.21' });
|
||||
const result = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'lodash',
|
||||
currentVersion: 'not-a-version',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
// First check - should hit registry with 'never' strategy
|
||||
const result1 = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: testPackage,
|
||||
expect(result.status).toEqual('error');
|
||||
expect(result.error).toBeInstanceOf(smartupdate.InvalidVersionError);
|
||||
expect(checkedPackages).toHaveLength(0);
|
||||
});
|
||||
|
||||
tap.test('modern API: invalid package names are reported before registry access', async () => {
|
||||
const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ lodash: '4.17.21' });
|
||||
const result = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'invalid package name',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
expect(result1.cacheHit).toBeFalse();
|
||||
expect(result1.status).toEqual('update-available');
|
||||
expect(result.status).toEqual('error');
|
||||
expect(result.error).toBeInstanceOf(smartupdate.InvalidPackageNameError);
|
||||
expect(checkedPackages).toHaveLength(0);
|
||||
});
|
||||
|
||||
// Do a second check with time-based that will create cache
|
||||
const result2 = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: testPackage,
|
||||
tap.test('modern API: caches update-available results for time-based checks', async () => {
|
||||
const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ express: '5.0.0' });
|
||||
|
||||
const result1 = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'express',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
const result2 = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'express',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
|
||||
// Third immediate check - should use cache with 'always' strategy
|
||||
const result3 = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: testPackage,
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'always',
|
||||
});
|
||||
|
||||
expect(result3.cacheHit).toBeTrue();
|
||||
expect(result3.status).toEqual('check-skipped');
|
||||
// nextCheckTime may or may not be set depending on cache strategy implementation
|
||||
if (result3.nextCheckTime) {
|
||||
expect(result3.nextCheckTime).toBeInstanceOf(Date);
|
||||
}
|
||||
expect(result1.status).toEqual('update-available');
|
||||
expect(result2.status).toEqual('check-skipped');
|
||||
expect(result2.cacheHit).toBeTrue();
|
||||
expect(result2.latestVersion).toEqual('5.0.0');
|
||||
expect(checkedPackages).toHaveLength(1);
|
||||
});
|
||||
|
||||
tap.test('modern API: exports all types and classes', async () => {
|
||||
// Verify exports
|
||||
tap.test('modern API: caches up-to-date results for time-based checks', async () => {
|
||||
const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ express: '5.0.0' });
|
||||
|
||||
const result1 = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'express',
|
||||
currentVersion: '5.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
const result2 = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'express',
|
||||
currentVersion: '5.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
|
||||
expect(result1.status).toEqual('up-to-date');
|
||||
expect(result2.status).toEqual('check-skipped');
|
||||
expect(result2.cacheHit).toBeTrue();
|
||||
expect(result2.latestVersion).toEqual('5.0.0');
|
||||
expect(checkedPackages).toHaveLength(1);
|
||||
});
|
||||
|
||||
tap.test('modern API: cache entries are scoped to the checked current version', async () => {
|
||||
const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ express: '5.0.0' });
|
||||
|
||||
const result1 = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'express',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
const result2 = await testSmartUpdate.checkForUpdate({
|
||||
packageName: 'express',
|
||||
currentVersion: '5.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
|
||||
expect(result1.status).toEqual('update-available');
|
||||
expect(result2.status).toEqual('up-to-date');
|
||||
expect(result2.cacheHit).toBeFalse();
|
||||
expect(checkedPackages).toHaveLength(2);
|
||||
});
|
||||
|
||||
tap.test('modern API: checkForCli returns true for cached known updates', async () => {
|
||||
const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ 'my-cli': '2.0.0' });
|
||||
|
||||
const result1 = await testSmartUpdate.checkForCli('my-cli', '1.0.0');
|
||||
const result2 = await testSmartUpdate.checkForCli('my-cli', '1.0.0');
|
||||
|
||||
expect(result1).toBeTrue();
|
||||
expect(result2).toBeTrue();
|
||||
expect(checkedPackages).toHaveLength(1);
|
||||
});
|
||||
|
||||
tap.test('cache manager: clears package-specific and complete cache data', async () => {
|
||||
const cacheManager = new smartupdate.UpdateCacheManager({
|
||||
durationMs: 60_000,
|
||||
storeType: 'ephemeral',
|
||||
});
|
||||
|
||||
await cacheManager.setCached('one', cacheManager.createCacheStatus('1.0.0'));
|
||||
await cacheManager.setCached('two', cacheManager.createCacheStatus('2.0.0'));
|
||||
|
||||
await cacheManager.clearCache('one');
|
||||
expect(await cacheManager.getCached('one')).toBeNull();
|
||||
expect(await cacheManager.getCached('two')).toBeTypeOf('object');
|
||||
|
||||
await cacheManager.clearCache();
|
||||
expect(await cacheManager.getCached('two')).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('modern API: exports all runtime classes and constants', async () => {
|
||||
expect(smartupdate.SmartUpdate).toBeTypeOf('function');
|
||||
expect(smartupdate.UpdateCacheManager).toBeTypeOf('function');
|
||||
expect(smartupdate.UpdateNotifier).toBeTypeOf('function');
|
||||
expect(smartupdate.RegistryUnavailableError).toBeTypeOf('function');
|
||||
expect(smartupdate.PackageNotFoundError).toBeTypeOf('function');
|
||||
expect(smartupdate.InvalidVersionError).toBeTypeOf('function');
|
||||
expect(smartupdate.InvalidPackageNameError).toBeTypeOf('function');
|
||||
expect(smartupdate.LOG_LEVELS).toBeTypeOf('object');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user