Skip to content

Commit

Permalink
Add Runtime errors for IdentityMetadata use cases (#383)
Browse files Browse the repository at this point in the history
* refactor: restructure tests of IdentityMetadata use cases

* feat: include Runtime errors for IdentityMetadata use cases

* test: use specialized error for unfindable IdentityMetadata records

* test: upsert IdentityMetadata only for known Identities

* refactor: more precise wording of error message
  • Loading branch information
britsta authored Jan 9, 2025
1 parent 1e8f177 commit 3db301c
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 40 deletions.
14 changes: 14 additions & 0 deletions packages/runtime/src/useCases/common/RuntimeErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,19 @@ class IdentityDeletionProcess {
}
}

class IdentityMetadata {
public notFound() {
return new ApplicationError("error.runtime.identityMetadata.notFound", "There is no stored IdentityMetadata for the specified combination of reference and key.");
}

public unfamiliarReferencedIdentity() {
return new ApplicationError(
"error.runtime.identityMetadata.unfamiliarReferencedIdentity",
"The reference of the IdentityMetadata resolves neither to the address of a peer of a Relationship nor the address of the own Identity."
);
}
}

class IdentityRecoveryKits {
public datawalletDisabled() {
return new ApplicationError(
Expand Down Expand Up @@ -303,6 +316,7 @@ export class RuntimeErrors {
public static readonly notifications = new Notifications();
public static readonly attributes = new Attributes();
public static readonly identityDeletionProcess = new IdentityDeletionProcess();
public static readonly identityMetadata = new IdentityMetadata();
public static readonly identityRecoveryKits = new IdentityRecoveryKits();
public static readonly deciderModule = new DeciderModule();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result } from "@js-soft/ts-utils";
import { IdentityMetadata, IdentityMetadataController } from "@nmshd/consumption";
import { IdentityMetadataController } from "@nmshd/consumption";
import { CoreAddress } from "@nmshd/core-types";
import { AccountController } from "@nmshd/transport";
import { Inject } from "@nmshd/typescript-ioc";
Expand Down Expand Up @@ -29,7 +29,7 @@ export class DeleteIdentityMetadataUseCase extends UseCase<DeleteIdentityMetadat
protected override async executeInternal(request: DeleteIdentityMetadataRequest): Promise<Result<void>> {
const identityMetadata = await this.identityMetadataController.getIdentityMetadata(CoreAddress.from(request.reference), request.key);
if (!identityMetadata) {
return Result.fail(RuntimeErrors.general.recordNotFound(IdentityMetadata));
return Result.fail(RuntimeErrors.identityMetadata.notFound());
}

await this.identityMetadataController.deleteIdentityMetadata(identityMetadata);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result } from "@js-soft/ts-utils";
import { IdentityMetadata, IdentityMetadataController } from "@nmshd/consumption";
import { IdentityMetadataController } from "@nmshd/consumption";
import { CoreAddress } from "@nmshd/core-types";
import { Inject } from "@nmshd/typescript-ioc";
import { IdentityMetadataDTO } from "../../../types";
Expand Down Expand Up @@ -29,7 +29,7 @@ export class GetIdentityMetadataUseCase extends UseCase<GetIdentityMetadataReque
protected override async executeInternal(request: GetIdentityMetadataRequest): Promise<Result<IdentityMetadataDTO>> {
const identityMetadata = await this.identityMetadataController.getIdentityMetadata(CoreAddress.from(request.reference), request.key);
if (!identityMetadata) {
return Result.fail(RuntimeErrors.general.recordNotFound(IdentityMetadata));
return Result.fail(RuntimeErrors.identityMetadata.notFound());
}

return Result.ok(IdentityMetadataMapper.toIdentityMetadataDTO(identityMetadata));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { JSONWrapper } from "@js-soft/ts-serval";
import { Result } from "@js-soft/ts-utils";
import { IdentityMetadataController } from "@nmshd/consumption";
import { CoreAddress } from "@nmshd/core-types";
import { AccountController } from "@nmshd/transport";
import { AccountController, RelationshipsController } from "@nmshd/transport";
import { Inject } from "@nmshd/typescript-ioc";
import { IdentityMetadataDTO } from "../../../types";
import { AddressString, SchemaRepository, SchemaValidator } from "../../common";
import { AddressString, RuntimeErrors, SchemaRepository, SchemaValidator } from "../../common";
import { UseCase } from "../../common/UseCase";
import { IdentityMetadataMapper } from "./IdentityMetadataMapper";

Expand All @@ -24,15 +24,25 @@ class Validator extends SchemaValidator<UpsertIdentityMetadataRequest> {
export class UpsertIdentityMetadataUseCase extends UseCase<UpsertIdentityMetadataRequest, IdentityMetadataDTO> {
public constructor(
@Inject private readonly identityMetadataController: IdentityMetadataController,
@Inject private readonly relationshipsController: RelationshipsController,
@Inject private readonly accountController: AccountController,
@Inject validator: Validator
) {
super(validator);
}

protected override async executeInternal(request: UpsertIdentityMetadataRequest): Promise<Result<IdentityMetadataDTO>> {
const referencedIdentity = CoreAddress.from(request.reference);
const peersOfRelationships = (await this.relationshipsController.getRelationships({})).map((relationship) => relationship.peer.address);

const referencedIdentityIsFamiliar =
peersOfRelationships.some((peer) => referencedIdentity.equals(peer)) || referencedIdentity.equals(this.accountController.identity.address);
if (!referencedIdentityIsFamiliar) {
return Result.fail(RuntimeErrors.identityMetadata.unfamiliarReferencedIdentity());
}

const value = JSONWrapper.fromAny(request.value);
const identityMetadata = await this.identityMetadataController.upsertIdentityMetadata({ reference: CoreAddress.from(request.reference), key: request.key, value });
const identityMetadata = await this.identityMetadataController.upsertIdentityMetadata({ reference: referencedIdentity, key: request.key, value });

await this.accountController.syncDatawallet();

Expand Down
118 changes: 85 additions & 33 deletions packages/runtime/test/consumption/identityMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { IdentityMetadata } from "@nmshd/consumption";
import { Random, RandomCharacterRange } from "@nmshd/transport";
import { ConsumptionServices } from "../../src";
import { RuntimeServiceProvider } from "../lib";
import { AddressString } from "src/useCases/common";
import { ConsumptionServices, TransportServices } from "../../src";
import { establishRelationship, RuntimeServiceProvider } from "../lib";

const runtimeServiceProvider = new RuntimeServiceProvider();
let consumptionServices: ConsumptionServices;
let transportServices: TransportServices;
let peerTransportServices: TransportServices;

let ownAddress: AddressString;
let peerAddress: AddressString;

beforeAll(async () => {
const runtimeServices = await runtimeServiceProvider.launch(1);
const runtimeServices = await runtimeServiceProvider.launch(2);
consumptionServices = runtimeServices[0].consumption;
transportServices = runtimeServices[0].transport;
peerTransportServices = runtimeServices[1].transport;

await establishRelationship(transportServices, peerTransportServices);

ownAddress = (await transportServices.account.getIdentityInfo()).value.address;
peerAddress = (await peerTransportServices.account.getIdentityInfo()).value.address;
}, 30000);

afterAll(async () => await runtimeServiceProvider.stop());
Expand All @@ -22,53 +35,92 @@ afterEach(async () => {
}
});

describe("IdentityMetadata", () => {
test.each([
{
reference: "did:e:localhost:dids:1234567890abcdef123456",
key: undefined,
value: "value"
},
{
reference: "did:e:localhost:dids:1234567890abcdef123456",
key: undefined,
value: { a: "json" }
},
{
reference: "did:e:localhost:dids:1234567890abcdef123456",
key: "key",
value: "value"
}
])("should upsert an IdentityMetadata with key '$key' and value '$value'", async (data) => {
const result = await consumptionServices.identityMetadata.upsertIdentityMetadata(data);
describe("Upsert IdentityMetadata", () => {
test("should upsert an IdentityMetadata with a string as value", async () => {
const upsertRequest = { reference: peerAddress, value: "value" };
const result = await consumptionServices.identityMetadata.upsertIdentityMetadata(upsertRequest);
expect(result).toBeSuccessful();

const identityMetadata = result.value;
expect(identityMetadata.reference.toString()).toStrictEqual(upsertRequest.reference);
expect(identityMetadata.key).toBeUndefined();
expect(identityMetadata.value).toStrictEqual(upsertRequest.value);
});

test("should upsert an IdentityMetadata with a JSON object as value", async () => {
const upsertRequest = { reference: peerAddress, value: { a: "json" } };
const result = await consumptionServices.identityMetadata.upsertIdentityMetadata(upsertRequest);
expect(result).toBeSuccessful();

const identityMetadata = result.value;
expect(identityMetadata.reference.toString()).toStrictEqual(upsertRequest.reference);
expect(identityMetadata.key).toBeUndefined();
expect(identityMetadata.value).toStrictEqual(upsertRequest.value);
});

test("should upsert an IdentityMetadata with a key", async () => {
const upsertRequest = { reference: peerAddress, key: "key", value: "value" };
const result = await consumptionServices.identityMetadata.upsertIdentityMetadata(upsertRequest);
expect(result).toBeSuccessful();

const identityMetadata = result.value;
expect(identityMetadata.reference.toString()).toStrictEqual(data.reference);
expect(identityMetadata.key).toStrictEqual(data.key);
expect(identityMetadata.value).toStrictEqual(data.value);
expect(identityMetadata.reference.toString()).toStrictEqual(upsertRequest.reference);
expect(identityMetadata.key).toStrictEqual(upsertRequest.key);
expect(identityMetadata.value).toStrictEqual(upsertRequest.value);
});

test("should upsert an IdentityMetadata for the own Identity", async () => {
const upsertRequest = { reference: ownAddress, value: "value" };
const result = await consumptionServices.identityMetadata.upsertIdentityMetadata(upsertRequest);
expect(result).toBeSuccessful();

const identityMetadata = result.value;
expect(identityMetadata.reference.toString()).toStrictEqual(upsertRequest.reference);
expect(identityMetadata.key).toBeUndefined();
expect(identityMetadata.value).toStrictEqual(upsertRequest.value);
});

test("cannot upsert an IdentityMetadata if the referenced Identity is unfamiliar", async () => {
const unknownIdentityReference = await generateReference();
const result = await consumptionServices.identityMetadata.upsertIdentityMetadata({ reference: unknownIdentityReference, value: "value" });
expect(result).toBeAnError(
"The reference of the IdentityMetadata resolves neither to the address of a peer of a Relationship nor the address of the own Identity.",
"error.runtime.identityMetadata.unfamiliarReferencedIdentity"
);
});
});

describe("Get IdentityMetadata", () => {
test("should get an IdentityMetadata", async () => {
const reference = await generateReference();
await consumptionServices.identityMetadata.upsertIdentityMetadata({ reference: reference, value: "value" });
await consumptionServices.identityMetadata.upsertIdentityMetadata({ reference: peerAddress, value: "value" });

const result = await consumptionServices.identityMetadata.getIdentityMetadata({ reference: reference });
const result = await consumptionServices.identityMetadata.getIdentityMetadata({ reference: peerAddress });
expect(result).toBeSuccessful();

const identityMetadata = result.value;
expect(identityMetadata.value).toBe("value");
});

test("cannot get an IdentityMetadata if it does not exist", async () => {
const result = await consumptionServices.identityMetadata.getIdentityMetadata({ reference: peerAddress });
expect(result).toBeAnError("There is no stored IdentityMetadata for the specified combination of reference and key.", "error.runtime.identityMetadata.notFound");
});
});

describe("Delete IdentityMetadata", () => {
test("should delete an IdentityMetadata", async () => {
const reference = await generateReference();
await consumptionServices.identityMetadata.upsertIdentityMetadata({ reference: reference, value: "value" });
await consumptionServices.identityMetadata.upsertIdentityMetadata({ reference: peerAddress, value: "value" });

const result = await consumptionServices.identityMetadata.deleteIdentityMetadata({ reference: reference });
const result = await consumptionServices.identityMetadata.deleteIdentityMetadata({ reference: peerAddress });
expect(result).toBeSuccessful();

const getResult = await consumptionServices.identityMetadata.getIdentityMetadata({ reference: reference });
expect(getResult).toBeAnError("IdentityMetadata not found. Make sure the ID exists and the record is not expired.", "error.runtime.recordNotFound");
const getResult = await consumptionServices.identityMetadata.getIdentityMetadata({ reference: peerAddress });
expect(getResult).toBeAnError("There is no stored IdentityMetadata for the specified combination of reference and key.", "error.runtime.identityMetadata.notFound");
});

test("cannot delete an IdentityMetadata if it does not exist", async () => {
const result = await consumptionServices.identityMetadata.deleteIdentityMetadata({ reference: peerAddress });
expect(result).toBeAnError("There is no stored IdentityMetadata for the specified combination of reference and key.", "error.runtime.identityMetadata.notFound");
});
});

Expand Down

0 comments on commit 3db301c

Please sign in to comment.