Skip to content

Commit

Permalink
feat: add getAccessRequest (#676)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeswr authored Jul 24, 2023
1 parent 777663a commit 058c19f
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 139 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html

- `deleteSolidDataset` and `deleteFile`: Add functions to the `resource` module
to delete resources, following the interface of `@inrupt/solid-client`.
- `getAccessRequest`: a function exported by the `./manage` module to
get the Access Request from the Access Request URL.

## [2.3.2](https://github.com/inrupt/solid-client-access-grants-js/releases/tag/v2.3.2) - 2023-06-05

Expand Down
1 change: 1 addition & 0 deletions src/gConsent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export {
getAccessGrant,
getAccessGrantAll,
getAccessRequestFromRedirectUrl,
getAccessRequest,
redirectToRequestor,
revokeAccessGrant,
GRANT_VC_URL_PARAM_NAME,
Expand Down
110 changes: 110 additions & 0 deletions src/gConsent/manage/getAccessRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { describe, it, jest, expect, beforeEach } from "@jest/globals";
import type { getVerifiableCredential } from "@inrupt/solid-client-vc";
import { fetch } from "@inrupt/universal-fetch";
import { getAccessRequest } from "./getAccessRequest";
import { mockAccessGrantVc, mockAccessRequestVc } from "../util/access.mock";
import type { getSessionFetch } from "../../common/util/getSessionFetch";

jest.mock("../../common/util/getSessionFetch");
jest.mock("@inrupt/solid-client-vc", () => {
const vcModule = jest.requireActual("@inrupt/solid-client-vc") as any;
return {
...vcModule,
getVerifiableCredential: jest.fn(),
};
});

describe("getAccessRequest", () => {
let mockedFetch: jest.MockedFunction<typeof fetch>;
let embeddedFetch: jest.Mocked<{ getSessionFetch: typeof getSessionFetch }>;
let vcModule: jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
beforeEach(() => {
mockedFetch = jest.fn(fetch);
embeddedFetch = jest.requireMock("../../common/util/getSessionFetch");
embeddedFetch.getSessionFetch.mockResolvedValueOnce(mockedFetch);
vcModule = jest.requireMock("@inrupt/solid-client-vc");
vcModule.getVerifiableCredential.mockResolvedValue(mockAccessRequestVc());
});

it("uses the default fetch if none is provided", async () => {
await getAccessRequest("https://some.vc");
expect(vcModule.getVerifiableCredential).toHaveBeenCalledWith(
"https://some.vc",
{
fetch: mockedFetch,
}
);
});

it("uses the provided fetch if any", async () => {
await getAccessRequest("https://some.vc", {
fetch: mockedFetch,
});
expect(vcModule.getVerifiableCredential).toHaveBeenCalledWith(
"https://some.vc",
{
fetch: mockedFetch,
}
);
});

it("returns the fetched VC and the redirect URL", async () => {
// Check that both URL strings and objects are supported.
const accessRequestFromString = await getAccessRequest("https://some.vc");
const accessRequestFromUrl = await getAccessRequest(
new URL("https://some.vc")
);

expect(accessRequestFromString).toStrictEqual(accessRequestFromUrl);
expect(accessRequestFromString).toStrictEqual(mockAccessRequestVc());
});

it("throws if the fetched VC is not an Access Request", async () => {
vcModule.getVerifiableCredential.mockResolvedValueOnce(mockAccessGrantVc());
await expect(getAccessRequest("https://some.vc")).rejects.toThrow();
});

it("normalizes equivalent JSON-LD VCs", async () => {
const normalizedAccessRequest = mockAccessRequestVc();
// The server returns an equivalent JSON-LD with a different frame:
vcModule.getVerifiableCredential.mockResolvedValueOnce({
...normalizedAccessRequest,
credentialSubject: {
...normalizedAccessRequest.credentialSubject,
hasConsent: {
...normalizedAccessRequest.credentialSubject.hasConsent,
// The 1-value array is replaced by the literal value.
forPersonalData:
normalizedAccessRequest.credentialSubject.hasConsent
.forPersonalData[0],
mode: normalizedAccessRequest.credentialSubject.hasConsent.mode[0],
},
},
});
const accessRequest = await getAccessRequest("https://some.vc");
expect(accessRequest).toStrictEqual(mockAccessRequestVc());
});
});
56 changes: 56 additions & 0 deletions src/gConsent/manage/getAccessRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import type { UrlString } from "@inrupt/solid-client";
import { getVerifiableCredential } from "@inrupt/solid-client-vc";
import { isAccessRequest } from "../guard/isAccessRequest";
import type { AccessRequest } from "../type/AccessRequest";
import { getSessionFetch } from "../../common/util/getSessionFetch";
import { normalizeAccessRequest } from "../request/issueAccessRequest";

/**
* Fetch the Access Request from the given URL.
*
* @param url The URL of the Access Request.
* @param options Optional properties to customise the behaviour:
* - fetch: an authenticated fetch function. If not provided, the default session
* from @inrupt/solid-client-authn-browser will be used if available.
* @returns An Access Request.
* @since 0.5.0
*/
export async function getAccessRequest(
url: UrlString | URL,
options: { fetch?: typeof fetch } = {}
): Promise<AccessRequest> {
const accessRequest = normalizeAccessRequest(
await getVerifiableCredential(url.toString(), {
fetch: options.fetch ?? (await getSessionFetch(options)),
})
);

if (!isAccessRequest(accessRequest)) {
throw new Error(
`${JSON.stringify(accessRequest)} is not an Access Request`
);
}
return accessRequest;
}

export default getAccessRequest;
137 changes: 28 additions & 109 deletions src/gConsent/manage/getAccessRequestFromRedirectUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { describe, it, jest, expect } from "@jest/globals";
import type { getVerifiableCredential } from "@inrupt/solid-client-vc";
import { fetch } from "@inrupt/universal-fetch";
import { getAccessRequestFromRedirectUrl } from "./getAccessRequestFromRedirectUrl";
import { mockAccessGrantVc, mockAccessRequestVc } from "../util/access.mock";
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import type { getSessionFetch } from "../../common/util/getSessionFetch";
import { mockAccessGrantVc, mockAccessRequestVc } from "../util/access.mock";
import { getAccessRequestFromRedirectUrl } from "./getAccessRequestFromRedirectUrl";

jest.mock("../../common/util/getSessionFetch");
jest.mock("@inrupt/solid-client-vc", () => {
Expand All @@ -36,56 +36,45 @@ jest.mock("@inrupt/solid-client-vc", () => {
});

describe("getAccessRequestFromRedirectUrl", () => {
it("throws if the requestVcUrl query parameter is missing", async () => {
const redirectUrl = new URL("https://redirect.url");
redirectUrl.searchParams.append(
let mockedFetch: jest.MockedFunction<typeof fetch>;
let embeddedFetch: jest.Mocked<{ getSessionFetch: typeof getSessionFetch }>;
let vcModule: jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
let redirectedToUrl: URL;
beforeEach(() => {
mockedFetch = jest.fn(fetch);
embeddedFetch = jest.requireMock("../../common/util/getSessionFetch");
embeddedFetch.getSessionFetch.mockResolvedValueOnce(mockedFetch);
vcModule = jest.requireMock("@inrupt/solid-client-vc");
vcModule.getVerifiableCredential.mockResolvedValue(mockAccessRequestVc());

redirectedToUrl = new URL("https://redirect.url");
redirectedToUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);
redirectedToUrl.searchParams.append(
"redirectUrl",
encodeURI("https://requestor.redirect.url")
);
});

it("throws if the requestVcUrl query parameter is missing", async () => {
redirectedToUrl.searchParams.delete("requestVcUrl");
await expect(
getAccessRequestFromRedirectUrl(redirectUrl.href)
getAccessRequestFromRedirectUrl(redirectedToUrl.href)
).rejects.toThrow(/https:\/\/redirect.url.*requestVcUrl/);
});

it("throws if the redirectUrl query parameter is missing", async () => {
const redirectUrl = new URL("https://redirect.url");
redirectUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);

redirectedToUrl.searchParams.delete("redirectUrl");
await expect(
getAccessRequestFromRedirectUrl(redirectUrl.href)
getAccessRequestFromRedirectUrl(redirectedToUrl.href)
).rejects.toThrow(/https:\/\/redirect.url.*redirectUrl/);
});

it("uses the default fetch if none is provided", async () => {
const mockedFetch = jest.fn(fetch);
const embeddedFetch = jest.requireMock(
"../../common/util/getSessionFetch"
) as jest.Mocked<{ getSessionFetch: typeof getSessionFetch }>;
embeddedFetch.getSessionFetch.mockResolvedValueOnce(mockedFetch);

const vcModule = jest.requireMock(
"@inrupt/solid-client-vc"
) as jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
vcModule.getVerifiableCredential.mockResolvedValueOnce(
mockAccessRequestVc()
);

const redirectedToUrl = new URL("https://redirect.url");
redirectedToUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);
redirectedToUrl.searchParams.append(
"redirectUrl",
encodeURI("https://requestor.redirect.url")
);

await getAccessRequestFromRedirectUrl(redirectedToUrl.href, {
fetch: mockedFetch,
});
Expand All @@ -98,26 +87,6 @@ describe("getAccessRequestFromRedirectUrl", () => {
});

it("uses the provided fetch if any", async () => {
const vcModule = jest.requireMock(
"@inrupt/solid-client-vc"
) as jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
vcModule.getVerifiableCredential.mockResolvedValueOnce(
mockAccessRequestVc()
);

const redirectedToUrl = new URL("https://redirect.url");
redirectedToUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);
redirectedToUrl.searchParams.append(
"redirectUrl",
encodeURI("https://requestor.redirect.url")
);

const mockedFetch = jest.fn(fetch);
await getAccessRequestFromRedirectUrl(redirectedToUrl.href, {
fetch: mockedFetch,
});
Expand All @@ -130,26 +99,6 @@ describe("getAccessRequestFromRedirectUrl", () => {
});

it("returns the fetched VC and the redirect URL", async () => {
const vcModule = jest.requireMock(
"@inrupt/solid-client-vc"
) as jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
vcModule.getVerifiableCredential
.mockResolvedValueOnce(mockAccessRequestVc())
// We test this twice, once for string and one for URL object.
.mockResolvedValueOnce(mockAccessRequestVc());

const redirectedToUrl = new URL("https://redirect.url");
redirectedToUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);
redirectedToUrl.searchParams.append(
"redirectUrl",
encodeURI("https://requestor.redirect.url")
);

// Check that both URL strings and objects are supported.
const accessRequestFromString = await getAccessRequestFromRedirectUrl(
redirectedToUrl.href
Expand All @@ -172,34 +121,13 @@ describe("getAccessRequestFromRedirectUrl", () => {
});

it("throws if the fetched VC is not an Access Request", async () => {
const vcModule = jest.requireMock(
"@inrupt/solid-client-vc"
) as jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
vcModule.getVerifiableCredential.mockResolvedValueOnce(mockAccessGrantVc());

const redirectedToUrl = new URL("https://redirect.url");
redirectedToUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);
redirectedToUrl.searchParams.append(
"redirectUrl",
encodeURI("https://requestor.redirect.url")
);

await expect(
getAccessRequestFromRedirectUrl(redirectedToUrl.href)
).rejects.toThrow();
});

it("normalizes equivalent JSON-LD VCs", async () => {
const vcModule = jest.requireMock(
"@inrupt/solid-client-vc"
) as jest.Mocked<{
getVerifiableCredential: typeof getVerifiableCredential;
}>;
const normalizedAccessRequest = mockAccessRequestVc();
// The server returns an equivalent JSON-LD with a different frame:
vcModule.getVerifiableCredential.mockResolvedValueOnce({
Expand All @@ -216,15 +144,6 @@ describe("getAccessRequestFromRedirectUrl", () => {
},
},
});
const redirectedToUrl = new URL("https://redirect.url");
redirectedToUrl.searchParams.append(
"requestVcUrl",
encodeURI("https://some.vc")
);
redirectedToUrl.searchParams.append(
"redirectUrl",
encodeURI("https://requestor.redirect.url")
);
const { accessRequest } = await getAccessRequestFromRedirectUrl(
redirectedToUrl
);
Expand Down
Loading

1 comment on commit 058c19f

@vercel
Copy link

@vercel vercel bot commented on 058c19f Jul 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.