Skip to content
Closed
118 changes: 118 additions & 0 deletions tests/ocl/ocl-cm-provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ describe('OCLConceptMapProvider', () => {
];

const getMock = jest.fn().mockImplementation((url) => {
// {source}/mappings/ — one request returns every mapping the source owns.
// Verified against the live OCL instance: this returns the same set as
// walking concepts and asking each one, without the per-concept fan-out.
// Must precede the '/sources/' branch below, which would otherwise swallow it.
if (url.endsWith('/mappings/') && !url.includes('/concepts/')) {
return Promise.resolve({ data: mappings });
}
// source search — resolve canonical for SourceA
if (url.includes('/sources/') && !url.includes('/concepts/')) {
return Promise.resolve({
Expand Down Expand Up @@ -411,3 +418,114 @@ describe('OCLConceptMapProvider', () => {
});
});
});

// ---------------------------------------------------------------
// $resolveReference integration (#2261)
// ---------------------------------------------------------------
describe('OCLConceptMapProvider $resolveReference integration', () => {
const { OclReferenceResolver } = require('../../tx/ocl/resolve/reference-resolver');

const SEARCH_ENDPOINT = '/orgs/TestOrg/sources/';

function makeProvider({ token = 'Token abc', post } = {}) {
const provider = new OCLConceptMapProvider({ org: 'TestOrg', token });
const httpClient = {
get: jest.fn(async () => ({ data: [] })),
post: post || jest.fn(async () => ({ data: [] }))
};
// The provider captures httpClient at construction, so rebuild the resolver
// on the mock the same way the other tests swap httpClient.
provider.httpClient = httpClient;
provider.referenceResolver = new OclReferenceResolver({
httpClient,
org: 'TestOrg',
token,
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() }
});
return { provider, httpClient };
}

function resolveReferenceReply(url) {
return jest.fn(async () => ({
data: [
{
reference_type: 'canonical',
resolved: true,
request: null,
resolution_url: 'http://x.org/cs',
url_registry_entry: null,
result: { type: 'Source Version', short_code: 'S', url }
}
]
}));
}

function getPaths(httpClient) {
return httpClient.get.mock.calls.map(call => call[0]);
}

const searchParams = [{ name: 'source-system', value: 'http://x.org/cs' }];

it('uses the resolved repo and skips the heuristic source search', async () => {
const { provider, httpClient } = makeProvider({
post: resolveReferenceReply('/users/joe/sources/S/HEAD/')
});

await provider.searchConceptMaps(searchParams);

expect(httpClient.post).toHaveBeenCalledTimes(1);
// The authoritative answer makes the q= text search unnecessary.
expect(getPaths(httpClient)).not.toContain(SEARCH_ENDPOINT);
// ...and a user-owned repo survives, which the old /orgs/-only filter dropped.
expect(getPaths(httpClient)).toContain('/users/joe/sources/S/HEAD/mappings/');
});

// Regression: searchConceptMaps used to lower-case every param value. The old text
// search tolerated it (#norm lower-cases anyway), but $resolveReference matches the
// canonical exactly, so a lower-cased URL never resolved.
it('sends the canonical to $resolveReference with its original casing', async () => {
const { provider, httpClient } = makeProvider({
post: resolveReferenceReply('/orgs/Mangara/sources/S/HEAD/')
});

await provider.searchConceptMaps([
{ name: 'source-system', value: 'https://mangara.hsl.org.br/fhir/CodeSystem/AlcoolSPA_uso_Mangara' }
]);

expect(httpClient.post).toHaveBeenCalledTimes(1);
expect(httpClient.post.mock.calls[0][1]).toEqual([
'https://mangara.hsl.org.br/fhir/CodeSystem/AlcoolSPA_uso_Mangara'
]);
});

it('falls back to the source search when no token is configured', async () => {
const { provider, httpClient } = makeProvider({ token: null });

await provider.searchConceptMaps(searchParams);

expect(httpClient.post).not.toHaveBeenCalled();
expect(getPaths(httpClient)).toContain(SEARCH_ENDPOINT);
});

it('falls back to the source search when OCL cannot resolve the canonical', async () => {
const post = jest.fn(async () => ({
data: [{ reference_type: 'canonical', resolved: false, result: null }]
}));
const { provider, httpClient } = makeProvider({ post });

await provider.searchConceptMaps(searchParams);

expect(post).toHaveBeenCalledTimes(1);
expect(getPaths(httpClient)).toContain(SEARCH_ENDPOINT);
});

it('falls back to the source search when $resolveReference is unavailable', async () => {
const error = new Error('Request failed with status code 404');
error.response = { status: 404 };
const { provider, httpClient } = makeProvider({ post: jest.fn().mockRejectedValue(error) });

await provider.searchConceptMaps(searchParams);

expect(getPaths(httpClient)).toContain(SEARCH_ENDPOINT);
});
});
Loading
Loading