diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d96a15fa4..9b831b491f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The following changes are pending, and will be applied on the next major release The following changes have been implemented but not released yet: +### Bugfixes + +- [1549](https://github.com/inrupt/solid-client-js/issues/1549): The universal API had a bug preventing it from handling correctly resources with a missing ACL. This is now resolved, and it correctly proceeds to creating the target ACL if required. + ## [1.30.1] - 2023-09-15 ### Patch diff --git a/src/universal/getAclServerResourceInfo.test.ts b/src/universal/getAclServerResourceInfo.test.ts index dd2c428082..cb68e073ab 100644 --- a/src/universal/getAclServerResourceInfo.test.ts +++ b/src/universal/getAclServerResourceInfo.test.ts @@ -20,35 +20,78 @@ // import { jest, describe, it, expect } from "@jest/globals"; -import { getResourceInfo } from "../resource/resource"; +import * as ResourceModule from "../resource/resource"; import { getAclServerResourceInfo } from "./getAclServerResourceInfo"; +import type { WithServerResourceInfo } from "../interfaces"; + +const { getResourceInfo } = ResourceModule; jest.mock("../resource/resource", () => ({ - getResourceInfo: jest.fn().mockImplementation(() => ({})), + getResourceInfo: jest.fn(), })); +const mockResourceInfo = (options: { + aclUrl?: string; +}): WithServerResourceInfo["internal_resourceInfo"] => ({ + isRawData: false, + aclUrl: options.aclUrl, + sourceIri: "https://example.org/some-resource", + linkedResources: {}, +}); + describe("getAclServerResourceInfo", () => { it("fetches the ACL resource info if the resource has an ACL", async () => { - await getAclServerResourceInfo({ - internal_resourceInfo: { aclUrl: "x" }, - } as any); - expect(getResourceInfo).toHaveBeenCalledTimes(1); - expect(getResourceInfo).toHaveBeenCalledWith("x", undefined); + const aclResourceInfo = mockResourceInfo({}); + const { getResourceInfo } = jest.mocked(ResourceModule); + getResourceInfo.mockResolvedValueOnce({ + internal_resourceInfo: aclResourceInfo, + }); + + await expect( + getAclServerResourceInfo({ + internal_resourceInfo: mockResourceInfo({ + aclUrl: "https://example.org/some-acl", + }), + }), + ).resolves.toStrictEqual({ + internal_resourceInfo: aclResourceInfo, + }); + }); + + it("returns null if the resource ACL cannot be discovered", async () => { + const { getResourceInfo } = jest.mocked(ResourceModule); + getResourceInfo.mockRejectedValueOnce(null); + await expect( + getAclServerResourceInfo({ + internal_resourceInfo: mockResourceInfo({ + aclUrl: "https://example.org/some-missing-acl", + }), + }), + ).resolves.toBeNull(); }); - it("returns null if the resource has no ACL", async () => { - await getAclServerResourceInfo({ - internal_resourceInfo: { aclUrl: undefined }, - } as any); - expect(getResourceInfo).toHaveBeenCalledTimes(0); + it("returns null if fetching the resource ACL fails", async () => { + await expect( + getAclServerResourceInfo({ + internal_resourceInfo: mockResourceInfo({ aclUrl: undefined }), + }), + ).resolves.toBeNull(); }); it("passes the fetch option to fetch the ACL resource info", async () => { + const mockedFetch = jest.fn(); await getAclServerResourceInfo( - { internal_resourceInfo: { aclUrl: "x" } } as any, - { fetch: "x" } as any, + { + internal_resourceInfo: mockResourceInfo({ + aclUrl: "https://example.org/some-acl", + }), + }, + { fetch: mockedFetch }, ); expect(getResourceInfo).toHaveBeenCalledTimes(1); - expect(getResourceInfo).toHaveBeenCalledWith("x", { fetch: "x" }); + expect(getResourceInfo).toHaveBeenCalledWith( + "https://example.org/some-acl", + { fetch: mockedFetch }, + ); }); }); diff --git a/src/universal/getAclServerResourceInfo.ts b/src/universal/getAclServerResourceInfo.ts index f6c2fb2ede..6ead3a327b 100644 --- a/src/universal/getAclServerResourceInfo.ts +++ b/src/universal/getAclServerResourceInfo.ts @@ -39,8 +39,18 @@ export async function getAclServerResourceInfo( resource: WithServerResourceInfo, options?: DefaultOptions, ): Promise { - if (typeof resource.internal_resourceInfo.aclUrl === "string") { - return getResourceInfo(resource.internal_resourceInfo.aclUrl, options); + if (typeof resource.internal_resourceInfo.aclUrl !== "string") { + return null; + } + try { + return await getResourceInfo( + resource.internal_resourceInfo.aclUrl, + options, + ); + } catch { + // A WAC-governed resource may have a link to a non-existant ACL (by design). + // The absence of an ACL at the target URL is a useful information that is + // used by the universal API to pick between ACR and WAC. + return null; } - return null; }