From ec8b637d6751534765e1ad68a924d664c30adecd Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 21 Nov 2023 00:16:17 +0000 Subject: [PATCH] chore: improve test coverage and resolve lint warnings --- src/common/getters.test.ts | 334 +++++++++++++----- src/common/getters.ts | 272 +++++++------- .../manage/approveAccessRequest.test.ts | 10 +- src/gConsent/manage/denyAccessRequest.test.ts | 25 +- src/gConsent/manage/getAccessGrant.test.ts | 12 + src/gConsent/util/access.mock.ts | 131 ++----- 6 files changed, 447 insertions(+), 337 deletions(-) diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts index 62343b21c..753eb5ba5 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -20,6 +20,8 @@ // import { it, expect, describe, jest, beforeAll } from "@jest/globals"; +import { Store, DataFactory } from "n3"; +import { promisifyEventEmitter } from "event-emitter-promisify"; import { mockAccessGrantVc as mockGConsentGrant, mockAccessRequestVc as mockGConsentRequest, @@ -27,6 +29,8 @@ import { import { AccessGrantWrapper, getAccessModes, + getConsent, + getCredentialSubject, getExpirationDate, getId, getInherit, @@ -39,6 +43,10 @@ import { getTypes, } from "./getters"; import type { AccessGrant, AccessRequest } from "../gConsent"; +import { GC_FOR_PERSONAL_DATA } from "../gConsent/constants"; +import { TYPE, cred, gc } from "./constants"; + +const { quad, namedNode, literal, blankNode } = DataFactory; jest.mock("@inrupt/universal-fetch", () => ({ fetch: (async (...args) => { @@ -47,47 +55,140 @@ jest.mock("@inrupt/universal-fetch", () => ({ })); describe("getters", () => { - let gConsentGrant: AccessGrant; - let gConsentRequest: AccessRequest; + let mockedGConsentGrant: AccessGrant; + let mockedGConsentRequest: AccessRequest; beforeAll(async () => { - gConsentGrant = await mockGConsentGrant(); - gConsentRequest = await mockGConsentRequest(); + mockedGConsentGrant = await mockGConsentGrant(); + mockedGConsentRequest = await mockGConsentRequest(); }); describe("getResources", () => { it("gets the resources from a gConsent access grant", async () => { - expect(getResources(gConsentGrant)).toEqual( - gConsentGrant.credentialSubject.providedConsent.forPersonalData, + expect(getResources(mockedGConsentGrant)).toEqual( + mockedGConsentGrant.credentialSubject.providedConsent.forPersonalData, + ); + }); + + it("errors when resources are not NamedNodes", async () => { + expect(() => + getResources( + new Store([ + ...mockedGConsentGrant, + quad( + getConsent(mockedGConsentGrant), + namedNode(GC_FOR_PERSONAL_DATA), + literal("hello world"), + ), + ]) as unknown as AccessGrant, + ), + ).toThrow( + "Expected resource to be a Named Node. Instead got [hello world] with term type [Literal]", ); }); it("gets the resources from a gConsent access request", async () => { - expect(getResources(gConsentRequest)).toEqual( - gConsentRequest.credentialSubject.hasConsent.forPersonalData, + expect(getResources(mockedGConsentRequest)).toEqual( + mockedGConsentRequest.credentialSubject.hasConsent.forPersonalData, ); }); }); // FIXME: Add some checks where purposes are actually present describe("getPurposes", () => { - it("gets the purposes from a gConsent access grant", async () => { - expect(getPurposes(gConsentGrant)).toEqual( - gConsentGrant.credentialSubject.providedConsent.forPurpose ?? [], + it("gets the purposes from a gConsent access grant as empty list when there are no purposes", async () => { + expect(getPurposes(mockedGConsentGrant)).toEqual( + mockedGConsentGrant.credentialSubject.providedConsent.forPurpose ?? [], ); }); + it("gets the purposes from a gConsent access grant", async () => { + expect( + getPurposes( + await mockGConsentGrant(undefined, (object) => { + // eslint-disable-next-line no-param-reassign + object.credentialSubject.providedConsent.forPurpose = [ + "https://some.purpose", + ]; + }), + ), + ).toEqual(["https://some.purpose"]); + }); + it("gets the purposes from a gConsent access request", async () => { - expect(getPurposes(gConsentRequest)).toEqual( - gConsentRequest.credentialSubject.hasConsent.forPurpose ?? [], + expect(getPurposes(mockedGConsentRequest)).toEqual( + mockedGConsentRequest.credentialSubject.hasConsent.forPurpose ?? [], + ); + }); + + it("errors when purposes are not NamedNodes", async () => { + expect(() => + getPurposes( + new Store([ + ...mockedGConsentGrant, + quad( + getConsent(mockedGConsentGrant), + gc.forPurpose, + literal("hello world"), + ), + ]) as unknown as AccessGrant, + ), + ).toThrow( + "Expected purpose to be Named Node. Instead got [hello world] with term type [Literal]", + ); + }); + + it("errors if there are multiple consents present", async () => { + expect(() => + getPurposes( + new Store([ + ...mockedGConsentGrant, + quad( + getCredentialSubject(mockedGConsentGrant), + gc.providedConsent, + blankNode(), + ), + quad( + getCredentialSubject(mockedGConsentGrant), + gc.hasConsent, + blankNode(), + ), + ]) as unknown as AccessGrant, + ), + ).toThrow("Expected exactly 1 consent value. Found 3."); + }); + + it("errors if the consent is a NamedNode", async () => { + const store = new Store([...mockedGConsentGrant]); + + await promisifyEventEmitter( + store.removeMatches( + getCredentialSubject(mockedGConsentGrant) as any, + gc.providedConsent, + ), + ); + + expect(() => + getPurposes( + new Store([ + ...store, + quad( + getCredentialSubject(mockedGConsentGrant), + gc.hasConsent, + literal("hello world"), + ), + ]) as unknown as AccessGrant, + ), + ).toThrow( + "Expected consent to be a Named Node or Blank Node, instead got [Literal].", ); }); }); describe("getResourceOwner", () => { it("gets the resource owner from a gConsent access grant", async () => { - expect(getResourceOwner(gConsentGrant)).toBe( - gConsentGrant.credentialSubject.id, + expect(getResourceOwner(mockedGConsentGrant)).toBe( + mockedGConsentGrant.credentialSubject.id, ); }); @@ -101,29 +202,51 @@ describe("getters", () => { }); it("returns undefined if the resource owner is absent from a gConsent access request", async () => { - const gConsentRequest = await mockGConsentRequest( - { - resourceOwner: null, - }, - { - // The resource owner is actually required on access requests - skipValidation: true, - }, - ); + const gConsentRequest = await mockGConsentRequest({ + resourceOwner: null, + }); expect(getResourceOwner(gConsentRequest)).toBeUndefined(); }); }); describe("getRequestor", () => { it("gets the recipient of a gConsent access grant", async () => { - expect(getRequestor(gConsentGrant)).toBe( - gConsentGrant.credentialSubject.providedConsent.isProvidedTo, + expect(getRequestor(mockedGConsentGrant)).toBe( + mockedGConsentGrant.credentialSubject.providedConsent.isProvidedTo, + ); + }); + + it("errors if there are multiple requestors", async () => { + const store = new Store([...mockedGConsentGrant]); + + store.addQuad( + getConsent(mockedGConsentGrant), + gc.isProvidedTo, + namedNode("http://example.org/another/requestor") + ) + + expect(() => getRequestor(store as any)).toThrowError( + "Expected exactly one result. Found 2.", + ); + }); + + it("errors if there are multiple consent objects", async () => { + const store = new Store([...mockedGConsentGrant]); + + store.addQuad( + getCredentialSubject(mockedGConsentGrant), + gc.providedConsent, + namedNode("http://example.org/another/consent"), + ); + + expect(() => getRequestor(store as any)).toThrow( + "Expected exactly one result. Found 2.", ); }); it("gets the resource owner from a gConsent access request", async () => { - expect(getRequestor(gConsentRequest)).toBe( - gConsentRequest.credentialSubject.id, + expect(getRequestor(mockedGConsentRequest)).toBe( + mockedGConsentRequest.credentialSubject.id, ); }); }); @@ -131,9 +254,9 @@ describe("getters", () => { describe("getAccessModes", () => { it("gets the access modes of a gConsent access grant", async () => { expect( - gConsentGrant.credentialSubject.providedConsent.mode, + mockedGConsentGrant.credentialSubject.providedConsent.mode, ).toStrictEqual(["http://www.w3.org/ns/auth/acl#Read"]); - expect(getAccessModes(gConsentGrant)).toStrictEqual({ + expect(getAccessModes(mockedGConsentGrant)).toStrictEqual({ read: true, append: false, write: false, @@ -154,39 +277,65 @@ describe("getters", () => { describe("getId", () => { it("gets the gConsent access grant id", async () => { - expect(getId(gConsentGrant)).toBe(gConsentGrant.id); + expect(getId(mockedGConsentGrant)).toBe(mockedGConsentGrant.id); }); it("gets the gConsent access request id", async () => { - expect(getId(gConsentRequest)).toBe(gConsentRequest.id); + expect(getId(mockedGConsentRequest)).toBe(mockedGConsentRequest.id); }); }); describe("getTypes", () => { it("gets the gConsent access grant types", async () => { - for (const type of gConsentGrant.type) { - expect(getTypes(gConsentGrant)).toContainEqual(type); + for (const type of mockedGConsentGrant.type) { + expect(getTypes(mockedGConsentGrant)).toContainEqual(type); } }); + it("errors if there are non NamedNode grant types", async () => { + const store = new Store([ + ...mockedGConsentGrant, + quad( + namedNode(mockedGConsentGrant.id), + TYPE, + literal("this is a literal"), + ), + ]); + + expect(() => getTypes(store as any)).toThrow( + "Expected every type to be a Named Node, but found [this is a literal] with term type [Literal]", + ); + }); + it("gets the gConsent access request id", async () => { - for (const type of gConsentRequest.type) { - expect(getTypes(gConsentRequest)).toContainEqual(type); + for (const type of mockedGConsentRequest.type) { + expect(getTypes(mockedGConsentRequest)).toContainEqual(type); } }); + + it("errors if there are non NamedNode request types", async () => { + const store = new Store([ + ...mockedGConsentRequest, + quad( + namedNode(mockedGConsentRequest.id), + TYPE, + literal("this is a literal"), + ), + ]); + + expect(() => getTypes(store as any)).toThrow( + "Expected every type to be a Named Node, but found [this is a literal] with term type [Literal]", + ); + }); }); describe("getIssuanceDate", () => { it("gets the gConsent access issuance date", async () => { const issuanceDate = new Date().toString(); - const gConsentGrant = await mockGConsentGrant( - undefined, - undefined, - (obj) => { - // eslint-disable-next-line no-param-reassign - obj.issuanceDate = issuanceDate; - }, - ); + const gConsentGrant = await mockGConsentGrant(undefined, (obj) => { + // eslint-disable-next-line no-param-reassign + obj.issuanceDate = issuanceDate; + }); expect(getIssuanceDate(gConsentGrant)).toStrictEqual( new Date(issuanceDate), ); @@ -194,14 +343,10 @@ describe("getters", () => { it("gets the gConsent access request issuance date", async () => { const issuanceDate = new Date().toString(); - const gConsentRequest = await mockGConsentRequest( - undefined, - undefined, - (obj) => { - // eslint-disable-next-line no-param-reassign - obj.issuanceDate = issuanceDate; - }, - ); + const gConsentRequest = await mockGConsentRequest(undefined, (obj) => { + // eslint-disable-next-line no-param-reassign + obj.issuanceDate = issuanceDate; + }); expect(getIssuanceDate(gConsentRequest)).toStrictEqual( new Date(issuanceDate), ); @@ -211,29 +356,49 @@ describe("getters", () => { describe("getExpirationDate", () => { it("gets the gConsent access expiration date", async () => { const expirationDate = new Date().toString(); - const gConsentGrant = await mockGConsentGrant( - undefined, - undefined, - (obj) => { - // eslint-disable-next-line no-param-reassign - obj.expirationDate = expirationDate; - }, - ); + const gConsentGrant = await mockGConsentGrant(undefined, (obj) => { + // eslint-disable-next-line no-param-reassign + obj.expirationDate = expirationDate; + }); expect(getExpirationDate(gConsentGrant)).toStrictEqual( new Date(expirationDate), ); }); + it("errors if the expiration date is a NamedNode", async () => { + const store = new Store([...mockedGConsentGrant]); + + store.addQuad( + namedNode(getId(mockedGConsentGrant)), + cred.expirationDate, + namedNode("http://example.org/this/is/a/date"), + ); + + expect(() => getExpirationDate(store as any)).toThrow( + "Expected [http://example.org/this/is/a/date] to be a Literal. Found [NamedNode]", + ); + }); + + it("errors if the expiration date is a literal without xsd:type", async () => { + const store = new Store([...mockedGConsentGrant]); + + store.addQuad( + namedNode(getId(mockedGConsentGrant)), + cred.expirationDate, + literal("boo"), + ); + + expect(() => getExpirationDate(store as any)).toThrow( + "Expected date to be a dateTime; recieved [http://www.w3.org/2001/XMLSchema#string]", + ); + }); + it("gets the gConsent access request expiration date", async () => { const expirationDate = new Date().toString(); - const gConsentRequest = await mockGConsentRequest( - undefined, - undefined, - (obj) => { - // eslint-disable-next-line no-param-reassign - obj.expirationDate = expirationDate; - }, - ); + const gConsentRequest = await mockGConsentRequest(undefined, (obj) => { + // eslint-disable-next-line no-param-reassign + obj.expirationDate = expirationDate; + }); expect(getExpirationDate(gConsentRequest)).toStrictEqual( new Date(expirationDate), ); @@ -242,11 +407,15 @@ describe("getters", () => { describe("getIssuer", () => { it("gets the gConsent access grant issuer", async () => { - expect(getIssuer(gConsentGrant)).toStrictEqual(gConsentGrant.issuer); + expect(getIssuer(mockedGConsentGrant)).toStrictEqual( + mockedGConsentGrant.issuer, + ); }); it("gets the gConsent access request issuer", async () => { - expect(getIssuer(gConsentRequest)).toStrictEqual(gConsentRequest.issuer); + expect(getIssuer(mockedGConsentRequest)).toStrictEqual( + mockedGConsentRequest.issuer, + ); }); }); @@ -274,33 +443,38 @@ describe("getters", () => { describe("AccessGrant", () => { it("wraps calls to the underlying functions", async () => { - const wrappedConsentRequest = new AccessGrantWrapper(gConsentRequest); + const wrappedConsentRequest = new AccessGrantWrapper( + mockedGConsentRequest, + ); expect(wrappedConsentRequest.getAccessModes()).toStrictEqual( - getAccessModes(gConsentRequest), + getAccessModes(mockedGConsentRequest), ); expect(wrappedConsentRequest.getExpirationDate()).toStrictEqual( - getExpirationDate(gConsentRequest), + getExpirationDate(mockedGConsentRequest), ); expect(wrappedConsentRequest.getId()).toStrictEqual( - getId(gConsentRequest), + getId(mockedGConsentRequest), ); expect(wrappedConsentRequest.getIssuanceDate()).toStrictEqual( - getIssuanceDate(gConsentRequest), + getIssuanceDate(mockedGConsentRequest), ); expect(wrappedConsentRequest.getIssuer()).toStrictEqual( - getIssuer(gConsentRequest), + getIssuer(mockedGConsentRequest), ); expect(wrappedConsentRequest.getRequestor()).toStrictEqual( - getRequestor(gConsentRequest), + getRequestor(mockedGConsentRequest), ); expect(wrappedConsentRequest.getResourceOwner()).toStrictEqual( - getResourceOwner(gConsentRequest), + getResourceOwner(mockedGConsentRequest), ); expect(wrappedConsentRequest.getResources()).toStrictEqual( - getResources(gConsentRequest), + getResources(mockedGConsentRequest), ); expect(wrappedConsentRequest.getTypes()).toStrictEqual( - getTypes(gConsentRequest), + getTypes(mockedGConsentRequest), + ); + expect(wrappedConsentRequest.getInherit()).toStrictEqual( + getInherit(mockedGConsentRequest), ); }); }); diff --git a/src/common/getters.ts b/src/common/getters.ts index a47c20bc2..36d458af3 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -39,6 +39,136 @@ import { const { namedNode, defaultGraph, quad, literal } = DataFactory; +/** + * @internal + */ +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "NamedNode", +): NamedNode; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "NamedNode", + required: false, +): NamedNode | undefined; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "Literal", + required: false, +): Literal | undefined; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "BlankNode", +): BlankNode; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "Literal", +): Literal; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, +): NamedNode | BlankNode; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: undefined, + required: false, +): NamedNode | BlankNode | undefined; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type?: Term["termType"], + required = true, +): Term | undefined { + const results = [...vc.match(subject, predicate, null, defaultGraph())]; + + if (results.length === 0 && !required) { + return undefined; + } + + if (results.length !== 1) { + throw new Error(`Expected exactly one result. Found ${results.length}.`); + } + + const [{ object }] = results; + const expectedTypes = type ? [type] : ["NamedNode", "BlankNode"]; + if (!expectedTypes.includes(object.termType)) { + throw new Error( + `Expected [${object.value}] to be a ${expectedTypes.join( + " or ", + )}. Found [${object.termType}]`, + ); + } + + return object; +} + +/** + * Get the ID (URL) of an Access Grant/Request. + * + * @example + * + * ``` + * const id = getId(accessGrant); + * ``` + * + * @param vc The Access Grant/Request + * @returns The VC ID URL + */ +export function getId(vc: AccessGrantGConsent | AccessRequestGConsent): string { + return vc.id; +} + +/** + * @internal + */ +export function getCredentialSubject( + vc: AccessGrantGConsent | AccessRequestGConsent, +) { + return getSingleObject( + vc, + namedNode(getId(vc)), + cred.credentialSubject, + "NamedNode", + ); +} + +/** + * @internal + */ +export function getConsent(vc: AccessGrantGConsent | AccessRequestGConsent) { + const credentialSubject = getCredentialSubject(vc); + const consents = [ + ...vc.match(credentialSubject, gc.providedConsent, null, defaultGraph()), + ...vc.match(credentialSubject, gc.hasConsent, null, defaultGraph()), + ]; + if (consents.length !== 1) { + throw new Error( + `Expected exactly 1 consent value. Found ${consents.length}.`, + ); + } + const [{ object }] = consents; + if (object.termType !== "BlankNode" && object.termType !== "NamedNode") { + throw new Error( + `Expected consent to be a Named Node or Blank Node, instead got [${object.termType}].`, + ); + } + return object; +} + /** * Get the resources to which an Access Grant/Request applies. * @@ -99,7 +229,7 @@ export function getPurposes( )) { if (object.termType !== "NamedNode") { throw new Error( - `Expected all purposes to be Named Nodes. Found [${object.value}] of type [${object.termType}]`, + `Expected purpose to be Named Node. Instead got [${object.value}] with term type [${object.termType}]`, ); } purposes.push(object.value); @@ -172,35 +302,6 @@ export function getResourceOwner( )?.value; } -function getCredentialSubject(vc: AccessGrantGConsent | AccessRequestGConsent) { - return getSingleObject( - vc, - namedNode(getId(vc)), - cred.credentialSubject, - "NamedNode", - ); -} - -function getConsent(vc: AccessGrantGConsent | AccessRequestGConsent) { - const credentialSubject = getCredentialSubject(vc); - const consents = [ - ...vc.match(credentialSubject, gc.providedConsent, null, defaultGraph()), - ...vc.match(credentialSubject, gc.hasConsent, null, defaultGraph()), - ]; - if (consents.length !== 1) { - throw new Error( - `Expected exactly 1 consent value. Found ${consents.length}.`, - ); - } - const [{ object }] = consents; - if (object.termType !== "BlankNode" && object.termType !== "NamedNode") { - throw new Error( - `Expected consent to be a Named Node or Blank Node, instead got ${object.termType}`, - ); - } - return object; -} - /** * Get the requestor asking for access to a resources with an Access Grant/Request. * @@ -231,80 +332,6 @@ export function getRequestor( .value; } -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "NamedNode", -): NamedNode; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "NamedNode", - required: false, -): NamedNode | undefined; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "Literal", - required: false, -): Literal | undefined; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "BlankNode", -): BlankNode; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "Literal", -): Literal; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, -): NamedNode | BlankNode; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: undefined, - required: false, -): NamedNode | BlankNode | undefined; -function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type?: Term["termType"], - required = true, -): Term | undefined { - const results = [...vc.match(subject, predicate, null, defaultGraph())]; - - if (results.length === 0 && !required) { - return undefined; - } - - if (results.length !== 1) { - throw new Error(`Expected exactly one result. Found ${results.length}.`); - } - - const [{ object }] = results; - const expectedTypes = type ? [type] : ["NamedNode", "BlankNode"]; - if (!expectedTypes.includes(object.termType)) { - throw new Error( - `Expected [${object.value}] to be a ${expectedTypes.join( - " or ", - )}. Found [${object.termType}]`, - ); - } - - return object; -} - /** * Get the access modes granted to a resources via an Access Grant/Request. * @@ -328,21 +355,15 @@ export function getAccessModes( }; } -/** - * Get the ID (URL) of an Access Grant/Request. - * - * @example - * - * ``` - * const id = getId(accessGrant); - * ``` - * - * @param vc The Access Grant/Request - * @returns The VC ID URL - */ -export function getId(vc: AccessGrantGConsent | AccessRequestGConsent): string { - return vc.id; -} +const shorthand = { + "http://www.w3.org/ns/solid/vc#SolidAccessRequest": "SolidAccessRequest", + "http://www.w3.org/ns/solid/vc#SolidAccessDenial": "SolidAccessDenial", + "http://www.w3.org/ns/solid/vc#SolidAccessGrant": "SolidAccessGrant", + "https://www.w3.org/2018/credentials#VerifiableCredential": + "VerifiableCredential", + "https://www.w3.org/2018/credentials#VerifiablePresentation": + "VerifiablePresentation", +}; /** * Get the VC types of an Access Grant/Request. @@ -380,16 +401,6 @@ export function getTypes( return types; } -const shorthand = { - "http://www.w3.org/ns/solid/vc#SolidAccessRequest": "SolidAccessRequest", - "http://www.w3.org/ns/solid/vc#SolidAccessDenial": "SolidAccessDenial", - "http://www.w3.org/ns/solid/vc#SolidAccessGrant": "SolidAccessGrant", - "https://www.w3.org/2018/credentials#VerifiableCredential": - "VerifiableCredential", - "https://www.w3.org/2018/credentials#VerifiablePresentation": - "VerifiablePresentation", -}; - function wrapDate(date: Literal) { if ( !date.datatype.equals( @@ -511,6 +522,9 @@ export class AccessGrantWrapper { this.vc = vc; } + // FIXME: Make sure we have full coverage of public exported + // functions in this file + getResources(): ReturnType { return getResources(this.vc); } diff --git a/src/gConsent/manage/approveAccessRequest.test.ts b/src/gConsent/manage/approveAccessRequest.test.ts index 75021fdf8..d6c32aba6 100644 --- a/src/gConsent/manage/approveAccessRequest.test.ts +++ b/src/gConsent/manage/approveAccessRequest.test.ts @@ -136,10 +136,10 @@ describe("approveAccessRequest", () => { let consentGrantVc: Awaited>; beforeAll(async () => { - accessRequestVc = await mockAccessRequestVc({}, { expandModeUri: true }); + accessRequestVc = await mockAccessRequestVc(); accessGrantVc = await mockAccessGrantVc(); - consentRequestVc = await mockConsentRequestVc({}, { expandModeUri: true }); - consentGrantVc = await mockConsentGrantVc({}, { expandModeUri: true }); + consentRequestVc = await mockConsentRequestVc(); + consentGrantVc = await mockConsentGrantVc(); }); // FIXME: This test must run before the other tests mocking the ACP client. @@ -396,9 +396,7 @@ describe("approveAccessRequest", () => { mockedVcModule, "issueVerifiableCredential", ); - spiedIssueRequest.mockResolvedValueOnce( - await mockAccessGrantVc({}, { expandModeUri: true }), - ); + spiedIssueRequest.mockResolvedValueOnce(await mockAccessGrantVc()); await approveAccessRequest(consentRequestVc, undefined, { fetch: jest.fn(global.fetch), }); diff --git a/src/gConsent/manage/denyAccessRequest.test.ts b/src/gConsent/manage/denyAccessRequest.test.ts index b857de622..fb4427442 100644 --- a/src/gConsent/manage/denyAccessRequest.test.ts +++ b/src/gConsent/manage/denyAccessRequest.test.ts @@ -70,10 +70,7 @@ describe("denyAccessRequest", () => { beforeAll(async () => { accessRequestVc = await mockAccessRequestVc(); - accessRequestVcExpanded = await mockAccessRequestVc( - {}, - { expandModeUri: true }, - ); + accessRequestVcExpanded = await mockAccessRequestVc(); }); it("throws if the provided VC isn't a Solid access request", async () => { @@ -145,12 +142,9 @@ describe("denyAccessRequest", () => { const spiedIssueRequest = jest .spyOn(mockedVcModule, "issueVerifiableCredential") .mockResolvedValueOnce(await mockAccessGrantVc()); - const accessRequestWithPurpose = await mockAccessRequestVc( - { - purpose: ["https://example.org/some-purpose"], - }, - { expandModeUri: true }, - ); + const accessRequestWithPurpose = await mockAccessRequestVc({ + purpose: ["https://example.org/some-purpose"], + }); await denyAccessRequest(accessRequestWithPurpose, { fetch: jest.fn(global.fetch) as typeof fetch, }); @@ -297,14 +291,9 @@ describe("denyAccessRequest", () => { .spyOn(mockedVcModule, "issueVerifiableCredential") .mockResolvedValueOnce(await mockAccessGrantVc()); - const accessRequestWithPurpose = await mockAccessRequestVc( - { - purpose: ["https://example.org/some-purpose"], - }, - { - expandModeUri: true, - }, - ); + const accessRequestWithPurpose = await mockAccessRequestVc({ + purpose: ["https://example.org/some-purpose"], + }); const mockedFetch = jest .fn(global.fetch) diff --git a/src/gConsent/manage/getAccessGrant.test.ts b/src/gConsent/manage/getAccessGrant.test.ts index c00e838e2..fab835f6c 100644 --- a/src/gConsent/manage/getAccessGrant.test.ts +++ b/src/gConsent/manage/getAccessGrant.test.ts @@ -111,6 +111,9 @@ describe("getAccessGrant", () => { ).rejects.toThrow(/not an Access Grant/); }); + // There is an expect call in the `toBeEqual` function, + // but the linter doesn't pick up on this. + // eslint-disable-next-line jest/expect-expect it("supports denied access grants with a given IRI", async () => { mockAccessApiEndpoint(); const mockedAccessGrant = mockAccessGrantObject(); @@ -128,6 +131,9 @@ describe("getAccessGrant", () => { toBeEqual(accessGrant, mockedAccessGrant); }); + // There is an expect call in the `toBeEqual` function, + // but the linter doesn't pick up on this. + // eslint-disable-next-line jest/expect-expect it("returns the access grant with the given IRI", async () => { mockAccessApiEndpoint(); const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( @@ -142,6 +148,9 @@ describe("getAccessGrant", () => { toBeEqual(accessGrant, mockAccessGrant); }); + // There is an expect call in the `toBeEqual` function, + // but the linter doesn't pick up on this. + // eslint-disable-next-line jest/expect-expect it("normalizes equivalent JSON-LD VCs", async () => { mockAccessApiEndpoint(); const normalizedAccessGrant = mockAccessGrantObject(); @@ -178,6 +187,9 @@ describe("getAccessGrant", () => { ); }); + // There is an expect call in the `toBeEqual` function, + // but the linter doesn't pick up on this. + // eslint-disable-next-line jest/expect-expect it("returns the access grant with the given URL object", async () => { mockAccessApiEndpoint(); const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( diff --git a/src/gConsent/util/access.mock.ts b/src/gConsent/util/access.mock.ts index e3690e7b7..dd0601917 100644 --- a/src/gConsent/util/access.mock.ts +++ b/src/gConsent/util/access.mock.ts @@ -19,29 +19,27 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // /* eslint-disable no-param-reassign */ +import type { UrlString } from "@inrupt/solid-client"; import { - type VerifiableCredential, verifiableCredentialToDataset, + type VerifiableCredential, } from "@inrupt/solid-client-vc"; -import type { UrlString } from "@inrupt/solid-client"; import type { DatasetCore, Quad } from "@rdfjs/types"; -import type { - BaseGrantBody, - BaseRequestBody, -} from "../type/AccessVerifiableCredential"; +import type { ResourceAccessMode } from "../../type/ResourceAccessMode"; import { CREDENTIAL_TYPE_ACCESS_GRANT, CREDENTIAL_TYPE_ACCESS_REQUEST, GC_CONSENT_STATUS_REQUESTED, MOCK_CONTEXT, } from "../constants"; -import type { ResourceAccessMode } from "../../type/ResourceAccessMode"; +import { normalizeAccessGrant } from "../manage/approveAccessRequest"; +import { normalizeAccessRequest } from "../request/issueAccessRequest"; import type { AccessGrant } from "../type/AccessGrant"; import type { AccessRequest } from "../type/AccessRequest"; -import { normalizeAccessRequest } from "../request/issueAccessRequest"; -import { isAccessRequest } from "../guard/isAccessRequest"; -import { normalizeAccessGrant } from "../manage/approveAccessRequest"; -import { isAccessGrant } from "../guard/isAccessGrant"; +import type { + BaseGrantBody, + BaseRequestBody, +} from "../type/AccessVerifiableCredential"; type RequestVcOptions = Partial<{ resources: UrlString[]; @@ -64,7 +62,7 @@ export const mockAccessRequestVcObject = (options?: RequestVcOptions) => { if (options?.resourceOwner === null) { delete hasConsent.isConsentForDataSubject; } else if (options?.resourceOwner) { - hasConsent.isConsentForDataSubject = options?.resourceOwner; + hasConsent.isConsentForDataSubject = options.resourceOwner; } // We want to be able to mutate the record so we cannot set as a const @@ -104,49 +102,15 @@ export const mockAccessRequestVcObject = (options?: RequestVcOptions) => { export const mockAccessRequestVc = async ( options?: RequestVcOptions, - framingOptions?: { - // This should only be used for testing /issue calls - expandModeUri?: boolean; - skipValidation?: boolean; - }, // eslint-disable-next-line @typescript-eslint/no-explicit-any modify?: (asObject: Record) => void, ): Promise> => { const asObject = mockAccessRequestVcObject(options) as VerifiableCredential; modify?.(asObject); - const accessRequest = normalizeAccessRequest( + return normalizeAccessRequest( await verifiableCredentialToDataset(asObject, { baseIRI: asObject.id }), - ); - - if (framingOptions?.skipValidation) { - return accessRequest as unknown as AccessRequest & DatasetCore; - } - - if (!isAccessRequest(accessRequest)) { - throw new Error( - `${JSON.stringify( - accessRequest, - null, - 2, - )} is not an Access Request. Trying to reframe [${JSON.stringify( - asObject, - null, - 2, - )}] [${JSON.stringify(framingOptions)}]`, - ); - } - - if (framingOptions?.expandModeUri) { - accessRequest.credentialSubject.hasConsent.mode = - accessRequest.credentialSubject.hasConsent.mode.map((mode) => - mode.startsWith("http") - ? mode - : `http://www.w3.org/ns/auth/acl#${mode}`, - ); - } - - return accessRequest; + ) as AccessRequest; }; export const mockAccessGrantObject = ( @@ -189,62 +153,29 @@ export const mockAccessGrantVc = async ( subjectId: string; inherit: boolean; resources: string[]; + purposes: string[]; }>, - framingOptions?: { - // This should only be used for testing /issue calls - expandModeUri?: boolean; - }, // eslint-disable-next-line @typescript-eslint/no-explicit-any modify?: (asObject: Record) => void, ): Promise => { const asObject = mockAccessGrantObject(options); modify?.(asObject); - const accessGrant = normalizeAccessGrant( + return normalizeAccessGrant( await verifiableCredentialToDataset(asObject, { baseIRI: asObject.id }), - ); - - // FIXME the type casting ias bad - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!isAccessGrant(accessGrant as any)) { - throw new Error("Not an access grant"); - } - - if (framingOptions?.expandModeUri) { - const providedConsent = accessGrant.credentialSubject?.providedConsent as - | { mode?: string[] | undefined } - | undefined; - if (providedConsent) { - providedConsent.mode = providedConsent.mode?.map((mode: string) => - mode.startsWith("http") - ? mode - : `http://www.w3.org/ns/auth/acl#${mode}`, - ); - } - } - - // FIXME type casting is bad - return accessGrant as unknown as AccessGrant; + ) as unknown as AccessGrant; }; export const mockConsentRequestVc = async ( options?: RequestVcOptions, - framingOptions?: { - // This should only be used for testing /issue calls - expandModeUri?: boolean; - }, ): Promise< VerifiableCredential & BaseRequestBody & DatasetCore > => { - const requestVc = await mockAccessRequestVc( - options, - framingOptions, - (object) => { - object.credentialSubject.hasConsent.forPurpose = ["https://some.purpose"]; - object.expirationDate = new Date(2021, 8, 14).toISOString(); - object.issuanceDate = new Date(2021, 8, 13).toISOString(); - }, - ); + const requestVc = await mockAccessRequestVc(options, (object) => { + object.credentialSubject.hasConsent.forPurpose = ["https://some.purpose"]; + object.expirationDate = new Date(2021, 8, 14).toISOString(); + object.issuanceDate = new Date(2021, 8, 13).toISOString(); + }); return requestVc; }; @@ -255,21 +186,13 @@ export const mockConsentGrantVc = async ( inherit: boolean; resources: string[]; }>, - framingOptions?: { - // This should only be used for testing /issue calls - expandModeUri?: boolean; - }, ): Promise> => { - const requestVc = await mockAccessGrantVc( - options, - framingOptions, - (object) => { - object.credentialSubject.providedConsent.forPurpose = [ - "https://some.purpose", - ]; - object.expirationDate = new Date(2021, 8, 14).toISOString(); - object.issuanceDate = new Date(2021, 8, 13).toISOString(); - }, - ); + const requestVc = await mockAccessGrantVc(options, (object) => { + object.credentialSubject.providedConsent.forPurpose = [ + "https://some.purpose", + ]; + object.expirationDate = new Date(2021, 8, 14).toISOString(); + object.issuanceDate = new Date(2021, 8, 13).toISOString(); + }); return requestVc; };