diff --git a/package.json b/package.json index b7e03327..738dec60 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "eslint-plugin-tree-shaking": "^1.10.0", "jest": "^29.0.0", "jest-environment-jsdom": "^29.0.0", + "jest-fetch-mock": "^3.0.3", "node-fetch": "^3.2.10", "playwright": "^1.42.1", "prettier": "^3.0.0", diff --git a/src/hydra/fetchHydra.test.ts b/src/hydra/fetchHydra.test.ts new file mode 100644 index 00000000..a3c2dfd0 --- /dev/null +++ b/src/hydra/fetchHydra.test.ts @@ -0,0 +1,96 @@ +import type { HttpError } from 'react-admin'; +import fetchMock from 'jest-fetch-mock'; +import fetchHydra from './fetchHydra.js'; +import schemaAnalyzer from './schemaAnalyzer.js'; + +fetchMock.enableMocks(); + +const headers = { + 'Content-Type': 'application/ld+json; charset=utf-8', + Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', +}; + +test.each([ + [ + 'ld+json', + { + '@context': '/contexts/ConstraintViolationList', + '@type': 'ConstraintViolationList', + 'hydra:title': 'An error occurred', + 'hydra:description': + 'plainPassword: Password must be at least 6 characters long.', + violations: [ + { + propertyPath: 'plainPassword', + message: 'Password must be at least 6 characters long.', + }, + ], + }, + { plainPassword: 'Password must be at least 6 characters long.' }, + ], + [ + 'problem+json', + { + '@id': '\\/validation_errors\\/6b3befbc-2f01-4ddf-be21-b57898905284', + '@type': 'ConstraintViolationList', + status: 422, + violations: [ + { + propertyPath: 'entitlements', + message: + 'At least one product must be selected if policy is restricted.', + code: '6b3befbc-2f01-4ddf-be21-b57898905284', + }, + ], + detail: + 'entitlements: At least one product must be selected if policy is restricted.', + 'hydra:title': 'An error occurred', + 'hydra:description': + 'entitlements: At least one product must be selected if policy is restricted.', + type: '\\/validation_errors\\/6b3befbc-2f01-4ddf-be21-b57898905284', + title: 'An error occurred', + }, + { + entitlements: + 'At least one product must be selected if policy is restricted.', + }, + ], +])( + '%s violation list expanding', + async (format: string, resBody: object, expected: object) => { + fetchMock.mockResponses( + [ + JSON.stringify(resBody), + { + status: 422, + statusText: '422 Unprocessable Content', + headers: { + ...headers, + 'Content-Type': `application/${format}; charset=utf-8`, + }, + }, + ], + [ + JSON.stringify({ + '@context': { + '@vocab': 'http://localhost/docs.jsonld#', + hydra: 'http://www.w3.org/ns/hydra/core#', + }, + }), + { + status: 200, + statusText: 'OK', + headers, + }, + ], + ); + + let violations; + try { + await fetchHydra(new URL('http://localhost/users')); + } catch (error) { + violations = schemaAnalyzer().getSubmissionErrors(error as HttpError); + } + expect(violations).toStrictEqual(expected); + }, +); diff --git a/src/hydra/fetchHydra.ts b/src/hydra/fetchHydra.ts index e64597c9..80036057 100644 --- a/src/hydra/fetchHydra.ts +++ b/src/hydra/fetchHydra.ts @@ -4,7 +4,7 @@ import { getDocumentationUrlFromHeaders, } from '@api-platform/api-doc-parser'; import jsonld from 'jsonld'; -import type { NodeObject } from 'jsonld'; +import type { ContextDefinition, NodeObject } from 'jsonld'; import type { JsonLdObj } from 'jsonld/jsonld-spec'; import type { HttpClientOptions, HydraHttpClientResponse } from '../types.js'; @@ -54,12 +54,20 @@ function fetchHydra( return response; }); }; + const base = getDocumentationUrlFromHeaders(headers); - return jsonld - .expand(body, { - base: getDocumentationUrlFromHeaders(headers), - documentLoader, - }) + return ( + '@context' in body + ? jsonld.expand(body, { + base, + documentLoader, + }) + : documentLoader(base).then((response) => + jsonld.expand(body, { + expandContext: response.document as ContextDefinition, + }), + ) + ) .then((json) => Promise.reject( new HttpError(