Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDK-3311: Integrate problem details library #1103

Merged
merged 3 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The following changes have been implemented but not released yet:

## Unreleased

### New feature

- Integrate @inrupt/solid-client-errors for handling HTTP errors.
edwardsph marked this conversation as resolved.
Show resolved Hide resolved

## [1.0.3](https://github.com/inrupt/solid-client-vc-js/releases/tag/v1.0.3) - 2024-05-15

### Patch changes
Expand Down
4 changes: 3 additions & 1 deletion e2e/node/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,9 @@ describe("End-to-end verifiable credentials tests for environment", () => {
fetch: session.fetch,
},
);
await expect(vcPromise).rejects.toThrow(/400/);
await expect(vcPromise).rejects.toThrow(
`The VC issuing endpoint [${issuerService}] could not successfully issue a VC`,
);
});
});

Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
},
"dependencies": {
"@inrupt/solid-client": "^2.0.0",
"@inrupt/solid-client-errors": "^0.0.1",
"event-emitter-promisify": "^1.1.0",
"jsonld-context-parser": "^3.0.0",
"jsonld-streaming-parser": "^3.3.0",
Expand Down
35 changes: 20 additions & 15 deletions src/common/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import type { IJsonLdContext } from "jsonld-context-parser";
import { DataFactory, Store } from "n3";
import { isomorphic } from "rdf-isomorphic";
import { UnauthorizedError } from "@inrupt/solid-client-errors";
import { jsonLdStringToStore } from "../parser/jsonld";
import type { VerifiableCredential } from "./common";
import {
Expand All @@ -44,6 +45,7 @@
import { cred, rdf } from "./constants";
import isRdfjsVerifiableCredential from "./isRdfjsVerifiableCredential";
import isRdfjsVerifiablePresentation from "./isRdfjsVerifiablePresentation";
import { mockedFetchWithResponse } from "../tests.internal";

const { namedNode, quad, blankNode } = DataFactory;

Expand Down Expand Up @@ -529,27 +531,30 @@
let mockedFetch: jest.MockedFunction<typeof fetch>;

beforeEach(() => {
mockedFetch = jest.fn<typeof fetch>().mockResolvedValueOnce(
new Response(undefined, {
status: 401,
statusText: "Unauthenticated",
}),
mockedFetch = mockedFetchWithResponse(
401,
`{"status": 401, "title": "Unauthorized", "detail": "Example detail"}`,
{ "Content-Type": "application/problem+json" },
Comment on lines +534 to +537
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be helpful to expose a mock function from the base error library? I reckon we'll do this type of mock for all libraries being integrated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps, although there are probably other common mock functions that could be in a base library but not errors.

);
});

it.each([[true], [false]])(
"returnLegacyJsonld: %s",
async (returnLegacyJsonld) => {
await expect(
getVerifiableCredential(
"https://example.org/ns/someCredentialInstance",
{
fetch: mockedFetch,
returnLegacyJsonld,
},
),
).rejects.toThrow(
"Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed: 401 Unauthenticated",
const err: UnauthorizedError = await getVerifiableCredential(
"https://example.org/ns/someCredentialInstance",
{
fetch: mockedFetch,
returnLegacyJsonld,
},
).catch((e) => e);

expect(err).toBeInstanceOf(UnauthorizedError);
expect(err.problemDetails.status).toBe(401);
expect(err.problemDetails.title).toBe("Unauthorized");
expect(err.problemDetails.detail).toBe("Example detail");
expect(err.message).toBe(
"Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed",
);
},
);
Expand Down Expand Up @@ -615,7 +620,7 @@

// FIXME: Enable this when we add content type checks in the next major version
// see https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1414124524
it.skip("throws if the dereferenced data has an unsupported content type", async () => {

Check warning on line 623 in src/common/common.test.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Disabled test
const mockedFetch = jest
.fn<typeof fetch>()
.mockResolvedValueOnce(
Expand Down
8 changes: 6 additions & 2 deletions src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from "@inrupt/solid-client";
import type { DatasetCore, Quad } from "@rdfjs/types";
import { DataFactory } from "n3";
import { handleErrorResponse } from "@inrupt/solid-client-errors";
import type { ParseOptions } from "../parser/jsonld";
import { jsonLdToStore } from "../parser/jsonld";
import isRdfjsVerifiableCredential from "./isRdfjsVerifiableCredential";
Expand Down Expand Up @@ -743,8 +744,11 @@ export async function getVerifiableCredential(
const response = await authFetch(vcUrl);

if (!response.ok) {
throw new Error(
`Fetching the Verifiable Credential [${vcUrl}] failed: ${response.status} ${response.statusText}`,
const responseBody = await response.text();
throw handleErrorResponse(
response,
responseBody,
`Fetching the Verifiable Credential [${vcUrl}] failed`,
);
}

Expand Down
33 changes: 19 additions & 14 deletions src/issue/issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
//

import { jest, describe, it, expect } from "@jest/globals";
import { BadRequestError } from "@inrupt/solid-client-errors";
import { defaultContext, defaultCredentialTypes } from "../common/common";
import { mockDefaultCredential } from "../common/common.mock";
import { mockedFetchWithResponse } from "../tests.internal";
import { issueVerifiableCredential } from "./issue";

describe("issueVerifiableCredential", () => {
Expand Down Expand Up @@ -57,21 +59,24 @@ describe("issueVerifiableCredential", () => {
});

it("throws if the issuer returns an error", async () => {
const mockedFetch = jest.fn<typeof fetch>().mockResolvedValueOnce(
new Response(undefined, {
status: 400,
statusText: "Bad request",
}),
const mockedFetch = mockedFetchWithResponse(
400,
`{"status": 400, "title": "Bad request", "detail": "Example detail"}`,
{ "Content-Type": "application/problem+json" },
);
await expect(
issueVerifiableCredential(
"https://some.endpoint",
{ "@context": ["https://some.context"] },
{ "@context": ["https://some.context"] },
{ fetch: mockedFetch },
),
).rejects.toThrow(
/https:\/\/some\.endpoint.*could not successfully issue a VC.*400.*Bad request/,
const err: BadRequestError = await issueVerifiableCredential(
"https://some.endpoint",
{ "@context": ["https://some.context"] },
{ "@context": ["https://some.context"] },
{ fetch: mockedFetch },
).catch((e) => e);

expect(err).toBeInstanceOf(BadRequestError);
expect(err.problemDetails.status).toBe(400);
expect(err.problemDetails.title).toBe("Bad request");
expect(err.problemDetails.detail).toBe("Example detail");
expect(err.message).toMatch(
/https:\/\/some\.endpoint.*could not successfully issue a VC/,
);
});

Expand Down
9 changes: 6 additions & 3 deletions src/issue/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* @module issue
*/

import { handleErrorResponse } from "@inrupt/solid-client-errors";
import type {
Iri,
JsonLd,
Expand Down Expand Up @@ -181,9 +182,11 @@ export async function issueVerifiableCredential(
},
);
if (!response.ok) {
// TODO: use the error library when available.
edwardsph marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(
`The VC issuing endpoint [${issuerEndpoint}] could not successfully issue a VC: ${response.status} ${response.statusText}`,
const responseBody = await response.text();
throw handleErrorResponse(
response,
responseBody,
`The VC issuing endpoint [${issuerEndpoint}] could not successfully issue a VC`,
);
}

Expand Down
37 changes: 22 additions & 15 deletions src/lookup/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { DataFactory } from "n3";
import { NotFoundError } from "@inrupt/solid-client-errors";
import {
defaultVerifiableClaims,
mockAccessGrant,
Expand All @@ -31,6 +32,7 @@ import {
import { cred, rdf } from "../common/constants";
import type { QueryByExample } from "./query";
import { query } from "./query";
import { mockedFetchWithResponse } from "../tests.internal";

const { namedNode } = DataFactory;
const mockRequest: QueryByExample = {
Expand Down Expand Up @@ -202,22 +204,27 @@ describe("query", () => {
it.each([[true], [false]])(
"throws if the given endpoint returns an error [returnLegacyJsonld: %s]",
async (returnLegacyJsonld) => {
const mockedFetch = jest.fn<typeof fetch>(
async () =>
new Response(undefined, {
status: 404,
}),
const mockedFetch = mockedFetchWithResponse(
404,
`{"status": 404, "title": "Not found", "detail": "Example detail"}`,
{ "Content-Type": "application/problem+json" },
);
const err: NotFoundError = await query(
"https://example.org/query",
{ query: [mockRequest] },
{
fetch: mockedFetch,
returnLegacyJsonld,
},
).catch((e) => e);

expect(err).toBeInstanceOf(NotFoundError);
expect(err.problemDetails.status).toBe(404);
expect(err.problemDetails.title).toBe("Not found");
expect(err.problemDetails.detail).toBe("Example detail");
expect(err.message).toBe(
"The query endpoint [https://example.org/query] returned an error",
);
await expect(() =>
query(
"https://example.org/query",
{ query: [mockRequest] },
{
fetch: mockedFetch,
returnLegacyJsonld,
},
),
).rejects.toThrow();
},
);

Expand Down
16 changes: 10 additions & 6 deletions src/lookup/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import type { DatasetCore } from "@rdfjs/types";
import { DataFactory } from "n3";
import { handleErrorResponse } from "@inrupt/solid-client-errors";
import type {
DatasetWithId,
Iri,
Expand Down Expand Up @@ -91,14 +92,14 @@ export type MinimalPresentation = {
/**
* Send a Verifiable Presentation Request to a query endpoint in order to retrieve
* all Verifiable Credentials matching the query, wrapped in a single Presentation.
*
*
* @example The following shows how to query for credentials of a certain type. Adding
* a reason to the request is helpful when interacting with a user. The resulting
* Verifiable Presentation will wrap zero or more Verifiable Credentials.
*
*
* ```
* const verifiablePresentation = await query(
"https://example.org/query", {
"https://example.org/query", {
query: [{
type: "QueryByExample",
credentialQuery: [
Expand Down Expand Up @@ -175,8 +176,11 @@ export async function query(
body: JSON.stringify(vpRequest),
});
if (!response.ok) {
throw new Error(
`The query endpoint [${queryEndpoint}] returned an error: ${response.status} ${response.statusText}`,
const responseBody = await response.text();
throw handleErrorResponse(
response,
responseBody,
`The query endpoint [${queryEndpoint}] returned an error`,
);
}

Expand Down Expand Up @@ -292,7 +296,7 @@ export async function query(
.map(async (_vc: VerifiableCredentialBase) => {
let vc = _vc;
if (typeof vc !== "object" || vc === null) {
throw new Error(`Verifiable Credentail is an invalid object`);
throw new Error(`Verifiable Credential is an invalid object`);
}

if (options.normalize) {
Expand Down
29 changes: 17 additions & 12 deletions src/revoke/revoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
//

import { jest, it, describe, expect } from "@jest/globals";
import { BadRequestError } from "@inrupt/solid-client-errors";
import defaultRevokeVerifiableCredential, {
revokeVerifiableCredential,
} from "./revoke";
import { mockedFetchWithResponse } from "../tests.internal";

const spiedFetch = jest.spyOn(globalThis, "fetch").mockImplementation(() => {
throw new Error("Unexpected fetch call");
Expand Down Expand Up @@ -59,19 +61,22 @@ describe("revokeVerifiableCredential", () => {
});

it("throws if the issuer returns an error", async () => {
const mockedFetch = jest.fn<typeof fetch>().mockResolvedValue(
new Response(undefined, {
status: 400,
statusText: "Bad request",
}),
const mockedFetch = mockedFetchWithResponse(
400,
`{"status": 400, "title": "Bad request", "detail": "Example detail"}`,
{ "Content-Type": "application/problem+json" },
);
await expect(
revokeVerifiableCredential(
"https://some.endpoint",
"https://some.example#credential",
{ fetch: mockedFetch },
),
).rejects.toThrow(/some.endpoint.*400.*Bad request/);
const err: BadRequestError = await revokeVerifiableCredential(
"https://some.endpoint",
"https://some.example#credential",
{ fetch: mockedFetch },
).catch((e) => e);

expect(err).toBeInstanceOf(BadRequestError);
expect(err.problemDetails.status).toBe(400);
expect(err.problemDetails.title).toBe("Bad request");
expect(err.problemDetails.detail).toBe("Example detail");
expect(err.message).toMatch(/https:\/\/some\.endpoint.*returned an error/);
});

it("sends an appropriate revocation request to the issuer", async () => {
Expand Down
8 changes: 6 additions & 2 deletions src/revoke/revoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
/**
* @module revoke
*/
import { handleErrorResponse } from "@inrupt/solid-client-errors";
import type { Iri } from "../common/common";

/**
Expand Down Expand Up @@ -65,8 +66,11 @@ export async function revokeVerifiableCredential(
}),
});
if (!response.ok) {
throw new Error(
`The issuer [${issuerEndpoint}] returned an error: ${response.status} ${response.statusText}`,
const responseBody = await response.text();
throw handleErrorResponse(
response,
responseBody,
`The issuer [${issuerEndpoint}] returned an error`,
);
}
}
Expand Down
Loading
Loading