From 7b2d541cacb5ba9e95deb93ee210e5ae0f5b0f39 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sat, 4 Feb 2023 14:58:02 +0100 Subject: [PATCH] fix(backend): Use regex to parse version Signed-off-by: Tilman Vatteroth --- backend/src/utils/serverVersion.spec.ts | 91 ++++++++++++++++++++----- backend/src/utils/serverVersion.ts | 61 ++++++++++------- 2 files changed, 112 insertions(+), 40 deletions(-) diff --git a/backend/src/utils/serverVersion.spec.ts b/backend/src/utils/serverVersion.spec.ts index aa17ccfea..096f3cc85 100644 --- a/backend/src/utils/serverVersion.spec.ts +++ b/backend/src/utils/serverVersion.spec.ts @@ -5,23 +5,80 @@ */ import { promises as fs } from 'fs'; -import { getServerVersionFromPackageJson } from './serverVersion'; +import { + clearCachedVersion, + getServerVersionFromPackageJson, +} from './serverVersion'; -it('getServerVersionFromPackageJson works', async () => { - const major = 2; - const minor = 0; - const patch = 0; - const preRelease = 'dev'; - /* eslint-disable @typescript-eslint/require-await*/ - jest.spyOn(fs, 'readFile').mockImplementationOnce(async (_) => { - return `{ -"version": "${major}.${minor}.${patch}" -} -`; +jest.mock('fs', () => ({ + promises: { + readFile: jest.fn(), + }, +})); + +describe('getServerVersionFromPackageJson', () => { + afterEach(() => { + clearCachedVersion(); + }); + + it('parses a complete version string', async () => { + const major = 2; + const minor = 0; + const patch = 0; + const preRelease = 'dev'; + jest + .spyOn(fs, 'readFile') + .mockImplementationOnce( + async (_) => + `{ "version": "${major}.${minor}.${patch}-${preRelease}" }`, + ); + const serverVersion = await getServerVersionFromPackageJson(); + expect(serverVersion.major).toEqual(major); + expect(serverVersion.minor).toEqual(minor); + expect(serverVersion.patch).toEqual(patch); + expect(serverVersion.preRelease).toEqual(preRelease); + }); + + it('parses a version string without pre release', async () => { + const major = 2; + const minor = 0; + const patch = 0; + jest + .spyOn(fs, 'readFile') + .mockImplementationOnce( + async (_) => `{ "version": "${major}.${minor}.${patch}" }`, + ); + const serverVersion = await getServerVersionFromPackageJson(); + expect(serverVersion.major).toEqual(major); + expect(serverVersion.minor).toEqual(minor); + expect(serverVersion.patch).toEqual(patch); + expect(serverVersion.preRelease).toEqual(undefined); + }); + + it("throws an error if package.json can't be found", async () => { + jest.spyOn(fs, 'readFile').mockImplementationOnce(async (_) => { + throw new Error('package.json not found'); + }); + await expect(getServerVersionFromPackageJson()).rejects.toThrow( + 'package.json not found', + ); + }); + + it("throws an error if version isn't present in package.json", async () => { + jest.spyOn(fs, 'readFile').mockImplementationOnce(async (_) => `{ }`); + await expect(getServerVersionFromPackageJson()).rejects.toThrow( + 'No version found in root package.json', + ); + }); + + it('throws an error if the version is malformed', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementationOnce( + async (_) => `{ "version": "TwoDotZeroDotZero" }`, + ); + await expect(getServerVersionFromPackageJson()).rejects.toThrow( + 'Version from package.json is malformed. Got TwoDotZeroDotZero', + ); }); - const serverVersion = await getServerVersionFromPackageJson(); - expect(serverVersion.major).toEqual(major); - expect(serverVersion.minor).toEqual(minor); - expect(serverVersion.patch).toEqual(patch); - expect(serverVersion.preRelease).toEqual(preRelease); }); diff --git a/backend/src/utils/serverVersion.ts b/backend/src/utils/serverVersion.ts index f73779965..b96af4244 100644 --- a/backend/src/utils/serverVersion.ts +++ b/backend/src/utils/serverVersion.ts @@ -3,36 +3,51 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { Optional } from '@mrdrogdrog/optional'; import { promises as fs } from 'fs'; import { join as joinPath } from 'path'; import { ServerVersion } from '../monitoring/server-status.dto'; -let versionCache: ServerVersion; +let versionCache: ServerVersion | undefined = undefined; +/** + * Reads the HedgeDoc version from the root package.json. This is done only once per run. + * + * @return {Promise} A Promise that contains the parsed server version. + * @throws {Error} if the package.json couldn't be found or doesn't contain a correct version. + */ export async function getServerVersionFromPackageJson(): Promise { - if (versionCache === undefined) { - const rawFileContent: string = await fs.readFile( - joinPath(__dirname, '../../package.json'), - { encoding: 'utf8' }, - ); - // TODO: Should this be validated in more detail? - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const packageInfo: { version: string } = JSON.parse(rawFileContent); - const versionParts: number[] = packageInfo.version - .split('.') - .map((x) => parseInt(x, 10)); - const preRelease = 'dev'; // TODO: Replace this? - versionCache = { - major: versionParts[0], - minor: versionParts[1], - patch: versionParts[2], - preRelease: preRelease, - fullString: `${versionParts[0]}.${versionParts[1]}.${versionParts[2]}${ - preRelease ? '-' + preRelease : '' - }`, - }; + if (!versionCache) { + versionCache = await parseVersionFromPackageJson(); } - return versionCache; } + +async function parseVersionFromPackageJson(): Promise { + const rawFileContent: string = await fs.readFile( + joinPath(__dirname, '../../../package.json'), + { encoding: 'utf8' }, + ); + const packageInfo = JSON.parse(rawFileContent) as { version: string }; + const versionParts = Optional.ofNullable(packageInfo.version) + .orThrow(() => new Error('No version found in root package.json')) + .map((version) => /^(\d+).(\d+).(\d+)(?:-(\w+))?$/g.exec(version)) + .orElseThrow( + () => + new Error( + `Version from package.json is malformed. Got ${packageInfo.version}`, + ), + ); + return { + major: parseInt(versionParts[1]), + minor: parseInt(versionParts[2]), + patch: parseInt(versionParts[3]), + preRelease: versionParts[4], + fullString: versionParts[0], + }; +} + +export function clearCachedVersion(): void { + versionCache = undefined; +}