diff --git a/package-lock.json b/package-lock.json index 9b9b2c997..584b6143b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "dotenv-flow": "^3.2.0", "eslint": "^8.18.0", "eslint-config-next": "^13.0.6", + "event-emitter-promisify": "^1.1.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", "license-checker": "^25.0.1", diff --git a/package.json b/package.json index c0b300aaf..05117e4ef 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "dotenv-flow": "^3.2.0", "eslint": "^8.18.0", "eslint-config-next": "^13.0.6", + "event-emitter-promisify": "^1.1.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", "license-checker": "^25.0.1", diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts index 4aedb3e73..f3388340b 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -45,7 +45,6 @@ import { 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", () => ({ diff --git a/src/gConsent/manage/getAccessGrant.test.ts b/src/gConsent/manage/getAccessGrant.test.ts index fab835f6c..f327539f6 100644 --- a/src/gConsent/manage/getAccessGrant.test.ts +++ b/src/gConsent/manage/getAccessGrant.test.ts @@ -23,7 +23,6 @@ import type * as CrossFetch from "@inrupt/universal-fetch"; import { Response } from "@inrupt/universal-fetch"; import { beforeAll, describe, expect, it, jest } from "@jest/globals"; -import { mockAccessApiEndpoint } from "../request/request.mock"; import { mockAccessGrantObject, mockAccessGrantVc, @@ -31,6 +30,7 @@ import { } from "../util/access.mock"; import { toBeEqual } from "../util/toBeEqual.mock"; import { getAccessGrant } from "./getAccessGrant"; +import { verifiableCredentialToDataset } from "@inrupt/solid-client-vc"; jest.mock("@inrupt/universal-fetch", () => { const crossFetch = jest.requireActual( @@ -38,7 +38,7 @@ jest.mock("@inrupt/universal-fetch", () => { ) as jest.Mocked; return { // Do no mock the globals such as Response. - ...crossFetch, + Response: crossFetch.Response, fetch: jest.fn<(typeof crossFetch)["fetch"]>(), }; }); @@ -51,7 +51,6 @@ describe("getAccessGrant", () => { }); it("uses the provided fetch if any", async () => { - mockAccessApiEndpoint(); const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(mockAccessGrantObject()), { headers: new Headers([["content-type", "application/json"]]), @@ -65,16 +64,9 @@ describe("getAccessGrant", () => { }); it("throws if resolving the IRI results in an HTTP error", async () => { - mockAccessApiEndpoint(); - const mockedFetch = jest - .fn(global.fetch) - .mockResolvedValueOnce( - new Response("Not Found", { status: 404, statusText: "Not Found" }), - ); - await expect( getAccessGrant("https://some.vc.url", { - fetch: mockedFetch, + fetch: async () => new Response("Not Found", { status: 404, statusText: "Not Found" }), }), ).rejects.toThrow( /Could not resolve \[https:\/\/some.vc.url\].*404 Not Found/, @@ -82,14 +74,9 @@ describe("getAccessGrant", () => { }); it("throws if the given IRI does not resolve to a Verifiable Credential", async () => { - mockAccessApiEndpoint(); - const mockedFetch = jest - .fn(global.fetch) - .mockResolvedValueOnce(new Response("{'someKey': 'someValue'}")); - await expect( getAccessGrant("https://some.vc.url", { - fetch: mockedFetch, + fetch: async () => new Response("{'someKey': 'someValue'}"), }), ).rejects.toThrow( /Unexpected response.*\[https:\/\/some.vc.url\].*not a Verifiable Credential/, @@ -97,16 +84,11 @@ describe("getAccessGrant", () => { }); it("throws if the given IRI does not resolve to a access grant Verifiable Credential", async () => { - mockAccessApiEndpoint(); - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( - new Response(JSON.stringify(await mockAccessRequestVc()), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); - await expect( getAccessGrant("https://some.vc.url", { - fetch: mockedFetch, + fetch: async () => new Response(JSON.stringify(await mockAccessRequestVc()), { + headers: new Headers([["content-type", "application/json"]]), + }), }), ).rejects.toThrow(/not an Access Grant/); }); @@ -115,18 +97,14 @@ describe("getAccessGrant", () => { // 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(); mockedAccessGrant.credentialSubject.providedConsent.hasStatus = "https://w3id.org/GConsent#ConsentStatusDenied"; - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( - new Response(JSON.stringify(mockedAccessGrant), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); const accessGrant = await getAccessGrant("https://some.vc.url", { - fetch: mockedFetch, + fetch: async () => new Response(JSON.stringify(mockedAccessGrant), { + headers: new Headers([["content-type", "application/json"]]), + }), }); toBeEqual(accessGrant, mockedAccessGrant); }); @@ -135,15 +113,10 @@ describe("getAccessGrant", () => { // 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( - new Response(JSON.stringify(mockAccessGrantObject()), { + const accessGrant = await getAccessGrant("https://some.vc.url", { + fetch: async () => new Response(JSON.stringify(mockAccessGrantObject()), { headers: new Headers([["content-type", "application/json"]]), }), - ); - - const accessGrant = await getAccessGrant("https://some.vc.url", { - fetch: mockedFetch, }); toBeEqual(accessGrant, mockAccessGrant); }); @@ -152,36 +125,31 @@ describe("getAccessGrant", () => { // 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(); - // The server returns an equivalent JSON-LD with a different frame: - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( - new Response( - JSON.stringify({ - ...normalizedAccessGrant, - credentialSubject: { - ...normalizedAccessGrant.credentialSubject, - providedConsent: { - ...normalizedAccessGrant.credentialSubject.providedConsent, - // The 1-value array is replaced by the literal value. - forPersonalData: - normalizedAccessGrant.credentialSubject.providedConsent - .forPersonalData[0], - mode: normalizedAccessGrant.credentialSubject.providedConsent - .mode[0], - inherit: "true", - }, - }, - }), - { - headers: new Headers([["content-type", "application/json"]]), - }, - ), - ); - toBeEqual( await getAccessGrant("https://some.vc.url", { - fetch: mockedFetch, + // The server returns an equivalent JSON-LD with a different frame: + fetch: async () => new Response( + JSON.stringify({ + ...normalizedAccessGrant, + credentialSubject: { + ...normalizedAccessGrant.credentialSubject, + providedConsent: { + ...normalizedAccessGrant.credentialSubject.providedConsent, + // The 1-value array is replaced by the literal value. + forPersonalData: + normalizedAccessGrant.credentialSubject.providedConsent + .forPersonalData[0], + mode: normalizedAccessGrant.credentialSubject.providedConsent + .mode[0], + inherit: "true", + }, + }, + }), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), }), mockAccessGrant, ); @@ -191,7 +159,6 @@ describe("getAccessGrant", () => { // 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( new Response(JSON.stringify(mockAccessGrantObject()), { headers: new Headers([["content-type", "application/json"]]), @@ -203,4 +170,21 @@ describe("getAccessGrant", () => { }); toBeEqual(accessGrant, mockAccessGrant); }); + + it("errors if the response is not a full access grant", async () => { + expect(getAccessGrant(new URL("https://some.vc.url"), { + fetch: async () => new Response(JSON.stringify({ + "@context": "https://www.w3.org/2018/credentials/v1", + id: "https://some.credential", + })), + })).rejects.toThrow('the result is not a Verifiable Credential'); + }); + + it("errors if the response is an empty json object", async () => { + expect(getAccessGrant(new URL("https://some.vc.url"), { + fetch: async () => new Response(JSON.stringify({}), { + headers: new Headers([["content-type", "application/json"]]), + }), + })).rejects.toThrow('the result is not a Verifiable Credential'); + }); }); diff --git a/src/gConsent/manage/getAccessGrant.ts b/src/gConsent/manage/getAccessGrant.ts index 9a31b95c8..8e7a2ed7f 100644 --- a/src/gConsent/manage/getAccessGrant.ts +++ b/src/gConsent/manage/getAccessGrant.ts @@ -54,18 +54,17 @@ export async function getAccessGrant( `Could not resolve [${vcUrl}]: ${response.status} ${response.statusText}`, ); } - const responseErrorClone = response.clone(); + const responseErrorClone = await response.text(); let data; try { - data = await verifiableCredentialToDataset(await response.json(), { + data = await verifiableCredentialToDataset(normalizeAccessGrant(JSON.parse(responseErrorClone)), { baseIRI: accessGrantVcUrl.toString(), }); } catch (e) { throw new Error( - `Unexpected response when resolving [${vcUrl}], the result is not a Verifiable Credential: ${await responseErrorClone.text()}.\n\nError details: ${e}`, + `Unexpected response when resolving [${vcUrl}], the result is not a Verifiable Credential: ${responseErrorClone}.\n\nError details: ${e}`, ); } - data = normalizeAccessGrant(data); if ( !isVerifiableCredential(data) || !isBaseAccessGrantVerifiableCredential(data) ||