Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit tests for registry clients #32

Merged
merged 20 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
45b3cd8
Added final test, minor test title change within DiskBasedPackageCach…
KaelynJefferson Jul 2, 2024
76de76e
Adding unit tests for FHIRRegistryClient
KaelynJefferson Jul 2, 2024
bdf167c
missing mock test parts, styling
KaelynJefferson Jul 2, 2024
803ddfa
prettier fixes to unmodified files
KaelynJefferson Jul 2, 2024
ff3cfd2
Adding DefaultRegistryClient unit tests
KaelynJefferson Jul 2, 2024
892917a
separated constructor tests into own group
KaelynJefferson Jul 10, 2024
50bae16
Adding redundant registry client tests
KaelynJefferson Jul 12, 2024
9eb01a9
adding default registry client tests
KaelynJefferson Jul 12, 2024
a82f35c
adding npm registry client tests, small modification to npm client
KaelynJefferson Jul 12, 2024
3fb2357
Adding additional tests, error checking within client
KaelynJefferson Jul 22, 2024
f45ce99
prettier fix to diskbased client
KaelynJefferson Jul 22, 2024
ffcfade
removing one test in default client
KaelynJefferson Jul 22, 2024
93967ba
cleanup, await expect
KaelynJefferson Jul 22, 2024
c2cf489
cleanup, await expect, some test fixes
KaelynJefferson Jul 22, 2024
47f0078
cleanup, await expect
KaelynJefferson Jul 22, 2024
c5a322e
pull common tests into registryClientHelper, sync up fhir and npm reg…
KaelynJefferson Jul 23, 2024
57ba2bf
clean up tests for registry clients, rename registry utils
KaelynJefferson Aug 6, 2024
7d7b774
comment out logging bug
KaelynJefferson Aug 6, 2024
a68b260
removing multi client test from default registry client
KaelynJefferson Aug 6, 2024
1444dba
removed unnecessary await before log check
KaelynJefferson Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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', () => {
cmoesel marked this conversation as resolved.
Show resolved Hide resolved
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);
});
cmoesel marked this conversation as resolved.
Show resolved Hide resolved

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/);
});
});
});
39 changes: 39 additions & 0 deletions test/registry/DefaultRegistryClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { loggerSpy } from '../testhelpers';
import { DefaultRegistryClient } from '../../src/registry/DefaultRegistryClient';
import { NPMRegistryClient } from '../../src/registry/NPMRegistryClient';
import { FHIRRegistryClient } from '../../src/registry/FHIRRegistryClient';

describe('DefaultRegistryClient', () => {
describe('#constructor', () => {
beforeEach(() => {
loggerSpy.reset();
delete process.env.FPL_REGISTRY;
});

it('should make a client with custom registry when it has been specified', () => {
process.env.FPL_REGISTRY = 'https://custom-registry.example.org';
const defaultClient = new DefaultRegistryClient({ log: loggerSpy.log });
expect(defaultClient.clients).toHaveLength(1);
expect(defaultClient.clients[0]).toHaveProperty(
'endpoint',
'https://custom-registry.example.org'
);
expect(defaultClient.clients[0]).toBeInstanceOf(NPMRegistryClient);
expect(loggerSpy.getLastMessage('info')).toBe(
'Using custom registry specified by FPL_REGISTRY environment variable: https://custom-registry.example.org'
);
});

it('should make a client with fhir registries if no custom registry specified', () => {
const defaultClient = new DefaultRegistryClient({ log: loggerSpy.log });
expect(defaultClient.clients).toHaveLength(2);
expect(defaultClient.clients[0]).toHaveProperty('endpoint', 'https://packages.fhir.org');
expect(defaultClient.clients[0]).toBeInstanceOf(FHIRRegistryClient);
expect(defaultClient.clients[1]).toHaveProperty(
'endpoint',
'https://packages2.fhir.org/packages'
);
expect(defaultClient.clients[1]).toBeInstanceOf(FHIRRegistryClient);
});
});
});
Loading