Skip to content
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: 2 additions & 2 deletions packages/uma/config/routes/resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
{
"@id": "urn:uma:default:ResourceRegistrationRoute",
"@type": "HttpHandlerRoute",
"methods": [ "POST" ],
"methods": [ "GET", "POST" ],
"handler": { "@id": "urn:uma:default:ResourceRegistrationHandler" },
"path": "/uma/resources/"
},
{
"@id": "urn:uma:default:ResourceRegistrationOpsRoute",
"@type": "HttpHandlerRoute",
"methods": [ "PUT", "DELETE" ],
"methods": [ "GET", "PUT", "DELETE" ],
"handler": { "@id": "urn:uma:default:ResourceRegistrationHandler" },
"path": "/uma/resources/{id}"
}
Expand Down
19 changes: 19 additions & 0 deletions packages/uma/src/routes/ResourceRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,32 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
const { owner } = await this.validator.handleSafe({ request });

switch (request.method) {
case 'GET': return this.handleGet(request, owner);
case 'POST': return this.handlePost(request, owner);
case 'PUT': return this.handlePut(request, owner);
case 'DELETE': return this.handleDelete(request, owner);
default: throw new MethodNotAllowedHttpError([ request.method ]);
}
}

protected async handleGet(request: HttpHandlerRequest, owner: string): Promise<HttpHandlerResponse> {
const id = request.parameters?.id;
if (id) {
const registration = await this.registrationStore.get(id);
if (!registration) {
throw new NotFoundHttpError();
}
if (registration.owner !== owner) {
throw new ForbiddenHttpError()
}
return { status: 200, body: registration.description };
}

// No ID so return the list of all owned resources
const identifiers = await this.ownershipStore.get(owner) ?? [];
return { status: 200, body: identifiers };
}

protected async handlePost(request: HttpHandlerRequest, owner: string): Promise<HttpHandlerResponse> {
const { body } = request;

Expand Down
32 changes: 31 additions & 1 deletion packages/uma/test/unit/routes/ResourceRegistration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ vi.mock('node:crypto', () => ({

describe('ResourceRegistration', (): void => {
const owner = 'owner';
const resource = 'http://example.com/resource';
let input: HttpHandlerContext<ResourceDescription>;
let policyStore: Store;

Expand Down Expand Up @@ -61,7 +62,7 @@ describe('ResourceRegistration', (): void => {
} satisfies Partial<KeyValueStorage<string, ResourceDescription>> as any;

ownershipStore = {
get: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue([ resource ]),
set: vi.fn(),
delete: vi.fn(),
} satisfies Partial<KeyValueStorage<string, string[]>> as any;
Expand All @@ -80,9 +81,38 @@ describe('ResourceRegistration', (): void => {
});

it('throws an error if the method is not allowed.', async(): Promise<void> => {
input.request.method = 'PATCH';
await expect(handler.handle(input)).rejects.toThrow(MethodNotAllowedHttpError);
});

describe('with GET requests', (): void => {
it('can return a list of owned resource identifiers.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toEqual({ status: 200, body: [ resource ] });
expect(ownershipStore.get).toHaveBeenCalledExactlyOnceWith(owner);
});

it('can return the details of a single resource.', async(): Promise<void> => {
input.request.parameters = { id: resource };
await expect(handler.handle(input)).resolves
.toEqual({ status: 200, body: input.request.body });
expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource);
});

it('returns a 404 for unknown resource identifiers.', async(): Promise<void> => {
input.request.parameters = { id: resource };
registrationStore.get.mockResolvedValueOnce(undefined);
await expect(handler.handle(input)).rejects.toThrow(NotFoundHttpError);
expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource);
});

it('returns a 403 if the user is not the actual owner.', async(): Promise<void> => {
input.request.parameters = { id: resource };
registrationStore.get.mockResolvedValueOnce({ owner: 'someone else' } as any);
await expect(handler.handle(input)).rejects.toThrow(ForbiddenHttpError);
expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource);
});
});

describe('with POST requests', (): void => {
beforeEach(async(): Promise<void> => {
input.request.method = 'POST';
Expand Down
24 changes: 23 additions & 1 deletion test/integration/Aggregation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ describe('An aggregation setup', (): void => {
// Update registration with derivation ID
const description: ResourceDescription = {
name: `http://localhost:${aggregatorPort}/resource`,
resource_scopes: [ 'read' ],
resource_scopes: [ 'http://www.w3.org/ns/odrl/2/read' ],
derived_from: [{
issuer: srcConfig.issuer,
derivation_resource_id: derivationId,
Expand All @@ -292,6 +292,28 @@ describe('An aggregation setup', (): void => {
expect(response.status).toBe(200);
});

it('can read the resource registration on the AS.', async(): Promise<void> => {
let response = await fetch(aggConfig.resource_registration_endpoint, {
headers: { authorization: pat }
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual([aggregatedResourceId]);

const url = joinUrl(aggConfig.resource_registration_endpoint, encodeURIComponent(aggregatedResourceId));
response = await fetch(url, {
headers: { authorization: pat },
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
name: `http://localhost:${aggregatorPort}/resource`,
resource_scopes: [ 'http://www.w3.org/ns/odrl/2/read' ],
derived_from: [{
issuer: srcConfig.issuer,
derivation_resource_id: derivationId,
}],
});
});

it('a client cannot read an aggregated resource without the necessary tokens.', async(): Promise<void> => {
// We don't have an actual aggregator server so simulating the request
const body = [{
Expand Down
3 changes: 0 additions & 3 deletions test/integration/Base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ describe('A server setup', (): void => {
headers: { authorization: `WebID ${encodeURIComponent(owner)}`, 'content-type': 'text/turtle' },
body: policy,
});
console.log(await policyResponse.text());
expect(policyResponse.status).toBe(201);

const putResponse = await fetch(collectionResource, {
Expand Down Expand Up @@ -247,15 +246,13 @@ describe('A server setup', (): void => {
headers: { authorization: `WebID ${encodeURIComponent(owner)}`, 'content-type': 'text/turtle' },
body: policy,
});
console.log(await policyResponse.text());
expect(policyResponse.status).toBe(201);

const putResponse = await fetch(collectionResource, {
method: 'PUT',
headers: { 'content-type': 'text/plain' },
body: 'Some new text!',
});
console.log(await putResponse.text());
expect(putResponse.status).toBe(205);

const getResponse = await fetch(collectionResource);
Expand Down