Skip to content

Commit

Permalink
Unit tests for registry clients (#32) and current (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
KaelynJefferson authored and cmoesel committed Dec 2, 2024
1 parent 8e96844 commit 82bbb5c
Show file tree
Hide file tree
Showing 14 changed files with 1,654 additions and 54 deletions.
17 changes: 13 additions & 4 deletions src/cache/DiskBasedPackageCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,20 @@ export class DiskBasedPackageCache implements PackageCache {
let resource = this.lruCache.get(resourcePath);
if (!resource) {
if (/.xml$/i.test(resourcePath)) {
// TODO: Consider error handling
const xml = fs.readFileSync(resourcePath).toString();
resource = this.fhirConverter.xmlToObj(xml);
try {
const xml = fs.readFileSync(resourcePath).toString();
resource = this.fhirConverter.xmlToObj(xml);
} catch {
throw new Error(`Failed to get XML resource at path ${resourcePath}`);
}
} else if (/.json$/i.test(resourcePath)) {
try {
resource = fs.readJSONSync(resourcePath);
} catch {
throw new Error(`Failed to get JSON resource at path ${resourcePath}`);
}
} else {
resource = fs.readJSONSync(resourcePath);
throw new Error(`Failed to find XML or JSON file at path ${resourcePath}`);
}
this.lruCache.set(resourcePath, resource);
}
Expand Down
1 change: 1 addition & 0 deletions src/current/BuildDotFhirDotOrgClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class BuildDotFhirDotOrgClient implements CurrentBuildClient {
if (tarballRes?.status === 200 && tarballRes?.data) {
return tarballRes.data;
}
throw new Error(`Failed to download ${name}#${version} from ${url}`);
}

async getCurrentBuildDate(name: string, branch?: string) {
Expand Down
5 changes: 4 additions & 1 deletion src/errors/InvalidPackageError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export class InvalidPackageError extends Error {
constructor(public packagePath: string, public reason: string) {
constructor(
public packagePath: string,
public reason: string
) {
super(`The package at ${packagePath} is not a valid FHIR package: ${reason}.`);
}
}
5 changes: 4 additions & 1 deletion src/errors/InvalidResourceError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export class InvalidResourceError extends Error {
constructor(public resourcePath: string, public reason: string) {
constructor(
public resourcePath: string,
public reason: string
) {
super(`The resource at ${resourcePath} is not a valid FHIR resource: ${reason}.`);
}
}
46 changes: 4 additions & 42 deletions src/registry/FHIRRegistryClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Readable } from 'stream';
import { maxSatisfying } from 'semver';
import { LogFunction, axiosGet } from '../utils';
import { RegistryClient, RegistryClientOptions } from './RegistryClient';
import { IncorrectWildcardVersionFormatError, LatestVersionUnavailableError } from '../errors';
import { IncorrectWildcardVersionFormatError } from '../errors';
import { lookUpLatestVersion, lookUpLatestPatchVersion } from './utils';

export class FHIRRegistryClient implements RegistryClient {
public endpoint: string;
Expand All @@ -17,9 +17,9 @@ export class FHIRRegistryClient implements RegistryClient {
async download(name: string, version: string): Promise<Readable> {
// Resolve version if necessary
if (version === 'latest') {
version = await this.lookUpLatestVersion(name);
version = await lookUpLatestVersion(this.endpoint, name);
} else if (/^\d+\.\d+\.x$/.test(version)) {
version = await this.lookUpLatestPatchVersion(name, version);
version = await lookUpLatestPatchVersion(this.endpoint, name, version);
} else if (/^\d+\.x$/.test(version)) {
throw new IncorrectWildcardVersionFormatError(name, version);
}
Expand All @@ -34,42 +34,4 @@ export class FHIRRegistryClient implements RegistryClient {
}
throw new Error(`Failed to download ${name}#${version} from ${url}`);
}

private async lookUpLatestVersion(name: string): Promise<string> {
try {
const res = await axiosGet(`${this.endpoint}/${name}`, {
responseType: 'json'
});
if (res?.data?.['dist-tags']?.latest?.length) {
return res.data['dist-tags'].latest;
} else {
throw new LatestVersionUnavailableError(name);
}
} catch {
throw new LatestVersionUnavailableError(name);
}
}

private async lookUpLatestPatchVersion(name: string, version: string): Promise<string> {
if (!/^\d+\.\d+\.x$/.test(version)) {
throw new IncorrectWildcardVersionFormatError(name, version);
}
try {
const res = await axiosGet(`${this.endpoint}/${name}`, {
responseType: 'json'
});
if (res?.data?.versions) {
const versions = Object.keys(res.data.versions);
const latest = maxSatisfying(versions, version);
if (latest == null) {
throw new LatestVersionUnavailableError(name, null, true);
}
return latest;
} else {
throw new LatestVersionUnavailableError(name, null, true);
}
} catch {
throw new LatestVersionUnavailableError(name, null, true);
}
}
}
22 changes: 19 additions & 3 deletions src/registry/NPMRegistryClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Readable } from 'stream';
import { LogFunction, axiosGet } from '../utils';
import { RegistryClient, RegistryClientOptions } from './RegistryClient';
import { IncorrectWildcardVersionFormatError } from '../errors';
import { lookUpLatestVersion, lookUpLatestPatchVersion } from './utils';

export class NPMRegistryClient implements RegistryClient {
public endpoint: string;
Expand All @@ -13,10 +15,24 @@ export class NPMRegistryClient implements RegistryClient {
}

async download(name: string, version: string): Promise<Readable> {
// Resolve version if necessary
if (version === 'latest') {
version = await lookUpLatestVersion(this.endpoint, name);
} else if (/^\d+\.\d+\.x$/.test(version)) {
version = await lookUpLatestPatchVersion(this.endpoint, name, version);
} else if (/^\d+\.x$/.test(version)) {
throw new IncorrectWildcardVersionFormatError(name, version);
}

// Get the manifest information about the package from the registry
const manifestRes = await axiosGet(`${this.endpoint}/${name}`);
// Find the NPM tarball location in the manifest
let url = manifestRes.data?.versions?.[version]?.dist?.tarball;
let url;
try {
const manifestRes = await axiosGet(`${this.endpoint}/${name}`);
// Find the NPM tarball location in the manifest
url = manifestRes.data?.versions?.[version]?.dist?.tarball;
} catch {
// Do nothing. Undefined url handled below.
}
// If tarball URL is not found, fallback to standard NPM approach per
// https://docs.fire.ly/projects/Simplifier/features/api.html#package-server-api
if (!url) {
Expand Down
5 changes: 4 additions & 1 deletion src/registry/RedundantRegistryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { RegistryClient, RegistryClientOptions } from './RegistryClient';

export class RedundantRegistryClient implements RegistryClient {
protected log: LogFunction;
constructor(private clients: RegistryClient[], options: RegistryClientOptions = {}) {
constructor(
public clients: RegistryClient[],
options: RegistryClientOptions = {}
) {
this.log = options.log ?? (() => {});
}

Expand Down
45 changes: 45 additions & 0 deletions src/registry/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IncorrectWildcardVersionFormatError, LatestVersionUnavailableError } from '../errors';
import { axiosGet } from '../utils';
import { maxSatisfying } from 'semver';

export async function lookUpLatestVersion(endpoint: string, name: string): Promise<string> {
try {
const res = await axiosGet(`${endpoint}/${name}`, {
responseType: 'json'
});
if (res?.data?.['dist-tags']?.latest?.length) {
return res.data['dist-tags'].latest;
} else {
throw new LatestVersionUnavailableError(name);
}
} catch {
throw new LatestVersionUnavailableError(name);
}
}

export async function lookUpLatestPatchVersion(
endpoint: string,
name: string,
version: string
): Promise<string> {
if (!/^\d+\.\d+\.x$/.test(version)) {
throw new IncorrectWildcardVersionFormatError(name, version);
}
try {
const res = await axiosGet(`${endpoint}/${name}`, {
responseType: 'json'
});
if (res?.data?.versions) {
const versions = Object.keys(res.data.versions);
const latest = maxSatisfying(versions, version);
if (latest == null) {
throw new LatestVersionUnavailableError(name, null, true);
}
return latest;
} else {
throw new LatestVersionUnavailableError(name, null, true);
}
} catch {
throw new LatestVersionUnavailableError(name, null, true);
}
}
48 changes: 46 additions & 2 deletions test/cache/DiskBasedPackageCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ describe('DiskBasedPackageCache', () => {
expect(loggerSpy.getAllLogs()).toHaveLength(0);
});

it('should return undefined for a package with different version in the cache', () => {
it('should return empty array for a package with different version in the cache', () => {
const potentials = cache.getPotentialResourcePaths('fhir.small', '0.2.0');
expect(potentials).toHaveLength(0);
expect(loggerSpy.getAllLogs()).toHaveLength(0);
Expand Down Expand Up @@ -231,6 +231,50 @@ describe('DiskBasedPackageCache', () => {
});

describe('#getResourceAtPath', () => {
// tests go here
it('should return a resource with a given resource path', () => {
const rootPath = path.resolve(cacheFolder, 'fhir.small#0.1.0', 'package');
const totalPath = path.resolve(rootPath, 'StructureDefinition-MyPatient.json');
const resource = cache.getResourceAtPath(totalPath);
expect(resource).toBeDefined();
expect(resource.id).toBe('MyPatient');
expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
});

it('should return a resource with an xml path where xml was converted to a resource', () => {
const totalPath = path.resolve(local1Folder, 'StructureDefinition-true-false.xml');
const resource = cache.getResourceAtPath(totalPath);
expect(resource).toBeDefined();
expect(resource.id).toBe('true-false');
expect(resource.xml).toBeUndefined();
expect(loggerSpy.getAllLogs('error')).toHaveLength(0);
});

it('should throw error when path points to a xml file that does not exist', () => {
const totalPath = path.resolve(local1Folder, 'example-file-that-doesnt-exist.xml');
expect(() => {
cache.getResourceAtPath(totalPath);
}).toThrow(/Failed to get XML resource at path/);
});

it('should throw error when path points to a json file that does not exist', () => {
const totalPath = path.resolve(local1Folder, 'example-file-that-doesnt-exist.json');
expect(() => {
cache.getResourceAtPath(totalPath);
}).toThrow(/Failed to get JSON resource at path/);
});

it('should throw error when path points to an invalid file type that is not json or xml', () => {
const totalPath = path.resolve(local1Folder, 'example-file-that-doesnt-exist.txt');
expect(() => {
cache.getResourceAtPath(totalPath);
}).toThrow(/Failed to find XML or JSON file/);
});

it('should throw error when path points to a file that does not exist', () => {
const totalPath = path.resolve(local1Folder, '');
expect(() => {
cache.getResourceAtPath(totalPath);
}).toThrow(/Failed to find XML or JSON file/);
});
});
});
Loading

0 comments on commit 82bbb5c

Please sign in to comment.