From 6c0061923a53c99c15e0034b0ec26914df5c9685 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 8 Sep 2025 13:45:54 -0400 Subject: [PATCH] test(file-modification): add unit tests for version comparison methods - Introduced a new test suite for the FileModification class to validate version comparison methods. - Implemented tests for isUnraidVersionGreaterThanOrEqualTo and isUnraidVersionLessThanOrEqualTo, including scenarios for stable and prerelease versions. - Enhanced the compareUnraidVersion method to streamline version comparison logic. --- .../file-modification.spec.ts | 149 ++++++++++++++++++ .../unraid-file-modifier/file-modification.ts | 40 ++++- .../auth-request.modification.ts | 29 +++- 3 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 api/src/unraid-api/unraid-file-modifier/file-modification.spec.ts diff --git a/api/src/unraid-api/unraid-file-modifier/file-modification.spec.ts b/api/src/unraid-api/unraid-file-modifier/file-modification.spec.ts new file mode 100644 index 000000000..185686e6b --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/file-modification.spec.ts @@ -0,0 +1,149 @@ +import { Logger } from '@nestjs/common'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import * as getUnraidVersionModule from '@app/common/dashboard/get-unraid-version.js'; +import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +vi.mock('@app/common/dashboard/get-unraid-version.js'); + +class TestFileModification extends FileModification { + id = 'test'; + filePath = '/test/file'; + + protected async generatePatch(): Promise { + return 'test patch'; + } +} + +describe('FileModification', () => { + let modification: TestFileModification; + let getUnraidVersionMock: any; + + beforeEach(() => { + vi.clearAllMocks(); + const logger = new Logger('TestFileModification'); + modification = new TestFileModification(logger); + getUnraidVersionMock = vi.mocked(getUnraidVersionModule.getUnraidVersion); + }); + + describe('version comparison methods', () => { + describe('isUnraidVersionGreaterThanOrEqualTo', () => { + it('should return true when current version is greater', async () => { + getUnraidVersionMock.mockResolvedValue('7.3.0'); + const result = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0'); + expect(result).toBe(true); + }); + + it('should return true when current version is equal', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0'); + const result = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0'); + expect(result).toBe(true); + }); + + it('should return false when current version is less', async () => { + getUnraidVersionMock.mockResolvedValue('7.1.0'); + const result = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0'); + expect(result).toBe(false); + }); + + it('should handle prerelease versions correctly', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.1'); + const result = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0-beta.1'); + expect(result).toBe(true); + }); + + it('should treat prerelease as greater than stable when base versions are equal', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.1'); + const result = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0', { + includePrerelease: true, + }); + expect(result).toBe(true); + }); + + it('should compare prerelease versions correctly', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.2.4'); + const result = + await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0-beta.2.3'); + expect(result).toBe(true); + }); + + it('should handle beta.2.3 being less than beta.2.4', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.2.3'); + const result = + await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0-beta.2.4'); + expect(result).toBe(false); + }); + }); + + describe('isUnraidVersionLessThanOrEqualTo', () => { + it('should return true when current version is less', async () => { + getUnraidVersionMock.mockResolvedValue('7.1.0'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0'); + expect(result).toBe(true); + }); + + it('should return true when current version is equal', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0'); + expect(result).toBe(true); + }); + + it('should return false when current version is greater', async () => { + getUnraidVersionMock.mockResolvedValue('7.3.0'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0'); + expect(result).toBe(false); + }); + + it('should handle prerelease versions correctly', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.1'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0-beta.1'); + expect(result).toBe(true); + }); + + it('should treat prerelease as less than stable when base versions are equal', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.1'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0', { + includePrerelease: true, + }); + expect(result).toBe(false); + }); + + it('should compare prerelease versions correctly', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.2.3'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0-beta.2.4'); + expect(result).toBe(true); + }); + + it('should handle beta.2.3 being equal to beta.2.3', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.2.3'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0-beta.2.3'); + expect(result).toBe(true); + }); + + it('should handle beta.2.4 being greater than beta.2.3', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0-beta.2.4'); + const result = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0-beta.2.3'); + expect(result).toBe(false); + }); + }); + + describe('inverse relationship', () => { + it('should have opposite results for greater-than-or-equal and less-than-or-equal when not equal', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.5'); + const gte = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0'); + const lte = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0'); + expect(gte).toBe(true); + expect(lte).toBe(false); + }); + + it('should both return true when versions are equal', async () => { + getUnraidVersionMock.mockResolvedValue('7.2.0'); + const gte = await modification['isUnraidVersionGreaterThanOrEqualTo']('7.2.0'); + const lte = await modification['isUnraidVersionLessThanOrEqualTo']('7.2.0'); + expect(gte).toBe(true); + expect(lte).toBe(true); + }); + }); + }); +}); diff --git a/api/src/unraid-api/unraid-file-modifier/file-modification.ts b/api/src/unraid-api/unraid-file-modifier/file-modification.ts index af16882e6..0dcfd0e32 100644 --- a/api/src/unraid-api/unraid-file-modifier/file-modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/file-modification.ts @@ -5,7 +5,7 @@ import { access, readFile, unlink, writeFile } from 'fs/promises'; import { basename, dirname, join } from 'path'; import { applyPatch, createPatch, parsePatch, reversePatch } from 'diff'; -import { coerce, compare, gte } from 'semver'; +import { coerce, compare, gte, lte } from 'semver'; import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version.js'; @@ -259,29 +259,53 @@ export abstract class FileModification { return patch; } - protected async isUnraidVersionGreaterThanOrEqualTo( - version: string = '7.2.0', // Defaults to the version of Unraid that includes the API by default + private async compareUnraidVersion( + version: string, + compareFn: typeof gte | typeof lte, { includePrerelease = true }: { includePrerelease?: boolean } = {} ): Promise { const unraidVersion = coerce(await getUnraidVersion(), { includePrerelease }); const comparedVersion = coerce(version, { includePrerelease }); + if (!unraidVersion) { throw new Error(`Failed to compare Unraid version - missing unraid version`); } if (!comparedVersion) { throw new Error(`Failed to compare Unraid version - missing comparison version`); } - // If includePrerelease and base versions are equal, treat prerelease as greater + + // Special handling for prerelease versions when base versions are equal if (includePrerelease) { const baseUnraid = `${unraidVersion.major}.${unraidVersion.minor}.${unraidVersion.patch}`; const baseCompared = `${comparedVersion.major}.${comparedVersion.minor}.${comparedVersion.patch}`; + if (baseUnraid === baseCompared) { - // If unraidVersion has prerelease and comparedVersion does not, treat as greater - if (unraidVersion.prerelease.length && !comparedVersion.prerelease.length) { - return true; + const unraidHasPrerelease = unraidVersion.prerelease.length > 0; + const comparedHasPrerelease = comparedVersion.prerelease.length > 0; + + // If one has prerelease and the other doesn't, handle specially + if (unraidHasPrerelease && !comparedHasPrerelease) { + // For gte: prerelease is considered greater than stable + // For lte: prerelease is considered less than stable + return compareFn === gte; } } } - return gte(unraidVersion, comparedVersion); + + return compareFn(unraidVersion, comparedVersion); + } + + protected async isUnraidVersionGreaterThanOrEqualTo( + version: string = '7.2.0', // Defaults to the version of Unraid that includes the API by default + { includePrerelease = true }: { includePrerelease?: boolean } = {} + ): Promise { + return this.compareUnraidVersion(version, gte, { includePrerelease }); + } + + protected async isUnraidVersionLessThanOrEqualTo( + version: string, + { includePrerelease = true }: { includePrerelease?: boolean } = {} + ): Promise { + return this.compareUnraidVersion(version, lte, { includePrerelease }); } } diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts index 68f95aa1b..61c4dc81d 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts @@ -2,7 +2,10 @@ import { readFile } from 'fs/promises'; import { join } from 'node:path'; import { fileExists } from '@app/core/utils/files/file-exists.js'; -import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js'; +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; export default class AuthRequestModification extends FileModification { public filePath: string = '/usr/local/emhttp/auth-request.php' as const; @@ -30,6 +33,30 @@ export default class AuthRequestModification extends FileModification { return null; } + /** + * Check if this modification should be applied based on Unraid version + * Only apply for Unraid versions up to 7.2.0-beta.2.3 + */ + async shouldApply(): Promise { + // Apply for versions up to and including 7.2.0-beta.2.3 + const maxVersion = '7.2.0-beta.2.3'; + const isCompatibleVersion = await this.isUnraidVersionLessThanOrEqualTo(maxVersion, { + includePrerelease: true, + }); + + if (!isCompatibleVersion) { + return { + shouldApply: false, + reason: `Auth request modification only applies to Unraid versions up to ${maxVersion}`, + }; + } + + return { + shouldApply: true, + reason: `Auth request modification needed for Unraid version <= ${maxVersion}`, + }; + } + /** * Generate a patch for the auth-request.php file * @param overridePath - The path to override the default file path