From 93f20762ed5f95b886f01eb09b6c20140b2b1f3d Mon Sep 17 00:00:00 2001 From: Ashley Harwood <60303491+ashleythedeveloper@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:35:59 +0000 Subject: [PATCH] chore: pass headers to request (#130) * chore: pass headers to request * chore: update tests * refactor: adjust import lodash for ESM Signed-off-by: Nam Hoang --------- Signed-off-by: Nam Hoang Co-authored-by: Nam Hoang --- .../getting-started.md | 4 +- .../storage/testing.md | 14 +++- .../untp-extensions/rendering.md | 8 +- packages/services/src/vckit.service.ts | 4 +- .../__tests__/httpService.test.ts | 52 +++++++++++++ packages/vc-test-suite/config.ts | 2 +- packages/vc-test-suite/httpService.ts | 12 ++- .../QrlinkEncrypted/qrlink-encrypted.test.ts | 28 +++---- .../render-template-2024.test.ts | 76 ++++++++++--------- 9 files changed, 144 insertions(+), 56 deletions(-) create mode 100644 packages/vc-test-suite/__tests__/httpService.test.ts diff --git a/documentation/docs/test-suites/technical-interoperability/getting-started.md b/documentation/docs/test-suites/technical-interoperability/getting-started.md index d68b2c31..cca8fc2a 100644 --- a/documentation/docs/test-suites/technical-interoperability/getting-started.md +++ b/documentation/docs/test-suites/technical-interoperability/getting-started.md @@ -21,6 +21,8 @@ The Getting Started section covers the following topics for technical interopera **Note**: The preset configuration in the config file (`packages/vc-test-suite/config.ts`) has already been preconfigured to use the services set up with Docker Compose. If you're using the default Docker setup, you may not need to modify these settings. + **Note**: In the config file, you can specify headers to be included in the request for each test suite within the Tier 1 test suite (e.g. `RenderTemplate2024`, `Storage`, etc.). Each test case will inherit these headers from the respective config. This is particularly useful if you need to include an authorization token, such as `Bearer `. + 3. **Usage**: Learn how to run the various components of the technical interoperability test suite and interpret the results. This section will guide you through: - Running the W3C V2 VCDM tests - Executing UNTP extension tests @@ -30,4 +32,4 @@ The Getting Started section covers the following topics for technical interopera By the end of this section, you will have a solid foundation for installing, configuring, and using the UNTP Technical Interoperability Test Suite. You'll be able to validate your UNTP implementation's technical components against the required standards. -If you're using the default Docker setup, most of the configuration has been done for you. However, if you need to customise any settings or are using a different setup, the configuration section of each component will guide you through the necessary steps. \ No newline at end of file +If you're using the default Docker setup, most of the configuration has been done for you. However, if you need to customise any settings or are using a different setup, the configuration section of each component will guide you through the necessary steps. diff --git a/documentation/docs/test-suites/technical-interoperability/storage/testing.md b/documentation/docs/test-suites/technical-interoperability/storage/testing.md index f0397a28..7a6fe976 100644 --- a/documentation/docs/test-suites/technical-interoperability/storage/testing.md +++ b/documentation/docs/test-suites/technical-interoperability/storage/testing.md @@ -43,4 +43,16 @@ To test your Storage implementation, follow these steps: 3. **View Test Results**: - Navigate to `packages/vc-test-suite/reports/index.html` - Open this file in a web browser - - Look for the "Storage Service" section to view your test results \ No newline at end of file + - Look for the "Storage Service" section to view your test results + +- `url`: This is the full URL for the unencrypted storage service endpoint of your storage service. + +- `encryptionUrl`: This is the full URL for the encryption service endpoint of your storage service. + +- `headers`: An object containing any additional HTTP headers required for the request to the storage service. You may need to add headers depending on your implementation. + +- `method`: The HTTP method used to request the verifiable credential from the storage service. In this case, it's set to 'POST'. + +- `additionalParams`: An object containing any additional parameters required for the request to the storage service. + +- `additionalPayload`: An object containing any additional payload required for the request to the storage service. diff --git a/documentation/docs/test-suites/technical-interoperability/untp-extensions/rendering.md b/documentation/docs/test-suites/technical-interoperability/untp-extensions/rendering.md index d6fdd05f..3b5ea429 100644 --- a/documentation/docs/test-suites/technical-interoperability/untp-extensions/rendering.md +++ b/documentation/docs/test-suites/technical-interoperability/untp-extensions/rendering.md @@ -87,7 +87,7 @@ To test your Rendering implementation, follow these steps: testSuites: { RenderTemplate2024: { url: 'http://localhost:3332/agent/renderCredential', - headers: {}, + headers: { Authorization: 'Bearer test123' }, method: 'POST', }, }, @@ -105,3 +105,9 @@ To test your Rendering implementation, follow these steps: - Navigate to `packages/vc-test-suite/reports/index.html` - Open this file in a web browser - Look for the "RenderTemplate2024" section to view your test results + +- `url`: This is the full URL for the credential renderer endpoint of your verifiable credential service. + +- `headers`: An object containing any additional HTTP headers required for the request to the verifiable credential service. You may need to add headers depending on your implementation. + +- `method`: The HTTP method used to request the rendered credential from the verifiable credential service. In this case, it's set to 'POST'. diff --git a/packages/services/src/vckit.service.ts b/packages/services/src/vckit.service.ts index 0c7f3509..d5b585f7 100644 --- a/packages/services/src/vckit.service.ts +++ b/packages/services/src/vckit.service.ts @@ -9,7 +9,7 @@ import { } from '@vckit/core-types'; import { decodeJwt } from 'jose'; import { privateAPI } from './utils/httpService.js'; -import { isPlainObject, every, isString } from 'lodash'; +import _ from 'lodash'; export const contextDefault = [ 'https://www.w3.org/ns/credentials/v2', @@ -208,7 +208,7 @@ export const decodeEnvelopedVC = (vc: VerifiableCredential): UnsignedCredential * @param headers */ const _validateVckitHeaders = (headers: Record) => { - if (!isPlainObject(headers) || !every(headers, isString)) { + if (!_.isPlainObject(headers) || !_.every(headers, (value) => _.isString(value))) { throw new Error('VcKit headers defined in app config must be a plain object with string values'); } }; diff --git a/packages/vc-test-suite/__tests__/httpService.test.ts b/packages/vc-test-suite/__tests__/httpService.test.ts new file mode 100644 index 00000000..1bf75a96 --- /dev/null +++ b/packages/vc-test-suite/__tests__/httpService.test.ts @@ -0,0 +1,52 @@ +import { request } from '../httpService'; +import axios from 'axios'; + +jest.mock('axios'); + +const mockedAxios = axios as jest.Mocked; + +describe('httpService', () => { + beforeEach(() => { + jest.clearAllMocks(); + + const mockRequest = jest.fn().mockResolvedValue({ data: 'mocked response' }); + + mockedAxios.create.mockReturnValue({ + request: mockRequest, + } as any); + }); + + it('should accept a plain object for headers', async () => { + const params = { headers: { test: 'test' } }; + const result = await request(params); + + expect(result).toEqual({ data: 'mocked response' }); + + const mockAxiosInstance = mockedAxios.create.mock.results[0].value; + expect(mockAxiosInstance.request).toHaveBeenCalledTimes(1); + expect(mockAxiosInstance.request).toHaveBeenCalledWith(params); + }); + + it('should throw an error if headers is not a plain object', async () => { + await expect(request({ headers: [] as any })).rejects.toThrow( + 'Headers specified in the config must be a plain object with string values.', + ); + }); + + it('should throw an error if headers contain non-string values', async () => { + await expect(request({ headers: { test: 123 } as any })).rejects.toThrow( + 'Headers specified in the config must be a plain object with string values.', + ); + }); + + it('should work when no headers are provided in params', async () => { + const params = { url: 'test-url' }; + const result = await request(params); + + expect(result).toEqual({ data: 'mocked response' }); + + const mockAxiosInstance = mockedAxios.create.mock.results[0].value; + expect(mockAxiosInstance.request).toHaveBeenCalledTimes(1); + expect(mockAxiosInstance.request).toHaveBeenCalledWith(params); + }); +}); diff --git a/packages/vc-test-suite/config.ts b/packages/vc-test-suite/config.ts index 4713fff7..7afd4551 100644 --- a/packages/vc-test-suite/config.ts +++ b/packages/vc-test-suite/config.ts @@ -8,7 +8,7 @@ export default { }, RenderTemplate2024: { url: 'http://localhost:3332/agent/renderCredential', - headers: {}, + headers: { Authorization: 'Bearer test123' }, method: 'POST', }, Storage: { diff --git a/packages/vc-test-suite/httpService.ts b/packages/vc-test-suite/httpService.ts index 05334e82..8539dda5 100644 --- a/packages/vc-test-suite/httpService.ts +++ b/packages/vc-test-suite/httpService.ts @@ -1,12 +1,22 @@ import axios, { AxiosRequestConfig } from 'axios'; +import _ from 'lodash'; const defaultHeaders = { 'Content-Type': 'application/json', }; export const request = async (params: AxiosRequestConfig) => { + if (params.headers) { + if (!_.isPlainObject(params.headers) || !_.every(params.headers, (value) => _.isString(value))) { + throw new Error('Headers specified in the config must be a plain object with string values.'); + } + } + const instance = axios.create({ - headers: defaultHeaders, + headers: { + ...defaultHeaders, + ...params.headers, + }, }); const response = await instance.request(params); diff --git a/packages/vc-test-suite/tests/QrlinkEncrypted/qrlink-encrypted.test.ts b/packages/vc-test-suite/tests/QrlinkEncrypted/qrlink-encrypted.test.ts index 56992ece..4684d46f 100644 --- a/packages/vc-test-suite/tests/QrlinkEncrypted/qrlink-encrypted.test.ts +++ b/packages/vc-test-suite/tests/QrlinkEncrypted/qrlink-encrypted.test.ts @@ -1,6 +1,6 @@ import { decryptCredential } from '@mock-app/services'; import chai from 'chai'; -import * as config from '../../config'; +import config from '../../config'; import { reportRow, setupMatrix } from '../../helpers'; import { request } from '../../httpService'; import { isURLEncoded, parseQRLink } from './helper'; @@ -8,47 +8,48 @@ import { isURLEncoded, parseQRLink } from './helper'; const expect = chai.expect; describe('QR Link Verification', function () { - const qrLink = config.default.testSuites.QrLinkEncrypted.url; - const method = config.default.testSuites.QrLinkEncrypted.method; + const { url: qrLink, method, headers } = config.testSuites.QrLinkEncrypted; const parsedLink = parseQRLink(qrLink); - setupMatrix.call(this, [config.default.implementationName], 'Implementer'); + setupMatrix.call(this, [config.implementationName], 'Implementer'); - reportRow('QR link MUST be URL encoded', config.default.implementationName, function () { + reportRow('QR link MUST be URL encoded', config.implementationName, function () { const data = isURLEncoded(qrLink); data.should.be.true; }); - reportRow('Verification page link MUST exist and be a string', config.default.implementationName, function () { + reportRow('Verification page link MUST exist and be a string', config.implementationName, function () { expect(parsedLink.verify_app_address).to.be.a('string'); }); - reportRow('Payload MUST exist and be an object', config.default.implementationName, function () { + reportRow('Payload MUST exist and be an object', config.implementationName, function () { expect(parsedLink.q.payload).to.be.an('object'); }); - reportRow('URI MUST exist and be a string', config.default.implementationName, function () { + reportRow('URI MUST exist and be a string', config.implementationName, function () { expect(parsedLink.q.payload.uri).to.be.a('string'); }); - reportRow('URI MUST be resolvable', config.default.implementationName, async function () { + reportRow('URI MUST be resolvable', config.implementationName, async function () { const { data } = await request({ url: parsedLink.q.payload.uri, method, + headers, }); data.should.be.an('object'); }); - reportRow('Hash MUST exist and be a string', config.default.implementationName, function () { + reportRow('Hash MUST exist and be a string', config.implementationName, function () { expect(parsedLink.q.payload.hash).to.be.a('string'); }); - reportRow('Hash MUST match the credential hash', config.default.implementationName, async function () { + reportRow('Hash MUST match the credential hash', config.implementationName, async function () { // TODO: Implement this test case with hash comparison // const res = await request({ // url: parsedLink.q.payload.uri, // method: 'GET', + // headers, // }); // const stringifyVC = decryptCredential({ // ...res.document, @@ -59,14 +60,15 @@ describe('QR Link Verification', function () { // expect(parsedLink.q.payload.hash).to.equal(credentialHash); }); - reportRow('Key exist and be a string', config.default.implementationName, function () { + reportRow('Key exist and be a string', config.implementationName, function () { expect(parsedLink.q.payload.key).to.be.a('string'); }); - reportRow('Key MUST decrypt the encrypted credential', config.default.implementationName, async function () { + reportRow('Key MUST decrypt the encrypted credential', config.implementationName, async function () { const { data } = await request({ url: parsedLink.q.payload.uri, method, + headers, }); const stringifyVC = decryptCredential({ diff --git a/packages/vc-test-suite/tests/RenderTemplate2024/render-template-2024.test.ts b/packages/vc-test-suite/tests/RenderTemplate2024/render-template-2024.test.ts index a0e63ec0..cca56b87 100644 --- a/packages/vc-test-suite/tests/RenderTemplate2024/render-template-2024.test.ts +++ b/packages/vc-test-suite/tests/RenderTemplate2024/render-template-2024.test.ts @@ -1,21 +1,22 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import assert from 'assert'; import chai from 'chai'; -import * as config from '../../config'; +import config from '../../config'; import { reportRow, setupMatrix } from '../../helpers'; import { request } from '../../httpService'; +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); chai.should(); describe('RenderTemplate2024', function () { - const url = config.default.testSuites.RenderTemplate2024.url; - const method = config.default.testSuites.RenderTemplate2024.method; + const { url, method, headers } = config.testSuites.RenderTemplate2024; - setupMatrix.call(this, [config.default.implementationName], 'Implementer'); + setupMatrix.call(this, [config.implementationName], 'Implementer'); reportRow( 'should verify that each item in the documents array that is valid', - config.default.implementationName, + config.implementationName, async () => { // Import the input data for the test from the specified JSON file. const input = require('./input/valid-request-payload-ok.json'); @@ -25,6 +26,7 @@ describe('RenderTemplate2024', function () { method, url, data: input, + headers, }); data.should.eql({ @@ -42,7 +44,7 @@ describe('RenderTemplate2024', function () { reportRow( "should verify that the response fails when the request payload is missing the 'credential' property", - config.default.implementationName, + config.implementationName, async () => { // Import the input data for the test from the specified JSON file. const input = require('./input/missing-credential-fail.json'); @@ -53,6 +55,7 @@ describe('RenderTemplate2024', function () { method, url, data: input, + headers, }), ); }, @@ -60,7 +63,7 @@ describe('RenderTemplate2024', function () { reportRow( 'should verify that the response fails when the request payload is empty', - config.default.implementationName, + config.implementationName, async () => { // Send an empty payload to the service and expect it to fail. await assert.rejects( @@ -68,6 +71,7 @@ describe('RenderTemplate2024', function () { method, url, data: {}, + headers, }), ); }, @@ -75,7 +79,7 @@ describe('RenderTemplate2024', function () { reportRow( "should verify that the 'name' field, if present, is a non-empty string", - config.default.implementationName, + config.implementationName, async () => { // Import the input data for the test from the specified JSON file. const input = require('./input/name-field-empty-ok.json'); @@ -85,6 +89,7 @@ describe('RenderTemplate2024', function () { method, url, data: input, + headers, }); // Verify each item in the returned documents array. @@ -97,35 +102,31 @@ describe('RenderTemplate2024', function () { }, ); - // TODO check grammar - reportRow( - "should successfully render when 'mediaQuery' is not provided", - config.default.implementationName, - async () => { - // Import the input data for the test from the specified JSON file. - const input = require('./input/no-mediaQuery-in-renderMethod-ok.json'); - - // Send the input data to the service to get the rendered document response. - const { data } = await request({ - method, - url, - data: input, - }); - - data.should.eql({ - documents: [ - { - type: 'RenderTemplate2024', - renderedTemplate: 'PHA+SmFuZSBEb2U8L3A+', - }, - ], - }); - }, - ); + reportRow('should render successfully when ‘mediaQuery’ is not provided', config.implementationName, async () => { + // Import the input data for the test from the specified JSON file. + const input = require('./input/no-mediaQuery-in-renderMethod-ok.json'); + + // Send the input data to the service to get the rendered document response. + const { data } = await request({ + method, + url, + data: input, + headers, + }); + + data.should.eql({ + documents: [ + { + type: 'RenderTemplate2024', + renderedTemplate: 'PHA+SmFuZSBEb2U8L3A+', + }, + ], + }); + }); reportRow( 'should successfully render when the request payload contains additional properties', - config.default.implementationName, + config.implementationName, async () => { // Import the input data for the test from the specified JSON file. const input = require('./input/additional-properties-ok.json'); @@ -135,6 +136,7 @@ describe('RenderTemplate2024', function () { method, url, data: input, + headers, }); // Verify each item in the returned documents array. @@ -153,7 +155,7 @@ describe('RenderTemplate2024', function () { reportRow( "should verify that the response fails when the request payload is missing '@context'", - config.default.implementationName, + config.implementationName, async () => { // Import the input data for the test from the specified JSON file. const input = require('./input/missing-@context-fail.json'); @@ -164,6 +166,7 @@ describe('RenderTemplate2024', function () { method, url, data: input, + headers, }), ); }, @@ -171,7 +174,7 @@ describe('RenderTemplate2024', function () { reportRow( "should verify that the response fails when the request payload is missing 'renderMethod'", - config.default.implementationName, + config.implementationName, async () => { // Import the input data for the test from the specified JSON file. const input = require('./input/missing-renderMethod-fail.json'); @@ -182,6 +185,7 @@ describe('RenderTemplate2024', function () { method, url, data: input, + headers, }), ); },