diff --git a/.dev/appsettings.override.json b/.dev/appsettings.override.json index 6f4e08224..e655dfaec 100644 --- a/.dev/appsettings.override.json +++ b/.dev/appsettings.override.json @@ -49,6 +49,9 @@ } } } + }, + "Application": { + "didDomainName": "localhost" } }, "Files": { @@ -69,6 +72,9 @@ "Provider": "Postgres", "ConnectionString": "User ID=messages;Password=Passw0rd;Server=postgres;Port=5432;Database=enmeshed;" } + }, + "Application": { + "didDomainName": "localhost" } }, "Relationships": { @@ -77,6 +83,9 @@ "Provider": "Postgres", "ConnectionString": "User ID=relationships;Password=Passw0rd;Server=postgres;Port=5432;Database=enmeshed;" } + }, + "Application": { + "didDomainName": "localhost" } }, "Synchronization": { diff --git a/.dev/compose.backbone.env b/.dev/compose.backbone.env index 775d5b6c9..358d4582c 100644 --- a/.dev/compose.backbone.env +++ b/.dev/compose.backbone.env @@ -1 +1 @@ -BACKBONE_VERSION=5.9.1 +BACKBONE_VERSION=6.4.0 diff --git a/.dev/compose.backbone.yml b/.dev/compose.backbone.yml index 26e3b8d3a..8428deec3 100644 --- a/.dev/compose.backbone.yml +++ b/.dev/compose.backbone.yml @@ -108,7 +108,7 @@ services: image: postgres environment: - PGPASSWORD=Passw0rd - command: /bin/bash -c 'env && apt update -y && apt install -y wget && wget https://raw.githubusercontent.com/nmshd/backbone/${BACKBONE_VERSION}/setup-db/setup-postgres.sql -O /setup-postgres.sql && psql -h postgres -U postgres -d enmeshed -f /setup-postgres.sql' + command: /bin/bash -c 'env && apt update -y && apt install -y wget && wget https://raw.githubusercontent.com/nmshd/backbone/${BACKBONE_VERSION}/scripts/sql/postgres/setup.sql -O /setup-postgres.sql && psql -h postgres -U postgres -d enmeshed -f /setup-postgres.sql' depends_on: database: condition: service_healthy diff --git a/.github/workflows/publish-prereleases.yml b/.github/workflows/publish-prereleases.yml index 09662f375..94621f536 100644 --- a/.github/workflows/publish-prereleases.yml +++ b/.github/workflows/publish-prereleases.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm run build:ci -ws diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index edf9e28b1..5fe59345f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm run build:ci -ws diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33c1abfbd..ffe9d78e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node - run: npm run test:ci --workspace=@nmshd/app-runtime @@ -46,7 +46,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - name: Start MongoDB uses: supercharge/mongodb-github-action@v1 @@ -71,7 +71,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - uses: js-soft/ferretdb-github-action@1.1.0 with: @@ -93,7 +93,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node - run: npm run test:ci:lokijs --workspace=@nmshd/consumption @@ -108,7 +108,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci env: BUILD_NUMBER: ${{ github.run_number }} @@ -129,7 +129,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - uses: supercharge/mongodb-github-action@v1 - run: npm ci - run: npm run build:node @@ -155,7 +155,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - uses: js-soft/ferretdb-github-action@1.1.0 with: ferretdb-telemetry: "enabled" @@ -179,7 +179,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node - run: npm run build:schemas --workspace=@nmshd/runtime @@ -198,7 +198,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node - name: Start MongoDB @@ -224,7 +224,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node - uses: js-soft/ferretdb-github-action@1.1.0 @@ -247,7 +247,7 @@ jobs: run: npm run start:backbone - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node env: @@ -266,7 +266,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: current + node-version: 22.4.1 - run: npm ci - run: npm run build:node env: diff --git a/.vscode/settings.json b/.vscode/settings.json index 36734b026..6b81ef66c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,11 +49,8 @@ "xmlTools.splitAttributesOnFormat": true, "xmlTools.enforcePrettySelfClosingTagOnFormat": true, "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" + "source.organizeImports": "always" }, "files.eol": "\n", - "typescript.unstable": { - "organizeImportsIgnoreCase": true - }, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/package-lock.json b/package-lock.json index 1eed78122..1b67ccb53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10734,7 +10734,7 @@ }, "packages/app-runtime": { "name": "@nmshd/app-runtime", - "version": "3.3.0", + "version": "5.0.0", "license": "MIT", "dependencies": { "@js-soft/docdb-access-loki": "^1.1.0", @@ -10748,12 +10748,12 @@ "@types/luxon": "^3.4.2" }, "peerDependencies": { - "@nmshd/runtime": "^4.11.0" + "@nmshd/runtime": "^5.0.0" } }, "packages/consumption": { "name": "@nmshd/consumption", - "version": "3.12.4", + "version": "5.0.0", "license": "MIT", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", @@ -10775,7 +10775,7 @@ }, "packages/content": { "name": "@nmshd/content", - "version": "2.11.0", + "version": "5.0.0", "license": "MIT", "dependencies": { "@js-soft/logging-abstractions": "^1.0.1", @@ -10800,17 +10800,17 @@ }, "packages/runtime": { "name": "@nmshd/runtime", - "version": "4.14.4", + "version": "5.0.0", "license": "MIT", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", "@js-soft/logging-abstractions": "^1.0.1", "@js-soft/ts-serval": "2.0.10", "@js-soft/ts-utils": "^2.3.3", - "@nmshd/consumption": "3.12.4", - "@nmshd/content": "2.11.0", + "@nmshd/consumption": "5.0.0", + "@nmshd/content": "5.0.0", "@nmshd/crypto": "2.0.6", - "@nmshd/transport": "2.8.2", + "@nmshd/transport": "5.0.0", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", @@ -10842,7 +10842,7 @@ }, "packages/transport": { "name": "@nmshd/transport", - "version": "2.8.2", + "version": "5.0.0", "license": "MIT", "dependencies": { "@js-soft/docdb-access-abstractions": "1.0.4", diff --git a/package.json b/package.json index 5d74bd79d..daa097b8b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "lint:prettier": "prettier --check .", "lint:tsc": "npm run -ws lint:tsc", "outdated": "ncu -i && ncu -i -ws", + "pull:backbone": "docker compose -p runtime-test-backbone --env-file .dev/compose.backbone.env -f .dev/compose.backbone.yml pull", "start:backbone": "docker compose -p runtime-test-backbone --env-file .dev/compose.backbone.env -f .dev/compose.backbone.yml up -d", "teardown:backbone": "docker compose -p runtime-test-backbone --env-file .dev/compose.backbone.env -f .dev/compose.backbone.yml down -v", "test:teardown": "docker compose -f .dev/compose.yml down -fsv" diff --git a/packages/app-runtime/package.json b/packages/app-runtime/package.json index 989595083..7b41a8075 100644 --- a/packages/app-runtime/package.json +++ b/packages/app-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/app-runtime", - "version": "3.3.0", + "version": "5.0.0", "description": "The App Runtime", "homepage": "https://enmeshed.eu", "repository": { @@ -63,7 +63,7 @@ "@types/luxon": "^3.4.2" }, "peerDependencies": { - "@nmshd/runtime": "^4.11.0" + "@nmshd/runtime": "^5.0.0" }, "publishConfig": { "access": "public", diff --git a/packages/app-runtime/src/AppConfig.ts b/packages/app-runtime/src/AppConfig.ts index cb02448b6..4a9c3862a 100644 --- a/packages/app-runtime/src/AppConfig.ts +++ b/packages/app-runtime/src/AppConfig.ts @@ -1,5 +1,5 @@ import { RuntimeConfig } from "@nmshd/runtime"; -import { IConfigOverwrite, Realm } from "@nmshd/transport"; +import { IConfigOverwrite } from "@nmshd/transport"; import { defaultsDeep } from "lodash"; export interface AppConfig extends RuntimeConfig { @@ -23,7 +23,6 @@ export function createAppConfig(...configs: AppConfigOverwrite[]): AppConfig { const appConfig = { accountsDbName: "accounts", transportLibrary: { - realm: Realm.Prod, datawalletEnabled: true }, modules: { diff --git a/packages/app-runtime/src/AppRuntimeErrors.ts b/packages/app-runtime/src/AppRuntimeErrors.ts index ccbf36639..14685caf5 100644 --- a/packages/app-runtime/src/AppRuntimeErrors.ts +++ b/packages/app-runtime/src/AppRuntimeErrors.ts @@ -83,12 +83,6 @@ class PushNotificationModule { } } -class MultiAccount { - public wrongRealm(): UserfriendlyApplicationError { - return new UserfriendlyApplicationError("error.runtime.MultiAccount.WrongRealm", "The given realm is invalid."); - } -} - class Modules { public readonly pushNotificationModule = new PushNotificationModule(); } @@ -97,5 +91,4 @@ export class AppRuntimeErrors { public static readonly general = new General(); public static readonly startup = new Startup(); public static readonly modules = new Modules(); - public static readonly multiAccount = new MultiAccount(); } diff --git a/packages/app-runtime/src/events/OnboardingChangeReceivedEvent.ts b/packages/app-runtime/src/events/OnboardingChangeReceivedEvent.ts index abc6ba1da..04cff56b2 100644 --- a/packages/app-runtime/src/events/OnboardingChangeReceivedEvent.ts +++ b/packages/app-runtime/src/events/OnboardingChangeReceivedEvent.ts @@ -1,16 +1,16 @@ -import { DataEvent, IdentityDVO, RelationshipChangeDTO, RelationshipDTO } from "@nmshd/runtime"; +import { DataEvent, IdentityDVO, RelationshipAuditLogEntryDTO, RelationshipDTO } from "@nmshd/runtime"; export class OnboardingChangeReceivedEvent extends DataEvent<{ - change: RelationshipChangeDTO; relationship: RelationshipDTO; + auditLogEntry: RelationshipAuditLogEntryDTO; identity: IdentityDVO; }> { public static readonly namespace: string = "app.onboardingChangeReceived"; - public constructor(address: string, change: RelationshipChangeDTO, relationship: RelationshipDTO, identity: IdentityDVO) { + public constructor(address: string, relationship: RelationshipDTO, auditLogEntry: RelationshipAuditLogEntryDTO, identity: IdentityDVO) { super(OnboardingChangeReceivedEvent.namespace, address, { - change, relationship, + auditLogEntry, identity }); } diff --git a/packages/app-runtime/src/extensibility/facades/AppRelationshipFacade.ts b/packages/app-runtime/src/extensibility/facades/AppRelationshipFacade.ts index fc0511833..d5393d0a8 100644 --- a/packages/app-runtime/src/extensibility/facades/AppRelationshipFacade.ts +++ b/packages/app-runtime/src/extensibility/facades/AppRelationshipFacade.ts @@ -1,12 +1,12 @@ import { - AcceptRelationshipChangeRequest, + AcceptRelationshipRequest, CreateRelationshipRequest, GetRelationshipByAddressRequest, GetRelationshipRequest, GetRelationshipsRequest, IdentityDVO, - RejectRelationshipChangeRequest, - RevokeRelationshipChangeRequest + RejectRelationshipRequest, + RevokeRelationshipRequest } from "@nmshd/runtime"; import { UserfriendlyApplicationError } from "../../UserfriendlyApplicationError"; import { UserfriendlyResult } from "../../UserfriendlyResult"; @@ -43,34 +43,24 @@ export class AppRelationshipFacade extends AppRuntimeFacade { return UserfriendlyResult.ok(dvo); } - public async acceptRelationshipCreationChange(relationshipId: string, content: any): Promise> { - const result = await this.transportServices.relationships.getRelationship({ id: relationshipId }); - if (result.isError) { - return await this.parseErrorResult(result); - } - - const changeId = result.value.changes[0].id; - return await this.acceptRelationshipChange({ relationshipId, changeId, content }); + public async createRelationship(request: CreateRelationshipRequest): Promise> { + const result = await this.transportServices.relationships.createRelationship(request); + return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); } - public async rejectRelationshipCreationChange(relationshipId: string, content: any): Promise> { - const result = await this.transportServices.relationships.getRelationship({ id: relationshipId }); - if (result.isError) { - return await this.parseErrorResult(result); - } - - const changeId = result.value.changes[0].id; - return await this.rejectRelationshipChange({ relationshipId, changeId, content }); + public async acceptRelationship(request: AcceptRelationshipRequest): Promise> { + const result = await this.transportServices.relationships.acceptRelationship(request); + return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); } - public async revokeRelationshipCreationChange(relationshipId: string, content: any): Promise> { - const result = await this.transportServices.relationships.getRelationship({ id: relationshipId }); - if (result.isError) { - return await this.parseErrorResult(result); - } + public async rejectRelationship(request: RejectRelationshipRequest): Promise> { + const result = await this.transportServices.relationships.rejectRelationship(request); + return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); + } - const changeId = result.value.changes[0].id; - return await this.revokeRelationshipChange({ relationshipId, changeId, content }); + public async revokeRelationship(request: RevokeRelationshipRequest): Promise> { + const result = await this.transportServices.relationships.revokeRelationship(request); + return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); } public async getRelationships(request: GetRelationshipsRequest): Promise> { @@ -87,24 +77,4 @@ export class AppRelationshipFacade extends AppRuntimeFacade { const result = await this.transportServices.relationships.getRelationshipByAddress(request); return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); } - - public async createRelationship(request: CreateRelationshipRequest): Promise> { - const result = await this.transportServices.relationships.createRelationship(request); - return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); - } - - public async acceptRelationshipChange(request: AcceptRelationshipChangeRequest): Promise> { - const result = await this.transportServices.relationships.acceptRelationshipChange(request); - return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); - } - - public async rejectRelationshipChange(request: RejectRelationshipChangeRequest): Promise> { - const result = await this.transportServices.relationships.rejectRelationshipChange(request); - return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); - } - - public async revokeRelationshipChange(request: RevokeRelationshipChangeRequest): Promise> { - const result = await this.transportServices.relationships.revokeRelationshipChange(request); - return await this.handleResult(result, (v) => this.expander.expandRelationshipDTO(v)); - } } diff --git a/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts b/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts index 3545ae476..7408b7c19 100644 --- a/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts @@ -1,4 +1,4 @@ -import { RelationshipChangeStatus } from "@nmshd/runtime"; +import { RelationshipAuditLogEntryReason } from "@nmshd/runtime"; import { AppRuntimeError } from "../../AppRuntimeError"; import { OnboardingChangeReceivedEvent } from "../../events"; import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "../AppRuntimeModule"; @@ -17,32 +17,35 @@ export class OnboardingChangeReceivedModule extends AppRuntimeModule { diff --git a/packages/app-runtime/src/modules/runtimeEvents/RelationshipChangedModule.ts b/packages/app-runtime/src/modules/runtimeEvents/RelationshipChangedModule.ts index b650b57ae..f366a5b5f 100644 --- a/packages/app-runtime/src/modules/runtimeEvents/RelationshipChangedModule.ts +++ b/packages/app-runtime/src/modules/runtimeEvents/RelationshipChangedModule.ts @@ -1,4 +1,4 @@ -import { RelationshipChangedEvent } from "@nmshd/runtime"; +import { RelationshipAuditLogEntryReason, RelationshipChangedEvent } from "@nmshd/runtime"; import { AppRuntimeError } from "../../AppRuntimeError"; import { OnboardingChangeReceivedEvent } from "../../events"; import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "../AppRuntimeModule"; @@ -18,20 +18,28 @@ export class RelationshipChangedModule extends AppRuntimeModule { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (realm !== Realm.Dev && realm !== Realm.Prod && realm !== Realm.Stage) { - throw AppRuntimeErrors.multiAccount.wrongRealm(); - } - - const [localAccount] = await this.multiAccountController.createAccount(realm, name); + public async createAccount(name: string): Promise { + const [localAccount] = await this.multiAccountController.createAccount(name); return LocalAccountMapper.toLocalAccountDTO(localAccount); } diff --git a/packages/app-runtime/src/multiAccount/MultiAccountController.ts b/packages/app-runtime/src/multiAccount/MultiAccountController.ts index f4a9057f6..b70277b2c 100644 --- a/packages/app-runtime/src/multiAccount/MultiAccountController.ts +++ b/packages/app-runtime/src/multiAccount/MultiAccountController.ts @@ -8,7 +8,6 @@ import { CoreError, CoreId, DeviceSharedSecret, - Realm, Transport, CoreErrors as TransportCoreErrors, TransportLoggerFactory @@ -162,7 +161,6 @@ export class MultiAccountController { id, address: deviceSharedSecret.identity.address, directory: ".", - realm: deviceSharedSecret.identity.realm, name: name ?? deviceSharedSecret.name ?? deviceSharedSecret.identity.address.toString(), order: -1 }); @@ -185,14 +183,12 @@ export class MultiAccountController { return [updatedLocalAccount, accountController]; } - public async createAccount(realm: Realm, name: string): Promise<[LocalAccount, AccountController]> { - this._log.trace(`Creating account for realm ${realm}.`); + public async createAccount(name: string): Promise<[LocalAccount, AccountController]> { const id = await CoreId.generate(); let localAccount = LocalAccount.from({ id, directory: ".", - realm, name, order: -1 }); diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccount.ts b/packages/app-runtime/src/multiAccount/data/LocalAccount.ts index 3686f2571..06c2a358a 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccount.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccount.ts @@ -1,11 +1,10 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreAddress, CoreDate, CoreId, CoreSerializable, ICoreDate, ICoreSerializable, Realm } from "@nmshd/transport"; +import { CoreAddress, CoreDate, CoreId, CoreSerializable, ICoreDate, ICoreSerializable } from "@nmshd/transport"; export interface ILocalAccount extends ICoreSerializable { id: CoreId; address?: CoreAddress; name: string; - realm: Realm; directory: string; order: number; lastAccessedAt?: ICoreDate; @@ -26,10 +25,6 @@ export class LocalAccount extends CoreSerializable implements ILocalAccount { @serialize() public name: string; - @validate() - @serialize() - public realm: Realm; - @validate() @serialize() public directory: string; diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts b/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts index afbcfc63c..69bb38ede 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccountDTO.ts @@ -2,7 +2,6 @@ export interface LocalAccountDTO { id: string; address?: string; name: string; - realm: string; directory: string; order: number; lastAccessedAt?: string; diff --git a/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts b/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts index e2c5cb537..4de37fd74 100644 --- a/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts +++ b/packages/app-runtime/src/multiAccount/data/LocalAccountMapper.ts @@ -7,7 +7,6 @@ export class LocalAccountMapper { id: localAccount.id.toString(), address: localAccount.address?.toString(), name: localAccount.name, - realm: localAccount.realm, directory: localAccount.directory.toString(), order: localAccount.order, lastAccessedAt: localAccount.lastAccessedAt?.toString(), diff --git a/packages/app-runtime/test/extensibility/MessageFacade.test.ts b/packages/app-runtime/test/extensibility/MessageFacade.test.ts index 696782fe5..bded4f089 100644 --- a/packages/app-runtime/test/extensibility/MessageFacade.test.ts +++ b/packages/app-runtime/test/extensibility/MessageFacade.test.ts @@ -1,4 +1,3 @@ -import { Realm } from "@nmshd/transport"; import { AppRuntime, LocalAccountDTO } from "../../src"; import { TestUtil } from "../lib"; @@ -11,7 +10,7 @@ describe("MessageFacade", function () { runtime = await TestUtil.createRuntime(); await runtime.start(); - localAccount = await runtime.accountServices.createAccount(Realm.Prod, "Profil 1"); + localAccount = await runtime.accountServices.createAccount("Profil 1"); await runtime.selectAccount(localAccount.id); }); diff --git a/packages/app-runtime/test/lib/TestUtil.ts b/packages/app-runtime/test/lib/TestUtil.ts index c717dfa40..c28ba00b0 100644 --- a/packages/app-runtime/test/lib/TestUtil.ts +++ b/packages/app-runtime/test/lib/TestUtil.ts @@ -1,10 +1,19 @@ /* eslint-disable jest/no-standalone-expect */ import { ILoggerFactory } from "@js-soft/logging-abstractions"; import { SimpleLoggerFactory } from "@js-soft/simple-logger"; -import { Serializable } from "@js-soft/ts-serval"; import { Result, sleep, SubscriptionTarget } from "@js-soft/ts-utils"; -import { FileDTO, MessageDTO, RelationshipDTO, RelationshipTemplateDTO, SyncEverythingResponse } from "@nmshd/runtime"; -import { CoreDate, IConfigOverwrite, Realm, TransportLoggerFactory } from "@nmshd/transport"; +import { ArbitraryMessageContent, ArbitraryRelationshipCreationContent, ArbitraryRelationshipTemplateContent } from "@nmshd/content"; +import { + FileDTO, + MessageContentDerivation, + MessageDTO, + RelationshipCreationContentDerivation, + RelationshipDTO, + RelationshipTemplateContentDerivation, + RelationshipTemplateDTO, + SyncEverythingResponse +} from "@nmshd/runtime"; +import { CoreDate, IConfigOverwrite, TransportLoggerFactory } from "@nmshd/transport"; import { LogLevel } from "typescript-logging"; import { AppConfig, AppRuntime, LocalAccountDTO, LocalAccountSession, createAppConfig as runtime_createAppConfig } from "../../src"; import { NativeBootstrapperMock } from "../mocks/NativeBootstrapperMock"; @@ -55,7 +64,7 @@ export class TestUtil { } public static async createSession(runtime: AppRuntime): Promise { - const localAccount1 = await runtime.accountServices.createAccount(Realm.Prod, "Profil 1"); + const localAccount1 = await runtime.accountServices.createAccount("Profil 1"); return await runtime.selectAccount(localAccount1.id); } @@ -157,7 +166,7 @@ export class TestUtil { const accounts: LocalAccountDTO[] = []; for (let i = 0; i < count; i++) { - accounts.push(await runtime.accountServices.createAccount(Realm.Prod, `Account ${i}`)); + accounts.push(await runtime.accountServices.createAccount(`Account ${i}`)); } return accounts; @@ -166,9 +175,7 @@ export class TestUtil { public static async createAndLoadPeerTemplate( from: LocalAccountSession, to: LocalAccountSession, - content: any = { - mycontent: "template" - } + content: RelationshipTemplateContentDerivation = ArbitraryRelationshipTemplateContent.from({ value: {} }).toJSON() ): Promise { const templateFrom = ( await from.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ @@ -195,80 +202,24 @@ export class TestUtil { public static async requestRelationshipForTemplate( from: LocalAccountSession, templateId: string, - content: any = { - mycontent: "request" - } + content: RelationshipCreationContentDerivation = ArbitraryRelationshipCreationContent.from({ value: {} }).toJSON() ): Promise { - const relRequest = await from.transportServices.relationships.createRelationship({ templateId, content }); + const relRequest = await from.transportServices.relationships.createRelationship({ templateId, creationContent: content }); return relRequest.value; } - public static async acceptRelationship( - session: LocalAccountSession, - relationshipId: string, - content: any = { - mycontent: "response" - } - ): Promise { - const relationship = ( - await session.transportServices.relationships.getRelationship({ - id: relationshipId - }) - ).value; - - const acceptedRelationship = ( - await session.transportServices.relationships.acceptRelationshipChange({ - changeId: relationship.changes[0].id, - content, - relationshipId - }) - ).value; + public static async acceptRelationship(session: LocalAccountSession, relationshipId: string): Promise { + const acceptedRelationship = (await session.transportServices.relationships.acceptRelationship({ relationshipId })).value; return acceptedRelationship; } - public static async rejectRelationship( - session: LocalAccountSession, - relationshipId: string, - content: any = { - mycontent: "rejection" - } - ): Promise { - const relationship = ( - await session.transportServices.relationships.getRelationship({ - id: relationshipId - }) - ).value; - - const rejectedRelationship = ( - await session.transportServices.relationships.rejectRelationshipChange({ - changeId: relationship.changes[0].id, - content, - relationshipId - }) - ).value; + public static async rejectRelationship(session: LocalAccountSession, relationshipId: string): Promise { + const rejectedRelationship = (await session.transportServices.relationships.rejectRelationship({ relationshipId })).value; return rejectedRelationship; } - public static async revokeRelationship( - session: LocalAccountSession, - relationshipId: string, - content: any = { - mycontent: "revokation" - } - ): Promise { - const relationship = ( - await session.transportServices.relationships.getRelationship({ - id: relationshipId - }) - ).value; - - const rejectedRelationship = ( - await session.transportServices.relationships.revokeRelationshipChange({ - changeId: relationship.changes[0].id, - content, - relationshipId - }) - ).value; + public static async revokeRelationship(session: LocalAccountSession, relationshipId: string): Promise { + const rejectedRelationship = (await session.transportServices.relationships.revokeRelationship({ relationshipId })).value; return rejectedRelationship; } @@ -339,13 +290,18 @@ export class TestUtil { return syncResult.messages[0]; } - public static async sendMessage(from: LocalAccountSession, to: LocalAccountSession, content?: any): Promise { + public static async sendMessage(from: LocalAccountSession, to: LocalAccountSession, content?: MessageContentDerivation): Promise { return await this.sendMessagesWithAttachments(from, [to], [], content); } - public static async sendMessagesWithAttachments(from: LocalAccountSession, recipients: LocalAccountSession[], attachments: string[], content?: any): Promise { + public static async sendMessagesWithAttachments( + from: LocalAccountSession, + recipients: LocalAccountSession[], + attachments: string[], + content?: MessageContentDerivation + ): Promise { if (!content) { - content = Serializable.fromUnknown({ content: "TestContent" }); + content = ArbitraryMessageContent.from({ value: "TestContent" }).toJSON(); } const result = await from.transportServices.messages.sendMessage({ diff --git a/packages/app-runtime/test/modules/MessageEventing.test.ts b/packages/app-runtime/test/modules/MessageEventing.test.ts index 09dbb4552..bf8bb8302 100644 --- a/packages/app-runtime/test/modules/MessageEventing.test.ts +++ b/packages/app-runtime/test/modules/MessageEventing.test.ts @@ -1,3 +1,4 @@ +import { MailJSON } from "@nmshd/content"; import { MessageReceivedEvent } from "@nmshd/runtime"; import { AppRuntime, LocalAccountSession, MailReceivedEvent } from "../../src"; import { EventListener, TestUtil } from "../lib"; @@ -26,7 +27,7 @@ describe("MessageEventingTest", function () { test("should fire events when mail is received", async function () { const createdBy = (await sessionA.transportServices.account.getIdentityInfo()).value.address; const recipient = (await sessionB.transportServices.account.getIdentityInfo()).value.address; - const mail = { + const mail: MailJSON = { "@type": "Mail", to: [recipient], subject: "Hallo Horst", @@ -54,6 +55,6 @@ describe("MessageEventingTest", function () { expect(mailReceivedEvent.data.date).toBe(message.createdAt); expect(mailReceivedEvent.data.type).toBe("MailDVO"); expect(mailReceivedEvent.data.createdBy.type).toBe("IdentityDVO"); - expect(mailReceivedEvent.data.createdBy.name).toBe(createdBy.substring(3, 9)); + expect(mailReceivedEvent.data.createdBy.name).toBe("i18n://dvo.identity.unknown"); }); }); diff --git a/packages/app-runtime/test/modules/RelationshipEventingAccept.test.ts b/packages/app-runtime/test/modules/RelationshipEventingAccept.test.ts index e93c52342..cec74ef2d 100644 --- a/packages/app-runtime/test/modules/RelationshipEventingAccept.test.ts +++ b/packages/app-runtime/test/modules/RelationshipEventingAccept.test.ts @@ -1,4 +1,4 @@ -import { RelationshipChangedEvent, RelationshipChangeStatus, RelationshipStatus } from "@nmshd/runtime"; +import { RelationshipChangedEvent, RelationshipStatus } from "@nmshd/runtime"; import { AppRuntime, LocalAccountSession, OnboardingChangeReceivedEvent } from "../../src"; import { EventListener, TestUtil } from "../lib"; @@ -42,11 +42,10 @@ describe("RelationshipEventingAcceptTest", function () { const onboardingChangeReceivedEvent = events[1].instance as OnboardingChangeReceivedEvent; expect(onboardingChangeReceivedEvent).toBeInstanceOf(OnboardingChangeReceivedEvent); expect(onboardingChangeReceivedEvent.data).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.change.status).toBe(RelationshipChangeStatus.Pending); - expect(onboardingChangeReceivedEvent.data.change).toBe(relationshipChangedEvent.data.changes[0]); + expect(onboardingChangeReceivedEvent.data.auditLogEntry.newStatus).toBe(RelationshipStatus.Pending); expect(onboardingChangeReceivedEvent.data.identity).toBeDefined(); expect(onboardingChangeReceivedEvent.data.identity.id).toBe(sessionB.address.toString()); - expect(onboardingChangeReceivedEvent.data.identity.name).toBe(sessionB.address.toString().substring(3, 9)); + expect(onboardingChangeReceivedEvent.data.identity.name).toBe("i18n://dvo.identity.unknown"); expect(onboardingChangeReceivedEvent.data.identity.hasRelationship).toBe(true); expect(onboardingChangeReceivedEvent.data.identity.relationship?.id).toBe(relationshipChangedEvent.data.id); expect(onboardingChangeReceivedEvent.data.relationship).toBe(relationshipChangedEvent.data); @@ -85,10 +84,9 @@ describe("RelationshipEventingAcceptTest", function () { const onboardingChangeReceivedEvent = events[1].instance as OnboardingChangeReceivedEvent; expect(onboardingChangeReceivedEvent).toBeInstanceOf(OnboardingChangeReceivedEvent); expect(onboardingChangeReceivedEvent.data).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.change.status).toBe(RelationshipChangeStatus.Accepted); - expect(onboardingChangeReceivedEvent.data.change).toBe(relationshipChangedEvent.data.changes[0]); + expect(onboardingChangeReceivedEvent.data.auditLogEntry.newStatus).toBe(RelationshipStatus.Active); expect(onboardingChangeReceivedEvent.data.identity).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.identity.name).toBe(sessionA.accountController.identity.address.toString().substring(3, 9)); + expect(onboardingChangeReceivedEvent.data.identity.name).toBe("i18n://dvo.identity.unknown"); expect(onboardingChangeReceivedEvent.data.identity.id).toBe(sessionA.accountController.identity.address.toString()); expect(onboardingChangeReceivedEvent.data.identity.hasRelationship).toBe(true); expect(onboardingChangeReceivedEvent.data.identity.relationship?.id).toBe(relationshipChangedEvent.data.id); diff --git a/packages/app-runtime/test/modules/RelationshipEventingReject.test.ts b/packages/app-runtime/test/modules/RelationshipEventingReject.test.ts index 152177267..6f39cfe42 100644 --- a/packages/app-runtime/test/modules/RelationshipEventingReject.test.ts +++ b/packages/app-runtime/test/modules/RelationshipEventingReject.test.ts @@ -1,4 +1,4 @@ -import { RelationshipChangedEvent, RelationshipChangeStatus, RelationshipStatus } from "@nmshd/runtime"; +import { RelationshipChangedEvent, RelationshipStatus } from "@nmshd/runtime"; import { AppRuntime, LocalAccountSession, OnboardingChangeReceivedEvent } from "../../src"; import { EventListener, TestUtil } from "../lib"; @@ -42,10 +42,9 @@ describe("RelationshipEventingRejectTest", function () { const onboardingChangeReceivedEvent = events[1].instance as OnboardingChangeReceivedEvent; expect(onboardingChangeReceivedEvent).toBeInstanceOf(OnboardingChangeReceivedEvent); expect(onboardingChangeReceivedEvent.data).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.change.status).toBe(RelationshipChangeStatus.Pending); - expect(onboardingChangeReceivedEvent.data.change).toBe(relationshipChangedEvent.data.changes[0]); + expect(onboardingChangeReceivedEvent.data.auditLogEntry.newStatus).toBe(RelationshipStatus.Pending); expect(onboardingChangeReceivedEvent.data.identity).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.identity.name).toBe(sessionB.accountController.identity.address.toString().substring(3, 9)); + expect(onboardingChangeReceivedEvent.data.identity.name).toBe("i18n://dvo.identity.unknown"); expect(onboardingChangeReceivedEvent.data.identity.id).toBe(sessionB.accountController.identity.address.toString()); expect(onboardingChangeReceivedEvent.data.relationship).toBe(relationshipChangedEvent.data); @@ -84,11 +83,10 @@ describe("RelationshipEventingRejectTest", function () { const onboardingChangeReceivedEvent = events[1].instance as OnboardingChangeReceivedEvent; expect(onboardingChangeReceivedEvent).toBeInstanceOf(OnboardingChangeReceivedEvent); expect(onboardingChangeReceivedEvent.data).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.change.status).toBe(RelationshipChangeStatus.Rejected); - expect(onboardingChangeReceivedEvent.data.change).toBe(relationshipChangedEvent.data.changes[0]); + expect(onboardingChangeReceivedEvent.data.auditLogEntry.newStatus).toBe(RelationshipStatus.Rejected); expect(onboardingChangeReceivedEvent.data.identity).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.identity.name).toBe(sessionA.accountController.identity.address.toString().substring(3, 9)); + expect(onboardingChangeReceivedEvent.data.identity.name).toBe("i18n://dvo.identity.unknown"); expect(onboardingChangeReceivedEvent.data.identity.id).toBe(sessionA.accountController.identity.address.toString()); expect(onboardingChangeReceivedEvent.data.identity.hasRelationship).toBe(true); expect(onboardingChangeReceivedEvent.data.identity.relationship?.id).toBe(relationshipChangedEvent.data.id); diff --git a/packages/app-runtime/test/modules/RelationshipEventingRevoke.test.ts b/packages/app-runtime/test/modules/RelationshipEventingRevoke.test.ts index fd2a0822a..b2f6d3bfc 100644 --- a/packages/app-runtime/test/modules/RelationshipEventingRevoke.test.ts +++ b/packages/app-runtime/test/modules/RelationshipEventingRevoke.test.ts @@ -1,4 +1,4 @@ -import { RelationshipChangedEvent, RelationshipChangeStatus, RelationshipStatus } from "@nmshd/runtime"; +import { RelationshipChangedEvent, RelationshipStatus } from "@nmshd/runtime"; import { AppRuntime, LocalAccountSession, OnboardingChangeReceivedEvent } from "../../src"; import { EventListener, TestUtil } from "../lib"; @@ -43,11 +43,10 @@ describe("RelationshipEventingRevokeTest", function () { const onboardingChangeReceivedEvent = events[1].instance as OnboardingChangeReceivedEvent; expect(onboardingChangeReceivedEvent).toBeInstanceOf(OnboardingChangeReceivedEvent); expect(onboardingChangeReceivedEvent.data).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.change.status).toBe(RelationshipChangeStatus.Pending); - expect(onboardingChangeReceivedEvent.data.change).toBe(relationshipChangedEvent.data.changes[0]); + expect(onboardingChangeReceivedEvent.data.auditLogEntry.newStatus).toBe(RelationshipStatus.Pending); expect(onboardingChangeReceivedEvent.data.identity).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.identity.name).toBe(sessionB.accountController.identity.address.toString().substring(3, 9)); + expect(onboardingChangeReceivedEvent.data.identity.name).toBe("i18n://dvo.identity.unknown"); expect(onboardingChangeReceivedEvent.data.identity.id).toBe(sessionB.accountController.identity.address.toString()); expect(onboardingChangeReceivedEvent.data.identity.hasRelationship).toBe(true); @@ -86,11 +85,10 @@ describe("RelationshipEventingRevokeTest", function () { const onboardingChangeReceivedEvent = events[1].instance as OnboardingChangeReceivedEvent; expect(onboardingChangeReceivedEvent).toBeInstanceOf(OnboardingChangeReceivedEvent); expect(onboardingChangeReceivedEvent.data).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.change.status).toBe(RelationshipChangeStatus.Revoked); - expect(onboardingChangeReceivedEvent.data.change).toBe(relationshipChangedEvent.data.changes[0]); + expect(onboardingChangeReceivedEvent.data.auditLogEntry.newStatus).toBe(RelationshipStatus.Revoked); expect(onboardingChangeReceivedEvent.data.identity).toBeDefined(); - expect(onboardingChangeReceivedEvent.data.identity.name).toBe(sessionB.accountController.identity.address.toString().substring(3, 9)); + expect(onboardingChangeReceivedEvent.data.identity.name).toBe("i18n://dvo.identity.unknown"); expect(onboardingChangeReceivedEvent.data.identity.id).toBe(sessionB.accountController.identity.address.toString()); expect(onboardingChangeReceivedEvent.data.identity.hasRelationship).toBe(true); expect(onboardingChangeReceivedEvent.data.identity.relationship?.id).toBe(relationshipChangedEvent.data.id); diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index dee6cff63..2f1c1414f 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -1,4 +1,3 @@ -import { Realm } from "@nmshd/transport"; import { AppRuntime } from "../../src"; import { TestUtil } from "../lib"; @@ -14,7 +13,7 @@ describe("AppStringProcessor", function () { }); test("should process a URL", async function () { - const account = await runtime.accountServices.createAccount(Realm.Prod, Math.random().toString(36).substring(7)); + const account = await runtime.accountServices.createAccount(Math.random().toString(36).substring(7)); const result = await runtime.stringProcessor.processURL("nmshd://qr#", account); expect(result.isError).toBeDefined(); diff --git a/packages/app-runtime/test/runtime/Startup.test.ts b/packages/app-runtime/test/runtime/Startup.test.ts index c68b6b8eb..2649ea483 100644 --- a/packages/app-runtime/test/runtime/Startup.test.ts +++ b/packages/app-runtime/test/runtime/Startup.test.ts @@ -1,4 +1,3 @@ -import { Realm } from "@nmshd/transport"; import { AppRuntime, LocalAccountDTO } from "../../src"; import { EventListener, TestUtil } from "../lib"; @@ -54,7 +53,7 @@ describe("Runtime Startup", function () { }); test("should create an account", async function () { - localAccount = await runtime.accountServices.createAccount(Realm.Prod, "Profil 1"); + localAccount = await runtime.accountServices.createAccount("Profil 1"); expect(localAccount).toBeDefined(); }); diff --git a/packages/consumption/package.json b/packages/consumption/package.json index baff8d6cc..58eb0ccb6 100644 --- a/packages/consumption/package.json +++ b/packages/consumption/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/consumption", - "version": "3.12.4", + "version": "5.0.0", "description": "The consumption library extends the transport library.", "homepage": "https://enmeshed.eu", "repository": { diff --git a/packages/consumption/src/consumption/ConsumptionController.ts b/packages/consumption/src/consumption/ConsumptionController.ts index 6a00be256..e4683208f 100644 --- a/packages/consumption/src/consumption/ConsumptionController.ts +++ b/packages/consumption/src/consumption/ConsumptionController.ts @@ -13,7 +13,7 @@ import { ShareAttributeRequestItem, ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItem } from "@nmshd/content"; -import { AccountController, Transport } from "@nmshd/transport"; +import { AccountController, CoreAddress, CoreId, Transport } from "@nmshd/transport"; import { AttributeListenersController, AttributesController, @@ -109,7 +109,8 @@ export class ConsumptionController { requestItemProcessorRegistry, this, this.transport.eventBus, - this.accountController.identity + this.accountController.identity, + this.accountController.relationships ).init(); const notificationItemProcessorRegistry = new NotificationItemProcessorRegistry(this, this.getDefaultNotificationItemProcessors()); @@ -153,4 +154,13 @@ export class ConsumptionController { [ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItem, ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItemProcessor] ]); } + + public async cleanupDataOfDecomposedRelationship(peer: CoreAddress, relationshipId: CoreId): Promise { + await this.attributes.deleteAttributesExchangedWithPeer(peer); + await this.outgoingRequests.deleteRequestsToPeer(peer); + await this.incomingRequests.deleteRequestsFromPeer(peer); + await this.settings.deleteSettingsForRelationship(relationshipId); + await this.attributeListeners.deletePeerAttributeListeners(peer); + await this.notifications.deleteNotificationsExchangedWithPeer(peer); + } } diff --git a/packages/consumption/src/consumption/CoreErrors.ts b/packages/consumption/src/consumption/CoreErrors.ts index 2812099c1..4dcc29082 100644 --- a/packages/consumption/src/consumption/CoreErrors.ts +++ b/packages/consumption/src/consumption/CoreErrors.ts @@ -15,54 +15,54 @@ class Attributes { public successionMustNotChangeKey() { return new CoreError( "error.consumption.attributes.successionMustNotChangeKey", - "The predecessor attribute's key does not match that of the successor. The succession of a relationship attribute must not change the key." + "The predecessor RelationshipAttribute's key does not match that of the successor. The succession of a RelationshipAttribute must not change the key." ); } public successionPeerIsNotOwner() { - return new CoreError("error.consumption.attributes.successionPeerIsNotOwner", "The peer of the succeeded attribute is not its owner. This may be an attempt of spoofing."); + return new CoreError("error.consumption.attributes.successionPeerIsNotOwner", "The peer of the succeeded Attribute is not its owner. This may be an attempt of spoofing."); } public predecessorSourceAttributeIsNotRepositoryAttribute() { - return new CoreError("error.consumption.attributes.predecessorSourceAttributeIsNotRepositoryAttribute", "Predecessor source attribute is not a repository attribute."); + return new CoreError("error.consumption.attributes.predecessorSourceAttributeIsNotRepositoryAttribute", "Predecessor sourceAttribute is not a RepositoryAttribute."); } public successorSourceAttributeIsNotRepositoryAttribute() { - return new CoreError("error.consumption.attributes.successorSourceAttributeIsNotRepositoryAttribute", "Successor source attribute is not a repository attribute."); + return new CoreError("error.consumption.attributes.successorSourceAttributeIsNotRepositoryAttribute", "Successor sourceAttribute is not a RepositoryAttribute."); } public successorSourceDoesNotSucceedPredecessorSource() { return new CoreError( "error.consumption.attributes.successorSourceDoesNotSucceedPredecessorSource", - "Predecessor source attribute is not succeeded by successor source attribute." + "Predecessor sourceAttribute is not succeeded by successor sourceAttribute." ); } public predecessorSourceContentIsNotEqualToCopyContent() { return new CoreError( "error.consumption.attributes.predecessorSourceContentIsNotEqualToCopyContent", - "Predecessor source attribute content doesn't match predecessor shared attribute copy." + "Predecessor sourceAttribute content doesn't match the content of the predecessor shared Attribute copy." ); } public successorSourceContentIsNotEqualToCopyContent() { return new CoreError( "error.consumption.attributes.successorSourceContentIsNotEqualToCopyContent", - "Successor source attribute content doesn't match successor shared attribute copy." + "Successor sourceAttribute content doesn't match the content of the successor shared Attribute copy." ); } public cannotSucceedChildOfComplexAttribute(parentId: string | CoreId) { return new CoreError( "error.consumption.attributes.cannotSucceedChildOfComplexAttribute", - `The attribute you want to succeed is child attribute of a complex attribute (id: ${parentId}), and cannot be succeeded on its own. Instead, succeed the parent which will implicitly succeed all its children.` + `The Attribute you want to succeed is a child Attribute of a complex Attribute (id: '${parentId}'), and cannot be succeeded on its own. Instead, succeed the parent which will implicitly succeed all its children.` ); } public successorMustNotYetExist() { return new CoreError( "error.consumption.attributes.successorMustNotYetExist", - "The predecessor attribute's successor must not exist. It will be created by the succession handlers and must not be created manually." + "The predecessor Attribute's successor must not exist. It will be created by the succession handlers and must not be created manually." ); } @@ -73,57 +73,57 @@ class Attributes { } public predecessorIsNotRepositoryAttribute() { - return new CoreError("error.consumption.attributes.predecessorIsNotRepositoryAttribute", "Predecessor is not a repository attribute."); + return new CoreError("error.consumption.attributes.predecessorIsNotRepositoryAttribute", "Predecessor is not a RepositoryAttribute."); } public predecessorIsNotOwnSharedIdentityAttribute() { - return new CoreError("error.consumption.attributes.predecessorIsNotOwnSharedIdentityAttribute", "Predecessor is not an own shared identity attribute."); + return new CoreError("error.consumption.attributes.predecessorIsNotOwnSharedIdentityAttribute", "Predecessor is not an own shared IdentityAttribute."); } public predecessorIsNotPeerSharedIdentityAttribute() { - return new CoreError("error.consumption.attributes.predecessorIsNotPeerSharedIdentityAttribute", "Predecessor is not a peer shared identity attribute."); + return new CoreError("error.consumption.attributes.predecessorIsNotPeerSharedIdentityAttribute", "Predecessor is not a peer shared IdentityAttribute."); } public predecessorIsNotOwnSharedRelationshipAttribute() { - return new CoreError("error.consumption.attributes.predecessorIsNotOwnSharedRelationshipAttribute", "Predecessor is not an own shared relationship attribute."); + return new CoreError("error.consumption.attributes.predecessorIsNotOwnSharedRelationshipAttribute", "Predecessor is not an own shared RelationshipAttribute."); } public predecessorIsNotPeerSharedRelationshipAttribute() { - return new CoreError("error.consumption.attributes.predecessorIsNotPeerSharedRelationshipAttribute", "Predecessor is not a peer shared relationship attribute."); + return new CoreError("error.consumption.attributes.predecessorIsNotPeerSharedRelationshipAttribute", "Predecessor is not a peer shared RelationshipAttribute."); } public predecessorIsNotThirdPartyOwnedRelationshipAttribute() { - return new CoreError("error.consumption.attributes.predecessorIsNotThirdPartyOwnedRelationshipAttribute", "Predecessor is not a third party owned relationship attribute."); + return new CoreError("error.consumption.attributes.predecessorIsNotThirdPartyOwnedRelationshipAttribute", "Predecessor is not a third party owned RelationshipAttribute."); } public successorIsNotRepositoryAttribute() { - return new CoreError("error.consumption.attributes.successorIsNotRepositoryAttribute", "Successor is not a repository attribute."); + return new CoreError("error.consumption.attributes.successorIsNotRepositoryAttribute", "Successor is not a RepositoryAttribute."); } public successorIsNotOwnSharedIdentityAttribute() { - return new CoreError("error.consumption.attributes.successorIsNotOwnSharedIdentityAttribute", "Successor is not an own shared identity attribute."); + return new CoreError("error.consumption.attributes.successorIsNotOwnSharedIdentityAttribute", "Successor is not an own shared IdentityAttribute."); } public successorIsNotPeerSharedIdentityAttribute() { - return new CoreError("error.consumption.attributes.successorIsNotPeerSharedIdentityAttribute", "Successor is not a peer shared identity attribute."); + return new CoreError("error.consumption.attributes.successorIsNotPeerSharedIdentityAttribute", "Successor is not a peer shared IdentityAttribute."); } public successorIsNotOwnSharedRelationshipAttribute() { - return new CoreError("error.consumption.attributes.successorIsNotOwnSharedRelationshipAttribute", "Successor is not an own shared relationship attribute."); + return new CoreError("error.consumption.attributes.successorIsNotOwnSharedRelationshipAttribute", "Successor is not an own shared RelationshipAttribute."); } public successorIsNotPeerSharedRelationshipAttribute() { - return new CoreError("error.consumption.attributes.successorIsNotPeerSharedRelationshipAttribute", "Successor is not a peer shared relationship attribute."); + return new CoreError("error.consumption.attributes.successorIsNotPeerSharedRelationshipAttribute", "Successor is not a peer shared RelationshipAttribute."); } public successorIsNotThirdPartyOwnedRelationshipAttribute() { - return new CoreError("error.consumption.attributes.successorIsNotThirdPartyOwnedRelationshipAttribute", "Successor is not a third party owned relationship attribute."); + return new CoreError("error.consumption.attributes.successorIsNotThirdPartyOwnedRelationshipAttribute", "Successor is not a third party owned RelationshipAttribute."); } public setPredecessorIdDoesNotMatchActualPredecessorId() { return new CoreError( "error.consumption.attributes.setPredecessorIdDoesNotMatchActualPredecessorId", - "The predecessor's id and the explicitly set value for the successor's succeeds field don't match." + "The predecessor's ID and the explicitly set value for the successor's succeeds field don't match." ); } @@ -136,36 +136,36 @@ class Attributes { } public successorSourceAttributeIsNotSpecified() { - return new CoreError("error.consumption.attributes.successorSourceAttributeIsNotSpecified", "You must specify the source attribute of the successor."); + return new CoreError("error.consumption.attributes.successorSourceAttributeIsNotSpecified", "You must specify the sourceAttribute of the successor."); } public successorSourceAttributeDoesNotExist() { - return new CoreError("error.consumption.attributes.successorSourceAttributeDoesNotExist", "The successor source Attribute does not exist."); + return new CoreError("error.consumption.attributes.successorSourceAttributeDoesNotExist", "The successor sourceAttribute does not exist."); } public successionMustNotChangeOwner() { return new CoreError( "error.consumption.attributes.successionMustNotChangeOwner", - "The successor attribute's owner does not match that of the predecessor. An attribute succession must not change the attribute's owner." + "The successor Attribute's owner does not match that of the predecessor. An Attribute succession must not change the Attribute's owner." ); } public successionMustNotChangeValueType() { return new CoreError( "error.consumption.attributes.successionMustNotChangeValueType", - "The successor attribute's value type does not match that of the predecessor. An attribute succession must not change the attribute's value type." + "The successor Attribute's value type does not match that of the predecessor. An Attribute succession must not change the Attribute's value type." ); } public successionMustNotChangeContentType() { return new CoreError( "error.consumption.attributes.successionMustNotChangeContentType", - "The successor attribute's content type does not match that of the predecessor. An attribute succession must not change the attribute's content type, i.e. an identity attribute must not be succeeded by a relationship attribute and v.v." + "The successor Attribute's content type does not match that of the predecessor. An Attribute succession must not change the Attribute's content type, i.e. an IdentityAttribute must not be succeeded by a RelationshipAttribute and v.v." ); } public successionMustNotChangePeer(comment?: string) { - let errorMessage = "The peer of the shared attribute must not change."; + let errorMessage = "The peer of the shared Attribute must not change."; if (comment) errorMessage += ` ${comment}`; return new CoreError("error.consumption.attributes.successionMustNotChangePeer", errorMessage); } @@ -173,18 +173,18 @@ class Attributes { public cannotSucceedAttributesWithASuccessor(successorId: string | CoreId) { return new CoreError( "error.consumption.attributes.cannotSucceedAttributesWithASuccessor", - `The attribute you want to succeed has a successor (id: ${successorId}). You cannot succeed attributes with a successor. Instead, succeed the successor.` + `The Attribute you want to succeed has a successor (id: ${successorId}). You cannot succeed Attributes with a successor. Instead, succeed the successor.` ); } public invalidParentSuccessor(parentSuccessorId: string | CoreId) { - return new CoreError("error.consumption.attributes.invalidParentSuccessor", `The complex parent successor (id: ${parentSuccessorId}) does not exist.`); + return new CoreError("error.consumption.attributes.invalidParentSuccessor", `The complex parent successor (id: '${parentSuccessorId}') does not exist.`); } public cannotSucceedAttributesWithDeletionInfo() { return new CoreError( "error.consumption.attributes.cannotSucceedAttributesWithDeletionInfo", - "You cannot succeed attributes with a deletionInfo, since the peer may have already deleted it or marked it for deletion." + "You cannot succeed Attributes with a deletionInfo, since the peer may have already deleted it or marked it for deletion." ); } @@ -216,33 +216,40 @@ class Attributes { ); } - public invalidPropertyValue(message: string) { - return new CoreError("error.consumption.attributes.invalidPropertyValue", message); + public wrongOwnerOfRepositoryAttribute() { + return new CoreError( + "error.consumption.attributes.wrongOwnerOfRepositoryAttribute", + "A wrong owner was provided wanting to create a RepositoryAttribute. You can only create RepositoryAttributes for yourself." + ); + } + + public isNotRepositoryAttribute(attributeId: string | CoreId) { + return new CoreError("error.consumption.attributes.isNotRepositoryAttribute", `The attribute (id: ${attributeId}) is not a RepositoryAttribute.`); } public isNotSharedAttribute(attributeId: string | CoreId) { - return new CoreError("error.consumption.attributes.isNotSharedAttribute", `The attribute (id: ${attributeId}) is not a shared attribute.`); + return new CoreError("error.consumption.attributes.isNotSharedAttribute", `The Attribute (id: '${attributeId}') is not a shared Attribute.`); } public isNotOwnSharedAttribute(attributeId: string | CoreId) { - return new CoreError("error.consumption.attributes.isNotOwnSharedAttribute", `The attribute (id: ${attributeId}) is not an own shared attribute.`); + return new CoreError("error.consumption.attributes.isNotOwnSharedAttribute", `The Attribute (id: '${attributeId}') is not an own shared Attribute.`); } public isNotPeerSharedAttribute(attributeId: string | CoreId) { - return new CoreError("error.consumption.attributes.isNotPeerSharedAttribute", `The attribute (id: ${attributeId}) is not a peer shared attribute.`); + return new CoreError("error.consumption.attributes.isNotPeerSharedAttribute", `The Attribute (id: '${attributeId}') is not a peer shared Attribute.`); } public isNotThirdPartyOwnedRelationshipAttribute(attributeId: string | CoreId) { return new CoreError( "error.consumption.attributes.isNotThirdPartyOwnedRelationshipAttribute", - `The attribute (id: ${attributeId}) is not a third party owned RelationshipAttribute.` + `The Attribute (id: '${attributeId}') is not a third party owned RelationshipAttribute.` ); } public senderIsNotPeerOfSharedAttribute(senderId: string | CoreAddress, attributeId: string | CoreId) { return new CoreError( "error.consumption.attributes.senderIsNotPeerOfSharedAttribute", - `The sender (id: ${senderId}) is not the peer you shared the attribute (id: ${attributeId}) with.` + `The sender (id: '${senderId}') of the Notification is not the peer you shared the Attribute (id: '${attributeId}') with.` ); } } @@ -276,13 +283,25 @@ class Requests { return new CoreError("error.consumption.requests.invalidRequestItem", message); } + public attributeQueryMismatch(message: string) { + return new CoreError("error.consumption.requests.attributeQueryMismatch", message); + } + + public wrongRelationshipStatus(message: string) { + return new CoreError("error.consumption.requests.wrongRelationshipStatus", message); + } + + public missingRelationship(message: string) { + return new CoreError("error.consumption.requests.missingRelationship", message); + } + private static readonly _decideValidation = class { public invalidNumberOfItems(message: string) { return new ApplicationError("error.consumption.requests.decide.validation.invalidNumberOfItems", message); } - public itemAcceptedButParentNotAccepted(message: string): ApplicationError { - return new ApplicationError("error.consumption.requests.decide.validation.itemAcceptedButParentNotAccepted", message); + public itemAcceptedButRequestNotAccepted(message: string): ApplicationError { + return new ApplicationError("error.consumption.requests.decide.validation.itemAcceptedButRequestNotAccepted", message); } public mustBeAcceptedItemNotAccepted(message: string): ApplicationError { diff --git a/packages/consumption/src/modules/attributeListeners/AttributeListenersController.ts b/packages/consumption/src/modules/attributeListeners/AttributeListenersController.ts index 7636d5e71..7ef0b4740 100644 --- a/packages/consumption/src/modules/attributeListeners/AttributeListenersController.ts +++ b/packages/consumption/src/modules/attributeListeners/AttributeListenersController.ts @@ -53,4 +53,13 @@ export class AttributeListenersController extends ConsumptionBaseController { return listener; } + + public async deletePeerAttributeListeners(peerAddress: CoreAddress): Promise { + const listenerDocs = await this.attributeListeners.find({ peer: peerAddress.toString() }); + const listeners = this.parseArray(listenerDocs, LocalAttributeListener); + + for (const listener of listeners) { + await this.attributeListeners.delete(listener); + } + } } diff --git a/packages/consumption/src/modules/attributes/AttributesController.ts b/packages/consumption/src/modules/attributes/AttributesController.ts index 93974f628..27f26c272 100644 --- a/packages/consumption/src/modules/attributes/AttributesController.ts +++ b/packages/consumption/src/modules/attributes/AttributesController.ts @@ -11,7 +11,8 @@ import { IThirdPartyRelationshipAttributeQuery, RelationshipAttributeJSON, RelationshipAttributeQuery, - ThirdPartyRelationshipAttributeQuery + ThirdPartyRelationshipAttributeQuery, + ThirdPartyRelationshipAttributeQueryOwner } from "@nmshd/content"; import * as iql from "@nmshd/iql"; import { CoreAddress, CoreDate, CoreId, ICoreDate, ICoreId, SynchronizedCollection, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; @@ -33,9 +34,9 @@ import { ThirdPartyOwnedRelationshipAttributeSucceededEvent } from "./events"; import { AttributeSuccessorParams, AttributeSuccessorParamsJSON, IAttributeSuccessorParams } from "./local/AttributeSuccessorParams"; -import { CreateLocalAttributeParams, ICreateLocalAttributeParams } from "./local/CreateLocalAttributeParams"; -import { ICreatePeerLocalAttributeParams } from "./local/CreatePeerLocalAttributeParams"; +import { CreateRepositoryAttributeParams, ICreateRepositoryAttributeParams } from "./local/CreateRepositoryAttributeParams"; import { CreateSharedLocalAttributeCopyParams, ICreateSharedLocalAttributeCopyParams } from "./local/CreateSharedLocalAttributeCopyParams"; +import { ICreateSharedLocalAttributeParams } from "./local/CreateSharedLocalAttributeParams"; import { ILocalAttribute, LocalAttribute, LocalAttributeJSON } from "./local/LocalAttribute"; import { DeletionStatus } from "./local/LocalAttributeDeletionInfo"; import { LocalAttributeShareInfo } from "./local/LocalAttributeShareInfo"; @@ -178,6 +179,10 @@ export class AttributesController extends ConsumptionBaseController { const dbQuery = RelationshipAttributeQueryTranslator.translate(parsedQuery); dbQuery["content.confidentiality"] = { $ne: "private" }; + if (parsedQuery.owner.equals("")) { + dbQuery["content.owner"] = { $eq: this.identity.address.toString() }; + } + const attributes = await this.attributes.find(dbQuery); const attribute = attributes.length > 0 ? LocalAttribute.from(attributes[0]) : undefined; @@ -187,9 +192,31 @@ export class AttributesController extends ConsumptionBaseController { public async executeThirdPartyRelationshipAttributeQuery(query: IThirdPartyRelationshipAttributeQuery): Promise { const parsedQuery = ThirdPartyRelationshipAttributeQuery.from(query); - const dbQuery = ThirdPartyRelationshipAttributeQueryTranslator.translate(parsedQuery); + let dbQuery = ThirdPartyRelationshipAttributeQueryTranslator.translate(parsedQuery); dbQuery["content.confidentiality"] = { $ne: "private" }; + switch (parsedQuery.owner) { + case ThirdPartyRelationshipAttributeQueryOwner.Recipient: + dbQuery["content.owner"] = { $eq: this.identity.address.toString() }; + break; + case ThirdPartyRelationshipAttributeQueryOwner.ThirdParty: + dbQuery["content.owner"] = { $in: parsedQuery.thirdParty.map((aThirdParty) => aThirdParty.toString()) }; + break; + case ThirdPartyRelationshipAttributeQueryOwner.Empty: + const recipientOrThirdPartyIsOwnerQuery = { + $or: [ + { + ["content.owner"]: { $eq: this.identity.address.toString() } + }, + { + ["content.owner"]: { $in: parsedQuery.thirdParty.map((aThirdParty) => aThirdParty.toString()) } + } + ] + }; + dbQuery = { $and: [dbQuery, recipientOrThirdPartyIsOwnerQuery] }; + break; + } + const attributes = await this.attributes.find(dbQuery); return this.parseArray(attributes, LocalAttribute); @@ -206,23 +233,24 @@ export class AttributesController extends ConsumptionBaseController { return this.parseArray(attributes, LocalAttribute); } - public async createLocalAttribute(params: ICreateLocalAttributeParams): Promise { - const parsedParams = CreateLocalAttributeParams.from(params); - const localAttribute = LocalAttribute.from({ + public async createRepositoryAttribute(params: ICreateRepositoryAttributeParams): Promise { + if (params.content.owner.toString() !== this.identity.address.toString()) { + throw CoreErrors.attributes.wrongOwnerOfRepositoryAttribute(); + } + + const parsedParams = CreateRepositoryAttributeParams.from(params); + let localAttribute = LocalAttribute.from({ id: parsedParams.id ?? (await ConsumptionIds.attribute.generate()), createdAt: CoreDate.utc(), content: parsedParams.content, - parentId: parsedParams.parentId, - succeeds: parsedParams.succeeds, - shareInfo: parsedParams.shareInfo + parentId: parsedParams.parentId }); await this.attributes.create(localAttribute); - if ( - localAttribute.content instanceof IdentityAttribute && // Local Attributes for children should only be created for Identity Attributes - localAttribute.content.value instanceof AbstractComplexValue - ) { + localAttribute = await this.setAsDefaultRepositoryAttribute(localAttribute, true); + + if (localAttribute.content.value instanceof AbstractComplexValue) { await this.createLocalAttributesForChildrenOfComplexAttribute(localAttribute); } @@ -244,13 +272,58 @@ export class AttributesController extends ConsumptionBaseController { value: propertyValue.toJSON() as AttributeValues.Identity.Json }); - await this.createLocalAttribute({ + await this.createRepositoryAttribute({ content: childAttribute, parentId: localAttribute.id }); } } + public async setAsDefaultRepositoryAttribute(newDefaultAttribute: LocalAttribute, skipOverwrite?: boolean): Promise { + if (!newDefaultAttribute.isRepositoryAttribute(this.identity.address)) { + throw CoreErrors.attributes.isNotRepositoryAttribute(newDefaultAttribute.id); + } + + if (newDefaultAttribute.isDefault) return newDefaultAttribute; + + if (newDefaultAttribute.parentId) { + const parentAttribute = await this.getLocalAttribute(newDefaultAttribute.parentId); + if (!parentAttribute) throw TransportCoreErrors.general.recordNotFound(LocalAttribute, newDefaultAttribute.parentId.toString()); + if (parentAttribute.isDefault) skipOverwrite = false; + } + + const valueType = newDefaultAttribute.content.value.constructor.name; + const query = { + $and: [ + { + [`${nameof((c) => c.content)}.value.@type`]: valueType + }, + { + [nameof((c) => c.isDefault)]: true + } + ] + }; + + const currentDefaultAttributeResult = await this.getLocalAttributes(query); + + if (currentDefaultAttributeResult.length > 1) { + throw new ConsumptionError(`There are multiple default Attributes for type ${valueType.toString()}, even though only one is expected.`); + } + + const currentDefaultAttributeExists = currentDefaultAttributeResult.length === 1; + if (skipOverwrite && currentDefaultAttributeExists) return newDefaultAttribute; + + if (!skipOverwrite && currentDefaultAttributeExists) { + const currentDefaultAttribute = currentDefaultAttributeResult[0]; + currentDefaultAttribute.isDefault = undefined; + await this.updateAttributeUnsafe(currentDefaultAttribute); + } + + newDefaultAttribute.isDefault = true; + await this.updateAttributeUnsafe(newDefaultAttribute); + return newDefaultAttribute; + } + public async createSharedLocalAttributeCopy(params: ICreateSharedLocalAttributeCopyParams): Promise { const parsedParams = CreateSharedLocalAttributeCopyParams.from(params); const sourceAttribute = await this.getLocalAttribute(parsedParams.sourceAttributeId); @@ -271,7 +344,7 @@ export class AttributesController extends ConsumptionBaseController { return sharedLocalAttributeCopy; } - public async createPeerLocalAttribute(params: ICreatePeerLocalAttributeParams): Promise { + public async createSharedLocalAttribute(params: ICreateSharedLocalAttributeParams): Promise { const shareInfo = LocalAttributeShareInfo.from({ peer: params.peer, requestReference: params.requestReference @@ -297,6 +370,13 @@ export class AttributesController extends ConsumptionBaseController { this.eventBus.publish(new AttributeDeletedEvent(this.identity.address.toString(), attribute)); } + public async deleteAttributesExchangedWithPeer(peer: CoreAddress): Promise { + const attributes = await this.getLocalAttributes({ "shareInfo.peer": peer.toString() }); + for (const attribute of attributes) { + await this.deleteAttributeUnsafe(attribute.id); + } + } + private async deleteChildAttributesOfComplexAttribute(complexAttribute: LocalAttribute): Promise { if (!(complexAttribute.content instanceof IdentityAttribute)) { throw new ConsumptionError("Only IdentityAttributes may have child Attributes."); @@ -539,15 +619,26 @@ export class AttributesController extends ConsumptionBaseController { shareInfo: successorParams.shareInfo, parentId: successorParams.parentId, createdAt: successorParams.createdAt, - succeededBy: successorParams.succeededBy + succeededBy: successorParams.succeededBy, + isDefault: predecessor.isDefault }); + await this.removeDefault(predecessor); + predecessor.succeededBy = successor.id; await this.updateAttributeUnsafe(predecessor); return { predecessor, successor }; } + private async removeDefault(attribute: LocalAttribute): Promise { + if (!attribute.isDefault) return attribute; + + attribute.isDefault = undefined; + await this.updateAttributeUnsafe(attribute); + return attribute; + } + public async validateRepositoryAttributeSuccession( predecessorId: CoreId, successorParams: IAttributeSuccessorParams | AttributeSuccessorParamsJSON @@ -908,7 +999,8 @@ export class AttributesController extends ConsumptionBaseController { parentId: attributeData.parentId, succeededBy: attributeData.succeededBy, succeeds: attributeData.succeeds, - deletionInfo: attributeData.deletionInfo + deletionInfo: attributeData.deletionInfo, + isDefault: attributeData.isDefault }); await this.attributes.create(localAttribute); return localAttribute; @@ -930,7 +1022,8 @@ export class AttributesController extends ConsumptionBaseController { shareInfo: attributeParams.shareInfo, succeededBy: attributeParams.succeededBy, succeeds: attributeParams.succeeds, - deletionInfo: attributeParams.deletionInfo + deletionInfo: attributeParams.deletionInfo, + isDefault: attributeParams.isDefault }; const newAttribute = LocalAttribute.from(params); await this.attributes.update(doc, newAttribute); @@ -962,6 +1055,9 @@ export class AttributesController extends ConsumptionBaseController { await this.detachAttributeCopies(attributeCopiesToDetach); await this.deletePredecessorsOfAttribute(attribute.id); + + await this.transferDefault(attribute); + await this.deleteAttribute(attribute); } @@ -1023,6 +1119,31 @@ export class AttributesController extends ConsumptionBaseController { } } + private async transferDefault(attribute: LocalAttribute): Promise { + if (!attribute.isDefault) return; + + const valueType = attribute.content.value.constructor.name; + const query = { + $and: [ + { + [`${nameof((c) => c.content)}.value.@type`]: valueType + }, + { + [nameof((c) => c.succeededBy)]: undefined + }, + { + [nameof((c) => c.id)]: { $ne: attribute.id.toString() } + } + ] + }; + + const defaultCandidates = await this.getLocalAttributes(query); + if (defaultCandidates.length === 0) return; + + defaultCandidates[defaultCandidates.length - 1].isDefault = true; + await this.updateAttributeUnsafe(defaultCandidates[defaultCandidates.length - 1]); + } + public async getVersionsOfAttribute(id: CoreId): Promise { const attribute = await this.getLocalAttribute(id); if (!attribute) { diff --git a/packages/consumption/src/modules/attributes/index.ts b/packages/consumption/src/modules/attributes/index.ts index e0ebc8a63..dc83fa843 100644 --- a/packages/consumption/src/modules/attributes/index.ts +++ b/packages/consumption/src/modules/attributes/index.ts @@ -1,9 +1,9 @@ export * from "./AttributesController"; export * from "./events"; export * from "./local/AttributeSuccessorParams"; -export * from "./local/CreateLocalAttributeParams"; -export * from "./local/CreatePeerLocalAttributeParams"; +export * from "./local/CreateRepositoryAttributeParams"; export * from "./local/CreateSharedLocalAttributeCopyParams"; +export * from "./local/CreateSharedLocalAttributeParams"; export * from "./local/LocalAttribute"; export * from "./local/LocalAttributeDeletionInfo"; export * from "./local/LocalAttributeShareInfo"; diff --git a/packages/consumption/src/modules/attributes/local/CreateLocalAttributeParams.ts b/packages/consumption/src/modules/attributes/local/CreateLocalAttributeParams.ts deleted file mode 100644 index 0c3486847..000000000 --- a/packages/consumption/src/modules/attributes/local/CreateLocalAttributeParams.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ISerializable, Serializable, serialize, validate } from "@js-soft/ts-serval"; -import { IIdentityAttribute, IRelationshipAttribute, IdentityAttribute, IdentityAttributeJSON, RelationshipAttribute, RelationshipAttributeJSON } from "@nmshd/content"; -import { CoreId, ICoreId } from "@nmshd/transport"; -import { ILocalAttributeShareInfo, LocalAttributeShareInfo, LocalAttributeShareInfoJSON } from "./LocalAttributeShareInfo"; - -export interface CreateLocalAttributeParamsJSON { - id?: string; - content: IdentityAttributeJSON | RelationshipAttributeJSON; - parentId?: string; - succeeds?: string; - shareInfo?: LocalAttributeShareInfoJSON; -} - -export interface ICreateLocalAttributeParams extends ISerializable { - id?: ICoreId; - content: IIdentityAttribute | IRelationshipAttribute; - parentId?: ICoreId; - succeeds?: ICoreId; - shareInfo?: ILocalAttributeShareInfo; -} - -export class CreateLocalAttributeParams extends Serializable implements ICreateLocalAttributeParams { - @serialize() - @validate({ nullable: true }) - public id?: CoreId; - - @serialize({ unionTypes: [IdentityAttribute, RelationshipAttribute] }) - @validate() - public content: IdentityAttribute | RelationshipAttribute; - - @serialize() - @validate({ nullable: true }) - public parentId?: CoreId; - - @serialize() - @validate({ nullable: true }) - public succeeds?: CoreId; - - @serialize() - @validate({ nullable: true }) - public shareInfo?: LocalAttributeShareInfo; - - public static from(value: ICreateLocalAttributeParams | CreateLocalAttributeParamsJSON): CreateLocalAttributeParams { - return this.fromAny(value); - } -} diff --git a/packages/consumption/src/modules/attributes/local/CreateRepositoryAttributeParams.ts b/packages/consumption/src/modules/attributes/local/CreateRepositoryAttributeParams.ts new file mode 100644 index 000000000..0e081dcc4 --- /dev/null +++ b/packages/consumption/src/modules/attributes/local/CreateRepositoryAttributeParams.ts @@ -0,0 +1,33 @@ +import { ISerializable, Serializable, serialize, validate } from "@js-soft/ts-serval"; +import { IIdentityAttribute, IdentityAttribute, IdentityAttributeJSON, RelationshipAttribute } from "@nmshd/content"; +import { CoreId, ICoreId } from "@nmshd/transport"; + +export interface CreateRepositoryAttributeParamsJSON { + id?: string; + content: IdentityAttributeJSON; + parentId?: string; +} + +export interface ICreateRepositoryAttributeParams extends ISerializable { + id?: ICoreId; + content: IIdentityAttribute; + parentId?: ICoreId; +} + +export class CreateRepositoryAttributeParams extends Serializable implements ICreateRepositoryAttributeParams { + @serialize() + @validate({ nullable: true }) + public id?: CoreId; + + @serialize({ unionTypes: [IdentityAttribute, RelationshipAttribute] }) + @validate() + public content: IdentityAttribute; + + @serialize() + @validate({ nullable: true }) + public parentId?: CoreId; + + public static from(value: ICreateRepositoryAttributeParams | CreateRepositoryAttributeParamsJSON): CreateRepositoryAttributeParams { + return this.fromAny(value); + } +} diff --git a/packages/consumption/src/modules/attributes/local/CreatePeerLocalAttributeParams.ts b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts similarity index 69% rename from packages/consumption/src/modules/attributes/local/CreatePeerLocalAttributeParams.ts rename to packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts index 83696cc4e..2f1cc5e81 100644 --- a/packages/consumption/src/modules/attributes/local/CreatePeerLocalAttributeParams.ts +++ b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts @@ -2,24 +2,24 @@ import { ISerializable, Serializable, serialize, validate } from "@js-soft/ts-se import { IIdentityAttribute, IRelationshipAttribute, IdentityAttribute, IdentityAttributeJSON, RelationshipAttribute, RelationshipAttributeJSON } from "@nmshd/content"; import { CoreAddress, CoreId, ICoreAddress, ICoreId } from "@nmshd/transport"; -export interface CreatePeerLocalAttributeParamsJSON { - id: string; +export interface CreateSharedLocalAttributeParamsJSON { + id?: string; content: IdentityAttributeJSON | RelationshipAttributeJSON; requestReferece: string; peer: string; } -export interface ICreatePeerLocalAttributeParams extends ISerializable { +export interface ICreateSharedLocalAttributeParams extends ISerializable { id?: ICoreId; // needs to be optional because sometimes (e.g. when accepting a CreateAttributeRequestItem) the id is not known yet content: IIdentityAttribute | IRelationshipAttribute; requestReference: ICoreId; peer: ICoreAddress; } -export class CreatePeerLocalAttributeParams extends Serializable implements ICreatePeerLocalAttributeParams { +export class CreateSharedLocalAttributeParams extends Serializable implements ICreateSharedLocalAttributeParams { @serialize() - @validate() - public id: CoreId; + @validate({ nullable: true }) + public id?: CoreId; @serialize({ unionTypes: [IdentityAttribute, RelationshipAttribute] }) @validate() @@ -33,7 +33,7 @@ export class CreatePeerLocalAttributeParams extends Serializable implements ICre @validate() public peer: CoreAddress; - public static from(value: ICreatePeerLocalAttributeParams | CreatePeerLocalAttributeParamsJSON): CreatePeerLocalAttributeParams { + public static from(value: ICreateSharedLocalAttributeParams | CreateSharedLocalAttributeParamsJSON): CreateSharedLocalAttributeParams { return this.fromAny(value); } } diff --git a/packages/consumption/src/modules/attributes/local/LocalAttribute.ts b/packages/consumption/src/modules/attributes/local/LocalAttribute.ts index 21349a4df..45387c13b 100644 --- a/packages/consumption/src/modules/attributes/local/LocalAttribute.ts +++ b/packages/consumption/src/modules/attributes/local/LocalAttribute.ts @@ -1,10 +1,10 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { AbstractComplexValue, - IdentityAttribute, - IdentityAttributeJSON, IIdentityAttribute, IRelationshipAttribute, + IdentityAttribute, + IdentityAttributeJSON, RelationshipAttribute, RelationshipAttributeJSON } from "@nmshd/content"; @@ -23,6 +23,7 @@ export interface LocalAttributeJSON { shareInfo?: LocalAttributeShareInfoJSON; deletionInfo?: LocalAttributeDeletionInfoJSON; parentId?: string; + isDefault?: true; } export interface ILocalAttribute extends ICoreSynchronizable { @@ -33,36 +34,43 @@ export interface ILocalAttribute extends ICoreSynchronizable { shareInfo?: ILocalAttributeShareInfo; deletionInfo?: ILocalAttributeDeletionInfo; parentId?: ICoreId; + isDefault?: true; } export type OwnSharedIdentityAttribute = LocalAttribute & { content: IdentityAttribute; shareInfo: LocalAttributeShareInfo; + isDefault: undefined; }; export type OwnSharedRelationshipAttribute = LocalAttribute & { content: RelationshipAttribute; shareInfo: LocalAttributeShareInfo; + isDefault: undefined; }; export type PeerSharedIdentityAttribute = LocalAttribute & { content: IdentityAttribute; shareInfo: LocalAttributeShareInfo & { sourceAttribute: undefined }; + isDefault: undefined; }; export type PeerSharedRelationshipAttribute = LocalAttribute & { content: RelationshipAttribute; shareInfo: LocalAttributeShareInfo & { sourceAttribute: undefined }; + isDefault: undefined; }; export type ThirdPartyOwnedRelationshipAttribute = LocalAttribute & { content: RelationshipAttribute; shareInfo: LocalAttributeShareInfo; + isDefault: undefined; }; export type RepositoryAttribute = LocalAttribute & { content: IdentityAttribute; shareInfo: undefined; + deletionInfo: undefined; }; @type("LocalAttribute") @@ -75,7 +83,8 @@ export class LocalAttribute extends CoreSynchronizable implements ILocalAttribut nameof((r) => r.succeededBy), nameof((r) => r.shareInfo), nameof((r) => r.deletionInfo), - nameof((r) => r.parentId) + nameof((r) => r.parentId), + nameof((r) => r.isDefault) ]; public override readonly userdataProperties = [nameof((r) => r.content)]; @@ -108,6 +117,10 @@ export class LocalAttribute extends CoreSynchronizable implements ILocalAttribut @serialize() public parentId?: CoreId; + @validate({ nullable: true }) + @serialize() + public isDefault?: true; + public isOwnSharedIdentityAttribute(ownAddress: CoreAddress, peerAddress?: CoreAddress): this is OwnSharedIdentityAttribute { return this.isIdentityAttribute() && this.isOwnSharedAttribute(ownAddress, peerAddress); } @@ -135,6 +148,8 @@ export class LocalAttribute extends CoreSynchronizable implements ILocalAttribut public isOwnSharedAttribute(ownAddress: CoreAddress, peerAddress?: CoreAddress): this is OwnSharedIdentityAttribute | OwnSharedRelationshipAttribute { let isOwnSharedAttribute = this.isShared() && this.isOwnedBy(ownAddress); + isOwnSharedAttribute &&= !this.isDefault; + if (peerAddress) isOwnSharedAttribute &&= this.shareInfo!.peer.equals(peerAddress); return isOwnSharedAttribute; } @@ -144,6 +159,8 @@ export class LocalAttribute extends CoreSynchronizable implements ILocalAttribut isPeerSharedAttribute &&= !this.shareInfo!.sourceAttribute; + isPeerSharedAttribute &&= !this.isDefault; + if (peerAddress) isPeerSharedAttribute &&= this.isOwnedBy(peerAddress); return isPeerSharedAttribute; } @@ -151,6 +168,8 @@ export class LocalAttribute extends CoreSynchronizable implements ILocalAttribut public isThirdPartyOwnedAttribute(ownAddress: CoreAddress, thirdPartyAddress?: CoreAddress): this is ThirdPartyOwnedRelationshipAttribute { let isThirdPartyOwnedAttribute = this.isShared() && !this.isOwnedBy(ownAddress) && !this.isOwnedBy(this.shareInfo.peer); + isThirdPartyOwnedAttribute &&= !this.isDefault; + if (thirdPartyAddress) isThirdPartyOwnedAttribute &&= this.isOwnedBy(thirdPartyAddress); return isThirdPartyOwnedAttribute; } diff --git a/packages/consumption/src/modules/common/ValidationResult.ts b/packages/consumption/src/modules/common/ValidationResult.ts index c6296f28c..99a817c0c 100644 --- a/packages/consumption/src/modules/common/ValidationResult.ts +++ b/packages/consumption/src/modules/common/ValidationResult.ts @@ -21,7 +21,13 @@ export abstract class ValidationResult { public static fromItems(items: ValidationResult[]): ValidationResult { return items.some((r) => r.isError()) - ? ValidationResult.error(new ApplicationError("inheritedFromItem", "Some child items have errors."), items) + ? ValidationResult.error( + new ApplicationError( + "error.consumption.validation.inheritedFromItem", + "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." + ), + items + ) : ValidationResult.success(items); } } diff --git a/packages/consumption/src/modules/notifications/NotificationsController.ts b/packages/consumption/src/modules/notifications/NotificationsController.ts index c9ed215d2..23a8293ac 100644 --- a/packages/consumption/src/modules/notifications/NotificationsController.ts +++ b/packages/consumption/src/modules/notifications/NotificationsController.ts @@ -1,6 +1,6 @@ import { Event, EventBus } from "@js-soft/ts-utils"; import { Notification, NotificationItem } from "@nmshd/content"; -import { CoreId, Message, SynchronizedCollection, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; +import { CoreAddress, CoreId, Message, SynchronizedCollection, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; import { ConsumptionBaseController } from "../../consumption/ConsumptionBaseController"; import { ConsumptionController } from "../../consumption/ConsumptionController"; import { ConsumptionControllerName } from "../../consumption/ConsumptionControllerName"; @@ -34,7 +34,7 @@ export class NotificationsController extends ConsumptionBaseController { } public async sent(message: Message): Promise { - if (!message.isOwn) throw new Error("Cannot send a Notification from a foreign message."); + if (!message.isOwn) throw new Error("Cannot mark a LocalNotification as sent from a received Message."); const content = this.extractNotificationFromMessage(message); @@ -156,4 +156,11 @@ export class NotificationsController extends ConsumptionBaseController { return notification; } + + public async deleteNotificationsExchangedWithPeer(peer: CoreAddress): Promise { + const notifications = await this.getNotifications({ peer: peer.toString() }); + for (const notification of notifications) { + await this.localNotifications.delete(notification); + } + } } diff --git a/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts b/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts index d7248fae9..cbbebdcd9 100644 --- a/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts +++ b/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts @@ -14,7 +14,7 @@ export class DecideRequestParametersValidator { } if (params.items.length !== request.content.items.length) { - return ValidationResult.error(CoreErrors.requests.decideValidation.invalidNumberOfItems("Number of items in Request and Response do not match")); + return ValidationResult.error(CoreErrors.requests.decideValidation.invalidNumberOfItems("The number of items in the Request and the Response do not match.")); } const validationResults = request.content.items.map((requestItem, index) => this.checkItemOrGroup(requestItem, params.items[index], params.accept)); @@ -24,25 +24,27 @@ export class DecideRequestParametersValidator { private checkItemOrGroup( requestItem: RequestItem | RequestItemGroup, responseItem: DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON, - isParentAccepted: boolean + isRequestAccepted: boolean ): ValidationResult { if (requestItem instanceof RequestItem) { - return this.checkItem(requestItem, responseItem, isParentAccepted); + return this.checkItem(requestItem, responseItem, isRequestAccepted); } - return this.checkItemGroup(requestItem, responseItem, isParentAccepted); + return this.checkItemGroup(requestItem, responseItem, isRequestAccepted); } - private checkItem(requestItem: RequestItem, response: DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON, isParentAccepted: boolean): ValidationResult { + private checkItem(requestItem: RequestItem, response: DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON, isRequestAccepted: boolean): ValidationResult { if (isDecideRequestItemGroupParametersJSON(response)) { return ValidationResult.error(CoreErrors.requests.decideValidation.requestItemAnsweredAsRequestItemGroup()); } - if (!isParentAccepted && response.accept) { - return ValidationResult.error(CoreErrors.requests.decideValidation.itemAcceptedButParentNotAccepted("The RequestItem was accepted, but the parent was not accepted.")); + if (!isRequestAccepted && response.accept) { + return ValidationResult.error( + CoreErrors.requests.decideValidation.itemAcceptedButRequestNotAccepted("The RequestItem was accepted, but the Request was not accepted.") + ); } - if (isParentAccepted && requestItem.mustBeAccepted && !response.accept) { + if (isRequestAccepted && requestItem.mustBeAccepted && !response.accept) { return ValidationResult.error( CoreErrors.requests.decideValidation.mustBeAcceptedItemNotAccepted("The RequestItem is flagged as 'mustBeAccepted', but it was not accepted.") ); @@ -54,33 +56,19 @@ export class DecideRequestParametersValidator { private checkItemGroup( requestItemGroup: RequestItemGroup, responseItemGroup: DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON, - isParentAccepted: boolean + isRequestAccepted: boolean ): ValidationResult { if (isDecideRequestItemParametersJSON(responseItemGroup)) { return ValidationResult.error(CoreErrors.requests.decideValidation.requestItemGroupAnsweredAsRequestItem()); } if (responseItemGroup.items.length !== requestItemGroup.items.length) { - return ValidationResult.error(CoreErrors.requests.decideValidation.invalidNumberOfItems("Number of items in RequestItemGroup and ResponseItemGroup do not match")); - } - - const isGroupAccepted = responseItemGroup.items.some((value) => value.accept); - - if (!isParentAccepted && isGroupAccepted) { - return ValidationResult.error( - CoreErrors.requests.decideValidation.itemAcceptedButParentNotAccepted("The RequestItemGroup was accepted, but the parent was not accepted.") - ); - } - - if (isParentAccepted && requestItemGroup.mustBeAccepted && !isGroupAccepted) { return ValidationResult.error( - CoreErrors.requests.decideValidation.mustBeAcceptedItemNotAccepted( - "The RequestItemGroup is flagged as 'mustBeAccepted', but it was not accepted. Please accept all 'mustBeAccepted' items in this group." - ) + CoreErrors.requests.decideValidation.invalidNumberOfItems("The number of items in the RequestItemGroup and the ResponseItemGroup do not match.") ); } - const validationResults = requestItemGroup.items.map((requestItem, index) => this.checkItem(requestItem, responseItemGroup.items[index], isGroupAccepted)); + const validationResults = requestItemGroup.items.map((requestItem, index) => this.checkItem(requestItem, responseItemGroup.items[index], isRequestAccepted)); return ValidationResult.fromItems(validationResults); } } diff --git a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts index a773ab9b7..a34a5b7c8 100644 --- a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts +++ b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts @@ -1,7 +1,19 @@ import { ServalError } from "@js-soft/ts-serval"; import { EventBus } from "@js-soft/ts-utils"; import { RequestItem, RequestItemGroup, Response, ResponseItemDerivations, ResponseItemGroup, ResponseResult } from "@nmshd/content"; -import { CoreAddress, CoreDate, CoreId, ICoreAddress, ICoreId, Message, RelationshipTemplate, SynchronizedCollection, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; +import { + CoreAddress, + CoreDate, + CoreId, + ICoreAddress, + ICoreId, + Message, + Relationship, + RelationshipStatus, + RelationshipTemplate, + SynchronizedCollection, + CoreErrors as TransportCoreErrors +} from "@nmshd/transport"; import { ConsumptionBaseController } from "../../../consumption/ConsumptionBaseController"; import { ConsumptionController } from "../../../consumption/ConsumptionController"; import { ConsumptionControllerName } from "../../../consumption/ConsumptionControllerName"; @@ -14,13 +26,13 @@ import { RequestItemProcessorRegistry } from "../itemProcessors/RequestItemProce import { ILocalRequestSource, LocalRequest } from "../local/LocalRequest"; import { LocalRequestStatus } from "../local/LocalRequestStatus"; import { LocalResponse, LocalResponseSource } from "../local/LocalResponse"; +import { DecideRequestParametersValidator } from "./DecideRequestParametersValidator"; import { CheckPrerequisitesOfIncomingRequestParameters, ICheckPrerequisitesOfIncomingRequestParameters } from "./checkPrerequisites/CheckPrerequisitesOfIncomingRequestParameters"; import { CompleteIncomingRequestParameters, ICompleteIncomingRequestParameters } from "./complete/CompleteIncomingRequestParameters"; import { DecideRequestItemGroupParametersJSON } from "./decide/DecideRequestItemGroupParameters"; import { DecideRequestItemParametersJSON } from "./decide/DecideRequestItemParameters"; import { DecideRequestParametersJSON } from "./decide/DecideRequestParameters"; import { InternalDecideRequestParameters, InternalDecideRequestParametersJSON } from "./decide/InternalDecideRequestParameters"; -import { DecideRequestParametersValidator } from "./DecideRequestParametersValidator"; import { IReceivedIncomingRequestParameters, ReceivedIncomingRequestParameters } from "./received/ReceivedIncomingRequestParameters"; import { IRequireManualDecisionOfIncomingRequestParameters, @@ -35,7 +47,10 @@ export class IncomingRequestsController extends ConsumptionBaseController { private readonly processorRegistry: RequestItemProcessorRegistry, parent: ConsumptionController, private readonly eventBus: EventBus, - private readonly identity: { address: CoreAddress } + private readonly identity: { address: CoreAddress }, + private readonly relationshipResolver: { + getRelationshipToIdentity(id: CoreAddress): Promise; + } ) { super(ConsumptionControllerName.RequestsController, parent); } @@ -165,9 +180,21 @@ export class IncomingRequestsController extends ConsumptionBaseController { private async canDecide(params: InternalDecideRequestParametersJSON): Promise { // syntactic validation InternalDecideRequestParameters.from(params); - const request = await this.getOrThrow(params.requestId); + const relationship = await this.relationshipResolver.getRelationshipToIdentity(request.peer); + // It is safe to decide an incoming Request when no Relationship is found as this is the case when the Request origins from onNewRelationship of the RelationshipTemplateContent + const possibleStatuses = + request.source?.type === "RelationshipTemplate" ? [RelationshipStatus.Active, RelationshipStatus.Rejected, RelationshipStatus.Revoked] : [RelationshipStatus.Active]; + + if (relationship && !possibleStatuses.includes(relationship.status)) { + return ValidationResult.error( + CoreErrors.requests.wrongRelationshipStatus( + `You cannot decide a request from '${request.peer.toString()}' since the relationship is in status '${relationship.status}'.` + ) + ); + } + this.assertRequestStatus(request, LocalRequestStatus.DecisionRequired, LocalRequestStatus.ManualDecisionRequired); const validationResult = this.decideRequestParamsValidator.validate(params, request); @@ -339,7 +366,7 @@ export class IncomingRequestsController extends ConsumptionBaseController { if (parsedParams.responseSourceObject) { request.response!.source = LocalResponseSource.from({ - type: parsedParams.responseSourceObject instanceof Message ? "Message" : "RelationshipChange", + type: parsedParams.responseSourceObject instanceof Message ? "Message" : "Relationship", reference: parsedParams.responseSourceObject.id }); } else if (!requestIsRejected || !requestIsFromTemplate) { @@ -395,6 +422,13 @@ export class IncomingRequestsController extends ConsumptionBaseController { await this.localRequests.update(requestDoc, request); } + public async deleteRequestsFromPeer(peer: CoreAddress): Promise { + const requests = await this.getIncomingRequests({ peer: peer.toString() }); + for (const request of requests) { + await this.localRequests.delete(request); + } + } + private assertRequestStatus(request: LocalRequest, ...status: LocalRequestStatus[]) { if (!status.includes(request.status)) { throw new ConsumptionError(`Local Request has to be in status '${status.join("/")}'.`); diff --git a/packages/consumption/src/modules/requests/incoming/complete/CompleteIncomingRequestParameters.ts b/packages/consumption/src/modules/requests/incoming/complete/CompleteIncomingRequestParameters.ts index 56da95c66..c71cfdc72 100644 --- a/packages/consumption/src/modules/requests/incoming/complete/CompleteIncomingRequestParameters.ts +++ b/packages/consumption/src/modules/requests/incoming/complete/CompleteIncomingRequestParameters.ts @@ -1,9 +1,9 @@ import { ISerializable, Serializable, serialize, validate } from "@js-soft/ts-serval"; -import { CoreId, ICoreId, IMessage, IRelationshipChange, Message, RelationshipChange } from "@nmshd/transport"; +import { CoreId, ICoreId, IMessage, IRelationship, Message, Relationship } from "@nmshd/transport"; export interface ICompleteIncomingRequestParameters extends ISerializable { requestId: ICoreId; - responseSourceObject?: IMessage | IRelationshipChange; + responseSourceObject?: IMessage | IRelationship; } export class CompleteIncomingRequestParameters extends Serializable implements ICompleteIncomingRequestParameters { @@ -11,9 +11,9 @@ export class CompleteIncomingRequestParameters extends Serializable implements I @validate() public requestId: CoreId; - @serialize({ unionTypes: [Message, RelationshipChange] }) + @serialize({ unionTypes: [Message, Relationship] }) @validate({ nullable: true }) - public responseSourceObject?: Message | RelationshipChange; + public responseSourceObject?: Message | Relationship; public static from(value: ICompleteIncomingRequestParameters): CompleteIncomingRequestParameters { return this.fromAny(value); diff --git a/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts index 3aa8ee2a3..87b225c8c 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts @@ -18,26 +18,46 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce const ownerIsEmptyString = requestItem.attribute.owner.toString() === ""; if (requestItem.attribute instanceof IdentityAttribute) { + if (recipientIsAttributeOwner || ownerIsEmptyString) { + return ValidationResult.success(); + } + if (senderIsAttributeOwner) { return ValidationResult.error( - CoreErrors.requests.invalidRequestItem("Cannot create own Attributes with a CreateAttributeRequestItem. Use a ShareAttributeRequestItem instead.") + CoreErrors.requests.invalidRequestItem("Cannot create own IdentityAttributes with a CreateAttributeRequestItem. Use a ShareAttributeRequestItem instead.") ); } - if (recipientIsAttributeOwner || ownerIsEmptyString || !recipient) return ValidationResult.success(); + if (typeof recipient !== "undefined") { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem( + "The owner of the provided IdentityAttribute for the `attribute` property can only be the Recipient's Address or an empty string. The latter will default to the Recipient's Address." + ) + ); + } return ValidationResult.error( CoreErrors.requests.invalidRequestItem( - "The owner of the given `attribute` can only be the recipient's address or an empty string. The latter will default to the recipient's address." + "The owner of the provided IdentityAttribute for the `attribute` property can only be an empty string. It will default to the Recipient's Address." ) ); } - if (recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString || !recipient) return ValidationResult.success(); + if (recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString) { + return ValidationResult.success(); + } + + if (typeof recipient !== "undefined") { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem( + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the Sender's Address, the Recipient's Address or an empty string. The latter will default to the Recipient's Address." + ) + ); + } return ValidationResult.error( CoreErrors.requests.invalidRequestItem( - "The owner of the given 'attribute' can only be the sender's address, the recipient's address or an empty string. The latter will default to the recipient's address." + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the Sender's Address or an empty string. The latter will default to the Recipient's Address." ) ); } @@ -54,7 +74,7 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce let sharedAttribute: LocalAttribute; if (requestItem.attribute instanceof IdentityAttribute) { - const repositoryAttribute = await this.consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await this.consumptionController.attributes.createRepositoryAttribute({ content: requestItem.attribute }); @@ -64,7 +84,7 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce sourceAttributeId: repositoryAttribute.id }); } else { - sharedAttribute = await this.consumptionController.attributes.createPeerLocalAttribute({ + sharedAttribute = await this.consumptionController.attributes.createSharedLocalAttribute({ content: requestItem.attribute, peer: requestInfo.peer, requestReference: requestInfo.id @@ -90,7 +110,7 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce requestItem.attribute.owner = requestInfo.peer; } - await this.consumptionController.attributes.createPeerLocalAttribute({ + await this.consumptionController.attributes.createSharedLocalAttribute({ id: responseItem.attributeId, content: requestItem.attribute, peer: requestInfo.peer, diff --git a/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts index a9f702cb2..ff54f84ec 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts @@ -2,6 +2,7 @@ import { AttributeAlreadySharedAcceptResponseItem, AttributeSuccessionAcceptResponseItem, IdentityAttribute, + IdentityAttributeQuery, ProposeAttributeAcceptResponseItem, ProposeAttributeRequestItem, RejectResponseItem, @@ -11,12 +12,14 @@ import { ResponseItemResult } from "@nmshd/content"; import { CoreAddress, CoreId, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; +import { nameof } from "ts-simple-nameof"; import { CoreErrors } from "../../../../consumption/CoreErrors"; import { AttributeSuccessorParams, DeletionStatus, LocalAttributeShareInfo, PeerSharedAttributeSucceededEvent } from "../../../attributes"; import { LocalAttribute } from "../../../attributes/local/LocalAttribute"; import { ValidationResult } from "../../../common/ValidationResult"; import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; import { LocalRequestInfo } from "../IRequestItemProcessor"; +import validateAttributeMatchesWithQuery from "../utility/validateAttributeMatchesWithQuery"; import validateQuery from "../utility/validateQuery"; import { AcceptProposeAttributeRequestItemParameters, AcceptProposeAttributeRequestItemParametersJSON } from "./AcceptProposeAttributeRequestItemParameters"; @@ -27,19 +30,29 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc return queryValidationResult; } - const attributeValidationResult = this.validateAttribute(requestItem.attribute); + const attributeValidationResult = ProposeAttributeRequestItemProcessor.validateAttribute(requestItem.attribute); if (attributeValidationResult.isError()) { return attributeValidationResult; } + const proposedAttributeMatchesWithQueryValidationResult = validateAttributeMatchesWithQuery( + requestItem.query, + requestItem.attribute, + CoreAddress.from(""), + this.currentIdentityAddress + ); + if (proposedAttributeMatchesWithQueryValidationResult.isError()) { + return proposedAttributeMatchesWithQueryValidationResult; + } + return ValidationResult.success(); } - private validateAttribute(attribute: IdentityAttribute | RelationshipAttribute) { + private static validateAttribute(attribute: IdentityAttribute | RelationshipAttribute) { if (attribute.owner.toString() !== "") { return ValidationResult.error( CoreErrors.requests.invalidRequestItem( - "The owner of the given `attribute` can only be an empty string. This is because you can only propose Attributes where the recipient of the Request is the owner anyway. And in order to avoid mistakes, the owner will be automatically filled for you." + "The owner of the given `attribute` can only be an empty string. This is because you can only propose Attributes where the Recipient of the Request is the owner anyway. And in order to avoid mistakes, the owner will be automatically filled for you." ) ); } @@ -56,7 +69,7 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc if (requestItem.query instanceof RelationshipAttributeQuery && requestItem.query.owner.toString() !== "") { return ValidationResult.error( CoreErrors.requests.invalidRequestItem( - "The owner of the given `query` can only be an empty string. This is because you can only propose Attributes where the recipient of the Request is the owner anyway. And in order to avoid mistakes, the owner will be automatically filled for you." + "The owner of the given `query` can only be an empty string. This is because you can only propose Attributes where the Recipient of the Request is the owner anyway. And in order to avoid mistakes, the owner will be automatically filled for you." ) ); } @@ -65,44 +78,84 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc } public override async canAccept( - _requestItem: ProposeAttributeRequestItem, + requestItem: ProposeAttributeRequestItem, params: AcceptProposeAttributeRequestItemParametersJSON, requestInfo: LocalRequestInfo ): Promise { const parsedParams = AcceptProposeAttributeRequestItemParameters.from(params); - - let attribute = parsedParams.attribute; + let attribute; if (parsedParams.isWithExistingAttribute()) { - const foundAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId); + if (requestItem.query instanceof RelationshipAttributeQuery) { + return ValidationResult.error( + CoreErrors.requests.invalidAcceptParameters("When responding to a RelationshipAttributeQuery, only new RelationshipAttributes may be provided.") + ); + } - if (!foundAttribute) return ValidationResult.error(TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.attributeId.toString())); + const foundLocalAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId); - const latestSharedVersion = await this.consumptionController.attributes.getSharedVersionsOfAttribute(parsedParams.attributeId, [requestInfo.peer], true); - if (latestSharedVersion.length > 0) { - if (!latestSharedVersion[0].shareInfo?.sourceAttribute) { - throw new Error( - `The Attribute ${latestSharedVersion[0].id} doesn't have a 'shareInfo.sourceAttribute', even though it was found as shared version of an Attribute.` + if (!foundLocalAttribute) { + return ValidationResult.error(TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.attributeId.toString())); + } + + attribute = foundLocalAttribute.content; + + if (requestItem.query instanceof IdentityAttributeQuery && attribute instanceof IdentityAttribute && this.accountController.identity.isMe(attribute.owner)) { + if (foundLocalAttribute.isShared()) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + "The provided IdentityAttribute is a shared copy of a RepositoryAttribute. You can only share RepositoryAttributes." + ) ); } - const latestSharedVersionSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(latestSharedVersion[0].shareInfo.sourceAttribute); - if (!latestSharedVersionSourceAttribute) throw new Error(`The Attribute ${latestSharedVersion[0].shareInfo.sourceAttribute} was not found.`); + const ownSharedIdentityAttributeSuccessors = await this.consumptionController.attributes.getSharedSuccessorsOfAttribute(foundLocalAttribute, { + "shareInfo.peer": requestInfo.peer.toString() + }); + + if (ownSharedIdentityAttributeSuccessors.length > 0) { + if (!ownSharedIdentityAttributeSuccessors[0].shareInfo?.sourceAttribute) { + throw new Error( + `The LocalAttribute ${ownSharedIdentityAttributeSuccessors[0].id} does not have a 'shareInfo.sourceAttribute', even though it was found as a shared version of a LocalAttribute.` + ); + } + + const successorRepositorySourceAttribute = await this.consumptionController.attributes.getLocalAttribute( + ownSharedIdentityAttributeSuccessors[0].shareInfo.sourceAttribute + ); + if (!successorRepositorySourceAttribute) { + throw new Error(`The RepositoryAttribute ${ownSharedIdentityAttributeSuccessors[0].shareInfo.sourceAttribute} was not found.`); + } - if (await this.consumptionController.attributes.isSubsequentInSuccession(foundAttribute, latestSharedVersionSourceAttribute)) { - return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("You cannot share the predecessor of an already shared Attribute version.")); + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + `The provided IdentityAttribute is outdated. You have already shared the successor '${ownSharedIdentityAttributeSuccessors[0].shareInfo.sourceAttribute}' of it.` + ) + ); } } + } else if (parsedParams.isWithNewAttribute()) { + attribute = parsedParams.attribute; - attribute = foundAttribute.content; + const ownerIsEmpty = attribute.owner.equals(""); + if (ownerIsEmpty) { + attribute.owner = this.currentIdentityAddress; + } } - const ownerIsEmpty = attribute!.owner.equals(""); - const ownerIsCurrentIdentity = attribute!.owner.equals(this.currentIdentityAddress); - if (!ownerIsEmpty && !ownerIsCurrentIdentity) { - return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("The given Attribute belongs to someone else. You can only share own Attributes.")); + if (typeof attribute === "undefined") { + return ValidationResult.error( + CoreErrors.requests.invalidAcceptParameters( + `You have to specify either ${nameof( + (x) => x.attribute + )} or ${nameof((x) => x.attributeId)}.` + ) + ); } + const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer); + if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult; + return ValidationResult.success(); } @@ -112,8 +165,8 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc requestInfo: LocalRequestInfo ): Promise { const parsedParams = AcceptProposeAttributeRequestItemParameters.from(params); + let sharedLocalAttribute; - let sharedLocalAttribute: LocalAttribute; if (parsedParams.isWithExistingAttribute()) { const existingSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId); if (!existingSourceAttribute) throw TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.attributeId.toString()); @@ -172,16 +225,21 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc } } } + } else if (parsedParams.isWithNewAttribute()) { + if (parsedParams.attribute.owner.equals("")) { + parsedParams.attribute.owner = this.currentIdentityAddress; + } + sharedLocalAttribute = await this.createNewAttribute(parsedParams.attribute, requestInfo); } - if (!parsedParams.attribute) { + if (!sharedLocalAttribute) { throw new Error( - "The ProposeAttributeRequestItem wasn't answered with a new Attribute, but it wasn't handled as having been answered with an existing Attribute, either." + `You have to specify either ${nameof( + (x) => x.attribute + )} or ${nameof((x) => x.attributeId)}.` ); } - sharedLocalAttribute = await this.createNewAttribute(parsedParams.attribute, requestInfo); - return ProposeAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, attributeId: sharedLocalAttribute.id, @@ -204,18 +262,18 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc private async createNewAttribute(attribute: IdentityAttribute | RelationshipAttribute, requestInfo: LocalRequestInfo) { if (attribute instanceof IdentityAttribute) { - const repositoryLocalAttribute = await this.consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await this.consumptionController.attributes.createRepositoryAttribute({ content: attribute }); return await this.consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: CoreId.from(repositoryLocalAttribute.id), + sourceAttributeId: CoreId.from(repositoryAttribute.id), peer: CoreAddress.from(requestInfo.peer), requestReference: CoreId.from(requestInfo.id) }); } - return await this.consumptionController.attributes.createPeerLocalAttribute({ + return await this.consumptionController.attributes.createSharedLocalAttribute({ content: attribute, peer: requestInfo.peer, requestReference: CoreId.from(requestInfo.id) @@ -228,7 +286,7 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc requestInfo: LocalRequestInfo ): Promise { if (responseItem instanceof ProposeAttributeAcceptResponseItem) { - await this.consumptionController.attributes.createPeerLocalAttribute({ + await this.consumptionController.attributes.createSharedLocalAttribute({ id: responseItem.attributeId, content: responseItem.attribute, peer: requestInfo.peer, diff --git a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts index d29df2127..1d638bdfc 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts @@ -2,26 +2,32 @@ import { AttributeAlreadySharedAcceptResponseItem, AttributeSuccessionAcceptResponseItem, IdentityAttribute, + IdentityAttributeQuery, ReadAttributeAcceptResponseItem, ReadAttributeRequestItem, RejectResponseItem, RelationshipAttribute, + RelationshipAttributeConfidentiality, + RelationshipAttributeQuery, Request, - ResponseItemResult + ResponseItemResult, + ThirdPartyRelationshipAttributeQuery } from "@nmshd/content"; import { CoreAddress, CoreId, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; +import { nameof } from "ts-simple-nameof"; import { CoreErrors } from "../../../../consumption/CoreErrors"; import { AttributeSuccessorParams, DeletionStatus, LocalAttributeShareInfo, PeerSharedAttributeSucceededEvent } from "../../../attributes"; import { LocalAttribute } from "../../../attributes/local/LocalAttribute"; import { ValidationResult } from "../../../common/ValidationResult"; import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; import { LocalRequestInfo } from "../IRequestItemProcessor"; +import validateAttributeMatchesWithQuery from "../utility/validateAttributeMatchesWithQuery"; import validateQuery from "../utility/validateQuery"; import { AcceptReadAttributeRequestItemParameters, AcceptReadAttributeRequestItemParametersJSON } from "./AcceptReadAttributeRequestItemParameters"; export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcessor { public override canCreateOutgoingRequestItem(requestItem: ReadAttributeRequestItem, _request: Request, recipient?: CoreAddress): ValidationResult { - const queryValidationResult = validateQuery(requestItem.query, this.currentIdentityAddress, recipient); + const queryValidationResult = this.validateQuery(requestItem, recipient); if (queryValidationResult.isError()) { return queryValidationResult; } @@ -29,37 +35,148 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess return ValidationResult.success(); } + private validateQuery(requestItem: ReadAttributeRequestItem, recipient?: CoreAddress) { + const commonQueryValidationResult = validateQuery(requestItem.query, this.currentIdentityAddress, recipient); + if (commonQueryValidationResult.isError()) { + return commonQueryValidationResult; + } + + if (requestItem.query instanceof RelationshipAttributeQuery && !["", this.currentIdentityAddress.toString()].includes(requestItem.query.owner.toString())) { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem( + "The owner of the given `query` can only be an empty string or yourself. This is because you can only request RelationshipAttributes using a ReadAttributeRequestitem with a RelationshipAttributeQuery where the Recipient of the Request or yourself is the owner. And in order to avoid mistakes, the Recipient automatically will become the owner of the RelationshipAttribute later on if the owner of the `query` is an empty string." + ) + ); + } + + return ValidationResult.success(); + } + public override async canAccept( - _requestItem: ReadAttributeRequestItem, + requestItem: ReadAttributeRequestItem, params: AcceptReadAttributeRequestItemParametersJSON, requestInfo: LocalRequestInfo ): Promise { const parsedParams = AcceptReadAttributeRequestItemParameters.from(params); + let attribute; if (parsedParams.isWithExistingAttribute()) { - const foundAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.existingAttributeId); - if (!foundAttribute) return ValidationResult.error(TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.existingAttributeId.toString())); + if (requestItem.query instanceof RelationshipAttributeQuery) { + return ValidationResult.error( + CoreErrors.requests.invalidAcceptParameters("When responding to a RelationshipAttributeQuery, only new RelationshipAttributes may be provided.") + ); + } + + const foundLocalAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.existingAttributeId); - const ownerIsCurrentIdentity = this.accountController.identity.isMe(foundAttribute.content.owner); - if (!ownerIsCurrentIdentity && foundAttribute.content instanceof IdentityAttribute) { - return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("The given Attribute belongs to someone else. You can only share own Attributes.")); + if (!foundLocalAttribute) { + return ValidationResult.error(TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.existingAttributeId.toString())); } - const latestSharedVersion = await this.consumptionController.attributes.getSharedVersionsOfAttribute(parsedParams.existingAttributeId, [requestInfo.peer], true); - if (latestSharedVersion.length > 0) { - if (!latestSharedVersion[0].shareInfo?.sourceAttribute) { + attribute = foundLocalAttribute.content; + + if (requestItem.query instanceof IdentityAttributeQuery && attribute instanceof IdentityAttribute && this.accountController.identity.isMe(attribute.owner)) { + if (foundLocalAttribute.isShared()) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + "The provided IdentityAttribute is a shared copy of a RepositoryAttribute. You can only share RepositoryAttributes." + ) + ); + } + + const ownSharedIdentityAttributeSuccessors = await this.consumptionController.attributes.getSharedSuccessorsOfAttribute(foundLocalAttribute, { + "shareInfo.peer": requestInfo.peer.toString() + }); + + if (ownSharedIdentityAttributeSuccessors.length > 0) { + if (!ownSharedIdentityAttributeSuccessors[0].shareInfo?.sourceAttribute) { + throw new Error( + `The LocalAttribute ${ownSharedIdentityAttributeSuccessors[0].id} does not have a 'shareInfo.sourceAttribute', even though it was found as a shared version of a LocalAttribute.` + ); + } + + const successorSourceRepositoryAttribute = await this.consumptionController.attributes.getLocalAttribute( + ownSharedIdentityAttributeSuccessors[0].shareInfo.sourceAttribute + ); + if (!successorSourceRepositoryAttribute) { + throw new Error(`The RepositoryAttribute ${ownSharedIdentityAttributeSuccessors[0].shareInfo.sourceAttribute} was not found.`); + } + + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + `The provided IdentityAttribute is outdated. You have already shared the successor '${ownSharedIdentityAttributeSuccessors[0].shareInfo.sourceAttribute}' of it.` + ) + ); + } + } + + if (requestItem.query instanceof ThirdPartyRelationshipAttributeQuery && attribute instanceof RelationshipAttribute) { + if (!foundLocalAttribute.isShared()) { throw new Error( - `The Attribute ${latestSharedVersion[0].id} doesn't have a 'shareInfo.sourceAttribute', even though it was found as shared version of an Attribute.` + "The LocalAttribute found is faulty because its shareInfo is undefined, although its content is given by a RelationshipAttribute. Since RelationshipAttributes only make sense in the context of Relationships, they must always be shared." ); } - const latestSharedVersionSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(latestSharedVersion[0].shareInfo.sourceAttribute); - if (!latestSharedVersionSourceAttribute) throw new Error(`The Attribute ${latestSharedVersion[0].shareInfo.sourceAttribute} was not found.`); + if (typeof foundLocalAttribute.shareInfo.sourceAttribute !== "undefined") { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + "When responding to a ThirdPartyRelationshipAttributeQuery, only RelationshipAttributes that are not a copy of a sourceAttribute may be provided." + ) + ); + } - if (await this.consumptionController.attributes.isSubsequentInSuccession(foundAttribute, latestSharedVersionSourceAttribute)) { - return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("You cannot share the predecessor of an already shared Attribute version.")); + const queriedThirdParties = requestItem.query.thirdParty.map((aThirdParty) => aThirdParty.toString()); + + if ( + (this.accountController.identity.isMe(attribute.owner) || queriedThirdParties.includes(attribute.owner.toString())) && + !queriedThirdParties.includes("") && + !queriedThirdParties.includes(foundLocalAttribute.shareInfo.peer.toString()) + ) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + "The provided RelationshipAttribute exists in the context of a Relationship with a third party that should not be involved." + ) + ); } } + } else if (parsedParams.isWithNewAttribute()) { + if (requestItem.query instanceof ThirdPartyRelationshipAttributeQuery) { + return ValidationResult.error( + CoreErrors.requests.invalidAcceptParameters( + "When responding to a ThirdPartyRelationshipAttributeQuery, only RelationshipAttributes that already exist may be provided." + ) + ); + } + + attribute = parsedParams.newAttribute; + + const ownerIsEmpty = attribute.owner.equals(""); + if (ownerIsEmpty) { + attribute.owner = this.currentIdentityAddress; + } + } + + if (typeof attribute === "undefined") { + return ValidationResult.error( + CoreErrors.requests.invalidAcceptParameters( + `You have to specify either ${nameof( + (x) => x.newAttribute + )} or ${nameof((x) => x.existingAttributeId)}.` + ) + ); + } + + const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer); + if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult; + + if ( + requestItem.query instanceof ThirdPartyRelationshipAttributeQuery && + attribute instanceof RelationshipAttribute && + attribute.confidentiality === RelationshipAttributeConfidentiality.Private + ) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("The confidentiality of the provided RelationshipAttribute is private. Therefore you are not allowed to share it.") + ); } return ValidationResult.success(); @@ -71,8 +188,8 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess requestInfo: LocalRequestInfo ): Promise { const parsedParams = AcceptReadAttributeRequestItemParameters.from(params); + let sharedLocalAttribute; - let sharedLocalAttribute: LocalAttribute; if (parsedParams.isWithExistingAttribute()) { const existingSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.existingAttributeId); if (!existingSourceAttribute) { @@ -139,13 +256,21 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess }); } } + } else if (parsedParams.isWithNewAttribute()) { + if (parsedParams.newAttribute.owner.equals("")) { + parsedParams.newAttribute.owner = this.currentIdentityAddress; + } + sharedLocalAttribute = await this.createNewAttribute(parsedParams.newAttribute, requestInfo); } - if (!parsedParams.newAttribute) { - throw new Error("The ReadAttributeRequestItem wasn't answered with a new Attribute, but it wasn't handled as having been answered with an existing Attribute, either."); + if (!sharedLocalAttribute) { + throw new Error( + `You have to specify either ${nameof( + (x) => x.newAttribute + )} or ${nameof((x) => x.existingAttributeId)}.` + ); } - sharedLocalAttribute = await this.createNewAttribute(parsedParams.newAttribute, requestInfo); return ReadAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, attributeId: sharedLocalAttribute.id, @@ -194,18 +319,18 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess private async createNewAttribute(attribute: IdentityAttribute | RelationshipAttribute, requestInfo: LocalRequestInfo) { if (attribute instanceof IdentityAttribute) { - const repositoryLocalAttribute = await this.consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await this.consumptionController.attributes.createRepositoryAttribute({ content: attribute }); return await this.consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: CoreId.from(repositoryLocalAttribute.id), + sourceAttributeId: CoreId.from(repositoryAttribute.id), peer: CoreAddress.from(requestInfo.peer), requestReference: CoreId.from(requestInfo.id) }); } - return await this.consumptionController.attributes.createPeerLocalAttribute({ + return await this.consumptionController.attributes.createSharedLocalAttribute({ content: attribute, peer: requestInfo.peer, requestReference: CoreId.from(requestInfo.id) @@ -218,7 +343,7 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess requestInfo: LocalRequestInfo ): Promise { if (responseItem instanceof ReadAttributeAcceptResponseItem) { - await this.consumptionController.attributes.createPeerLocalAttribute({ + await this.consumptionController.attributes.createSharedLocalAttribute({ id: responseItem.attributeId, content: responseItem.attribute, peer: requestInfo.peer, diff --git a/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts index c388887b3..5fa1eda12 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts @@ -11,6 +11,7 @@ import { import { CoreAddress } from "@nmshd/transport"; import _ from "lodash"; import { CoreErrors } from "../../../../consumption/CoreErrors"; +import { DeletionStatus } from "../../../attributes"; import { ValidationResult } from "../../../common/ValidationResult"; import { AcceptRequestItemParametersJSON } from "../../incoming/decide/AcceptRequestItemParameters"; import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; @@ -18,8 +19,9 @@ import { LocalRequestInfo } from "../IRequestItemProcessor"; export class ShareAttributeRequestItemProcessor extends GenericRequestItemProcessor { public override async canCreateOutgoingRequestItem(requestItem: ShareAttributeRequestItem, _request: Request, recipient?: CoreAddress): Promise { - const attribute = await this.consumptionController.attributes.getLocalAttribute(requestItem.sourceAttributeId); - if (!attribute) { + const foundAttribute = await this.consumptionController.attributes.getLocalAttribute(requestItem.sourceAttributeId); + + if (typeof foundAttribute === "undefined") { return ValidationResult.error( CoreErrors.requests.invalidRequestItem(`The Attribute with the given sourceAttributeId '${requestItem.sourceAttributeId.toString()}' could not be found.`) ); @@ -30,46 +32,120 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces requestItemAttributeJSON.owner = this.currentIdentityAddress.toString(); } - if (!_.isEqual(attribute.content.toJSON(), requestItemAttributeJSON)) { + if (!_.isEqual(foundAttribute.content.toJSON(), requestItemAttributeJSON)) { return ValidationResult.error( CoreErrors.requests.invalidRequestItem( - `The Attribute with the given sourceAttributeId '${requestItem.sourceAttributeId.toString()}' does not match the given attribute.` + `The Attribute with the given sourceAttributeId '${requestItem.sourceAttributeId.toString()}' does not match the given Attribute.` ) ); } + if (requestItem.attribute instanceof IdentityAttribute && this.accountController.identity.isMe(requestItem.attribute.owner)) { + if (foundAttribute.isShared()) { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem("The provided IdentityAttribute is a shared copy of a RepositoryAttribute. You can only share RepositoryAttributes.") + ); + } + + if (typeof recipient !== "undefined") { + const query: any = { + "shareInfo.sourceAttribute": requestItem.sourceAttributeId.toString(), + "shareInfo.peer": recipient.toString(), + "deletionInfo.deletionStatus": { $nin: [DeletionStatus.DeletedByPeer, DeletionStatus.ToBeDeletedByPeer] } + }; + + if ((await this.consumptionController.attributes.getLocalAttributes(query)).length > 0) { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem( + `The IdentityAttribute with the given sourceAttributeId '${requestItem.sourceAttributeId.toString()}' is already shared with the peer.` + ) + ); + } + + const ownSharedIdentityAttributeSuccessors = await this.consumptionController.attributes.getSharedSuccessorsOfAttribute(foundAttribute, { + "shareInfo.peer": recipient.toString(), + "deletionInfo.deletionStatus": { $nin: [DeletionStatus.DeletedByPeer, DeletionStatus.ToBeDeletedByPeer] } + }); + + if (ownSharedIdentityAttributeSuccessors.length > 0) { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem( + `The provided IdentityAttribute is outdated. Its successor '${ownSharedIdentityAttributeSuccessors[0].shareInfo?.sourceAttribute}' is already shared with the peer.` + ) + ); + } + + const ownSharedIdentityAttributePredecessors = await this.consumptionController.attributes.getSharedPredecessorsOfAttribute(foundAttribute, { + "shareInfo.peer": recipient.toString(), + "deletionInfo.deletionStatus": { $nin: [DeletionStatus.DeletedByPeer, DeletionStatus.ToBeDeletedByPeer] } + }); + + if (ownSharedIdentityAttributePredecessors.length > 0) { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem( + `The predecessor '${ownSharedIdentityAttributePredecessors[0].shareInfo?.sourceAttribute}' of the IdentityAttribute is already shared with the peer. Instead of sharing it, you should notify the peer about the Attribute succession.` + ) + ); + } + } + } + + if (requestItem.attribute instanceof RelationshipAttribute) { + if (!foundAttribute.isShared()) { + throw new Error( + "The LocalAttribute found is faulty because its shareInfo is undefined, although its content is given by a RelationshipAttribute. Since RelationshipAttributes only make sense in the context of Relationships, they must always be shared." + ); + } + + if (typeof foundAttribute.shareInfo.sourceAttribute !== "undefined") { + return ValidationResult.error(CoreErrors.requests.invalidRequestItem("You can only share RelationshipAttributes that are not a copy of a sourceAttribute.")); + } + + if (typeof recipient !== "undefined") { + const query: any = { + "shareInfo.sourceAttribute": requestItem.sourceAttributeId.toString(), + "shareInfo.peer": recipient.toString(), + "deletionInfo.deletionStatus": { $nin: [DeletionStatus.DeletedByPeer, DeletionStatus.ToBeDeletedByPeer] } + }; + const thirdPartyRelationshipAttribute = await this.consumptionController.attributes.getLocalAttributes(query); + + if (foundAttribute.shareInfo.peer.equals(recipient) || thirdPartyRelationshipAttribute.length > 0) { + return ValidationResult.error( + CoreErrors.requests.invalidRequestItem("The provided RelationshipAttribute already exists in the context of the Relationship with the peer.") + ); + } + } + } + if (requestItem.attribute instanceof IdentityAttribute) { return this.canCreateWithIdentityAttribute(requestItem); } - return this.canCreateWithRelationshipAttribute(requestItem.attribute, recipient); + return ShareAttributeRequestItemProcessor.canCreateWithRelationshipAttribute(requestItem.attribute, recipient); } private canCreateWithIdentityAttribute(requestItem: ShareAttributeRequestItem) { - const ownerIsEmpty = requestItem.attribute.owner.toString() === ""; const ownerIsCurrentIdentity = requestItem.attribute.owner.equals(this.currentIdentityAddress); - if (!ownerIsEmpty && !ownerIsCurrentIdentity) { + if (!ownerIsCurrentIdentity) { return ValidationResult.error( - CoreErrors.requests.invalidRequestItem( - "The owner of the given `attribute` can only be an empty string. This is because you can only send Attributes where the recipient of the Request is the owner anyway. And in order to avoid mistakes, the owner will be automatically filled for you." - ) + CoreErrors.requests.invalidRequestItem("The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes.") ); } return ValidationResult.success(); } - private canCreateWithRelationshipAttribute(attribute: RelationshipAttribute, recipient?: CoreAddress) { + private static canCreateWithRelationshipAttribute(attribute: RelationshipAttribute, recipient?: CoreAddress) { + if (attribute.owner.equals(recipient)) { + return ValidationResult.error(CoreErrors.requests.invalidRequestItem("It doesn't make sense to share a RelationshipAttribute with its owner.")); + } + if (attribute.confidentiality === RelationshipAttributeConfidentiality.Private) { return ValidationResult.error( CoreErrors.requests.invalidRequestItem("The confidentiality of the given `attribute` is private. Therefore you are not allowed to share it.") ); } - if (attribute.owner.equals(recipient)) { - return ValidationResult.error(CoreErrors.requests.invalidRequestItem("It doesn't make sense to share a RelationshipAttribute with its owner.")); - } - return ValidationResult.success(); } @@ -82,7 +158,7 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces requestItem.attribute.owner = requestInfo.peer; } - const localAttribute = await this.consumptionController.attributes.createPeerLocalAttribute({ + const localAttribute = await this.consumptionController.attributes.createSharedLocalAttribute({ content: requestItem.attribute, peer: requestInfo.peer, requestReference: requestInfo.id diff --git a/packages/consumption/src/modules/requests/itemProcessors/utility/validateAttributeMatchesWithQuery.ts b/packages/consumption/src/modules/requests/itemProcessors/utility/validateAttributeMatchesWithQuery.ts new file mode 100644 index 000000000..bc8173162 --- /dev/null +++ b/packages/consumption/src/modules/requests/itemProcessors/utility/validateAttributeMatchesWithQuery.ts @@ -0,0 +1,196 @@ +import { + Consent, + IdentityAttribute, + IdentityAttributeQuery, + IQLQuery, + RelationshipAttribute, + RelationshipAttributeQuery, + ThirdPartyRelationshipAttributeQuery +} from "@nmshd/content"; +import { CoreAddress } from "@nmshd/transport"; +import { CoreErrors } from "../../../../consumption/CoreErrors"; +import { ValidationResult } from "../../../common/ValidationResult"; + +export default function validateAttributeMatchesWithQuery( + query: IdentityAttributeQuery | IQLQuery | RelationshipAttributeQuery | ThirdPartyRelationshipAttributeQuery, + attribute: IdentityAttribute | RelationshipAttribute, + recipient: CoreAddress, + sender: CoreAddress +): ValidationResult { + if (query instanceof IdentityAttributeQuery) { + const result = validateAttributeMatchesWithIdentityAttributeQuery(query, attribute, recipient); + if (result.isError()) return result; + } else if (query instanceof IQLQuery) { + const result = validateAttributeMatchesWithIQLQuery(query, attribute, recipient); + if (result.isError()) return result; + } else if (query instanceof RelationshipAttributeQuery) { + const result = validateAttributeMatchesWithRelationshipAttributeQuery(query, attribute, recipient); + if (result.isError()) return result; + } else if (query instanceof ThirdPartyRelationshipAttributeQuery) { + const result = validateAttributeMatchesWithThirdPartyRelationshipAttributeQuery(query, attribute, recipient, sender); + if (result.isError()) return result; + } else { + return ValidationResult.error( + CoreErrors.requests.unexpectedErrorDuringRequestItemProcessing( + "The query is not of a known type. Only the IdentityAttributeQuery, IQLQuery, RelationshipAttributeQuery or ThirdPartyRelationshipAttributeQuery can be used." + ) + ); + } + + if (query instanceof IdentityAttributeQuery || query instanceof RelationshipAttributeQuery || query instanceof ThirdPartyRelationshipAttributeQuery) { + if ( + (typeof query.validFrom === "undefined" && typeof attribute.validFrom !== "undefined") || + (typeof query.validFrom !== "undefined" && typeof attribute.validFrom !== "undefined" && query.validFrom.isBefore(attribute.validFrom)) + ) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided Attribute is not valid in the queried time frame.")); + } + + if ( + (typeof query.validTo === "undefined" && typeof attribute.validTo !== "undefined") || + (typeof query.validTo !== "undefined" && typeof attribute.validTo !== "undefined" && query.validTo.isAfter(attribute.validTo)) + ) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided Attribute is not valid in the queried time frame.")); + } + } + + return ValidationResult.success(); +} + +function validateAttributeMatchesWithIdentityAttributeQuery( + query: IdentityAttributeQuery, + attribute: IdentityAttribute | RelationshipAttribute, + recipient: CoreAddress +): ValidationResult { + if (!(attribute instanceof IdentityAttribute)) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided Attribute is not an IdentityAttribute, but an IdentityAttribute was queried.")); + } + + const recipientIsAttributeOwner = recipient.equals(attribute.owner); + + if (!recipientIsAttributeOwner) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes.") + ); + } + + if (query.valueType !== attribute.value.constructor.name) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided IdentityAttribute is not of the queried IdentityAttribute value type.")); + } + + if (typeof query.tags !== "undefined" && query.tags.length !== 0) { + if (attribute.tags === undefined || attribute.tags.length === 0 || !query.tags.some((aQueriedTag) => attribute.tags!.includes(aQueriedTag))) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The tags of the provided IdentityAttribute do not contain at least one queried tag.")); + } + } + + return ValidationResult.success(); +} + +function validateAttributeMatchesWithIQLQuery(query: IQLQuery, attribute: IdentityAttribute | RelationshipAttribute, recipient: CoreAddress): ValidationResult { + if (!(attribute instanceof IdentityAttribute)) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("The provided Attribute is not an IdentityAttribute. Currently, only IdentityAttributes can be queried by an IQLQuery.") + ); + } + + const recipientIsAttributeOwner = recipient.equals(attribute.owner); + + if (!recipientIsAttributeOwner) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes.") + ); + } + + return ValidationResult.success(); +} + +function validateAttributeMatchesWithRelationshipAttributeQuery( + query: RelationshipAttributeQuery, + attribute: IdentityAttribute | RelationshipAttribute, + recipient: CoreAddress +): ValidationResult { + if (!(attribute instanceof RelationshipAttribute)) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("The provided Attribute is not a RelationshipAttribute, but a RelationshipAttribute was queried.") + ); + } + + const recipientIsAttributeOwner = recipient.equals(attribute.owner); + const queriedOwnerIsEmpty = query.owner.equals(""); + + if (!queriedOwnerIsEmpty && !query.owner.equals(attribute.owner)) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not belong to the queried owner.")); + } + + if (queriedOwnerIsEmpty && !recipientIsAttributeOwner) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("You are not the owner of the provided RelationshipAttribute, but an empty string was specified for the owner of the query.") + ); + } + + if (query.key !== attribute.key) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not have the queried key.")); + } + + if (query.attributeCreationHints.confidentiality !== attribute.confidentiality) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not have the queried confidentiality.")); + } + + if (query.attributeCreationHints.valueType !== attribute.value.constructor.name) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute is not of the queried RelationshipAttribute value type.")); + } + + if (!(attribute.value instanceof Consent)) { + if (query.attributeCreationHints.title !== attribute.value.title) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not have the queried title.")); + } + + if (query.attributeCreationHints.description !== attribute.value.description) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not have the queried description.")); + } + } + + return ValidationResult.success(); +} + +function validateAttributeMatchesWithThirdPartyRelationshipAttributeQuery( + query: ThirdPartyRelationshipAttributeQuery, + attribute: IdentityAttribute | RelationshipAttribute, + recipient: CoreAddress, + sender: CoreAddress +): ValidationResult { + if (!(attribute instanceof RelationshipAttribute)) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch("The provided Attribute is not a RelationshipAttribute, but a RelationshipAttribute was queried.") + ); + } + + const recipientIsAttributeOwner = recipient.equals(attribute.owner); + const senderIsAttributeOwner = sender.equals(attribute.owner); + + const queriedThirdParties = query.thirdParty.map((aThirdParty) => aThirdParty.toString()); + + if ( + senderIsAttributeOwner || + (query.owner === "recipient" && !recipientIsAttributeOwner) || + (query.owner === "thirdParty" && + !queriedThirdParties.includes(attribute.owner.toString()) && + (!queriedThirdParties.includes("") || (recipientIsAttributeOwner && !queriedThirdParties.includes(recipient.toString())))) + ) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not belong to a queried owner.")); + } + + if (query.owner === "" && !recipientIsAttributeOwner && !queriedThirdParties.includes("") && !queriedThirdParties.includes(attribute.owner.toString())) { + return ValidationResult.error( + CoreErrors.requests.attributeQueryMismatch( + "Neither you nor one of the involved third parties is the owner of the provided RelationshipAttribute, but an empty string was specified for the owner of the query." + ) + ); + } + + if (query.key !== attribute.key) { + return ValidationResult.error(CoreErrors.requests.attributeQueryMismatch("The provided RelationshipAttribute does not have the queried key.")); + } + + return ValidationResult.success(); +} diff --git a/packages/consumption/src/modules/requests/itemProcessors/utility/validateQuery.ts b/packages/consumption/src/modules/requests/itemProcessors/utility/validateQuery.ts index eb01de8ed..1c8458e15 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/utility/validateQuery.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/utility/validateQuery.ts @@ -14,10 +14,6 @@ export default function validateQuery( const result = validateThirdParty(thirdParty, sender, recipient); if (result.isError()) return result; } - - if (query.owner.equals(sender)) { - return ValidationResult.error(CoreErrors.requests.invalidRequestItem("Cannot query own Attributes from a third party.")); - } } else if (query instanceof IQLQuery) { const validationResult = iqlValidate(query.queryString); if (!validationResult.isValid) { diff --git a/packages/consumption/src/modules/requests/local/LocalResponse.ts b/packages/consumption/src/modules/requests/local/LocalResponse.ts index d4943c0b4..6b28933c3 100644 --- a/packages/consumption/src/modules/requests/local/LocalResponse.ts +++ b/packages/consumption/src/modules/requests/local/LocalResponse.ts @@ -3,7 +3,7 @@ import { IResponse, Response } from "@nmshd/content"; import { CoreDate, CoreId, CoreSerializable, ICoreDate, ICoreId, ICoreSerializable } from "@nmshd/transport"; export interface ILocalResponseSource extends ICoreSerializable { - type: "Message" | "RelationshipChange"; + type: "Message" | "Relationship"; reference: ICoreId; } @@ -11,7 +11,7 @@ export interface ILocalResponseSource extends ICoreSerializable { export class LocalResponseSource extends CoreSerializable implements ILocalResponseSource { @serialize() @validate() - public type: "Message" | "RelationshipChange"; + public type: "Message" | "Relationship"; @serialize() @validate() diff --git a/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts b/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts index 4c7d8b13d..4152f1616 100644 --- a/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts +++ b/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts @@ -4,11 +4,10 @@ import { CoreAddress, CoreDate, CoreId, - ICoreAddress, ICoreId, Message, Relationship, - RelationshipChange, + RelationshipStatus, RelationshipTemplate, SynchronizedCollection, CoreErrors as TransportCoreErrors @@ -18,6 +17,7 @@ import { ConsumptionController } from "../../../consumption/ConsumptionControlle import { ConsumptionControllerName } from "../../../consumption/ConsumptionControllerName"; import { ConsumptionError } from "../../../consumption/ConsumptionError"; import { ConsumptionIds } from "../../../consumption/ConsumptionIds"; +import { CoreErrors } from "../../../consumption/CoreErrors"; import { DeletionStatus, LocalAttributeDeletionInfo } from "../../attributes"; import { ValidationResult } from "../../common/ValidationResult"; import { OutgoingRequestCreatedAndCompletedEvent, OutgoingRequestCreatedEvent, OutgoingRequestStatusChangedEvent } from "../events"; @@ -42,7 +42,7 @@ export class OutgoingRequestsController extends ConsumptionBaseController { private readonly eventBus: EventBus, private readonly identity: { address: CoreAddress }, private readonly relationshipResolver: { - getActiveRelationshipToIdentity(id: ICoreAddress): Promise; + getRelationshipToIdentity(id: CoreAddress): Promise; } ) { super(ConsumptionControllerName.RequestsController, parent); @@ -50,6 +50,24 @@ export class OutgoingRequestsController extends ConsumptionBaseController { public async canCreate(params: ICanCreateOutgoingRequestParameters): Promise { const parsedParams = CanCreateOutgoingRequestParameters.from(params); + if (parsedParams.peer) { + const relationship = await this.relationshipResolver.getRelationshipToIdentity(parsedParams.peer); + + // there should at minimum be a Pending relationship to the peer + if (!relationship) { + return ValidationResult.error( + CoreErrors.requests.missingRelationship(`You cannot create a request to '${parsedParams.peer.toString()}' since you are not in a relationship.`) + ); + } + + if (!(relationship.status === RelationshipStatus.Pending || relationship.status === RelationshipStatus.Active)) { + return ValidationResult.error( + CoreErrors.requests.wrongRelationshipStatus( + `You cannot create a request to '${parsedParams.peer.toString()}' since the relationship is in status '${relationship.status}'.` + ) + ); + } + } const innerResults = await this.canCreateItems(parsedParams.content, parsedParams.peer); @@ -126,17 +144,17 @@ export class OutgoingRequestsController extends ConsumptionBaseController { public async createAndCompleteFromRelationshipTemplateResponse(params: ICreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters): Promise { const parsedParams = CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters.from(params); - const peer = parsedParams.responseSource instanceof RelationshipChange ? parsedParams.responseSource.request.createdBy : parsedParams.responseSource.cache!.createdBy; + const peer = parsedParams.responseSource instanceof Relationship ? parsedParams.responseSource.peer.address : parsedParams.responseSource.cache!.createdBy; const response = parsedParams.response; const requestId = response.requestId; const templateContent = parsedParams.template.cache!.content; if (!(templateContent instanceof RelationshipTemplateContent)) { - throw new ConsumptionError("The content of the template is not supported as it is not type of RelationshipTemplateContent."); + throw new ConsumptionError("The content of the template is not of type RelationshipTemplateContent hence it's not possible to create a request from it."); } // checking for an active relationship is not secure as in the meantime the relationship could have been accepted - const isFromNewRelationship = parsedParams.responseSource instanceof RelationshipChange && parsedParams.responseSource.type === "Creation"; + const isFromNewRelationship = parsedParams.responseSource instanceof Relationship && parsedParams.responseSource.cache!.auditLog.length === 1; const requestContent = isFromNewRelationship ? templateContent.onNewRelationship : templateContent.onExistingRelationship; @@ -172,6 +190,13 @@ export class OutgoingRequestsController extends ConsumptionBaseController { return request; } + public async deleteRequestsToPeer(peer: CoreAddress): Promise { + const requests = await this.getOutgoingRequests({ peer: peer.toString() }); + for (const request of requests) { + await this.localRequests.delete(request); + } + } + private async _sent(requestId: CoreId, requestSourceObject: Message | RelationshipTemplate): Promise { const request = await this.getOrThrow(requestId); @@ -251,12 +276,13 @@ export class OutgoingRequestsController extends ConsumptionBaseController { return request; } - private async _complete(requestId: CoreId, responseSourceObject: Message | RelationshipChange, receivedResponse: Response): Promise { + private async _complete(requestId: CoreId, responseSourceObject: Message | Relationship, receivedResponse: Response): Promise { const request = await this.getOrThrow(requestId); this.assertRequestStatus(request, LocalRequestStatus.Open, LocalRequestStatus.Expired); - const responseSourceObjectCreationDate = responseSourceObject instanceof Message ? responseSourceObject.cache!.createdAt : responseSourceObject.request.createdAt; + const responseSourceObjectCreationDate = + responseSourceObject instanceof Message ? responseSourceObject.cache!.createdAt : responseSourceObject.cache!.auditLog[0].createdAt; if (request.status === LocalRequestStatus.Expired && request.isExpired(responseSourceObjectCreationDate)) { throw new ConsumptionError("Cannot complete an expired request with a response that was created before the expiration date"); } @@ -267,12 +293,12 @@ export class OutgoingRequestsController extends ConsumptionBaseController { await this.applyItems(request.content.items, receivedResponse.items, request); - let responseSource: "Message" | "RelationshipChange"; + let responseSource: "Message" | "Relationship"; if (responseSourceObject instanceof Message) { responseSource = "Message"; - } else if (responseSourceObject instanceof RelationshipChange) { - responseSource = "RelationshipChange"; + } else if (responseSourceObject instanceof Relationship) { + responseSource = "Relationship"; } else { throw new ConsumptionError("Invalid responseSourceObject"); } diff --git a/packages/consumption/src/modules/requests/outgoing/createAndCompleteFromRelationshipTemplateResponse/CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters.ts b/packages/consumption/src/modules/requests/outgoing/createAndCompleteFromRelationshipTemplateResponse/CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters.ts index 25b70f160..aee050f58 100644 --- a/packages/consumption/src/modules/requests/outgoing/createAndCompleteFromRelationshipTemplateResponse/CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters.ts +++ b/packages/consumption/src/modules/requests/outgoing/createAndCompleteFromRelationshipTemplateResponse/CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters.ts @@ -1,14 +1,14 @@ import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; import { IResponse, Response } from "@nmshd/content"; -import { IMessage, IRelationshipChange, IRelationshipTemplate, Message, RelationshipChange, RelationshipTemplate } from "@nmshd/transport"; +import { CoreDate, IMessage, IRelationship, IRelationshipTemplate, Message, Relationship, RelationshipTemplate } from "@nmshd/transport"; export interface ICreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters extends ISerializable { template: IRelationshipTemplate; - responseSource: IRelationshipChange | IMessage; + responseSource: IRelationship | IMessage; response: IResponse; } -@type("CreateAndCompleteOutgoingRequestFromRelationshipCreationChangeParameters") +@type("CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters") export class CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters extends Serializable implements ICreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters @@ -17,14 +17,18 @@ export class CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponsePar @validate() public template: RelationshipTemplate; - @serialize({ unionTypes: [RelationshipChange, Message] }) + @serialize({ unionTypes: [Relationship, Message] }) @validate() - public responseSource: RelationshipChange | Message; + public responseSource: Relationship | Message; @serialize() @validate() public response: Response; + @serialize() + @validate({ nullable: true }) + public responseCreationDate?: CoreDate; + public static from( value: ICreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters ): CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters { diff --git a/packages/consumption/src/modules/settings/SettingsController.ts b/packages/consumption/src/modules/settings/SettingsController.ts index ae6f0b607..9f3424b17 100644 --- a/packages/consumption/src/modules/settings/SettingsController.ts +++ b/packages/consumption/src/modules/settings/SettingsController.ts @@ -56,4 +56,11 @@ export class SettingsController extends ConsumptionBaseController { public async deleteSetting(setting: Setting): Promise { await this.settings.delete(setting); } + + public async deleteSettingsForRelationship(relationshipId: CoreId): Promise { + const settings = await this.getSettings({ reference: relationshipId.toString(), scope: SettingScope.Relationship }); + for (const setting of settings) { + await this.deleteSetting(setting); + } + } } diff --git a/packages/consumption/test/core/TestUtil.ts b/packages/consumption/test/core/TestUtil.ts index 307064d3d..2a28ff8ee 100644 --- a/packages/consumption/test/core/TestUtil.ts +++ b/packages/consumption/test/core/TestUtil.ts @@ -241,7 +241,7 @@ export class TestUtil { const relRequest = await to.relationships.sendRelationship({ template: templateTo, - content: requestContent ?? { + creationContent: requestContent ?? { metadata: { mycontent: "request" } } }); @@ -252,7 +252,7 @@ export class TestUtil { const pendingRelationship = syncedRelationships[0]; expect(pendingRelationship.status).toStrictEqual(RelationshipStatus.Pending); - const acceptedRelationshipFromSelf = await from.relationships.acceptChange(pendingRelationship.cache!.creationChange, {}); + const acceptedRelationshipFromSelf = await from.relationships.accept(pendingRelationship.id); expect(acceptedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Active); // Get accepted relationship @@ -270,6 +270,27 @@ export class TestUtil { return [acceptedRelationshipFromSelf, acceptedRelationshipPeer]; } + public static async terminateRelationship( + from: AccountController, + to: AccountController + ): Promise<{ terminatedRelationshipFromSelf: Relationship; terminatedRelationshipPeer: Relationship }> { + const relationshipId = (await from.relationships.getRelationshipToIdentity(to.identity.address))!.id; + const terminatedRelationshipFromSelf = await from.relationships.terminate(relationshipId); + const terminatedRelationshipPeer = (await TestUtil.syncUntil(to, (syncResult) => syncResult.relationships.length > 0)).relationships[0]; + + return { terminatedRelationshipFromSelf, terminatedRelationshipPeer }; + } + + public static async decomposeRelationship(fromAccount: AccountController, fromConsumption: ConsumptionController, to: AccountController): Promise { + const relationship = (await fromAccount.relationships.getRelationshipToIdentity(to.identity.address))!; + await fromAccount.relationships.decompose(relationship.id); + await fromAccount.cleanupDataOfDecomposedRelationship(relationship); + await fromConsumption.cleanupDataOfDecomposedRelationship(to.identity.address, relationship.id); + const decomposedRelationshipPeer = (await TestUtil.syncUntil(to, (syncResult) => syncResult.relationships.length > 0)).relationships[0]; + + return decomposedRelationshipPeer; + } + /** * SyncEvents in the backbone are only enventually consistent. This means that if you send a message now and * get all SyncEvents right after, you cannot rely on getting a NewMessage SyncEvent right away. So instead @@ -347,7 +368,7 @@ export class TestUtil { content: "request" }; - return await account.relationships.sendRelationship({ template, content }); + return await account.relationships.sendRelationship({ template, creationContent: content }); } public static async fetchRelationshipTemplateFromTokenReference(account: AccountController, tokenReference: string): Promise { diff --git a/packages/consumption/test/modules/attributeListeners/AttributeListenersController.test.ts b/packages/consumption/test/modules/attributeListeners/AttributeListenersController.test.ts index 85770b712..9f0637235 100644 --- a/packages/consumption/test/modules/attributeListeners/AttributeListenersController.test.ts +++ b/packages/consumption/test/modules/attributeListeners/AttributeListenersController.test.ts @@ -12,8 +12,8 @@ describe("AttributeListenersController", function () { let transport: Transport; const dummyQuery = ThirdPartyRelationshipAttributeQuery.from({ - key: "aKey", - owner: "anOwner", + key: "AKey", + owner: "", thirdParty: ["aThirdParty"] }); const dummyPeer = CoreAddress.from("aPeer"); @@ -82,4 +82,15 @@ describe("AttributeListenersController", function () { expect(listeners).toHaveLength(1); expect(listeners.map((l) => l.toJSON())).toContainEqual(expect.objectContaining({ id: listener.id.toString() })); }); + + test("should delete peer attribute listeners", async function () { + const listener = await consumptionController.attributeListeners.createAttributeListener({ + peer: dummyPeer, + query: dummyQuery + }); + + await consumptionController.attributeListeners.deletePeerAttributeListeners(dummyPeer); + const retrievedListener = await consumptionController.attributeListeners.getAttributeListener(listener.id); + expect(retrievedListener).toBeUndefined(); + }); }); diff --git a/packages/consumption/test/modules/attributes/AttributesController.test.ts b/packages/consumption/test/modules/attributes/AttributesController.test.ts index 299d3f275..5079721a2 100644 --- a/packages/consumption/test/modules/attributes/AttributesController.test.ts +++ b/packages/consumption/test/modules/attributes/AttributesController.test.ts @@ -1,6 +1,7 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { BirthDate, + BirthYear, City, Country, EMailAddress, @@ -10,11 +11,11 @@ import { IIQLQuery, IRelationshipAttributeQuery, Nationality, - PhoneNumber, RelationshipAttribute, RelationshipAttributeConfidentiality, Street, StreetAddress, + ThirdPartyRelationshipAttributeQueryOwner, ZipCode } from "@nmshd/content"; import { AccountController, CoreAddress, CoreDate, CoreId, Transport } from "@nmshd/transport"; @@ -24,9 +25,9 @@ import { ConsumptionController, DeletionStatus, IAttributeSuccessorParams, - ICreateLocalAttributeParams, - ICreatePeerLocalAttributeParams, + ICreateRepositoryAttributeParams, ICreateSharedLocalAttributeCopyParams, + ICreateSharedLocalAttributeParams, LocalAttribute, LocalAttributeDeletionInfo, LocalAttributeShareInfo, @@ -59,27 +60,7 @@ describe("AttributesController", function () { await connection.close(); }); - beforeEach(async function () { - const emailParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: EMailAddress.from({ - value: "my@email.address" - }), - owner: CoreAddress.from("address") - }) - }; - - const phoneParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: PhoneNumber.from({ - value: "000000000" - }), - owner: CoreAddress.from("address") - }) - }; - await consumptionController.attributes.createLocalAttribute(emailParams); - await consumptionController.attributes.createLocalAttribute(phoneParams); - + beforeEach(function () { mockEventBus.clearPublishedEvents(); }); @@ -91,450 +72,671 @@ describe("AttributesController", function () { } }); - test("should list all attributes", async function () { - const attributes = await consumptionController.attributes.getLocalAttributes(); - expect(attributes).toHaveLength(2); - }); + describe("create Attributes", function () { + test("should create new attributes", async function () { + const birthDateParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "DisplayName", + value: "ADisplayName" + }, + owner: consumptionController.accountController.identity.address + }) + }; + + const birthDate = await consumptionController.attributes.createRepositoryAttribute(birthDateParams); + expect(birthDate).toBeInstanceOf(LocalAttribute); + expect(birthDate.content).toBeInstanceOf(IdentityAttribute); - test("should create new attributes", async function () { - const attributesBeforeCreate = await consumptionController.attributes.getLocalAttributes(); - const nrAttributesBeforeCreate = attributesBeforeCreate.length; + const attributesAfterCreate = await consumptionController.attributes.getLocalAttributes(); + expect(attributesAfterCreate).toHaveLength(1); - const birthDateParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ + mockEventBus.expectPublishedEvents(AttributeCreatedEvent); + }); + + test("should create LocalAttributes for each property of a complex Identity Attribute", async function () { + const identityAttribute = IdentityAttribute.from({ value: { - "@type": "DisplayName", - value: "ADisplayName" + "@type": "StreetAddress", + recipient: "ARecipient", + street: "AStreet", + houseNo: "AHouseNo", + zipCode: "AZipCode", + city: "ACity", + country: "DE" }, - owner: CoreAddress.from("address") - }) - }; + validTo: CoreDate.utc(), + owner: consumptionController.accountController.identity.address + }); + + const address = await consumptionController.attributes.createRepositoryAttribute({ + content: identityAttribute + }); - const birthDate = await consumptionController.attributes.createLocalAttribute(birthDateParams); - expect(birthDate).toBeInstanceOf(LocalAttribute); - expect(birthDate.content).toBeInstanceOf(IdentityAttribute); + expect(address).toBeInstanceOf(LocalAttribute); + expect(address.content).toBeInstanceOf(IdentityAttribute); - const attributesAfterCreate = await consumptionController.attributes.getLocalAttributes(); - const nrAttributesAfterCreate = attributesAfterCreate.length; - expect(nrAttributesAfterCreate).toStrictEqual(nrAttributesBeforeCreate + 1); + const childAttributes = await consumptionController.attributes.getLocalAttributes({ + parentId: address.id.toString() + }); + expect(childAttributes).toHaveLength(5); + expect(childAttributes[0].content.value).toBeInstanceOf(Street); + expect(childAttributes[1].content.value).toBeInstanceOf(HouseNumber); + expect(childAttributes[2].content.value).toBeInstanceOf(ZipCode); + expect(childAttributes[3].content.value).toBeInstanceOf(City); + expect(childAttributes[4].content.value).toBeInstanceOf(Country); - mockEventBus.expectPublishedEvents(AttributeCreatedEvent); - }); + const attributesAfterCreate = await consumptionController.attributes.getLocalAttributes(); + expect(attributesAfterCreate).toHaveLength(6); + }); + + test("should trigger an AttributeCreatedEvent for each created child Attribute of a complex Attribute", async function () { + await consumptionController.attributes.getLocalAttributes(); - test("should create LocalAttributes for each property of a complex Identity Attribute", async function () { - const attributesBeforeCreate = await consumptionController.attributes.getLocalAttributes(); - const nrAttributesBeforeCreate = attributesBeforeCreate.length; - - const identityAttribute = IdentityAttribute.from({ - value: { - "@type": "StreetAddress", - recipient: "ARecipient", - street: "AStreet", - houseNo: "AHouseNo", - zipCode: "AZipCode", - city: "ACity", - country: "DE" - }, - validTo: CoreDate.utc(), - owner: CoreAddress.from("address") + const identityAttribute = IdentityAttribute.from({ + value: { + "@type": "StreetAddress", + recipient: "ARecipient", + street: "AStreet", + houseNo: "AHouseNo", + zipCode: "AZipCode", + city: "ACity", + country: "DE" + }, + validTo: CoreDate.utc(), + owner: consumptionController.accountController.identity.address + }); + + await consumptionController.attributes.createRepositoryAttribute({ content: identityAttribute }); + + mockEventBus.expectPublishedEvents( + AttributeCreatedEvent, + AttributeCreatedEvent, + AttributeCreatedEvent, + AttributeCreatedEvent, + AttributeCreatedEvent, + AttributeCreatedEvent + ); }); - const address = await consumptionController.attributes.createLocalAttribute({ - content: identityAttribute + test("should set an Attribute as default if it is the only of its value type", async function () { + const attributeParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: EMailAddress.from({ + value: "my@email.address" + }), + owner: consumptionController.accountController.identity.address + }) + }; + const attribute = await consumptionController.attributes.createRepositoryAttribute(attributeParams); + expect(attribute.isDefault).toBe(true); + }); + + test("should not set an Attribute as default if already another exists with that value type", async function () { + const attributeParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: EMailAddress.from({ + value: "my@email.address" + }), + owner: consumptionController.accountController.identity.address + }) + }; + const firstAttribute = await consumptionController.attributes.createRepositoryAttribute(attributeParams); + expect(firstAttribute.isDefault).toBe(true); + + const secondAttribute = await consumptionController.attributes.createRepositoryAttribute(attributeParams); + expect(secondAttribute.isDefault).toBeUndefined(); }); - expect(address).toBeInstanceOf(LocalAttribute); - expect(address.content).toBeInstanceOf(IdentityAttribute); + test("should set a child Attribute of a complex default attribute as default", async function () { + const complexBirthDate = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: BirthDate.from({ + day: 28, + month: 2, + year: 2000 + }), + owner: consumptionController.accountController.identity.address + }) + }); + expect(complexBirthDate.isDefault).toBe(true); - const childAttributes = await consumptionController.attributes.getLocalAttributes({ - parentId: address.id.toString() + const childBirthYear = await consumptionController.attributes.getLocalAttributes({ + parentId: complexBirthDate.id.toString(), + "content.value.@type": "BirthYear" + }); + expect(childBirthYear).toHaveLength(1); + expect(childBirthYear[0].isDefault).toBe(true); }); - expect(childAttributes).toHaveLength(5); - expect(childAttributes[0].content.value).toBeInstanceOf(Street); - expect(childAttributes[1].content.value).toBeInstanceOf(HouseNumber); - expect(childAttributes[2].content.value).toBeInstanceOf(ZipCode); - expect(childAttributes[3].content.value).toBeInstanceOf(City); - expect(childAttributes[4].content.value).toBeInstanceOf(Country); - - const attributesAfterCreate = await consumptionController.attributes.getLocalAttributes(); - const nrAttributesAfterCreate = attributesAfterCreate.length; - expect(nrAttributesAfterCreate).toStrictEqual(nrAttributesBeforeCreate + 6); - }); - test("should trigger an AttributeCreatedEvent for each created child Attribute of a complex Attribute", async function () { - await consumptionController.attributes.getLocalAttributes(); - - const identityAttribute = IdentityAttribute.from({ - value: { - "@type": "StreetAddress", - recipient: "ARecipient", - street: "AStreet", - houseNo: "AHouseNo", - zipCode: "AZipCode", - city: "ACity", - country: "DE" - }, - validTo: CoreDate.utc(), - owner: CoreAddress.from("address") + test("should set a child Attribute of a complex default attribute as default even if already another attribute with that value type exists", async function () { + const independentBirthYear = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: BirthYear.from({ + value: 2000 + }), + owner: consumptionController.accountController.identity.address + }) + }); + expect(independentBirthYear.isDefault).toBe(true); + + const complexBirthDate = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: BirthDate.from({ + day: 28, + month: 2, + year: 2000 + }), + owner: consumptionController.accountController.identity.address + }) + }); + expect(complexBirthDate.isDefault).toBe(true); + + const childBirthYear = await consumptionController.attributes.getLocalAttributes({ parentId: complexBirthDate.id.toString(), "content.value.@type": "BirthYear" }); + expect(childBirthYear).toHaveLength(1); + expect(childBirthYear[0].isDefault).toBe(true); + + const updatedIndependentBirthYear = await consumptionController.attributes.getLocalAttribute(independentBirthYear.id); + expect(updatedIndependentBirthYear!.isDefault).toBeUndefined(); }); - await consumptionController.attributes.createLocalAttribute({ content: identityAttribute }); + test("should allow to create a shared attribute copy", async function () { + const nationalityParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "DE" + }, + owner: testAccount.identity.address + }) + }; + const nationalityAttribute = await consumptionController.attributes.createRepositoryAttribute(nationalityParams); - mockEventBus.expectPublishedEvents( - AttributeCreatedEvent, - AttributeCreatedEvent, - AttributeCreatedEvent, - AttributeCreatedEvent, - AttributeCreatedEvent, - AttributeCreatedEvent - ); - }); + const peer = CoreAddress.from("address"); + const createSharedAttributesParams: ICreateSharedLocalAttributeCopyParams = { + sourceAttributeId: nationalityAttribute.id, + peer: peer, + requestReference: CoreId.from("requestId") + }; + + const sharedNationalityAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy(createSharedAttributesParams); + expect(sharedNationalityAttribute).toBeInstanceOf(LocalAttribute); + expect(sharedNationalityAttribute.shareInfo?.peer).toStrictEqual(peer); + + mockEventBus.expectLastPublishedEvent(SharedAttributeCopyCreatedEvent); + }); - test("should allow to create a share attribute copy", async function () { - const nationalityParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ + test("should allow to create a shared attribute", async function () { + const content = IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: testAccount.identity.address - }) - }; - const nationalityAttribute = await consumptionController.attributes.createLocalAttribute(nationalityParams); - - const peer = CoreAddress.from("address"); - const createSharedAttributesParams: ICreateSharedLocalAttributeCopyParams = { - sourceAttributeId: nationalityAttribute.id, - peer: peer, - requestReference: CoreId.from("requestId") - }; - - const sharedNationalityAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy(createSharedAttributesParams); - expect(sharedNationalityAttribute).toBeInstanceOf(LocalAttribute); - expect(sharedNationalityAttribute.shareInfo?.peer).toStrictEqual(peer); - - mockEventBus.expectLastPublishedEvent(SharedAttributeCopyCreatedEvent); - }); + owner: CoreAddress.from("address") + }); + const createPeerAttributeParams: ICreateSharedLocalAttributeParams = { + content: content, + requestReference: CoreId.from("requestId"), + peer: CoreAddress.from("address") + }; + const peerLocalAttribute = await consumptionController.attributes.createSharedLocalAttribute(createPeerAttributeParams); + expect(peerLocalAttribute.content.toJSON()).toStrictEqual(content.toJSON()); + expect(peerLocalAttribute.content.value).toBeInstanceOf(Nationality); + expect(createPeerAttributeParams.peer.address).toStrictEqual(CoreAddress.from("address").toString()); + expect(createPeerAttributeParams.requestReference.toString()).toStrictEqual(CoreId.from("requestId").toString()); - test("should allow to query public relationship attributes", async function () { - const relationshipAttributeParams: ICreateLocalAttributeParams = { - content: RelationshipAttribute.from({ - key: "customerId", + mockEventBus.expectLastPublishedEvent(AttributeCreatedEvent); + }); + + test("should allow to create an attribute shared by a peer", async function () { + const content = IdentityAttribute.from({ value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer ID" + "@type": "Nationality", + value: "DE" }, - owner: testAccount.identity.address, - confidentiality: RelationshipAttributeConfidentiality.Public - }) - }; - await consumptionController.attributes.createLocalAttribute(relationshipAttributeParams); - - const query: IRelationshipAttributeQuery = { - key: "customerId", - owner: testAccount.identity.address, - attributeCreationHints: { - valueType: "ProprietaryString", - title: "someHintTitle", - confidentiality: "public" as RelationshipAttributeConfidentiality - } - }; + owner: CoreAddress.from("address") + }); + const createSharedAttributeParams: ICreateSharedLocalAttributeParams = { + content: content, + requestReference: CoreId.from("requestId"), + peer: CoreAddress.from("address") + }; + const peerLocalAttribute = await consumptionController.attributes.createSharedLocalAttribute(createSharedAttributeParams); + expect(peerLocalAttribute.content.toJSON()).toStrictEqual(content.toJSON()); + expect(peerLocalAttribute.content.value).toBeInstanceOf(Nationality); + expect(createSharedAttributeParams.peer.address).toStrictEqual(CoreAddress.from("address").toString()); + expect(createSharedAttributeParams.requestReference.toString()).toStrictEqual(CoreId.from("requestId").toString()); - const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); - expect(attribute).toBeDefined(); - expect(attribute!.content).toBeInstanceOf(RelationshipAttribute); - expect((attribute!.content as RelationshipAttribute).key).toBe("customerId"); + mockEventBus.expectLastPublishedEvent(AttributeCreatedEvent); + }); }); - test("should allow to query protected relationship attributes", async function () { - const relationshipAttributeParams: ICreateLocalAttributeParams = { - content: RelationshipAttribute.from({ + describe("query Attributes", function () { + test("should allow to query relationship attributes with empty owner", async function () { + const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "AStringValue", + title: "ATtitle" + }, + owner: testAccount.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + await consumptionController.attributes.createSharedLocalAttribute(relationshipAttributeParams); + + const query: IRelationshipAttributeQuery = { + key: "AKey", + owner: CoreAddress.from(""), + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }; + + const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); + expect(attribute).toBeDefined(); + }); + + test("should allow to query public relationship attributes", async function () { + const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: testAccount.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + await consumptionController.attributes.createSharedLocalAttribute(relationshipAttributeParams); + + const query: IRelationshipAttributeQuery = { key: "customerId", - value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer ID" - }, owner: testAccount.identity.address, - confidentiality: RelationshipAttributeConfidentiality.Protected - }) - }; - await consumptionController.attributes.createLocalAttribute(relationshipAttributeParams); - - const query: IRelationshipAttributeQuery = { - key: "customerId", - owner: testAccount.identity.address, - attributeCreationHints: { - valueType: "ProprietaryString", - title: "someHintTitle", - confidentiality: RelationshipAttributeConfidentiality.Protected - } - }; + attributeCreationHints: { + valueType: "ProprietaryString", + title: "someHintTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }; - const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); - expect(attribute).toBeDefined(); - expect(attribute!.content).toBeInstanceOf(RelationshipAttribute); - expect((attribute!.content as RelationshipAttribute).key).toBe("customerId"); - }); + const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); + expect(attribute).toBeDefined(); + expect(attribute!.content).toBeInstanceOf(RelationshipAttribute); + expect((attribute!.content as RelationshipAttribute).key).toBe("customerId"); + }); - test("should not allow to query private relationship attributes", async function () { - const relationshipAttributeParams: ICreateLocalAttributeParams = { - content: RelationshipAttribute.from({ + test("should allow to query protected relationship attributes", async function () { + const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: testAccount.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Protected + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + await consumptionController.attributes.createSharedLocalAttribute(relationshipAttributeParams); + + const query: IRelationshipAttributeQuery = { key: "customerId", - value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer ID" - }, owner: testAccount.identity.address, - confidentiality: RelationshipAttributeConfidentiality.Private - }) - }; - await consumptionController.attributes.createLocalAttribute(relationshipAttributeParams); - - const query: IRelationshipAttributeQuery = { - key: "customerId", - owner: testAccount.identity.address, - attributeCreationHints: { - valueType: "ProprietaryString", - title: "someHintTitle", - confidentiality: RelationshipAttributeConfidentiality.Private - } - }; + attributeCreationHints: { + valueType: "ProprietaryString", + title: "someHintTitle", + confidentiality: RelationshipAttributeConfidentiality.Protected + } + }; - const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); - expect(attribute).toBeUndefined(); - }); + const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); + expect(attribute).toBeDefined(); + expect(attribute!.content).toBeInstanceOf(RelationshipAttribute); + expect((attribute!.content as RelationshipAttribute).key).toBe("customerId"); + }); - test("should query relationship attributes using the ThirdPartyRelationshipAttributeQuery", async function () { - const relationshipAttribute = await consumptionController.attributes.createPeerLocalAttribute({ - content: RelationshipAttribute.from({ + test("should not allow to query private relationship attributes", async function () { + const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: testAccount.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Private + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + await consumptionController.attributes.createSharedLocalAttribute(relationshipAttributeParams); + + const query: IRelationshipAttributeQuery = { key: "customerId", - value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer ID" - }, owner: testAccount.identity.address, - confidentiality: RelationshipAttributeConfidentiality.Protected - }), - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("requestId") + attributeCreationHints: { + valueType: "ProprietaryString", + title: "someHintTitle", + confidentiality: RelationshipAttributeConfidentiality.Private + } + }; + + const attribute = await consumptionController.attributes.executeRelationshipAttributeQuery(query); + expect(attribute).toBeUndefined(); }); - const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ - key: "customerId", - owner: testAccount.identity.address, - thirdParty: [CoreAddress.from("peerAddress")] + test("should query relationship attributes using the ThirdPartyRelationshipAttributeQuery", async function () { + const relationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: CoreAddress.from("peerAddress"), + confidentiality: RelationshipAttributeConfidentiality.Protected + }), + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("requestId") + }); + + const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ + key: "customerId", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [CoreAddress.from("peerAddress")] + }); + expect(attributes).toHaveLength(1); + expect(attributes[0].id.toString()).toStrictEqual(relationshipAttribute.id.toString()); }); - expect(attributes).toHaveLength(1); - expect(attributes[0].id.toString()).toStrictEqual(relationshipAttribute.id.toString()); - }); - test("should not query relationship attributes with confidentiality set to `Private` using the ThirdPartyRelationshipAttributeQuery", async function () { - await consumptionController.attributes.createPeerLocalAttribute({ - content: RelationshipAttribute.from({ + test("should not query relationship attributes with confidentiality set to `Private` using the ThirdPartyRelationshipAttributeQuery", async function () { + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: CoreAddress.from("peerAddress"), + confidentiality: RelationshipAttributeConfidentiality.Private + }), + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("requestId") + }); + + const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ key: "customerId", - value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer ID" - }, - owner: testAccount.identity.address, - confidentiality: RelationshipAttributeConfidentiality.Private - }), - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("requestId") + owner: ThirdPartyRelationshipAttributeQueryOwner.Empty, + thirdParty: [CoreAddress.from("peerAddress")] + }); + expect(attributes).toHaveLength(0); }); - const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ - key: "customerId", - owner: testAccount.identity.address, - thirdParty: [CoreAddress.from("peerAddress")] + test("should not query relationship attributes with not matching key using the ThirdPartyRelationshipAttributeQuery", async function () { + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: CoreAddress.from("peerAddress"), + confidentiality: RelationshipAttributeConfidentiality.Private + }), + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("requestId") + }); + + const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ + key: "notMatchingKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Empty, + thirdParty: [CoreAddress.from("peerAddress")] + }); + expect(attributes).toHaveLength(0); }); - expect(attributes).toHaveLength(0); - }); - test("should not query relationship attributes with not matching key using the ThirdPartyRelationshipAttributeQuery", async function () { - await consumptionController.attributes.createPeerLocalAttribute({ - content: RelationshipAttribute.from({ - key: "customerId", - value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer ID" - }, - owner: testAccount.identity.address, - confidentiality: RelationshipAttributeConfidentiality.Private - }), - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("requestId") + test("can call executeThirdPartyRelationshipAttributeQuery with ThirdPartyRelationshipAttributeQueryOwner.Recipient", async function () { + const recipient = testAccount.identity.address; + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "AStringValue", + title: "ATitle" + }, + owner: recipient, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("thirdPartyAddress"), + requestReference: CoreId.from("requestId") + }); + + const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [CoreAddress.from("thirdPartyAddress")] + }); + expect(attributes).toHaveLength(1); }); - const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ - key: "notMatchingKey", - owner: testAccount.identity.address, - thirdParty: [CoreAddress.from("peerAddress")] + test("can call executeThirdPartyRelationshipAttributeQuery with ThirdPartyRelationshipAttributeQueryOwner.ThirdParty", async function () { + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "AStringValue", + title: "ATitle" + }, + owner: CoreAddress.from("thirdPartyAddress"), + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("thirdPartyAddress"), + requestReference: CoreId.from("requestId") + }); + + const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [CoreAddress.from("thirdPartyAddress")] + }); + expect(attributes).toHaveLength(1); }); - expect(attributes).toHaveLength(0); - }); - test("should allow to query identity attributes", async function () { - const identityAttributeParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: { - "@type": "Nationality", - value: "DE" - }, - owner: testAccount.identity.address - }) - }; - const identityAttribute = await consumptionController.attributes.createLocalAttribute(identityAttributeParams); + test("can call executeThirdPartyRelationshipAttributeQuery with ThirdPartyRelationshipAttributeQueryOwner.Empty", async function () { + const recipient = testAccount.identity.address; + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "AStringValue", + title: "ATitle" + }, + owner: recipient, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("thirdPartyAddress"), + requestReference: CoreId.from("requestId") + }); - const relationshipAttributeParams: ICreateLocalAttributeParams = { - content: RelationshipAttribute.from({ - key: "customerId", - value: { - "@type": "ProprietaryString", - value: "0815", - title: "Customer Id" - }, - owner: testAccount.identity.address, - confidentiality: "public" as RelationshipAttributeConfidentiality - }) - }; - const relationshipAttribute = await consumptionController.attributes.createLocalAttribute(relationshipAttributeParams); - - const query: IIdentityAttributeQuery = { - valueType: "Nationality" - }; - - const attributes = await consumptionController.attributes.executeIdentityAttributeQuery(query); - const attributesId = attributes.map((v) => v.id.toString()); - expect(attributesId).not.toContain(relationshipAttribute.id.toString()); - expect(attributesId).toContain(identityAttribute.id.toString()); - }); + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "AStringValue", + title: "ATitle" + }, + owner: CoreAddress.from("thirdPartyAddress"), + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("anotherThirdPartyAddress"), + requestReference: CoreId.from("requestId") + }); - test("should successfully execute IQL queries only with repository attributes", async function () { - const identityAttributeParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: { - "@type": "Nationality", - value: "DE" - }, - owner: testAccount.identity.address - }) - }; - const identityAttribute = await consumptionController.attributes.createLocalAttribute(identityAttributeParams); - await consumptionController.attributes.createSharedLocalAttributeCopy({ - peer: CoreAddress.from("a-fake-peer"), - requestReference: CoreId.from("a-fake-reference"), - sourceAttributeId: identityAttribute.id, - attributeId: CoreId.from("fake-attribute-id") + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "AStringValue", + title: "ATitle" + }, + owner: CoreAddress.from("uninvolvedThirdPartyAddress"), + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("thirdPartyAddress"), + requestReference: CoreId.from("requestId") + }); + + const attributes = await consumptionController.attributes.executeThirdPartyRelationshipAttributeQuery({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Empty, + thirdParty: [CoreAddress.from("thirdPartyAddress"), CoreAddress.from("anotherThirdPartyAddress")] + }); + expect(attributes).toHaveLength(2); }); - const iqlQuery: IIQLQuery = { queryString: "Nationality=DE" }; - const matchedAttributes = await consumptionController.attributes.executeIQLQuery(iqlQuery); - expect(matchedAttributes).toHaveLength(1); - const matchedAttributeIds = matchedAttributes.map((v) => v.id.toString()); - expect(matchedAttributeIds).toContain(identityAttribute.id.toString()); - }); + test("should allow to query identity attributes", async function () { + const repositoryAttributeParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "DE" + }, + owner: testAccount.identity.address + }) + }; + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute(repositoryAttributeParams); - test("should only return repository attributes on IdentityAttributeQuery", async function () { - const identityAttributeParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: { - "@type": "DisplayName", - value: "Dis Play" - }, - owner: testAccount.identity.address - }) - }; - const identityAttribute = await consumptionController.attributes.createLocalAttribute(identityAttributeParams); + const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer Id" + }, + owner: testAccount.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + const relationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute(relationshipAttributeParams); - const relationshipAttributeParams: ICreateLocalAttributeParams = { - content: RelationshipAttribute.from({ - key: "displayName", - value: { - "@type": "ProprietaryString", - title: "A Title", - value: "DE" - }, - owner: testAccount.identity.address, - confidentiality: "public" as RelationshipAttributeConfidentiality - }) - }; - const relationshipAttribute = await consumptionController.attributes.createLocalAttribute(relationshipAttributeParams); + const query: IIdentityAttributeQuery = { + valueType: "Nationality" + }; + + const attributes = await consumptionController.attributes.executeIdentityAttributeQuery(query); + const attributesId = attributes.map((v) => v.id.toString()); + expect(attributesId).not.toContain(relationshipAttribute.id.toString()); + expect(attributesId).toContain(repositoryAttribute.id.toString()); + }); + + test("should successfully execute IQL queries only with repository attributes", async function () { + const repositoryAttributeParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "DE" + }, + owner: testAccount.identity.address + }) + }; + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute(repositoryAttributeParams); + await consumptionController.attributes.createSharedLocalAttributeCopy({ + peer: CoreAddress.from("a-fake-peer"), + requestReference: CoreId.from("a-fake-reference"), + sourceAttributeId: repositoryAttribute.id, + attributeId: CoreId.from("fake-attribute-id") + }); + + const iqlQuery: IIQLQuery = { queryString: "Nationality=DE" }; + const matchedAttributes = await consumptionController.attributes.executeIQLQuery(iqlQuery); + expect(matchedAttributes).toHaveLength(1); + const matchedAttributeIds = matchedAttributes.map((v) => v.id.toString()); + expect(matchedAttributeIds).toContain(repositoryAttribute.id.toString()); + }); + + test("should only return repository attributes on IdentityAttributeQuery", async function () { + const repositoryAttributeParams: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "DisplayName", + value: "Dis Play" + }, + owner: testAccount.identity.address + }) + }; + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute(repositoryAttributeParams); - const peerAttributeParams: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: { - "@type": "DisplayName", - value: "DE" - }, - owner: CoreAddress.from("peer") - }) - }; - const peerAttribute = await consumptionController.attributes.createLocalAttribute(peerAttributeParams); - - const sentAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ - peer: CoreAddress.from("peer"), - requestReference: CoreId.from("ref"), - sourceAttributeId: identityAttribute.id - }); + const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { + content: RelationshipAttribute.from({ + key: "displayName", + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "DE" + }, + owner: testAccount.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + const relationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute(relationshipAttributeParams); - const receivedAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ - peer: CoreAddress.from("peer"), - requestReference: CoreId.from("ref2"), - sourceAttributeId: peerAttribute.id, - attributeId: CoreId.from("attr1") - }); + const peerSharedIdentityAttributeParams: ICreateSharedLocalAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "DisplayName", + value: "DE" + }, + owner: CoreAddress.from("peer") + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }; + const peerAttribute = await consumptionController.attributes.createSharedLocalAttribute(peerSharedIdentityAttributeParams); - const query: IIdentityAttributeQuery = { - valueType: "DisplayName" - }; - - const attributes = await consumptionController.attributes.executeIdentityAttributeQuery(query); - const attributesId = attributes.map((v) => v.id.toString()); - expect(attributes).toHaveLength(1); - expect(attributesId).not.toContain(relationshipAttribute.id.toString()); - expect(attributesId).not.toContain(peerAttribute.id.toString()); - expect(attributesId).not.toContain(sentAttribute.id.toString()); - expect(attributesId).not.toContain(receivedAttribute.id.toString()); - expect(attributesId).toContain(identityAttribute.id.toString()); - }); + const query: IIdentityAttributeQuery = { + valueType: "DisplayName" + }; - test("should allow to create an attribute shared by a peer", async function () { - const attribute: ICreateLocalAttributeParams = { - content: IdentityAttribute.from({ - value: { - "@type": "Nationality", - value: "DE" - }, - owner: CoreAddress.from("address") - }) - }; - const localAttribute = await consumptionController.attributes.createLocalAttribute(attribute); - const createPeerAttributeParams: ICreatePeerLocalAttributeParams = { - id: localAttribute.id, - content: attribute.content, - requestReference: CoreId.from("requestId"), - peer: CoreAddress.from("address") - }; - const peerLocalAttribute = await consumptionController.attributes.createPeerLocalAttribute(createPeerAttributeParams); - expect(peerLocalAttribute.content.toJSON()).toStrictEqual(localAttribute.content.toJSON()); - expect(peerLocalAttribute.content.value).toBeInstanceOf(Nationality); - expect(createPeerAttributeParams.id).toStrictEqual(localAttribute.id); - expect(createPeerAttributeParams.peer.address).toStrictEqual(CoreAddress.from("address").toString()); - expect(createPeerAttributeParams.requestReference.toString()).toStrictEqual(CoreId.from("requestId").toString()); - - mockEventBus.expectLastPublishedEvent(AttributeCreatedEvent); + const attributes = await consumptionController.attributes.executeIdentityAttributeQuery(query); + const attributesId = attributes.map((v) => v.id.toString()); + expect(attributes).toHaveLength(1); + expect(attributesId).not.toContain(relationshipAttribute.id.toString()); + expect(attributesId).not.toContain(peerAttribute.id.toString()); + expect(attributesId).toContain(repositoryAttribute.id.toString()); + }); }); - describe("Attribute deletion", function () { + describe("delete Attributes", function () { test("should delete a simple attribute", async function () { - const attributeParams: ICreateLocalAttributeParams = { + const attributeParams: ICreateRepositoryAttributeParams = { content: IdentityAttribute.from({ value: EMailAddress.from({ value: "my@email.address" @@ -542,7 +744,7 @@ describe("AttributesController", function () { owner: consumptionController.accountController.identity.address }) }; - const simpleAttribute = await consumptionController.attributes.createLocalAttribute(attributeParams); + const simpleAttribute = await consumptionController.attributes.createRepositoryAttribute(attributeParams); const createdAttribute = await consumptionController.attributes.getLocalAttribute(simpleAttribute.id); expect(createdAttribute).toBeDefined(); @@ -557,17 +759,17 @@ describe("AttributesController", function () { }); test("should delete a complex attribute", async function () { - const attributeParams: ICreateLocalAttributeParams = { + const attributeParams: ICreateRepositoryAttributeParams = { content: IdentityAttribute.from({ value: BirthDate.from({ day: 24, month: 12, year: 2000 }), - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }; - const complexAttribute = await consumptionController.attributes.createLocalAttribute(attributeParams); + const complexAttribute = await consumptionController.attributes.createRepositoryAttribute(attributeParams); mockEventBus.clearPublishedEvents(); const createdAttribute = await consumptionController.attributes.getLocalAttribute(complexAttribute.id); @@ -598,7 +800,7 @@ describe("AttributesController", function () { let predecessorOwnSharedIdentityAttribute: LocalAttribute; beforeEach(async () => { - predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: EMailAddress.from({ value: "my@email.address" @@ -711,6 +913,42 @@ describe("AttributesController", function () { const updatedPredecessorOwnSharedIdentityAttribute = await consumptionController.attributes.getLocalAttribute(predecessorOwnSharedIdentityAttribute.id); expect(updatedPredecessorOwnSharedIdentityAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); }); + + test("should change default from deleted attribute to newest of the same value type if another exists", async function () { + const otherRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: EMailAddress.from({ + value: "my2@email.address" + }), + owner: consumptionController.accountController.identity.address + }) + }); + + const otherNewerRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: EMailAddress.from({ + value: "my3@email.address" + }), + owner: consumptionController.accountController.identity.address + }) + }); + + expect(successorRepositoryAttribute.isDefault).toBe(true); + expect(otherRepositoryAttribute.isDefault).toBeUndefined(); + expect(otherNewerRepositoryAttribute.isDefault).toBeUndefined(); + + await consumptionController.attributes.executeFullAttributeDeletionProcess(successorRepositoryAttribute); + const updatedOtherRepositoryAttribute = await consumptionController.attributes.getLocalAttribute(otherNewerRepositoryAttribute.id); + expect(updatedOtherRepositoryAttribute!.isDefault).toBe(true); + }); + + test("should not set a default if the deleted default attribute had predecessors but no other candidate exists", async function () { + expect(successorRepositoryAttribute.isDefault).toBe(true); + await consumptionController.attributes.executeFullAttributeDeletionProcess(successorRepositoryAttribute); + + const defaultAttributes = await consumptionController.attributes.getLocalAttributes({ isDefault: "true" }); + expect(defaultAttributes).toHaveLength(0); + }); }); describe("should validate and execute full attribute deletion process for child attribute", function () { @@ -721,7 +959,7 @@ describe("AttributesController", function () { let successorChildAttribute: LocalAttribute; beforeEach(async () => { - predecessorComplexAttribute = await consumptionController.attributes.createLocalAttribute({ + predecessorComplexAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: BirthDate.from({ day: 29, @@ -813,36 +1051,29 @@ describe("AttributesController", function () { }); }); - describe("Attribute successions", function () { + describe("succeed Attributes", function () { describe("Common validator", function () { - afterEach(async function () { - const attributes = await consumptionController.attributes.getLocalAttributes(); - for (const attribute of attributes) { - await consumptionController.attributes.deleteAttributeUnsafe(attribute.id); - } - }); - test("should catch if the successor attribute already exist, if an explicit id is provided", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); - const successorData: IAttributeSuccessorParams = { + const successorData = { id: CoreId.from("successorId"), content: IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("differentAddress") + owner: consumptionController.accountController.identity.address }) }; - await consumptionController.attributes.createLocalAttribute(successorData); + await consumptionController.attributes.createRepositoryAttribute(successorData); const validationResult = await consumptionController.attributes.validateAttributeSuccessionCommon(predecessor.id, successorData); expect(validationResult).errorValidationResult({ @@ -851,24 +1082,24 @@ describe("AttributesController", function () { }); test("should catch if the successor is not linked to predecessor, if succeeds is explicitly set", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }), - succeeds: CoreId.from("differentAddress") + succeeds: CoreId.from("differentAttributeId") }; const validationResult = await consumptionController.attributes.validateAttributeSuccessionCommon(predecessor.id, successorData); @@ -878,24 +1109,24 @@ describe("AttributesController", function () { }); test("should catch if the successor already has a successor itself", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }), - succeededBy: CoreId.from("differentAddress") + succeededBy: CoreId.from("differentAttributeId") }; const validationResult = await consumptionController.attributes.validateAttributeSuccessionCommon(predecessor.id, successorData); @@ -905,22 +1136,22 @@ describe("AttributesController", function () { }); test("should catch if the successor has parent", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }), parentId: CoreId.from("parentId") }; @@ -936,9 +1167,9 @@ describe("AttributesController", function () { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("differentAddress") + owner: consumptionController.accountController.identity.address }) }; @@ -956,16 +1187,16 @@ describe("AttributesController", function () { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }; @@ -983,16 +1214,16 @@ describe("AttributesController", function () { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }; @@ -1003,20 +1234,20 @@ describe("AttributesController", function () { }); test("should catch attempted change of owner", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, owner: CoreAddress.from("differentAddress") }) @@ -1029,13 +1260,13 @@ describe("AttributesController", function () { }); test("should catch attempted change of content type", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { - "@type": "Citizenship", + "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { @@ -1046,7 +1277,7 @@ describe("AttributesController", function () { value: "ADisplayName", title: "Display Name" }, - owner: CoreAddress.from("address"), + owner: consumptionController.accountController.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public }) }; @@ -1058,13 +1289,13 @@ describe("AttributesController", function () { }); test("should catch attempted change of value type", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "BirthName", value: "Müller" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }); const successorData: IAttributeSuccessorParams = { @@ -1073,7 +1304,7 @@ describe("AttributesController", function () { "@type": "BirthCountry", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }; @@ -1090,7 +1321,7 @@ describe("AttributesController", function () { "@type": "Nationality", value: "DE" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }), deletionInfo: LocalAttributeDeletionInfo.from({ deletionStatus: DeletionStatus.ToBeDeleted, @@ -1101,9 +1332,9 @@ describe("AttributesController", function () { content: IdentityAttribute.from({ value: { "@type": "Nationality", - value: "DE" + value: "US" }, - owner: CoreAddress.from("address") + owner: consumptionController.accountController.identity.address }) }; @@ -1143,12 +1374,12 @@ describe("AttributesController", function () { }); describe("Validator for own shared identity attribute successions", function () { - let predecessorRelationshipAttribute: LocalAttribute; - let successorRelationshipAttribute: LocalAttribute; + let predecessorRepositoryAttribute: LocalAttribute; + let successorRepositoryAttribute: LocalAttribute; let predecessorOwnSharedIdentityAttribute: LocalAttribute; let successorOwnSharedIdentityAttributeParams: IAttributeSuccessorParams; beforeEach(async function () { - predecessorRelationshipAttribute = await consumptionController.attributes.createLocalAttribute({ + predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Citizenship", @@ -1158,7 +1389,7 @@ describe("AttributesController", function () { }) }); - ({ successor: successorRelationshipAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(predecessorRelationshipAttribute.id, { + ({ successor: successorRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(predecessorRepositoryAttribute.id, { content: IdentityAttribute.from({ value: { "@type": "Citizenship", @@ -1169,7 +1400,7 @@ describe("AttributesController", function () { })); predecessorOwnSharedIdentityAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: predecessorRelationshipAttribute.id, + sourceAttributeId: predecessorRepositoryAttribute.id, peer: CoreAddress.from("peer"), requestReference: CoreId.from("reqRef") }); @@ -1185,23 +1416,17 @@ describe("AttributesController", function () { shareInfo: { peer: CoreAddress.from("peer"), notificationReference: CoreId.from("notRef"), - sourceAttribute: successorRelationshipAttribute.id + sourceAttribute: successorRepositoryAttribute.id } }; }); - afterEach(async function () { - const attributes = await consumptionController.attributes.getLocalAttributes(); - for (const attribute of attributes) { - await consumptionController.attributes.deleteAttributeUnsafe(attribute.id); - } - }); test("should catch if the source attributes do not succeed one another", async function () { - predecessorRelationshipAttribute.succeededBy = undefined; - await consumptionController.attributes.updateAttributeUnsafe(predecessorRelationshipAttribute); + predecessorRepositoryAttribute.succeededBy = undefined; + await consumptionController.attributes.updateAttributeUnsafe(predecessorRepositoryAttribute); - successorRelationshipAttribute.succeeds = undefined; - await consumptionController.attributes.updateAttributeUnsafe(successorRelationshipAttribute); + successorRepositoryAttribute.succeeds = undefined; + await consumptionController.attributes.updateAttributeUnsafe(successorRepositoryAttribute); const validationResult = await consumptionController.attributes.validateOwnSharedIdentityAttributeSuccession( predecessorOwnSharedIdentityAttribute.id, @@ -1212,7 +1437,7 @@ describe("AttributesController", function () { }); }); - test("should catch if the predecessor is not an own shared identity attribute", async function () { + test("should catch if the predecessor is not an own shared IdentityAttribute", async function () { predecessorOwnSharedIdentityAttribute.shareInfo = undefined; await consumptionController.attributes.updateAttributeUnsafe(predecessorOwnSharedIdentityAttribute); @@ -1225,7 +1450,7 @@ describe("AttributesController", function () { }); }); - test("should catch if the successor is not an own shared identity attribute", async function () { + test("should catch if the successor is not an own shared IdentityAttribute", async function () { successorOwnSharedIdentityAttributeParams.shareInfo = undefined; const validationResult = await consumptionController.attributes.validateOwnSharedIdentityAttributeSuccession( @@ -1263,13 +1488,13 @@ describe("AttributesController", function () { }); test("should catch if the successor source attribute is not a repository attribute", async function () { - successorRelationshipAttribute.shareInfo = LocalAttributeShareInfo.from({ + successorRepositoryAttribute.shareInfo = LocalAttributeShareInfo.from({ peer: CoreAddress.from("peer"), requestReference: CoreId.from("reqRef") }); - await consumptionController.attributes.updateAttributeUnsafe(successorRelationshipAttribute); + await consumptionController.attributes.updateAttributeUnsafe(successorRepositoryAttribute); - successorOwnSharedIdentityAttributeParams.shareInfo!.sourceAttribute = successorRelationshipAttribute.id; + successorOwnSharedIdentityAttributeParams.shareInfo!.sourceAttribute = successorRepositoryAttribute.id; const validationResult = await consumptionController.attributes.validateOwnSharedIdentityAttributeSuccession( predecessorOwnSharedIdentityAttribute.id, @@ -1329,7 +1554,7 @@ describe("AttributesController", function () { }); test("should catch if successor source attribute doesn't exist", async function () { - await consumptionController.attributes.deleteAttributeUnsafe(successorRelationshipAttribute.id); + await consumptionController.attributes.deleteAttributeUnsafe(successorRepositoryAttribute.id); const validationResult = await consumptionController.attributes.validateOwnSharedIdentityAttributeSuccession( predecessorOwnSharedIdentityAttribute.id, @@ -1341,13 +1566,13 @@ describe("AttributesController", function () { }); test("should allow to succeed an own shared identity attribute whose predecessor source attribute was deleted", async function () { - await consumptionController.attributes.deleteAttributeUnsafe(predecessorRelationshipAttribute.id); + await consumptionController.attributes.deleteAttributeUnsafe(predecessorRepositoryAttribute.id); predecessorOwnSharedIdentityAttribute.shareInfo!.sourceAttribute = undefined; await consumptionController.attributes.updateAttributeUnsafe(predecessorOwnSharedIdentityAttribute); - successorRelationshipAttribute.succeeds = undefined; - await consumptionController.attributes.updateAttributeUnsafe(successorRelationshipAttribute); + successorRepositoryAttribute.succeeds = undefined; + await consumptionController.attributes.updateAttributeUnsafe(successorRepositoryAttribute); const validationResult = await consumptionController.attributes.validateOwnSharedIdentityAttributeSuccession( predecessorOwnSharedIdentityAttribute.id, @@ -1358,15 +1583,8 @@ describe("AttributesController", function () { }); describe("Happy paths for attribute successions", function () { - afterEach(async function () { - const attributes = await consumptionController.attributes.getLocalAttributes(); - for (const attribute of attributes) { - await consumptionController.attributes.deleteAttributeUnsafe(attribute.id); - } - }); - test("should succeed a repository attribute", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -1395,8 +1613,35 @@ describe("AttributesController", function () { expect((successor.content.value.toJSON() as any).value).toBe("US"); }); + test("should make successor default succeeding a default repository attribute", async function () { + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "DE" + }, + owner: consumptionController.accountController.identity.address + }) + }); + expect(predecessor.isDefault).toBe(true); + + const successorParams: IAttributeSuccessorParams = { + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "US" + }, + owner: consumptionController.accountController.identity.address + }) + }; + + const { predecessor: updatedPredecessor, successor } = await consumptionController.attributes.succeedRepositoryAttribute(predecessor.id, successorParams); + expect(successor.isDefault).toBe(true); + expect(updatedPredecessor.isDefault).toBeUndefined(); + }); + test("should succeed an own shared identity attribute", async function () { - const predecessorRepo = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepo = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -1447,7 +1692,7 @@ describe("AttributesController", function () { }); test("should succeed an own shared identity attribute skipping one version", async function () { - const predecessorRepo = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepo = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -1528,7 +1773,7 @@ describe("AttributesController", function () { owner: consumptionController.accountController.identity.address }); - repoVersion0 = await consumptionController.attributes.createLocalAttribute({ + repoVersion0 = await consumptionController.attributes.createRepositoryAttribute({ content: identityAttribute }); @@ -1646,7 +1891,7 @@ describe("AttributesController", function () { owner: consumptionController.accountController.identity.address }); - repoVersion0 = await consumptionController.attributes.createLocalAttribute({ + repoVersion0 = await consumptionController.attributes.createRepositoryAttribute({ content: identityAttribute }); @@ -1690,7 +1935,7 @@ describe("AttributesController", function () { }, owner: consumptionController.accountController.identity.address }); - repoVersion0 = await consumptionController.attributes.createLocalAttribute({ + repoVersion0 = await consumptionController.attributes.createRepositoryAttribute({ content: identityAttribute }); @@ -1845,12 +2090,10 @@ describe("AttributesController", function () { owner: CoreAddress.from("peer") }); - const peerSharedVersion0 = await consumptionController.attributes.createLocalAttribute({ + const peerSharedVersion0 = await consumptionController.attributes.createSharedLocalAttribute({ content: identityAttribute, - shareInfo: { - peer: CoreAddress.from("peer"), - requestReference: CoreId.from("reqRef2") - } + peer: CoreAddress.from("peer"), + requestReference: CoreId.from("reqRef2") }); const peerSharedVersion1Params: IAttributeSuccessorParams = { @@ -1893,7 +2136,7 @@ describe("AttributesController", function () { }); test("should succeed a peer shared identity attribute", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createAttributeUnsafe({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -1931,7 +2174,7 @@ describe("AttributesController", function () { }); test("should succeed an own shared relationship attribute", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "customerId", value: { @@ -1942,10 +2185,8 @@ describe("AttributesController", function () { owner: consumptionController.accountController.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public }), - shareInfo: { - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("reqRefA") - } + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRefA") }); const successorParams: IAttributeSuccessorParams = { content: RelationshipAttribute.from({ @@ -1978,7 +2219,7 @@ describe("AttributesController", function () { }); test("should succeed a peer shared relationship attribute", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "customerId", value: { @@ -1989,10 +2230,8 @@ describe("AttributesController", function () { owner: CoreAddress.from("peerAddress"), confidentiality: RelationshipAttributeConfidentiality.Public }), - shareInfo: { - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("reqRefA") - } + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRefA") }); const successorParams: IAttributeSuccessorParams = { content: RelationshipAttribute.from({ @@ -2025,7 +2264,7 @@ describe("AttributesController", function () { }); test("should succeed a third party owned relationship attribute", async function () { - const predecessor = await consumptionController.attributes.createLocalAttribute({ + const predecessor = await consumptionController.attributes.createAttributeUnsafe({ content: RelationshipAttribute.from({ key: "customerId", value: { @@ -2075,9 +2314,76 @@ describe("AttributesController", function () { }); }); - describe("hideTechnical", function () { + describe("change default Attributes", function () { + test("should change default RepositoryAttribute", async function () { + const firstAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "My default given name" + }, + owner: consumptionController.accountController.identity.address + }) + }); + + const secondAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "My other given name" + }, + owner: consumptionController.accountController.identity.address + }) + }); + expect(secondAttribute.isDefault).toBeUndefined(); + + const updatedSecondAttribute = await consumptionController.attributes.setAsDefaultRepositoryAttribute(secondAttribute); + expect(updatedSecondAttribute.isDefault).toBe(true); + + const updatedFirstAttribute = await consumptionController.attributes.getLocalAttribute(firstAttribute.id); + expect(updatedFirstAttribute!.isDefault).toBeUndefined(); + }); + + test("should not change default RepositoryAttribute if candidate is already default", async function () { + const firstAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "My default given name" + }, + owner: consumptionController.accountController.identity.address + }) + }); + expect(firstAttribute.isDefault).toBe(true); + const updatedFirstAttribute = await consumptionController.attributes.setAsDefaultRepositoryAttribute(firstAttribute); + expect(updatedFirstAttribute.isDefault).toBe(true); + }); + + test("should throw an error if the new default Attribute is not a RepositoryAttribute", async function () { + const sharedAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "My shared given name" + }, + owner: consumptionController.accountController.identity.address + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: CoreAddress.from("peer"), + requestReference: CoreId.from("reqRef") + }) + }); + + await TestUtil.expectThrowsAsync( + consumptionController.attributes.setAsDefaultRepositoryAttribute(sharedAttribute), + "error.consumption.attributes.isNotRepositoryAttribute" + ); + }); + }); + + describe("get Attributes", function () { beforeEach(async function () { - await consumptionController.attributes.createLocalAttribute({ + await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "notTechnical", value: { @@ -2088,10 +2394,12 @@ describe("AttributesController", function () { isTechnical: false, owner: testAccount.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public - }) + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") }); - await consumptionController.attributes.createLocalAttribute({ + await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "isTechnicalNotDefined", value: { @@ -2101,10 +2409,12 @@ describe("AttributesController", function () { }, owner: testAccount.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public - }) + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") }); - await consumptionController.attributes.createLocalAttribute({ + await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "technical", value: { @@ -2115,27 +2425,33 @@ describe("AttributesController", function () { isTechnical: true, owner: testAccount.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public - }) + }), + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") }); }); + test("should list all attributes", async function () { + const attributes = await consumptionController.attributes.getLocalAttributes(); + expect(attributes).toHaveLength(3); + }); test("should hide technical attributes when no query is given", async function () { const attributes = await consumptionController.attributes.getLocalAttributes(undefined, true); - expect(attributes).toHaveLength(4); + expect(attributes).toHaveLength(2); }); test("should hide technical attributes when empty query is given", async function () { const attributes = await consumptionController.attributes.getLocalAttributes({}, true); - expect(attributes).toHaveLength(4); + expect(attributes).toHaveLength(2); }); }); - describe("get versions of attribute", function () { - let rAVersion0: LocalAttribute; - let rAVersion1: LocalAttribute; - let rAVersion2: LocalAttribute; + describe("get versions of an Attribute", function () { + let repositoryAttributeVersion0: LocalAttribute; + let repositoryAttributeVersion1: LocalAttribute; + let repositoryAttributeVersion2: LocalAttribute; beforeEach(async function () { - rAVersion0 = await consumptionController.attributes.createLocalAttribute({ + repositoryAttributeVersion0 = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -2163,41 +2479,48 @@ describe("AttributesController", function () { }) }; - ({ predecessor: rAVersion0, successor: rAVersion1 } = await consumptionController.attributes.succeedRepositoryAttribute(rAVersion0.id, successorParams1)); - ({ predecessor: rAVersion1, successor: rAVersion2 } = await consumptionController.attributes.succeedRepositoryAttribute(rAVersion1.id, successorParams2)); + ({ predecessor: repositoryAttributeVersion0, successor: repositoryAttributeVersion1 } = await consumptionController.attributes.succeedRepositoryAttribute( + repositoryAttributeVersion0.id, + successorParams1 + )); + ({ predecessor: repositoryAttributeVersion1, successor: repositoryAttributeVersion2 } = await consumptionController.attributes.succeedRepositoryAttribute( + repositoryAttributeVersion1.id, + successorParams2 + )); }); + test("should return all predecessors of a succeeded repository attribute", async function () { - const result0 = await consumptionController.attributes.getPredecessorsOfAttribute(rAVersion0.id); + const result0 = await consumptionController.attributes.getPredecessorsOfAttribute(repositoryAttributeVersion0.id); expect(result0).toStrictEqual([]); - const result1 = await consumptionController.attributes.getPredecessorsOfAttribute(rAVersion1.id); - expect(result1).toStrictEqual([rAVersion0]); + const result1 = await consumptionController.attributes.getPredecessorsOfAttribute(repositoryAttributeVersion1.id); + expect(JSON.stringify(result1)).toStrictEqual(JSON.stringify([repositoryAttributeVersion0])); - const result2 = await consumptionController.attributes.getPredecessorsOfAttribute(rAVersion2.id); - expect(result2).toStrictEqual([rAVersion1, rAVersion0]); + const result2 = await consumptionController.attributes.getPredecessorsOfAttribute(repositoryAttributeVersion2.id); + expect(JSON.stringify(result2)).toStrictEqual(JSON.stringify([repositoryAttributeVersion1, repositoryAttributeVersion0])); }); test("should return all successors of a succeeded repository attribute", async function () { - const result0 = await consumptionController.attributes.getSuccessorsOfAttribute(rAVersion0.id); - expect(result0).toStrictEqual([rAVersion1, rAVersion2]); + const result0 = await consumptionController.attributes.getSuccessorsOfAttribute(repositoryAttributeVersion0.id); + expect(JSON.stringify(result0)).toStrictEqual(JSON.stringify([repositoryAttributeVersion1, repositoryAttributeVersion2])); - const result1 = await consumptionController.attributes.getSuccessorsOfAttribute(rAVersion1.id); - expect(result1).toStrictEqual([rAVersion2]); + const result1 = await consumptionController.attributes.getSuccessorsOfAttribute(repositoryAttributeVersion1.id); + expect(JSON.stringify(result1)).toStrictEqual(JSON.stringify([repositoryAttributeVersion2])); - const result2 = await consumptionController.attributes.getSuccessorsOfAttribute(rAVersion2.id); + const result2 = await consumptionController.attributes.getSuccessorsOfAttribute(repositoryAttributeVersion2.id); expect(result2).toStrictEqual([]); }); test("should return all versions of a succeeded repository attribute", async function () { - const allVersions = [rAVersion2, rAVersion1, rAVersion0]; + const allVersions = [repositoryAttributeVersion2, repositoryAttributeVersion1, repositoryAttributeVersion0]; for (const version of allVersions) { const result = await consumptionController.attributes.getVersionsOfAttribute(version.id); - expect(result).toStrictEqual([rAVersion2, rAVersion1, rAVersion0]); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify([repositoryAttributeVersion2, repositoryAttributeVersion1, repositoryAttributeVersion0])); } }); test("should return only input attribute if no successions were performed", async function () { - const version0 = await consumptionController.attributes.createLocalAttribute({ + const version0 = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -2212,17 +2535,17 @@ describe("AttributesController", function () { }); test("should return all versions of a possibly succeeded own shared identity attribute", async function () { - const oSIAVersion0 = await consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: rAVersion0.id, + const ownSharedIdentityAttributeVersion0 = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: repositoryAttributeVersion0.id, peer: CoreAddress.from("peerA"), requestReference: CoreId.from("reqRef") }); - const oSIAVersionsBeforeSuccession = await consumptionController.attributes.getVersionsOfAttribute(oSIAVersion0.id); - expect(oSIAVersionsBeforeSuccession).toStrictEqual([oSIAVersion0]); + const ownSharedIdentityAttributeVersionsBeforeSuccession = await consumptionController.attributes.getVersionsOfAttribute(ownSharedIdentityAttributeVersion0.id); + expect(JSON.stringify(ownSharedIdentityAttributeVersionsBeforeSuccession)).toStrictEqual(JSON.stringify([ownSharedIdentityAttributeVersion0])); - const oSIAVersion1 = await consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: rAVersion1.id, + const ownSharedIdentityAttributeVersion1 = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: repositoryAttributeVersion1.id, peer: CoreAddress.from("peerB"), requestReference: CoreId.from("reqRef1") }); @@ -2238,24 +2561,22 @@ describe("AttributesController", function () { shareInfo: { peer: CoreAddress.from("peerB"), requestReference: CoreId.from("reqRef2"), - sourceAttribute: rAVersion2.id + sourceAttribute: repositoryAttributeVersion2.id } }; - const { predecessor: updatedOSIAVersion1, successor: oSIAVersion2 } = await consumptionController.attributes.succeedOwnSharedIdentityAttribute( - oSIAVersion1.id, - successorParams - ); + const { predecessor: updatedOwnSharedIdentityAttributeVersion1, successor: ownSharedIdentityAttributeVersion2 } = + await consumptionController.attributes.succeedOwnSharedIdentityAttribute(ownSharedIdentityAttributeVersion1.id, successorParams); - const allVersions = [oSIAVersion2, updatedOSIAVersion1]; + const allVersions = [ownSharedIdentityAttributeVersion2, updatedOwnSharedIdentityAttributeVersion1]; for (const version of allVersions) { const result = await consumptionController.attributes.getVersionsOfAttribute(version.id); - expect(result).toStrictEqual(allVersions); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(allVersions)); } }); test("should return all versions of a possibly succeeded peer shared identity attribute", async function () { - const version0 = await consumptionController.attributes.createLocalAttribute({ + const version0 = await consumptionController.attributes.createSharedLocalAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -2263,10 +2584,8 @@ describe("AttributesController", function () { }, owner: CoreAddress.from("peer") }), - shareInfo: { - peer: CoreAddress.from("peer"), - requestReference: CoreId.from("reqRefA") - } + peer: CoreAddress.from("peer"), + requestReference: CoreId.from("reqRefA") }); const successorParams1: IAttributeSuccessorParams = { content: IdentityAttribute.from({ @@ -2296,7 +2615,7 @@ describe("AttributesController", function () { }; const onlyVersion0 = await consumptionController.attributes.getVersionsOfAttribute(version0.id); - expect(onlyVersion0).toStrictEqual([version0]); + expect(JSON.stringify(onlyVersion0)).toStrictEqual(JSON.stringify([version0])); const { predecessor: updatedVersion0, successor: version1 } = await consumptionController.attributes.succeedPeerSharedIdentityAttribute(version0.id, successorParams1); const { predecessor: updatedVersion1, successor: version2 } = await consumptionController.attributes.succeedPeerSharedIdentityAttribute(version1.id, successorParams2); @@ -2305,12 +2624,12 @@ describe("AttributesController", function () { for (const version of allVersions) { const result = await consumptionController.attributes.getVersionsOfAttribute(version.id); - expect(result).toStrictEqual(allVersions); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(allVersions)); } }); test("should return all versions of a possibly succeeded own shared relationship attribute", async function () { - const version0 = await consumptionController.attributes.createLocalAttribute({ + const version0 = await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "customerId", value: { @@ -2321,10 +2640,8 @@ describe("AttributesController", function () { owner: consumptionController.accountController.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public }), - shareInfo: { - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("reqRefA") - } + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRefA") }); const successorParams1: IAttributeSuccessorParams = { content: RelationshipAttribute.from({ @@ -2360,7 +2677,7 @@ describe("AttributesController", function () { }; const onlyVersion0 = await consumptionController.attributes.getVersionsOfAttribute(version0.id); - expect(onlyVersion0).toStrictEqual([version0]); + expect(JSON.stringify(onlyVersion0)).toStrictEqual(JSON.stringify([version0])); const { predecessor: updatedVersion0, successor: version1 } = await consumptionController.attributes.succeedOwnSharedRelationshipAttribute( version0.id, @@ -2374,12 +2691,12 @@ describe("AttributesController", function () { const allVersions = [version2, updatedVersion1, updatedVersion0]; for (const version of allVersions) { const result = await consumptionController.attributes.getVersionsOfAttribute(version.id); - expect(result).toStrictEqual(allVersions); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(allVersions)); } }); test("should return all versions of a possibly succeeded peer shared relationship attribute", async function () { - const version0 = await consumptionController.attributes.createLocalAttribute({ + const version0 = await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ key: "customerId", value: { @@ -2390,10 +2707,8 @@ describe("AttributesController", function () { owner: CoreAddress.from("peerAddress"), confidentiality: RelationshipAttributeConfidentiality.Public }), - shareInfo: { - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("reqRefA") - } + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRefA") }); const successorParams1: IAttributeSuccessorParams = { content: RelationshipAttribute.from({ @@ -2429,7 +2744,7 @@ describe("AttributesController", function () { }; const onlyVersion0 = await consumptionController.attributes.getVersionsOfAttribute(version0.id); - expect(onlyVersion0).toStrictEqual([version0]); + expect(JSON.stringify(onlyVersion0)).toStrictEqual(JSON.stringify([version0])); const { predecessor: updatedVersion0, successor: version1 } = await consumptionController.attributes.succeedPeerSharedRelationshipAttribute( version0.id, @@ -2443,7 +2758,7 @@ describe("AttributesController", function () { const allVersions = [version2, updatedVersion1, updatedVersion0]; for (const version of allVersions) { const result = await consumptionController.attributes.getVersionsOfAttribute(version.id); - expect(result).toStrictEqual(allVersions); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(allVersions)); } }); @@ -2452,7 +2767,7 @@ describe("AttributesController", function () { }); test("should check if two attributes are subsequent in succession", async function () { - const version0 = await consumptionController.attributes.createLocalAttribute({ + const version0 = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -2492,7 +2807,7 @@ describe("AttributesController", function () { }); }); - describe("get shared versions of an attribute", function () { + describe("get shared versions of an Attribute", function () { let repositoryAttributeV0: LocalAttribute; let repositoryAttributeV1: LocalAttribute; let repositoryAttributeV2: LocalAttribute; @@ -2500,7 +2815,7 @@ describe("AttributesController", function () { let ownSharedIdentityAttributeV1PeerB: LocalAttribute; let ownSharedIdentityAttributeV2PeerB: LocalAttribute; beforeEach(async function () { - repositoryAttributeV0 = await consumptionController.attributes.createLocalAttribute({ + repositoryAttributeV0 = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: { "@type": "Nationality", @@ -2575,7 +2890,7 @@ describe("AttributesController", function () { test("should return all shared predecessors for a single peer", async function () { const result = await consumptionController.attributes.getSharedPredecessorsOfAttribute(repositoryAttributeV2, { "shareInfo.peer": "peerB" }); - expect(result).toStrictEqual([ownSharedIdentityAttributeV1PeerB]); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify([ownSharedIdentityAttributeV1PeerB])); }); test("should return all shared successors for all peers", async function () { @@ -2585,7 +2900,7 @@ describe("AttributesController", function () { test("should return all shared successors for a single peer", async function () { const result = await consumptionController.attributes.getSharedSuccessorsOfAttribute(repositoryAttributeV0, { "shareInfo.peer": "peerB" }); - expect(result).toStrictEqual([ownSharedIdentityAttributeV1PeerB, ownSharedIdentityAttributeV2PeerB]); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify([ownSharedIdentityAttributeV1PeerB, ownSharedIdentityAttributeV2PeerB])); }); test("should return all shared versions for all peers", async function () { @@ -2609,10 +2924,10 @@ describe("AttributesController", function () { const allOwnSharedAttributeVersionsPeerB = [ownSharedIdentityAttributeV2PeerB, ownSharedIdentityAttributeV1PeerB]; for (const repositoryAttributeVersion of allRepositoryAttributeVersions) { const resultA = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerA")], false); - expect(resultA).toStrictEqual([ownSharedIdentityAttributeV1PeerA]); + expect(JSON.stringify(resultA)).toStrictEqual(JSON.stringify([ownSharedIdentityAttributeV1PeerA])); const resultB = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerB")], false); - expect(resultB).toStrictEqual(allOwnSharedAttributeVersionsPeerB); + expect(JSON.stringify(resultB)).toStrictEqual(JSON.stringify(allOwnSharedAttributeVersionsPeerB)); } }); @@ -2640,7 +2955,7 @@ describe("AttributesController", function () { }); test("should return an empty list if a shared identity attribute is queried", async function () { - const sharedIdentityAttribute = await consumptionController.attributes.createLocalAttribute({ + const sharedIdentityAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: IdentityAttribute.from({ value: { "@type": "DisplayName", @@ -2648,10 +2963,8 @@ describe("AttributesController", function () { }, owner: consumptionController.accountController.identity.address }), - shareInfo: { - peer: CoreAddress.from("peer"), - requestReference: CoreId.from("reqRefX") - } + peer: CoreAddress.from("peer"), + requestReference: CoreId.from("reqRefX") }); const result = await consumptionController.attributes.getSharedVersionsOfAttribute(sharedIdentityAttribute.id); @@ -2659,9 +2972,9 @@ describe("AttributesController", function () { }); test("should return an empty list if a relationship attribute without associated third party relationship attributes is queried", async function () { - const relationshipAttribute = await consumptionController.attributes.createLocalAttribute({ + const relationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ - key: "Some key", + key: "AKey", value: { "@type": "ProprietaryString", value: "Some value", @@ -2670,14 +2983,50 @@ describe("AttributesController", function () { owner: consumptionController.accountController.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public }), - shareInfo: { - peer: CoreAddress.from("peerAddress"), - requestReference: CoreId.from("reqRef123") - } + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRef123") }); const result = await consumptionController.attributes.getSharedVersionsOfAttribute(relationshipAttribute.id); expect(result).toHaveLength(0); }); }); + + test("should delete attributes exchanged with peer", async function () { + const ownRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "Some value", + title: "Some title" + }, + owner: consumptionController.accountController.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRef123") + }); + + const peerRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + value: { + "@type": "ProprietaryString", + value: "Some value", + title: "Some title" + }, + owner: consumptionController.accountController.identity.address, + confidentiality: RelationshipAttributeConfidentiality.Public + }), + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRef123") + }); + + await consumptionController.attributes.deleteAttributesExchangedWithPeer(CoreAddress.from("peerAddress")); + const ownAttribute = await consumptionController.attributes.getLocalAttribute(ownRelationshipAttribute.id); + const peerAttribute = await consumptionController.attributes.getLocalAttribute(peerRelationshipAttribute.id); + expect(ownAttribute).toBeUndefined(); + expect(peerAttribute).toBeUndefined(); + }); }); diff --git a/packages/consumption/test/modules/attributes/LocalAttributeDeletionInfo.test.ts b/packages/consumption/test/modules/attributes/LocalAttributeDeletionInfo.test.ts index caa93d432..1c2a81172 100644 --- a/packages/consumption/test/modules/attributes/LocalAttributeDeletionInfo.test.ts +++ b/packages/consumption/test/modules/attributes/LocalAttributeDeletionInfo.test.ts @@ -33,12 +33,12 @@ describe("LocalAttributeDeletionInfo", function () { }); beforeEach(async function () { - repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ value: EMailAddress.from({ value: "my@email.com" }), - owner: CoreAddress.from(testAccount.identity.address) + owner: testAccount.identity.address }) }); @@ -48,7 +48,7 @@ describe("LocalAttributeDeletionInfo", function () { requestReference: CoreId.from("request") }); - peerSharedIdentityAttribute = await consumptionController.attributes.createPeerLocalAttribute({ + peerSharedIdentityAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: IdentityAttribute.from({ value: EMailAddress.from({ value: "peer@email.com" @@ -59,7 +59,7 @@ describe("LocalAttributeDeletionInfo", function () { requestReference: CoreId.from("request") }); - thirdPartyOwnedRelationshipAttribute = await consumptionController.attributes.createPeerLocalAttribute({ + thirdPartyOwnedRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: RelationshipAttribute.from({ value: ProprietaryEMailAddress.from({ value: "thirdParty@email.com", diff --git a/packages/consumption/test/modules/notifications/NotificationEnd2End.test.ts b/packages/consumption/test/modules/notifications/NotificationEnd2End.test.ts index c8bdd4219..060dbf1ff 100644 --- a/packages/consumption/test/modules/notifications/NotificationEnd2End.test.ts +++ b/packages/consumption/test/modules/notifications/NotificationEnd2End.test.ts @@ -66,7 +66,7 @@ describe("End2End Notification via Messages", function () { TestNotificationItemProcessor.reset(); }); - test("sender: sent Notification", async function () { + test("sender: mark LocalNotification as sent", async function () { const localNotification = await sConsumptionController.notifications.sent(sMessageWithNotification); expect(localNotification.status).toStrictEqual(LocalNotificationStatus.Sent); expect(localNotification.content.items[0]).toBeInstanceOf(TestNotificationItem); @@ -150,4 +150,14 @@ describe("End2End Notification via Messages", function () { await rConsumptionController.notifications.processOpenNotifactionsReceivedByCurrentDevice(); expect(TestNotificationItemProcessor.processedItems).toHaveLength(0); }); + + test("recipient: delete notification while decomposing relationship", async function () { + await TestUtil.terminateRelationship(rAccountController, sAccountController); + await TestUtil.decomposeRelationship(rAccountController, rConsumptionController, sAccountController); + + const notification = await rNotificationsCollection.findOne({ + id: rLocalNotification.id.toString() + }); + expect(notification).toBeFalsy(); + }); }); diff --git a/packages/consumption/test/modules/notifications/NotificationsController.test.ts b/packages/consumption/test/modules/notifications/NotificationsController.test.ts new file mode 100644 index 000000000..c98d1aa23 --- /dev/null +++ b/packages/consumption/test/modules/notifications/NotificationsController.test.ts @@ -0,0 +1,68 @@ +import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { Notification } from "@nmshd/content"; +import { AccountController, CoreAddress, CoreDate, CoreId, SynchronizedCollection, Transport } from "@nmshd/transport"; +import { ConsumptionController, ConsumptionIds, LocalNotification, LocalNotificationSource, LocalNotificationStatus } from "../../../src"; +import { TestUtil } from "../../core/TestUtil"; +import { TestNotificationItem, TestNotificationItemProcessor } from "./testHelpers/TestNotificationItem"; + +describe("End2End Notification via Messages", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let sAccountController: AccountController; + let sConsumptionController: ConsumptionController; + let rAccountController: AccountController; + let rConsumptionController: ConsumptionController; + let localNotification: LocalNotification; + + let rNotificationsCollection: SynchronizedCollection; + + beforeAll(async function () { + connection = await TestUtil.createConnection(); + transport = TestUtil.createTransport(connection); + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + + ({ accountController: sAccountController, consumptionController: sConsumptionController } = accounts[0]); + sConsumptionController.notifications["processorRegistry"].registerProcessor(TestNotificationItem, TestNotificationItemProcessor); + ({ accountController: rAccountController, consumptionController: rConsumptionController } = accounts[1]); + rConsumptionController.notifications["processorRegistry"].registerProcessor(TestNotificationItem, TestNotificationItemProcessor); + rNotificationsCollection = await rConsumptionController.accountController.getSynchronizedCollection("Notifications"); + + await TestUtil.addRelationship(sAccountController, rAccountController); + + const id = await ConsumptionIds.notification.generate(); + localNotification = LocalNotification.from({ + id, + content: Notification.from({ id, items: [TestNotificationItem.from({})] }), + isOwn: true, + peer: CoreAddress.from("anAddress"), + createdAt: CoreDate.utc(), + status: LocalNotificationStatus.Open, + source: LocalNotificationSource.message(CoreId.from("anId")) + }); + + await sAccountController.messages.sendMessage({ + content: localNotification.content, + recipients: [rAccountController.identity.address] + }); + }); + + afterAll(async function () { + await connection.close(); + }); + + beforeEach(function () { + TestNotificationItemProcessor.reset(); + }); + + test("recipient: delete notification while decomposing relationship", async function () { + await rConsumptionController.notifications.deleteNotificationsExchangedWithPeer(sAccountController.identity.address); + + const notification = await rNotificationsCollection.findOne({ + id: localNotification.id.toString() + }); + expect(notification).toBeFalsy(); + }); +}); diff --git a/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/OwnSharedAttributeDeletedByOwnerNotificationItemProcessor.test.ts b/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/OwnSharedAttributeDeletedByOwnerNotificationItemProcessor.test.ts index 3b71dc849..8e2890aba 100644 --- a/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/OwnSharedAttributeDeletedByOwnerNotificationItemProcessor.test.ts +++ b/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/OwnSharedAttributeDeletedByOwnerNotificationItemProcessor.test.ts @@ -299,7 +299,7 @@ describe("OwnSharedAttributeDeletedByPeerNotificationItemProcessor", function () }); test("runs all processor methods for an unknown attribute", async function () { - const unknownAttributeId = CoreId.from("id1xxxxxxxxxxxxxxxxx"); + const unknownAttributeId = CoreId.from("ATT"); const notificationItem = OwnSharedAttributeDeletedByOwnerNotificationItem.from({ attributeId: unknownAttributeId diff --git a/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/PeerSharedAttributeDeletedByPeerNotificationItemProcessor.test.ts b/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/PeerSharedAttributeDeletedByPeerNotificationItemProcessor.test.ts index 868cbf090..33c402d0f 100644 --- a/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/PeerSharedAttributeDeletedByPeerNotificationItemProcessor.test.ts +++ b/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/PeerSharedAttributeDeletedByPeerNotificationItemProcessor.test.ts @@ -249,7 +249,7 @@ describe("PeerSharedAttributeDeletedByPeerNotificationItemProcessor", function ( }); test("runs all processor methods for an unknown attribute", async function () { - const unknownAttributeId = CoreId.from("id1xxxxxxxxxxxxxxxxx"); + const unknownAttributeId = CoreId.from("ATT"); const notificationItem = PeerSharedAttributeDeletedByPeerNotificationItem.from({ attributeId: unknownAttributeId diff --git a/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItemProcessor.test.ts b/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItemProcessor.test.ts index 5c72abbaa..963337ec6 100644 --- a/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItemProcessor.test.ts +++ b/packages/consumption/test/modules/notifications/itemProcessors/attributeDeleted/ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItemProcessor.test.ts @@ -113,7 +113,7 @@ describe("ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItemProce }); test("runs all processor methods for an unknown attribute", async function () { - const unknownAttributeId = CoreId.from("id1xxxxxxxxxxxxxxxxx"); + const unknownAttributeId = CoreId.from("ATT"); const notificationItem = ThirdPartyOwnedRelationshipAttributeDeletedByPeerNotificationItem.from({ attributeId: unknownAttributeId diff --git a/packages/consumption/test/modules/notifications/itemProcessors/attributeSucceeded/PeerSharedAttributeSucceededNotificationItemProcessor.test.ts b/packages/consumption/test/modules/notifications/itemProcessors/attributeSucceeded/PeerSharedAttributeSucceededNotificationItemProcessor.test.ts index 2e9c75b15..15cd9c32e 100644 --- a/packages/consumption/test/modules/notifications/itemProcessors/attributeSucceeded/PeerSharedAttributeSucceededNotificationItemProcessor.test.ts +++ b/packages/consumption/test/modules/notifications/itemProcessors/attributeSucceeded/PeerSharedAttributeSucceededNotificationItemProcessor.test.ts @@ -243,7 +243,7 @@ describe("PeerSharedAttributeSucceededNotificationItemProcessor", function () { const checkResult = await processor.checkPrerequisitesOfIncomingNotificationItem(notificationItem, notification); expect(checkResult).errorValidationResult({ code: "error.consumption.attributes.successionPeerIsNotOwner", - message: "The peer of the succeeded attribute is not its owner. This may be an attempt of spoofing." + message: "The peer of the succeeded Attribute is not its owner. This may be an attempt of spoofing." }); }); }); diff --git a/packages/consumption/test/modules/requests/DecideRequestParamsValidator.test.ts b/packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts similarity index 78% rename from packages/consumption/test/modules/requests/DecideRequestParamsValidator.test.ts rename to packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts index 29dedefab..be794a482 100644 --- a/packages/consumption/test/modules/requests/DecideRequestParamsValidator.test.ts +++ b/packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts @@ -39,7 +39,7 @@ describe("DecideRequestParametersValidator", function () { const successParams: TestParam[] = [ { - description: "(1) success: accept Request with one RequestItem and accept the item", + description: "(1) success: accept Request with one RequestItem and accept the RequestItem", input: { request: TestObjectFactory.createRequestWithOneItem(), response: { @@ -50,7 +50,7 @@ describe("DecideRequestParametersValidator", function () { } }, { - description: "(2) success: accept Request with RequestItemGroup and accept the item", + description: "(2) success: accept Request with RequestItemGroup and accept the contained RequestItem", input: { request: TestObjectFactory.createRequestWithOneItemGroup(), response: { @@ -61,7 +61,7 @@ describe("DecideRequestParametersValidator", function () { } }, { - description: "(3) success: accept Request with one RequestItem and reject the item", + description: "(3) success: accept Request with one RequestItem and reject the RequestItem", input: { request: TestObjectFactory.createRequestWithOneItem(), response: { @@ -72,7 +72,7 @@ describe("DecideRequestParametersValidator", function () { } }, { - description: "(4) success: accept Request with RequestItemGroup and reject the item", + description: "(4) success: accept Request with RequestItemGroup and reject the contained RequestItem", input: { request: TestObjectFactory.createRequestWithOneItemGroup(), response: { @@ -83,29 +83,7 @@ describe("DecideRequestParametersValidator", function () { } }, { - description: "(5) success: group must not be accepted, item must be accepted; reject item", - input: { - request: Request.from({ - items: [ - RequestItemGroup.from({ - mustBeAccepted: false, - items: [TestRequestItem.from({ mustBeAccepted: true })] - }) - ] - }), - response: { - accept: true, - items: [ - { - items: [{ accept: false }] - } - ], - requestId - } - } - }, - { - description: "(6) success: accept a Request without accepting any item (no items mustBeAccepted)", + description: "(5) success: accept a Request without accepting any RequestItem (no RequestItems mustBeAccepted)", input: { request: Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] @@ -118,13 +96,12 @@ describe("DecideRequestParametersValidator", function () { } }, { - description: "(7) success: items that must not be accepted in a group are rejected", + description: "(6) success: RequestItems that must not be accepted in a RequestItemGroup are rejected", input: { request: Request.from({ items: [ RequestItemGroup.from({ - items: [TestRequestItem.from({ mustBeAccepted: false }), TestRequestItem.from({ mustBeAccepted: true })], - mustBeAccepted: false + items: [TestRequestItem.from({ mustBeAccepted: false }), TestRequestItem.from({ mustBeAccepted: true })] }) ] }), @@ -143,7 +120,7 @@ describe("DecideRequestParametersValidator", function () { const errorParams: TestParam[] = [ { - description: "(1) error: Request with two items is answered with one item", + description: "(1) error: Request with two RequestItems is answered with one item", input: { request: TestObjectFactory.createRequestWithTwoItems(), response: { @@ -154,11 +131,11 @@ describe("DecideRequestParametersValidator", function () { }, expectedError: { code: "error.consumption.requests.decide.validation.invalidNumberOfItems", - message: "Number of items in Request and Response do not match" + message: "The number of items in the Request and the Response do not match." } }, { - description: "(2) error: Request with one item is answered with two items", + description: "(2) error: Request with one RequestItem is answered with two items", input: { request: TestObjectFactory.createRequestWithOneItem(), response: { @@ -169,7 +146,7 @@ describe("DecideRequestParametersValidator", function () { }, expectedError: { code: "error.consumption.requests.decide.validation.invalidNumberOfItems", - message: "Number of items in Request and Response do not match" + message: "The number of items in the Request and the Response do not match." } }, { @@ -221,7 +198,7 @@ describe("DecideRequestParametersValidator", function () { expectedError: { indexPath: [0], code: "error.consumption.requests.decide.validation.invalidNumberOfItems", - message: "Number of items in RequestItemGroup and ResponseItemGroup do not match" + message: "The number of items in the RequestItemGroup and the ResponseItemGroup do not match." } }, { @@ -241,19 +218,29 @@ describe("DecideRequestParametersValidator", function () { } }, { - description: "(7) error: item in a RequestItemGroup that must be accepted was rejected", + description: "(7) error: RequestItem contained within a RequestItemGroup that must be accepted was rejected", input: { - request: TestObjectFactory.createRequestWithOneItemGroup(undefined, true), + request: Request.from({ + items: [ + RequestItemGroup.from({ + items: [TestRequestItem.from({ mustBeAccepted: true }), TestRequestItem.from({ mustBeAccepted: true })] + }) + ] + }), response: { accept: true, - items: [{ items: [{ accept: false }] }], + items: [ + { + items: [{ accept: true }, { accept: false }] + } + ], requestId } }, expectedError: { - indexPath: [0], + indexPath: [0, 1], code: "error.consumption.requests.decide.validation.mustBeAcceptedItemNotAccepted", - message: "The RequestItemGroup is flagged as 'mustBeAccepted', but it was not accepted. Please accept all 'mustBeAccepted' items in this group." + message: "The RequestItem is flagged as 'mustBeAccepted', but it was not accepted." } }, { @@ -268,12 +255,12 @@ describe("DecideRequestParametersValidator", function () { }, expectedError: { indexPath: [0], - code: "error.consumption.requests.decide.validation.itemAcceptedButParentNotAccepted", - message: "The RequestItem was accepted, but the parent was not accepted." + code: "error.consumption.requests.decide.validation.itemAcceptedButRequestNotAccepted", + message: "The RequestItem was accepted, but the Request was not accepted." } }, { - description: "(9) error: when the Request is rejected no RequestItemGroup may be accepted", + description: "(9) error: when the Request is rejected no RequestItem contained within a RequestItemGroup may be accepted", input: { request: TestObjectFactory.createRequestWithOneItemGroup(), response: { @@ -284,35 +271,8 @@ describe("DecideRequestParametersValidator", function () { }, expectedError: { indexPath: [0], - code: "error.consumption.requests.decide.validation.itemAcceptedButParentNotAccepted", - message: "The RequestItemGroup was accepted, but the parent was not accepted." - } - }, - { - description: "(10) error: accepting a group but not accepting all 'mustBeAccepted' items in the group", - input: { - request: Request.from({ - items: [ - RequestItemGroup.from({ - items: [TestRequestItem.from({ mustBeAccepted: true }), TestRequestItem.from({ mustBeAccepted: true })], - mustBeAccepted: false - }) - ] - }), - response: { - accept: true, - items: [ - { - items: [{ accept: true }, { accept: false }] - } - ], - requestId - } - }, - expectedError: { - indexPath: [0, 1], - code: "error.consumption.requests.decide.validation.mustBeAcceptedItemNotAccepted", - message: "The RequestItem is flagged as 'mustBeAccepted', but it was not accepted." + code: "error.consumption.validation.inheritedFromItem", + message: "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." } } ]; @@ -323,7 +283,7 @@ describe("DecideRequestParametersValidator", function () { content: data.input.request, createdAt: CoreDate.utc(), isOwn: true, - peer: CoreAddress.from("id1"), + peer: CoreAddress.from("did:e:a-domain:dids:anidentity"), source: { reference: await CoreId.generate(), type: "Message" }, status: LocalRequestStatus.Open, statusLog: [] @@ -355,8 +315,9 @@ describe("DecideRequestParametersValidator", function () { return; } - expect(validationResult.error.code).toBe("inheritedFromItem"); - expect(validationResult.error.message).toBe("Some child items have errors."); + expect(validationResult).errorValidationResult({ + code: "error.consumption.validation.inheritedFromItem" + }); let childResult = validationResult; for (const index of errorIndexPath) childResult = childResult.items[index] as ErrorValidationResult; diff --git a/packages/consumption/test/modules/requests/DeleteRequest.test.ts b/packages/consumption/test/modules/requests/DeleteRequest.test.ts new file mode 100644 index 000000000..846c9956e --- /dev/null +++ b/packages/consumption/test/modules/requests/DeleteRequest.test.ts @@ -0,0 +1,66 @@ +import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { Request } from "@nmshd/content"; +import { AccountController, Message, Transport } from "@nmshd/transport"; +import { ConsumptionController, LocalRequest } from "../../../src"; +import { TestUtil } from "../../core/TestUtil"; +import { TestRequestItem } from "./testHelpers/TestRequestItem"; +import { TestRequestItemProcessor } from "./testHelpers/TestRequestItemProcessor"; + +let connection: IDatabaseConnection; +let transport: Transport; +describe("Delete requests", function () { + let sAccountController: AccountController; + let sConsumptionController: ConsumptionController; + let rAccountController: AccountController; + let rConsumptionController: ConsumptionController; + + let sLocalRequest: LocalRequest; + let rMessageWithRequest: Message; + let rLocalRequest: LocalRequest; + + beforeAll(async function () { + connection = await TestUtil.createConnection(); + transport = TestUtil.createTransport(connection); + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + + ({ accountController: sAccountController, consumptionController: sConsumptionController } = accounts[0]); + sConsumptionController.incomingRequests["processorRegistry"].registerProcessor(TestRequestItem, TestRequestItemProcessor); + ({ accountController: rAccountController, consumptionController: rConsumptionController } = accounts[1]); + rConsumptionController.incomingRequests["processorRegistry"].registerProcessor(TestRequestItem, TestRequestItemProcessor); + + await TestUtil.addRelationship(sAccountController, rAccountController); + sLocalRequest = await sConsumptionController.outgoingRequests.create({ + content: Request.from({ + items: [TestRequestItem.from({ mustBeAccepted: false })] + }), + peer: rAccountController.identity.address + }); + + await sAccountController.messages.sendMessage({ + content: sLocalRequest.content, + recipients: [rAccountController.identity.address] + }); + const messages = await TestUtil.syncUntilHasMessages(rAccountController); + rMessageWithRequest = messages[0]; + rLocalRequest = await rConsumptionController.incomingRequests.received({ + receivedRequest: rMessageWithRequest.cache!.content as Request, + requestSourceObject: rMessageWithRequest + }); + }); + + afterAll(async function () { + await connection.close(); + }); + + test("requests should be deleted after decomposing", async function () { + await TestUtil.terminateRelationship(sAccountController, rAccountController); + await TestUtil.decomposeRelationship(sAccountController, sConsumptionController, rAccountController); + await TestUtil.decomposeRelationship(rAccountController, rConsumptionController, sAccountController); + const sRequest = await sConsumptionController.outgoingRequests.getOutgoingRequest(sLocalRequest.id); + const rRequest = await sConsumptionController.incomingRequests.getIncomingRequest(rLocalRequest.id); + expect(sRequest).toBeUndefined(); + expect(rRequest).toBeUndefined(); + }); +}); diff --git a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts index 5fcb179ba..d1c80c3b2 100644 --- a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts @@ -1,16 +1,15 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { IRequest, IRequestItemGroup, Request, RequestItemGroup, ResponseItem, ResponseItemGroup, ResponseItemResult } from "@nmshd/content"; -import { CoreDate, CoreId, RelationshipChangeType, TransportLoggerFactory } from "@nmshd/transport"; +import { CoreDate, CoreId, TransportLoggerFactory } from "@nmshd/transport"; import { ConsumptionIds, DecideRequestItemGroupParametersJSON, DecideRequestParametersJSON, - ErrorValidationResult, IncomingRequestReceivedEvent, IncomingRequestStatusChangedEvent, LocalRequestStatus } from "../../../src"; -import { loggerFactory, TestUtil } from "../../core/TestUtil"; +import { TestUtil, loggerFactory } from "../../core/TestUtil"; import { RequestsGiven, RequestsTestsContext, RequestsThen, RequestsWhen } from "./RequestsIntegrationTest"; import { TestObjectFactory } from "./testHelpers/TestObjectFactory"; import { ITestRequestItem, TestRequestItem } from "./testHelpers/TestRequestItem"; @@ -133,7 +132,6 @@ describe("IncomingRequestsController", function () { items: [ { "@type": "RequestItemGroup", - mustBeAccepted: false, items: [ { "@type": "TestRequestItem", @@ -261,7 +259,6 @@ describe("IncomingRequestsController", function () { items: [ { "@type": "RequestItemGroup", - mustBeAccepted: false, items: [ { "@type": "TestRequestItem", @@ -318,7 +315,6 @@ describe("IncomingRequestsController", function () { mustBeAccepted: false }), RequestItemGroup.from({ - mustBeAccepted: false, items: [ TestRequestItem.from({ mustBeAccepted: false, @@ -367,22 +363,30 @@ describe("IncomingRequestsController", function () { }); expect(validationResult).errorValidationResult({ - code: "inheritedFromItem", - message: "Some child items have errors." + code: "error.consumption.validation.inheritedFromItem", + message: "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." }); expect(validationResult.items).toHaveLength(2); expect(validationResult.items[0].isError()).toBe(false); expect(validationResult.items[1].isError()).toBe(true); - expect((validationResult.items[1] as ErrorValidationResult).error.code).toBe("inheritedFromItem"); - expect((validationResult.items[1] as ErrorValidationResult).error.message).toBe("Some child items have errors."); + expect(validationResult.items[1]).errorValidationResult({ code: "error.consumption.validation.inheritedFromItem" }); expect(validationResult.items[1].items).toHaveLength(3); expect(validationResult.items[1].items[0].isError()).toBe(true); expect(validationResult.items[1].items[1].isError()).toBe(false); expect(validationResult.items[1].items[2].isError()).toBe(true); }); + + test("returns 'error' on terminated relationship", async function () { + await Given.aTerminatedRelationshipToIdentity(); + await Given.anIncomingRequestInStatus(LocalRequestStatus.DecisionRequired); + const validationResult = await When.iCallCanAccept(); + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.wrongRelationshipStatus" + }); + }); }); describe("CanReject", function () { @@ -443,7 +447,6 @@ describe("IncomingRequestsController", function () { items: [ { "@type": "RequestItemGroup", - mustBeAccepted: false, items: [ { "@type": "TestRequestItem", @@ -498,7 +501,6 @@ describe("IncomingRequestsController", function () { mustBeAccepted: false }), RequestItemGroup.from({ - mustBeAccepted: false, items: [ TestRequestItem.from({ mustBeAccepted: false, @@ -546,22 +548,30 @@ describe("IncomingRequestsController", function () { const validationResult = await When.iCallCanRejectWith(rejectParams); expect(validationResult).errorValidationResult({ - code: "inheritedFromItem", - message: "Some child items have errors." + code: "error.consumption.validation.inheritedFromItem", + message: "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." }); expect(validationResult.items).toHaveLength(2); expect(validationResult.items[0].isError()).toBe(false); expect(validationResult.items[1].isError()).toBe(true); - expect((validationResult.items[1] as ErrorValidationResult).error.code).toBe("inheritedFromItem"); - expect((validationResult.items[1] as ErrorValidationResult).error.message).toBe("Some child items have errors."); + expect(validationResult.items[1]).errorValidationResult({ code: "error.consumption.validation.inheritedFromItem" }); expect(validationResult.items[1].items).toHaveLength(3); expect(validationResult.items[1].items[0].isError()).toBe(true); expect(validationResult.items[1].items[1].isError()).toBe(false); expect(validationResult.items[1].items[2].isError()).toBe(true); }); + + test("returns 'error' on terminated relationship", async function () { + await Given.aTerminatedRelationshipToIdentity(); + await Given.anIncomingRequestInStatus(LocalRequestStatus.DecisionRequired); + const validationResult = await When.iCallCanReject(); + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.wrongRelationshipStatus" + }); + }); }); describe("Accept", function () { @@ -794,14 +804,14 @@ describe("IncomingRequestsController", function () { }); }); - test("can handle valid input with a RelationshipChange as responseSource", async function () { + test("can handle valid input with a Relationship as responseSource", async function () { await Given.anIncomingRequestInStatus(LocalRequestStatus.Decided); - const outgoingRelationshipCreationChange = TestObjectFactory.createOutgoingIRelationshipChange(RelationshipChangeType.Creation, context.currentIdentity); + const outgoingRelationship = TestObjectFactory.createPendingRelationship(); await When.iCompleteTheIncomingRequestWith({ - responseSourceObject: outgoingRelationshipCreationChange + responseSourceObject: outgoingRelationship }); await Then.theRequestMovesToStatus(LocalRequestStatus.Completed); - await Then.theResponseHasItsSourcePropertySetCorrectly({ responseSourceType: "RelationshipChange" }); + await Then.theResponseHasItsSourcePropertySetCorrectly({ responseSourceType: "Relationship" }); await Then.theChangesArePersistedInTheDatabase(); await Then.eventHasBeenPublished(IncomingRequestStatusChangedEvent, { newStatus: LocalRequestStatus.Completed @@ -847,6 +857,7 @@ describe("IncomingRequestsController", function () { }); test("returns undefined when the given id belongs to an outgoing Request", async function () { + await Given.anActiveRelationshipToIdentity(); const outgoingRequest = await Given.anOutgoingRequest(); await When.iGetTheIncomingRequestWith(outgoingRequest.id); await Then.iExpectUndefinedToBeReturned(); @@ -900,6 +911,7 @@ describe("IncomingRequestsController", function () { }); test("does not return outgoing Requests", async function () { + await Given.anActiveRelationshipToIdentity(); await Given.anIncomingRequest(); await Given.anOutgoingRequest(); await When.iGetIncomingRequestsWithTheQuery({}); @@ -981,11 +993,11 @@ describe("IncomingRequestsController", function () { ] }); - const relationshipChange = TestObjectFactory.createOutgoingIRelationshipChange(RelationshipChangeType.Creation, context.currentIdentity); + const relationship = TestObjectFactory.createPendingRelationship(); cnsRequest = await context.incomingRequestsController.complete({ requestId: cnsRequest.id, - responseSourceObject: relationshipChange + responseSourceObject: relationship }); expect(cnsRequest).toBeDefined(); diff --git a/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts b/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts index 4c44cf8b5..8804727c8 100644 --- a/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts @@ -11,7 +11,7 @@ import { ResponseItemResult, ResponseResult } from "@nmshd/content"; -import { CoreAddress, CoreDate, CoreId, RelationshipChangeType, TransportLoggerFactory } from "@nmshd/transport"; +import { CoreAddress, CoreDate, CoreId, TransportLoggerFactory } from "@nmshd/transport"; import { ConsumptionIds, ErrorValidationResult, @@ -23,7 +23,7 @@ import { OutgoingRequestStatusChangedEvent, ValidationResult } from "../../../src"; -import { loggerFactory, TestUtil } from "../../core/TestUtil"; +import { TestUtil, loggerFactory } from "../../core/TestUtil"; import { RequestsGiven, RequestsTestsContext, RequestsThen, RequestsWhen } from "./RequestsIntegrationTest"; import { TestObjectFactory } from "./testHelpers/TestObjectFactory"; import { ITestRequestItem, TestRequestItem } from "./testHelpers/TestRequestItem"; @@ -59,7 +59,11 @@ describe("OutgoingRequestsController", function () { context.reset(); }); - describe("CanCreate", function () { + describe("CanCreate (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("returns 'success' on valid parameters", async function () { await When.iCallCanCreateForAnOutgoingRequest(); await Then.itReturnsASuccessfulValidationResult(); @@ -113,7 +117,6 @@ describe("OutgoingRequestsController", function () { items: [ { "@type": "RequestItemGroup", - mustBeAccepted: false, items: [ { "@type": "TestRequestItem", @@ -147,8 +150,8 @@ describe("OutgoingRequestsController", function () { } }); expect(validationResult).errorValidationResult({ - code: "inheritedFromItem", - message: "Some child items have errors." + code: "error.consumption.validation.inheritedFromItem", + message: "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." }); expect(validationResult.items).toHaveLength(2); @@ -169,7 +172,6 @@ describe("OutgoingRequestsController", function () { mustBeAccepted: false }), RequestItemGroup.from({ - mustBeAccepted: false, items: [ TestRequestItem.from({ mustBeAccepted: false, @@ -181,23 +183,29 @@ describe("OutgoingRequestsController", function () { } }); expect(validationResult).errorValidationResult({ - code: "inheritedFromItem", - message: "Some child items have errors." + code: "error.consumption.validation.inheritedFromItem", + message: "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." }); expect(validationResult.items).toHaveLength(2); expect(validationResult.items[0].isError()).toBe(false); expect(validationResult.items[1].isError()).toBe(true); - expect((validationResult.items[1] as ErrorValidationResult).error.code).toBe("inheritedFromItem"); - expect((validationResult.items[1] as ErrorValidationResult).error.message).toBe("Some child items have errors."); + expect((validationResult.items[1] as ErrorValidationResult).error.code).toBe("error.consumption.validation.inheritedFromItem"); + expect((validationResult.items[1] as ErrorValidationResult).error.message).toBe( + "Some child items have errors. If this error occurred during the specification of a Request, call 'canCreate' to get more information." + ); expect(validationResult.items[1].items).toHaveLength(1); expect(validationResult.items[1].items[0].isError()).toBe(true); }); }); - describe("Create", function () { + describe("Create (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("can handle valid input", async function () { await When.iCreateAnOutgoingRequest(); await Then.theCreatedOutgoingRequestHasAllProperties(); @@ -232,23 +240,23 @@ describe("OutgoingRequestsController", function () { }); describe("CreateFromRelationshipTemplateResponse", function () { - describe("with a RelationshipCreationChange", function () { + describe("with a RelationshipCreation", function () { test("combines calls to create, sent and complete", async function () { - await When.iCreateAnOutgoingRequestFromRelationshipCreationChange(); + await When.iCreateAnOutgoingRequestFromRelationshipCreation(); await Then.theCreatedOutgoingRequestHasAllProperties(); await Then.theRequestIsInStatus(LocalRequestStatus.Completed); await Then.theRequestHasItsSourcePropertySet(); await Then.theRequestHasItsResponsePropertySetCorrectly(ResponseItemResult.Accepted); await Then.theResponseHasItsSourcePropertySetCorrectly({ - responseSourceType: "RelationshipChange" + responseSourceType: "Relationship" }); await Then.theNewRequestIsPersistedInTheDatabase(); await Then.eventsHaveBeenPublished(OutgoingRequestCreatedAndCompletedEvent); }); test("uses the id from the response for the created Local Request", async function () { - await When.iCreateAnOutgoingRequestFromRelationshipCreationChangeWith({ - responseSource: TestObjectFactory.createIncomingIRelationshipChange(RelationshipChangeType.Creation, "requestIdReceivedFromPeer"), + await When.iCreateAnOutgoingRequestFromRelationshipCreationWith({ + responseSource: TestObjectFactory.createPendingRelationship(), response: TestObjectFactory.createResponse("requestIdReceivedFromPeer") }); @@ -258,16 +266,14 @@ describe("OutgoingRequestsController", function () { }); test("create an outgoing request from relationship creation with an active relationship", async function () { - await Given.anActiveRelationshipToIdentity(); - - await When.iCreateAnOutgoingRequestFromRelationshipCreationChangeWith({ + await When.iCreateAnOutgoingRequestFromRelationshipCreationWith({ template: TestObjectFactory.createOutgoingIRelationshipTemplate( context.currentIdentity, RelationshipTemplateContent.from({ onNewRelationship: TestObjectFactory.createRequestWithOneItem() }) ), - responseSource: TestObjectFactory.createIncomingIRelationshipChange(RelationshipChangeType.Creation, "requestIdReceivedFromPeer"), + responseSource: TestObjectFactory.createPendingRelationship(), response: TestObjectFactory.createResponse("requestIdReceivedFromPeer") }); @@ -280,19 +286,23 @@ describe("OutgoingRequestsController", function () { await When.iTryToCreateAnOutgoingRequestFromRelationshipTemplateResponseWithoutResponseSource(); await Then.itThrowsAnErrorWithTheErrorMessage("*responseSource*Value is not defined*"); }); + }); - test("uses the content from onExistingRelationship when the relationship exists", async function () { - await When.iCreateAnOutgoingRequestFromRelationshipCreationChangeWhenRelationshipExistsWith({ - responseSource: TestObjectFactory.createIncomingIMessageWithResponse(CoreAddress.from("id1"), "requestIdReceivedFromPeer"), - response: TestObjectFactory.createResponse("requestIdReceivedFromPeer") - }); - await Then.theRequestHasTheId("requestIdReceivedFromPeer"); - await Then.eventsHaveBeenPublished(OutgoingRequestCreatedAndCompletedEvent); - await Then.theRequestHasCorrectItemCount(2); + test("uses the content from onExistingRelationship when the relationship exists", async function () { + await When.iCreateAnOutgoingRequestFromRelationshipCreationWhenRelationshipExistsWith({ + responseSource: TestObjectFactory.createIncomingIMessageWithResponse(CoreAddress.from("did:e:a-domain:dids:anidentity"), "requestIdReceivedFromPeer"), + response: TestObjectFactory.createResponse("requestIdReceivedFromPeer") }); + await Then.theRequestHasTheId("requestIdReceivedFromPeer"); + await Then.eventsHaveBeenPublished(OutgoingRequestCreatedAndCompletedEvent); + await Then.theRequestHasCorrectItemCount(2); }); - describe("with a Message", function () { + describe("with a Message (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("combines calls to create, sent and complete", async function () { await When.iCreateAnOutgoingRequestFromMessage(); await Then.theCreatedOutgoingRequestHasAllProperties(); @@ -306,7 +316,10 @@ describe("OutgoingRequestsController", function () { test("uses the id from the Message content for the created Local Request", async function () { await When.iCreateAnOutgoingRequestFromMessageWith({ - responseSource: TestObjectFactory.createIncomingIMessageWithResponse(CoreAddress.from("id1"), CoreId.from("requestIdReceivedFromPeer")), + responseSource: TestObjectFactory.createIncomingIMessageWithResponse( + CoreAddress.from("did:e:a-domain:dids:anidentity"), + CoreId.from("requestIdReceivedFromPeer") + ), response: TestObjectFactory.createResponse("requestIdReceivedFromPeer") }); await Then.theRequestHasTheId("requestIdReceivedFromPeer"); @@ -320,7 +333,11 @@ describe("OutgoingRequestsController", function () { }); }); - describe("Sent", function () { + describe("Sent (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("can handle valid input", async function () { await Given.anOutgoingRequestInStatus(LocalRequestStatus.Draft); await When.iCallSent(); @@ -346,7 +363,6 @@ describe("OutgoingRequestsController", function () { test("sets the source property depending on the given source", async function () { const source = TestObjectFactory.createOutgoingIMessage(context.currentIdentity); - await Given.anOutgoingRequestInStatus(LocalRequestStatus.Draft); await When.iCallSentWith({ requestSourceObject: source }); await Then.theRequestHasItsSourcePropertySetTo({ @@ -365,7 +381,6 @@ describe("OutgoingRequestsController", function () { test("throws when passing an incoming Message", async function () { const invalidSource = TestObjectFactory.createIncomingIMessage(context.currentIdentity); - await Given.anOutgoingRequestInStatus(LocalRequestStatus.Draft); await When.iTryToCallSentWith({ requestSourceObject: invalidSource }); await Then.itThrowsAnErrorWithTheErrorMessage("Cannot create outgoing Request from a peer*"); @@ -377,7 +392,11 @@ describe("OutgoingRequestsController", function () { }); }); - describe("Complete", function () { + describe("Complete (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("can handle valid input with a Message as responseSourceObject", async function () { await Given.anOutgoingRequestInStatus(LocalRequestStatus.Open); const incomingMessage = TestObjectFactory.createIncomingIMessage(context.currentIdentity); @@ -454,7 +473,6 @@ describe("OutgoingRequestsController", function () { } as ITestRequestItem, { "@type": "RequestItemGroup", - mustBeAccepted: false, items: [ { "@type": "TestRequestItem", @@ -524,7 +542,6 @@ describe("OutgoingRequestsController", function () { items: [ { "@type": "RequestItemGroup", - mustBeAccepted: false, items: [ { "@type": "TestRequestItem", @@ -578,7 +595,6 @@ describe("OutgoingRequestsController", function () { test("allows completing an expired Request with a Message that was created before the expiry date", async function () { const incomingMessage = TestObjectFactory.createIncomingIMessage(context.currentIdentity, CoreDate.utc().subtract({ days: 2 })); - await Given.anOutgoingRequestWith({ status: LocalRequestStatus.Expired, content: { @@ -593,7 +609,6 @@ describe("OutgoingRequestsController", function () { test("throws when trying to complete an expired Request with a Message that was created after the expiry date", async function () { const incomingMessage = TestObjectFactory.createIncomingIMessage(context.currentIdentity); - await Given.anOutgoingRequestWith({ status: LocalRequestStatus.Expired, content: { @@ -607,7 +622,11 @@ describe("OutgoingRequestsController", function () { }); }); - describe("Get", function () { + describe("Get (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("returns the Request with the given id if it exists", async function () { const outgoingRequest = await Given.anOutgoingRequest(); await When.iGetTheOutgoingRequest(); @@ -666,7 +685,11 @@ describe("OutgoingRequestsController", function () { }); }); - describe("GetOutgoingRequests", function () { + describe("GetOutgoingRequests (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("returns all outgoing Requests when invoked with no query", async function () { await Given.anOutgoingRequest(); await Given.anOutgoingRequest(); @@ -728,7 +751,11 @@ describe("OutgoingRequestsController", function () { }); }); - describe("Discard", function () { + describe("Discard (on active relationship)", function () { + beforeEach(async function () { + await Given.anActiveRelationshipToIdentity(); + }); + test("discards a Request in status Draft", async function () { await Given.anOutgoingRequestInStatus(LocalRequestStatus.Draft); await When.iDiscardTheOutgoingRequest(); @@ -747,4 +774,23 @@ describe("OutgoingRequestsController", function () { await Then.itThrowsAnErrorWithTheErrorMessage("*Local Request has to be in status 'Draft'*"); }); }); + + describe("CanCreate (on terminated relationship)", function () { + test("returns 'error' when the relationship is terminated", async function () { + await Given.aTerminatedRelationshipToIdentity(); + const validationResult = await When.iCallCanCreateForAnOutgoingRequest({ + content: { + items: [ + TestRequestItem.from({ + mustBeAccepted: false, + shouldFailAtCanCreateOutgoingRequestItem: true + }) + ] + } + }); + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.wrongRelationshipStatus" + }); + }); + }); }); diff --git a/packages/consumption/test/modules/requests/RequestEnd2End.test.ts b/packages/consumption/test/modules/requests/RequestEnd2End.test.ts index 5c178ba52..1f2b3ca72 100644 --- a/packages/consumption/test/modules/requests/RequestEnd2End.test.ts +++ b/packages/consumption/test/modules/requests/RequestEnd2End.test.ts @@ -1,6 +1,6 @@ /* eslint-disable jest/expect-expect */ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; -import { AcceptResponseItem, RelationshipCreationChangeRequestContent, RelationshipTemplateContent, Request, Response, ResponseWrapper } from "@nmshd/content"; +import { AcceptResponseItem, RelationshipCreationContent, RelationshipTemplateContent, Request, Response, ResponseWrapper } from "@nmshd/content"; import { AccountController, CoreDate, Message, Relationship, RelationshipTemplate, Transport } from "@nmshd/transport"; import { ConsumptionController, LocalRequest, LocalRequestStatus } from "../../../src"; import { TestUtil } from "../../core/TestUtil"; @@ -10,7 +10,7 @@ import { TestRequestItemProcessor } from "./testHelpers/TestRequestItemProcessor let connection: IDatabaseConnection; let transport: Transport; -describe("End2End Request/Response via Relationship Template/ChangeRequest", function () { +describe("End2End Request/Response via Relationship Template", function () { let sAccountController: AccountController; let sConsumptionController: ConsumptionController; let rAccountController: AccountController; @@ -23,6 +23,9 @@ describe("End2End Request/Response via Relationship Template/ChangeRequest", fun let sRelationship: Relationship; let sLocalRequest: LocalRequest; + let sCreationContent: RelationshipCreationContent; + let rCreationContent: RelationshipCreationContent; + beforeAll(async function () { connection = await TestUtil.createConnection(); transport = TestUtil.createTransport(connection); @@ -85,19 +88,18 @@ describe("End2End Request/Response via Relationship Template/ChangeRequest", fun }); }); - test("recipient: create Relationship with Response in Relationship Change", async function () { + test("recipient: create Relationship with Response in Relationship Creation Content", async function () { rRelationship = await rAccountController.relationships.sendRelationship({ template: rTemplate, - content: RelationshipCreationChangeRequestContent.from({ - response: rLocalRequest.response!.content - }) + creationContent: RelationshipCreationContent.from({ response: rLocalRequest.response!.content }) }); + rCreationContent = rRelationship.cache?.creationContent as RelationshipCreationContent; }); test("recipient: complete Local Request", async function () { rLocalRequest = await rConsumptionController.incomingRequests.complete({ requestId: rLocalRequest.id, - responseSourceObject: rRelationship.cache!.changes[0] + responseSourceObject: rRelationship }); }); @@ -107,11 +109,12 @@ describe("End2End Request/Response via Relationship Template/ChangeRequest", fun }); test("sender: create Local Request and Response from Relationship Change", async function () { - const response = (sRelationship.cache!.changes[0].request.content as RelationshipCreationChangeRequestContent).response; + sCreationContent = sRelationship.cache!.creationContent as RelationshipCreationContent; + const response = sCreationContent.response; sLocalRequest = await sConsumptionController.outgoingRequests.createAndCompleteFromRelationshipTemplateResponse({ template: sTemplate, - responseSource: sRelationship.cache!.changes[0], + responseSource: sRelationship, response }); }); @@ -134,8 +137,8 @@ describe("End2End Request/Response via Relationship Template/ChangeRequest", fun expect(sLocalRequest.content.items[0]).toBeInstanceOf(TestRequestItem); expect(rLocalRequest.content.items[0]).toBeInstanceOf(TestRequestItem); - expect((sRelationship.cache!.changes[0].request.content as RelationshipCreationChangeRequestContent).response).toBeInstanceOf(Response); - expect((rRelationship.cache!.changes[0].request.content as RelationshipCreationChangeRequestContent).response).toBeInstanceOf(Response); + expect(sCreationContent.response).toBeInstanceOf(Response); + expect(rCreationContent.response).toBeInstanceOf(Response); expect(sLocalRequest.response!.content.items[0]).toBeInstanceOf(AcceptResponseItem); expect(rLocalRequest.response!.content.items[0]).toBeInstanceOf(AcceptResponseItem); diff --git a/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts b/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts index 87f91892f..80e208251 100644 --- a/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts +++ b/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts @@ -4,9 +4,9 @@ import { DataEvent, EventEmitter2EventBus } from "@js-soft/ts-utils"; import { AcceptResponseItem, DeleteAttributeRequestItem, - IdentityAttribute, IRequest, IResponse, + IdentityAttribute, RelationshipTemplateContent, Request, RequestItemGroup, @@ -23,7 +23,6 @@ import { IRelationshipTemplate, Message, Relationship, - RelationshipChangeType, RelationshipTemplate, SynchronizedCollection, Transport @@ -40,10 +39,10 @@ import { ICreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters, ICreateOutgoingRequestParameters, ILocalRequestSource, - IncomingRequestsController, IReceivedIncomingRequestParameters, IRequireManualDecisionOfIncomingRequestParameters, ISentOutgoingRequestParameters, + IncomingRequestsController, LocalRequest, LocalRequestSource, LocalRequestStatus, @@ -68,7 +67,7 @@ export class RequestsTestsContext { public outgoingRequestsController: OutgoingRequestsController; public currentIdentity: CoreAddress; public mockEventBus = new MockEventBus(); - public relationshipToReturnFromGetActiveRelationshipToIdentity: Relationship | undefined; + public relationshipToReturnFromGetRelationshipToIdentity: Relationship | undefined; public consumptionController: ConsumptionController; private constructor() { @@ -109,13 +108,23 @@ export class RequestsTestsContext { context.mockEventBus, { address: account.accountController.identity.address }, { - getActiveRelationshipToIdentity: () => Promise.resolve(context.relationshipToReturnFromGetActiveRelationshipToIdentity) + getRelationshipToIdentity: () => Promise.resolve(context.relationshipToReturnFromGetRelationshipToIdentity) + } + ); + + context.incomingRequestsController = new IncomingRequestsController( + collection, + processorRegistry, + context.consumptionController, + context.mockEventBus, + { + address: account.accountController.identity.address + }, + { + getRelationshipToIdentity: () => Promise.resolve(context.relationshipToReturnFromGetRelationshipToIdentity) } ); - context.incomingRequestsController = new IncomingRequestsController(collection, processorRegistry, context.consumptionController, context.mockEventBus, { - address: account.accountController.identity.address - }); context.requestsCollection = context.incomingRequestsController["localRequests"]; const originalCanCreate = context.outgoingRequestsController.canCreate; @@ -133,7 +142,7 @@ export class RequestsTestsContext { this.localRequestAfterAction = undefined; this.validationResult = undefined; this.actionToTry = undefined; - this.relationshipToReturnFromGetActiveRelationshipToIdentity = undefined; + this.relationshipToReturnFromGetRelationshipToIdentity = undefined; TestRequestItemProcessor.numberOfApplyIncomingResponseItemCalls = 0; @@ -157,7 +166,19 @@ export class RequestsGiven { } public anActiveRelationshipToIdentity(): Promise { - this.context.relationshipToReturnFromGetActiveRelationshipToIdentity = TestObjectFactory.createRelationship(); + this.context.relationshipToReturnFromGetRelationshipToIdentity = TestObjectFactory.createActiveRelationship(); + + return Promise.resolve(); + } + + public aPendingRelationshipToIdentity(): Promise { + this.context.relationshipToReturnFromGetRelationshipToIdentity = TestObjectFactory.createPendingRelationship(); + + return Promise.resolve(); + } + + public aTerminatedRelationshipToIdentity(): Promise { + this.context.relationshipToReturnFromGetRelationshipToIdentity = TestObjectFactory.createTerminatedRelationship(); return Promise.resolve(); } @@ -172,7 +193,6 @@ export class RequestsGiven { } }), RequestItemGroup.from({ - mustBeAccepted: false, metadata: { groupMetaKey: "groupMetaValue" }, @@ -282,7 +302,7 @@ export class RequestsGiven { this.context.givenLocalRequest = await this.context.outgoingRequestsController.create({ content: params.content, - peer: CoreAddress.from("id1") + peer: CoreAddress.from("did:e:a-domain:dids:anidentity") }); await this.moveOutgoingRequestToStatus(this.context.givenLocalRequest, params.status); @@ -303,8 +323,8 @@ export class RequestsGiven { } export class RequestsWhen { - public async iCallCanAccept(): Promise { - await this.iCallCanAcceptWith({}); + public async iCallCanAccept(): Promise { + return await this.iCallCanAcceptWith({}); } public async iTryToCallCanAccept(): Promise { @@ -338,8 +358,8 @@ export class RequestsWhen { return this.context.validationResult; } - public async iCallCanReject(): Promise { - await this.iCallCanRejectWith({}); + public async iCallCanReject(): Promise { + return await this.iCallCanRejectWith({}); } public async iTryToCallCanReject(): Promise { @@ -575,7 +595,7 @@ export class RequestsWhen { }) ] }, - peer: params?.peer ?? CoreAddress.from("id1") + peer: params?.peer ?? CoreAddress.from("did:e:a-domain:dids:anidentity") }; this.context.validationResult = await this.context.outgoingRequestsController.canCreate(realParams); @@ -594,26 +614,24 @@ export class RequestsWhen { }) ] }, - peer: CoreAddress.from("id1") + peer: CoreAddress.from("did:e:a-domain:dids:anidentity") }; this.context.localRequestAfterAction = await this.context.outgoingRequestsController.create(params); } - public async iCreateAnOutgoingRequestFromRelationshipCreationChange(): Promise { - await this.iCreateAnOutgoingRequestFromRelationshipCreationChangeWith({}); + public async iCreateAnOutgoingRequestFromRelationshipCreation(): Promise { + await this.iCreateAnOutgoingRequestFromRelationshipCreationWith({}); } - public async iCreateAnOutgoingRequestFromRelationshipCreationChangeWhenRelationshipExistsWith( + public async iCreateAnOutgoingRequestFromRelationshipCreationWhenRelationshipExistsWith( params: Partial ): Promise { - this.context.relationshipToReturnFromGetActiveRelationshipToIdentity = TestObjectFactory.createRelationship(); - await this.iCreateAnOutgoingRequestFromRelationshipCreationChangeWith(params); + this.context.relationshipToReturnFromGetRelationshipToIdentity = TestObjectFactory.createActiveRelationship(); + await this.iCreateAnOutgoingRequestFromRelationshipCreationWith(params); } - public async iCreateAnOutgoingRequestFromRelationshipCreationChangeWith( - params: Partial - ): Promise { + public async iCreateAnOutgoingRequestFromRelationshipCreationWith(params: Partial): Promise { params.template ??= TestObjectFactory.createOutgoingIRelationshipTemplate( this.context.currentIdentity, RelationshipTemplateContent.from({ @@ -621,9 +639,11 @@ export class RequestsWhen { onExistingRelationship: TestObjectFactory.createRequestWithTwoItems() }) ); - params.responseSource ??= TestObjectFactory.createIncomingIRelationshipChange(RelationshipChangeType.Creation); + const relationship = TestObjectFactory.createPendingRelationship(); + params.responseSource ??= relationship; params.response ??= TestObjectFactory.createResponse(); + this.context.relationshipToReturnFromGetRelationshipToIdentity = relationship; this.context.localRequestAfterAction = await this.context.outgoingRequestsController.createAndCompleteFromRelationshipTemplateResponse( params as ICreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters ); @@ -641,7 +661,7 @@ export class RequestsWhen { onExistingRelationship: TestObjectFactory.createRequestWithOneItem() }) ); - params.responseSource ??= TestObjectFactory.createIncomingIMessageWithResponse(CoreAddress.from("id1"), CoreId.from("REQ1")); + params.responseSource ??= TestObjectFactory.createIncomingIMessageWithResponse(CoreAddress.from("did:e:a-domain:dids:anidentity"), CoreId.from("REQ1")); params.response ??= TestObjectFactory.createResponse(); this.context.localRequestAfterAction = await this.context.outgoingRequestsController.createAndCompleteFromRelationshipTemplateResponse( @@ -700,7 +720,6 @@ export class RequestsWhen { content: { items: [ RequestItemGroup.from({ - mustBeAccepted: false, items: [ DeleteAttributeRequestItem.from({ attributeId: sOwnSharedIdentityAttribute1.id.toString(), @@ -900,7 +919,7 @@ export class RequestsWhen { }) ] }, - peer: CoreAddress.from("id1") + peer: CoreAddress.from("did:e:a-domain:dids:anidentity") }; this.context.actionToTry = async () => { @@ -912,7 +931,7 @@ export class RequestsWhen { public iTryToCreateAnOutgoingRequestWithoutContent(): Promise { const paramsWithoutItems: Omit = { - peer: CoreAddress.from("id1") + peer: CoreAddress.from("did:e:a-domain:dids:anidentity") }; this.context.actionToTry = async () => { diff --git a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts index be56ca69d..b4e00b8dd 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts @@ -45,7 +45,7 @@ describe("CreateAttributeRequestItemProcessor", function () { await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSender }); await Then.theResultShouldBeAnErrorWith({ - message: /Cannot create own Attributes with a CreateAttributeRequestItem. Use a ShareAttributeRequestItem instead./ + message: "Cannot create own IdentityAttributes with a CreateAttributeRequestItem. Use a ShareAttributeRequestItem instead." }); }); @@ -65,65 +65,71 @@ describe("CreateAttributeRequestItemProcessor", function () { await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSomeoneElse }); await Then.theResultShouldBeAnErrorWith({ - message: /The owner of the given `attribute` can only be the recipient's address or an empty string. The latter will default to the recipient's address./ + message: + "The owner of the provided IdentityAttribute for the `attribute` property can only be the Recipient's Address or an empty string. The latter will default to the Recipient's Address." }); }); - test("returns Success when passing an Identity Attribute with owner={{SomeoneElse}}, but no recipient", async function () { + test("returns Success when passing an Identity Attribute with owner={{SomeoneElse}}, but no Recipient", async function () { const identityAttributeOfSomeoneElse = TestObjectFactory.createIdentityAttribute({ owner: TestIdentity.SOMEONE_ELSE }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSomeoneElse }, TestIdentity.UNDEFINED); - await Then.theResultShouldBeASuccess(); + await Then.theResultShouldBeAnErrorWith({ + message: "The owner of the provided IdentityAttribute for the `attribute` property can only be an empty string. It will default to the Recipient's Address." + }); }); test("returns Success when passing a Relationship Attribute with owner={{Recipient}}", async function () { - const identityAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({ + const relationshipAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({ owner: TestIdentity.RECIPIENT }); - await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfRecipient }); + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfRecipient }); await Then.theResultShouldBeASuccess(); }); test("returns Success when passing a Relationship Attribute with owner={{Sender}}", async function () { - const identityAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ owner: TestIdentity.SENDER }); - await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSender }); + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSender }); await Then.theResultShouldBeASuccess(); }); test("returns Success when passing a Relationship Attribute with owner={{Empty}}", async function () { - const identityAttributeWithEmptyOwner = TestObjectFactory.createRelationshipAttribute({ + const relationshipAttributeWithEmptyOwner = TestObjectFactory.createRelationshipAttribute({ owner: TestIdentity.EMPTY }); - await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeWithEmptyOwner }); + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeWithEmptyOwner }); await Then.theResultShouldBeASuccess(); }); test("returns an Error when passing a Relationship Attribute with owner={{SomeoneElse}}", async function () { - const identityAttributeOfSomeoneElse = TestObjectFactory.createRelationshipAttribute({ + const relationshipAttributeOfSomeoneElse = TestObjectFactory.createRelationshipAttribute({ owner: TestIdentity.SOMEONE_ELSE }); - await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSomeoneElse }); + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSomeoneElse }); await Then.theResultShouldBeAnErrorWith({ message: - /The owner of the given 'attribute' can only be the sender's address, the recipient's address or an empty string. The latter will default to the recipient's address./ + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the Sender's Address, the Recipient's Address or an empty string. The latter will default to the Recipient's Address." }); }); - test("returns Success when passing a Relationship Attribute with owner={{SomeoneElse}}, but no recipient", async function () { - const identityAttributeOfSomeoneElse = TestObjectFactory.createRelationshipAttribute({ + test("returns Success when passing a Relationship Attribute with owner={{SomeoneElse}}, but no Recipient", async function () { + const relationshipAttributeOfSomeoneElse = TestObjectFactory.createRelationshipAttribute({ owner: TestIdentity.SOMEONE_ELSE }); - await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSomeoneElse }, TestIdentity.UNDEFINED); - await Then.theResultShouldBeASuccess(); + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSomeoneElse }, TestIdentity.UNDEFINED); + await Then.theResultShouldBeAnErrorWith({ + message: + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the Sender's Address or an empty string. The latter will default to the Recipient's Address." + }); }); }); diff --git a/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts index 7efba679e..72c279893 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts @@ -8,6 +8,7 @@ import { ProposeAttributeAcceptResponseItem, ProposeAttributeRequestItem, ProprietaryString, + RelationshipAttribute, RelationshipAttributeConfidentiality, RelationshipAttributeQuery, Request, @@ -57,7 +58,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { describe("canCreateOutgoingRequestItem", function () { test("returns success when proposing an Identity Attribute", function () { - const recipientAddress = CoreAddress.from("recipientAddress"); + const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: false, @@ -70,57 +71,57 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipientAddress); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); test("returns success when proposing a Relationship Attribute", function () { - const recipientAddress = CoreAddress.from("recipientAddress"); + const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: false, attribute: TestObjectFactory.createRelationshipAttribute({ - value: ProprietaryString.from({ title: "A Title", value: "AGivenName" }), + value: ProprietaryString.from({ title: "ATitle", value: "AGivenName" }), owner: CoreAddress.from("") }), query: RelationshipAttributeQuery.from({ - key: "aKey", + key: "AKey", owner: CoreAddress.from(""), attributeCreationHints: { valueType: "ProprietaryString", - title: "aTitle", + title: "ATitle", confidentiality: RelationshipAttributeConfidentiality.Public } }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipientAddress); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); test("returns an error when passing anything other than an empty string as an owner into 'attribute'", function () { - const recipientAddress = CoreAddress.from("recipientAddress"); + const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: false, attribute: TestObjectFactory.createRelationshipAttribute({ - value: ProprietaryString.from({ title: "A Title", value: "AGivenName" }), - owner: recipientAddress + value: ProprietaryString.from({ title: "ATitle", value: "AGivenName" }), + owner: recipient }), query: RelationshipAttributeQuery.from({ - key: "aKey", + key: "AKey", owner: CoreAddress.from(""), attributeCreationHints: { valueType: "ProprietaryString", - title: "aTitle", + title: "ATitle", confidentiality: RelationshipAttributeConfidentiality.Public } }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipientAddress); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidRequestItem" @@ -128,26 +129,26 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); test("returns an error when passing anything other than an empty string as an owner into 'query'", function () { - const recipientAddress = CoreAddress.from("recipientAddress"); + const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: false, attribute: TestObjectFactory.createRelationshipAttribute({ - value: ProprietaryString.from({ title: "A Title", value: "AGivenName" }), + value: ProprietaryString.from({ title: "ATitle", value: "AGivenName" }), owner: CoreAddress.from("") }), query: RelationshipAttributeQuery.from({ - key: "aKey", - owner: recipientAddress, + key: "AKey", + owner: recipient, attributeCreationHints: { valueType: "ProprietaryString", - title: "aTitle", + title: "ATitle", confidentiality: RelationshipAttributeConfidentiality.Public } }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipientAddress); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidRequestItem" @@ -157,6 +158,8 @@ describe("ProposeAttributeRequestItemProcessor", function () { describe("query", function () { describe("IdentityAttributeQuery", function () { test("simple query", function () { + const recipient = CoreAddress.from("Recipient"); + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: false, attribute: TestObjectFactory.createIdentityAttribute({ @@ -167,7 +170,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipientAddress")); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); @@ -175,9 +178,11 @@ describe("ProposeAttributeRequestItemProcessor", function () { describe("RelationshipAttributeQuery", function () { test("simple query", function () { + const recipient = CoreAddress.from("Recipient"); + const query = RelationshipAttributeQuery.from({ owner: "", - key: "aKey", + key: "AKey", attributeCreationHints: { valueType: "ProprietaryString", title: "ATitle", @@ -189,12 +194,12 @@ describe("ProposeAttributeRequestItemProcessor", function () { mustBeAccepted: false, query: query, attribute: TestObjectFactory.createRelationshipAttribute({ - value: ProprietaryString.fromAny({ title: "aTitle", value: "AGivenName" }), + value: ProprietaryString.fromAny({ title: "ATitle", value: "AGivenName" }), owner: CoreAddress.from("") }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipientAddress")); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); @@ -204,12 +209,12 @@ describe("ProposeAttributeRequestItemProcessor", function () { describe("canAccept", function () { test("returns success when called with the id of an existing own LocalAttribute", async function () { - const senderAddress = CoreAddress.from("recipientAddress"); - const recipientAddress = accountController.identity.address; + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; - const existingLocalAttribute = await consumptionController.attributes.createLocalAttribute({ + const existingLocalAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: recipient }) }); @@ -217,7 +222,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { mustBeAccepted: false, attribute: TestObjectFactory.createIdentityAttribute({ value: GivenName.fromAny({ value: "AGivenName" }), - owner: recipientAddress + owner: CoreAddress.from("") }), query: IdentityAttributeQuery.from({ valueType: "GivenName" @@ -228,7 +233,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: senderAddress, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -248,6 +253,9 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); test("returns success when called with a new own Attribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -258,7 +266,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -271,7 +279,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { accept: true, attribute: { "@type": "IdentityAttribute", - owner: accountController.identity.address.toString(), + owner: recipient.toString(), value: { "@type": "GivenName", value: "AGivenName" @@ -284,7 +292,9 @@ describe("ProposeAttributeRequestItemProcessor", function () { expect(result).successfulValidationResult(); }); - test("returns success when called with a the proposed Attribute", async function () { + test("returns success when called with the proposed Attribute", async function () { + const sender = CoreAddress.from("Sender"); + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -299,7 +309,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -319,6 +329,8 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); test("returns an error when the given Attribute id does not exist", async function () { + const sender = CoreAddress.from("Sender"); + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -329,7 +341,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -350,31 +362,31 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); }); - test("returns an error when the given Attribute id belongs to a peer Attribute", async function () { - const someOtherIdentity = CoreAddress.from("id1"); - - const idOfAttributeOfOtherIdentity = await ConsumptionIds.attribute.generate(); + test("returns an error when the existing IdentityAttribute is already shared", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; - await consumptionController.attributes.createPeerLocalAttribute({ - id: idOfAttributeOfOtherIdentity, + const attribute = await consumptionController.attributes.createSharedLocalAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: someOtherIdentity + owner: recipient }), - peer: someOtherIdentity, + peer: sender, requestReference: await ConsumptionIds.request.generate() }); const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), - attribute: TestObjectFactory.createIdentityAttribute() + attribute: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from("") + }) }); const requestId = await ConsumptionIds.request.generate(); const request = LocalRequest.from({ id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: accountController.identity.address, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -385,31 +397,57 @@ describe("ProposeAttributeRequestItemProcessor", function () { const acceptParams: AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON = { accept: true, - attributeId: idOfAttributeOfOtherIdentity.toString() + attributeId: attribute.id.toString() }; const result = await processor.canAccept(requestItem, acceptParams, request); expect(result).errorValidationResult({ - code: "error.consumption.requests.invalidAcceptParameters", - message: /The given Attribute belongs to someone else. You can only share own Attributes./ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided IdentityAttribute is a shared copy of a RepositoryAttribute. You can only share RepositoryAttributes." }); }); - test("returns an error when the given Attribute's owner is not the current identity", async function () { - const someOtherIdentity = CoreAddress.from("id1"); + test("returns an error when a successor of the existing IdentityAttribute is already shared", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: recipient + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + const ownSharedCopyOfSuccessor = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: successorOfRepositoryAttribute.id, + peer: sender, + requestReference: await ConsumptionIds.request.generate() + }); const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), - attribute: TestObjectFactory.createIdentityAttribute() + attribute: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from("") + }) }); const requestId = await ConsumptionIds.request.generate(); const request = LocalRequest.from({ id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: accountController.identity.address, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -418,25 +456,91 @@ describe("ProposeAttributeRequestItemProcessor", function () { statusLog: [] }); - const acceptParams: AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON = { + const acceptParams: AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON = { accept: true, - attribute: TestObjectFactory.createIdentityAttribute({ owner: someOtherIdentity }).toJSON() + attributeId: repositoryAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: `The provided IdentityAttribute is outdated. You have already shared the successor '${ownSharedCopyOfSuccessor.shareInfo?.sourceAttribute?.toString()}' of it.` + }); + }); + + test("returns an error when a RelationshipAttribute was queried and the Recipient tries to respond with an existing RelationshipAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: recipient.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }), + attribute: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AnotherStringValue" + }) + }), + peer: sender, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + attributeId: localAttribute.id.toString() }; const result = await processor.canAccept(requestItem, acceptParams, request); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidAcceptParameters", - message: /The given Attribute belongs to someone else. You can only share own Attributes./ + message: "When responding to a RelationshipAttributeQuery, only new RelationshipAttributes may be provided." }); }); test("returns an error trying to share the predecessor of an already shared Attribute", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -446,7 +550,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { "@type": "GivenName", value: "A new given name" }, - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -483,17 +587,20 @@ describe("ProposeAttributeRequestItemProcessor", function () { const result = await processor.canAccept(requestItem, acceptParams, request); expect(result).errorValidationResult({ - code: "error.consumption.requests.invalidAcceptParameters", - message: "You cannot share the predecessor of an already shared Attribute version." + code: "error.consumption.requests.attributeQueryMismatch", + message: `The provided IdentityAttribute is outdated. You have already shared the successor '${successorRepositoryAttribute.id}' of it.` }); }); }); describe("accept", function () { - test("accept with existing Attribute that wasn't shared before", async function () { - const attribute = await consumptionController.attributes.createLocalAttribute({ + test("accept with existing RepositoryAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const attribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: recipient }) }); @@ -507,7 +614,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -529,7 +636,10 @@ describe("ProposeAttributeRequestItemProcessor", function () { expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); }); - test("accept proposed Attribute", async function () { + test("accept proposed IdentityAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -544,7 +654,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -564,9 +674,13 @@ describe("ProposeAttributeRequestItemProcessor", function () { expect(createdAttribute).toBeDefined(); expect(createdAttribute!.shareInfo).toBeDefined(); expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(createdAttribute!.content.owner.toString()).toStrictEqual(recipient.toString()); }); - test("accept with new IdentityAttribute", async function () { + test("in case of accepting with a new IdentityAttribute, create a new RepositoryAttribute as well as a copy of it for the Recipient", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -577,7 +691,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -590,7 +704,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { accept: true, attribute: { "@type": "IdentityAttribute", - owner: accountController.identity.address.toString(), + owner: recipient.toString(), value: { "@type": "GivenName", value: "AGivenName" @@ -611,12 +725,14 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); test("accept with new RelationshipAttribute", async function () { - const senderAddress = accountController.identity.address; + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: RelationshipAttributeQuery.from({ - key: "aKey", - owner: senderAddress, + key: "AKey", + owner: "", attributeCreationHints: { valueType: "ProprietaryString", title: "ATitle", @@ -630,7 +746,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: senderAddress, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -645,10 +761,10 @@ describe("ProposeAttributeRequestItemProcessor", function () { "@type": "RelationshipAttribute", key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public, - owner: senderAddress.toString(), + owner: "", value: { "@type": "ProprietaryString", - title: "aTitle", + title: "ATitle", value: "AStringValue" } } @@ -661,14 +777,15 @@ describe("ProposeAttributeRequestItemProcessor", function () { expect(createdSharedAttribute!.shareInfo).toBeDefined(); expect(createdSharedAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(createdSharedAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); + expect(createdSharedAttribute!.content.owner.toString()).toStrictEqual(recipient.toString()); }); test("accept with existing IdentityAttribute whose predecessor was already shared", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -684,7 +801,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { "@type": "GivenName", value: "A new given name" }, - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -733,9 +850,9 @@ describe("ProposeAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute whose predecessor was already shared but is DeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -745,13 +862,13 @@ describe("ProposeAttributeRequestItemProcessor", function () { "@type": "GivenName", value: "A succeeded given name" }, - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); await consumptionController.attributes.createAttributeUnsafe({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }), shareInfo: LocalAttributeShareInfo.from({ sourceAttribute: predecessorRepositoryAttribute.id, peer: sender, requestReference: await CoreId.generate() }), deletionInfo: LocalAttributeDeletionInfo.from({ deletionStatus: DeletionStatus.DeletedByPeer, deletionDate: CoreDate.utc().subtract({ days: 1 }) }) @@ -797,7 +914,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute whose predecessor was already shared but is ToBeDeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) }) @@ -861,9 +978,9 @@ describe("ProposeAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute that is already shared and the latest shared version", async function () { const sender = CoreAddress.from("Sender"); - const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -909,15 +1026,15 @@ describe("ProposeAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute that is already shared and the latest shared version but is DeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); const alreadySharedAttribute = await consumptionController.attributes.createAttributeUnsafe({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }), shareInfo: LocalAttributeShareInfo.from({ sourceAttribute: repositoryAttribute.id, peer: sender, requestReference: await CoreId.generate() }), deletionInfo: LocalAttributeDeletionInfo.from({ deletionStatus: DeletionStatus.DeletedByPeer, deletionDate: CoreDate.utc().subtract({ days: 1 }) }) @@ -962,7 +1079,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute that is already shared and the latest shared version but is ToBeDeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) }) @@ -1021,7 +1138,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { attribute: TestObjectFactory.createIdentityAttribute() }); const requestId = await ConsumptionIds.request.generate(); - const peer = CoreAddress.from("id1"); + const peer = CoreAddress.from("did:e:a-domain:dids:anidentity"); const incomingRequest = LocalRequest.from({ id: requestId, @@ -1057,14 +1174,12 @@ describe("ProposeAttributeRequestItemProcessor", function () { test("succeeds an existing peer shared IdentityAttribute with the Attribute received in the ResponseItem", async function () { const sender = CoreAddress.from("Sender"); - const predecessorPeerSharedIdentityAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorPeerSharedIdentityAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: sender }), - shareInfo: LocalAttributeShareInfo.from({ - peer: sender, - requestReference: CoreId.from("oldReqRef") - }) + peer: sender, + requestReference: CoreId.from("oldReqRef") }); const requestItem = ProposeAttributeRequestItem.from({ diff --git a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts index 3becf28f5..8f13190f0 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts @@ -4,6 +4,8 @@ import { AttributeSuccessionAcceptResponseItem, IdentityAttribute, IdentityAttributeQuery, + IQLQuery, + ProprietaryString, ReadAttributeAcceptResponseItem, ReadAttributeRequestItem, RelationshipAttribute, @@ -11,7 +13,8 @@ import { RelationshipAttributeQuery, Request, ResponseItemResult, - ThirdPartyRelationshipAttributeQuery + ThirdPartyRelationshipAttributeQuery, + ThirdPartyRelationshipAttributeQueryOwner } from "@nmshd/content"; import { AccountController, CoreAddress, CoreDate, CoreId, Transport } from "@nmshd/transport"; import { @@ -67,7 +70,7 @@ describe("ReadAttributeRequestItemProcessor", function () { query: query }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipientAddress")); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipient")); expect(result).successfulValidationResult(); }); @@ -77,6 +80,7 @@ describe("ReadAttributeRequestItemProcessor", function () { enum TestIdentity { Self, Recipient, + Empty, OtherWithRelationship, OtherWithoutRelationship, ThirdParty @@ -105,6 +109,37 @@ describe("ReadAttributeRequestItemProcessor", function () { success: true } }, + { + description: "query with owner=empty string, owner will become the Recipient later on", + input: { + owner: TestIdentity.Empty + }, + expectedOutput: { + success: true + } + }, + { + description: "cannot query with owner=Recipient", + input: { + owner: TestIdentity.Recipient + }, + expectedOutput: { + errorCode: "error.consumption.requests.invalidRequestItem", + errorMessage: + "The owner of the given `query` can only be an empty string or yourself. This is because you can only request RelationshipAttributes using a ReadAttributeRequestitem with a RelationshipAttributeQuery where the Recipient of the Request or yourself is the owner. And in order to avoid mistakes, the Recipient automatically will become the owner of the RelationshipAttribute later on if the owner of the `query` is an empty string." + } + }, + { + description: "cannot query with owner=thirdParty", + input: { + owner: TestIdentity.ThirdParty + }, + expectedOutput: { + errorCode: "error.consumption.requests.invalidRequestItem", + errorMessage: + "The owner of the given `query` can only be an empty string or yourself. This is because you can only request RelationshipAttributes using a ReadAttributeRequestitem with a RelationshipAttributeQuery where the Recipient of the Request or yourself is the owner. And in order to avoid mistakes, the Recipient automatically will become the owner of the RelationshipAttribute later on if the owner of the `query` is an empty string." + } + }, { description: "query with owner=thirdParty=someThirdParty, used for e.g. the bonuscard-number of a different company", input: { @@ -116,20 +151,19 @@ describe("ReadAttributeRequestItemProcessor", function () { } }, { - description: "cannot query own attributes from third party", + description: "can query with thirdParty = empty string", input: { - owner: TestIdentity.Self, - thirdParty: TestIdentity.ThirdParty + owner: TestIdentity.ThirdParty, + thirdParty: TestIdentity.Empty }, expectedOutput: { - errorCode: "error.consumption.requests.invalidRequestItem", - errorMessage: "Cannot query own Attributes from a third party." + success: true } }, { description: "cannot query with thirdParty = self", input: { - owner: TestIdentity.Self, + owner: TestIdentity.Recipient, thirdParty: TestIdentity.Self }, expectedOutput: { @@ -155,13 +189,15 @@ describe("ReadAttributeRequestItemProcessor", function () { case TestIdentity.Self: return accountController.identity.address.toString(); case TestIdentity.Recipient: - return CoreAddress.from("recipientAddress").toString(); + return CoreAddress.from("recipient").toString(); + case TestIdentity.Empty: + return CoreAddress.from("").toString(); case TestIdentity.OtherWithRelationship: - return CoreAddress.from("recipientAddress").toString(); + return CoreAddress.from("recipient").toString(); case TestIdentity.OtherWithoutRelationship: return "someAddressWithoutRelationship"; case TestIdentity.ThirdParty: - return "someThirdPartyAddress"; + return "thirdParty"; default: throw new Error("Given TestIdentity does not exist"); } @@ -171,13 +207,13 @@ describe("ReadAttributeRequestItemProcessor", function () { if (testParams.input.thirdParty !== undefined) { query = ThirdPartyRelationshipAttributeQuery.from({ owner: translateTestIdentityToAddress(testParams.input.owner), - key: "aKey", + key: "AKey", thirdParty: [translateTestIdentityToAddress(testParams.input.thirdParty)] }); } else { query = RelationshipAttributeQuery.from({ owner: translateTestIdentityToAddress(testParams.input.owner), - key: "aKey", + key: "AKey", attributeCreationHints: { valueType: "ProprietaryString", title: "ATitle", @@ -191,7 +227,7 @@ describe("ReadAttributeRequestItemProcessor", function () { query: query }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipientAddress")); + const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipient")); if (testParams.expectedOutput.hasOwnProperty("success")) { // eslint-disable-next-line jest/no-conditional-expect @@ -210,12 +246,13 @@ describe("ReadAttributeRequestItemProcessor", function () { describe("canAccept", function () { test("can be called with the id of an existing own LocalAttribute", async function () { - const attribute = await consumptionController.attributes.createPeerLocalAttribute({ + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const attribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) - }), - peer: CoreAddress.from(accountController.identity.address), - requestReference: CoreId.from("someRequestReference") + owner: recipient + }) }); const requestItem = ReadAttributeRequestItem.from({ @@ -227,7 +264,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -247,6 +284,9 @@ describe("ReadAttributeRequestItemProcessor", function () { }); test("can be called with a new Attribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }) @@ -256,7 +296,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -269,11 +309,12 @@ describe("ReadAttributeRequestItemProcessor", function () { accept: true, newAttribute: { "@type": "IdentityAttribute", - owner: accountController.identity.address.toString(), + owner: recipient.toString(), value: { "@type": "GivenName", value: "AGivenName" - } + }, + tags: ["ATag"] } }; @@ -283,18 +324,29 @@ describe("ReadAttributeRequestItemProcessor", function () { }); test("can be called with an existing RelationshipAttribute by a third party", async function () { - const attribute = await consumptionController.attributes.createLocalAttribute({ - content: TestObjectFactory.createRelationshipAttribute({ - owner: CoreAddress.from("id1") - }) + const sender = CoreAddress.from("Sender"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const attribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: aThirdParty, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() }); const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: ThirdPartyRelationshipAttributeQuery.from({ - key: "aKey", - owner: "id1", - thirdParty: ["id1"] + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [aThirdParty.toString()] }) }); const requestId = await ConsumptionIds.request.generate(); @@ -302,7 +354,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -322,6 +374,8 @@ describe("ReadAttributeRequestItemProcessor", function () { }); test("returns an error when the given Attribute id does not exist", async function () { + const sender = CoreAddress.from("Sender"); + const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }) @@ -331,7 +385,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -352,57 +406,12 @@ describe("ReadAttributeRequestItemProcessor", function () { }); }); - test("returns an error when the given Attribute id belongs to a peer shared Attribute", async function () { - const peer = CoreAddress.from("Peer"); - - const peerSharedAttributeId = await ConsumptionIds.attribute.generate(); - - await consumptionController.attributes.createPeerLocalAttribute({ - id: peerSharedAttributeId, - content: TestObjectFactory.createIdentityAttribute({ - owner: peer - }), - peer: peer, - requestReference: await ConsumptionIds.request.generate() - }); - - const requestItem = ReadAttributeRequestItem.from({ - mustBeAccepted: true, - query: IdentityAttributeQuery.from({ valueType: "GivenName" }) - }); - const requestId = await ConsumptionIds.request.generate(); - const request = LocalRequest.from({ - id: requestId, - createdAt: CoreDate.utc(), - isOwn: false, - peer: peer, - status: LocalRequestStatus.DecisionRequired, - content: Request.from({ - id: requestId, - items: [requestItem] - }), - statusLog: [] - }); - - const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { - accept: true, - existingAttributeId: peerSharedAttributeId.toString() - }; - - const result = await processor.canAccept(requestItem, acceptParams, request); - - expect(result).errorValidationResult({ - code: "error.consumption.requests.invalidAcceptParameters", - message: /The given Attribute belongs to someone else. You can only share own Attributes./ - }); - }); - test("returns an error trying to share the predecessor of an already shared Attribute", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -412,7 +421,7 @@ describe("ReadAttributeRequestItemProcessor", function () { "@type": "GivenName", value: "A new given name" }, - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -448,17 +457,635 @@ describe("ReadAttributeRequestItemProcessor", function () { const result = await processor.canAccept(requestItem, acceptParams, request); expect(result).errorValidationResult({ - code: "error.consumption.requests.invalidAcceptParameters", - message: "You cannot share the predecessor of an already shared Attribute version." + code: "error.consumption.requests.attributeQueryMismatch", + message: `The provided IdentityAttribute is outdated. You have already shared the successor '${successorRepositoryAttribute.id}' of it.` + }); + }); + + describe("canAccept ReadAttributeRequestitem with IdentityAttributeQuery", function () { + test("returns an error when the existing IdentityAttribute is already shared", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const attribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: recipient + }), + peer: sender, + requestReference: await ConsumptionIds.request.generate() + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: attribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided IdentityAttribute is a shared copy of a RepositoryAttribute. You can only share RepositoryAttributes." + }); + }); + + test("returns an error when a successor of the existing IdentityAttribute is already shared", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: recipient + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + const ownSharedCopyOfSuccessor = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: successorOfRepositoryAttribute.id, + peer: sender, + requestReference: await ConsumptionIds.request.generate() + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: repositoryAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: `The provided IdentityAttribute is outdated. You have already shared the successor '${ownSharedCopyOfSuccessor.shareInfo?.sourceAttribute?.toString()}' of it.` + }); + }); + + test("can be called with property tags used in the IdentityAttributeQuery", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ tags: ["ATag"], valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + tags: ["ATag", "AnotherTag"], + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).successfulValidationResult(); + }); + }); + + describe("canAccept ReadAttributeRequestitem with IQLQuery", function () { + test("can be called with property tags used in the IQLQuery", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IQLQuery.from({ queryString: "GivenName", attributeCreationHints: { valueType: "GivenName", tags: ["tagA", "tagB", "tagC"] } }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + tags: ["tagA", "tagD", "tagE"], + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).successfulValidationResult(); + }); + }); + + describe("canAccept ReadAttributeRequestitem with RelationshipAttributeQuery", function () { + test("returns an error when a RelationshipAttribute was queried using a RelationshipAttributeQuery and the Recipient tries to respond with an existing RelationshipAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: recipient.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: sender, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidAcceptParameters", + message: "When responding to a RelationshipAttributeQuery, only new RelationshipAttributes may be provided." + }); + }); + + test("can be called when a RelationshipAttribute of Value Type Consent is queried even though title and description is specified", async function () { + const sender = CoreAddress.from("Sender"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "Consent", + title: "ATitle", + description: "ADescription", + confidentiality: RelationshipAttributeConfidentiality.Private + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Private, + owner: sender.toString(), + value: { + "@type": "Consent", + consent: "AConsent" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).successfulValidationResult(); + }); + + test("can be called with properties validFrom and validTo used in the query", async function () { + const sender = CoreAddress.from("Sender"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + validFrom: "2024-02-14T08:47:35.077Z", + validTo: "2024-02-14T09:35:12.824Z", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: sender.toString(), + validFrom: "2024-02-14T08:40:35.077Z", + validTo: "2024-02-14T09:35:12.824Z", + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).successfulValidationResult(); + }); + }); + + describe("canAccept ReadAttributeRequestitem with ThirdPartyRelationshipAttributeQuery", function () { + test("returns an error when a RelationshipAttribute is a copy of a sourceAttribute that was queried using a ThirdPartyRelationshipAttributeQuery", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const aThirdParty = CoreAddress.from("AThirdParty"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + shareInfo: { + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: CoreId.from("sourceAttributeId") + } + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "When responding to a ThirdPartyRelationshipAttributeQuery, only RelationshipAttributes that are not a copy of a sourceAttribute may be provided." + }); + }); + + test("returns an error when a RelationshipAttribute is not shared with one of the third parties that were queried using a ThirdPartyRelationshipAttributeQuery", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const aThirdParty = CoreAddress.from("AThirdParty"); + const anUninvolvedThirdParty = CoreAddress.from("AnUninvolvedThirdParty"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: anUninvolvedThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute exists in the context of a Relationship with a third party that should not be involved." + }); + }); + + test("returns an error when a RelationshipAttribute was queried using a ThirdPartyRelationshipAttributeQuery and the Recipient tries to respond with a new RelationshipAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: aThirdParty.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidAcceptParameters", + message: "When responding to a ThirdPartyRelationshipAttributeQuery, only RelationshipAttributes that already exist may be provided." + }); + }); + + test("can be called with an arbitrary third party if the thirdParty string array of the ThirdPartyRelationshipAttributeQuery contains an empty string", async function () { + const sender = CoreAddress.from("Sender"); + const aThirdParty = CoreAddress.from("AThirdParty"); + const anUninvolvedThirdParty = CoreAddress.from("AnUninvolvedThirdParty"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: "", + key: "AKey", + thirdParty: ["", aThirdParty.toString()] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: anUninvolvedThirdParty, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: anUninvolvedThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).successfulValidationResult(); + }); + + test("returns an error when the confidentiality of the existing RelationshipAttribute to be shared is private", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const aThirdParty = CoreAddress.from("AThirdParty"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Private, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The confidentiality of the provided RelationshipAttribute is private. Therefore you are not allowed to share it." + }); }); }); }); describe("accept", function () { - test("accept with existing Attribute that wasn't shared before", async function () { - const attribute = await consumptionController.attributes.createLocalAttribute({ + test("accept with existing RepositoryAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + const attribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: recipient }) }); @@ -471,7 +1098,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -495,6 +1122,8 @@ describe("ReadAttributeRequestItemProcessor", function () { }); test("accept with new IdentityAttribute", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }) @@ -504,7 +1133,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: CoreAddress.from("id1"), + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -517,7 +1146,7 @@ describe("ReadAttributeRequestItemProcessor", function () { accept: true, newAttribute: { "@type": "IdentityAttribute", - owner: accountController.identity.address.toString(), + owner: recipient.toString(), value: { "@type": "GivenName", value: "AGivenName" @@ -539,12 +1168,13 @@ describe("ReadAttributeRequestItemProcessor", function () { }); test("accept with new RelationshipAttribute", async function () { - const senderAddress = accountController.identity.address; + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: RelationshipAttributeQuery.from({ - key: "aKey", - owner: senderAddress, + key: "AKey", + owner: recipient, attributeCreationHints: { valueType: "ProprietaryString", title: "ATitle", @@ -557,7 +1187,7 @@ describe("ReadAttributeRequestItemProcessor", function () { id: requestId, createdAt: CoreDate.utc(), isOwn: false, - peer: senderAddress, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ id: requestId, @@ -572,10 +1202,10 @@ describe("ReadAttributeRequestItemProcessor", function () { "@type": "RelationshipAttribute", key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public, - owner: senderAddress.toString(), + owner: recipient.toString(), value: { "@type": "ProprietaryString", - title: "aTitle", + title: "ATitle", value: "AStringValue" } } @@ -594,7 +1224,7 @@ describe("ReadAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute whose predecessor was already shared", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) }) @@ -657,9 +1287,9 @@ describe("ReadAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute whose predecessor was already shared but is DeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -669,13 +1299,13 @@ describe("ReadAttributeRequestItemProcessor", function () { "@type": "GivenName", value: "A succeeded given name" }, - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); await consumptionController.attributes.createAttributeUnsafe({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }), shareInfo: LocalAttributeShareInfo.from({ sourceAttribute: predecessorRepositoryAttribute.id, peer: sender, requestReference: await CoreId.generate() }), deletionInfo: LocalAttributeDeletionInfo.from({ deletionStatus: DeletionStatus.DeletedByPeer, deletionDate: CoreDate.utc().subtract({ days: 1 }) }) @@ -717,7 +1347,7 @@ describe("ReadAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute whose predecessor was already shared but is ToBeDeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) }) @@ -777,9 +1407,9 @@ describe("ReadAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute that is already shared and the latest shared version", async function () { const sender = CoreAddress.from("Sender"); - const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); @@ -821,15 +1451,15 @@ describe("ReadAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute that is already shared and the latest shared version but is DeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }) }); const alreadySharedAttribute = await consumptionController.attributes.createAttributeUnsafe({ content: TestObjectFactory.createIdentityAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }), shareInfo: LocalAttributeShareInfo.from({ sourceAttribute: repositoryAttribute.id, peer: sender, requestReference: await CoreId.generate() }), deletionInfo: LocalAttributeDeletionInfo.from({ deletionStatus: DeletionStatus.DeletedByPeer, deletionDate: CoreDate.utc().subtract({ days: 1 }) }) @@ -870,7 +1500,7 @@ describe("ReadAttributeRequestItemProcessor", function () { test("accept with existing IdentityAttribute that is already shared and the latest shared version but is ToBeDeletedByPeer", async function () { const sender = CoreAddress.from("Sender"); - const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) }) @@ -920,14 +1550,12 @@ describe("ReadAttributeRequestItemProcessor", function () { const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); const sender = CoreAddress.from("Sender"); - const predecessorSourceAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorSourceAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: TestObjectFactory.createRelationshipAttribute({ - owner: CoreAddress.from(accountController.identity.address) + owner: accountController.identity.address }), - shareInfo: LocalAttributeShareInfo.from({ - peer: thirdPartyAddress, - requestReference: CoreId.from("reqRef") - }) + peer: thirdPartyAddress, + requestReference: CoreId.from("reqRef") }); const predecessorOwnSharedRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ @@ -940,12 +1568,12 @@ describe("ReadAttributeRequestItemProcessor", function () { content: RelationshipAttribute.from({ value: { "@type": "ProprietaryString", - title: "A new title", - value: "A new value" + title: "ANewTitle", + value: "ANewValue" }, confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey", - owner: CoreAddress.from(accountController.identity.address) + key: "AKey", + owner: accountController.identity.address }), shareInfo: LocalAttributeShareInfo.from({ peer: thirdPartyAddress, @@ -956,9 +1584,9 @@ describe("ReadAttributeRequestItemProcessor", function () { const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: ThirdPartyRelationshipAttributeQuery.from({ - key: "aKey", - owner: accountController.identity.address, - thirdParty: [thirdPartyAddress] + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [thirdPartyAddress.toString()] }) }); @@ -999,14 +1627,12 @@ describe("ReadAttributeRequestItemProcessor", function () { const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); const sender = CoreAddress.from("Sender"); - const predecessorSourceAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorSourceAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: TestObjectFactory.createRelationshipAttribute({ owner: thirdPartyAddress }), - shareInfo: LocalAttributeShareInfo.from({ - peer: thirdPartyAddress, - requestReference: CoreId.from("reqRef") - }) + peer: thirdPartyAddress, + requestReference: CoreId.from("reqRef") }); const predecessorOwnSharedRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ @@ -1019,11 +1645,11 @@ describe("ReadAttributeRequestItemProcessor", function () { content: RelationshipAttribute.from({ value: { "@type": "ProprietaryString", - title: "A new title", - value: "A new value" + title: "ANewTitle", + value: "ANewValue" }, confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey", + key: "AKey", owner: thirdPartyAddress }), shareInfo: LocalAttributeShareInfo.from({ @@ -1035,9 +1661,9 @@ describe("ReadAttributeRequestItemProcessor", function () { const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: ThirdPartyRelationshipAttributeQuery.from({ - key: "aKey", - owner: accountController.identity.address, - thirdParty: [thirdPartyAddress] + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [thirdPartyAddress.toString()] }) }); @@ -1118,14 +1744,12 @@ describe("ReadAttributeRequestItemProcessor", function () { test("succeeds an existing peer shared IdentityAttribute with the Attribute received in the ResponseItem", async function () { const recipient = CoreAddress.from("Recipient"); - const predecessorPeerSharedIdentityAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorPeerSharedIdentityAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: recipient }), - shareInfo: LocalAttributeShareInfo.from({ - peer: recipient, - requestReference: CoreId.from("oldReqRef") - }) + peer: recipient, + requestReference: CoreId.from("oldReqRef") }); const requestItem = ReadAttributeRequestItem.from({ @@ -1175,22 +1799,20 @@ describe("ReadAttributeRequestItemProcessor", function () { const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); const recipient = CoreAddress.from("Recipient"); - const predecessorPeerSharedRelationshipAttribute = await consumptionController.attributes.createLocalAttribute({ + const predecessorPeerSharedRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ content: TestObjectFactory.createRelationshipAttribute({ owner: thirdPartyAddress }), - shareInfo: LocalAttributeShareInfo.from({ - peer: recipient, - requestReference: CoreId.from("oldReqRef") - }) + peer: recipient, + requestReference: CoreId.from("oldReqRef") }); const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: ThirdPartyRelationshipAttributeQuery.from({ - key: "aKey", - owner: thirdPartyAddress, - thirdParty: [thirdPartyAddress] + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [thirdPartyAddress.toString()] }) }); const requestId = await ConsumptionIds.request.generate(); diff --git a/packages/consumption/test/modules/requests/itemProcessors/registerAttributeListener/RegisterAttributeListenerRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/registerAttributeListener/RegisterAttributeListenerRequestItemProcessor.test.ts index a4cee700e..e547b5a07 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/registerAttributeListener/RegisterAttributeListenerRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/registerAttributeListener/RegisterAttributeListenerRequestItemProcessor.test.ts @@ -40,8 +40,8 @@ describe("CreateAttributeRequestItemProcessor", function () { test("creates an AttributeListener and persists it to the DB", async function () { const requestItem = RegisterAttributeListenerRequestItem.from({ query: ThirdPartyRelationshipAttributeQuery.from({ - key: "aKey", - owner: "anOwner", + key: "AKey", + owner: "", thirdParty: ["aThirdParty"] }), mustBeAccepted: true diff --git a/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts index 3f57e269e..af07a7cb3 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts @@ -1,9 +1,9 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { GivenName, + IdentityAttribute, IIdentityAttribute, IRelationshipAttribute, - IdentityAttribute, ProprietaryString, RelationshipAttribute, RelationshipAttributeConfidentiality, @@ -14,7 +14,7 @@ import { Surname } from "@nmshd/content"; import { AccountController, CoreAddress, CoreDate, CoreId, Transport } from "@nmshd/transport"; -import { ConsumptionController, ConsumptionIds, LocalAttribute, LocalRequest, LocalRequestStatus, ShareAttributeRequestItemProcessor } from "../../../../../src"; +import { ConsumptionController, ConsumptionIds, DeletionStatus, LocalAttribute, LocalRequest, LocalRequestStatus, ShareAttributeRequestItemProcessor } from "../../../../../src"; import { TestUtil } from "../../../../core/TestUtil"; import { TestObjectFactory } from "../../testHelpers/TestObjectFactory"; @@ -52,7 +52,7 @@ describe("ShareAttributeRequestItemProcessor", function () { result: "success", attribute: IdentityAttribute.from({ value: GivenName.fromAny({ value: "AGivenName" }), - owner: CoreAddress.from("{{sender}}") + owner: CoreAddress.from("Sender") }) }, { @@ -68,8 +68,7 @@ describe("ShareAttributeRequestItemProcessor", function () { result: "error", expectedError: { code: "error.consumption.requests.invalidRequestItem", - message: - /The owner of the given `attribute` can only be an empty string. This is because you can only send Attributes where the recipient of the Request is the owner anyway. And in order to avoid mistakes, the owner will be automatically filled for you./ + message: "The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes." }, attribute: IdentityAttribute.from({ value: GivenName.fromAny({ value: "AGivenName" }), @@ -80,78 +79,93 @@ describe("ShareAttributeRequestItemProcessor", function () { scenario: "a Relationship Attribute with owner=sender", result: "success", attribute: RelationshipAttribute.from({ - value: ProprietaryString.fromAny({ value: "AGivenName", title: "aTitle" }), - owner: CoreAddress.from("{{sender}}"), + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + owner: CoreAddress.from("Sender"), confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey" + key: "AKey" }) }, { scenario: "a Relationship Attribute with owner=", result: "success", attribute: RelationshipAttribute.from({ - value: ProprietaryString.fromAny({ value: "AGivenName", title: "aTitle" }), - owner: CoreAddress.from("{{sender}}"), + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + owner: CoreAddress.from("Sender"), confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey" + key: "AKey" }) }, { scenario: "a Relationship Attribute with owner=someOtherOwner", result: "success", attribute: RelationshipAttribute.from({ - value: ProprietaryString.fromAny({ value: "AGivenName", title: "aTitle" }), - owner: CoreAddress.from("{{sender}}"), + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + owner: CoreAddress.from("Sender"), confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey" + key: "AKey" }) }, { scenario: "a Relationship Attribute with confidentiality=private", result: "error", attribute: RelationshipAttribute.from({ - value: ProprietaryString.fromAny({ value: "AGivenName", title: "aTitle" }), - owner: CoreAddress.from("{{sender}}"), + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + owner: CoreAddress.from("Sender"), confidentiality: RelationshipAttributeConfidentiality.Private, - key: "aKey" + key: "AKey" }), expectedError: { code: "error.consumption.requests.invalidRequestItem", - message: /The confidentiality of the given `attribute` is private. Therefore you are not allowed to share it./ + message: "The confidentiality of the given `attribute` is private. Therefore you are not allowed to share it." } }, { scenario: "a Relationship Attribute with owner=recipient", result: "error", attribute: RelationshipAttribute.from({ - value: ProprietaryString.fromAny({ value: "AGivenName", title: "aTitle" }), - owner: CoreAddress.from("{{recipient}}"), + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + owner: CoreAddress.from("Recipient"), confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey" + key: "AKey" }), expectedError: { code: "error.consumption.requests.invalidRequestItem", - message: /It doesn't make sense to share a RelationshipAttribute with its owner./ + message: "It doesn't make sense to share a RelationshipAttribute with its owner." } } ])("returns ${value.result} when passing ${value.scenario}", async function (testParams) { - const senderAddress = testAccount.identity.address; - const recipientAddress = CoreAddress.from("recipientAddress"); + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + const aThirdParty = CoreAddress.from("AThirdParty"); - if (testParams.attribute.owner.address === "{{sender}}") { - testParams.attribute.owner = senderAddress; + if (testParams.attribute.owner.address === "Sender") { + testParams.attribute.owner = sender; } - if (testParams.attribute.owner.address === "{{recipient}}") { - testParams.attribute.owner = recipientAddress; + if (testParams.attribute.owner.address === "Recipient") { + testParams.attribute.owner = recipient; + } + + let sourceAttribute; + + if (testParams.attribute instanceof IdentityAttribute) { + sourceAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: { + ...testParams.attribute.toJSON(), + owner: testParams.attribute.owner.equals("") ? sender : testParams.attribute.owner + } as IIdentityAttribute + }); + } else { + sourceAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: { + ...testParams.attribute.toJSON(), + owner: testParams.attribute.owner.equals("") ? sender : testParams.attribute.owner + } as IRelationshipAttribute, + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); } - const sourceAttribute = await consumptionController.attributes.createLocalAttribute({ - content: { - ...testParams.attribute.toJSON(), - owner: testParams.attribute.owner.equals("") ? senderAddress : testParams.attribute.owner - } as IIdentityAttribute | IRelationshipAttribute - }); const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: sourceAttribute.content, @@ -159,7 +173,7 @@ describe("ShareAttributeRequestItemProcessor", function () { }); const request = Request.from({ items: [requestItem] }); - const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipientAddress); + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); if (testParams.result === "success") { // eslint-disable-next-line jest/no-conditional-expect @@ -171,19 +185,20 @@ describe("ShareAttributeRequestItemProcessor", function () { }); test("returns error when the attribute doesn't exists", async function () { - const recipientAddress = CoreAddress.from("recipientAddress"); + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: IdentityAttribute.from({ value: GivenName.fromAny({ value: "AGivenName" }), - owner: CoreAddress.from("{{sender}}") + owner: sender }), sourceAttributeId: CoreId.from("anIdThatDoesntExist") }); const request = Request.from({ items: [requestItem] }); - const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipientAddress); + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidRequestItem", @@ -192,38 +207,693 @@ describe("ShareAttributeRequestItemProcessor", function () { }); test("returns error when the attribute content is not equal to the content persisted in the attribute collection", async function () { - const recipientAddress = CoreAddress.from("recipientAddress"); + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); const attribute = IdentityAttribute.from({ value: GivenName.fromAny({ value: "AGivenName" }), - owner: CoreAddress.from("{{sender}}") + owner: sender }); - const sourceAttribute = await consumptionController.attributes.createLocalAttribute({ + const sourceAttribute = await consumptionController.attributes.createRepositoryAttribute({ content: attribute }); const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: IdentityAttribute.from({ ...sourceAttribute.content.toJSON(), - value: Surname.from("aSurname").toJSON() + value: Surname.from("ASurname").toJSON() }), sourceAttributeId: sourceAttribute.id }); const request = Request.from({ items: [requestItem] }); - const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipientAddress); + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: `The Attribute with the given sourceAttributeId '${sourceAttribute.id.toString()}' does not match the given Attribute.` + }); + }); + + test("returns error when the IdentityAttribute is a shared copy of a RepositoryAttribute", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const localAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: CoreId.from("sourceAttributeId") + } + }); + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: localAttribute.content, + sourceAttributeId: localAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: "The provided IdentityAttribute is a shared copy of a RepositoryAttribute. You can only share RepositoryAttributes." + }); + }); + + test("returns error when the IdentityAttribute is already shared with the peer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const localAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }) + }); + + const localAttributeCopy = await consumptionController.attributes.createSharedLocalAttributeCopy({ + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttributeId: localAttribute.id + }); + expect(localAttributeCopy.isShared()).toBe(true); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: localAttribute.content, + sourceAttributeId: localAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: `The IdentityAttribute with the given sourceAttributeId '${requestItem.sourceAttributeId.toString()}' is already shared with the peer.` + }); + }); + + test("returns success when the IdentityAttribute is already shared with the peer but DeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const localAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }) + }); + + const localAttributeCopy = await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: localAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.DeletedByPeer, + deletionDate: CoreDate.utc().subtract({ days: 1 }) + } + }); + expect(localAttributeCopy.isShared()).toBe(true); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: localAttribute.content, + sourceAttributeId: localAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns success when the IdentityAttribute is already shared with the peer but ToBeDeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const localAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }) + }); + + const localAttributeCopy = await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: localAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.ToBeDeletedByPeer, + deletionDate: CoreDate.utc().add({ days: 1 }) + } + }); + expect(localAttributeCopy.isShared()).toBe(true); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: localAttribute.content, + sourceAttributeId: localAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns an error when a successor of the existing IdentityAttribute is already shared with the peer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + const ownSharedCopyOfSuccessor = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: successorOfRepositoryAttribute.id, + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: repositoryAttribute.content, + sourceAttributeId: repositoryAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidRequestItem", - message: `The Attribute with the given sourceAttributeId '${sourceAttribute.id.toString()}' does not match the given attribute.` + message: `The provided IdentityAttribute is outdated. Its successor '${ownSharedCopyOfSuccessor.shareInfo?.sourceAttribute?.toString()}' is already shared with the peer.` }); }); + + test("returns success when a successor of the existing IdentityAttribute is already shared with the peer but DeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: successorOfRepositoryAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.DeletedByPeer, + deletionDate: CoreDate.utc().subtract({ days: 1 }) + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: repositoryAttribute.content, + sourceAttributeId: repositoryAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns success when a successor of the existing IdentityAttribute is already shared with the peer but ToBeDeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: successorOfRepositoryAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.ToBeDeletedByPeer, + deletionDate: CoreDate.utc().add({ days: 1 }) + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: repositoryAttribute.content, + sourceAttributeId: repositoryAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns an error when a predecessor of the existing IdentityAttribute is already shared and therefore the user should notify about the Attribute succession instead of share it.", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const ownSharedCopyOfPredecessor = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: repositoryAttribute.id, + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + + expect(ownSharedCopyOfPredecessor.isShared()).toBe(true); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + expect(successorOfRepositoryAttribute.isShared()).toBe(false); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: successorOfRepositoryAttribute.content, + sourceAttributeId: successorOfRepositoryAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: `The predecessor '${ownSharedCopyOfPredecessor.shareInfo?.sourceAttribute?.toString()}' of the IdentityAttribute is already shared with the peer. Instead of sharing it, you should notify the peer about the Attribute succession.` + }); + }); + + test("returns success when a predecessor of the existing IdentityAttribute is already shared with the peer but DeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: repositoryAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.DeletedByPeer, + deletionDate: CoreDate.utc().subtract({ days: 1 }) + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: successorOfRepositoryAttribute.content, + sourceAttributeId: successorOfRepositoryAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns success when a predecessor of the existing IdentityAttribute is already shared with the peer but ToBeDeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const { successor: successorOfRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(repositoryAttribute.id, { + content: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: IdentityAttribute.from({ + owner: sender, + value: GivenName.from({ + value: "AGivenName" + }) + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: repositoryAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.ToBeDeletedByPeer, + deletionDate: CoreDate.utc().add({ days: 1 }) + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: successorOfRepositoryAttribute.content, + sourceAttributeId: successorOfRepositoryAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns error when the RelationshipAttribute is a copy of another RelationshipAttribute", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const relationshipAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: CoreId.from("sourceAttributeId") + } + }); + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: relationshipAttribute.content, + sourceAttributeId: relationshipAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: "You can only share RelationshipAttributes that are not a copy of a sourceAttribute." + }); + }); + + test("returns error when the initial RelationshipAttribute already exists in the context of the Relationship with the peer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + + const relationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: relationshipAttribute.content, + sourceAttributeId: relationshipAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: "The provided RelationshipAttribute already exists in the context of the Relationship with the peer." + }); + }); + + test("returns error when a ThirdPartyRelationshipAttribute already exists in the context of the Relationship with the peer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const initialRelationshipAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: initialRelationshipAttribute.id + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: initialRelationshipAttribute.content, + sourceAttributeId: initialRelationshipAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: "The provided RelationshipAttribute already exists in the context of the Relationship with the peer." + }); + }); + + test("returns success when a ThirdPartyRelationshipAttribute already exists in the context of the Relationship with the peer but is ToBeDeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const initialRelationshipAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: initialRelationshipAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.ToBeDeletedByPeer, + deletionDate: CoreDate.utc().add({ day: 1 }) + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: initialRelationshipAttribute.content, + sourceAttributeId: initialRelationshipAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns success when a ThirdPartyRelationshipAttribute already exists in the context of the Relationship with the peer but is DeletedByPeer", async function () { + const sender = testAccount.identity.address; + const recipient = CoreAddress.from("Recipient"); + const aThirdParty = CoreAddress.from("AThirdParty"); + + const initialRelationshipAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + } + }); + + await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + owner: sender, + value: ProprietaryString.fromAny({ value: "AGivenName", title: "ATitle" }), + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "AKey" + }), + shareInfo: { + peer: recipient, + requestReference: await ConsumptionIds.request.generate(), + sourceAttribute: initialRelationshipAttribute.id + }, + deletionInfo: { + deletionStatus: DeletionStatus.DeletedByPeer, + deletionDate: CoreDate.utc().subtract({ day: 1 }) + } + }); + + const requestItem = ShareAttributeRequestItem.from({ + mustBeAccepted: false, + attribute: initialRelationshipAttribute.content, + sourceAttributeId: initialRelationshipAttribute.id + }); + const request = Request.from({ items: [requestItem] }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, request, recipient); + + expect(result).successfulValidationResult(); + }); }); describe("accept", function () { - test("in case of an IdentityAttribute with 'owner=', creates a Local Attribute for the sender of the Request", async function () { - const senderAddress = CoreAddress.from("SenderAddress"); + test("in case of an IdentityAttribute with 'owner=', creates a Local Attribute for the Sender of the Request", async function () { + const sender = CoreAddress.from("Sender"); + const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: true, sourceAttributeId: CoreId.from("aSourceAttributeId"), @@ -235,7 +905,7 @@ describe("ShareAttributeRequestItemProcessor", function () { id: await ConsumptionIds.request.generate(), createdAt: CoreDate.utc(), isOwn: false, - peer: senderAddress, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ items: [requestItem] @@ -252,13 +922,14 @@ describe("ShareAttributeRequestItemProcessor", function () { const createdAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); expect(createdAttribute).toBeDefined(); expect(createdAttribute!.shareInfo).toBeDefined(); - expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(senderAddress.toString()); + expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(sender.toString()); expect(createdAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); - expect(createdAttribute!.content.owner.toString()).toStrictEqual(senderAddress.toString()); + expect(createdAttribute!.content.owner.toString()).toStrictEqual(sender.toString()); }); - test("in case of a RelationshipAttribute with 'owner=', creates a Local Attribute for the sender of the Request", async function () { - const senderAddress = CoreAddress.from("SenderAddress"); + test("in case of a RelationshipAttribute with 'owner=', creates a Local Attribute for the Sender of the Request", async function () { + const sender = CoreAddress.from("Sender"); + const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: true, sourceAttributeId: CoreId.from("aSourceAttributeId"), @@ -270,7 +941,7 @@ describe("ShareAttributeRequestItemProcessor", function () { id: await ConsumptionIds.request.generate(), createdAt: CoreDate.utc(), isOwn: false, - peer: senderAddress, + peer: sender, status: LocalRequestStatus.DecisionRequired, content: Request.from({ items: [requestItem] @@ -287,9 +958,9 @@ describe("ShareAttributeRequestItemProcessor", function () { const createdAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); expect(createdAttribute).toBeDefined(); expect(createdAttribute!.shareInfo).toBeDefined(); - expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(senderAddress.toString()); + expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(sender.toString()); expect(createdAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); - expect(createdAttribute!.content.owner.toString()).toStrictEqual(senderAddress.toString()); + expect(createdAttribute!.content.owner.toString()).toStrictEqual(sender.toString()); }); }); @@ -301,7 +972,7 @@ describe("ShareAttributeRequestItemProcessor", function () { }, { attributeType: "IdentityAttribute", - attributeOwner: "{{sender}}" + attributeOwner: "Sender" }, { attributeType: "RelationshipAttribute", @@ -309,7 +980,7 @@ describe("ShareAttributeRequestItemProcessor", function () { }, { attributeType: "RelationshipAttribute", - attributeOwner: "{{sender}}" + attributeOwner: "Sender" } ])( "in case of a ${value.attributeType}, creates a LocalAttribute with the Attribute from the RequestItem and the attributeId from the ResponseItem for the peer of the Request", @@ -320,11 +991,11 @@ describe("ShareAttributeRequestItemProcessor", function () { ? TestObjectFactory.createIdentityAttribute({ owner: testAccount.identity.address }) : TestObjectFactory.createRelationshipAttribute({ owner: testAccount.identity.address }); - const sourceAttribute = await consumptionController.attributes.createLocalAttribute({ + const sourceAttribute = await consumptionController.attributes.createAttributeUnsafe({ content: sourceAttributeContent }); - testParams.attributeOwner = testParams.attributeOwner.replace("{{sender}}", testAccount.identity.address.toString()); + testParams.attributeOwner = testParams.attributeOwner.replace("Sender", testAccount.identity.address.toString()); sourceAttribute.content.owner = CoreAddress.from(testParams.attributeOwner); @@ -353,7 +1024,7 @@ describe("ShareAttributeRequestItemProcessor", function () { sourceAttributeId: sourceAttribute.id }); const requestId = await ConsumptionIds.request.generate(); - const peer = CoreAddress.from("id1"); + const peer = CoreAddress.from("did:e:a-domain:dids:anidentity"); const localRequest = LocalRequest.from({ id: requestId, createdAt: CoreDate.utc(), diff --git a/packages/consumption/test/modules/requests/itemProcessors/utility/validateAttributeMatchesWithQuery.test.ts b/packages/consumption/test/modules/requests/itemProcessors/utility/validateAttributeMatchesWithQuery.test.ts new file mode 100644 index 000000000..85b390ce3 --- /dev/null +++ b/packages/consumption/test/modules/requests/itemProcessors/utility/validateAttributeMatchesWithQuery.test.ts @@ -0,0 +1,1147 @@ +import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { + IdentityAttributeQuery, + IQLQuery, + ProprietaryString, + ReadAttributeRequestItem, + RelationshipAttribute, + RelationshipAttributeConfidentiality, + RelationshipAttributeQuery, + Request, + ThirdPartyRelationshipAttributeQuery, + ThirdPartyRelationshipAttributeQueryOwner +} from "@nmshd/content"; +import { AccountController, CoreAddress, CoreDate, Transport } from "@nmshd/transport"; +import { + AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON, + AcceptReadAttributeRequestItemParametersWithNewAttributeJSON, + ConsumptionController, + ConsumptionIds, + LocalRequest, + LocalRequestStatus, + ReadAttributeRequestItemProcessor +} from "../../../../../src"; +import { TestUtil } from "../../../../core/TestUtil"; +import { TestObjectFactory } from "../../testHelpers/TestObjectFactory"; + +describe("validateAttributeMatchesWithQuery", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let consumptionController: ConsumptionController; + let accountController: AccountController; + + let readProcessor: ReadAttributeRequestItemProcessor; + + let recipient: CoreAddress; + const sender = CoreAddress.from("Sender"); + const aThirdParty = CoreAddress.from("AThirdParty"); + const anUninvolvedThirdParty = CoreAddress.from("AnUninvolvedThirdParty"); + beforeAll(async function () { + connection = await TestUtil.createConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 1); + ({ accountController, consumptionController } = accounts[0]); + recipient = accountController.identity.address; + }); + + afterAll(async function () { + await connection.close(); + }); + + describe("IdentityAttributeQuery", function () { + beforeEach(function () { + readProcessor = new ReadAttributeRequestItemProcessor(consumptionController); + }); + + test("returns an error when an IdentityAttribute was queried by an IdentityAttributeQuery and the peer tries to respond with a RelationshipAttribute", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not an IdentityAttribute, but an IdentityAttribute was queried." + }); + }); + + test("returns an error when the given Attribute id belongs to a peer Attribute", async function () { + const thirdPartyAttributeId = await ConsumptionIds.attribute.generate(); + await consumptionController.attributes.createSharedLocalAttribute({ + id: thirdPartyAttributeId, + content: TestObjectFactory.createIdentityAttribute({ + owner: aThirdParty + }), + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: thirdPartyAttributeId.toString() + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes." + }); + }); + + test("returns an error when the new IdentityAttribute to be created and shared belongs to a third party", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: aThirdParty.toString(), + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes." + }); + }); + + test("returns an error when an IdentityAttribute of a specific type was queried by an IdentityAttributeQuery and the peer tries to respond with an IdentityAttribute of another type", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "DisplayName", + value: "ADisplayName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided IdentityAttribute is not of the queried IdentityAttribute value type." + }); + }); + + test("returns an error when an IdentityAttribute has no tag but at least one tag was queried by IdentityAttributeQuery", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName", tags: ["ATag"] }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The tags of the provided IdentityAttribute do not contain at least one queried tag." + }); + }); + + test("returns an error when the tags of the IdentityAttribute do not match the tags queried by IdentityAttributeQuery", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ tags: ["tagA", "tagB", "tagC"], valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + tags: ["tagD", "tagE", "tagF"], + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The tags of the provided IdentityAttribute do not contain at least one queried tag." + }); + }); + + test("returns an error when an IdentityAttribute is not valid in the queried time frame", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ validTo: "2024-02-14T09:35:12.824Z", valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + validFrom: "2024-02-14T08:47:35.077Z", + validTo: "2024-02-14T09:35:12.824Z", + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not valid in the queried time frame." + }); + }); + }); + + describe("IQLQuery", function () { + beforeEach(function () { + readProcessor = new ReadAttributeRequestItemProcessor(consumptionController); + }); + + test("returns an error when an IdentityAttribute was queried by an IQLQuery and the peer tries to respond with a RelationshipAttribute", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IQLQuery.from({ queryString: "GivenName", attributeCreationHints: { valueType: "GivenName" } }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not an IdentityAttribute. Currently, only IdentityAttributes can be queried by an IQLQuery." + }); + }); + + test("returns an error when the IdentityAttribute queried by an IQLQuery is not owned by the Recipient", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IQLQuery.from({ queryString: "GivenName", attributeCreationHints: { valueType: "GivenName" } }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: sender.toString(), + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided IdentityAttribute belongs to someone else. You can only share own IdentityAttributes." + }); + }); + }); + + describe("RelationshipAttributeQuery", function () { + beforeEach(function () { + readProcessor = new ReadAttributeRequestItemProcessor(consumptionController); + }); + + test("returns an error when a RelationshipAttribute was queried and the Recipient tries to respond with an IdentityAttribute", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: recipient.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not a RelationshipAttribute, but a RelationshipAttribute was queried." + }); + }); + + test("returns an error when a RelationshipAttribute of a specific type was queried and the Recipient tries to respond with a RelationshipAttribute of another type", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: recipient.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryInteger", + title: "ATitle", + value: 1 + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute is not of the queried RelationshipAttribute value type." + }); + }); + + test("returns an error when a RelationshipAttribute does not belong to the queried owner", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not belong to the queried owner." + }); + }); + + test("returns an error when a RelationshipAttribute does not belong to the Recipient, but an empty string was specified for the owner of the query", async function () { + const sender = CoreAddress.from("Sender"); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: "", + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: sender.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "You are not the owner of the provided RelationshipAttribute, but an empty string was specified for the owner of the query." + }); + }); + + test("returns an error when a RelationshipAttribute does not have the queried key", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AnotherKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: sender.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not have the queried key." + }); + }); + + test("returns an error when a RelationshipAttribute does not have the queried confidentiality", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Protected + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Private, + owner: sender.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not have the queried confidentiality." + }); + }); + + test("returns an error when a RelationshipAttribute does not have the queried title", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + description: "ADescription", + confidentiality: RelationshipAttributeConfidentiality.Private + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Private, + owner: sender.toString(), + value: { + "@type": "ProprietaryString", + title: "AnotherTitle", + description: "ADescription", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not have the queried title." + }); + }); + + test("returns an error when a RelationshipAttribute does not have the queried description", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + description: "ADescription", + confidentiality: RelationshipAttributeConfidentiality.Private + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Private, + owner: sender.toString(), + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not have the queried description." + }); + }); + + test("returns an error when a RelationshipAttribute is not valid in the queried time frame", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: sender.toString(), + key: "AKey", + validFrom: "2024-02-14T08:47:35.077Z", + validTo: "2024-02-14T09:35:12.824Z", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: sender.toString(), + validFrom: "2024-02-14T08:47:35.077Z", + validTo: "2024-02-14T09:30:00.000Z", + value: { + "@type": "ProprietaryString", + title: "ATitle", + value: "AStringValue" + } + } + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not valid in the queried time frame." + }); + }); + }); + + describe("ThirdPartyRelationshipAttributeQuery", function () { + beforeEach(function () { + readProcessor = new ReadAttributeRequestItemProcessor(consumptionController); + }); + + test("returns an error when a RelationshipAttribute was queried using a ThirdPartyRelationshipAttributeQuery and the Recipient tries to respond with an IdentityAttribute", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: recipient + }) + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not a RelationshipAttribute, but a RelationshipAttribute was queried." + }); + }); + + test("returns an error when a RelationshipAttribute does not belong to the owner that was queried using a ThirdPartyRelationshipAttributeQuery", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: aThirdParty, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not belong to a queried owner." + }); + }); + + test("returns an error when a RelationshipAttribute that was queried by a ThirdPartyRelationshipAttributeQuery does not belong to the Recipient or one of the involved third parties, but an empty string was specified for the owner of the query", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Empty, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: anUninvolvedThirdParty, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: anUninvolvedThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: + "Neither you nor one of the involved third parties is the owner of the provided RelationshipAttribute, but an empty string was specified for the owner of the query." + }); + }); + + test("returns an error when a RelationshipAttribute does not have the key that was queried using a ThirdPartyRelationshipAttributeQuery", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AnotherKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided RelationshipAttribute does not have the queried key." + }); + }); + + test("returns an error when a RelationshipAttribute is not valid in the time frame that was queried using a ThirdPartyRelationshipAttributeQuery", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + validFrom: "2024-02-14T08:47:35.077Z", + validTo: "2024-02-14T09:35:12.824Z", + key: "AKey", + thirdParty: [aThirdParty.toString()] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const request = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const localAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "AKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + validFrom: CoreDate.from("2024-02-14T08:47:35.077Z"), + validTo: CoreDate.from("2024-02-14T09:30:00.000Z"), + value: ProprietaryString.from({ + title: "ATitle", + value: "AStringValue" + }) + }), + peer: aThirdParty, + requestReference: await ConsumptionIds.request.generate() + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: localAttribute.id.toString() + }; + + const result = await readProcessor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.attributeQueryMismatch", + message: "The provided Attribute is not valid in the queried time frame." + }); + }); + }); +}); diff --git a/packages/consumption/test/modules/requests/testHelpers/TestObjectFactory.ts b/packages/consumption/test/modules/requests/testHelpers/TestObjectFactory.ts index 4996073f5..ce8983a68 100644 --- a/packages/consumption/test/modules/requests/testHelpers/TestObjectFactory.ts +++ b/packages/consumption/test/modules/requests/testHelpers/TestObjectFactory.ts @@ -5,7 +5,6 @@ import { IdentityAttribute, IIdentityAttribute, IRelationshipAttribute, - IRelationshipCreationChangeRequestContent, IRelationshipTemplateContent, IRequest, IResponse, @@ -28,13 +27,10 @@ import { Identity, IMessage, IRelationship, - IRelationshipChange, IRelationshipTemplate, Message, - Realm, Relationship, - RelationshipChangeStatus, - RelationshipChangeType, + RelationshipAuditLogEntryReason, RelationshipStatus, RelationshipTemplate, RelationshipTemplatePublicKey @@ -43,36 +39,125 @@ import { ILocalRequest, LocalRequest, LocalRequestStatus, LocalRequestStatusLogE import { TestRequestItem } from "./TestRequestItem"; export class TestObjectFactory { - public static createRelationship(properties?: Partial): Relationship { + public static createPendingRelationship(properties?: Partial): Relationship { return Relationship.from({ id: properties?.id ?? CoreId.from("REL1"), peer: properties?.peer ?? Identity.from({ - address: CoreAddress.from("id1"), + address: CoreAddress.from("did:e:a-domain:dids:anidentity"), publicKey: CryptoSignaturePublicKey.from({ algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519, publicKey: CoreBuffer.from("L1sPFQgS5CxgGs1ejBcWCQLCpeFXbRc1TQnSpuHQqDQ") - }), - realm: Realm.Prod + }) + }), + status: properties?.status ?? RelationshipStatus.Pending, + relationshipSecretId: properties?.relationshipSecretId ?? CoreId.from("RELSEC1"), + cachedAt: properties?.cachedAt ?? CoreDate.from("2020-01-02T00:00:00.000Z"), + cache: + properties?.cache ?? + CachedRelationship.from({ + creationContent: {}, + auditLog: [ + { + createdAt: CoreDate.from("2020-01-01T00:00:00.000Z"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity2"), + createdByDevice: CoreId.from("DVC1"), + reason: RelationshipAuditLogEntryReason.Creation, + newStatus: RelationshipStatus.Pending + } + ], + template: this.createIncomingRelationshipTemplate() + }) + }); + } + + public static createActiveRelationship(properties?: Partial): Relationship { + return Relationship.from({ + id: properties?.id ?? CoreId.from("REL1"), + peer: + properties?.peer ?? + Identity.from({ + address: CoreAddress.from("did:e:a-domain:dids:anidentity"), + publicKey: CryptoSignaturePublicKey.from({ + algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519, + publicKey: CoreBuffer.from("L1sPFQgS5CxgGs1ejBcWCQLCpeFXbRc1TQnSpuHQqDQ") + }) }), status: properties?.status ?? RelationshipStatus.Active, relationshipSecretId: properties?.relationshipSecretId ?? CoreId.from("RELSEC1"), + cachedAt: properties?.cachedAt ?? CoreDate.from("2020-01-02T00:00:00.000Z"), + cache: + properties?.cache ?? + CachedRelationship.from({ + creationContent: {}, + auditLog: [ + { + createdAt: CoreDate.from("2020-01-01T00:00:00.000Z"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity2"), + createdByDevice: CoreId.from("DVC1"), + reason: RelationshipAuditLogEntryReason.Creation, + newStatus: RelationshipStatus.Pending + }, + + { + createdAt: CoreDate.from("2020-01-02T00:00:00.000Z"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity"), + createdByDevice: CoreId.from("DVC1"), + reason: RelationshipAuditLogEntryReason.AcceptanceOfCreation, + oldStatus: RelationshipStatus.Pending, + newStatus: RelationshipStatus.Active + } + ], + template: this.createIncomingRelationshipTemplate() + }) + }); + } + + public static createTerminatedRelationship(properties?: Partial): Relationship { + return Relationship.from({ + id: properties?.id ?? CoreId.from("REL1"), + peer: + properties?.peer ?? + Identity.from({ + address: CoreAddress.from("did:e:a-domain:dids:anidentity"), + publicKey: CryptoSignaturePublicKey.from({ + algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519, + publicKey: CoreBuffer.from("L1sPFQgS5CxgGs1ejBcWCQLCpeFXbRc1TQnSpuHQqDQ") + }) + }), + status: properties?.status ?? RelationshipStatus.Terminated, + relationshipSecretId: properties?.relationshipSecretId ?? CoreId.from("RELSEC1"), + cachedAt: properties?.cachedAt ?? CoreDate.from("2020-01-02T00:00:00.000Z"), cache: properties?.cache ?? CachedRelationship.from({ - changes: [ + creationContent: {}, + auditLog: [ + { + createdAt: CoreDate.from("2020-01-01T00:00:00.000Z"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity2"), + createdByDevice: CoreId.from("DVC1"), + reason: RelationshipAuditLogEntryReason.Creation, + newStatus: RelationshipStatus.Pending + }, + + { + createdAt: CoreDate.from("2020-01-02T00:00:00.000Z"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity"), + createdByDevice: CoreId.from("DVC1"), + reason: RelationshipAuditLogEntryReason.AcceptanceOfCreation, + oldStatus: RelationshipStatus.Pending, + newStatus: RelationshipStatus.Active + }, + { - id: CoreId.from("RELCH1"), - type: RelationshipChangeType.Creation, - status: RelationshipChangeStatus.Accepted, - relationshipId: CoreId.from("REL1"), - request: { - createdAt: CoreDate.from("2020-01-01T00:00:00.000Z"), - content: {}, - createdBy: CoreAddress.from("id1"), - createdByDevice: CoreId.from("DEV1") - } + createdAt: CoreDate.from("2020-01-03T00:00:00.000Z"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity"), + createdByDevice: CoreId.from("DVC1"), + reason: RelationshipAuditLogEntryReason.Termination, + oldStatus: RelationshipStatus.Active, + newStatus: RelationshipStatus.Terminated } ], template: this.createIncomingRelationshipTemplate() @@ -83,17 +168,17 @@ export class TestObjectFactory { public static createIdentityAttribute(properties?: Partial): IdentityAttribute { return IdentityAttribute.from({ value: properties?.value ?? GivenName.fromAny({ value: "AGivenName" }), - owner: properties?.owner ?? CoreAddress.from("id1") + owner: properties?.owner ?? CoreAddress.from("did:e:a-domain:dids:anidentity") }); } public static createRelationshipAttribute(properties?: Partial): RelationshipAttribute { return RelationshipAttribute.from({ - value: properties?.value ?? ProprietaryString.from({ title: "A Title", value: "AGivenName" }), + value: properties?.value ?? ProprietaryString.from({ title: "ATitle", value: "AProprietaryStringValue" }), confidentiality: RelationshipAttributeConfidentiality.Public, - key: "aKey", + key: "AKey", isTechnical: false, - owner: properties?.owner ?? CoreAddress.from("id1") + owner: properties?.owner ?? CoreAddress.from("did:e:a-domain:dids:anidentity") }); } @@ -109,7 +194,7 @@ export class TestObjectFactory { const requestJSON: ILocalRequest = { id: CoreId.from("REQ1"), isOwn: true, - peer: CoreAddress.from("id11"), + peer: CoreAddress.from("did:e:a-domain:dids:anidentity"), createdAt: CoreDate.from("2020-01-01T00:00:00.000Z"), content: TestObjectFactory.createRequestWithOneItem(params.contentProperties), source: { type: "Message", reference: CoreId.from("MSG1") }, @@ -141,8 +226,7 @@ export class TestObjectFactory { return Request.from({ items: [ RequestItemGroup.from({ - items: [TestRequestItem.from({ mustBeAccepted })], - mustBeAccepted: mustBeAccepted + items: [TestRequestItem.from({ mustBeAccepted })] }) ], ...properties @@ -207,7 +291,7 @@ export class TestObjectFactory { cache: { content: content, createdAt: creationDate, - createdBy: CoreAddress.from("id1"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity"), createdByDevice: { id: "senderDeviceId" }, receivedByEveryone: false, recipients: [ @@ -262,7 +346,7 @@ export class TestObjectFactory { receivedByEveryone: false, recipients: [ { - address: CoreAddress.from("id1"), + address: CoreAddress.from("did:e:a-domain:dids:anidentity"), encryptedKey: CryptoCipher.from({ cipher: CoreBuffer.fromUtf8("test"), algorithm: CryptoEncryptionAlgorithm.XCHACHA20_POLY1305, @@ -278,53 +362,6 @@ export class TestObjectFactory { return RelationshipTemplate.from(this.createIncomingIRelationshipTemplate()); } - public static createIncomingIRelationshipChange(type: RelationshipChangeType, requestId?: string): IRelationshipChange { - return { - // @ts-expect-error - "@type": "RelationshipChange", - id: CoreId.from("RCH1"), - relationshipId: CoreId.from("REL1"), - type: type, - status: RelationshipChangeStatus.Pending, - request: { - createdAt: CoreDate.utc(), - createdBy: CoreAddress.from("id1"), - createdByDevice: CoreId.from("DVC1"), - content: { - "@type": "RelationshipCreationChangeRequestContent", - response: { - "@type": "Response", - result: ResponseResult.Accepted, - items: [ - { - // @ts-expect-error - "@type": "AcceptResponseItem", - result: ResponseItemResult.Accepted - } - ], - requestId: CoreId.from(requestId ?? "REQ1") - } as IResponse - } as IRelationshipCreationChangeRequestContent - } - }; - } - - public static createOutgoingIRelationshipChange(type: RelationshipChangeType, sender: CoreAddress): IRelationshipChange { - return { - // @ts-expect-error - "@type": "RelationshipChange", - id: CoreId.from("RCH1"), - relationshipId: CoreId.from("REL1"), - type: type, - status: RelationshipChangeStatus.Pending, - request: { - createdAt: CoreDate.utc(), - createdBy: sender, - createdByDevice: CoreId.from("DVC1") - } - }; - } - public static createIncomingIRelationshipTemplate(): IRelationshipTemplate { return { // @ts-expect-error @@ -338,16 +375,15 @@ export class TestObjectFactory { cache: { content: {}, createdAt: CoreDate.utc(), - createdBy: CoreAddress.from("id1"), + createdBy: CoreAddress.from("did:e:a-domain:dids:anidentity"), createdByDevice: { id: "senderDeviceId" }, maxNumberOfAllocations: 1, identity: { - address: CoreAddress.from("id1"), + address: CoreAddress.from("did:e:a-domain:dids:anidentity"), publicKey: CryptoSignaturePublicKey.from({ algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519, publicKey: CoreBuffer.fromBase64URL("aS-A8ywidL00DfBlZySOG_1-NdSBW38uGD1il_Ymk5g") - }), - realm: Realm.Prod + }) }, templateKey: RelationshipTemplatePublicKey.from({ id: CoreId.from("b9uMR7u7lsKLzRfVJNYb"), @@ -362,7 +398,7 @@ export class TestObjectFactory { return RelationshipTemplate.from(this.createOutgoingIRelationshipTemplate(creator)); } - public static createOutgoingIRelationshipTemplate(creator: CoreAddress, content?: IRequest | IRelationshipTemplateContent): IRelationshipTemplate { + public static createOutgoingIRelationshipTemplate(creator: CoreAddress, content?: IRelationshipTemplateContent): IRelationshipTemplate { return { // @ts-expect-error "@type": "RelationshipTemplate", @@ -383,8 +419,7 @@ export class TestObjectFactory { publicKey: CryptoSignaturePublicKey.from({ algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519, publicKey: CoreBuffer.fromBase64URL("aS-A8ywidL00DfBlZySOG_1-NdSBW38uGD1il_Ymk5g") - }), - realm: Realm.Prod + }) }, templateKey: RelationshipTemplatePublicKey.from({ id: CoreId.from("b9uMR7u7lsKLzRfVJNYb"), diff --git a/packages/consumption/test/modules/requests/testHelpers/TestRequestItem.ts b/packages/consumption/test/modules/requests/testHelpers/TestRequestItem.ts index 5b83ab0d5..b67837188 100644 --- a/packages/consumption/test/modules/requests/testHelpers/TestRequestItem.ts +++ b/packages/consumption/test/modules/requests/testHelpers/TestRequestItem.ts @@ -1,5 +1,14 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; -import { IRequestItem, RequestItem } from "@nmshd/content"; +import { IRequestItem, RequestItem, RequestItemJSON } from "@nmshd/content"; + +export interface TestRequestItemJSON extends RequestItemJSON { + shouldFailAtCanAccept?: true; + shouldFailAtCanReject?: true; + shouldFailAtCanCreateOutgoingRequestItem?: true; + shouldFailAtCanApplyIncomingResponseItem?: true; + shouldThrowOnAccept?: true; + shouldThrowOnReject?: true; +} export interface ITestRequestItem extends IRequestItem { shouldFailAtCanAccept?: true; diff --git a/packages/content/package.json b/packages/content/package.json index 3e680eda1..a0735523d 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/content", - "version": "2.11.0", + "version": "5.0.0", "description": "The content library defines data structures that can be transmitted using the transport library.", "homepage": "https://enmeshed.eu", "repository": { diff --git a/packages/content/src/attributes/AttributeValueTypes.ts b/packages/content/src/attributes/AttributeValueTypes.ts index fc962d68d..e8c63c020 100644 --- a/packages/content/src/attributes/AttributeValueTypes.ts +++ b/packages/content/src/attributes/AttributeValueTypes.ts @@ -166,7 +166,7 @@ import { ZipCodeJSON } from "./types"; -// ################################################ Editable Identity Attribute Value Types ################################################################### +// ################################################ Editable IdentityAttribute Value Types ################################################################### export module AttributeValues { export module Identity { diff --git a/packages/content/src/attributes/RelationshipAttributeQuery.ts b/packages/content/src/attributes/RelationshipAttributeQuery.ts index 7bde74088..ee7c4cbb4 100644 --- a/packages/content/src/attributes/RelationshipAttributeQuery.ts +++ b/packages/content/src/attributes/RelationshipAttributeQuery.ts @@ -4,7 +4,7 @@ import { AbstractAttributeQuery, AbstractAttributeQueryJSON, IAbstractAttributeQ import { AttributeValues } from "./AttributeValueTypes"; import { IValueHints, ValueHints, ValueHintsJSON } from "./hints"; import { RelationshipAttributeConfidentiality } from "./RelationshipAttributeConfidentiality"; -import { PROPRIETARY_ATTRIBUTE_MAX_DESCRIPTION_LENGTH } from "./types/proprietary/ProprietaryAttributeValue"; +import { PROPRIETARY_ATTRIBUTE_MAX_DESCRIPTION_LENGTH, PROPRIETARY_ATTRIBUTE_MAX_TITLE_LENGTH } from "./types/proprietary/ProprietaryAttributeValue"; export interface RelationshipAttributeCreationHintsJSON { title: string; @@ -30,7 +30,7 @@ export interface IRelationshipAttributeCreationHints extends ISerializable { @type("RelationshipAttributeCreationHints") export class RelationshipAttributeCreationHints extends Serializable implements IRelationshipAttributeCreationHints { @serialize() - @validate({ max: 100 }) + @validate({ max: PROPRIETARY_ATTRIBUTE_MAX_TITLE_LENGTH }) public title: string; @serialize() diff --git a/packages/content/src/attributes/ThirdPartyRelationshipAttributeQuery.ts b/packages/content/src/attributes/ThirdPartyRelationshipAttributeQuery.ts index 6347800be..2e7dc748a 100644 --- a/packages/content/src/attributes/ThirdPartyRelationshipAttributeQuery.ts +++ b/packages/content/src/attributes/ThirdPartyRelationshipAttributeQuery.ts @@ -12,12 +12,18 @@ export interface ThirdPartyRelationshipAttributeQueryJSON extends AbstractAttrib export interface IThirdPartyRelationshipAttributeQuery extends IAbstractAttributeQuery { key: string; - owner: ICoreAddress; + owner: ThirdPartyRelationshipAttributeQueryOwner; thirdParty: ICoreAddress[]; validFrom?: ICoreDate; validTo?: ICoreDate; } +export enum ThirdPartyRelationshipAttributeQueryOwner { + ThirdParty = "thirdParty", + Recipient = "recipient", + Empty = "" +} + @type("ThirdPartyRelationshipAttributeQuery") export class ThirdPartyRelationshipAttributeQuery extends AbstractAttributeQuery implements IThirdPartyRelationshipAttributeQuery { @serialize() @@ -25,8 +31,13 @@ export class ThirdPartyRelationshipAttributeQuery extends AbstractAttributeQuery public key: string; @serialize() - @validate() - public owner: CoreAddress; + @validate({ + customValidator: (v) => + !Object.values(ThirdPartyRelationshipAttributeQueryOwner).includes(v) + ? `must be one of: ${Object.values(ThirdPartyRelationshipAttributeQueryOwner).map((o) => `"${o}"`)}` + : undefined + }) + public owner: ThirdPartyRelationshipAttributeQueryOwner; @serialize({ type: CoreAddress }) @validate({ customValidator: (v) => (v.length < 1 ? "may not be empty" : undefined) }) diff --git a/packages/content/src/attributes/types/strings/AbstractEMailAddress.ts b/packages/content/src/attributes/types/strings/AbstractEMailAddress.ts index db1858576..5781e0a87 100644 --- a/packages/content/src/attributes/types/strings/AbstractEMailAddress.ts +++ b/packages/content/src/attributes/types/strings/AbstractEMailAddress.ts @@ -19,7 +19,7 @@ export abstract class AbstractEMailAddress extends AbstractString { return super.valueHints.copyWith({ min: 3, max: 254, - pattern: String(AbstractEMailAddress.regExp) + pattern: AbstractEMailAddress.regExp.toString().slice(1, -1).replaceAll("/", "\\/") }); } diff --git a/packages/content/src/attributes/types/strings/AbstractURL.ts b/packages/content/src/attributes/types/strings/AbstractURL.ts index 5b7619fe7..5d37e5ea3 100644 --- a/packages/content/src/attributes/types/strings/AbstractURL.ts +++ b/packages/content/src/attributes/types/strings/AbstractURL.ts @@ -19,7 +19,7 @@ export abstract class AbstractURL extends AbstractString { return super.valueHints.copyWith({ min: 3, max: 1024, - pattern: String(AbstractURL.regExp) + pattern: AbstractURL.regExp.toString().slice(1, -1) }); } diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 3b72a10de..d77129ba9 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -1,7 +1,7 @@ export * from "./attributes"; export * from "./buildInformation"; export * from "./ContentJSON"; -export * from "./messages/Mail"; +export * from "./messages"; export * from "./notifications"; export * from "./relationships"; export * from "./requests"; diff --git a/packages/content/src/messages/ArbitraryMessageContent.ts b/packages/content/src/messages/ArbitraryMessageContent.ts new file mode 100644 index 000000000..3cc458eec --- /dev/null +++ b/packages/content/src/messages/ArbitraryMessageContent.ts @@ -0,0 +1,26 @@ +import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; +import { ContentJSON } from "../ContentJSON"; + +export interface ArbitraryMessageContentJSON extends ContentJSON { + "@type": "ArbitraryMessageContent"; + value: unknown; +} + +export interface IArbitraryMessageContent extends ISerializable { + value: unknown; +} + +@type("ArbitraryMessageContent") +export class ArbitraryMessageContent extends Serializable implements IArbitraryMessageContent { + @serialize({ any: true }) + @validate() + public value: unknown; + + public static from(value: IArbitraryMessageContent | Omit): ArbitraryMessageContent { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): ArbitraryMessageContentJSON { + return super.toJSON(verbose, serializeAsString) as ArbitraryMessageContentJSON; + } +} diff --git a/packages/content/src/messages/Mail.ts b/packages/content/src/messages/Mail.ts index 6f5ee37bd..f8337432a 100644 --- a/packages/content/src/messages/Mail.ts +++ b/packages/content/src/messages/Mail.ts @@ -9,6 +9,7 @@ export interface MailJSON extends ContentJSON { subject: string; body: string; } + export interface IMail extends ISerializable { to: ICoreAddress[]; cc?: ICoreAddress[]; diff --git a/packages/content/src/messages/index.ts b/packages/content/src/messages/index.ts new file mode 100644 index 000000000..c82ac67fa --- /dev/null +++ b/packages/content/src/messages/index.ts @@ -0,0 +1,2 @@ +export * from "./ArbitraryMessageContent"; +export * from "./Mail"; diff --git a/packages/content/src/notifications/Notification.ts b/packages/content/src/notifications/Notification.ts index be0e2a845..e08885411 100644 --- a/packages/content/src/notifications/Notification.ts +++ b/packages/content/src/notifications/Notification.ts @@ -35,4 +35,8 @@ export class Notification extends Serializable implements INotification { public static from(value: INotification): Notification { return this.fromAny(value); } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): NotificationJSON { + return super.toJSON(verbose, serializeAsString) as NotificationJSON; + } } diff --git a/packages/content/src/relationships/ArbitraryRelationshipCreationContent.ts b/packages/content/src/relationships/ArbitraryRelationshipCreationContent.ts new file mode 100644 index 000000000..ac85f154f --- /dev/null +++ b/packages/content/src/relationships/ArbitraryRelationshipCreationContent.ts @@ -0,0 +1,26 @@ +import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; +import { ContentJSON } from "../ContentJSON"; + +export interface ArbitraryRelationshipCreationContentJSON extends ContentJSON { + "@type": "ArbitraryRelationshipCreationContent"; + value: unknown; +} + +export interface IArbitraryRelationshipCreationContent extends ISerializable { + value: unknown; +} + +@type("ArbitraryRelationshipCreationContent") +export class ArbitraryRelationshipCreationContent extends Serializable implements IArbitraryRelationshipCreationContent { + @serialize({ any: true }) + @validate() + public value: unknown; + + public static from(value: IArbitraryRelationshipCreationContent | Omit): ArbitraryRelationshipCreationContent { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): ArbitraryRelationshipCreationContentJSON { + return super.toJSON(verbose, serializeAsString) as ArbitraryRelationshipCreationContentJSON; + } +} diff --git a/packages/content/src/relationships/ArbitraryRelationshipTemplateContent.ts b/packages/content/src/relationships/ArbitraryRelationshipTemplateContent.ts new file mode 100644 index 000000000..45b17e4c4 --- /dev/null +++ b/packages/content/src/relationships/ArbitraryRelationshipTemplateContent.ts @@ -0,0 +1,26 @@ +import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; +import { ContentJSON } from "../ContentJSON"; + +export interface ArbitraryRelationshipTemplateContentJSON extends ContentJSON { + "@type": "ArbitraryRelationshipTemplateContent"; + value: unknown; +} + +export interface IArbitraryRelationshipTemplateContent extends ISerializable { + value: unknown; +} + +@type("ArbitraryRelationshipTemplateContent") +export class ArbitraryRelationshipTemplateContent extends Serializable implements IArbitraryRelationshipTemplateContent { + @serialize({ any: true }) + @validate() + public value: unknown; + + public static from(value: IArbitraryRelationshipTemplateContent | Omit): ArbitraryRelationshipTemplateContent { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): ArbitraryRelationshipTemplateContentJSON { + return super.toJSON(verbose, serializeAsString) as ArbitraryRelationshipTemplateContentJSON; + } +} diff --git a/packages/content/src/relationships/RelationshipCreationChangeRequestContent.ts b/packages/content/src/relationships/RelationshipCreationChangeRequestContent.ts deleted file mode 100644 index ecfebd8d8..000000000 --- a/packages/content/src/relationships/RelationshipCreationChangeRequestContent.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; -import { ContentJSON } from "../ContentJSON"; -import { IResponse, Response, ResponseJSON } from "../requests/response/Response"; - -export interface RelationshipCreationChangeRequestContentJSON extends ContentJSON { - "@type": "RelationshipCreationChangeRequestContent"; - response: ResponseJSON; -} - -export interface IRelationshipCreationChangeRequestContent extends ISerializable { - response: IResponse; -} - -@type("RelationshipCreationChangeRequestContent") -export class RelationshipCreationChangeRequestContent extends Serializable implements IRelationshipCreationChangeRequestContent { - @serialize() - @validate() - public response: Response; - - public static from(value: IRelationshipCreationChangeRequestContent | Omit): RelationshipCreationChangeRequestContent { - return this.fromAny(value); - } - - public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): RelationshipCreationChangeRequestContentJSON { - return super.toJSON(verbose, serializeAsString) as RelationshipCreationChangeRequestContentJSON; - } -} diff --git a/packages/content/src/relationships/RelationshipCreationContent.ts b/packages/content/src/relationships/RelationshipCreationContent.ts new file mode 100644 index 000000000..a1872ae5f --- /dev/null +++ b/packages/content/src/relationships/RelationshipCreationContent.ts @@ -0,0 +1,27 @@ +import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; +import { ContentJSON } from "../ContentJSON"; +import { IResponse, Response, ResponseJSON } from "../requests/response/Response"; + +export interface RelationshipCreationContentJSON extends ContentJSON { + "@type": "RelationshipCreationContent"; + response: ResponseJSON; +} + +export interface IRelationshipCreationContent extends ISerializable { + response: IResponse; +} + +@type("RelationshipCreationContent") +export class RelationshipCreationContent extends Serializable implements IRelationshipCreationContent { + @serialize() + @validate() + public response: Response; + + public static from(value: IRelationshipCreationContent | Omit): RelationshipCreationContent { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): RelationshipCreationContentJSON { + return super.toJSON(verbose, serializeAsString) as RelationshipCreationContentJSON; + } +} diff --git a/packages/content/src/relationships/index.ts b/packages/content/src/relationships/index.ts index dcfeaed74..9820bd3b3 100644 --- a/packages/content/src/relationships/index.ts +++ b/packages/content/src/relationships/index.ts @@ -1,2 +1,4 @@ -export * from "./RelationshipCreationChangeRequestContent"; +export * from "./ArbitraryRelationshipCreationContent"; +export * from "./ArbitraryRelationshipTemplateContent"; +export * from "./RelationshipCreationContent"; export * from "./RelationshipTemplateContent"; diff --git a/packages/content/src/requests/RequestItem.ts b/packages/content/src/requests/RequestItem.ts index dc71ca65f..1f04ea89b 100644 --- a/packages/content/src/requests/RequestItem.ts +++ b/packages/content/src/requests/RequestItem.ts @@ -44,19 +44,14 @@ export interface RequestItemJSON extends ContentJSON { /** * This property can be used to add some arbitrary metadata to this item. The content * of this property will be copied into the response on the side of the recipient, so - * the sender can use it to identify the group content as they receive the response. + * the sender can use it to identify the item as they receive the response. */ metadata?: object; /** - * If set to `true`, the recipient has to accept this group if he wants to accept the + * If set to `true`, the recipient has to accept this item if they want to accept the * Request. - * If set to `false`, the recipient can decide whether he wants to accept it or not. - * - * Caution: this setting does not take effect in case it is inside of a - * {@link RequestItemGroupJSON RequestItemGroup}, which is not accepted by the recipient, - * since a {@link RequestItemJSON RequestItem} can only be accepted if the parent group - * is accepted as well. + * If set to `false`, the recipient can decide whether they want to accept it or not. */ mustBeAccepted: boolean; @@ -93,19 +88,14 @@ export interface IRequestItem extends ISerializable { /** * This property can be used to add some arbitrary metadata to this item. The content * of this property will be copied into the response on the side of the recipient, so - * the sender can use it to identify the group content as they receive the response. + * the sender can use it to identify the item as they receive the response. */ metadata?: object; /** - * If set to `true`, the recipient has to accept this group if he wants to accept the + * If set to `true`, the recipient has to accept this item if they want to accept the * Request. - * If set to `false`, the recipient can decide whether he wants to accept it or not. - * - * Caution: this setting does not take effect in case it is inside of a - * {@link RequestItemGroup RequestItemGroup}, which is not accepted by the recipient, - * since a {@link RequestItem RequestItem} can only be accepted if the parent group - * is accepted as well. + * If set to `false`, the recipient can decide whether they want to accept it or not. */ mustBeAccepted: boolean; @@ -165,3 +155,18 @@ export type RequestItemDerivations = | AuthenticationRequestItem | FreeTextRequestItem | RegisterAttributeListenerRequestItem; + +export function isRequestItemDerivation(input: any): input is RequestItemDerivations { + return ( + input["@type"] === "RequestItem" || + input["@type"] === "CreateAttributeRequestItem" || + input["@type"] === "DeleteAttributeRequestItem" || + input["@type"] === "ShareAttributeRequestItem" || + input["@type"] === "ProposeAttributeRequestItem" || + input["@type"] === "ReadAttributeRequestItem" || + input["@type"] === "ConsentRequestItem" || + input["@type"] === "AuthenticationRequestItem" || + input["@type"] === "FreeTextRequestItem" || + input["@type"] === "RegisterAttributeListenerRequestItem" + ); +} diff --git a/packages/content/src/requests/RequestItemGroup.ts b/packages/content/src/requests/RequestItemGroup.ts index 09863f90d..f1aee395b 100644 --- a/packages/content/src/requests/RequestItemGroup.ts +++ b/packages/content/src/requests/RequestItemGroup.ts @@ -1,17 +1,10 @@ -import { ISerializable, Serializable, serialize, type, validate, ValidationError } from "@js-soft/ts-serval"; -import { nameof } from "ts-simple-nameof"; +import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; import { ContentJSON } from "../ContentJSON"; import { IRequestItemDerivations, RequestItemDerivations, RequestItemJSONDerivations } from "./RequestItem"; /** * A RequestItemGroup can be used to group one or more RequestItems. This is useful - * if you want to - * * make sure that the items in the group can only be accepted together - * - * Example: when sending a `CreateRelationshipAttributeRequestItem` **and** a `ShareAttributeRequestItem` in a single - * Request where the latter one targets an attribute created by the first one, it it should be impossible to - * reject the first item, while accepting the second one. - * * visually group items on the UI and give the a common title/description + * if you want to visually group RequestItems on the UI and give them a common `title` or `description`. */ export interface RequestItemGroupJSON extends ContentJSON { "@type": "RequestItemGroup"; @@ -26,13 +19,6 @@ export interface RequestItemGroupJSON extends ContentJSON { */ description?: string; - /** - * If set to `true`, the recipient has to accept this group if he wants to accept the - * Request. - * If set to `false`, the recipient can decide whether he wants to accept it or not. - */ - mustBeAccepted: boolean; - /** * This property can be used to add some arbitrary metadata to this group. The content * of this property will be copied into the response on the side of the recipient, so @@ -48,13 +34,7 @@ export interface RequestItemGroupJSON extends ContentJSON { /** * A RequestItemGroup can be used to group one or more RequestItems. This is useful - * if you want to - * * make sure that the items in the group can only be accepted together - * - * Example: when sending a `CreateRelationshipAttributeRequestItem` **and** a `ShareAttributeRequestItem` in a single - * Request where the latter one targets an attribute created by the first one, it it should be impossible to - * reject the first item, while accepting the second one. - * * visually group items on the UI and give the a common title/description + * if you want to visually group RequestItems on the UI and give them a common `title` or `description`. */ export interface IRequestItemGroup extends ISerializable { /** @@ -67,13 +47,6 @@ export interface IRequestItemGroup extends ISerializable { */ description?: string; - /** - * If set to `true`, the recipient has to accept this group if he wants to accept the - * Request. - * If set to `false`, the recipient can decide whether he wants to accept it or not. - */ - mustBeAccepted: boolean; - /** * This property can be used to add some arbitrary metadata to this group. The content * of this property will be copied into the response on the side of the recipient, so @@ -97,10 +70,6 @@ export class RequestItemGroup extends Serializable { @validate({ nullable: true, max: 500 }) public description?: string; - @serialize() - @validate() - public mustBeAccepted: boolean; - @serialize() @validate({ customValidator: (v) => (v.length < 1 ? "may not be empty" : undefined) }) public items: RequestItemDerivations[]; @@ -113,20 +82,6 @@ export class RequestItemGroup extends Serializable { return this.fromAny(value); } - protected static override postFrom(value: T): T { - if (!(value instanceof RequestItemGroup)) throw new Error("this should never happen"); - - if (value.mustBeAccepted && value.items.every((item) => !item.mustBeAccepted)) { - throw new ValidationError( - RequestItemGroup.name, - nameof((x) => x.mustBeAccepted), - `${nameof((x) => x.mustBeAccepted)} can only be true if at least one item is flagged as ${nameof((x) => x.mustBeAccepted)}` - ); - } - - return value; - } - public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): RequestItemGroupJSON { return super.toJSON(verbose, serializeAsString) as RequestItemGroupJSON; } diff --git a/packages/content/src/requests/items/proposeAttribute/ProposeAttributeRequestItem.ts b/packages/content/src/requests/items/proposeAttribute/ProposeAttributeRequestItem.ts index 7f376b2bc..e71b80bf3 100644 --- a/packages/content/src/requests/items/proposeAttribute/ProposeAttributeRequestItem.ts +++ b/packages/content/src/requests/items/proposeAttribute/ProposeAttributeRequestItem.ts @@ -47,6 +47,23 @@ export class ProposeAttributeRequestItem extends RequestItem implements IPropose protected static override postFrom(value: T): T { if (!(value instanceof ProposeAttributeRequestItem)) throw new Error("this should never happen"); + // IQL queries might also be possible for Relationship Attributes in the future + if (value.attribute instanceof RelationshipAttribute && !(value.query instanceof RelationshipAttributeQuery)) { + throw new ValidationError( + ProposeAttributeRequestItem.name, + "", + "When proposing a RelationshipAttribute, the corresponding query has to be a RelationshipAttributeQuery." + ); + } + + if (value.attribute instanceof IdentityAttribute && !(value.query instanceof IdentityAttributeQuery || value.query instanceof IQLQuery)) { + throw new ValidationError( + ProposeAttributeRequestItem.name, + "", + "When proposing an IdentityAttribute, the corresponding query has to be a IdentityAttributeQuery or IQLQuery." + ); + } + if (value.query instanceof IdentityAttributeQuery) { const attributeValueType = (value.attribute.value.toJSON() as any)["@type"]; const queryValueType = value.query.valueType; @@ -60,21 +77,30 @@ export class ProposeAttributeRequestItem extends RequestItem implements IPropose } } - // IQL queries might also be possible for Relationship Attributes in the future - if (value.attribute instanceof RelationshipAttribute && !(value.query instanceof RelationshipAttributeQuery)) { - throw new ValidationError( - ProposeAttributeRequestItem.name, - "", - "When proposing a RelationshipAttribute, the corresponding query has to be a RelationshipAttributeQuery." - ); + if (value.query instanceof IQLQuery && typeof value.query.attributeCreationHints !== "undefined") { + const attributeValueType = (value.attribute.value.toJSON() as any)["@type"]; + const queryValueType = value.query.attributeCreationHints.valueType; + + if (attributeValueType !== queryValueType) { + throw new ValidationError( + ProposeAttributeRequestItem.name, + `${nameof((x) => x.query)}.${nameof((x) => x.attributeCreationHints!.valueType)}`, + `You cannot propose an Attribute whose type of the value ('${attributeValueType}') is different from the value type of the query ('${queryValueType}').` + ); + } } - if (value.attribute instanceof IdentityAttribute && !(value.query instanceof IdentityAttributeQuery || value.query instanceof IQLQuery)) { - throw new ValidationError( - ProposeAttributeRequestItem.name, - "", - "When proposing an IdentityAttribute, the corresponding query has to be a IdentityAttributeQuery or IQLQuery" - ); + if (value.query instanceof RelationshipAttributeQuery) { + const attributeValueType = (value.attribute.value.toJSON() as any)["@type"]; + const queryValueType = value.query.attributeCreationHints.valueType; + + if (attributeValueType !== queryValueType) { + throw new ValidationError( + ProposeAttributeRequestItem.name, + `${nameof((x) => x.query)}.${nameof((x) => x.attributeCreationHints.valueType)}`, + `You cannot propose an Attribute whose type of the value ('${attributeValueType}') is different from the value type of the query ('${queryValueType}').` + ); + } } return value; diff --git a/packages/content/test/attributeValues/StatementValueTest.test.ts b/packages/content/test/attributeValues/StatementValueTest.test.ts index abffbc453..fbb2e5d34 100644 --- a/packages/content/test/attributeValues/StatementValueTest.test.ts +++ b/packages/content/test/attributeValues/StatementValueTest.test.ts @@ -8,13 +8,13 @@ new GenericValueTest().runParametrized({ expectedJSON: { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: { value: "hasAttribute" }, object: { - address: "id1235", + address: "did:e:a-domain:dids:anidentity", attributes: [ { givenName: "Max", @@ -23,7 +23,7 @@ new GenericValueTest().runParametrized({ ] }, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", @@ -39,11 +39,11 @@ new GenericValueTest().runParametrized({ valueJSON: { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: "hasAttribute", object: { - address: "id1235", + address: "did:e:a-domain:dids:anidentity", attributes: [ { givenName: "Max", @@ -52,7 +52,7 @@ new GenericValueTest().runParametrized({ ] }, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", @@ -68,11 +68,11 @@ new GenericValueTest().runParametrized({ valueVerboseJSON: { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: "hasAttribute", object: { - address: "id1235", + address: "did:e:a-domain:dids:anidentity", attributes: [ { givenName: "Max", @@ -81,7 +81,7 @@ new GenericValueTest().runParametrized({ ] }, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", @@ -97,10 +97,10 @@ new GenericValueTest().runParametrized({ } }, valueInterface: { - subject: StatementSubject.fromAny({ address: "id1234" }), + subject: StatementSubject.fromAny({ address: "did:e:a-domain:dids:anidentity" }), predicate: StatementPredicate.fromAny("hasAttribute"), object: StatementObject.fromAny({ - address: "id1235", + address: "did:e:a-domain:dids:anidentity", attributes: [ { givenName: "Max", @@ -108,7 +108,7 @@ new GenericValueTest().runParametrized({ } ] }), - issuer: DigitalIdentityDescriptor.fromAny({ address: "id1236" }), + issuer: DigitalIdentityDescriptor.fromAny({ address: "did:e:a-domain:dids:anidentity" }), issuerConditions: StatementIssuerConditions.fromAny({ validTo: "2023-06-01T00:00:00.000Z", validFrom: "2023-06-01T00:00:00.000Z", @@ -130,16 +130,16 @@ new GenericValueTest().runParametrized({ expectedJSON: { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: { value: "z-isTeacher" }, object: { - address: "id1235" + address: "did:e:a-domain:dids:anidentity" }, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", @@ -155,14 +155,14 @@ new GenericValueTest().runParametrized({ valueJSON: { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: "z-isTeacher", object: { - address: "id1235" + address: "did:e:a-domain:dids:anidentity" }, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", @@ -178,14 +178,14 @@ new GenericValueTest().runParametrized({ valueVerboseJSON: { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: "z-isTeacher", object: { - address: "id1235" + address: "did:e:a-domain:dids:anidentity" }, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", @@ -201,10 +201,10 @@ new GenericValueTest().runParametrized({ } }, valueInterface: { - subject: StatementSubject.fromAny({ address: "id1234" }), + subject: StatementSubject.fromAny({ address: "did:e:a-domain:dids:anidentity" }), predicate: StatementPredicate.fromAny("z-isTeacher"), - object: StatementObject.fromAny({ address: "id1235" }), - issuer: DigitalIdentityDescriptor.fromAny({ address: "id1236" }), + object: StatementObject.fromAny({ address: "did:e:a-domain:dids:anidentity" }), + issuer: DigitalIdentityDescriptor.fromAny({ address: "did:e:a-domain:dids:anidentity" }), issuerConditions: StatementIssuerConditions.fromAny({ validTo: "2023-06-01T00:00:00.000Z", validFrom: "2023-06-01T00:00:00.000Z", @@ -224,22 +224,22 @@ describe("Statement negative Test", function () { describe("Statement set to 'hasPredicate'", function () { test.each([ { - address: "id1235", + address: "did:e:a-domain:dids:anidentity", attributes: [] }, { - address: "id1235" + address: "did:e:a-domain:dids:anidentity" } ])("should throw ValidationError for invalid input", function (value: any) { const invalidStatement = { "@type": "Statement", subject: { - address: "id1234" + address: "did:e:a-domain:dids:anidentity" }, predicate: "hasAttribute", object: value, issuer: { - address: "id1236" + address: "did:e:a-domain:dids:anidentity" }, issuerConditions: { validFrom: "2023-06-01T00:00:00.000Z", diff --git a/packages/content/test/attributes/BirthDate.test.ts b/packages/content/test/attributes/BirthDate.test.ts index 28b563bb2..d237f17bb 100644 --- a/packages/content/test/attributes/BirthDate.test.ts +++ b/packages/content/test/attributes/BirthDate.test.ts @@ -1,8 +1,8 @@ import { BirthDate, ValidationErrorWithoutProperty } from "@nmshd/content"; import { DateTime } from "luxon"; -describe("creation of RepositoryAttributes of Attribute Value Type BirthDate", () => { - test("can create a RepositoryAttribute of Attribute Value Type BirthDate", function () { +describe("creation of RepositoryAttributes of Attribute value type BirthDate", () => { + test("can create a RepositoryAttribute of Attribute value type BirthDate", function () { const validBirthDate = BirthDate.from({ day: 1, month: 12, year: 1990 }); expect(validBirthDate.constructor.name).toBe("BirthDate"); expect(validBirthDate.day.value).toBe(1); diff --git a/packages/content/test/attributes/EMailAddress.test.ts b/packages/content/test/attributes/EMailAddress.test.ts index ddf22e0bf..659ef24c9 100644 --- a/packages/content/test/attributes/EMailAddress.test.ts +++ b/packages/content/test/attributes/EMailAddress.test.ts @@ -27,6 +27,7 @@ describe("Test invalid EMailAddresses", () => { ) ); }); + test("returns an error when trying to create an Attribute Value Type EMailAddress wich is empty", function () { const invalidEMailAddressCall = () => { EMailAddress.from({ diff --git a/packages/content/test/attributes/RelationshipAttribute.test.ts b/packages/content/test/attributes/RelationshipAttribute.test.ts index 5b9f4bb5b..8d9fd824f 100644 --- a/packages/content/test/attributes/RelationshipAttribute.test.ts +++ b/packages/content/test/attributes/RelationshipAttribute.test.ts @@ -21,7 +21,7 @@ describe("RelationshipAttribute", function () { test("should create a RelationshipAttribute (isTechnical: true)", function () { const attribute = RelationshipAttribute.from({ - key: "aKey", + key: "AKey", value: attributeValue, owner: CoreAddress.from("address"), isTechnical: true, @@ -33,7 +33,7 @@ describe("RelationshipAttribute", function () { test("should create a RelationshipAttribute (isTechnical: false)", function () { const attribute = RelationshipAttribute.from({ - key: "aKey", + key: "AKey", value: attributeValue, owner: CoreAddress.from("address"), isTechnical: false, @@ -45,7 +45,7 @@ describe("RelationshipAttribute", function () { test("should create a RelationshipAttribute (isTechnical: undefined)", function () { const attribute = RelationshipAttribute.from({ - key: "aKey", + key: "AKey", value: attributeValue, owner: CoreAddress.from("address"), confidentiality: RelationshipAttributeConfidentiality.Public diff --git a/packages/content/test/attributes/ThirdPartyRelationshipAttributeQuery.test.ts b/packages/content/test/attributes/ThirdPartyRelationshipAttributeQuery.test.ts index ff8fe182e..51158322b 100644 --- a/packages/content/test/attributes/ThirdPartyRelationshipAttributeQuery.test.ts +++ b/packages/content/test/attributes/ThirdPartyRelationshipAttributeQuery.test.ts @@ -37,7 +37,7 @@ describe("ThirdPartyRelationshipAttributeQuery", function () { ])("accepts '$in' as thirdParty", function (params) { const serialized = ThirdPartyRelationshipAttributeQuery.from({ key: "test", - owner: "test", + owner: "thirdParty", // casting as any to test backwards compatibility thirdParty: params.in as unknown as any @@ -53,7 +53,7 @@ describe("ThirdPartyRelationshipAttributeQuery", function () { expect(() => { ThirdPartyRelationshipAttributeQuery.from({ key: "test", - owner: "test", + owner: "", thirdParty: thirdParty }); // eslint-disable-next-line jest/require-to-throw-message @@ -71,7 +71,7 @@ describe("ThirdPartyRelationshipAttributeQuery", function () { query: { "@type": "ThirdPartyRelationshipAttributeQuery", key: "test", - owner: "test", + owner: "thirdParty", // casting as any to test backwards compatibility thirdParty: params.in as unknown as any @@ -81,7 +81,7 @@ describe("ThirdPartyRelationshipAttributeQuery", function () { const json = test.toJSON(); expect(json.query).toStrictEqual({ key: "test", - owner: "test", + owner: "thirdParty", thirdParty: params.out }); } diff --git a/packages/content/test/attributes/Website.test.ts b/packages/content/test/attributes/Website.test.ts index 53fba0c46..a629b836e 100644 --- a/packages/content/test/attributes/Website.test.ts +++ b/packages/content/test/attributes/Website.test.ts @@ -34,6 +34,7 @@ describe("Test invalid URLs", () => { ) ); }); + test("returns an error when trying to create an Attribute Value Type Website wich is empty", function () { const invalidWebsiteCall = () => { Website.from({ diff --git a/packages/content/test/messages/Mail.test.ts b/packages/content/test/messages/Mail.test.ts index cdd6d5364..14717a295 100644 --- a/packages/content/test/messages/Mail.test.ts +++ b/packages/content/test/messages/Mail.test.ts @@ -5,7 +5,7 @@ describe("Mail", function () { test("should create a Mail from JSON", function () { const mail = Serializable.fromUnknown({ "@type": "Mail", - to: ["id1PNvUP4jHD74qo6usnWNoaFGFf33MXZi6c"], + to: ["did:e:a-domain:dids:anidentity"], cc: [], subject: "A Subject", body: "A Body" @@ -17,7 +17,7 @@ describe("Mail", function () { test("should create a Mail from JSON if cc is missing", function () { const mail = Serializable.fromUnknown({ "@type": "Mail", - to: ["id1PNvUP4jHD74qo6usnWNoaFGFf33MXZi6c"], + to: ["did:e:a-domain:dids:anidentity"], subject: "A Subject", body: "A Body" }); diff --git a/packages/content/test/requests/Request.test.ts b/packages/content/test/requests/Request.test.ts index 88f967c72..1934d5798 100644 --- a/packages/content/test/requests/Request.test.ts +++ b/packages/content/test/requests/Request.test.ts @@ -24,7 +24,6 @@ describe("Request", function () { } as TestRequestItemJSON, { "@type": "RequestItemGroup", - mustBeAccepted: true, items: [ { "@type": "TestRequestItem", @@ -60,7 +59,6 @@ describe("Request", function () { } as ITestRequestItem, { "@type": "RequestItemGroup", - mustBeAccepted: true, items: [ { "@type": "TestRequestItem", @@ -102,7 +100,6 @@ describe("Request", function () { } as TestRequestItemJSON, { "@type": "RequestItemGroup", - mustBeAccepted: true, title: "item group - title", description: "item group - description", metadata: { @@ -147,7 +144,6 @@ describe("Request", function () { items: [ { "@type": "RequestItemGroup", - mustBeAccepted: true, items: [] } as RequestItemGroupJSON ] @@ -168,37 +164,4 @@ describe("Request", function () { await expectThrowsAsync(() => Serializable.fromUnknown(requestJSON), "TestRequestItem.mustBeAccepted*Value is not defined"); }); - - test("should validate the RequestItemGroups mustBeAccepted flag inside a Request", async function () { - const requestJSON = { - "@type": "Request", - items: [ - { - "@type": "RequestItemGroup", - mustBeAccepted: true, - items: [ - { - "@type": "TestRequestItem", - mustBeAccepted: false - } - ] - } - ] - } as RequestJSON; - - await expectThrowsAsync(() => Serializable.fromUnknown(requestJSON), "mustBeAccepted can only be true if at least one item is flagged as mustBeAccepted"); - }); -}); - -describe("RequestItemGroup", function () { - test("should validate the RequestItemGroups mustBeAccepted flag", async function () { - await expectThrowsAsync( - () => - RequestItemGroup.from({ - mustBeAccepted: true, - items: [TestRequestItem.fromAny({ mustBeAccepted: false })] - }), - "mustBeAccepted can only be true if at least one item is flagged as mustBeAccepted" - ); - }); }); diff --git a/packages/content/test/requests/items/ProposeAttributeRequestItem.test.ts b/packages/content/test/requests/items/ProposeAttributeRequestItem.test.ts new file mode 100644 index 000000000..44b7a784a --- /dev/null +++ b/packages/content/test/requests/items/ProposeAttributeRequestItem.test.ts @@ -0,0 +1,158 @@ +import { ValidationError } from "@js-soft/ts-serval"; +import { TestObjectFactory } from "@nmshd/consumption/test/modules/requests/testHelpers/TestObjectFactory"; +import { IdentityAttributeQuery, IQLQuery, ProposeAttributeRequestItem, RelationshipAttributeConfidentiality, RelationshipAttributeQuery } from "@nmshd/content"; +import { CoreAddress } from "@nmshd/transport"; +import { nameof } from "ts-simple-nameof"; + +describe("creation of ProposeAttributeRequestItem", () => { + describe("creation of ProposeAttributeRequestItem with IdentityAttributeQuery", () => { + test("can create a ProposeAttributeRequestItem with IdentityAttributeQuery", function () { + const validProposeAttributeRequestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from("") + }), + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + + expect(validProposeAttributeRequestItem.constructor.name).toBe("ProposeAttributeRequestItem"); + expect(validProposeAttributeRequestItem.attribute.constructor.name).toBe("IdentityAttribute"); + expect(validProposeAttributeRequestItem.attribute.value.constructor.name).toBe("GivenName"); + expect(validProposeAttributeRequestItem.query.constructor.name).toBe("IdentityAttributeQuery"); + }); + + test("returns an error when trying to create an invalid ProposeAttributeRequestItem with IdentityAttributeQuery and RelationshipAttribute", function () { + const invalidProposeAttributeRequestItemCall = () => { + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createRelationshipAttribute({ + owner: CoreAddress.from("") + }), + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + }; + expect(invalidProposeAttributeRequestItemCall).toThrow( + new ValidationError(ProposeAttributeRequestItem.name, "", "When proposing a RelationshipAttribute, the corresponding query has to be a RelationshipAttributeQuery.") + ); + }); + + test("returns an error when trying to create an invalid ProposeAttributeRequestItem with mismatching IdentityAttribute value type", function () { + const invalidProposeAttributeRequestItemCall = () => { + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from("") + }), + query: IdentityAttributeQuery.from({ valueType: "DisplayName" }) + }); + }; + expect(invalidProposeAttributeRequestItemCall).toThrow( + new ValidationError( + ProposeAttributeRequestItem.name, + `${nameof((x) => x.query)}.${nameof((x) => x.valueType)}`, + "You cannot propose an Attribute whose type of the value ('GivenName') is different from the value type of the query ('DisplayName')." + ) + ); + }); + }); + + describe("creation of ProposeAttributeRequestItem with IQLQuery", () => { + test("returns an error when trying to create an invalid ProposeAttributeRequestItem with IQLQuery and mismatching IdentityAttribute value type", function () { + const invalidProposeAttributeRequestItemCall = () => { + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from("") + }), + query: IQLQuery.from({ queryString: "DisplayName", attributeCreationHints: { valueType: "DisplayName" } }) + }); + }; + expect(invalidProposeAttributeRequestItemCall).toThrow( + new ValidationError( + ProposeAttributeRequestItem.name, + `${nameof((x) => x.query)}.${nameof((x) => x.attributeCreationHints!.valueType)}`, + "You cannot propose an Attribute whose type of the value ('GivenName') is different from the value type of the query ('DisplayName')." + ) + ); + }); + }); + + describe("creation of ProposeAttributeRequestItem with RelationshipAttributeQuery", () => { + test("can create a ProposeAttributeRequestItem with RelationshipAttributeQuery", function () { + const validProposeAttributeRequestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createRelationshipAttribute({ + owner: CoreAddress.from("") + }), + query: RelationshipAttributeQuery.from({ + owner: "", + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + + expect(validProposeAttributeRequestItem.constructor.name).toBe("ProposeAttributeRequestItem"); + expect(validProposeAttributeRequestItem.attribute.constructor.name).toBe("RelationshipAttribute"); + expect(validProposeAttributeRequestItem.attribute.value.constructor.name).toBe("ProprietaryString"); + expect(validProposeAttributeRequestItem.query.constructor.name).toBe("RelationshipAttributeQuery"); + }); + + test("returns an error when trying to create an invalid ProposeAttributeRequestItem with RelationshipAttributeQuery and IdentityAttribute", function () { + const invalidProposeAttributeRequestItemCall = () => { + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from("") + }), + query: RelationshipAttributeQuery.from({ + owner: "", + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + }; + expect(invalidProposeAttributeRequestItemCall).toThrow( + new ValidationError( + ProposeAttributeRequestItem.name, + "", + "When proposing an IdentityAttribute, the corresponding query has to be a IdentityAttributeQuery or IQLQuery." + ) + ); + }); + + test("returns an error when trying to create an invalid ProposeAttributeRequestItem with mismatching RelationshipAttribute value type", function () { + const invalidProposeAttributeRequestItemCall = () => { + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: TestObjectFactory.createRelationshipAttribute({ + owner: CoreAddress.from("") + }), + query: RelationshipAttributeQuery.from({ + owner: "", + key: "AKey", + attributeCreationHints: { + valueType: "ProprietaryInteger", + title: "ATitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + }; + expect(invalidProposeAttributeRequestItemCall).toThrow( + new ValidationError( + ProposeAttributeRequestItem.name, + `${nameof((x) => x.query)}.${nameof((x) => x.attributeCreationHints.valueType)}`, + "You cannot propose an Attribute whose type of the value ('ProprietaryString') is different from the value type of the query ('ProprietaryInteger')." + ) + ); + }); + }); +}); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 002be8e88..f4bfdf8c9 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/runtime", - "version": "4.14.4", + "version": "5.0.0", "description": "The enmeshed client runtime.", "homepage": "https://enmeshed.eu", "repository": { @@ -65,10 +65,10 @@ "@js-soft/logging-abstractions": "^1.0.1", "@js-soft/ts-serval": "2.0.10", "@js-soft/ts-utils": "^2.3.3", - "@nmshd/consumption": "3.12.4", - "@nmshd/content": "2.11.0", + "@nmshd/consumption": "5.0.0", + "@nmshd/content": "5.0.0", "@nmshd/crypto": "2.0.6", - "@nmshd/transport": "2.8.2", + "@nmshd/transport": "5.0.0", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", diff --git a/packages/runtime/src/dataViews/DataViewExpander.ts b/packages/runtime/src/dataViews/DataViewExpander.ts index 588e977e2..b9b8f3443 100644 --- a/packages/runtime/src/dataViews/DataViewExpander.ts +++ b/packages/runtime/src/dataViews/DataViewExpander.ts @@ -14,10 +14,10 @@ import { FreeTextAcceptResponseItemJSON, FreeTextRequestItemJSON, GivenNameJSON, + IQLQueryJSON, IdentityAttribute, IdentityAttributeJSON, IdentityAttributeQueryJSON, - IQLQueryJSON, MailJSON, MiddleNameJSON, ProposeAttributeAcceptResponseItemJSON, @@ -48,17 +48,18 @@ import { SurnameJSON, ThirdPartyRelationshipAttributeQueryJSON, ValueHints, - ValueHintsJSON + ValueHintsJSON, + isRequestItemDerivation } from "@nmshd/content"; -import { CoreAddress, CoreId, IdentityController, Realm, Relationship, RelationshipStatus } from "@nmshd/transport"; +import { CoreAddress, CoreId, IdentityController } from "@nmshd/transport"; import _ from "lodash"; import { Inject } from "typescript-ioc"; import { AuthenticationRequestItemDVO, ConsentRequestItemDVO, CreateAttributeRequestItemDVO, - DeleteAttributeRequestItemDVO, DVOError, + DeleteAttributeRequestItemDVO, FileDVO, FreeTextRequestItemDVO, IdentityDVO, @@ -83,11 +84,13 @@ import { MessageDTO, MessageWithAttachmentsDTO, RecipientDTO, - RelationshipChangeDTO, RelationshipDTO, + RelationshipStatus, RelationshipTemplateDTO } from "../types"; import { RuntimeErrors } from "../useCases"; +import { DataViewObject } from "./DataViewObject"; +import { DataViewTranslateable } from "./DataViewTranslateable"; import { LocalAttributeDVO, LocalAttributeListenerDVO, @@ -97,8 +100,8 @@ import { PeerAttributeDVO, PeerRelationshipAttributeDVO, ProcessedAttributeQueryDVO, - ProcessedIdentityAttributeQueryDVO, ProcessedIQLQueryDVO, + ProcessedIdentityAttributeQueryDVO, ProcessedRelationshipAttributeQueryDVO, ProcessedThirdPartyRelationshipAttributeQueryDVO, RelationshipSettingDVO, @@ -121,8 +124,8 @@ import { AttributeQueryDVO, DraftIdentityAttributeDVO, DraftRelationshipAttributeDVO, - IdentityAttributeQueryDVO, IQLQueryDVO, + IdentityAttributeQueryDVO, RelationshipAttributeQueryDVO, ThirdPartyRelationshipAttributeQueryDVO } from "./content/AttributeDVOs"; @@ -143,10 +146,8 @@ import { ResponseItemGroupDVO, ShareAttributeAcceptResponseItemDVO } from "./content/ResponseItemDVOs"; -import { DataViewObject } from "./DataViewObject"; -import { DataViewTranslateable } from "./DataViewTranslateable"; import { MessageDVO, MessageStatus, RecipientDVO } from "./transport/MessageDVO"; -import { RelationshipChangeDVO, RelationshipChangeResponseDVO, RelationshipDirection, RelationshipDVO } from "./transport/RelationshipDVO"; +import { RelationshipDVO, RelationshipDirection } from "./transport/RelationshipDVO"; export class DataViewExpander { public constructor( @@ -286,7 +287,7 @@ export class DataViewExpander { wasReadAt: message.wasReadAt }; - if (message.content["@type"] === "Mail" || message.content["@type"] === "RequestMail") { + if (message.content["@type"] === "Mail") { const mailContent = message.content as MailJSON; const to: RecipientDVO[] = mailContent.to.map((value) => addressMap[value]); @@ -345,7 +346,7 @@ export class DataViewExpander { return requestMessageDVO; } - if (message.content["@type"] === "Response") { + if (message.content["@type"] === "ResponseWrapper") { let localRequest: LocalRequestDTO; if (isOwn) { const localRequestsResult = await this.consumption.incomingRequests.getRequests({ @@ -803,10 +804,14 @@ export class DataViewExpander { isDecidable, title: requestGroupOrItem.title, description: requestGroupOrItem.description, - mustBeAccepted: requestGroupOrItem.mustBeAccepted, response: responseGroup }; } + + if (!isRequestItemDerivation(requestGroupOrItem)) { + throw new Error("A derivation of a RequestItem was expected."); + } + return await this.expandRequestItem(requestGroupOrItem, localRequestDTO, responseGroupOrItemDVO as ResponseItemDVO); } @@ -958,7 +963,7 @@ export class DataViewExpander { public async expandLocalAttributeListenerDTO(attributeListener: LocalAttributeListenerDTO): Promise { const query = (await this.expandAttributeQuery(attributeListener.query)) as IdentityAttributeQueryDVO | ThirdPartyRelationshipAttributeQueryDVO; - const peer = await this.expandIdentityForAddress(attributeListener.peer); + const peer = await this.expandAddress(attributeListener.peer); return { type: "LocalAttributeListenerDVO", name: "dvo.localAttributeListener.name", @@ -1211,7 +1216,8 @@ export class DataViewExpander { isDraft: false, sharedWith: sharedToPeerDVOs as SharedToPeerAttributeDVO[], tags: identityAttribute.tags, - valueType + valueType, + isDefault: attribute.isDefault }; } @@ -1394,12 +1400,17 @@ export class DataViewExpander { const matchedAttributeDTOs = await this.consumption.attributes.executeIdentityAttributeQuery({ query }); - const matchedAttributeDVOs = await this.expandLocalAttributeDTOs(matchedAttributeDTOs.value); + if (matchedAttributeDTOs.isError) throw matchedAttributeDTOs.error; + + const matchedAttributeDTOsSortedByDefaultFirst = matchedAttributeDTOs.value.sort((attribute1, attribute2) => + attribute1.isDefault === attribute2.isDefault ? 0 : attribute1.isDefault ? -1 : 1 + ); + const matchedAttributeDVOs = await this.expandLocalAttributeDTOs(matchedAttributeDTOsSortedByDefaultFirst); return { ...this.expandIdentityAttributeQuery(query), type: "ProcessedIdentityAttributeQueryDVO", - results: matchedAttributeDVOs as (RepositoryAttributeDVO | SharedToPeerAttributeDVO)[], + results: matchedAttributeDVOs as RepositoryAttributeDVO[], isProcessed: true }; } @@ -1559,7 +1570,6 @@ export class DataViewExpander { type: "IdentityDVO", name: name, initials: initials, - realm: Realm.Prod, description: "i18n://dvo.identity.self.description", isSelf: true, hasRelationship: false @@ -1567,16 +1577,13 @@ export class DataViewExpander { } public expandUnknown(address: string): IdentityDVO { - const name = address.substring(3, 9); - const initials = (name.match(/\b\w/g) ?? []).join(""); - return { id: address, type: "IdentityDVO", - name: name, - initials: initials, - realm: Realm.Prod, + name: "i18n://dvo.identity.unknown", + initials: "", description: "i18n://dvo.identity.unknown.description", + publicKey: "i18n://dvo.identity.publicKey.unknown", isSelf: false, hasRelationship: false }; @@ -1588,7 +1595,8 @@ export class DataViewExpander { } const result = await this.transport.relationships.getRelationshipByAddress({ address }); - if (result.isSuccess) { + // revoked relationships should be treated as non-existent as they will never have attributes attached + if (result.isSuccess && result.value.status !== RelationshipStatus.Rejected && result.value.status !== RelationshipStatus.Revoked) { return await this.expandRelationshipDTO(result.value); } @@ -1601,7 +1609,8 @@ export class DataViewExpander { }) ).value; if (requestResult.length > 0) { - return this.expandAddressFromRequest(requestResult[0]); // with no relationship max 1 request available + // with no relationship max 1 request available + return this.expandAddressFromRequest(requestResult[0]); } return this.expandUnknown(address); @@ -1627,60 +1636,16 @@ export class DataViewExpander { return await Promise.all(relationshipPromises); } - public expandRelationshipChangeDTO(relationship: RelationshipDTO, change: RelationshipChangeDTO): Promise { - const date = change.response ? change.response.createdAt : change.request.createdAt; - let isOwn = false; - if (this.identityController.isMe(CoreAddress.from(change.request.createdBy))) { - isOwn = true; - } - - let response: RelationshipChangeResponseDVO | undefined; - if (change.response) { - response = { - ...change.response, - id: `${change.id}_response`, - name: "i18n://dvo.relationshipChange.response.name", - type: "RelationshipChangeResponseDVO" - }; - } - - return Promise.resolve({ - type: "RelationshipChangeDVO", - id: change.id, - name: "", - date: date, - status: change.status, - statusText: `i18n://dvo.relationshipChange.${change.status}`, - changeType: change.type, - changeTypeText: `i18n://dvo.relationshipChange.${change.type}`, - isOwn: isOwn, - request: { - ...change.request, - id: `${change.id}_request`, - name: "i18n://dvo.relationshipChange.request.name", - type: "RelationshipChangeRequestDVO" - }, - response: response - }); - } - - public async expandRelationshipChangeDTOs(relationship: RelationshipDTO): Promise { - const changePromises = relationship.changes.map((change) => this.expandRelationshipChangeDTO(relationship, change)); - return await Promise.all(changePromises); - } - private expandAddressFromRequest(request: LocalRequestDTO): IdentityDVO { const sharedAttributesOnNewRelationship = this.getSharedAttributesFromRequest(request); const address = request.peer; - const name = this.getNameFromAttributeContents(sharedAttributesOnNewRelationship) ?? address.substring(3, 9); - const initials = (name.match(/\b\w/g) ?? []).join(""); + const name = this.getNameFromAttributeContents(sharedAttributesOnNewRelationship); return { type: "IdentityDVO", id: address, - name: name, - realm: Realm.Prod, - initials, + name: name ?? "i18n://dvo.identity.unknown", + initials: name ? (name.match(/\b\w/g) ?? []).join("") : "", description: "i18n://dvo.identity.unknown.description", isSelf: false, hasRelationship: false @@ -1768,24 +1733,34 @@ export class DataViewExpander { } let direction = RelationshipDirection.Incoming; - if (this.identityController.isMe(CoreAddress.from(relationship.changes[0].request.createdBy))) { + if (!relationship.template.isOwn) { direction = RelationshipDirection.Outgoing; } let statusText = ""; - if (relationship.status === RelationshipStatus.Pending && direction === RelationshipDirection.Outgoing) { - statusText = DataViewTranslateable.transport.relationshipOutgoing; - } else if (relationship.status === RelationshipStatus.Pending) { - statusText = DataViewTranslateable.transport.relationshipIncoming; - } else if (relationship.status === RelationshipStatus.Rejected) { - statusText = DataViewTranslateable.transport.relationshipRejected; - } else if (relationship.status === RelationshipStatus.Revoked) { - statusText = DataViewTranslateable.transport.relationshipRevoked; - } else if (relationship.status === RelationshipStatus.Active) { - statusText = DataViewTranslateable.transport.relationshipActive; + switch (relationship.status) { + case RelationshipStatus.Pending: + statusText = + direction === RelationshipDirection.Outgoing ? DataViewTranslateable.transport.relationshipOutgoing : DataViewTranslateable.transport.relationshipIncoming; + break; + case RelationshipStatus.Rejected: + statusText = DataViewTranslateable.transport.relationshipRejected; + break; + case RelationshipStatus.Revoked: + statusText = DataViewTranslateable.transport.relationshipRevoked; + break; + case RelationshipStatus.Active: + statusText = DataViewTranslateable.transport.relationshipActive; + break; + case RelationshipStatus.Terminated: + statusText = DataViewTranslateable.transport.relationshipTerminated; + break; + case RelationshipStatus.DeletionProposed: + statusText = DataViewTranslateable.transport.relationshipDeletionProposed; + break; } - const changes = await this.expandRelationshipChangeDTOs(relationship); + const creationDate = relationship.auditLog[0].createdAt; let name; if (stringByType["DisplayName"]) { @@ -1799,14 +1774,14 @@ export class DataViewExpander { } else if (stringByType["Surname"]) { name = `${stringByType["Surname"]}`; } else { - name = relationship.peer.substring(3, 9); + name = "i18n://dvo.identity.unknown"; } return { id: relationship.id, name: relationshipSetting.userTitle ?? name, description: relationshipSetting.userDescription ?? statusText, - date: relationship.changes[0].request.createdAt, + date: creationDate, image: "", type: "RelationshipDVO", status: relationship.status, @@ -1816,9 +1791,9 @@ export class DataViewExpander { attributeMap: attributesByType, items: expandedAttributes, nameMap: stringByType, - changes: changes, - changeCount: changes.length, - templateId: relationship.template.id + templateId: relationship.template.id, + auditLog: relationship.auditLog, + creationContent: relationship.creationContent }; } @@ -1833,7 +1808,6 @@ export class DataViewExpander { date: relationshipDVO.date, description: relationshipDVO.description, publicKey: relationship.peerIdentity.publicKey, - realm: relationship.peerIdentity.realm, initials, isSelf: false, hasRelationship: true, @@ -1842,38 +1816,8 @@ export class DataViewExpander { }; } - public async expandIdentityForAddress(address: string): Promise { - if (address === this.identityController.address.toString()) { - return this.expandSelf(); - } - - const relationshipResult = await this.transport.relationships.getRelationshipByAddress({ - address: address - }); - if (relationshipResult.isSuccess) { - return await this.expandRelationshipDTO(relationshipResult.value); - } - - if (relationshipResult.error.code !== RuntimeErrors.general.recordNotFound(Relationship).code) throw relationshipResult.error; - - const name = address.substring(3, 9); - const initials = (name.match(/\b\w/g) ?? []).join(""); - - return { - id: address, - type: "IdentityDVO", - name: name, - initials: initials, - publicKey: "i18n://dvo.identity.publicKey.unknown", - realm: this.identityController.realm.toString(), - description: "i18n://dvo.identity.unknown", - isSelf: false, - hasRelationship: false - }; - } - public async expandIdentityDTO(identity: IdentityDTO): Promise { - return await this.expandIdentityForAddress(identity.address); + return await this.expandAddress(identity.address); } public async expandRelationshipDTOs(relationships: RelationshipDTO[]): Promise { diff --git a/packages/runtime/src/dataViews/DataViewTranslateable.ts b/packages/runtime/src/dataViews/DataViewTranslateable.ts index e00ef661c..0afd35491 100644 --- a/packages/runtime/src/dataViews/DataViewTranslateable.ts +++ b/packages/runtime/src/dataViews/DataViewTranslateable.ts @@ -7,31 +7,18 @@ export class DataViewTranslateable { relationshipRejected: `${DataViewTranslateable.prefix}relationship.Rejected`, relationshipRevoked: `${DataViewTranslateable.prefix}relationship.Revoked`, relationshipActive: `${DataViewTranslateable.prefix}relationship.Active`, + relationshipTerminated: `${DataViewTranslateable.prefix}relationship.Terminated`, + relationshipDeletionProposed: `${DataViewTranslateable.prefix}relationship.DeletionProposed`, fileName: `${DataViewTranslateable.prefix}file.name` }; public static readonly consumption = { mails: { - mailSubjectFallback: `${DataViewTranslateable.prefix}mails.mailSubjectFallback`, - requestMailSubjectFallback: `${DataViewTranslateable.prefix}mails.requestMailSubjectFallback` + mailSubjectFallback: `${DataViewTranslateable.prefix}mails.mailSubjectFallback` }, attributes: { unknownAttributeName: `${DataViewTranslateable.prefix}attributes.UnknownAttributeName` }, - requests: { - // Request for sharing an attribute - attributesShareRequestName: `${DataViewTranslateable.prefix}requests.AttributesShareRequest.name`, - // Request for sharing multiple attributes - attributesShareRequestNamePlural: `${DataViewTranslateable.prefix}requests.AttributesShareRequest.namePlural`, - // You don't have a relationship to any of the recipients of this request - attributesShareRequestNoRelationship: `${DataViewTranslateable.prefix}requests.AttributesShareRequest.noRelationship`, - // The data is only shared with known recipients - attributesShareRequestOnlyRelationships: `${DataViewTranslateable.prefix}requests.AttributesShareRequest.onlyRelationships`, - // Request for changing an attribute - attributesChangeRequestName: `${DataViewTranslateable.prefix}requests.AttributesChangeRequest.name`, - // Request for changing multiple attributes - attributesChangeRequestNamePlural: `${DataViewTranslateable.prefix}requests.AttributesChangeRequest.namePlural` - }, identities: { self: `${DataViewTranslateable.prefix}identities.self.name` } diff --git a/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts b/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts index 454295d97..88f10a623 100644 --- a/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts +++ b/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts @@ -1,6 +1,6 @@ import { IdentityAttributeJSON, RelationshipAttributeCreationHintsJSON, RelationshipAttributeJSON, RenderHintsJSON, ValueHintsJSON } from "@nmshd/content"; -import { AttributeQueryDVO } from "../content/AttributeDVOs"; import { DataViewObject } from "../DataViewObject"; +import { AttributeQueryDVO } from "../content/AttributeDVOs"; import { IdentityDVO } from "../transport"; /** @@ -34,6 +34,7 @@ export interface RepositoryAttributeDVO extends LocalAttributeDVO { sharedWith: SharedToPeerAttributeDVO[]; isOwn: true; tags?: string[]; + isDefault?: true; } /** diff --git a/packages/runtime/src/dataViews/consumption/LocalRequestDVO.ts b/packages/runtime/src/dataViews/consumption/LocalRequestDVO.ts index 2d0893632..25a4c990c 100644 --- a/packages/runtime/src/dataViews/consumption/LocalRequestDVO.ts +++ b/packages/runtime/src/dataViews/consumption/LocalRequestDVO.ts @@ -32,6 +32,6 @@ export interface LocalResponseDVO extends DataViewObject { } export interface LocalResponseSourceDVO { - type: "Message" | "RelationshipChange"; + type: "Message" | "Relationship"; reference: string; } diff --git a/packages/runtime/src/dataViews/content/RequestItemDVOs.ts b/packages/runtime/src/dataViews/content/RequestItemDVOs.ts index 868a394ee..c4bf4fed1 100644 --- a/packages/runtime/src/dataViews/content/RequestItemDVOs.ts +++ b/packages/runtime/src/dataViews/content/RequestItemDVOs.ts @@ -9,7 +9,6 @@ export interface RequestItemGroupDVO { title?: string; description?: string; isDecidable: boolean; - mustBeAccepted: boolean; response?: ResponseItemGroupDVO; } diff --git a/packages/runtime/src/dataViews/transport/IdentityDVO.ts b/packages/runtime/src/dataViews/transport/IdentityDVO.ts index ce624be38..a42f6c112 100644 --- a/packages/runtime/src/dataViews/transport/IdentityDVO.ts +++ b/packages/runtime/src/dataViews/transport/IdentityDVO.ts @@ -5,7 +5,6 @@ export interface IdentityDVO extends DataViewObject { type: "IdentityDVO"; publicKey?: string; - realm: string; initials: string; isSelf: boolean; hasRelationship: boolean; diff --git a/packages/runtime/src/dataViews/transport/MessageDVO.ts b/packages/runtime/src/dataViews/transport/MessageDVO.ts index 5d1bcd03c..ce720db02 100644 --- a/packages/runtime/src/dataViews/transport/MessageDVO.ts +++ b/packages/runtime/src/dataViews/transport/MessageDVO.ts @@ -1,3 +1,4 @@ +import { MessageContentDerivation } from "../../types/transport/MessageDTO"; import { DataViewObject } from "../DataViewObject"; import { FileDVO } from "./FileDVO"; import { IdentityDVO } from "./IdentityDVO"; @@ -82,7 +83,7 @@ export interface MessageDVO extends DataViewObject { /** * The content of the message. */ - content: unknown; + content: MessageContentDerivation; /** * The read indicator of the message diff --git a/packages/runtime/src/dataViews/transport/RelationshipDVO.ts b/packages/runtime/src/dataViews/transport/RelationshipDVO.ts index cbbd6a36e..11ada7bfd 100644 --- a/packages/runtime/src/dataViews/transport/RelationshipDVO.ts +++ b/packages/runtime/src/dataViews/transport/RelationshipDVO.ts @@ -1,6 +1,6 @@ -import { RelationshipChangeStatus, RelationshipChangeType } from "@nmshd/transport"; -import { LocalAttributeDVO } from "../consumption"; +import { RelationshipAuditLogDTO, RelationshipCreationContentDerivation } from "../../types/transport/RelationshipDTO"; import { DataViewObject } from "../DataViewObject"; +import { LocalAttributeDVO } from "../consumption"; export enum RelationshipDirection { Outgoing = "Outgoing", @@ -14,8 +14,8 @@ export interface RelationshipDVO extends DataViewObject { statusText: string; isPinned: boolean; theme?: RelationshipTheme; - changes: RelationshipChangeDVO[]; - changeCount: number; + creationContent: RelationshipCreationContentDerivation; + auditLog: RelationshipAuditLogDTO; items: LocalAttributeDVO[]; attributeMap: Record; nameMap: Record; @@ -28,30 +28,3 @@ export interface RelationshipTheme { backgroundColor?: string; foregroundColor?: string; } - -export interface RelationshipChangeDVO extends DataViewObject { - type: "RelationshipChangeDVO"; - request: RelationshipChangeRequestDVO; - response?: RelationshipChangeResponseDVO; - status: RelationshipChangeStatus; - statusText: string; - changeType: RelationshipChangeType; - changeTypeText: string; - isOwn: boolean; -} - -export interface RelationshipChangeRequestDVO extends DataViewObject { - type: "RelationshipChangeRequestDVO"; - createdBy: string; - createdByDevice: string; - createdAt: string; - content?: unknown; -} - -export interface RelationshipChangeResponseDVO extends DataViewObject { - type: "RelationshipChangeResponseDVO"; - createdBy: string; - createdByDevice: string; - createdAt: string; - content?: unknown; -} diff --git a/packages/runtime/src/dataViews/transport/RelationshipTemplateDVO.ts b/packages/runtime/src/dataViews/transport/RelationshipTemplateDVO.ts index 3bc29f06a..7618753b6 100644 --- a/packages/runtime/src/dataViews/transport/RelationshipTemplateDVO.ts +++ b/packages/runtime/src/dataViews/transport/RelationshipTemplateDVO.ts @@ -1,6 +1,7 @@ +import { RelationshipTemplateContentDerivation } from "../../types/transport/RelationshipTemplateDTO"; +import { DataViewObject } from "../DataViewObject"; import { LocalRequestDVO } from "../consumption/LocalRequestDVO"; import { RequestDVO } from "../content"; -import { DataViewObject } from "../DataViewObject"; import { IdentityDVO } from "./IdentityDVO"; export interface RelationshipTemplateDVO extends DataViewObject { @@ -20,5 +21,5 @@ export interface RelationshipTemplateDVO extends DataViewObject { request?: LocalRequestDVO; - content: unknown; + content: RelationshipTemplateContentDerivation; } diff --git a/packages/runtime/src/events/EventProxy.ts b/packages/runtime/src/events/EventProxy.ts index e9e356784..f9304544d 100644 --- a/packages/runtime/src/events/EventProxy.ts +++ b/packages/runtime/src/events/EventProxy.ts @@ -10,7 +10,7 @@ import { IncomingRequestStatusChangedEvent, OutgoingRequestCreatedAndCompletedEvent, OutgoingRequestCreatedEvent, - OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent, + OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent, OutgoingRequestStatusChangedEvent, OwnSharedAttributeDeletedByOwnerEvent, OwnSharedAttributeSucceededEvent, @@ -27,7 +27,10 @@ import { MessageSentEvent, MessageWasReadAtChangedEvent, PeerRelationshipTemplateLoadedEvent, - RelationshipChangedEvent + RelationshipChangedEvent, + RelationshipDecomposedBySelfEvent, + RelationshipReactivationCompletedEvent, + RelationshipReactivationRequestedEvent } from "./transport"; export class EventProxy { @@ -71,6 +74,18 @@ export class EventProxy { this.targetEventBus.publish(new RelationshipChangedEvent(event.eventTargetAddress, RelationshipMapper.toRelationshipDTO(event.data))); }); + this.subscribeToSourceEvent(transport.RelationshipReactivationRequestedEvent, (event) => { + this.targetEventBus.publish(new RelationshipReactivationRequestedEvent(event.eventTargetAddress, RelationshipMapper.toRelationshipDTO(event.data))); + }); + + this.subscribeToSourceEvent(transport.RelationshipReactivationCompletedEvent, (event) => { + this.targetEventBus.publish(new RelationshipReactivationCompletedEvent(event.eventTargetAddress, RelationshipMapper.toRelationshipDTO(event.data))); + }); + + this.subscribeToSourceEvent(transport.RelationshipDecomposedBySelfEvent, (event) => { + this.targetEventBus.publish(new RelationshipDecomposedBySelfEvent(event.eventTargetAddress, { relationshipId: event.data.relationshipId.toString() })); + }); + this.subscribeToSourceEvent(transport.IdentityDeletionProcessStatusChangedEvent, (event) => { this.targetEventBus.publish( new IdentityDeletionProcessStatusChangedEvent(event.eventTargetAddress, IdentityDeletionProcessMapper.toIdentityDeletionProcessDTO(event.data)) @@ -158,8 +173,8 @@ export class EventProxy { this.targetEventBus.publish(new OutgoingRequestCreatedAndCompletedEvent(event.eventTargetAddress, mappedRequest)); - if (event.data.response?.source?.type === "RelationshipChange") { - this.targetEventBus.publish(new OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent(event.eventTargetAddress, mappedRequest)); + if (event.data.response?.source?.type === "Relationship") { + this.targetEventBus.publish(new OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent(event.eventTargetAddress, mappedRequest)); } }); diff --git a/packages/runtime/src/events/consumption/OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent.ts b/packages/runtime/src/events/consumption/OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent.ts similarity index 52% rename from packages/runtime/src/events/consumption/OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent.ts rename to packages/runtime/src/events/consumption/OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent.ts index 05d5e99e7..461fe66c9 100644 --- a/packages/runtime/src/events/consumption/OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent.ts +++ b/packages/runtime/src/events/consumption/OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent.ts @@ -1,11 +1,11 @@ import { LocalRequestDTO } from "../../types"; import { DataEvent } from "../DataEvent"; -export class OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent extends DataEvent { - public static readonly namespace = "consumption.outgoingRequestFromRelationshipCreationChangeCreatedAndCompleted"; +export class OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent extends DataEvent { + public static readonly namespace = "consumption.outgoingRequestFromRelationshipCreationCreatedAndCompleted"; public constructor(eventTargetAddress: string, data: LocalRequestDTO) { - super(OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent.namespace, eventTargetAddress, data); + super(OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent.namespace, eventTargetAddress, data); if (!data.isOwn) throw new Error("Cannot create this event for an incoming Request"); } diff --git a/packages/runtime/src/events/consumption/index.ts b/packages/runtime/src/events/consumption/index.ts index 8d05b5730..17aca274c 100644 --- a/packages/runtime/src/events/consumption/index.ts +++ b/packages/runtime/src/events/consumption/index.ts @@ -8,7 +8,7 @@ export * from "./MailReceivedEvent"; export * from "./MessageProcessedEvent"; export * from "./OutgoingRequestCreatedAndCompletedEvent"; export * from "./OutgoingRequestCreatedEvent"; -export * from "./OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent"; +export * from "./OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent"; export * from "./OutgoingRequestStatusChangedEvent"; export * from "./OwnSharedAttributeDeletedByOwnerEvent"; export * from "./OwnSharedAttributeSucceededEvent"; diff --git a/packages/runtime/src/events/transport/RelationshipDecomposedBySelfEvent.ts b/packages/runtime/src/events/transport/RelationshipDecomposedBySelfEvent.ts new file mode 100644 index 000000000..74acf0121 --- /dev/null +++ b/packages/runtime/src/events/transport/RelationshipDecomposedBySelfEvent.ts @@ -0,0 +1,13 @@ +import { DataEvent } from "../DataEvent"; + +export interface RelationshipDecomposedBySelfEventData { + relationshipId: string; +} + +export class RelationshipDecomposedBySelfEvent extends DataEvent { + public static readonly namespace = "transport.relationshipDecomposedBySelf"; + + public constructor(eventTargetAddress: string, data: RelationshipDecomposedBySelfEventData) { + super(RelationshipDecomposedBySelfEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/runtime/src/events/transport/RelationshipReactivationCompletedEvent.ts b/packages/runtime/src/events/transport/RelationshipReactivationCompletedEvent.ts new file mode 100644 index 000000000..16ddfd67c --- /dev/null +++ b/packages/runtime/src/events/transport/RelationshipReactivationCompletedEvent.ts @@ -0,0 +1,10 @@ +import { RelationshipDTO } from "../../types"; +import { DataEvent } from "../DataEvent"; + +export class RelationshipReactivationCompletedEvent extends DataEvent { + public static readonly namespace = "transport.relationshipReactivationCompleted"; + + public constructor(eventTargetAddress: string, data: RelationshipDTO) { + super(RelationshipReactivationCompletedEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/runtime/src/events/transport/RelationshipReactivationRequestedEvent.ts b/packages/runtime/src/events/transport/RelationshipReactivationRequestedEvent.ts new file mode 100644 index 000000000..71fd9de1d --- /dev/null +++ b/packages/runtime/src/events/transport/RelationshipReactivationRequestedEvent.ts @@ -0,0 +1,10 @@ +import { RelationshipDTO } from "../../types"; +import { DataEvent } from "../DataEvent"; + +export class RelationshipReactivationRequestedEvent extends DataEvent { + public static readonly namespace = "transport.relationshipReactivationRequested"; + + public constructor(eventTargetAddress: string, data: RelationshipDTO) { + super(RelationshipReactivationRequestedEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/runtime/src/events/transport/index.ts b/packages/runtime/src/events/transport/index.ts index 3db45d1c8..553041b4c 100644 --- a/packages/runtime/src/events/transport/index.ts +++ b/packages/runtime/src/events/transport/index.ts @@ -5,3 +5,6 @@ export * from "./MessageSentEvent"; export * from "./MessageWasReadAtChangedEvent"; export * from "./PeerRelationshipTemplateLoadedEvent"; export * from "./RelationshipChangedEvent"; +export * from "./RelationshipDecomposedBySelfEvent"; +export * from "./RelationshipReactivationCompletedEvent"; +export * from "./RelationshipReactivationRequestedEvent"; diff --git a/packages/runtime/src/extensibility/TransportServices.ts b/packages/runtime/src/extensibility/TransportServices.ts index ea6cd37ce..a6eeabaa5 100644 --- a/packages/runtime/src/extensibility/TransportServices.ts +++ b/packages/runtime/src/extensibility/TransportServices.ts @@ -5,7 +5,6 @@ import { DevicesFacade, FilesFacade, IdentityDeletionProcessesFacade, - IdentityFacade, MessagesFacade, RelationshipsFacade, RelationshipTemplatesFacade, @@ -22,7 +21,6 @@ export class TransportServices { @Inject public readonly account: AccountFacade, @Inject public readonly devices: DevicesFacade, @Inject public readonly challenges: ChallengesFacade, - @Inject public readonly identity: IdentityFacade, @Inject public readonly identityDeletionProcesses: IdentityDeletionProcessesFacade ) {} } diff --git a/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts b/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts index ecfbdb24e..181fd2c63 100644 --- a/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts +++ b/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts @@ -2,6 +2,8 @@ import { Result } from "@js-soft/ts-utils"; import { Inject } from "typescript-ioc"; import { LocalAttributeDTO, LocalRequestDTO } from "../../../types"; import { + ChangeDefaultRepositoryAttributeRequest, + ChangeDefaultRepositoryAttributeUseCase, CreateAndShareRelationshipAttributeRequest, CreateAndShareRelationshipAttributeUseCase, CreateRepositoryAttributeRequest, @@ -19,18 +21,18 @@ import { DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerRequest, DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerResponse, DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerUseCase, - ExecuteIdentityAttributeQueryRequest, - ExecuteIdentityAttributeQueryUseCase, ExecuteIQLQueryRequest, ExecuteIQLQueryUseCase, + ExecuteIdentityAttributeQueryRequest, + ExecuteIdentityAttributeQueryUseCase, ExecuteRelationshipAttributeQueryRequest, ExecuteRelationshipAttributeQueryUseCase, ExecuteThirdPartyRelationshipAttributeQueryRequest, ExecuteThirdPartyRelationshipAttributeQueryUseCase, GetAttributeRequest, + GetAttributeUseCase, GetAttributesRequest, GetAttributesUseCase, - GetAttributeUseCase, GetOwnSharedAttributesRequest, GetOwnSharedAttributesUseCase, GetPeerSharedAttributesRequest, @@ -39,8 +41,6 @@ import { GetRepositoryAttributesUseCase, GetSharedVersionsOfAttributeRequest, GetSharedVersionsOfAttributeUseCase, - GetSharedVersionsOfRepositoryAttributeRequest, - GetSharedVersionsOfRepositoryAttributeUseCase, GetVersionsOfAttributeRequest, GetVersionsOfAttributeUseCase, NotifyPeerAboutRepositoryAttributeSuccessionRequest, @@ -62,7 +62,6 @@ import { export class AttributesFacade { public constructor( @Inject private readonly createRepositoryAttributeUseCase: CreateRepositoryAttributeUseCase, - @Inject private readonly shareRepositoryAttributeUseCase: ShareRepositoryAttributeUseCase, @Inject private readonly getPeerSharedAttributesUseCase: GetPeerSharedAttributesUseCase, @Inject private readonly getOwnSharedAttributesUseCase: GetOwnSharedAttributesUseCase, @Inject private readonly getRepositoryAttributesUseCase: GetRepositoryAttributesUseCase, @@ -70,16 +69,17 @@ export class AttributesFacade { @Inject private readonly getAttributesUseCase: GetAttributesUseCase, @Inject private readonly getVersionsOfAttributeUseCase: GetVersionsOfAttributeUseCase, @Inject private readonly getSharedVersionsOfAttributeUseCase: GetSharedVersionsOfAttributeUseCase, - @Inject private readonly getSharedVersionsOfRepositoryAttributeUseCase: GetSharedVersionsOfRepositoryAttributeUseCase, - @Inject private readonly succeedRepositoryAttributeUseCase: SucceedRepositoryAttributeUseCase, @Inject private readonly executeIdentityAttributeQueryUseCase: ExecuteIdentityAttributeQueryUseCase, @Inject private readonly executeRelationshipAttributeQueryUseCase: ExecuteRelationshipAttributeQueryUseCase, - @Inject private readonly succeedRelationshipAttributeAndNotifyPeerUseCase: SucceedRelationshipAttributeAndNotifyPeerUseCase, @Inject private readonly executeThirdPartyRelationshipAttributeQueryUseCase: ExecuteThirdPartyRelationshipAttributeQueryUseCase, @Inject private readonly executeIQLQueryUseCase: ExecuteIQLQueryUseCase, @Inject private readonly validateIQLQueryUseCase: ValidateIQLQueryUseCase, - @Inject private readonly createAndShareRelationshipAttributeUseCase: CreateAndShareRelationshipAttributeUseCase, + @Inject private readonly succeedRepositoryAttributeUseCase: SucceedRepositoryAttributeUseCase, + @Inject private readonly shareRepositoryAttributeUseCase: ShareRepositoryAttributeUseCase, @Inject private readonly notifyPeerAboutRepositoryAttributeSuccessionUseCase: NotifyPeerAboutRepositoryAttributeSuccessionUseCase, + @Inject private readonly createAndShareRelationshipAttributeUseCase: CreateAndShareRelationshipAttributeUseCase, + @Inject private readonly succeedRelationshipAttributeAndNotifyPeerUseCase: SucceedRelationshipAttributeAndNotifyPeerUseCase, + @Inject private readonly changeDefaultRepositoryAttributeUseCase: ChangeDefaultRepositoryAttributeUseCase, @Inject private readonly deleteOwnSharedAttributeAndNotifyPeerUseCase: DeleteOwnSharedAttributeAndNotifyPeerUseCase, @Inject private readonly deletePeerSharedAttributeAndNotifyOwnerUseCase: DeletePeerSharedAttributeAndNotifyOwnerUseCase, @Inject private readonly deleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerUseCase: DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerUseCase, @@ -119,13 +119,6 @@ export class AttributesFacade { return await this.getSharedVersionsOfAttributeUseCase.execute(request); } - /** - * @deprecated getSharedVersionsOfRepositoryAttribute won't be available in version 5 anymore. Use getSharedVersionsOfAttribute instead. - */ - public async getSharedVersionsOfRepositoryAttribute(request: GetSharedVersionsOfRepositoryAttributeRequest): Promise> { - return await this.getSharedVersionsOfRepositoryAttributeUseCase.execute(request); - } - public async executeIdentityAttributeQuery(request: ExecuteIdentityAttributeQueryRequest): Promise> { return await this.executeIdentityAttributeQueryUseCase.execute(request); } @@ -134,12 +127,6 @@ export class AttributesFacade { return await this.executeRelationshipAttributeQueryUseCase.execute(request); } - public async succeedRelationshipAttributeAndNotifyPeer( - request: SucceedRelationshipAttributeAndNotifyPeerRequest - ): Promise> { - return await this.succeedRelationshipAttributeAndNotifyPeerUseCase.execute(request); - } - public async executeThirdPartyRelationshipAttributeQuery(request: ExecuteThirdPartyRelationshipAttributeQueryRequest): Promise> { return await this.executeThirdPartyRelationshipAttributeQueryUseCase.execute(request); } @@ -160,16 +147,26 @@ export class AttributesFacade { return await this.shareRepositoryAttributeUseCase.execute(request); } - public async createAndShareRelationshipAttribute(request: CreateAndShareRelationshipAttributeRequest): Promise> { - return await this.createAndShareRelationshipAttributeUseCase.execute(request); - } - public async notifyPeerAboutRepositoryAttributeSuccession( request: NotifyPeerAboutRepositoryAttributeSuccessionRequest ): Promise> { return await this.notifyPeerAboutRepositoryAttributeSuccessionUseCase.execute(request); } + public async createAndShareRelationshipAttribute(request: CreateAndShareRelationshipAttributeRequest): Promise> { + return await this.createAndShareRelationshipAttributeUseCase.execute(request); + } + + public async succeedRelationshipAttributeAndNotifyPeer( + request: SucceedRelationshipAttributeAndNotifyPeerRequest + ): Promise> { + return await this.succeedRelationshipAttributeAndNotifyPeerUseCase.execute(request); + } + + public async changeDefaultRepositoryAttribute(request: ChangeDefaultRepositoryAttributeRequest): Promise> { + return await this.changeDefaultRepositoryAttributeUseCase.execute(request); + } + public async deleteOwnSharedAttributeAndNotifyPeer(request: DeleteOwnSharedAttributeAndNotifyPeerRequest): Promise> { return await this.deleteOwnSharedAttributeAndNotifyPeerUseCase.execute(request); } diff --git a/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts b/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts index a04b90473..520e2ca14 100644 --- a/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts +++ b/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts @@ -14,9 +14,7 @@ import { RegisterPushNotificationTokenRequest, RegisterPushNotificationTokenResponse, RegisterPushNotificationTokenUseCase, - SyncDatawalletRequest, SyncDatawalletUseCase, - SyncEverythingRequest, SyncEverythingResponse, SyncEverythingUseCase, SyncInfo, @@ -53,12 +51,12 @@ export class AccountFacade { return await this.unregisterPushNotificationTokenUseCase.execute(); } - public async syncDatawallet(request: SyncDatawalletRequest = {}): Promise> { - return await this.syncDatawalletUseCase.execute(request); + public async syncDatawallet(): Promise> { + return await this.syncDatawalletUseCase.execute(); } - public async syncEverything(request: SyncEverythingRequest = {}): Promise> { - return await this.syncEverythingUseCase.execute(request); + public async syncEverything(): Promise> { + return await this.syncEverythingUseCase.execute(); } public async getSyncInfo(): Promise> { diff --git a/packages/runtime/src/extensibility/facades/transport/DevicesFacade.ts b/packages/runtime/src/extensibility/facades/transport/DevicesFacade.ts index 41bd38bce..a24d50b28 100644 --- a/packages/runtime/src/extensibility/facades/transport/DevicesFacade.ts +++ b/packages/runtime/src/extensibility/facades/transport/DevicesFacade.ts @@ -11,8 +11,10 @@ import { GetDeviceOnboardingInfoRequest, GetDeviceOnboardingInfoUseCase, GetDeviceRequest, - GetDeviceUseCase, GetDevicesUseCase, + GetDeviceUseCase, + SetCommunicationLanguageRequest, + SetCommunicationLanguageUseCase, UpdateDeviceRequest, UpdateDeviceUseCase } from "../../../useCases"; @@ -25,7 +27,8 @@ export class DevicesFacade { @Inject private readonly updateDeviceUseCase: UpdateDeviceUseCase, @Inject private readonly deleteDeviceUseCase: DeleteDeviceUseCase, @Inject private readonly getDeviceOnboardingInfoUseCase: GetDeviceOnboardingInfoUseCase, - @Inject private readonly getDeviceOnboardingTokenUseCase: CreateDeviceOnboardingTokenUseCase + @Inject private readonly getDeviceOnboardingTokenUseCase: CreateDeviceOnboardingTokenUseCase, + @Inject private readonly setCommunicationLanguageUseCase: SetCommunicationLanguageUseCase ) {} public async getDevice(request: GetDeviceRequest): Promise> { @@ -55,4 +58,8 @@ export class DevicesFacade { public async deleteDevice(request: DeleteDeviceRequest): Promise> { return await this.deleteDeviceUseCase.execute(request); } + + public async setCommunicationLanguage(request: SetCommunicationLanguageRequest): Promise> { + return await this.setCommunicationLanguageUseCase.execute(request); + } } diff --git a/packages/runtime/src/extensibility/facades/transport/IdentityFacade.ts b/packages/runtime/src/extensibility/facades/transport/IdentityFacade.ts deleted file mode 100644 index 5c5954b88..000000000 --- a/packages/runtime/src/extensibility/facades/transport/IdentityFacade.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Result } from "@js-soft/ts-utils"; -import { Inject } from "typescript-ioc"; -import { CheckIdentityRequest, CheckIdentityResponse, CheckIdentityUseCase } from "../../../useCases"; - -export class IdentityFacade { - public constructor(@Inject private readonly checkIdentityUseCase: CheckIdentityUseCase) {} - - public async checkIdentity(request: CheckIdentityRequest): Promise> { - return await this.checkIdentityUseCase.execute(request); - } -} diff --git a/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts b/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts index 451bcc6a4..180073aa4 100644 --- a/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts +++ b/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts @@ -1,24 +1,36 @@ -import { ApplicationError, Result } from "@js-soft/ts-utils"; +import { Result } from "@js-soft/ts-utils"; import { Inject } from "typescript-ioc"; import { RelationshipDTO } from "../../../types"; import { - AcceptRelationshipChangeRequest, - AcceptRelationshipChangeUseCase, + AcceptRelationshipReactivationRequest, + AcceptRelationshipReactivationUseCase, + AcceptRelationshipRequest, + AcceptRelationshipUseCase, CreateRelationshipRequest, CreateRelationshipUseCase, + DecomposeRelationshipRequest, + DecomposeRelationshipUseCase, GetAttributesForRelationshipRequest, GetAttributesForRelationshipResponse, GetAttributesForRelationshipUseCase, GetRelationshipByAddressRequest, GetRelationshipByAddressUseCase, GetRelationshipRequest, + GetRelationshipUseCase, GetRelationshipsRequest, GetRelationshipsUseCase, - GetRelationshipUseCase, - RejectRelationshipChangeRequest, - RejectRelationshipChangeUseCase, - RevokeRelationshipChangeRequest, - RevokeRelationshipChangeUseCase + RejectRelationshipReactivationRequest, + RejectRelationshipReactivationUseCase, + RejectRelationshipRequest, + RejectRelationshipUseCase, + RequestRelationshipReactivationRequest, + RequestRelationshipReactivationUseCase, + RevokeRelationshipReactivationRequest, + RevokeRelationshipReactivationUseCase, + RevokeRelationshipRequest, + RevokeRelationshipUseCase, + TerminateRelationshipRequest, + TerminateRelationshipUseCase } from "../../../useCases"; export class RelationshipsFacade { @@ -27,41 +39,71 @@ export class RelationshipsFacade { @Inject private readonly getRelationshipUseCase: GetRelationshipUseCase, @Inject private readonly getRelationshipByAddressUseCase: GetRelationshipByAddressUseCase, @Inject private readonly createRelationshipUseCase: CreateRelationshipUseCase, - @Inject private readonly acceptRelationshipChangeUseCase: AcceptRelationshipChangeUseCase, - @Inject private readonly rejectRelationshipChangeUseCase: RejectRelationshipChangeUseCase, - @Inject private readonly revokeRelationshipChangeUseCase: RevokeRelationshipChangeUseCase, + @Inject private readonly acceptRelationshipUseCase: AcceptRelationshipUseCase, + @Inject private readonly rejectRelationshipUseCase: RejectRelationshipUseCase, + @Inject private readonly revokeRelationshipUseCase: RevokeRelationshipUseCase, + @Inject private readonly terminateRelationshipUseCase: TerminateRelationshipUseCase, + @Inject private readonly requestRelationshipReactivationUseCase: RequestRelationshipReactivationUseCase, + @Inject private readonly acceptRelationshipReactivationUseCase: AcceptRelationshipReactivationUseCase, + @Inject private readonly rejectRelationshipReactivationUseCase: RejectRelationshipReactivationUseCase, + @Inject private readonly revokeRelationshipReactivationUseCase: RevokeRelationshipReactivationUseCase, + @Inject private readonly decomposeRelationshipUseCase: DecomposeRelationshipUseCase, @Inject private readonly getAttributesForRelationshipUseCase: GetAttributesForRelationshipUseCase ) {} - public async getRelationships(request: GetRelationshipsRequest): Promise> { + public async getRelationships(request: GetRelationshipsRequest): Promise> { return await this.getRelationshipsUseCase.execute(request); } - public async getRelationship(request: GetRelationshipRequest): Promise> { + public async getRelationship(request: GetRelationshipRequest): Promise> { return await this.getRelationshipUseCase.execute(request); } - public async getRelationshipByAddress(request: GetRelationshipByAddressRequest): Promise> { + public async getRelationshipByAddress(request: GetRelationshipByAddressRequest): Promise> { return await this.getRelationshipByAddressUseCase.execute(request); } - public async createRelationship(request: CreateRelationshipRequest): Promise> { + public async createRelationship(request: CreateRelationshipRequest): Promise> { return await this.createRelationshipUseCase.execute(request); } - public async acceptRelationshipChange(request: AcceptRelationshipChangeRequest): Promise> { - return await this.acceptRelationshipChangeUseCase.execute(request); + public async acceptRelationship(request: AcceptRelationshipRequest): Promise> { + return await this.acceptRelationshipUseCase.execute(request); + } + + public async rejectRelationship(request: RejectRelationshipRequest): Promise> { + return await this.rejectRelationshipUseCase.execute(request); + } + + public async revokeRelationship(request: RevokeRelationshipRequest): Promise> { + return await this.revokeRelationshipUseCase.execute(request); + } + + public async terminateRelationship(request: TerminateRelationshipRequest): Promise> { + return await this.terminateRelationshipUseCase.execute(request); + } + + public async requestRelationshipReactivation(request: RequestRelationshipReactivationRequest): Promise> { + return await this.requestRelationshipReactivationUseCase.execute(request); + } + + public async acceptRelationshipReactivation(request: AcceptRelationshipReactivationRequest): Promise> { + return await this.acceptRelationshipReactivationUseCase.execute(request); + } + + public async rejectRelationshipReactivation(request: RejectRelationshipReactivationRequest): Promise> { + return await this.rejectRelationshipReactivationUseCase.execute(request); } - public async rejectRelationshipChange(request: RejectRelationshipChangeRequest): Promise> { - return await this.rejectRelationshipChangeUseCase.execute(request); + public async revokeRelationshipReactivation(request: RevokeRelationshipReactivationRequest): Promise> { + return await this.revokeRelationshipReactivationUseCase.execute(request); } - public async revokeRelationshipChange(request: RevokeRelationshipChangeRequest): Promise> { - return await this.revokeRelationshipChangeUseCase.execute(request); + public async decomposeRelationship(request: DecomposeRelationshipRequest): Promise> { + return await this.decomposeRelationshipUseCase.execute(request); } - public async getAttributesForRelationship(request: GetAttributesForRelationshipRequest): Promise> { + public async getAttributesForRelationship(request: GetAttributesForRelationshipRequest): Promise> { return await this.getAttributesForRelationshipUseCase.execute(request); } } diff --git a/packages/runtime/src/extensibility/facades/transport/index.ts b/packages/runtime/src/extensibility/facades/transport/index.ts index 23872c6a1..a14fc3df7 100644 --- a/packages/runtime/src/extensibility/facades/transport/index.ts +++ b/packages/runtime/src/extensibility/facades/transport/index.ts @@ -3,8 +3,7 @@ export * from "./ChallengesFacade"; export * from "./DevicesFacade"; export * from "./FilesFacade"; export * from "./IdentityDeletionProcessesFacade"; -export * from "./IdentityFacade"; export * from "./MessagesFacade"; -export * from "./RelationshipsFacade"; export * from "./RelationshipTemplatesFacade"; +export * from "./RelationshipsFacade"; export * from "./TokensFacade"; diff --git a/packages/runtime/src/modules/MessageModule.ts b/packages/runtime/src/modules/MessageModule.ts index 47a97b374..d457fe0f6 100644 --- a/packages/runtime/src/modules/MessageModule.ts +++ b/packages/runtime/src/modules/MessageModule.ts @@ -1,5 +1,5 @@ import { Event } from "@js-soft/ts-utils"; -import { Mail } from "@nmshd/content"; +import { Mail, MailJSON } from "@nmshd/content"; import { MailReceivedEvent, MessageReceivedEvent, RelationshipEvent } from "../events"; import { ModuleConfiguration, RuntimeModule } from "../extensibility/modules/RuntimeModule"; @@ -23,7 +23,7 @@ export class MessageModule extends RuntimeModule { let event: Event | undefined; switch (type) { case "Mail": - const mail = Mail.from(message.content); + const mail = Mail.from(message.content as MailJSON); event = new MailReceivedEvent(messageReceivedEvent.eventTargetAddress, mail, message); this.runtime.eventBus.publish(event); this.logger.trace(`Published MailReceivedEvent for ${message.id}`); diff --git a/packages/runtime/src/modules/RequestModule.ts b/packages/runtime/src/modules/RequestModule.ts index c7f56608a..7a68774e3 100644 --- a/packages/runtime/src/modules/RequestModule.ts +++ b/packages/runtime/src/modules/RequestModule.ts @@ -1,14 +1,5 @@ import { LocalRequestStatus } from "@nmshd/consumption"; -import { - RelationshipCreationChangeRequestContent, - RelationshipCreationChangeRequestContentJSON, - RelationshipTemplateContentJSON, - RequestJSON, - ResponseJSON, - ResponseResult, - ResponseWrapper, - ResponseWrapperJSON -} from "@nmshd/content"; +import { RelationshipCreationContent, RequestJSON, ResponseJSON, ResponseResult, ResponseWrapper } from "@nmshd/content"; import { IncomingRequestStatusChangedEvent, MessageProcessedEvent, @@ -47,7 +38,7 @@ export class RequestModule extends RuntimeModule { return; } - const body = template.content as RelationshipTemplateContentJSON; + const body = template.content; const services = await this.runtime.getServices(event.eventTargetAddress); @@ -120,17 +111,11 @@ export class RequestModule extends RuntimeModule { const messageContentType = message.content["@type"]; switch (messageContentType) { case "Request": - await this.createIncomingRequest(services, message.content as RequestJSON, message.id); - break; - - // Handle responses directly sent via messages. This is only for backwards compatibility and - // not viable for responding to Requests from RelationshipTemplates' onExistingRelationship. - case "Response": - await this.completeExistingRequestWithResponseReceivedByMessage(services, message.id, message.content as ResponseJSON); + await this.createIncomingRequest(services, message.content, message.id); break; case "ResponseWrapper": - const responseWrapper = message.content as ResponseWrapperJSON; + const responseWrapper = message.content; if (responseWrapper.requestSourceType === "Message") { await this.completeExistingRequestWithResponseReceivedByMessage(services, message.id, responseWrapper.response); @@ -143,6 +128,8 @@ export class RequestModule extends RuntimeModule { response: responseWrapper.response }); break; + default: + break; } if (messageContentType !== "Request") { @@ -162,7 +149,7 @@ export class RequestModule extends RuntimeModule { if (message.content["@type"] !== "Request") return; const services = await this.runtime.getServices(event.eventTargetAddress); - const request = message.content as RequestJSON; + const request = message.content; const requestResult = await services.consumptionServices.outgoingRequests.sent({ requestId: request.id!, messageId: message.id }); if (requestResult.isError) { @@ -226,8 +213,8 @@ export class RequestModule extends RuntimeModule { return; } - const creationChangeContent = RelationshipCreationChangeRequestContent.from({ response: request.response!.content }); - const createRelationshipResult = await services.transportServices.relationships.createRelationship({ templateId, content: creationChangeContent }); + const creationContent = RelationshipCreationContent.from({ response: request.response!.content }).toJSON(); + const createRelationshipResult = await services.transportServices.relationships.createRelationship({ templateId, creationContent }); if (createRelationshipResult.isError) { this.logger.error(`Could not create relationship for templateId '${templateId}'. Root error:`, createRelationshipResult.error); return; @@ -236,7 +223,7 @@ export class RequestModule extends RuntimeModule { const requestId = request.id; const completeRequestResult = await services.consumptionServices.incomingRequests.complete({ requestId, - responseSourceId: createRelationshipResult.value.changes[0].id + responseSourceId: createRelationshipResult.value.id }); if (completeRequestResult.isError) { this.logger.error(`Could not complete the request '${requestId}'. Root error:`, completeRequestResult.error); @@ -295,20 +282,18 @@ export class RequestModule extends RuntimeModule { const templateId = template.id; // do not trigger for templates without the correct content type if (template.content["@type"] !== "RelationshipTemplateContent") return; + if (createdRelationship.creationContent["@type"] !== "RelationshipCreationContent") { + this.logger.error(`The creation content of relationshipId ${createdRelationship.id} is not of type RelationshipCreationContent.`); + return; + } - const relationshipCreationChange = createdRelationship.changes[0]; - const relationshipChangeId = relationshipCreationChange.id; - // do not trigger for creation changes without the correct content type - if (relationshipCreationChange.request.content["@type"] !== "RelationshipCreationChangeRequestContent") return; - - const relationshipCreationChangeContent = relationshipCreationChange.request.content as RelationshipCreationChangeRequestContentJSON; const result = await services.consumptionServices.outgoingRequests.createAndCompleteFromRelationshipTemplateResponse({ templateId, - responseSourceId: relationshipChangeId, - response: relationshipCreationChangeContent.response + responseSourceId: createdRelationship.id, + response: createdRelationship.creationContent.response }); if (result.isError) { - this.logger.error(`Could not create and complete request for templateId '${templateId}' and changeId '${relationshipChangeId}'. Root error:`, result.error); + this.logger.error(`Could not create and complete request for templateId '${templateId}' and relationshipId '${createdRelationship.id}'. Root error:`, result.error); return; } } diff --git a/packages/runtime/src/types/consumption/LocalAttributeDTO.ts b/packages/runtime/src/types/consumption/LocalAttributeDTO.ts index 6e110e998..8b08c0742 100644 --- a/packages/runtime/src/types/consumption/LocalAttributeDTO.ts +++ b/packages/runtime/src/types/consumption/LocalAttributeDTO.ts @@ -10,4 +10,5 @@ export interface LocalAttributeDTO { succeededBy?: string; shareInfo?: LocalAttributeShareInfoJSON; deletionInfo?: LocalAttributeDeletionInfoJSON; + isDefault?: true; } diff --git a/packages/runtime/src/types/consumption/LocalRequestDTO.ts b/packages/runtime/src/types/consumption/LocalRequestDTO.ts index f23d5f682..49510d8e9 100644 --- a/packages/runtime/src/types/consumption/LocalRequestDTO.ts +++ b/packages/runtime/src/types/consumption/LocalRequestDTO.ts @@ -18,7 +18,7 @@ export interface LocalRequestSourceDTO { } export interface LocalResponseSourceDTO { - type: "Message" | "RelationshipChange"; + type: "Message" | "Relationship"; reference: string; } diff --git a/packages/runtime/src/types/transport/IdentityDTO.ts b/packages/runtime/src/types/transport/IdentityDTO.ts index 586568dfa..2dc791cf1 100644 --- a/packages/runtime/src/types/transport/IdentityDTO.ts +++ b/packages/runtime/src/types/transport/IdentityDTO.ts @@ -1,5 +1,4 @@ export interface IdentityDTO { address: string; publicKey: string; - realm: string; } diff --git a/packages/runtime/src/types/transport/MessageDTO.ts b/packages/runtime/src/types/transport/MessageDTO.ts index d541d8797..80daafd80 100644 --- a/packages/runtime/src/types/transport/MessageDTO.ts +++ b/packages/runtime/src/types/transport/MessageDTO.ts @@ -1,8 +1,11 @@ +import { ArbitraryMessageContentJSON, MailJSON, NotificationJSON, RequestJSON, ResponseWrapperJSON } from "@nmshd/content"; import { RecipientDTO } from "./RecipientDTO"; +export type MessageContentDerivation = MailJSON | ResponseWrapperJSON | RequestJSON | NotificationJSON | ArbitraryMessageContentJSON; + export interface MessageDTO { id: string; - content: any; + content: MessageContentDerivation; createdBy: string; createdByDevice: string; recipients: RecipientDTO[]; diff --git a/packages/runtime/src/types/transport/RecipientDTO.ts b/packages/runtime/src/types/transport/RecipientDTO.ts index ffbcaa360..abf751580 100644 --- a/packages/runtime/src/types/transport/RecipientDTO.ts +++ b/packages/runtime/src/types/transport/RecipientDTO.ts @@ -2,5 +2,5 @@ export interface RecipientDTO { address: string; receivedAt?: string; receivedByDevice?: string; - relationshipId: string; + relationshipId?: string; } diff --git a/packages/runtime/src/types/transport/RelationshipChangeDTO.ts b/packages/runtime/src/types/transport/RelationshipChangeDTO.ts deleted file mode 100644 index 80b126618..000000000 --- a/packages/runtime/src/types/transport/RelationshipChangeDTO.ts +++ /dev/null @@ -1,34 +0,0 @@ -export enum RelationshipChangeStatus { - Pending = "Pending", - Rejected = "Rejected", - Revoked = "Revoked", - Accepted = "Accepted" -} - -export enum RelationshipChangeType { - Creation = "Creation", - Termination = "Termination", - TerminationCancellation = "TerminationCancellation" -} - -export interface RelationshipChangeRequestDTO { - createdBy: string; - createdByDevice: string; - createdAt: string; - content?: any; -} - -export interface RelationshipChangeResponseDTO { - createdBy: string; - createdByDevice: string; - createdAt: string; - content?: any; -} - -export interface RelationshipChangeDTO { - id: string; - request: RelationshipChangeRequestDTO; - status: RelationshipChangeStatus; - type: RelationshipChangeType; - response?: RelationshipChangeResponseDTO; -} diff --git a/packages/runtime/src/types/transport/RelationshipDTO.ts b/packages/runtime/src/types/transport/RelationshipDTO.ts index 657dfa64a..b3fa23c79 100644 --- a/packages/runtime/src/types/transport/RelationshipDTO.ts +++ b/packages/runtime/src/types/transport/RelationshipDTO.ts @@ -1,5 +1,5 @@ +import { ArbitraryRelationshipCreationContentJSON, RelationshipCreationContentJSON } from "@nmshd/content"; import { IdentityDTO } from "./IdentityDTO"; -import { RelationshipChangeDTO } from "./RelationshipChangeDTO"; import { RelationshipTemplateDTO } from "./RelationshipTemplateDTO"; export enum RelationshipStatus { @@ -7,15 +7,42 @@ export enum RelationshipStatus { Active = "Active", Rejected = "Rejected", Revoked = "Revoked", - Terminating = "Terminating", - Terminated = "Terminated" + Terminated = "Terminated", + DeletionProposed = "DeletionProposed" } +export enum RelationshipAuditLogEntryReason { + Creation = "Creation", + AcceptanceOfCreation = "AcceptanceOfCreation", + RejectionOfCreation = "RejectionOfCreation", + RevocationOfCreation = "RevocationOfCreation", + Termination = "Termination", + ReactivationRequested = "ReactivationRequested", + AcceptanceOfReactivation = "AcceptanceOfReactivation", + RejectionOfReactivation = "RejectionOfReactivation", + RevocationOfReactivation = "RevocationOfReactivation", + Decomposition = "Decomposition" +} + +export interface RelationshipAuditLogEntryDTO { + createdAt: string; + createdBy: string; + createdByDevice: string; + reason: RelationshipAuditLogEntryReason; + oldStatus?: RelationshipStatus; + newStatus: RelationshipStatus; +} + +export interface RelationshipAuditLogDTO extends Array {} + +export type RelationshipCreationContentDerivation = RelationshipCreationContentJSON | ArbitraryRelationshipCreationContentJSON; + export interface RelationshipDTO { id: string; template: RelationshipTemplateDTO; status: RelationshipStatus; peer: string; peerIdentity: IdentityDTO; - changes: RelationshipChangeDTO[]; + creationContent: RelationshipCreationContentDerivation; + auditLog: RelationshipAuditLogDTO; } diff --git a/packages/runtime/src/types/transport/RelationshipTemplateDTO.ts b/packages/runtime/src/types/transport/RelationshipTemplateDTO.ts index 2e38bb234..3b5f1a6a2 100644 --- a/packages/runtime/src/types/transport/RelationshipTemplateDTO.ts +++ b/packages/runtime/src/types/transport/RelationshipTemplateDTO.ts @@ -1,10 +1,14 @@ +import { ArbitraryRelationshipTemplateContentJSON, RelationshipTemplateContentJSON } from "@nmshd/content"; + +export type RelationshipTemplateContentDerivation = RelationshipTemplateContentJSON | ArbitraryRelationshipTemplateContentJSON; + export interface RelationshipTemplateDTO { id: string; isOwn: boolean; createdBy: string; createdByDevice: string; createdAt: string; - content: any; + content: RelationshipTemplateContentDerivation; expiresAt?: string; maxNumberOfAllocations?: number; secretKey: string; diff --git a/packages/runtime/src/types/transport/index.ts b/packages/runtime/src/types/transport/index.ts index 650f6ff6e..2235cf6e4 100644 --- a/packages/runtime/src/types/transport/index.ts +++ b/packages/runtime/src/types/transport/index.ts @@ -2,12 +2,11 @@ export * from "./ChallengeDTO"; export * from "./DeviceDTO"; export * from "./DeviceOnboardingInfoDTO"; export * from "./FileDTO"; -export * from "./IdentityDeletionProcessDTO"; export * from "./IdentityDTO"; +export * from "./IdentityDeletionProcessDTO"; export * from "./MessageDTO"; export * from "./MessageWithAttachmentsDTO"; export * from "./RecipientDTO"; -export * from "./RelationshipChangeDTO"; export * from "./RelationshipDTO"; export * from "./RelationshipTemplateDTO"; export * from "./TokenDTO"; diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index 849e1a511..dda47bf4f 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -1,4 +1,5 @@ import { ApplicationError } from "@js-soft/ts-utils"; +import { LocalAttribute } from "@nmshd/consumption"; import { CoreAddress, CoreId } from "@nmshd/transport"; import { Base64ForIdPrefix } from "./Base64ForIdPrefix"; @@ -44,7 +45,7 @@ class General { } public invalidTokenContent() { - return new ApplicationError("error.runtime.invalidTokenContent", "The given token has an invalid content for this route."); + return new ApplicationError("error.runtime.invalidTokenContent", "The given Token has an invalid content for this route."); } public cacheEmpty(entityName: string | Function, id: string) { @@ -70,37 +71,44 @@ class Files { public invalidReference(reference: string): ApplicationError { return new ApplicationError( "error.runtime.files.invalidReference", - `The given reference '${reference}' is not valid. The reference for a file must start with '${Base64ForIdPrefix.Token}' or '${Base64ForIdPrefix.File}'.` + `The given reference '${reference}' is not valid. The reference for a File must start with '${Base64ForIdPrefix.Token}' or '${Base64ForIdPrefix.File}'.` ); } } class RelationshipTemplates { public cannotCreateTokenForPeerTemplate(): ApplicationError { - return new ApplicationError("error.runtime.relationshipTemplates.cannotCreateTokenForPeerTemplate", "You cannot create a token for a peer template."); + return new ApplicationError("error.runtime.relationshipTemplates.cannotCreateTokenForPeerTemplate", "You cannot create a Token for a peer RelationshipTemplate."); } public cannotCreateQRCodeForPeerTemplate(): ApplicationError { - return new ApplicationError("error.runtime.relationshipTemplates.cannotCreateQRCodeForPeerTemplate", "You cannot create a QRCode for a peer template."); + return new ApplicationError("error.runtime.relationshipTemplates.cannotCreateQRCodeForPeerTemplate", "You cannot create a QR code for a peer RelationshipTemplate."); } public invalidReference(reference: string): ApplicationError { return new ApplicationError( "error.runtime.relationshipTemplates.invalidReference", - `The given reference '${reference}' is not valid. The reference for a relationship template must start with '${Base64ForIdPrefix.Token}' or '${Base64ForIdPrefix.RelationshipTemplate}'.` + `The given reference '${reference}' is not valid. The reference for a RelationshipTemplate must start with '${Base64ForIdPrefix.Token}' or '${Base64ForIdPrefix.RelationshipTemplate}'.` ); } } class Relationships { + public wrongRelationshipStatus(relationshipId: string, status: string): ApplicationError { + return new ApplicationError( + "error.runtime.relationships.wrongRelationshipStatus", + `The Relationship '${relationshipId}' has the wrong status ('${status}') to run this operation.` + ); + } + public isNeitherRejectedNorRevoked(): ApplicationError { - return new ApplicationError("error.runtime.relationships.isNeitherRejectedNorRevoked", 'The `status` of the Relationship is neither "Rejected" nor "Revoked".'); + return new ApplicationError("error.runtime.relationships.isNeitherRejectedNorRevoked", "The status of the Relationship is neither 'Rejected' nor 'Revoked'."); } } class Messages { public fileNotFoundInMessage(attachmentId: string) { - return new ApplicationError("error.runtime.messages.fileNotFoundInMessage", `The requested file '${attachmentId}' was not found in the given message.`); + return new ApplicationError("error.runtime.messages.fileNotFoundInMessage", `The requested File '${attachmentId}' was not found in the given Message.`); } } @@ -130,11 +138,14 @@ class Challenges { class Notifications { public cannotReceiveNotificationFromOwnMessage(): ApplicationError { - return new ApplicationError("error.runtime.notifications.cannotReceiveNotificationFromOwnMessage", "Cannot receive Notification from own message."); + return new ApplicationError("error.runtime.notifications.cannotReceiveNotificationFromOwnMessage", "Cannot receive Notification from own Message."); } - public cannotSaveSentNotificationFromPeerMessage(): ApplicationError { - return new ApplicationError("error.runtime.notifications.cannotSaveSendNotificationFromPeerMessage", "Cannot send Notification from peer message."); + public cannotSaveSentNotificationFromPeerMessage(messageId: CoreId): ApplicationError { + return new ApplicationError( + "error.runtime.notifications.cannotSaveSentNotificationFromPeerMessage", + `The Message '${messageId}' was received from a peer, but an own Message is expected here to save its Notification content.` + ); } public messageDoesNotContainNotification(messageId: CoreId): ApplicationError { @@ -147,7 +158,7 @@ class Notifications { class Attributes { public isNotRepositoryAttribute(attributeId: CoreId | string): ApplicationError { - return new ApplicationError("error.runtime.attributes.isNotRepositoryAttribute", `Attribute '${attributeId.toString()}' is not a repository attribute.`); + return new ApplicationError("error.runtime.attributes.isNotRepositoryAttribute", `Attribute '${attributeId.toString()}' is not a RepositoryAttribute.`); } public repositoryAttributeHasAlreadyBeenSharedWithPeer( @@ -157,40 +168,36 @@ class Attributes { ): ApplicationError { return new ApplicationError( "error.runtime.attributes.repositoryAttributeHasAlreadyBeenSharedWithPeer", - `Repository attribute '${repositoryAttributeId.toString()}' has already been shared with peer '${peer.toString()}'. ID of own shared identity attribute: ${ownSharedIdentityAttributeId.toString()}.` - ); - } - - public anotherVersionOfRepositoryAttributeHasAlreadyBeenSharedWithPeer( - repositoryAttributeId: CoreId | string, - peer: CoreAddress | string, - ownSharedIdentityAttributeId: CoreId | string - ): ApplicationError { - return new ApplicationError( - "error.runtime.attributes.anotherVersionOfRepositoryAttributeHasAlreadyBeenSharedWithPeer", - `Another version of repository attribute '${repositoryAttributeId.toString()}' has already been shared with peer '${peer.toString()}'. ID of previous own shared identity attribute: ${ownSharedIdentityAttributeId.toString()}.` + `RepositoryAttribute '${repositoryAttributeId.toString()}' has already been shared with peer '${peer.toString()}'. ID of own shared IdentityAttribute: '${ownSharedIdentityAttributeId.toString()}'.` ); } public noPreviousVersionOfRepositoryAttributeHasBeenSharedWithPeerBefore(repositoryAttributeId: CoreId | string, peer: CoreAddress | string): ApplicationError { return new ApplicationError( "error.runtime.attributes.noPreviousVersionOfRepositoryAttributeHasBeenSharedWithPeerBefore", - `No previous version of repository attribute '${repositoryAttributeId.toString()}' has been shared with peer '${peer.toString()}' before. If you wish to execute an initial sharing of this attribute, use 'ShareRepositoryAttribute'.` + `No previous version of the RepositoryAttribute '${repositoryAttributeId.toString()}' has been shared with peer '${peer.toString()}' before. If you wish to execute an initial sharing of this Attribute, use the ShareRepositoryAttributeUseCase instead.` ); } public isNotOwnSharedAttribute(attributeId: CoreId | string): ApplicationError { - return new ApplicationError("error.runtime.attributes.isNotOwnSharedAttribute", `Attribute '${attributeId.toString()}' is not an own shared attribute.`); + return new ApplicationError("error.runtime.attributes.isNotOwnSharedAttribute", `Attribute '${attributeId.toString()}' is not an own shared Attribute.`); } public isNotPeerSharedAttribute(attributeId: CoreId | string): ApplicationError { - return new ApplicationError("error.runtime.attributes.isNotPeerSharedAttribute", `Attribute '${attributeId.toString()}' is not a peer shared attribute.`); + return new ApplicationError("error.runtime.attributes.isNotPeerSharedAttribute", `Attribute '${attributeId.toString()}' is not a peer shared Attribute.`); } public isNotThirdPartyOwnedRelationshipAttribute(attributeId: CoreId | string): ApplicationError { return new ApplicationError( "error.runtime.attributes.isNotThirdPartyOwnedRelationshipAttribute", - `Attribute '${attributeId.toString()}' is not a third party owned relationship attribute.` + `Attribute '${attributeId.toString()}' is not a third party owned RelationshipAttribute.` + ); + } + + public hasSuccessor(predecessor: LocalAttribute): ApplicationError { + return new ApplicationError( + "error.runtime.attributes.hasSuccessor", + `Attribute '${predecessor.id.toString()}' already has a successor ${predecessor.succeededBy?.toString()}.` ); } diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 75e4666bf..110e365da 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -71,6 +71,29 @@ export const GetAttributeListenerRequest: any = { } } +export const ChangeDefaultRepositoryAttributeRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/ChangeDefaultRepositoryAttributeRequest", + "definitions": { + "ChangeDefaultRepositoryAttributeRequest": { + "type": "object", + "properties": { + "attributeId": { + "$ref": "#/definitions/AttributeIdString" + } + }, + "required": [ + "attributeId" + ], + "additionalProperties": false + }, + "AttributeIdString": { + "type": "string", + "pattern": "ATT[A-Za-z0-9]{17}" + } + } +} + export const AcceptIncomingRequestRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/AcceptIncomingRequestRequest", @@ -241,10 +264,6 @@ export const CanCreateOutgoingRequestRequest: any = { "type": "string", "description": "The human-readable description of this group." }, - "mustBeAccepted": { - "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not." - }, "metadata": { "type": "object", "description": "This property can be used to add some arbitrary metadata to this group. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." @@ -259,11 +278,10 @@ export const CanCreateOutgoingRequestRequest: any = { }, "required": [ "@type", - "items", - "mustBeAccepted" + "items" ], "additionalProperties": false, - "description": "A RequestItemGroup can be used to group one or more RequestItems. This is useful if you want to\n* make sure that the items in the group can only be accepted together\n\n Example: when sending a `CreateRelationshipAttributeRequestItem` **and** a `ShareAttributeRequestItem` in a single Request where the latter one targets an attribute created by the first one, it it should be impossible to reject the first item, while accepting the second one.\n* visually group items on the UI and give the a common title/description" + "description": "A RequestItemGroup can be used to group one or more RequestItems. This is useful if you want to visually group RequestItems on the UI and give them a common `title` or `description`." }, "RequestItemJSONDerivations": { "anyOf": [ @@ -321,11 +339,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -361,11 +379,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2322,11 +2340,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2366,11 +2384,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2421,11 +2439,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2709,11 +2727,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2806,11 +2824,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2853,11 +2871,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2893,11 +2911,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2937,11 +2955,11 @@ export const CanCreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -2967,7 +2985,7 @@ export const CanCreateOutgoingRequestRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" } } } @@ -3101,7 +3119,7 @@ export const CompleteIncomingRequestRequest: any = { "$ref": "#/definitions/MessageIdString" }, { - "$ref": "#/definitions/RelationshipChangeIdString" + "$ref": "#/definitions/RelationshipIdString" } ] } @@ -3119,9 +3137,9 @@ export const CompleteIncomingRequestRequest: any = { "type": "string", "pattern": "MSG[A-Za-z0-9]{17}" }, - "RelationshipChangeIdString": { + "RelationshipIdString": { "type": "string", - "pattern": "RCH[A-Za-z0-9]{17}" + "pattern": "REL[A-Za-z0-9]{17}" } } } @@ -5546,7 +5564,7 @@ export const CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseReq "responseSourceId": { "anyOf": [ { - "$ref": "#/definitions/RelationshipChangeIdString" + "$ref": "#/definitions/RelationshipIdString" }, { "$ref": "#/definitions/MessageIdString" @@ -5568,9 +5586,9 @@ export const CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseReq "type": "string", "pattern": "RLT[A-Za-z0-9]{17}" }, - "RelationshipChangeIdString": { + "RelationshipIdString": { "type": "string", - "pattern": "RCH[A-Za-z0-9]{17}" + "pattern": "REL[A-Za-z0-9]{17}" }, "MessageIdString": { "type": "string", @@ -8040,10 +8058,6 @@ export const CreateOutgoingRequestRequest: any = { "type": "string", "description": "The human-readable description of this group." }, - "mustBeAccepted": { - "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not." - }, "metadata": { "type": "object", "description": "This property can be used to add some arbitrary metadata to this group. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." @@ -8058,11 +8072,10 @@ export const CreateOutgoingRequestRequest: any = { }, "required": [ "@type", - "items", - "mustBeAccepted" + "items" ], "additionalProperties": false, - "description": "A RequestItemGroup can be used to group one or more RequestItems. This is useful if you want to\n* make sure that the items in the group can only be accepted together\n\n Example: when sending a `CreateRelationshipAttributeRequestItem` **and** a `ShareAttributeRequestItem` in a single Request where the latter one targets an attribute created by the first one, it it should be impossible to reject the first item, while accepting the second one.\n* visually group items on the UI and give the a common title/description" + "description": "A RequestItemGroup can be used to group one or more RequestItems. This is useful if you want to visually group RequestItems on the UI and give them a common `title` or `description`." }, "RequestItemJSONDerivations": { "anyOf": [ @@ -8120,11 +8133,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -8160,11 +8173,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10121,11 +10134,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10165,11 +10178,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10220,11 +10233,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10508,11 +10521,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10605,11 +10618,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10652,11 +10665,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10692,11 +10705,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10736,11 +10749,11 @@ export const CreateOutgoingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -10766,7 +10779,7 @@ export const CreateOutgoingRequestRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" } } } @@ -11350,10 +11363,6 @@ export const ReceivedIncomingRequestRequest: any = { "type": "string", "description": "The human-readable description of this group." }, - "mustBeAccepted": { - "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not." - }, "metadata": { "type": "object", "description": "This property can be used to add some arbitrary metadata to this group. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." @@ -11368,11 +11377,10 @@ export const ReceivedIncomingRequestRequest: any = { }, "required": [ "@type", - "items", - "mustBeAccepted" + "items" ], "additionalProperties": false, - "description": "A RequestItemGroup can be used to group one or more RequestItems. This is useful if you want to\n* make sure that the items in the group can only be accepted together\n\n Example: when sending a `CreateRelationshipAttributeRequestItem` **and** a `ShareAttributeRequestItem` in a single Request where the latter one targets an attribute created by the first one, it it should be impossible to reject the first item, while accepting the second one.\n* visually group items on the UI and give the a common title/description" + "description": "A RequestItemGroup can be used to group one or more RequestItems. This is useful if you want to visually group RequestItems on the UI and give them a common `title` or `description`." }, "RequestItemJSONDerivations": { "anyOf": [ @@ -11430,11 +11438,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -11470,11 +11478,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -13431,11 +13439,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -13475,11 +13483,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -13530,11 +13538,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -13818,11 +13826,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -13915,11 +13923,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -13962,11 +13970,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -14002,11 +14010,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -14046,11 +14054,11 @@ export const ReceivedIncomingRequestRequest: any = { }, "metadata": { "type": "object", - "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the group content as they receive the response." + "description": "This property can be used to add some arbitrary metadata to this item. The content of this property will be copied into the response on the side of the recipient, so the sender can use it to identify the item as they receive the response." }, "mustBeAccepted": { "type": "boolean", - "description": "If set to `true`, the recipient has to accept this group if he wants to accept the Request. If set to `false`, the recipient can decide whether he wants to accept it or not.\n\nCaution: this setting does not take effect in case it is inside of a {@link RequestItemGroupJSON RequestItemGroup } , which is not accepted by the recipient, since a {@link RequestItemJSON RequestItem } can only be accepted if the parent group is accepted as well." + "description": "If set to `true`, the recipient has to accept this item if they want to accept the Request. If set to `false`, the recipient can decide whether they want to accept it or not." }, "requireManualDecision": { "type": "boolean", @@ -14851,7 +14859,7 @@ export const CreateAndShareRelationshipAttributeRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" } } } @@ -16174,6 +16182,29 @@ export const DeleteRepositoryAttributeRequest: any = { } } +export const DeleteSharedAttributesForRejectedOrRevokedRelationshipRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/DeleteSharedAttributesForRejectedOrRevokedRelationshipRequest", + "definitions": { + "DeleteSharedAttributesForRejectedOrRevokedRelationshipRequest": { + "type": "object", + "properties": { + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" + } + }, + "required": [ + "relationshipId" + ], + "additionalProperties": false + }, + "RelationshipIdString": { + "type": "string", + "pattern": "REL[A-Za-z0-9]{17}" + } + } +} + export const DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerRequest", @@ -16197,15 +16228,37 @@ export const DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerRequest: any } } -export const ExecuteIdentityAttributeQueryRequest: any = { +export const ExecuteIQLQueryRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/ExecuteIdentityAttributeQueryRequest", + "$ref": "#/definitions/ExecuteIQLQueryRequest", "definitions": { - "ExecuteIdentityAttributeQueryRequest": { + "ExecuteIQLQueryRequest": { "type": "object", "properties": { "query": { - "$ref": "#/definitions/IdentityAttributeQueryJSON" + "type": "object", + "additionalProperties": false, + "properties": { + "@type": { + "type": "string", + "const": "IQLQuery" + }, + "queryString": { + "type": "string" + }, + "attributeCreationHints": { + "$ref": "#/definitions/IQLQueryCreationHintsJSON" + }, + "@context": { + "type": "string" + }, + "@version": { + "type": "string" + } + }, + "required": [ + "queryString" + ] } }, "required": [ @@ -16213,19 +16266,9 @@ export const ExecuteIdentityAttributeQueryRequest: any = { ], "additionalProperties": false }, - "IdentityAttributeQueryJSON": { + "IQLQueryCreationHintsJSON": { "type": "object", "properties": { - "@type": { - "type": "string", - "const": "IdentityAttributeQuery" - }, - "@context": { - "type": "string" - }, - "@version": { - "type": "string" - }, "valueType": { "$ref": "#/definitions/AttributeValues.Identity.TypeName" }, @@ -16234,16 +16277,9 @@ export const ExecuteIdentityAttributeQueryRequest: any = { "items": { "type": "string" } - }, - "validFrom": { - "type": "string" - }, - "validTo": { - "type": "string" } }, "required": [ - "@type", "valueType" ], "additionalProperties": false @@ -16313,37 +16349,15 @@ export const ExecuteIdentityAttributeQueryRequest: any = { } } -export const ExecuteIQLQueryRequest: any = { +export const ExecuteIdentityAttributeQueryRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/ExecuteIQLQueryRequest", + "$ref": "#/definitions/ExecuteIdentityAttributeQueryRequest", "definitions": { - "ExecuteIQLQueryRequest": { + "ExecuteIdentityAttributeQueryRequest": { "type": "object", "properties": { "query": { - "type": "object", - "additionalProperties": false, - "properties": { - "@type": { - "type": "string", - "const": "IQLQuery" - }, - "queryString": { - "type": "string" - }, - "attributeCreationHints": { - "$ref": "#/definitions/IQLQueryCreationHintsJSON" - }, - "@context": { - "type": "string" - }, - "@version": { - "type": "string" - } - }, - "required": [ - "queryString" - ] + "$ref": "#/definitions/IdentityAttributeQueryJSON" } }, "required": [ @@ -16351,9 +16365,19 @@ export const ExecuteIQLQueryRequest: any = { ], "additionalProperties": false }, - "IQLQueryCreationHintsJSON": { + "IdentityAttributeQueryJSON": { "type": "object", "properties": { + "@type": { + "type": "string", + "const": "IdentityAttributeQuery" + }, + "@context": { + "type": "string" + }, + "@version": { + "type": "string" + }, "valueType": { "$ref": "#/definitions/AttributeValues.Identity.TypeName" }, @@ -16362,9 +16386,16 @@ export const ExecuteIQLQueryRequest: any = { "items": { "type": "string" } + }, + "validFrom": { + "type": "string" + }, + "validTo": { + "type": "string" } }, "required": [ + "@type", "valueType" ], "additionalProperties": false @@ -16721,9 +16752,6 @@ export const GetAttributesRequest: any = { "type": "string" }, "parentId": { - "type": "string" - }, - "content.@type": { "anyOf": [ { "type": "string" @@ -16736,7 +16764,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.tags": { + "succeeds": { "anyOf": [ { "type": "string" @@ -16749,7 +16777,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.owner": { + "succeededBy": { "anyOf": [ { "type": "string" @@ -16762,7 +16790,10 @@ export const GetAttributesRequest: any = { } ] }, - "content.validFrom": { + "isDefault": { + "type": "string" + }, + "content.@type": { "anyOf": [ { "type": "string" @@ -16775,7 +16806,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.validTo": { + "content.tags": { "anyOf": [ { "type": "string" @@ -16788,7 +16819,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.key": { + "content.owner": { "anyOf": [ { "type": "string" @@ -16801,7 +16832,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.isTechnical": { + "content.validFrom": { "anyOf": [ { "type": "string" @@ -16814,7 +16845,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.confidentiality": { + "content.validTo": { "anyOf": [ { "type": "string" @@ -16827,7 +16858,7 @@ export const GetAttributesRequest: any = { } ] }, - "content.value.@type": { + "content.key": { "anyOf": [ { "type": "string" @@ -16840,7 +16871,10 @@ export const GetAttributesRequest: any = { } ] }, - "succeeds": { + "content.isTechnical": { + "type": "string" + }, + "content.confidentiality": { "anyOf": [ { "type": "string" @@ -16853,7 +16887,7 @@ export const GetAttributesRequest: any = { } ] }, - "succeededBy": { + "content.value.@type": { "anyOf": [ { "type": "string" @@ -17007,7 +17041,7 @@ export const GetOwnSharedAttributesRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" }, "GetOwnSharedAttributeRequestQuery": { "type": "object", @@ -17081,17 +17115,7 @@ export const GetOwnSharedAttributesRequest: any = { ] }, "content.isTechnical": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] + "type": "string" }, "content.confidentiality": { "anyOf": [ @@ -17247,7 +17271,7 @@ export const GetPeerSharedAttributesRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" }, "GetPeerSharedAttributesRequestQuery": { "type": "object", @@ -17321,17 +17345,7 @@ export const GetPeerSharedAttributesRequest: any = { ] }, "content.isTechnical": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] + "type": "string" }, "content.confidentiality": { "anyOf": [ @@ -17466,6 +17480,9 @@ export const GetRepositoryAttributesRequest: any = { "createdAt": { "type": "string" }, + "isDefault": { + "type": "string" + }, "content.tags": { "anyOf": [ { @@ -17556,44 +17573,7 @@ export const GetSharedVersionsOfAttributeRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" - } - } -} - -export const GetSharedVersionsOfRepositoryAttributeRequest: any = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetSharedVersionsOfRepositoryAttributeRequest", - "definitions": { - "GetSharedVersionsOfRepositoryAttributeRequest": { - "type": "object", - "properties": { - "attributeId": { - "$ref": "#/definitions/AttributeIdString" - }, - "peers": { - "type": "array", - "items": { - "$ref": "#/definitions/AddressString" - } - }, - "onlyLatestVersions": { - "type": "boolean", - "description": "default: true" - } - }, - "required": [ - "attributeId" - ], - "additionalProperties": false - }, - "AttributeIdString": { - "type": "string", - "pattern": "ATT[A-Za-z0-9]{17}" - }, - "AddressString": { - "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" } } } @@ -17647,7 +17627,7 @@ export const NotifyPeerAboutRepositoryAttributeSuccessionRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" } } } @@ -17714,7 +17694,7 @@ export const ShareRepositoryAttributeRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" }, "ISO8601DateTimeString": { "type": "string", @@ -20379,22 +20359,11 @@ export const RegisterPushNotificationTokenRequest: any = { } } -export const SyncDatawalletRequest: any = { +export const GetIdentityDeletionProcessRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/SyncDatawalletRequest", + "$ref": "#/definitions/GetIdentityDeletionProcessRequest", "definitions": { - "SyncDatawalletRequest": { - "type": "object", - "additionalProperties": false - } - } -} - -export const GetIdentityDeletionProcessRequest: any = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetIdentityDeletionProcessRequest", - "definitions": { - "GetIdentityDeletionProcessRequest": { + "GetIdentityDeletionProcessRequest": { "type": "object", "properties": { "id": { @@ -20444,17 +20413,6 @@ export const DownloadAttachmentRequest: any = { } } -export const SyncEverythingRequest: any = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/SyncEverythingRequest", - "definitions": { - "SyncEverythingRequest": { - "type": "object", - "additionalProperties": false - } - } -} - export const CreateRelationshipChallengeRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/CreateRelationshipChallengeRequest", @@ -20810,6 +20768,213 @@ export const GetDeviceOnboardingInfoRequest: any = { } } +export const SetCommunicationLanguageRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/SetCommunicationLanguageRequest", + "definitions": { + "SetCommunicationLanguageRequest": { + "type": "object", + "properties": { + "communicationLanguage": { + "$ref": "#/definitions/LanguageISO639" + } + }, + "required": [ + "communicationLanguage" + ], + "additionalProperties": false + }, + "LanguageISO639": { + "type": "string", + "enum": [ + "aa", + "ab", + "ae", + "af", + "ak", + "am", + "an", + "ar", + "as", + "av", + "ay", + "az", + "ba", + "be", + "bg", + "bi", + "bm", + "bn", + "bo", + "br", + "bs", + "ca", + "ce", + "ch", + "co", + "cr", + "cs", + "cu", + "cv", + "cy", + "da", + "de", + "dv", + "dz", + "ee", + "el", + "en", + "eo", + "es", + "et", + "eu", + "fa", + "ff", + "fi", + "fj", + "fo", + "fr", + "fy", + "ga", + "gd", + "gl", + "gn", + "gu", + "gv", + "ha", + "he", + "hi", + "ho", + "hr", + "ht", + "hu", + "hy", + "hz", + "ia", + "id", + "ie", + "ig", + "ii", + "ik", + "io", + "is", + "it", + "iu", + "ja", + "jv", + "ka", + "kg", + "ki", + "kj", + "kk", + "kl", + "km", + "kn", + "ko", + "kr", + "ks", + "ku", + "kv", + "kw", + "ky", + "la", + "lb", + "lg", + "li", + "ln", + "lo", + "lt", + "lu", + "lv", + "mg", + "mh", + "mi", + "mk", + "ml", + "mn", + "mr", + "ms", + "mt", + "my", + "na", + "nb", + "nd", + "ne", + "ng", + "nl", + "nn", + "no", + "nr", + "nv", + "ny", + "oc", + "oj", + "om", + "or", + "os", + "pa", + "pi", + "pl", + "ps", + "pt", + "qu", + "rm", + "rn", + "ro", + "ru", + "rw", + "sa", + "sc", + "sd", + "se", + "sg", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "sq", + "sr", + "ss", + "st", + "su", + "sv", + "sw", + "ta", + "te", + "tg", + "th", + "ti", + "tk", + "tl", + "tn", + "to", + "tr", + "ts", + "tt", + "tw", + "ty", + "ug", + "uk", + "ur", + "uz", + "ve", + "vi", + "vo", + "wa", + "wo", + "xh", + "yi", + "yo", + "za", + "zh", + "zu" + ] + } + } +} + export const UpdateDeviceRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/UpdateDeviceRequest", @@ -21361,29 +21526,6 @@ export const UploadOwnFileValidatableRequest: any = { } } -export const CheckIdentityRequest: any = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/CheckIdentityRequest", - "definitions": { - "CheckIdentityRequest": { - "type": "object", - "properties": { - "address": { - "$ref": "#/definitions/AddressString" - } - }, - "required": [ - "address" - ], - "additionalProperties": false - }, - "AddressString": { - "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" - } - } -} - export const GetAttachmentMetadataRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/GetAttachmentMetadataRequest", @@ -21679,7 +21821,7 @@ export const SendMessageRequest: any = { }, "AddressString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" }, "FileIdString": { "type": "string", @@ -21688,54 +21830,49 @@ export const SendMessageRequest: any = { } } -export const AcceptRelationshipChangeRequest: any = { +export const CreateOwnRelationshipTemplateRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/AcceptRelationshipChangeRequest", + "$ref": "#/definitions/CreateOwnRelationshipTemplateRequest", "definitions": { - "AcceptRelationshipChangeRequest": { + "CreateOwnRelationshipTemplateRequest": { "type": "object", "properties": { - "relationshipId": { - "$ref": "#/definitions/RelationshipIdString" - }, - "changeId": { - "$ref": "#/definitions/RelationshipChangeIdString" + "expiresAt": { + "$ref": "#/definitions/ISO8601DateTimeString" }, - "content": {} + "content": {}, + "maxNumberOfAllocations": { + "type": "number", + "minimum": 1 + } }, "required": [ - "relationshipId", - "changeId", + "expiresAt", "content" ], "additionalProperties": false }, - "RelationshipIdString": { - "type": "string", - "pattern": "REL[A-Za-z0-9]{17}" - }, - "RelationshipChangeIdString": { + "ISO8601DateTimeString": { "type": "string", - "pattern": "RCH[A-Za-z0-9]{17}" + "errorMessage": "must match ISO8601 datetime format", + "pattern": "^([+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24:?00)([.,]\\d+(?!:))?)?(\\17[0-5]\\d([.,]\\d+)?)?([zZ]|([+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" } } } -export const CreateRelationshipRequest: any = { +export const CreateQRCodeForOwnTemplateRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/CreateRelationshipRequest", + "$ref": "#/definitions/CreateQRCodeForOwnTemplateRequest", "definitions": { - "CreateRelationshipRequest": { + "CreateQRCodeForOwnTemplateRequest": { "type": "object", "properties": { "templateId": { "$ref": "#/definitions/RelationshipTemplateIdString" - }, - "content": {} + } }, "required": [ - "templateId", - "content" + "templateId" ], "additionalProperties": false }, @@ -21746,99 +21883,114 @@ export const CreateRelationshipRequest: any = { } } -export const GetAttributesForRelationshipRequest: any = { +export const CreateTokenForOwnTemplateRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetAttributesForRelationshipRequest", + "$ref": "#/definitions/CreateTokenForOwnTemplateRequest", "definitions": { - "GetAttributesForRelationshipRequest": { + "CreateTokenForOwnTemplateRequest": { "type": "object", "properties": { - "id": { - "$ref": "#/definitions/RelationshipIdString" + "templateId": { + "$ref": "#/definitions/RelationshipTemplateIdString" }, - "hideTechnical": { - "type": "boolean" + "expiresAt": { + "$ref": "#/definitions/ISO8601DateTimeString" }, - "onlyLatestVersions": { - "type": "boolean", - "description": "default: true" + "ephemeral": { + "type": "boolean" } }, "required": [ - "id" + "templateId" ], "additionalProperties": false }, - "RelationshipIdString": { + "RelationshipTemplateIdString": { "type": "string", - "pattern": "REL[A-Za-z0-9]{17}" + "pattern": "RLT[A-Za-z0-9]{17}" + }, + "ISO8601DateTimeString": { + "type": "string", + "errorMessage": "must match ISO8601 datetime format", + "pattern": "^([+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24:?00)([.,]\\d+(?!:))?)?(\\17[0-5]\\d([.,]\\d+)?)?([zZ]|([+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" } } } -export const GetRelationshipRequest: any = { +export const CreateTokenQRCodeForOwnTemplateRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetRelationshipRequest", + "$ref": "#/definitions/CreateTokenQRCodeForOwnTemplateRequest", "definitions": { - "GetRelationshipRequest": { + "CreateTokenQRCodeForOwnTemplateRequest": { "type": "object", "properties": { - "id": { - "$ref": "#/definitions/RelationshipIdString" + "templateId": { + "$ref": "#/definitions/RelationshipTemplateIdString" + }, + "expiresAt": { + "$ref": "#/definitions/ISO8601DateTimeString" } }, "required": [ - "id" + "templateId" ], "additionalProperties": false }, - "RelationshipIdString": { + "RelationshipTemplateIdString": { "type": "string", - "pattern": "REL[A-Za-z0-9]{17}" + "pattern": "RLT[A-Za-z0-9]{17}" + }, + "ISO8601DateTimeString": { + "type": "string", + "errorMessage": "must match ISO8601 datetime format", + "pattern": "^([+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24:?00)([.,]\\d+(?!:))?)?(\\17[0-5]\\d([.,]\\d+)?)?([zZ]|([+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" } } } -export const GetRelationshipByAddressRequest: any = { +export const GetRelationshipTemplateRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetRelationshipByAddressRequest", + "$ref": "#/definitions/GetRelationshipTemplateRequest", "definitions": { - "GetRelationshipByAddressRequest": { + "GetRelationshipTemplateRequest": { "type": "object", "properties": { - "address": { - "$ref": "#/definitions/AddressString" + "id": { + "$ref": "#/definitions/RelationshipTemplateIdString" } }, "required": [ - "address" + "id" ], "additionalProperties": false }, - "AddressString": { + "RelationshipTemplateIdString": { "type": "string", - "pattern": "id1[A-Za-z0-9]{32,33}" + "pattern": "RLT[A-Za-z0-9]{17}" } } } -export const GetRelationshipsRequest: any = { +export const GetRelationshipTemplatesRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetRelationshipsRequest", + "$ref": "#/definitions/GetRelationshipTemplatesRequest", "definitions": { - "GetRelationshipsRequest": { + "GetRelationshipTemplatesRequest": { "type": "object", "properties": { "query": { - "$ref": "#/definitions/GetRelationshipsQuery" + "$ref": "#/definitions/GetRelationshipTemplatesQuery" + }, + "ownerRestriction": { + "$ref": "#/definitions/OwnerRestriction" } }, "additionalProperties": false }, - "GetRelationshipsQuery": { + "GetRelationshipTemplatesQuery": { "type": "object", "properties": { - "peer": { + "isOwn": { "anyOf": [ { "type": "string" @@ -21851,7 +22003,7 @@ export const GetRelationshipsRequest: any = { } ] }, - "status": { + "createdAt": { "anyOf": [ { "type": "string" @@ -21864,7 +22016,46 @@ export const GetRelationshipsRequest: any = { } ] }, - "template.id": { + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "createdBy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "createdByDevice": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "maxNumberOfAllocations": { "anyOf": [ { "type": "string" @@ -21879,203 +22070,279 @@ export const GetRelationshipsRequest: any = { } }, "additionalProperties": false + }, + "OwnerRestriction": { + "type": "string", + "enum": [ + "o", + "p" + ] } } } -export const RejectRelationshipChangeRequest: any = { +export const LoadPeerRelationshipTemplateViaSecretRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/RejectRelationshipChangeRequest", + "$ref": "#/definitions/LoadPeerRelationshipTemplateViaSecretRequest", "definitions": { - "RejectRelationshipChangeRequest": { + "LoadPeerRelationshipTemplateViaSecretRequest": { "type": "object", "properties": { - "relationshipId": { - "$ref": "#/definitions/RelationshipIdString" - }, - "changeId": { - "$ref": "#/definitions/RelationshipChangeIdString" + "id": { + "$ref": "#/definitions/RelationshipTemplateIdString" }, - "content": {} + "secretKey": { + "type": "string", + "minLength": 10 + } }, "required": [ - "relationshipId", - "changeId", - "content" + "id", + "secretKey" ], "additionalProperties": false }, - "RelationshipIdString": { - "type": "string", - "pattern": "REL[A-Za-z0-9]{17}" - }, - "RelationshipChangeIdString": { + "RelationshipTemplateIdString": { "type": "string", - "pattern": "RCH[A-Za-z0-9]{17}" + "pattern": "RLT[A-Za-z0-9]{17}" } } } -export const RevokeRelationshipChangeRequest: any = { +export const LoadPeerRelationshipTemplateViaReferenceRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/RevokeRelationshipChangeRequest", + "$ref": "#/definitions/LoadPeerRelationshipTemplateViaReferenceRequest", "definitions": { - "RevokeRelationshipChangeRequest": { + "LoadPeerRelationshipTemplateViaReferenceRequest": { "type": "object", "properties": { - "relationshipId": { - "$ref": "#/definitions/RelationshipIdString" - }, - "changeId": { - "$ref": "#/definitions/RelationshipChangeIdString" - }, - "content": {} - }, - "required": [ - "relationshipId", - "changeId", - "content" - ], - "additionalProperties": false + "reference": { + "anyOf": [ + { + "$ref": "#/definitions/TokenReferenceString" + }, + { + "$ref": "#/definitions/RelationshipTemplateReferenceString" + } + ] + } + }, + "required": [ + "reference" + ], + "additionalProperties": false, + "errorMessage": "token / relationship template reference invalid" }, - "RelationshipIdString": { + "TokenReferenceString": { "type": "string", - "pattern": "REL[A-Za-z0-9]{17}" + "pattern": "VE9L.{84}" }, - "RelationshipChangeIdString": { + "RelationshipTemplateReferenceString": { "type": "string", - "pattern": "RCH[A-Za-z0-9]{17}" + "pattern": "UkxU.{84}" } } } -export const CreateOwnRelationshipTemplateRequest: any = { +export const LoadPeerRelationshipTemplateRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/CreateOwnRelationshipTemplateRequest", + "$ref": "#/definitions/LoadPeerRelationshipTemplateRequest", "definitions": { - "CreateOwnRelationshipTemplateRequest": { + "LoadPeerRelationshipTemplateRequest": { + "anyOf": [ + { + "$ref": "#/definitions/LoadPeerRelationshipTemplateViaSecretRequest" + }, + { + "$ref": "#/definitions/LoadPeerRelationshipTemplateViaReferenceRequest" + } + ] + }, + "LoadPeerRelationshipTemplateViaSecretRequest": { "type": "object", "properties": { - "expiresAt": { - "$ref": "#/definitions/ISO8601DateTimeString" + "id": { + "$ref": "#/definitions/RelationshipTemplateIdString" }, - "content": {}, - "maxNumberOfAllocations": { - "type": "number", - "minimum": 1 + "secretKey": { + "type": "string", + "minLength": 10 } }, "required": [ - "expiresAt", - "content" + "id", + "secretKey" ], "additionalProperties": false }, - "ISO8601DateTimeString": { + "RelationshipTemplateIdString": { "type": "string", - "errorMessage": "must match ISO8601 datetime format", - "pattern": "^([+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24:?00)([.,]\\d+(?!:))?)?(\\17[0-5]\\d([.,]\\d+)?)?([zZ]|([+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" + "pattern": "RLT[A-Za-z0-9]{17}" + }, + "LoadPeerRelationshipTemplateViaReferenceRequest": { + "type": "object", + "properties": { + "reference": { + "anyOf": [ + { + "$ref": "#/definitions/TokenReferenceString" + }, + { + "$ref": "#/definitions/RelationshipTemplateReferenceString" + } + ] + } + }, + "required": [ + "reference" + ], + "additionalProperties": false, + "errorMessage": "token / relationship template reference invalid" + }, + "TokenReferenceString": { + "type": "string", + "pattern": "VE9L.{84}" + }, + "RelationshipTemplateReferenceString": { + "type": "string", + "pattern": "UkxU.{84}" } } } -export const CreateQRCodeForOwnTemplateRequest: any = { +export const AcceptRelationshipRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/CreateQRCodeForOwnTemplateRequest", + "$ref": "#/definitions/AcceptRelationshipRequest", "definitions": { - "CreateQRCodeForOwnTemplateRequest": { + "AcceptRelationshipRequest": { "type": "object", "properties": { - "templateId": { - "$ref": "#/definitions/RelationshipTemplateIdString" + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" } }, "required": [ - "templateId" + "relationshipId" ], "additionalProperties": false }, - "RelationshipTemplateIdString": { + "RelationshipIdString": { "type": "string", - "pattern": "RLT[A-Za-z0-9]{17}" + "pattern": "REL[A-Za-z0-9]{17}" } } } -export const CreateTokenForOwnTemplateRequest: any = { +export const AcceptRelationshipReactivationRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/CreateTokenForOwnTemplateRequest", + "$ref": "#/definitions/AcceptRelationshipReactivationRequest", "definitions": { - "CreateTokenForOwnTemplateRequest": { + "AcceptRelationshipReactivationRequest": { + "type": "object", + "properties": { + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" + } + }, + "required": [ + "relationshipId" + ], + "additionalProperties": false + }, + "RelationshipIdString": { + "type": "string", + "pattern": "REL[A-Za-z0-9]{17}" + } + } +} + +export const CreateRelationshipRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/CreateRelationshipRequest", + "definitions": { + "CreateRelationshipRequest": { "type": "object", "properties": { "templateId": { "$ref": "#/definitions/RelationshipTemplateIdString" }, - "expiresAt": { - "$ref": "#/definitions/ISO8601DateTimeString" - }, - "ephemeral": { - "type": "boolean" - } + "creationContent": {} }, "required": [ - "templateId" + "templateId", + "creationContent" ], "additionalProperties": false }, "RelationshipTemplateIdString": { "type": "string", "pattern": "RLT[A-Za-z0-9]{17}" + } + } +} + +export const DecomposeRelationshipRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/DecomposeRelationshipRequest", + "definitions": { + "DecomposeRelationshipRequest": { + "type": "object", + "properties": { + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" + } + }, + "required": [ + "relationshipId" + ], + "additionalProperties": false }, - "ISO8601DateTimeString": { + "RelationshipIdString": { "type": "string", - "errorMessage": "must match ISO8601 datetime format", - "pattern": "^([+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24:?00)([.,]\\d+(?!:))?)?(\\17[0-5]\\d([.,]\\d+)?)?([zZ]|([+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" + "pattern": "REL[A-Za-z0-9]{17}" } } } -export const CreateTokenQRCodeForOwnTemplateRequest: any = { +export const GetAttributesForRelationshipRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/CreateTokenQRCodeForOwnTemplateRequest", + "$ref": "#/definitions/GetAttributesForRelationshipRequest", "definitions": { - "CreateTokenQRCodeForOwnTemplateRequest": { + "GetAttributesForRelationshipRequest": { "type": "object", "properties": { - "templateId": { - "$ref": "#/definitions/RelationshipTemplateIdString" + "id": { + "$ref": "#/definitions/RelationshipIdString" }, - "expiresAt": { - "$ref": "#/definitions/ISO8601DateTimeString" + "hideTechnical": { + "type": "boolean" + }, + "onlyLatestVersions": { + "type": "boolean", + "description": "default: true" } }, "required": [ - "templateId" + "id" ], "additionalProperties": false }, - "RelationshipTemplateIdString": { - "type": "string", - "pattern": "RLT[A-Za-z0-9]{17}" - }, - "ISO8601DateTimeString": { + "RelationshipIdString": { "type": "string", - "errorMessage": "must match ISO8601 datetime format", - "pattern": "^([+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24:?00)([.,]\\d+(?!:))?)?(\\17[0-5]\\d([.,]\\d+)?)?([zZ]|([+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" + "pattern": "REL[A-Za-z0-9]{17}" } } } -export const GetRelationshipTemplateRequest: any = { +export const GetRelationshipRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetRelationshipTemplateRequest", + "$ref": "#/definitions/GetRelationshipRequest", "definitions": { - "GetRelationshipTemplateRequest": { + "GetRelationshipRequest": { "type": "object", "properties": { "id": { - "$ref": "#/definitions/RelationshipTemplateIdString" + "$ref": "#/definitions/RelationshipIdString" } }, "required": [ @@ -22083,72 +22350,53 @@ export const GetRelationshipTemplateRequest: any = { ], "additionalProperties": false }, - "RelationshipTemplateIdString": { + "RelationshipIdString": { "type": "string", - "pattern": "RLT[A-Za-z0-9]{17}" + "pattern": "REL[A-Za-z0-9]{17}" } } } -export const GetRelationshipTemplatesRequest: any = { +export const GetRelationshipByAddressRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/GetRelationshipTemplatesRequest", + "$ref": "#/definitions/GetRelationshipByAddressRequest", "definitions": { - "GetRelationshipTemplatesRequest": { + "GetRelationshipByAddressRequest": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/AddressString" + } + }, + "required": [ + "address" + ], + "additionalProperties": false + }, + "AddressString": { + "type": "string", + "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" + } + } +} + +export const GetRelationshipsRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/GetRelationshipsRequest", + "definitions": { + "GetRelationshipsRequest": { "type": "object", "properties": { "query": { - "$ref": "#/definitions/GetRelationshipTemplatesQuery" - }, - "ownerRestriction": { - "$ref": "#/definitions/OwnerRestriction" + "$ref": "#/definitions/GetRelationshipsQuery" } }, "additionalProperties": false }, - "GetRelationshipTemplatesQuery": { + "GetRelationshipsQuery": { "type": "object", "properties": { - "isOwn": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "expiresAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "createdBy": { + "peer": { "anyOf": [ { "type": "string" @@ -22161,7 +22409,7 @@ export const GetRelationshipTemplatesRequest: any = { } ] }, - "createdByDevice": { + "status": { "anyOf": [ { "type": "string" @@ -22174,7 +22422,7 @@ export const GetRelationshipTemplatesRequest: any = { } ] }, - "maxNumberOfAllocations": { + "template.id": { "anyOf": [ { "type": "string" @@ -22189,142 +22437,144 @@ export const GetRelationshipTemplatesRequest: any = { } }, "additionalProperties": false - }, - "OwnerRestriction": { - "type": "string", - "enum": [ - "o", - "p" - ] } } } -export const LoadPeerRelationshipTemplateViaSecretRequest: any = { +export const RejectRelationshipRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/LoadPeerRelationshipTemplateViaSecretRequest", + "$ref": "#/definitions/RejectRelationshipRequest", "definitions": { - "LoadPeerRelationshipTemplateViaSecretRequest": { + "RejectRelationshipRequest": { "type": "object", "properties": { - "id": { - "$ref": "#/definitions/RelationshipTemplateIdString" - }, - "secretKey": { - "type": "string", - "minLength": 10 + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" } }, "required": [ - "id", - "secretKey" + "relationshipId" ], "additionalProperties": false }, - "RelationshipTemplateIdString": { + "RelationshipIdString": { "type": "string", - "pattern": "RLT[A-Za-z0-9]{17}" + "pattern": "REL[A-Za-z0-9]{17}" } } } -export const LoadPeerRelationshipTemplateViaReferenceRequest: any = { +export const RejectRelationshipReactivationRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/LoadPeerRelationshipTemplateViaReferenceRequest", + "$ref": "#/definitions/RejectRelationshipReactivationRequest", "definitions": { - "LoadPeerRelationshipTemplateViaReferenceRequest": { + "RejectRelationshipReactivationRequest": { "type": "object", "properties": { - "reference": { - "anyOf": [ - { - "$ref": "#/definitions/TokenReferenceString" - }, - { - "$ref": "#/definitions/RelationshipTemplateReferenceString" - } - ] + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" } }, "required": [ - "reference" + "relationshipId" ], - "additionalProperties": false, - "errorMessage": "token / relationship template reference invalid" - }, - "TokenReferenceString": { - "type": "string", - "pattern": "VE9L.{84}" + "additionalProperties": false }, - "RelationshipTemplateReferenceString": { + "RelationshipIdString": { "type": "string", - "pattern": "UkxU.{84}" + "pattern": "REL[A-Za-z0-9]{17}" } } } -export const LoadPeerRelationshipTemplateRequest: any = { +export const RequestRelationshipReactivationRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/LoadPeerRelationshipTemplateRequest", + "$ref": "#/definitions/RequestRelationshipReactivationRequest", "definitions": { - "LoadPeerRelationshipTemplateRequest": { - "anyOf": [ - { - "$ref": "#/definitions/LoadPeerRelationshipTemplateViaSecretRequest" - }, - { - "$ref": "#/definitions/LoadPeerRelationshipTemplateViaReferenceRequest" + "RequestRelationshipReactivationRequest": { + "type": "object", + "properties": { + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" } - ] + }, + "required": [ + "relationshipId" + ], + "additionalProperties": false }, - "LoadPeerRelationshipTemplateViaSecretRequest": { + "RelationshipIdString": { + "type": "string", + "pattern": "REL[A-Za-z0-9]{17}" + } + } +} + +export const RevokeRelationshipRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/RevokeRelationshipRequest", + "definitions": { + "RevokeRelationshipRequest": { "type": "object", "properties": { - "id": { - "$ref": "#/definitions/RelationshipTemplateIdString" - }, - "secretKey": { - "type": "string", - "minLength": 10 + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" } }, "required": [ - "id", - "secretKey" + "relationshipId" ], "additionalProperties": false }, - "RelationshipTemplateIdString": { + "RelationshipIdString": { "type": "string", - "pattern": "RLT[A-Za-z0-9]{17}" - }, - "LoadPeerRelationshipTemplateViaReferenceRequest": { + "pattern": "REL[A-Za-z0-9]{17}" + } + } +} + +export const RevokeRelationshipReactivationRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/RevokeRelationshipReactivationRequest", + "definitions": { + "RevokeRelationshipReactivationRequest": { "type": "object", "properties": { - "reference": { - "anyOf": [ - { - "$ref": "#/definitions/TokenReferenceString" - }, - { - "$ref": "#/definitions/RelationshipTemplateReferenceString" - } - ] + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" } }, "required": [ - "reference" + "relationshipId" ], - "additionalProperties": false, - "errorMessage": "token / relationship template reference invalid" + "additionalProperties": false }, - "TokenReferenceString": { + "RelationshipIdString": { "type": "string", - "pattern": "VE9L.{84}" + "pattern": "REL[A-Za-z0-9]{17}" + } + } +} + +export const TerminateRelationshipRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/TerminateRelationshipRequest", + "definitions": { + "TerminateRelationshipRequest": { + "type": "object", + "properties": { + "relationshipId": { + "$ref": "#/definitions/RelationshipIdString" + } + }, + "required": [ + "relationshipId" + ], + "additionalProperties": false }, - "RelationshipTemplateReferenceString": { + "RelationshipIdString": { "type": "string", - "pattern": "UkxU.{84}" + "pattern": "REL[A-Za-z0-9]{17}" } } } @@ -22610,27 +22860,4 @@ export const LoadPeerTokenRequest: any = { "pattern": "TOK[A-Za-z0-9]{17}" } } -} - -export const DeleteSharedAttributesForRejectedOrRevokedRelationshipRequest: any = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/DeleteSharedAttributesForRejectedOrRevokedRelationshipRequest", - "definitions": { - "DeleteSharedAttributesForRejectedOrRevokedRelationshipRequest": { - "type": "object", - "properties": { - "relationshipId": { - "$ref": "#/definitions/RelationshipIdString" - } - }, - "required": [ - "relationshipId" - ], - "additionalProperties": false - }, - "RelationshipIdString": { - "type": "string", - "pattern": "REL[A-Za-z0-9]{17}" - } - } } \ No newline at end of file diff --git a/packages/runtime/src/useCases/common/validation/ValidatableStrings.ts b/packages/runtime/src/useCases/common/validation/ValidatableStrings.ts index 64b039f8e..26e342c4c 100644 --- a/packages/runtime/src/useCases/common/validation/ValidatableStrings.ts +++ b/packages/runtime/src/useCases/common/validation/ValidatableStrings.ts @@ -1,5 +1,5 @@ /** - * @pattern id1[A-Za-z0-9]{32,33} + * @pattern did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22} */ export type AddressString = string; @@ -33,11 +33,6 @@ export type LocalDraftIdString = string; */ export type LocalSettingIdString = string; -/** - * @pattern RCH[A-Za-z0-9]{17} - */ -export type RelationshipChangeIdString = string; - /** * @pattern MSG[A-Za-z0-9]{17} */ diff --git a/packages/runtime/src/useCases/consumption/attributes/AttributeMapper.ts b/packages/runtime/src/useCases/consumption/attributes/AttributeMapper.ts index f57daac89..bef4a1c3d 100644 --- a/packages/runtime/src/useCases/consumption/attributes/AttributeMapper.ts +++ b/packages/runtime/src/useCases/consumption/attributes/AttributeMapper.ts @@ -11,7 +11,8 @@ export class AttributeMapper { succeeds: attribute.succeeds?.toString(), succeededBy: attribute.succeededBy?.toString(), shareInfo: attribute.shareInfo?.toJSON() as LocalAttributeShareInfoJSON, - deletionInfo: attribute.deletionInfo?.toJSON() as LocalAttributeDeletionInfoJSON + deletionInfo: attribute.deletionInfo?.toJSON() as LocalAttributeDeletionInfoJSON, + isDefault: attribute.isDefault }; } diff --git a/packages/runtime/src/useCases/consumption/attributes/ChangeDefaultRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/ChangeDefaultRepositoryAttribute.ts new file mode 100644 index 000000000..3773107fd --- /dev/null +++ b/packages/runtime/src/useCases/consumption/attributes/ChangeDefaultRepositoryAttribute.ts @@ -0,0 +1,46 @@ +import { Result } from "@js-soft/ts-utils"; +import { AttributesController, LocalAttribute } from "@nmshd/consumption"; +import { AccountController, CoreId } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { LocalAttributeDTO } from "../../../types"; +import { AttributeIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { AttributeMapper } from "./AttributeMapper"; + +export interface ChangeDefaultRepositoryAttributeRequest { + attributeId: AttributeIdString; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("ChangeDefaultRepositoryAttributeRequest")); + } +} + +export class ChangeDefaultRepositoryAttributeUseCase extends UseCase { + public constructor( + @Inject private readonly attributesController: AttributesController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: ChangeDefaultRepositoryAttributeRequest): Promise> { + const newDefaultAttribute = await this.attributesController.getLocalAttribute(CoreId.from(request.attributeId)); + if (!newDefaultAttribute) return Result.fail(RuntimeErrors.general.recordNotFound(LocalAttribute)); + + if (!newDefaultAttribute.isRepositoryAttribute(this.accountController.identity.address)) { + return Result.fail(RuntimeErrors.attributes.isNotRepositoryAttribute(CoreId.from(request.attributeId))); + } + + if (newDefaultAttribute.succeededBy) { + return Result.fail(RuntimeErrors.attributes.hasSuccessor(newDefaultAttribute)); + } + + const defaultRepositoryAttribute = await this.attributesController.setAsDefaultRepositoryAttribute(newDefaultAttribute, false); + + await this.accountController.syncDatawallet(); + + return Result.ok(AttributeMapper.toAttributeDTO(defaultRepositoryAttribute)); + } +} diff --git a/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts index b7cc32a63..7395cb353 100644 --- a/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts +++ b/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts @@ -1,5 +1,5 @@ import { Result } from "@js-soft/ts-utils"; -import { AttributesController, CreateLocalAttributeParams } from "@nmshd/consumption"; +import { AttributesController, CreateRepositoryAttributeParams } from "@nmshd/consumption"; import { AttributeValues } from "@nmshd/content"; import { AccountController } from "@nmshd/transport"; import { Inject } from "typescript-ioc"; @@ -32,16 +32,17 @@ export class CreateRepositoryAttributeUseCase extends UseCase> { - const params = CreateLocalAttributeParams.from({ + const params = CreateRepositoryAttributeParams.from({ content: { "@type": "IdentityAttribute", owner: this.accountController.identity.address.toString(), ...request.content } }); - const createdAttribute = await this.attributeController.createLocalAttribute(params); + const createdLocalAttribute = await this.attributeController.createRepositoryAttribute(params); + await this.accountController.syncDatawallet(); - return Result.ok(AttributeMapper.toAttributeDTO(createdAttribute)); + return Result.ok(AttributeMapper.toAttributeDTO(createdLocalAttribute)); } } diff --git a/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts index 60031cba7..abfa816de 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts @@ -18,18 +18,19 @@ export interface GetAttributesRequest { export interface GetAttributesRequestQuery { createdAt?: string; - parentId?: string; + parentId?: string | string[]; + succeeds?: string | string[]; + succeededBy?: string | string[]; + isDefault?: string; "content.@type"?: string | string[]; "content.tags"?: string | string[]; "content.owner"?: string | string[]; "content.validFrom"?: string | string[]; "content.validTo"?: string | string[]; "content.key"?: string | string[]; - "content.isTechnical"?: string | string[]; + "content.isTechnical"?: string; "content.confidentiality"?: string | string[]; "content.value.@type"?: string | string[]; - succeeds?: string | string[]; - succeededBy?: string | string[]; shareInfo?: string | string[]; "shareInfo.requestReference"?: string | string[]; "shareInfo.notificationReference"?: string | string[]; @@ -47,6 +48,7 @@ export class GetAttributesUseCase extends UseCase((x) => x.parentId)]: true, [nameof((x) => x.succeeds)]: true, [nameof((x) => x.succeededBy)]: true, + [nameof((x) => x.isDefault)]: true, // content.abstractAttribute [`${nameof((x) => x.content)}.${nameof((x) => x.validFrom)}`]: true, @@ -80,6 +82,7 @@ export class GetAttributesUseCase extends UseCase((x) => x.parentId)]: nameof((x) => x.parentId), [nameof((x) => x.succeeds)]: nameof((x) => x.succeeds), [nameof((x) => x.succeededBy)]: nameof((x) => x.succeededBy), + [nameof((x) => x.isDefault)]: nameof((x) => x.isDefault), // content.abstractAttribute [`${nameof((x) => x.content)}.${nameof((x) => x.validFrom)}`]: `${nameof((x) => x.content)}.${nameof((x) => x.validFrom)}`, diff --git a/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts index 0eca7b7f6..2eb864042 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts @@ -26,7 +26,7 @@ export interface GetOwnSharedAttributeRequestQuery { "content.validFrom"?: string | string[]; "content.validTo"?: string | string[]; "content.key"?: string | string[]; - "content.isTechnical"?: string | string[]; + "content.isTechnical"?: string; "content.confidentiality"?: string | string[]; "content.value.@type"?: string | string[]; shareInfo?: string | string[]; diff --git a/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts index 23764f3ca..517515021 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts @@ -25,7 +25,7 @@ export interface GetPeerSharedAttributesRequestQuery { "content.validFrom"?: string | string[]; "content.validTo"?: string | string[]; "content.key"?: string | string[]; - "content.isTechnical"?: string | string[]; + "content.isTechnical"?: string; "content.confidentiality"?: string | string[]; "content.value.@type"?: string | string[]; shareInfo?: string | string[]; diff --git a/packages/runtime/src/useCases/consumption/attributes/GetRepositoryAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetRepositoryAttributes.ts index 0395fba62..ff43cd711 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetRepositoryAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetRepositoryAttributes.ts @@ -3,7 +3,7 @@ import { AttributesController } from "@nmshd/consumption"; import { Inject } from "typescript-ioc"; import { AttributeMapper, GetAttributesRequestQuery, GetAttributesUseCase } from ".."; import { LocalAttributeDTO } from "../../../types"; -import { flattenObject, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { SchemaRepository, SchemaValidator, UseCase, flattenObject } from "../../common"; export interface GetRepositoryAttributesRequest { /** @@ -15,6 +15,7 @@ export interface GetRepositoryAttributesRequest { export interface GetRepositoryAttributesRequestQuery { createdAt?: string; + isDefault?: string; "content.tags"?: string | string[]; "content.validFrom"?: string | string[]; "content.validTo"?: string | string[]; diff --git a/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts deleted file mode 100644 index 2a1126eab..000000000 --- a/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Result } from "@js-soft/ts-utils"; -import { AttributesController, LocalAttribute } from "@nmshd/consumption"; -import { AccountController, CoreAddress, CoreId } from "@nmshd/transport"; -import { Inject } from "typescript-ioc"; -import { LocalAttributeDTO } from "../../../types"; -import { AddressString, AttributeIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; -import { AttributeMapper } from "./AttributeMapper"; - -export interface GetSharedVersionsOfRepositoryAttributeRequest { - attributeId: AttributeIdString; - peers?: AddressString[]; - /** - * default: true - */ - onlyLatestVersions?: boolean; -} - -class Validator extends SchemaValidator { - public constructor(@Inject schemaRepository: SchemaRepository) { - super(schemaRepository.getSchema("GetSharedVersionsOfRepositoryAttributeRequest")); - } -} - -export class GetSharedVersionsOfRepositoryAttributeUseCase extends UseCase { - public constructor( - @Inject private readonly accountController: AccountController, - @Inject private readonly attributeController: AttributesController, - @Inject validator: Validator - ) { - super(validator); - } - - protected async executeInternal(request: GetSharedVersionsOfRepositoryAttributeRequest): Promise> { - const repositoryAttributeId = CoreId.from(request.attributeId); - const repositoryAttribute = await this.attributeController.getLocalAttribute(repositoryAttributeId); - if (!repositoryAttribute) return Result.fail(RuntimeErrors.general.recordNotFound(LocalAttribute)); - - if (!repositoryAttribute.isRepositoryAttribute(this.accountController.identity.address)) { - return Result.fail(RuntimeErrors.attributes.isNotRepositoryAttribute(repositoryAttributeId)); - } - - if (request.peers?.length === 0) { - return Result.fail(RuntimeErrors.general.invalidPropertyValue("The `peers` property may not be an empty array.")); - } - - const peers = request.peers?.map((address) => CoreAddress.from(address)); - const sharedVersions = await this.attributeController.getSharedVersionsOfAttribute(repositoryAttributeId, peers, request.onlyLatestVersions); - - return Result.ok(AttributeMapper.toAttributeDTOList(sharedVersions)); - } -} diff --git a/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts index bcf1fe233..db2383600 100644 --- a/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts +++ b/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts @@ -1,5 +1,5 @@ import { Result } from "@js-soft/ts-utils"; -import { AttributesController, CreateOutgoingRequestParameters, DeletionStatus, LocalAttribute, OutgoingRequestsController } from "@nmshd/consumption"; +import { AttributesController, CreateOutgoingRequestParameters, ErrorValidationResult, LocalAttribute, OutgoingRequestsController } from "@nmshd/consumption"; import { Request, ShareAttributeRequestItem } from "@nmshd/content"; import { AccountController, CoreAddress, CoreId, MessageController } from "@nmshd/transport"; import { Inject } from "typescript-ioc"; @@ -50,30 +50,6 @@ export class ShareRepositoryAttributeUseCase extends UseCase 0) { - return Result.fail( - RuntimeErrors.attributes.repositoryAttributeHasAlreadyBeenSharedWithPeer(request.attributeId, request.peer, ownSharedIdentityAttributesOfRepositoryAttribute[0].id) - ); - } - - const sharedVersionsOfRepositoryAttribute = await this.attributeController.getSharedVersionsOfAttribute(repositoryAttributeId, [CoreAddress.from(request.peer)], false); - const activeSharedVersions = sharedVersionsOfRepositoryAttribute.filter( - (attr) => attr.deletionInfo?.deletionStatus !== DeletionStatus.DeletedByPeer && attr.deletionInfo?.deletionStatus !== DeletionStatus.ToBeDeletedByPeer - ); - if (activeSharedVersions.length > 0) { - return Result.fail( - RuntimeErrors.attributes.anotherVersionOfRepositoryAttributeHasAlreadyBeenSharedWithPeer(request.attributeId, request.peer, activeSharedVersions[0].id) - ); - } - const requestParams = CreateOutgoingRequestParameters.from({ peer: request.peer, content: Request.from({ @@ -90,7 +66,7 @@ export class ShareRepositoryAttributeUseCase extends UseCase { @@ -27,7 +27,7 @@ export class CompleteIncomingRequestUseCase extends UseCase { + private async getResponseSourceObject(request: CompleteIncomingRequestRequest): Promise { if (!request.responseSourceId) return; if (request.responseSourceId.startsWith("MSG")) { @@ -37,11 +37,11 @@ export class CompleteIncomingRequestUseCase extends UseCase { +export class SyncDatawalletUseCase extends UseCase { public constructor(@Inject private readonly accountController: AccountController) { super(); } - protected async executeInternal(request: SyncDatawalletRequest): Promise> { - await this.accountController.syncDatawallet(true, request.callback); + protected async executeInternal(): Promise> { + await this.accountController.syncDatawallet(true); return Result.ok(undefined); } } diff --git a/packages/runtime/src/useCases/transport/account/SyncEverything.ts b/packages/runtime/src/useCases/transport/account/SyncEverything.ts index 17f8b67ca..b8ffdf705 100644 --- a/packages/runtime/src/useCases/transport/account/SyncEverything.ts +++ b/packages/runtime/src/useCases/transport/account/SyncEverything.ts @@ -15,11 +15,7 @@ export interface SyncEverythingResponse { identityDeletionProcesses: IdentityDeletionProcessDTO[]; } -export interface SyncEverythingRequest { - callback?(percentage: number, syncStep: string): void; -} - -export class SyncEverythingUseCase extends UseCase { +export class SyncEverythingUseCase extends UseCase { private readonly logger: ILogger; public constructor( @Inject private readonly accountController: AccountController, @@ -32,12 +28,12 @@ export class SyncEverythingUseCase extends UseCase>; - protected async executeInternal(request: SyncEverythingRequest): Promise> { + protected async executeInternal(): Promise> { if (this.currentSync) { return await this.currentSync; } - this.currentSync = this._executeInternal(request); + this.currentSync = this._executeInternal(); try { return await this.currentSync; @@ -46,8 +42,8 @@ export class SyncEverythingUseCase extends UseCase { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("SetCommunicationLanguageRequest")); + } +} + +export class SetCommunicationLanguageUseCase extends UseCase { + public constructor( + @Inject private readonly deviceController: DeviceController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: SetCommunicationLanguageRequest): Promise> { + await this.deviceController.setCommunicationLanguage(request.communicationLanguage); + + return Result.ok(undefined); + } +} diff --git a/packages/runtime/src/useCases/transport/devices/index.ts b/packages/runtime/src/useCases/transport/devices/index.ts index 25762c470..6d43c41ad 100644 --- a/packages/runtime/src/useCases/transport/devices/index.ts +++ b/packages/runtime/src/useCases/transport/devices/index.ts @@ -5,4 +5,5 @@ export * from "./DeviceMapper"; export * from "./GetDevice"; export * from "./GetDeviceOnboardingInfo"; export * from "./GetDevices"; +export * from "./SetCommunicationLanguage"; export * from "./UpdateDevice"; diff --git a/packages/runtime/src/useCases/transport/identity/CheckIdentity.ts b/packages/runtime/src/useCases/transport/identity/CheckIdentity.ts deleted file mode 100644 index 27a1e5cf3..000000000 --- a/packages/runtime/src/useCases/transport/identity/CheckIdentity.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Result } from "@js-soft/ts-utils"; -import { CoreAddress, IdentityController, RelationshipsController, RelationshipStatus } from "@nmshd/transport"; -import { Inject } from "typescript-ioc"; -import { RelationshipDTO } from "../../../types"; -import { AddressString, SchemaRepository, SchemaValidator, UseCase } from "../../common"; -import { RelationshipMapper } from "../relationships/RelationshipMapper"; - -export interface CheckIdentityRequest { - address: AddressString; -} - -export interface CheckIdentityResponse { - unknown?: boolean; - self?: boolean; - peer?: boolean; - relationshipPending?: boolean; - relationshipActive?: boolean; - relationshipTerminated?: boolean; - relationship?: RelationshipDTO; -} - -class Validator extends SchemaValidator { - public constructor(@Inject schemaRepository: SchemaRepository) { - super(schemaRepository.getSchema("CheckIdentityRequest")); - } -} - -export class CheckIdentityUseCase extends UseCase { - public constructor( - @Inject private readonly identityController: IdentityController, - @Inject private readonly relationshipsController: RelationshipsController, - @Inject validator: Validator - ) { - super(validator); - } - - protected async executeInternal(request: CheckIdentityRequest): Promise> { - const address = CoreAddress.from(request.address); - const self = this.identityController.isMe(address); - - if (self) { - return Result.ok({ - self: true - }); - } - - const relationship = await this.relationshipsController.getRelationshipToIdentity(address); - if (relationship) { - const relationshipDTO = RelationshipMapper.toRelationshipDTO(relationship); - if (relationship.status === RelationshipStatus.Pending) { - return Result.ok({ - peer: true, - relationshipPending: true, - relationship: relationshipDTO - }); - } else if (relationship.status === RelationshipStatus.Active || relationship.status === RelationshipStatus.Terminating) { - return Result.ok({ - peer: true, - relationshipActive: true, - relationship: relationshipDTO - }); - } - - return Result.ok({ - peer: true, - relationshipTerminated: true, - relationship: relationshipDTO - }); - } - - return Result.ok({ - unknown: true - }); - } -} diff --git a/packages/runtime/src/useCases/transport/identity/index.ts b/packages/runtime/src/useCases/transport/identity/index.ts deleted file mode 100644 index b32aed2e6..000000000 --- a/packages/runtime/src/useCases/transport/identity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./CheckIdentity"; diff --git a/packages/runtime/src/useCases/transport/index.ts b/packages/runtime/src/useCases/transport/index.ts index 276e033c8..0bce894c3 100644 --- a/packages/runtime/src/useCases/transport/index.ts +++ b/packages/runtime/src/useCases/transport/index.ts @@ -2,9 +2,8 @@ export * from "./account"; export * from "./challenges"; export * from "./devices"; export * from "./files"; -export * from "./identity"; export * from "./identityDeletionProcesses"; export * from "./messages"; -export * from "./relationships"; export * from "./relationshipTemplates"; +export * from "./relationships"; export * from "./tokens"; diff --git a/packages/runtime/src/useCases/transport/messages/GetMessages.ts b/packages/runtime/src/useCases/transport/messages/GetMessages.ts index 51ea228d9..6f1be4b11 100644 --- a/packages/runtime/src/useCases/transport/messages/GetMessages.ts +++ b/packages/runtime/src/useCases/transport/messages/GetMessages.ts @@ -1,6 +1,6 @@ import { QueryTranslator } from "@js-soft/docdb-querytranslator"; import { Result } from "@js-soft/ts-utils"; -import { CachedMessage, Message, MessageController, MessageEnvelopeRecipient } from "@nmshd/transport"; +import { CachedMessage, CachedMessageRecipient, Message, MessageController, MessageEnvelopeRecipient } from "@nmshd/transport"; import { nameof } from "ts-simple-nameof"; import { Inject } from "typescript-ioc"; import { MessageDTO, RecipientDTO } from "../../../types"; @@ -54,6 +54,7 @@ export class GetMessagesUseCase extends UseCase((m) => m.recipients)}.${nameof((r) => r.address)}`]: `${nameof((m) => m.cache)}.${nameof( (m) => m.recipients )}.${nameof((r) => r.address)}`, + [`${nameof((m) => m.recipients)}.${nameof((r) => r.relationshipId)}`]: `${nameof((m) => m.cache)}.${nameof((m) => m.recipients)}.${nameof((m) => m.relationshipId)}`, [`${nameof((m) => m.content)}.@type`]: `${nameof((m) => m.cache)}.${nameof((m) => m.content)}.@type`, [`${nameof((m) => m.content)}.body`]: `${nameof((m) => m.cache)}.${nameof((m) => m.content)}.body`, [`${nameof((m) => m.content)}.subject`]: `${nameof((m) => m.cache)}.${nameof((m) => m.content)}.subject`, @@ -61,11 +62,6 @@ export class GetMessagesUseCase extends UseCase((m) => m.recipients)}.${nameof((r) => r.relationshipId)}`]: (query: any, input: any) => { - query[nameof((m) => m.relationshipIds)] = { - $containsAny: Array.isArray(input) ? input : [input] - }; - }, [nameof((m) => m.attachments)]: (query: any, input: any) => { if (input === "+") { query[`${nameof((m) => m.cache)}.${nameof((m) => m.attachments)}`] = { $not: { $size: 0 } }; diff --git a/packages/runtime/src/useCases/transport/messages/MessageMapper.ts b/packages/runtime/src/useCases/transport/messages/MessageMapper.ts index f749984e5..78d27d379 100644 --- a/packages/runtime/src/useCases/transport/messages/MessageMapper.ts +++ b/packages/runtime/src/useCases/transport/messages/MessageMapper.ts @@ -1,6 +1,8 @@ +import { Serializable } from "@js-soft/ts-serval"; +import { ArbitraryMessageContent, Mail, Notification, Request, ResponseWrapper } from "@nmshd/content"; import { CoreBuffer } from "@nmshd/crypto"; -import { CoreId, File, Message, MessageEnvelopeRecipient } from "@nmshd/transport"; -import { MessageDTO, MessageWithAttachmentsDTO, RecipientDTO } from "../../../types"; +import { CachedMessageRecipient, File, Message } from "@nmshd/transport"; +import { MessageDTO, MessageWithAttachmentsDTO } from "../../../types"; import { RuntimeErrors } from "../../common"; import { FileMapper } from "../files/FileMapper"; import { DownloadAttachmentResponse } from "./DownloadAttachment"; @@ -25,10 +27,10 @@ export class MessageMapper { return { id: message.id.toString(), - content: message.cache.content.toJSON(), + content: this.toMessageContent(message.cache.content), createdBy: message.cache.createdBy.toString(), createdByDevice: message.cache.createdByDevice.toString(), - recipients: message.cache.recipients.map((r, i) => this.toRecipient(r, message.relationshipIds[i])), + recipients: this.toRecipients(message.cache.recipients), createdAt: message.cache.createdAt.toString(), attachments: attachments.map((f) => FileMapper.toFileDTO(f)), isOwn: message.isOwn, @@ -43,10 +45,10 @@ export class MessageMapper { return { id: message.id.toString(), - content: message.cache.content.toJSON(), + content: this.toMessageContent(message.cache.content), createdBy: message.cache.createdBy.toString(), createdByDevice: message.cache.createdByDevice.toString(), - recipients: message.cache.recipients.map((r, i) => this.toRecipient(r, message.relationshipIds[i])), + recipients: this.toRecipients(message.cache.recipients), createdAt: message.cache.createdAt.toString(), attachments: message.cache.attachments.map((a) => a.toString()), isOwn: message.isOwn, @@ -58,12 +60,30 @@ export class MessageMapper { return messages.map((message) => this.toMessageDTO(message)); } - private static toRecipient(recipient: MessageEnvelopeRecipient, relationshipId: CoreId): RecipientDTO { - return { - address: recipient.address.toString(), - receivedAt: recipient.receivedAt?.toString(), - receivedByDevice: recipient.receivedByDevice?.toString(), - relationshipId: relationshipId.toString() - }; + private static toRecipients(recipients: CachedMessageRecipient[]) { + return recipients.map((r) => { + return { + address: r.address.toString(), + receivedAt: r.receivedAt?.toString(), + receivedByDevice: r.receivedByDevice?.toString(), + relationshipId: r.relationshipId?.toString() + }; + }); + } + + private static toMessageContent(content: Serializable) { + if ( + !( + content instanceof Mail || + content instanceof Request || + content instanceof ResponseWrapper || + content instanceof Notification || + content instanceof ArbitraryMessageContent + ) + ) { + return ArbitraryMessageContent.from({ value: content }).toJSON(); + } + + return content.toJSON(); } } diff --git a/packages/runtime/src/useCases/transport/messages/SendMessage.ts b/packages/runtime/src/useCases/transport/messages/SendMessage.ts index 389fdef8d..1faca9d92 100644 --- a/packages/runtime/src/useCases/transport/messages/SendMessage.ts +++ b/packages/runtime/src/useCases/transport/messages/SendMessage.ts @@ -1,7 +1,7 @@ import { Serializable } from "@js-soft/ts-serval"; import { Result } from "@js-soft/ts-utils"; import { OutgoingRequestsController } from "@nmshd/consumption"; -import { Request } from "@nmshd/content"; +import { ArbitraryMessageContent, Mail, Notification, Request, ResponseWrapper } from "@nmshd/content"; import { AccountController, CoreAddress, CoreId, File, FileController, MessageController } from "@nmshd/transport"; import _ from "lodash"; import { Inject } from "typescript-ioc"; @@ -58,6 +58,20 @@ export class SendMessageUseCase extends UseCase private async validateMessageContent(content: any, recipients: string[]) { const transformedContent = Serializable.fromUnknown(content); + if ( + !( + transformedContent instanceof Mail || + transformedContent instanceof ResponseWrapper || + transformedContent instanceof Notification || + transformedContent instanceof ArbitraryMessageContent || + transformedContent instanceof Request + ) + ) { + return RuntimeErrors.general.invalidPropertyValue( + "The content of a Message must either be a Mail, Request, ResponseWrapper, Notification or an ArbitraryMessageContent." + ); + } + if (!(transformedContent instanceof Request)) return; if (!transformedContent.id) return RuntimeErrors.general.invalidPropertyValue("The Request must have an id."); @@ -73,7 +87,6 @@ export class SendMessageUseCase extends UseCase const recipient = CoreAddress.from(recipients[0]); if (!recipient.equals(localRequest.peer)) return RuntimeErrors.general.invalidPropertyValue("The recipient does not match the Request's peer."); - return; } diff --git a/packages/runtime/src/useCases/transport/relationshipTemplates/CreateOwnRelationshipTemplate.ts b/packages/runtime/src/useCases/transport/relationshipTemplates/CreateOwnRelationshipTemplate.ts index 64c06a899..84f34e449 100644 --- a/packages/runtime/src/useCases/transport/relationshipTemplates/CreateOwnRelationshipTemplate.ts +++ b/packages/runtime/src/useCases/transport/relationshipTemplates/CreateOwnRelationshipTemplate.ts @@ -1,7 +1,7 @@ import { Serializable } from "@js-soft/ts-serval"; import { Result } from "@js-soft/ts-utils"; import { OutgoingRequestsController } from "@nmshd/consumption"; -import { RelationshipTemplateContent } from "@nmshd/content"; +import { ArbitraryRelationshipTemplateContent, RelationshipTemplateContent } from "@nmshd/content"; import { AccountController, CoreDate, RelationshipTemplateController } from "@nmshd/transport"; import { DateTime } from "luxon"; import { nameof } from "ts-simple-nameof"; @@ -68,6 +68,13 @@ export class CreateOwnRelationshipTemplateUseCase extends UseCase this.toRelationshipTemplateDTO(i)); } + + private static toTemplateContent(content: Serializable) { + if (!(content instanceof RelationshipTemplateContent || content instanceof ArbitraryRelationshipTemplateContent)) { + return ArbitraryRelationshipTemplateContent.from({ value: content }).toJSON(); + } + return content.toJSON(); + } } diff --git a/packages/runtime/src/useCases/transport/relationships/AcceptRelationshipChange.ts b/packages/runtime/src/useCases/transport/relationships/AcceptRelationship.ts similarity index 53% rename from packages/runtime/src/useCases/transport/relationships/AcceptRelationshipChange.ts rename to packages/runtime/src/useCases/transport/relationships/AcceptRelationship.ts index b5953327f..15cb9dea4 100644 --- a/packages/runtime/src/useCases/transport/relationships/AcceptRelationshipChange.ts +++ b/packages/runtime/src/useCases/transport/relationships/AcceptRelationship.ts @@ -1,23 +1,21 @@ import { Result } from "@js-soft/ts-utils"; -import { AccountController, CoreId, Relationship, RelationshipChange, RelationshipsController } from "@nmshd/transport"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; import { Inject } from "typescript-ioc"; import { RelationshipDTO } from "../../../types"; -import { RelationshipChangeIdString, RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; import { RelationshipMapper } from "./RelationshipMapper"; -export interface AcceptRelationshipChangeRequest { +export interface AcceptRelationshipRequest { relationshipId: RelationshipIdString; - changeId: RelationshipChangeIdString; - content: any; } -class Validator extends SchemaValidator { +class Validator extends SchemaValidator { public constructor(@Inject schemaRepository: SchemaRepository) { - super(schemaRepository.getSchema("AcceptRelationshipChangeRequest")); + super(schemaRepository.getSchema("AcceptRelationshipRequest")); } } -export class AcceptRelationshipChangeUseCase extends UseCase { +export class AcceptRelationshipUseCase extends UseCase { public constructor( @Inject private readonly relationshipsController: RelationshipsController, @Inject private readonly accountController: AccountController, @@ -26,7 +24,7 @@ export class AcceptRelationshipChangeUseCase extends UseCase> { + protected async executeInternal(request: AcceptRelationshipRequest): Promise> { const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); if (!relationship) { return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); @@ -36,12 +34,7 @@ export class AcceptRelationshipChangeUseCase extends UseCase c.id.toString() === request.changeId); - if (!change) { - return Result.fail(RuntimeErrors.general.recordNotFound(RelationshipChange)); - } - - const updatedRelationship = await this.relationshipsController.acceptChange(change, request.content); + const updatedRelationship = await this.relationshipsController.accept(relationship.id); await this.accountController.syncDatawallet(); diff --git a/packages/runtime/src/useCases/transport/relationships/RevokeRelationshipChange.ts b/packages/runtime/src/useCases/transport/relationships/AcceptRelationshipReactivation.ts similarity index 53% rename from packages/runtime/src/useCases/transport/relationships/RevokeRelationshipChange.ts rename to packages/runtime/src/useCases/transport/relationships/AcceptRelationshipReactivation.ts index a562ed523..f16c2cdfe 100644 --- a/packages/runtime/src/useCases/transport/relationships/RevokeRelationshipChange.ts +++ b/packages/runtime/src/useCases/transport/relationships/AcceptRelationshipReactivation.ts @@ -1,23 +1,21 @@ import { Result } from "@js-soft/ts-utils"; -import { AccountController, CoreId, Relationship, RelationshipChange, RelationshipsController } from "@nmshd/transport"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; import { Inject } from "typescript-ioc"; import { RelationshipDTO } from "../../../types"; -import { RelationshipChangeIdString, RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; import { RelationshipMapper } from "./RelationshipMapper"; -export interface RevokeRelationshipChangeRequest { +export interface AcceptRelationshipReactivationRequest { relationshipId: RelationshipIdString; - changeId: RelationshipChangeIdString; - content: any; } -class Validator extends SchemaValidator { +class Validator extends SchemaValidator { public constructor(@Inject schemaRepository: SchemaRepository) { - super(schemaRepository.getSchema("RevokeRelationshipChangeRequest")); + super(schemaRepository.getSchema("AcceptRelationshipReactivationRequest")); } } -export class RevokeRelationshipChangeUseCase extends UseCase { +export class AcceptRelationshipReactivationUseCase extends UseCase { public constructor( @Inject private readonly relationshipsController: RelationshipsController, @Inject private readonly accountController: AccountController, @@ -26,7 +24,7 @@ export class RevokeRelationshipChangeUseCase extends UseCase> { + protected async executeInternal(request: AcceptRelationshipReactivationRequest): Promise> { const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); if (!relationship) { return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); @@ -36,12 +34,7 @@ export class RevokeRelationshipChangeUseCase extends UseCase c.id.toString() === request.changeId); - if (!change) { - return Result.fail(RuntimeErrors.general.recordNotFound(RelationshipChange)); - } - - const updatedRelationship = await this.relationshipsController.revokeChange(change, request.content); + const updatedRelationship = await this.relationshipsController.acceptReactivation(relationship.id); await this.accountController.syncDatawallet(); diff --git a/packages/runtime/src/useCases/transport/relationships/CreateRelationship.ts b/packages/runtime/src/useCases/transport/relationships/CreateRelationship.ts index b38b452ed..fdf5bee8f 100644 --- a/packages/runtime/src/useCases/transport/relationships/CreateRelationship.ts +++ b/packages/runtime/src/useCases/transport/relationships/CreateRelationship.ts @@ -1,5 +1,7 @@ +import { Serializable } from "@js-soft/ts-serval"; import { Result } from "@js-soft/ts-utils"; -import { AccountController, CoreId, RelationshipsController, RelationshipTemplate, RelationshipTemplateController } from "@nmshd/transport"; +import { ArbitraryRelationshipCreationContent, RelationshipCreationContent } from "@nmshd/content"; +import { AccountController, CoreId, RelationshipTemplate, RelationshipTemplateController, RelationshipsController } from "@nmshd/transport"; import { Inject } from "typescript-ioc"; import { RelationshipDTO } from "../../../types"; import { RelationshipTemplateIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; @@ -7,7 +9,7 @@ import { RelationshipMapper } from "./RelationshipMapper"; export interface CreateRelationshipRequest { templateId: RelationshipTemplateIdString; - content: any; + creationContent: any; } class Validator extends SchemaValidator { @@ -32,10 +34,16 @@ export class CreateRelationshipUseCase extends UseCase { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("DecomposeRelationshipRequest")); + } +} + +export class DecomposeRelationshipUseCase extends UseCase { + public constructor( + @Inject private readonly accountController: AccountController, + @Inject private readonly consumptionController: ConsumptionController, + @Inject private readonly relationshipsController: RelationshipsController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: DecomposeRelationshipRequest): Promise> { + const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); + if (!relationship) { + return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); + } + + if (!relationship.cache) { + return Result.fail(RuntimeErrors.general.cacheEmpty(Relationship, relationship.id.toString())); + } + + // backbone call first so nothing is deleted in case it goes wrong + await this.relationshipsController.decompose(relationship.id); + + await this.accountController.cleanupDataOfDecomposedRelationship(relationship); + await this.consumptionController.cleanupDataOfDecomposedRelationship(relationship.peer.address, relationship.id); + + await this.accountController.syncDatawallet(); + + return Result.ok(undefined); + } +} diff --git a/packages/runtime/src/useCases/transport/relationships/RejectRelationshipChange.ts b/packages/runtime/src/useCases/transport/relationships/RejectRelationship.ts similarity index 53% rename from packages/runtime/src/useCases/transport/relationships/RejectRelationshipChange.ts rename to packages/runtime/src/useCases/transport/relationships/RejectRelationship.ts index 801765afb..23f58e70e 100644 --- a/packages/runtime/src/useCases/transport/relationships/RejectRelationshipChange.ts +++ b/packages/runtime/src/useCases/transport/relationships/RejectRelationship.ts @@ -1,23 +1,21 @@ import { Result } from "@js-soft/ts-utils"; -import { AccountController, CoreId, Relationship, RelationshipChange, RelationshipsController } from "@nmshd/transport"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; import { Inject } from "typescript-ioc"; import { RelationshipDTO } from "../../../types"; -import { RelationshipChangeIdString, RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; import { RelationshipMapper } from "./RelationshipMapper"; -export interface RejectRelationshipChangeRequest { +export interface RejectRelationshipRequest { relationshipId: RelationshipIdString; - changeId: RelationshipChangeIdString; - content: any; } -class Validator extends SchemaValidator { +class Validator extends SchemaValidator { public constructor(@Inject schemaRepository: SchemaRepository) { - super(schemaRepository.getSchema("RejectRelationshipChangeRequest")); + super(schemaRepository.getSchema("RejectRelationshipRequest")); } } -export class RejectRelationshipChangeUseCase extends UseCase { +export class RejectRelationshipUseCase extends UseCase { public constructor( @Inject private readonly relationshipsController: RelationshipsController, @Inject private readonly accountController: AccountController, @@ -26,7 +24,7 @@ export class RejectRelationshipChangeUseCase extends UseCase> { + protected async executeInternal(request: RejectRelationshipRequest): Promise> { const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); if (!relationship) { return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); @@ -36,12 +34,7 @@ export class RejectRelationshipChangeUseCase extends UseCase c.id.toString() === request.changeId); - if (!change) { - return Result.fail(RuntimeErrors.general.recordNotFound(RelationshipChange)); - } - - const updatedRelationship = await this.relationshipsController.rejectChange(change, request.content); + const updatedRelationship = await this.relationshipsController.reject(relationship.id); await this.accountController.syncDatawallet(); diff --git a/packages/runtime/src/useCases/transport/relationships/RejectRelationshipReactivation.ts b/packages/runtime/src/useCases/transport/relationships/RejectRelationshipReactivation.ts new file mode 100644 index 000000000..9d044a6f7 --- /dev/null +++ b/packages/runtime/src/useCases/transport/relationships/RejectRelationshipReactivation.ts @@ -0,0 +1,43 @@ +import { Result } from "@js-soft/ts-utils"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { RelationshipDTO } from "../../../types"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipMapper } from "./RelationshipMapper"; + +export interface RejectRelationshipReactivationRequest { + relationshipId: RelationshipIdString; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("RejectRelationshipReactivationRequest")); + } +} + +export class RejectRelationshipReactivationUseCase extends UseCase { + public constructor( + @Inject private readonly relationshipsController: RelationshipsController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: RejectRelationshipReactivationRequest): Promise> { + const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); + if (!relationship) { + return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); + } + + if (!relationship.cache) { + return Result.fail(RuntimeErrors.general.cacheEmpty(Relationship, relationship.id.toString())); + } + + const updatedRelationship = await this.relationshipsController.rejectReactivation(relationship.id); + + await this.accountController.syncDatawallet(); + + return Result.ok(RelationshipMapper.toRelationshipDTO(updatedRelationship)); + } +} diff --git a/packages/runtime/src/useCases/transport/relationships/RelationshipMapper.ts b/packages/runtime/src/useCases/transport/relationships/RelationshipMapper.ts index 685e4cff6..7205facbd 100644 --- a/packages/runtime/src/useCases/transport/relationships/RelationshipMapper.ts +++ b/packages/runtime/src/useCases/transport/relationships/RelationshipMapper.ts @@ -1,5 +1,7 @@ -import { Relationship, RelationshipChange, RelationshipChangeRequest, RelationshipChangeResponse } from "@nmshd/transport"; -import { RelationshipChangeDTO, RelationshipChangeRequestDTO, RelationshipChangeResponseDTO, RelationshipDTO } from "../../../types"; +import { Serializable } from "@js-soft/ts-serval"; +import { ArbitraryRelationshipCreationContent, RelationshipCreationContent } from "@nmshd/content"; +import { Relationship, RelationshipAuditLogEntry } from "@nmshd/transport"; +import { RelationshipAuditLogEntryDTO, RelationshipDTO } from "../../../types"; import { RuntimeErrors } from "../../common"; import { RelationshipTemplateMapper } from "../relationshipTemplates/RelationshipTemplateMapper"; @@ -16,42 +18,32 @@ export class RelationshipMapper { peer: relationship.peer.address.toString(), peerIdentity: { address: relationship.peer.address.toString(), - publicKey: relationship.peer.publicKey.toBase64(false), - realm: relationship.peer.realm + publicKey: relationship.peer.publicKey.toBase64(false) }, - changes: relationship.cache.changes.map((c) => this.toRelationshipChangeDTO(c)) + auditLog: relationship.cache.auditLog.map((entry) => this.toAuditLogEntryDTO(entry)), + creationContent: this.toCreationContent(relationship.cache.creationContent) }; } - public static toRelationshipDTOList(relationships: Relationship[]): RelationshipDTO[] { - return relationships.map((r) => this.toRelationshipDTO(r)); - } - - private static toRelationshipChangeRequestDTO(change: RelationshipChangeRequest): RelationshipChangeRequestDTO { + private static toAuditLogEntryDTO(entry: RelationshipAuditLogEntry): RelationshipAuditLogEntryDTO { return { - createdBy: change.createdBy.toString(), - createdByDevice: change.createdByDevice.toString(), - createdAt: change.createdAt.toString(), - content: change.content?.toJSON() + createdAt: entry.createdAt.toString(), + createdBy: entry.createdBy.toString(), + createdByDevice: entry.createdByDevice.toString(), + reason: entry.reason, + oldStatus: entry.oldStatus, + newStatus: entry.newStatus }; } - private static toRelationshipChangeResponseDTO(change: RelationshipChangeResponse): RelationshipChangeResponseDTO { - return { - createdBy: change.createdBy.toString(), - createdByDevice: change.createdByDevice.toString(), - createdAt: change.createdAt.toString(), - content: change.content?.toJSON() - }; + public static toRelationshipDTOList(relationships: Relationship[]): RelationshipDTO[] { + return relationships.map((r) => this.toRelationshipDTO(r)); } - private static toRelationshipChangeDTO(change: RelationshipChange): RelationshipChangeDTO { - return { - id: change.id.toString(), - request: this.toRelationshipChangeRequestDTO(change.request), - status: change.status, - type: change.type, - response: change.response ? this.toRelationshipChangeResponseDTO(change.response) : undefined - }; + private static toCreationContent(content: Serializable) { + if (!(content instanceof RelationshipCreationContent || content instanceof ArbitraryRelationshipCreationContent)) { + return ArbitraryRelationshipCreationContent.from({ value: content }).toJSON(); + } + return content.toJSON(); } } diff --git a/packages/runtime/src/useCases/transport/relationships/RequestRelationshipReactivation.ts b/packages/runtime/src/useCases/transport/relationships/RequestRelationshipReactivation.ts new file mode 100644 index 000000000..021731fcd --- /dev/null +++ b/packages/runtime/src/useCases/transport/relationships/RequestRelationshipReactivation.ts @@ -0,0 +1,43 @@ +import { Result } from "@js-soft/ts-utils"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { RelationshipDTO } from "../../../types"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipMapper } from "./RelationshipMapper"; + +export interface RequestRelationshipReactivationRequest { + relationshipId: RelationshipIdString; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("RequestRelationshipReactivationRequest")); + } +} + +export class RequestRelationshipReactivationUseCase extends UseCase { + public constructor( + @Inject private readonly relationshipsController: RelationshipsController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: RequestRelationshipReactivationRequest): Promise> { + const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); + if (!relationship) { + return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); + } + + if (!relationship.cache) { + return Result.fail(RuntimeErrors.general.cacheEmpty(Relationship, relationship.id.toString())); + } + + const updatedRelationship = await this.relationshipsController.requestReactivation(relationship.id); + + await this.accountController.syncDatawallet(); + + return Result.ok(RelationshipMapper.toRelationshipDTO(updatedRelationship)); + } +} diff --git a/packages/runtime/src/useCases/transport/relationships/RevokeRelationship.ts b/packages/runtime/src/useCases/transport/relationships/RevokeRelationship.ts new file mode 100644 index 000000000..115e2e0f1 --- /dev/null +++ b/packages/runtime/src/useCases/transport/relationships/RevokeRelationship.ts @@ -0,0 +1,43 @@ +import { Result } from "@js-soft/ts-utils"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { RelationshipDTO } from "../../../types"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipMapper } from "./RelationshipMapper"; + +export interface RevokeRelationshipRequest { + relationshipId: RelationshipIdString; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("RevokeRelationshipRequest")); + } +} + +export class RevokeRelationshipUseCase extends UseCase { + public constructor( + @Inject private readonly relationshipsController: RelationshipsController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: RevokeRelationshipRequest): Promise> { + const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); + if (!relationship) { + return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); + } + + if (!relationship.cache) { + return Result.fail(RuntimeErrors.general.cacheEmpty(Relationship, relationship.id.toString())); + } + + const updatedRelationship = await this.relationshipsController.revoke(relationship.id); + + await this.accountController.syncDatawallet(); + + return Result.ok(RelationshipMapper.toRelationshipDTO(updatedRelationship)); + } +} diff --git a/packages/runtime/src/useCases/transport/relationships/RevokeRelationshipReactivation.ts b/packages/runtime/src/useCases/transport/relationships/RevokeRelationshipReactivation.ts new file mode 100644 index 000000000..b42e865a1 --- /dev/null +++ b/packages/runtime/src/useCases/transport/relationships/RevokeRelationshipReactivation.ts @@ -0,0 +1,43 @@ +import { Result } from "@js-soft/ts-utils"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { RelationshipDTO } from "../../../types"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipMapper } from "./RelationshipMapper"; + +export interface RevokeRelationshipReactivationRequest { + relationshipId: RelationshipIdString; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("RevokeRelationshipReactivationRequest")); + } +} + +export class RevokeRelationshipReactivationUseCase extends UseCase { + public constructor( + @Inject private readonly relationshipsController: RelationshipsController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: RevokeRelationshipReactivationRequest): Promise> { + const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); + if (!relationship) { + return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); + } + + if (!relationship.cache) { + return Result.fail(RuntimeErrors.general.cacheEmpty(Relationship, relationship.id.toString())); + } + + const updatedRelationship = await this.relationshipsController.revokeReactivation(relationship.id); + + await this.accountController.syncDatawallet(); + + return Result.ok(RelationshipMapper.toRelationshipDTO(updatedRelationship)); + } +} diff --git a/packages/runtime/src/useCases/transport/relationships/TerminateRelationship.ts b/packages/runtime/src/useCases/transport/relationships/TerminateRelationship.ts new file mode 100644 index 000000000..e07deb71b --- /dev/null +++ b/packages/runtime/src/useCases/transport/relationships/TerminateRelationship.ts @@ -0,0 +1,43 @@ +import { Result } from "@js-soft/ts-utils"; +import { AccountController, CoreId, Relationship, RelationshipsController } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { RelationshipDTO } from "../../../types"; +import { RelationshipIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { RelationshipMapper } from "./RelationshipMapper"; + +export interface TerminateRelationshipRequest { + relationshipId: RelationshipIdString; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("TerminateRelationshipRequest")); + } +} + +export class TerminateRelationshipUseCase extends UseCase { + public constructor( + @Inject private readonly relationshipsController: RelationshipsController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: TerminateRelationshipRequest): Promise> { + const relationship = await this.relationshipsController.getRelationship(CoreId.from(request.relationshipId)); + if (!relationship) { + return Result.fail(RuntimeErrors.general.recordNotFound(Relationship)); + } + + if (!relationship.cache) { + return Result.fail(RuntimeErrors.general.cacheEmpty(Relationship, relationship.id.toString())); + } + + const updatedRelationship = await this.relationshipsController.terminate(relationship.id); + + await this.accountController.syncDatawallet(); + + return Result.ok(RelationshipMapper.toRelationshipDTO(updatedRelationship)); + } +} diff --git a/packages/runtime/src/useCases/transport/relationships/index.ts b/packages/runtime/src/useCases/transport/relationships/index.ts index 3203254e0..65db2183d 100644 --- a/packages/runtime/src/useCases/transport/relationships/index.ts +++ b/packages/runtime/src/useCases/transport/relationships/index.ts @@ -1,9 +1,15 @@ -export * from "./AcceptRelationshipChange"; +export * from "./AcceptRelationship"; +export * from "./AcceptRelationshipReactivation"; export * from "./CreateRelationship"; +export * from "./DecomposeRelationship"; export * from "./GetAttributesForRelationship"; export * from "./GetRelationship"; export * from "./GetRelationshipByAddress"; export * from "./GetRelationships"; -export * from "./RejectRelationshipChange"; +export * from "./RejectRelationship"; +export * from "./RejectRelationshipReactivation"; export * from "./RelationshipMapper"; -export * from "./RevokeRelationshipChange"; +export * from "./RequestRelationshipReactivation"; +export * from "./RevokeRelationship"; +export * from "./RevokeRelationshipReactivation"; +export * from "./TerminateRelationship"; diff --git a/packages/runtime/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index afeb20b83..62c8c4d18 100644 --- a/packages/runtime/test/consumption/attributes.test.ts +++ b/packages/runtime/test/consumption/attributes.test.ts @@ -9,11 +9,13 @@ import { RequestItemJSONDerivations, StreetJSON, ThirdPartyRelationshipAttributeQuery, + ThirdPartyRelationshipAttributeQueryOwner, ZipCodeJSON } from "@nmshd/content"; import { CoreAddress, CoreDate, CoreId } from "@nmshd/transport"; import { AttributeCreatedEvent, + ChangeDefaultRepositoryAttributeUseCase, CreateAndShareRelationshipAttributeRequest, CreateAndShareRelationshipAttributeUseCase, CreateRepositoryAttributeRequest, @@ -24,13 +26,13 @@ import { DeleteThirdPartyOwnedRelationshipAttributeAndNotifyPeerUseCase, ExecuteIdentityAttributeQueryUseCase, ExecuteRelationshipAttributeQueryUseCase, - GetAttributesUseCase, + ExecuteThirdPartyRelationshipAttributeQueryUseCase, GetAttributeUseCase, + GetAttributesUseCase, GetOwnSharedAttributesUseCase, GetPeerSharedAttributesUseCase, GetRepositoryAttributesUseCase, GetSharedVersionsOfAttributeUseCase, - GetSharedVersionsOfRepositoryAttributeUseCase, GetVersionsOfAttributeUseCase, LocalAttributeDTO, NotifyPeerAboutRepositoryAttributeSuccessionUseCase, @@ -45,6 +47,8 @@ import { ThirdPartyOwnedRelationshipAttributeDeletedByPeerEvent } from "../../src"; import { + RuntimeServiceProvider, + TestRuntimeServices, acceptIncomingShareAttributeRequest, establishRelationship, exchangeAndAcceptRequestByMessage, @@ -54,9 +58,7 @@ import { executeFullRequestAndShareThirdPartyRelationshipAttributeFlow, executeFullShareRepositoryAttributeFlow, executeFullSucceedRepositoryAttributeAndNotifyPeerFlow, - RuntimeServiceProvider, syncUntilHasMessageWithNotification, - TestRuntimeServices, waitForRecipientToReceiveNotification } from "../lib"; @@ -105,6 +107,14 @@ describe("get attribute(s)", () => { beforeAll(async function () { const senderRequests: CreateRepositoryAttributeRequest[] = [ + { + content: { + value: { + "@type": "GivenName", + value: "AGivenName" + } + } + }, { content: { value: { @@ -116,8 +126,8 @@ describe("get attribute(s)", () => { { content: { value: { - "@type": "GivenName", - value: "AGivenName" + "@type": "Surname", + value: "Another Surname" } } } @@ -131,12 +141,12 @@ describe("get attribute(s)", () => { const relationshipAttribute = await executeFullCreateAndShareRelationshipAttributeFlow(services1, services2, { content: { - key: "a key", + key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public, value: { "@type": "ProprietaryString", - value: "a String", - title: "a title" + value: "AString", + title: "ATitle" }, isTechnical: true } @@ -162,7 +172,7 @@ describe("get attribute(s)", () => { const result = await services1.consumption.attributes.getAttributes({ query: {} }); expect(result.isSuccess).toBe(true); const attributes = result.value; - expect(attributes).toHaveLength(3); + expect(attributes).toHaveLength(4); const attributeIds = attributes.map((attribute) => attribute.id); expect(attributeIds).toContain(relationshipAttributeId); expect(attributeIds).toStrictEqual(expect.arrayContaining(identityAttributeIds)); @@ -170,7 +180,7 @@ describe("get attribute(s)", () => { test("should allow to get an attribute by type", async function () { const result = await services1.consumption.attributes.getAttributes({ - query: { "content.value.@type": "Surname" } + query: { "content.value.@type": "GivenName" } }); expect(result).toBeSuccessful(); @@ -188,7 +198,10 @@ describe("get attribute(s)", () => { expect(result).toBeSuccessful(); const attributes = result.value; - expect(attributes).toHaveLength(2); + expect(attributes).toHaveLength(3); + + const attributeIds = attributes.map((attribute) => attribute.id); + expect(attributeIds).toStrictEqual(expect.arrayContaining(identityAttributeIds)); }); test("should hide technical attributes when hideTechnical=true", async () => { @@ -196,7 +209,7 @@ describe("get attribute(s)", () => { expect(result.isSuccess).toBe(true); const attributes = result.value; expect(attributes.filter((a) => a.id === relationshipAttributeId)).toHaveLength(0); - expect(attributes).toHaveLength(2); + expect(attributes).toHaveLength(3); const attributeIds = attributes.map((attribute) => attribute.id); expect(attributeIds).toStrictEqual(identityAttributeIds); }); @@ -206,11 +219,41 @@ describe("get attribute(s)", () => { expect(getAttributesResponse.isSuccess).toBe(true); const attributes = getAttributesResponse.value; expect(attributes.filter((a) => a.id === relationshipAttributeId)).toHaveLength(1); - expect(attributes).toHaveLength(3); + expect(attributes).toHaveLength(4); const attributeIds = attributes.map((attribute) => attribute.id); expect(attributeIds).toContain(relationshipAttributeId); expect(attributeIds).toStrictEqual(expect.arrayContaining(identityAttributeIds)); }); + + test("should allow to get only default attributes", async function () { + const result = await services1.consumption.attributes.getAttributes({ + query: { isDefault: "true" } + }); + expect(result).toBeSuccessful(); + + const attributes = result.value; + expect(attributes).toHaveLength(2); + + const attributeIds = attributes.map((attr) => attr.id); + expect(attributeIds).toContain(identityAttributeIds[0]); + expect(attributeIds).toContain(identityAttributeIds[1]); + expect(attributeIds).not.toContain(identityAttributeIds[2]); + expect(attributeIds).not.toContain(relationshipAttributeId); + }); + + test("should allow not to get default attributes", async function () { + const result = await services1.consumption.attributes.getAttributes({ + query: { isDefault: "!true" } + }); + expect(result).toBeSuccessful(); + + const attributes = result.value; + expect(attributes).toHaveLength(2); + + const attributeIds = attributes.map((attr) => attr.id); + expect(attributeIds).toContain(relationshipAttributeId); + expect(attributeIds).toContain(identityAttributeIds[2]); + }); }); }); @@ -233,8 +276,8 @@ describe("attribute queries", () => { content: { value: { "@type": "ProprietaryString", - title: "aTitle", - value: "aProprietaryStringValue" + title: "ATitle", + value: "AProprietaryStringValue" }, key: "website", confidentiality: RelationshipAttributeConfidentiality.Protected @@ -272,13 +315,13 @@ describe("attribute queries", () => { }); }); - describe(ExecuteRelationshipAttributeQueryUseCase.name, () => { + describe(ExecuteThirdPartyRelationshipAttributeQueryUseCase.name, () => { test("should allow to execute a thirdPartyRelationshipAttributeQuery", async function () { const result = await services2.consumption.attributes.executeThirdPartyRelationshipAttributeQuery({ query: { "@type": "ThirdPartyRelationshipAttributeQuery", key: "website", - owner: services1.address, + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, thirdParty: [services1.address] } }); @@ -366,12 +409,12 @@ describe("get repository, own shared and peer shared attributes", () => { // own shared succeeded relationship attribute services1SharedRelationshipAttributeV0 = await executeFullCreateAndShareRelationshipAttributeFlow(services1, services2, { content: { - key: "a key", + key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public, value: { "@type": "ProprietaryString", - value: "a String", - title: "a title" + value: "AString", + title: "ATitle" }, isTechnical: false } @@ -451,6 +494,22 @@ describe("get repository, own shared and peer shared attributes", () => { const repositoryAttributes = result.value; expect(repositoryAttributes).toStrictEqual([services1RepoSurnameV0, services1RepoSurnameV1, services1RepoGivenNameV0, services1RepoGivenNameV1]); }); + + test("should allow to get only default attributes", async function () { + const result = await services1.consumption.attributes.getRepositoryAttributes({ + query: { + isDefault: "true" + } + }); + expect(result).toBeSuccessful(); + + const attributes = result.value; + expect(attributes).toHaveLength(2); + + const attributeIds = attributes.map((attr) => attr.id); + expect(attributeIds).toContain(services1RepoSurnameV1.id); + expect(attributeIds).toContain(services1RepoGivenNameV1.id); + }); }); describe(GetOwnSharedAttributesUseCase.name, () => { @@ -610,6 +669,35 @@ describe(CreateRepositoryAttributeUseCase.name, () => { await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "City"); await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "Country"); }); + + test("should create a RepositoryAttribute that is the default if it is the first of its value type", async () => { + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "Pseudonym", + value: "A pseudonym" + } + } + }; + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + const attribute = result.value; + expect(attribute.isDefault).toBe(true); + }); + + test("should create a RepositoryAttribute that is not the default if it is not the first of its value type", async () => { + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "JobTitle", + value: "A job title" + } + } + }; + await services1.consumption.attributes.createRepositoryAttribute(request); + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + const attribute = result.value; + expect(attribute.isDefault).toBeUndefined(); + }); }); describe(ShareRepositoryAttributeUseCase.name, () => { @@ -702,7 +790,7 @@ describe(ShareRepositoryAttributeUseCase.name, () => { expect(sUpdatedOwnSharedIdentityAttribute.deletionInfo?.deletionStatus).toStrictEqual(DeletionStatus.DeletedByPeer); const shareRequestResult2 = await services1.consumption.attributes.shareRepositoryAttribute(shareRequest); - expect(shareRequestResult2.isSuccess).toBe(true); + expect(shareRequestResult2).toBeSuccessful(); }); test("should send a sharing request containing a repository attribute that was already shared but is to be deleted by the peer", async () => { @@ -740,7 +828,7 @@ describe(ShareRepositoryAttributeUseCase.name, () => { expect(sUpdatedOwnSharedIdentityAttribute.deletionInfo?.deletionStatus).toStrictEqual(DeletionStatus.ToBeDeletedByPeer); const shareRequestResult2 = await services1.consumption.attributes.shareRepositoryAttribute(shareRequest); - expect(shareRequestResult2.isSuccess).toBe(true); + expect(shareRequestResult2).toBeSuccessful(); }); test("should reject attempts to share the same repository attribute more than once with the same peer", async () => { @@ -751,7 +839,10 @@ describe(ShareRepositoryAttributeUseCase.name, () => { peer: services3.address }); - expect(repeatedShareRequestResult).toBeAnError(/.*/, "error.runtime.attributes.repositoryAttributeHasAlreadyBeenSharedWithPeer"); + expect(repeatedShareRequestResult).toBeAnError( + `The IdentityAttribute with the given sourceAttributeId '${sRepositoryAttribute.id}' is already shared with the peer.`, + "error.consumption.requests.invalidRequestItem" + ); }); test("should reject sharing an attribute, of which a previous version has been shared", async () => { @@ -780,7 +871,10 @@ describe(ShareRepositoryAttributeUseCase.name, () => { attributeId: successorRepositoryAttribute.id, peer: services2.address }); - expect(response).toBeAnError(/.*/, "error.runtime.attributes.anotherVersionOfRepositoryAttributeHasAlreadyBeenSharedWithPeer"); + expect(response).toBeAnError( + `The predecessor '${predecesssorOwnSharedIdentityAttribute.shareInfo!.sourceAttribute}' of the IdentityAttribute is already shared with the peer. Instead of sharing it, you should notify the peer about the Attribute succession.`, + "error.consumption.requests.invalidRequestItem" + ); }); test("should reject sharing an own shared identity attribute", async () => { @@ -812,8 +906,8 @@ describe(ShareRepositoryAttributeUseCase.name, () => { key: "test", value: { "@type": "ProprietaryString", - value: "a String", - title: "a title" + value: "AString", + title: "ATitle" }, confidentiality: RelationshipAttributeConfidentiality.Public }, @@ -1100,8 +1194,8 @@ describe(CreateAndShareRelationshipAttributeUseCase.name, () => { key: "test", value: { "@type": "ProprietaryString", - value: "a String", - title: "a title" + value: "AString", + title: "ATitle" }, confidentiality: RelationshipAttributeConfidentiality.Public }, @@ -1125,8 +1219,8 @@ describe(CreateAndShareRelationshipAttributeUseCase.name, () => { key: "test", value: { "@type": "ProprietaryString", - value: "a String", - title: "a title" + value: "AString", + title: "ATitle" }, confidentiality: RelationshipAttributeConfidentiality.Public }, @@ -1169,8 +1263,8 @@ describe(SucceedRelationshipAttributeAndNotifyPeerUseCase.name, () => { key: "test", value: { "@type": "ProprietaryString", - value: "a String", - title: "a title" + value: "AString", + title: "ATitle" }, confidentiality: RelationshipAttributeConfidentiality.Public } @@ -1251,6 +1345,114 @@ describe(SucceedRelationshipAttributeAndNotifyPeerUseCase.name, () => { }); }); +describe(ChangeDefaultRepositoryAttributeUseCase.name, () => { + beforeAll(async () => { + await cleanupAttributes(); + }); + + test("should change default RepositoryAttribute", async () => { + const defaultAttribute = ( + await services1.consumption.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "My default name" + } + } + }) + ).value; + expect(defaultAttribute.isDefault).toBe(true); + + const desiredDefaultAttribute = ( + await services1.consumption.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "My new default name" + } + } + }) + ).value; + expect(desiredDefaultAttribute.isDefault).toBeUndefined(); + + const result = await services1.consumption.attributes.changeDefaultRepositoryAttribute({ attributeId: desiredDefaultAttribute.id }); + expect(result.isSuccess).toBe(true); + const newDefaultAttribute = result.value; + expect(newDefaultAttribute.isDefault).toBe(true); + + const updatedFormerDesiredDefaultAttribute = (await services1.consumption.attributes.getAttribute({ id: desiredDefaultAttribute.id })).value; + expect(updatedFormerDesiredDefaultAttribute.isDefault).toBe(true); + + const updatedFormerDefaultAttribute = (await services1.consumption.attributes.getAttribute({ id: defaultAttribute.id })).value; + expect(updatedFormerDefaultAttribute.isDefault).toBeUndefined(); + }); + + test("should return an error if the new default attribute is not a RepositoryAttribute", async () => { + await services1.consumption.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "My default name" + } + } + }); + + const desiredSharedDefaultAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(services1, services2, { + content: { + value: { + "@type": "GivenName", + value: "My new shared name" + } + } + }); + const result = await services1.consumption.attributes.changeDefaultRepositoryAttribute({ attributeId: desiredSharedDefaultAttribute.id }); + expect(result).toBeAnError(`Attribute '${desiredSharedDefaultAttribute.id.toString()}' is not a RepositoryAttribute.`, "error.runtime.attributes.isNotRepositoryAttribute"); + }); + + test("should return an error if the new default attribute has a successor", async () => { + await services1.consumption.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "My default name" + } + } + }); + + const desiredDefaultAttribute = ( + await services1.consumption.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "My new default name" + } + } + }) + ).value; + + const successionResult = ( + await services1.consumption.attributes.succeedRepositoryAttribute({ + predecessorId: desiredDefaultAttribute.id, + successorContent: { + value: { + "@type": "GivenName", + value: "My new successor default name" + } + } + }) + ).value; + + const updatedDesiredDefaultAttribute = successionResult.predecessor; + const desiredDefaultAttributeSuccessor = successionResult.successor; + + const result = await services1.consumption.attributes.changeDefaultRepositoryAttribute({ attributeId: updatedDesiredDefaultAttribute.id }); + expect(result).toBeAnError( + `Attribute '${updatedDesiredDefaultAttribute.id.toString()}' already has a successor ${desiredDefaultAttributeSuccessor.id.toString()}.`, + "error.runtime.attributes.hasSuccessor" + ); + }); +}); + describe("Get (shared) versions of attribute", () => { let sRepositoryAttributeVersion0: LocalAttributeDTO; let sRepositoryAttributeVersion1: LocalAttributeDTO; @@ -1359,7 +1561,7 @@ describe("Get (shared) versions of attribute", () => { async function createAndShareRelationshipAttributeVersion0(): Promise { sOwnSharedRelationshipAttributeVersion0 = await executeFullCreateAndShareRelationshipAttributeFlow(services1, services2, { content: { - key: "Some key", + key: "AKey", value: { "@type": "ProprietaryInteger", title: "Version", @@ -1493,6 +1695,7 @@ describe("Get (shared) versions of attribute", () => { beforeAll(async () => { await setUpIdentityAttributeVersions(); }); + test("should get only latest shared version per peer of a repository attribute", async () => { for (const version of sRepositoryAttributeVersions) { const result1 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id }); @@ -1562,7 +1765,11 @@ describe("Get (shared) versions of attribute", () => { content: { items: [ ReadAttributeRequestItem.from({ - query: ThirdPartyRelationshipAttributeQuery.from({ key: "a key", owner: services1.address, thirdParty: [services2.address] }), + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [services2.address] + }), mustBeAccepted: true }).toJSON() ] @@ -1592,7 +1799,7 @@ describe("Get (shared) versions of attribute", () => { test("should return an empty list calling getSharedVersionsOfAttribute with a nonexistent peer", async () => { const result = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: sRepositoryAttributeVersion2.id, - peers: ["id1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] + peers: ["did:e:a-domain:dids:0000000000000000000000"] }); expect(result.isSuccess).toBe(true); const returnedVersions = result.value; @@ -1604,94 +1811,6 @@ describe("Get (shared) versions of attribute", () => { expect(result2).toBeAnError(/.*/, "error.runtime.recordNotFound"); }); }); - - describe(GetSharedVersionsOfRepositoryAttributeUseCase.name, () => { - beforeAll(async () => { - await setUpIdentityAttributeVersions(); - }); - test("should get only latest shared version per peer of a repository attribute", async () => { - for (const version of sRepositoryAttributeVersions) { - const result1 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: version.id }); - expect(result1.isSuccess).toBe(true); - const returnedVersions1 = result1.value; - expect(returnedVersions1).toStrictEqual(expect.arrayContaining([sOwnSharedIdentityAttributeVersion2, sOwnSharedIdentityAttributeVersion2FurtherPeer])); - - const result2 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: version.id, onlyLatestVersions: true }); - expect(result2.isSuccess).toBe(true); - const returnedVersions2 = result2.value; - expect(returnedVersions2).toStrictEqual(expect.arrayContaining([sOwnSharedIdentityAttributeVersion2, sOwnSharedIdentityAttributeVersion2FurtherPeer])); - } - }); - - test("should get all shared versions of a repository attribute", async () => { - for (const version of sRepositoryAttributeVersions) { - const result = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: version.id, onlyLatestVersions: false }); - expect(result.isSuccess).toBe(true); - - const returnedVersions = result.value; - expect(returnedVersions).toStrictEqual( - expect.arrayContaining([sOwnSharedIdentityAttributeVersion2, sOwnSharedIdentityAttributeVersion2FurtherPeer, sOwnSharedIdentityAttributeVersion0]) - ); - } - }); - - test("should get only latest shared version of a repository attribute for a specific peer", async () => { - for (const version of sRepositoryAttributeVersions) { - const result1 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: version.id, peers: [services2.address] }); - expect(result1.isSuccess).toBe(true); - const returnedVersions1 = result1.value; - expect(returnedVersions1).toStrictEqual([sOwnSharedIdentityAttributeVersion2]); - - const result2 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: version.id, peers: [services3.address] }); - expect(result2.isSuccess).toBe(true); - const returnedVersions2 = result2.value; - expect(returnedVersions2).toStrictEqual([sOwnSharedIdentityAttributeVersion2FurtherPeer]); - } - }); - - test("should get all shared versions of a repository attribute for a specific peer", async () => { - for (const version of sRepositoryAttributeVersions) { - const result1 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ - attributeId: version.id, - peers: [services2.address], - onlyLatestVersions: false - }); - expect(result1.isSuccess).toBe(true); - const returnedVersions1 = result1.value; - expect(returnedVersions1).toStrictEqual([sOwnSharedIdentityAttributeVersion2, sOwnSharedIdentityAttributeVersion0]); - - const result2 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ - attributeId: version.id, - peers: [services3.address], - onlyLatestVersions: false - }); - expect(result2.isSuccess).toBe(true); - const returnedVersions2 = result2.value; - expect(returnedVersions2).toStrictEqual([sOwnSharedIdentityAttributeVersion2FurtherPeer]); - } - }); - - test("should return an empty list calling getSharedVersionsOfRepositoryAttribute with a nonexistent peer", async () => { - const result = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ - attributeId: sRepositoryAttributeVersion2.id, - peers: ["id1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] - }); - expect(result.isSuccess).toBe(true); - const returnedVersions = result.value; - expect(returnedVersions).toStrictEqual([]); - }); - - test("should throw trying to call getSharedVersionsOfRepositoryAttribute with a nonexistent attributeId", async () => { - const result2 = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: "ATTxxxxxxxxxxxxxxxxx" }); - expect(result2).toBeAnError(/.*/, "error.runtime.recordNotFound"); - }); - - test("should throw trying to call getSharedVersionsOfRepositoryAttribute with a relationship attribute", async () => { - await createAndShareRelationshipAttributeVersion0(); - const result = await services1.consumption.attributes.getSharedVersionsOfRepositoryAttribute({ attributeId: sOwnSharedRelationshipAttributeVersion0.id }); - expect(result).toBeAnError(/.*/, "error.runtime.attributes.isNotRepositoryAttribute"); - }); - }); }); describe("DeleteAttributeUseCases", () => { @@ -1852,10 +1971,10 @@ describe("DeleteAttributeUseCases", () => { content: { value: { "@type": "ProprietaryString", - value: "a value", - title: "a title" + value: "AStringValue", + title: "ATitle" }, - key: "a key", + key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public } }); @@ -1865,7 +1984,11 @@ describe("DeleteAttributeUseCases", () => { content: { items: [ ReadAttributeRequestItem.from({ - query: ThirdPartyRelationshipAttributeQuery.from({ key: "a key", owner: services1.address, thirdParty: [services3.address] }), + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [services3.address] + }), mustBeAccepted: true }).toJSON() ] @@ -1956,10 +2079,10 @@ describe("DeleteAttributeUseCases", () => { content: { value: { "@type": "ProprietaryString", - value: "a value", - title: "a title" + value: "AStringValue", + title: "ATitle" }, - key: "a key", + key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public } }); @@ -1969,7 +2092,11 @@ describe("DeleteAttributeUseCases", () => { content: { items: [ ReadAttributeRequestItem.from({ - query: ThirdPartyRelationshipAttributeQuery.from({ key: "a key", owner: services3.address, thirdParty: [services3.address] }), + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [services3.address] + }), mustBeAccepted: true }).toJSON() ] @@ -2047,10 +2174,10 @@ describe("DeleteAttributeUseCases", () => { content: { value: { "@type": "ProprietaryString", - value: "A proprietary string", - title: "A title" + value: "AString", + title: "ATitle" }, - key: "A key", + key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public } }); @@ -2060,7 +2187,11 @@ describe("DeleteAttributeUseCases", () => { content: { items: [ ReadAttributeRequestItem.from({ - query: ThirdPartyRelationshipAttributeQuery.from({ key: "A key", owner: services3.address, thirdParty: [services3.address] }), + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [services3.address] + }), mustBeAccepted: true }).toJSON() ] diff --git a/packages/runtime/test/consumption/iqlQuery.test.ts b/packages/runtime/test/consumption/iqlQuery.test.ts index 579006877..fa565ccb5 100644 --- a/packages/runtime/test/consumption/iqlQuery.test.ts +++ b/packages/runtime/test/consumption/iqlQuery.test.ts @@ -4,7 +4,7 @@ import { IQLQueryJSON, ReadAttributeRequestItemJSON } from "@nmshd/content"; import { DateTime } from "luxon"; import { ConsumptionServices, CreateOutgoingRequestRequest, LocalAttributeDTO, OutgoingRequestCreatedEvent, OutgoingRequestStatusChangedEvent, TransportServices } from "../../src"; import { IncomingRequestReceivedEvent, IncomingRequestStatusChangedEvent } from "../../src/events"; -import { establishRelationship, exchangeMessageWithRequest, RuntimeServiceProvider, sendMessageWithRequest, TestRuntimeServices } from "../lib"; +import { RuntimeServiceProvider, TestRuntimeServices, establishRelationship, exchangeMessageWithRequest, sendMessageWithRequest } from "../lib"; import { exchangeMessageWithRequestAndRequireManualDecision, exchangeMessageWithRequestAndSendResponse } from "../lib/testUtilsWithInactiveModules"; describe("IQL Query", () => { @@ -110,7 +110,7 @@ describe("IQL Query", () => { expect(sLocalRequest.status).toBe(LocalRequestStatus.Draft); expect(sLocalRequest.content.items).toHaveLength(1); expect(sLocalRequest.content.items[0]["@type"]).toBe("ReadAttributeRequestItem"); - expect(sLocalRequest.content.items[0].mustBeAccepted).toBe(false); + expect((sLocalRequest.content.items[0] as ReadAttributeRequestItemJSON).mustBeAccepted).toBe(false); }); test("sender: send the outgoing IQL Request via Message", async () => { @@ -123,7 +123,7 @@ describe("IQL Query", () => { triggeredEvent = event; }); const sRequestMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); - const result = await sConsumptionServices.outgoingRequests.sent({ requestId: sRequestMessage.content.id, messageId: sRequestMessage.id }); + const result = await sConsumptionServices.outgoingRequests.sent({ requestId: sRequestMessage.content.id!, messageId: sRequestMessage.id }); expect(result).toBeSuccessful(); @@ -170,7 +170,7 @@ describe("IQL Query", () => { }); const result = await rConsumptionServices.incomingRequests.checkPrerequisites({ - requestId: message.content.id + requestId: message.content.id! }); expect(result).toBeSuccessful(); @@ -193,7 +193,7 @@ describe("IQL Query", () => { requestSourceId: message.id }); await rConsumptionServices.incomingRequests.checkPrerequisites({ - requestId: message.content.id + requestId: message.content.id! }); let triggeredEvent: IncomingRequestStatusChangedEvent | undefined; rEventBus.subscribeOnce(IncomingRequestStatusChangedEvent, (event) => { @@ -201,7 +201,7 @@ describe("IQL Query", () => { }); const result = await rConsumptionServices.incomingRequests.requireManualDecision({ - requestId: message.content.id + requestId: message.content.id! }); expect(result).toBeSuccessful(); diff --git a/packages/runtime/test/consumption/notifications.test.ts b/packages/runtime/test/consumption/notifications.test.ts index 78b77cea8..800484126 100644 --- a/packages/runtime/test/consumption/notifications.test.ts +++ b/packages/runtime/test/consumption/notifications.test.ts @@ -1,7 +1,7 @@ import { ConsumptionIds, LocalNotificationStatus } from "@nmshd/consumption"; import { Notification } from "@nmshd/content"; -import { CoreIdHelper } from "@nmshd/transport"; -import { ConsumptionServices, TransportServices } from "../../src"; +import { CoreId, CoreIdHelper } from "@nmshd/transport"; +import { ConsumptionServices, RuntimeErrors, TransportServices } from "../../src"; import { establishRelationship, RuntimeServiceProvider, @@ -53,6 +53,19 @@ describe("Notifications", () => { expect(getNotificationResult.value.status).toStrictEqual(LocalNotificationStatus.Sent); }); + test("sent Notification with error", async () => { + const id = await ConsumptionIds.notification.generate(); + const notificationToSend = Notification.from({ id, items: [TestNotificationItem.from({})] }); + await sTransportServices.messages.sendMessage({ recipients: [rAddress], content: notificationToSend.toJSON() }); + + const message = await syncUntilHasMessageWithNotification(rTransportServices, id); + const result = await rConsumptionServices.notifications.sentNotification({ messageId: message.id }); + expect(result).toBeAnError( + RuntimeErrors.notifications.cannotSaveSentNotificationFromPeerMessage(CoreId.from(message.id)).message, + "error.runtime.notifications.cannotSaveSentNotificationFromPeerMessage" + ); + }); + test("receivedNotification", async () => { const id = await ConsumptionIds.notification.generate(); const notificationToSend = Notification.from({ id, items: [TestNotificationItem.from({})] }); @@ -122,12 +135,12 @@ describe("Notifications", () => { expect(result).toBeAnError("'LocalNotification' not found.", "error.transport.recordNotFound"); }); - test("wrong message id for sentNotification", async () => { + test("wrong Message ID for sentNotification", async () => { const result = await sConsumptionServices.notifications.sentNotification({ messageId: "wrong-id" }); expect(result).toBeAnError("messageId must match pattern MSG\\[A-Za-z0-9\\]\\{17\\}", "error.runtime.validation.invalidPropertyValue"); }); - test("not existing message id for sentNotification", async () => { + test("not existing Message ID for sentNotification", async () => { const result = await sConsumptionServices.notifications.sentNotification({ messageId: (await new CoreIdHelper("MSG").generate()).toString() }); expect(result).toBeAnError("Message not found. Make sure the ID exists and the record is not expired.", "error.runtime.recordNotFound"); }); diff --git a/packages/runtime/test/consumption/requests.test.ts b/packages/runtime/test/consumption/requests.test.ts index 87c6e30f1..4a06974e2 100644 --- a/packages/runtime/test/consumption/requests.test.ts +++ b/packages/runtime/test/consumption/requests.test.ts @@ -1,24 +1,18 @@ import { EventBus } from "@js-soft/ts-utils"; import { LocalRequestStatus } from "@nmshd/consumption"; -import { RelationshipCreationChangeRequestContentJSON } from "@nmshd/content"; +import { TestRequestItemJSON } from "@nmshd/consumption/test/modules/requests/testHelpers/TestRequestItem"; +import { RelationshipCreationContentJSON, RelationshipTemplateContentJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/transport"; -import { - ConsumptionServices, - CreateOutgoingRequestRequest, - OutgoingRequestCreatedEvent, - OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent, - OutgoingRequestStatusChangedEvent, - TransportServices -} from "../../src"; +import { ConsumptionServices, CreateOutgoingRequestRequest, OutgoingRequestCreatedEvent, OutgoingRequestStatusChangedEvent, TransportServices } from "../../src"; import { IncomingRequestReceivedEvent, IncomingRequestStatusChangedEvent } from "../../src/events"; import { + RuntimeServiceProvider, + TestRuntimeServices, establishRelationship, exchangeMessageWithRequest, exchangeTemplate, - RuntimeServiceProvider, sendMessageWithRequest, - syncUntilHasRelationships, - TestRuntimeServices + syncUntilHasRelationships } from "../lib"; import { exchangeMessageWithRequestAndRequireManualDecision, @@ -97,7 +91,7 @@ describe("Requests", () => { expect(sLocalRequest.status).toBe(LocalRequestStatus.Draft); expect(sLocalRequest.content.items).toHaveLength(1); expect(sLocalRequest.content.items[0]["@type"]).toBe("TestRequestItem"); - expect(sLocalRequest.content.items[0].mustBeAccepted).toBe(false); + expect((sLocalRequest.content.items[0] as TestRequestItemJSON).mustBeAccepted).toBe(false); }); // eslint-disable-next-line jest/expect-expect @@ -111,7 +105,7 @@ describe("Requests", () => { triggeredEvent = event; }); const sRequestMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); - const result = await sConsumptionServices.outgoingRequests.sent({ requestId: sRequestMessage.content.id, messageId: sRequestMessage.id }); + const result = await sConsumptionServices.outgoingRequests.sent({ requestId: sRequestMessage.content.id!, messageId: sRequestMessage.id }); expect(result).toBeSuccessful(); expect(result.value.status).toBe(LocalRequestStatus.Open); @@ -138,7 +132,7 @@ describe("Requests", () => { expect(rLocalRequest).toBeDefined(); expect(rLocalRequest.status).toBe(LocalRequestStatus.Open); - expect(rLocalRequest.id).toBe(rRequestMessage.content.id); + expect(rLocalRequest.id).toBe(rRequestMessage.content.id!); expect(triggeredEvent).toBeDefined(); expect(triggeredEvent!.data).toBeDefined(); @@ -157,7 +151,7 @@ describe("Requests", () => { }); const result = await rConsumptionServices.incomingRequests.checkPrerequisites({ - requestId: message.content.id + requestId: message.content.id! }); expect(result).toBeSuccessful(); @@ -180,7 +174,7 @@ describe("Requests", () => { requestSourceId: message.id }); await rConsumptionServices.incomingRequests.checkPrerequisites({ - requestId: message.content.id + requestId: message.content.id! }); let triggeredEvent: IncomingRequestStatusChangedEvent | undefined; rEventBus.subscribeOnce(IncomingRequestStatusChangedEvent, (event) => { @@ -188,7 +182,7 @@ describe("Requests", () => { }); const result = await rConsumptionServices.incomingRequests.requireManualDecision({ - requestId: message.content.id + requestId: message.content.id! }); expect(result).toBeSuccessful(); @@ -355,9 +349,8 @@ describe("Requests", () => { let sTransportServices: TransportServices; let rTransportServices: TransportServices; let rEventBus: EventBus; - let sEventBus: EventBus; - const templateContent = { + const templateContent: RelationshipTemplateContentJSON = { "@type": "RelationshipTemplateContent", onNewRelationship: { "@type": "Request", @@ -379,7 +372,6 @@ describe("Requests", () => { rTransportServices = rRuntimeServices.transport; sConsumptionServices = sRuntimeServices.consumption; rConsumptionServices = rRuntimeServices.consumption; - sEventBus = sRuntimeServices.eventBus; rEventBus = rRuntimeServices.eventBus; }, 30000); afterAll(async () => await runtimeServiceProvider.stop()); @@ -405,7 +397,7 @@ describe("Requests", () => { }); const result = await rConsumptionServices.incomingRequests.received({ - receivedRequest: rRelationshipTemplate.content.onNewRelationship, + receivedRequest: (rRelationshipTemplate.content as RelationshipTemplateContentJSON).onNewRelationship, requestSourceId: rRelationshipTemplate.id }); @@ -426,7 +418,7 @@ describe("Requests", () => { const rRelationshipTemplate = await exchangeTemplate(sTransportServices, rTransportServices, templateContent); const incomingRequest = ( await rConsumptionServices.incomingRequests.received({ - receivedRequest: rRelationshipTemplate.content.onNewRelationship, + receivedRequest: (rRelationshipTemplate.content as RelationshipTemplateContentJSON).onNewRelationship, requestSourceId: rRelationshipTemplate.id }) ).value; @@ -456,7 +448,7 @@ describe("Requests", () => { const rRelationshipTemplate = await exchangeTemplate(sTransportServices, rTransportServices, templateContent); const incomingRequest = ( await rConsumptionServices.incomingRequests.received({ - receivedRequest: rRelationshipTemplate.content.onNewRelationship, + receivedRequest: (rRelationshipTemplate.content as RelationshipTemplateContentJSON).onNewRelationship, requestSourceId: rRelationshipTemplate.id }) ).value; @@ -556,7 +548,7 @@ describe("Requests", () => { const result = await rConsumptionServices.incomingRequests.complete({ requestId: request.id, - responseSourceId: action === "Accept" ? relationship?.changes[0].id : undefined + responseSourceId: action === "Accept" ? relationship?.id : undefined }); expect(result).toBeSuccessful(); @@ -577,17 +569,11 @@ describe("Requests", () => { expect(syncResult).toHaveLength(1); - const sRelationshipChange = syncResult[0].changes[0]; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let triggeredCompletionEvent: OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent | undefined; - sEventBus.subscribeOnce(OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent, (event) => { - triggeredCompletionEvent = event; - }); + const sRelationship = syncResult[0]; const completionResult = await sConsumptionServices.outgoingRequests.createAndCompleteFromRelationshipTemplateResponse({ - responseSourceId: sRelationshipChange.id, - response: (sRelationshipChange.request.content as RelationshipCreationChangeRequestContentJSON).response, + responseSourceId: sRelationship.id, + response: (sRelationship.creationContent as RelationshipCreationContentJSON).response, templateId: relationship!.template.id }); diff --git a/packages/runtime/test/dataViews/IdentityAttributeQueryExpanded.test.ts b/packages/runtime/test/dataViews/IdentityAttributeQueryExpanded.test.ts index d11e5c7db..9bb5dfb5e 100644 --- a/packages/runtime/test/dataViews/IdentityAttributeQueryExpanded.test.ts +++ b/packages/runtime/test/dataViews/IdentityAttributeQueryExpanded.test.ts @@ -18,33 +18,37 @@ describe("IdentityAttributeQueryExpanded", () => { const attributes: LocalAttributeDTO[] = []; beforeAll(async () => { - attributes.push( - ( - await consumptionServices1.attributes.createRepositoryAttribute({ - content: { - value: { - "@type": "GivenName", - value: "Hugo" - } - } - }) - ).value - ); - attributes.push( - ( - await consumptionServices1.attributes.createRepositoryAttribute({ - content: { - value: { - "@type": "GivenName", - value: "Egon" - } - } - }) - ).value - ); + const firstlyCreatedGivenName = ( + await consumptionServices1.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "A first given name" + }, + tags: ["notDefault"] + } + }) + ).value; + + const secondlyCreatedGivenName = ( + await consumptionServices1.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "A second given name" + }, + tags: ["default"] + } + }) + ).value; + + const updatedSecondlyCreatedGivenName = (await consumptionServices1.attributes.changeDefaultRepositoryAttribute({ attributeId: secondlyCreatedGivenName.id })).value; + const updatedFirstlyCreatedGivenName = (await consumptionServices1.attributes.getAttribute({ id: firstlyCreatedGivenName.id })).value; + + attributes.push(updatedSecondlyCreatedGivenName, updatedFirstlyCreatedGivenName); }); - test("check the GivenName", async () => { + test("check the order and content of the expanded LocalAttributes that match the query", async () => { const query: IdentityAttributeQueryJSON = { "@type": "IdentityAttributeQuery", valueType: "GivenName" @@ -75,7 +79,9 @@ describe("IdentityAttributeQueryExpanded", () => { expect(dvo.content).toStrictEqual(attribute.content); const givenName = dvo.value as AbstractStringJSON; expect(givenName["@type"]).toBe("GivenName"); - expect(givenName.value).toBe("Hugo"); + expect(givenName.value).toBe("A second given name"); + expect(dvo.tags).toStrictEqual(["default"]); + expect(dvo.isDefault).toBe(true); expect(dvo.createdAt).toStrictEqual(attribute.createdAt); expect(dvo.isOwn).toBe(true); expect(dvo.isValid).toBe(true); @@ -98,7 +104,9 @@ describe("IdentityAttributeQueryExpanded", () => { expect(dvo.content).toStrictEqual(attribute.content); const value = dvo.value as AbstractStringJSON; expect(value["@type"]).toBe("GivenName"); - expect(value.value).toBe("Egon"); + expect(value.value).toBe("A first given name"); + expect(dvo.tags).toStrictEqual(["notDefault"]); + expect(dvo.isDefault).toBeUndefined(); expect(dvo.createdAt).toStrictEqual(attribute.createdAt); expect(dvo.isOwn).toBe(true); expect(dvo.isValid).toBe(true); diff --git a/packages/runtime/test/dataViews/IdentityDVO.test.ts b/packages/runtime/test/dataViews/IdentityDVO.test.ts index aec1e3a11..b0901ce71 100644 --- a/packages/runtime/test/dataViews/IdentityDVO.test.ts +++ b/packages/runtime/test/dataViews/IdentityDVO.test.ts @@ -1,6 +1,13 @@ import { LocalRequestStatus } from "@nmshd/consumption"; -import { ShareAttributeRequestItemJSON } from "@nmshd/content"; -import { IncomingRequestStatusChangedEvent, LocalRequestDTO, PeerRelationshipTemplateDVO, RelationshipTemplateDTO } from "../../src"; +import { RelationshipTemplateContentJSON, ShareAttributeRequestItemJSON } from "@nmshd/content"; +import { + AttributeDeletedEvent, + IncomingRequestStatusChangedEvent, + LocalRequestDTO, + PeerRelationshipTemplateDVO, + RelationshipChangedEvent, + RelationshipTemplateDTO +} from "../../src"; import { createTemplate, RuntimeServiceProvider, TestRuntimeServices } from "../lib"; const serviceProvider = new RuntimeServiceProvider(); @@ -29,7 +36,7 @@ describe("IdentityDVO after loading a relationship template sharing a DisplayNam } }); - const templateContent = { + const templateContent: RelationshipTemplateContentJSON = { "@type": "RelationshipTemplateContent", onNewRelationship: { "@type": "Request", @@ -65,4 +72,23 @@ describe("IdentityDVO after loading a relationship template sharing a DisplayNam expect(dvo.peer.name).toBe("A Display Name"); expect(dvo.peer.initials).toBe("ADN"); }); + + test("IdentityDVO should use the DisplayName if a revoked relationship exists", async () => { + await requestor.consumption.incomingRequests.accept({ requestId: request.id, items: [{ accept: true }] }); + const relationshipId = (await requestor.eventBus.waitForEvent(RelationshipChangedEvent)).data.id; + await requestor.transport.relationships.revokeRelationship({ relationshipId }); + await requestor.eventBus.waitForEvent(AttributeDeletedEvent); + + requestor.eventBus.reset(); + requestorTemplate = (await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference })).value; + await requestor.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + + const dvo = await requestor.expander.expandLocalRequestDTO(request); + expect(dvo).toBeDefined(); + expect(dvo.createdBy.name).toBe("A Display Name"); + expect(dvo.createdBy.initials).toBe("ADN"); + + expect(dvo.peer.name).toBe("A Display Name"); + expect(dvo.peer.initials).toBe("ADN"); + }); }); diff --git a/packages/runtime/test/dataViews/MessageDVO.test.ts b/packages/runtime/test/dataViews/MessageDVO.test.ts index 69d7d6b3b..e9ce67a8f 100644 --- a/packages/runtime/test/dataViews/MessageDVO.test.ts +++ b/packages/runtime/test/dataViews/MessageDVO.test.ts @@ -1,4 +1,15 @@ -import { GivenName, IdentityAttribute, MailJSON, ReadAttributeAcceptResponseItem, ReadAttributeRequestItem, ResponseItemResult, ResponseResult } from "@nmshd/content"; +import { + ArbitraryMessageContent, + GivenName, + IdentityAttribute, + MailJSON, + ReadAttributeAcceptResponseItem, + ReadAttributeRequestItem, + RelationshipCreationContent, + RelationshipTemplateContent, + ResponseItemResult, + ResponseResult +} from "@nmshd/content"; import { CoreAddress, CoreId } from "@nmshd/transport"; import { DataViewExpander, MailDVO, SendMessageRequest, TransportServices } from "../../src"; import { establishRelationshipWithContents, getRelationship, RuntimeServiceProvider, syncUntilHasMessage, uploadFile } from "../lib"; @@ -18,7 +29,7 @@ beforeAll(async () => { await establishRelationshipWithContents( transportServices1, transportServices2, - { + RelationshipTemplateContent.from({ onNewRelationship: { "@type": "Request", items: [ @@ -31,12 +42,12 @@ beforeAll(async () => { }) ] } - }, - { + }).toJSON(), + RelationshipCreationContent.from({ response: { "@type": "Response", result: ResponseResult.Accepted, - requestId: await CoreId.generate(), + requestId: (await CoreId.generate()).toString(), items: [ ReadAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, @@ -45,10 +56,10 @@ beforeAll(async () => { owner: CoreAddress.from((await transportServices1.account.getIdentityInfo()).value.address), value: GivenName.from("AGivenName") }) - }) + }).toJSON() ] } - } + }).toJSON() ); }, 30000); @@ -70,9 +81,11 @@ describe("MessageDVO", () => { messageRequest = { recipients: [transportService2Address], - content: { - arbitraryValue: true - }, + content: ArbitraryMessageContent.from({ + value: { + arbitraryValue: true + } + }).toJSON(), attachments: [fileId] }; mailRequest = { @@ -107,7 +120,7 @@ describe("MessageDVO", () => { const recipient = dvo.recipients[0]; expect(recipient.type).toBe("RecipientDVO"); expect(recipient.id).toStrictEqual(dto.recipients[0].address); - expect(recipient.name).toBe(recipient.id.substring(3, 9)); // "Barbara" + expect(recipient.name).toBe("i18n://dvo.identity.unknown"); expect(recipient.isSelf).toBe(false); expect(dvo.status).toBe("Delivering"); }); @@ -129,7 +142,7 @@ describe("MessageDVO", () => { expect(dvo.isOwn).toBe(false); expect(dvo.createdBy.type).toBe("IdentityDVO"); expect(dvo.createdBy.id).toStrictEqual(dto.createdBy); - expect(dvo.createdBy.name).toBe(dvo.createdBy.id.substring(3, 9)); // "Jürgen" + expect(dvo.createdBy.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.createdBy.isSelf).toBe(false); const recipient = dvo.recipients[0]; expect(recipient.type).toBe("RecipientDVO"); @@ -164,14 +177,14 @@ describe("MessageDVO", () => { const recipient = dvo.recipients[0]; expect(recipient.type).toBe("RecipientDVO"); expect(recipient.id).toStrictEqual(dto.recipients[0].address); - expect(recipient.name).toBe(recipient.id.substring(3, 9)); // "Barbara" + expect(recipient.name).toBe("i18n://dvo.identity.unknown"); expect(recipient.isSelf).toBe(false); expect(dvo.to).toHaveLength(1); const to = dvo.to[0]; expect(to.type).toBe("RecipientDVO"); expect(to.id).toStrictEqual(mail.to[0]); - expect(to.name).toBe(to.id.substring(3, 9)); // "Barbara" + expect(to.name).toBe("i18n://dvo.identity.unknown"); expect(to.isSelf).toBe(false); expect(dvo.toCount).toStrictEqual(mail.to.length); expect(dvo.ccCount).toStrictEqual(mail.cc!.length); @@ -198,7 +211,7 @@ describe("MessageDVO", () => { expect(dvo.isOwn).toBe(false); expect(dvo.createdBy.type).toBe("IdentityDVO"); expect(dvo.createdBy.id).toStrictEqual(dto.createdBy); - expect(dvo.createdBy.name).toBe(dvo.createdBy.id.substring(3, 9)); // "Jürgen" + expect(dvo.createdBy.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.createdBy.isSelf).toBe(false); const recipient = dvo.recipients[0]; expect(recipient.type).toBe("RecipientDVO"); diff --git a/packages/runtime/test/dataViews/RelationshipDVO.test.ts b/packages/runtime/test/dataViews/RelationshipDVO.test.ts index 709dc5856..19cdc26f8 100644 --- a/packages/runtime/test/dataViews/RelationshipDVO.test.ts +++ b/packages/runtime/test/dataViews/RelationshipDVO.test.ts @@ -1,4 +1,13 @@ -import { GivenName, IdentityAttribute, ReadAttributeAcceptResponseItem, ReadAttributeRequestItem, ResponseItemResult, ResponseResult } from "@nmshd/content"; +import { + GivenName, + IdentityAttribute, + ReadAttributeAcceptResponseItem, + ReadAttributeRequestItem, + RelationshipCreationContent, + RelationshipTemplateContent, + ResponseItemResult, + ResponseResult +} from "@nmshd/content"; import { CoreAddress, CoreId } from "@nmshd/transport"; import { DataViewExpander, TransportServices } from "../../src"; import { establishRelationshipWithContents, RuntimeServiceProvider } from "../lib"; @@ -18,7 +27,8 @@ beforeAll(async () => { await establishRelationshipWithContents( transportServices1, transportServices2, - { + + RelationshipTemplateContent.from({ onNewRelationship: { "@type": "Request", items: [ @@ -31,12 +41,12 @@ beforeAll(async () => { }) ] } - }, - { + }).toJSON(), + RelationshipCreationContent.from({ response: { "@type": "Response", result: ResponseResult.Accepted, - requestId: await CoreId.generate(), + requestId: (await CoreId.generate()).toString(), items: [ ReadAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, @@ -45,10 +55,10 @@ beforeAll(async () => { owner: CoreAddress.from((await transportServices1.account.getIdentityInfo()).value.address), value: GivenName.from("AGivenName") }) - }) + }).toJSON() ] } - } + }).toJSON() ); }, 30000); @@ -62,35 +72,19 @@ describe("RelationshipDVO", () => { const dvo = dvos[0]; expect(dvo).toBeDefined(); expect(dvo.id).toBe(dto.peer); - expect(dvo.name).toBe(dto.peer.substring(3, 9)); + expect(dvo.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.description).toBe("i18n://dvo.relationship.Active"); expect(dvo.type).toBe("IdentityDVO"); - expect(dvo.date).toBe(dto.changes[0].request.createdAt); + expect(dvo.isSelf).toBe(false); expect(dvo.relationship!.id).toBe(dto.id); expect(dvo.relationship!.direction).toBe("Incoming"); expect(dvo.relationship!.status).toBe("Active"); expect(dvo.relationship!.statusText).toBe("i18n://dvo.relationship.Active"); - expect(dvo.relationship!.changeCount).toBe(1); - const change = dvo.relationship!.changes[0]; - expect(change.id).toBe(dto.changes[0].id); - expect(change.type).toBe("RelationshipChangeDVO"); - expect(change.status).toBe(dto.changes[0].status); - expect(change.statusText).toBe("i18n://dvo.relationshipChange.Accepted"); - expect(change.request.type).toBe("RelationshipChangeRequestDVO"); - expect(change.request.createdAt).toBe(dto.changes[0].request.createdAt); - expect(change.request.createdBy).toBe(dto.changes[0].request.createdBy); - expect(change.request.createdByDevice).toBe(dto.changes[0].request.createdByDevice); - expect(change.request.content).toBe(dto.changes[0].request.content); - expect(change.isOwn).toBe(false); - expect(change.response!.type).toBe("RelationshipChangeResponseDVO"); - expect(change.response!.createdAt).toBe(dto.changes[0].response!.createdAt); - expect(change.response!.createdBy).toBe(dto.changes[0].response!.createdBy); - expect(change.response!.createdByDevice).toBe(dto.changes[0].response!.createdByDevice); - expect(change.response!.content).toBe(dto.changes[0].response!.content); - expect(change.date).toBe(dto.changes[0].response!.createdAt); + expect(dvo.relationship!.templateId).toBe(dto.template.id); }); + test("check the relationship dvo for the requestor", async () => { const dtos = (await transportServices2.relationships.getRelationships({})).value; const dvos = await expander2.expandRelationshipDTOs(dtos); @@ -98,33 +92,16 @@ describe("RelationshipDVO", () => { const dvo = dvos[0]; expect(dvo).toBeDefined(); expect(dvo.id).toBe(dto.peer); - expect(dvo.name).toBe(dto.peer.substring(3, 9)); + expect(dvo.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.description).toBe("i18n://dvo.relationship.Active"); expect(dvo.type).toBe("IdentityDVO"); - expect(dvo.date).toBe(dto.changes[0].request.createdAt); + expect(dvo.isSelf).toBe(false); expect(dvo.relationship!.id).toBe(dto.id); expect(dvo.relationship!.direction).toBe("Outgoing"); expect(dvo.relationship!.status).toBe("Active"); expect(dvo.relationship!.statusText).toBe("i18n://dvo.relationship.Active"); - expect(dvo.relationship!.changeCount).toBe(1); - const change = dvo.relationship!.changes[0]; - expect(change.id).toBe(dto.changes[0].id); - expect(change.type).toBe("RelationshipChangeDVO"); - expect(change.status).toBe(dto.changes[0].status); - expect(change.statusText).toBe("i18n://dvo.relationshipChange.Accepted"); - expect(change.request.type).toBe("RelationshipChangeRequestDVO"); - expect(change.request.createdAt).toBe(dto.changes[0].request.createdAt); - expect(change.request.createdBy).toBe(dto.changes[0].request.createdBy); - expect(change.request.createdByDevice).toBe(dto.changes[0].request.createdByDevice); - expect(change.request.content).toBe(dto.changes[0].request.content); - expect(change.isOwn).toBe(true); - expect(change.response!.type).toBe("RelationshipChangeResponseDVO"); - expect(change.response!.createdAt).toBe(dto.changes[0].response!.createdAt); - expect(change.response!.createdBy).toBe(dto.changes[0].response!.createdBy); - expect(change.response!.createdByDevice).toBe(dto.changes[0].response!.createdByDevice); - expect(change.response!.content).toBe(dto.changes[0].response!.content); - expect(change.date).toBe(dto.changes[0].response!.createdAt); + expect(dvo.relationship!.templateId).toBe(dto.template.id); }); }); diff --git a/packages/runtime/test/dataViews/RelationshipTemplateDVO.test.ts b/packages/runtime/test/dataViews/RelationshipTemplateDVO.test.ts index a87608e16..a5835ed28 100644 --- a/packages/runtime/test/dataViews/RelationshipTemplateDVO.test.ts +++ b/packages/runtime/test/dataViews/RelationshipTemplateDVO.test.ts @@ -7,23 +7,27 @@ import { ProposeAttributeRequestItem, ProposeAttributeRequestItemJSON, RelationshipAttributeConfidentiality, + RelationshipAttributeJSON, + RelationshipTemplateContent, + RelationshipTemplateContentJSON, + RequestItemGroupJSON, Surname } from "@nmshd/content"; import { CoreAddress } from "@nmshd/transport"; import { IncomingRequestStatusChangedEvent, - OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent, + OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent, PeerRelationshipTemplateDVO, - PeerRelationshipTemplateLoadedEvent, RelationshipTemplateDTO, RequestItemGroupDVO } from "../../src"; -import { createTemplate, RuntimeServiceProvider, syncUntilHasRelationships, TestRuntimeServices } from "../lib"; +import { RuntimeServiceProvider, TestRuntimeServices, createTemplate, syncUntilHasRelationships } from "../lib"; const serviceProvider = new RuntimeServiceProvider(); let templator: TestRuntimeServices; let requestor: TestRuntimeServices; -let templatorTemplate: RelationshipTemplateDTO; +let templatorTemplate: RelationshipTemplateDTO & { content: RelationshipTemplateContentJSON }; +let templateId: string; let responseItems: DecideRequestItemGroupParametersJSON[]; beforeAll(async () => { @@ -41,36 +45,34 @@ beforeEach(function () { describe("RelationshipTemplateDVO", () => { beforeAll(async () => { - const relationshipAttributeContent1 = { + const relationshipAttributeContent1: RelationshipAttributeJSON = { "@type": "RelationshipAttribute", owner: templator.address, value: { "@type": "ProprietaryString", - title: "aTitle", - value: "aProprietaryStringValue" + title: "ATitle", + value: "AProprietaryStringValue" }, key: "givenName", confidentiality: "protected" as RelationshipAttributeConfidentiality }; - const relationshipAttributeContent2 = { + const relationshipAttributeContent2: RelationshipAttributeJSON = { "@type": "RelationshipAttribute", owner: templator.address, value: { "@type": "ProprietaryString", - title: "aTitle", - value: "aProprietaryStringValue" + title: "ATitle", + value: "AProprietaryStringValue" }, key: "surname", confidentiality: "protected" as RelationshipAttributeConfidentiality }; - const templateContent = { - "@type": "RelationshipTemplateContent", + const templateContent = RelationshipTemplateContent.from({ onNewRelationship: { "@type": "Request", items: [ { "@type": "RequestItemGroup", - mustBeAccepted: true, title: "Templator Attributes", items: [ { @@ -87,7 +89,6 @@ describe("RelationshipTemplateDVO", () => { }, { "@type": "RequestItemGroup", - mustBeAccepted: true, title: "Proposed Attributes", items: [ ProposeAttributeRequestItem.from({ @@ -99,7 +100,7 @@ describe("RelationshipTemplateDVO", () => { owner: CoreAddress.from(""), value: GivenName.from("Theo") }) - }), + }).toJSON(), ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ @@ -109,17 +110,17 @@ describe("RelationshipTemplateDVO", () => { owner: CoreAddress.from(""), value: Surname.from("Templator") }) - }) + }).toJSON() ] } ] } - }; + }).toJSON(); const newIdentityAttribute1 = IdentityAttribute.from( - (templateContent.onNewRelationship.items[1].items[0] as ProposeAttributeRequestItemJSON).attribute as IdentityAttributeJSON + ((templateContent.onNewRelationship.items[1] as RequestItemGroupJSON).items[0] as ProposeAttributeRequestItemJSON).attribute as IdentityAttributeJSON ).toJSON(); const newIdentityAttribute2 = IdentityAttribute.from( - (templateContent.onNewRelationship.items[1].items[1] as ProposeAttributeRequestItemJSON).attribute as IdentityAttributeJSON + ((templateContent.onNewRelationship.items[1] as RequestItemGroupJSON).items[1] as ProposeAttributeRequestItemJSON).attribute as IdentityAttributeJSON ).toJSON(); responseItems = [ { items: [{ accept: true }, { accept: true }] }, @@ -140,7 +141,8 @@ describe("RelationshipTemplateDVO", () => { ] } ]; - templatorTemplate = await createTemplate(templator.transport, templateContent); + templatorTemplate = (await createTemplate(templator.transport, templateContent)) as RelationshipTemplateDTO & { content: RelationshipTemplateContentJSON }; + templateId = templatorTemplate.id; }); test("TemplateDVO for templator", async () => { @@ -174,7 +176,9 @@ describe("RelationshipTemplateDVO", () => { }); test("TemplateDVO for requestor", async () => { - const requestorTemplate = (await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference })).value; + const requestorTemplate = (await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference })) + .value as RelationshipTemplateDTO & { content: RelationshipTemplateContentJSON }; + await requestor.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const dto = requestorTemplate; const dvo = (await requestor.expander.expandRelationshipTemplateDTO(dto)) as PeerRelationshipTemplateDVO; @@ -206,13 +210,21 @@ describe("RelationshipTemplateDVO", () => { }); test("RequestDVO for requestor", async () => { - const requestorTemplate = (await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference })).value; - await requestor.eventBus.waitForEvent(PeerRelationshipTemplateLoadedEvent); - const requestResult = await requestor.consumption.incomingRequests.getRequests({ + let requestResult; + requestResult = await requestor.consumption.incomingRequests.getRequests({ query: { - "source.reference": requestorTemplate.id + "source.reference": templateId } }); + await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference }); + if (requestResult.value.length === 0) { + await requestor.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + requestResult = await requestor.consumption.incomingRequests.getRequests({ + query: { + "source.reference": templateId + } + }); + } expect(requestResult).toBeSuccessful(); expect(requestResult.value).toHaveLength(1); @@ -232,17 +244,18 @@ describe("RelationshipTemplateDVO", () => { let dto; let dvo; let requestResult; - const requestorTemplate = (await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference })).value; requestResult = await requestor.consumption.incomingRequests.getRequests({ query: { - "source.reference": requestorTemplate.id + "source.reference": templateId } }); + const requestorTemplate = (await requestor.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templatorTemplate.truncatedReference })) + .value as RelationshipTemplateDTO & { content: RelationshipTemplateContentJSON }; if (requestResult.value.length === 0) { await requestor.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); requestResult = await requestor.consumption.incomingRequests.getRequests({ query: { - "source.reference": requestorTemplate.id + "source.reference": templateId } }); } @@ -253,7 +266,7 @@ describe("RelationshipTemplateDVO", () => { const requestResultAfterAcceptance = await requestor.consumption.incomingRequests.getRequests({ query: { - "source.reference": requestorTemplate.id + "source.reference": templateId } }); expect(acceptResult).toBeSuccessful(); @@ -323,10 +336,10 @@ describe("RelationshipTemplateDVO", () => { expect(attributeResult.value).toHaveLength(4); await syncUntilHasRelationships(templator.transport); - await templator.eventBus.waitForEvent(OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent); + await templator.eventBus.waitForEvent(OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent); const requestResultTemplator = await templator.consumption.outgoingRequests.getRequests({ query: { - "source.reference": requestorTemplate.id + "source.reference": templateId } }); expect(requestResultTemplator).toBeSuccessful(); diff --git a/packages/runtime/test/dataViews/requestItems/ComplexReadAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ComplexReadAttributeRequestItemDVO.test.ts index 958d4c565..3aa52c0bc 100644 --- a/packages/runtime/test/dataViews/requestItems/ComplexReadAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ComplexReadAttributeRequestItemDVO.test.ts @@ -100,7 +100,7 @@ describe("ComplexReadAttributeRequestItemDVO with IdentityAttributeQuery", () => test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -181,7 +181,7 @@ describe("ComplexReadAttributeRequestItemDVO with IdentityAttributeQuery", () => const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -257,7 +257,7 @@ describe("ComplexReadAttributeRequestItemDVO with IdentityAttributeQuery", () => expect(responseItem.attributeId).toStrictEqual(attributeResult.value[numberOfAttributes - 1].id); expect(returnedValue).toStrictEqual(attributeValue); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -375,7 +375,7 @@ describe("ComplexReadAttributeRequestItemDVO with IQL", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -442,7 +442,7 @@ describe("ComplexReadAttributeRequestItemDVO with IQL", () => { const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -511,7 +511,7 @@ describe("ComplexReadAttributeRequestItemDVO with IQL", () => { expect(responseItem.attributeId).toStrictEqual(attributeResult.value[0].id); expect(returnedValue).toStrictEqual(attributeValue); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); diff --git a/packages/runtime/test/dataViews/requestItems/CreateIdentityAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/CreateIdentityAttributeRequestItemDVO.test.ts index 08646b1be..af190868d 100644 --- a/packages/runtime/test/dataViews/requestItems/CreateIdentityAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/CreateIdentityAttributeRequestItemDVO.test.ts @@ -87,7 +87,7 @@ beforeEach(function () { describe("CreateIdentityAttributeRequestItemDVO", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); - await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id); + await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id!); const dto = senderMessage; const dvo = (await sExpander.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -154,7 +154,7 @@ describe("CreateIdentityAttributeRequestItemDVO", () => { const recipientMessage = await exchangeMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); await rEventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await rConsumptionServices.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -209,14 +209,14 @@ describe("CreateIdentityAttributeRequestItemDVO", () => { expect(responseItem.attribute.valueType).toBe("DisplayName"); expect((attributeResult.value[0].content.value as DisplayNameJSON).value).toStrictEqual((responseItem.attribute.content.value as DisplayNameJSON).value); - await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id!); await sEventBus.waitForEvent(OutgoingRequestStatusChangedEvent); }); test("check the sender's dvo for the recipient", async () => { const senderMessage = await exchangeAndAcceptRequestByMessage(sRuntimeServices, rRuntimeServices, requestContent, responseItems); const dvo = await rExpander.expandAddress(senderMessage.createdBy); - expect(dvo.name).toStrictEqual(senderMessage.createdBy.substring(3, 9)); + expect(dvo.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.items).toHaveLength(0); }); diff --git a/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts index 49557c6f5..393368ff6 100644 --- a/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts @@ -92,7 +92,7 @@ beforeEach(function () { describe("CreateRelationshipAttributeRequestItemDVO", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); - await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id); + await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id!); const dto = senderMessage; const dvo = (await sExpander.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -159,7 +159,7 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { const recipientMessage = await exchangeMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); await rEventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await rConsumptionServices.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -216,7 +216,7 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { expect(responseItem.attribute.valueType).toBe("ProprietaryString"); expect(proprietaryString.value).toStrictEqual((responseItem.attribute.content.value as ProprietaryStringJSON).value); - await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id!); await sEventBus.waitForEvent(OutgoingRequestStatusChangedEvent); }); @@ -331,7 +331,7 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { const senderMessage = await exchangeAndAcceptRequestByMessage(sRuntimeServices, rRuntimeServices, requestContent, responseItems); const dvo = await sExpander.expandAddress(senderMessage.recipients[0].address); - expect(dvo.name).toStrictEqual(senderMessage.recipients[0].address.substring(3, 9)); + expect(dvo.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.items).toHaveLength(0); }); }); diff --git a/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts index fe4edbe0d..fbb85cdb3 100644 --- a/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts @@ -95,7 +95,7 @@ afterAll(() => serviceProvider.stop()); describe("DeleteAttributeRequestItemDVO", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); - await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id); + await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id!); const dto = senderMessage; const dvo = (await sExpander.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -163,7 +163,7 @@ describe("DeleteAttributeRequestItemDVO", () => { const recipientMessage = await exchangeMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); await rEventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await rConsumptionServices.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -198,7 +198,7 @@ describe("DeleteAttributeRequestItemDVO", () => { expect((responseItem as any).deletionDate).toBe(deletionDate); expect(requestItemDVO.response).toStrictEqual(responseItem); - await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id!); await sEventBus.waitForEvent(OutgoingRequestStatusChangedEvent); }); @@ -252,7 +252,7 @@ describe("DeleteAttributeRequestItemDVO", () => { const senderMessage = await exchangeAndAcceptRequestByMessage(sRuntimeServices, rRuntimeServices, requestContent, responseItems); const dvo = await sExpander.expandAddress(senderMessage.recipients[0].address); - expect(dvo.name).toStrictEqual(senderMessage.recipients[0].address.substring(3, 9)); + expect(dvo.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.items).toHaveLength(0); }); }); diff --git a/packages/runtime/test/dataViews/requestItems/FreeTextRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/FreeTextRequestItemDVO.test.ts index 6fbe30d86..f4c07ff41 100644 --- a/packages/runtime/test/dataViews/requestItems/FreeTextRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/FreeTextRequestItemDVO.test.ts @@ -78,7 +78,7 @@ describe("FreeTextRequestItemDVO", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -130,7 +130,7 @@ describe("FreeTextRequestItemDVO", () => { const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -169,7 +169,7 @@ describe("FreeTextRequestItemDVO", () => { expect(responseItem.freeText).toBe("I accept the free text."); expect(requestItemDVO.response).toStrictEqual(responseItem); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); diff --git a/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts index 9e69ee6e7..745220682 100644 --- a/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts @@ -156,7 +156,7 @@ describe("ProposeAttributeRequestItemDVO with IdentityAttributeQuery", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -280,7 +280,7 @@ describe("ProposeAttributeRequestItemDVO with IdentityAttributeQuery", () => { const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -385,7 +385,7 @@ describe("ProposeAttributeRequestItemDVO with IdentityAttributeQuery", () => { expect(responseItem2.attributeId).toStrictEqual(surnameShareResult.value[0].id); expect(surname.value).toStrictEqual((responseItem2.attribute.content.value as SurnameJSON).value); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -535,7 +535,7 @@ describe("AttributeSuccessionAcceptResponseItemDVO with IdentityAttributeQuery", const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -613,7 +613,7 @@ describe("AttributeSuccessionAcceptResponseItemDVO with IdentityAttributeQuery", expect(responseItem.successorId).toStrictEqual(successorResult.value[0].id); expect(successorName.value).toStrictEqual((responseItem.successor.content.value as GivenNameJSON).value); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -745,7 +745,7 @@ describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuer const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -795,7 +795,7 @@ describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuer expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("Theodor"); expect(requestItemDVO.response).toStrictEqual(responseItem); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); diff --git a/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts index 204d6bcb0..0c2ce9172 100644 --- a/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts @@ -106,7 +106,7 @@ describe("ReadAttributeRequestItemDVO with IdentityAttributeQuery", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -174,7 +174,7 @@ describe("ReadAttributeRequestItemDVO with IdentityAttributeQuery", () => { const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -236,7 +236,7 @@ describe("ReadAttributeRequestItemDVO with IdentityAttributeQuery", () => { expect(responseItem.attributeId).toStrictEqual(attributeResult.value[0].id); expect(displayName.value).toStrictEqual((responseItem.attribute.content.value as GivenNameJSON).value); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -342,7 +342,7 @@ describe("ReadAttributeRequestItemDVO with IQL and results", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -405,7 +405,7 @@ describe("ReadAttributeRequestItemDVO with IQL and results", () => { const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -462,7 +462,7 @@ describe("ReadAttributeRequestItemDVO with IQL and results", () => { expect(responseItem.attributeId).toStrictEqual(attributeResult.value[0].id); expect(displayName.value).toStrictEqual((responseItem.attribute.content.value as GivenNameJSON).value); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -577,7 +577,7 @@ describe("ReadAttributeRequestItemDVO with IQL and fallback", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); - await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); const dto = senderMessage; const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -636,7 +636,7 @@ describe("ReadAttributeRequestItemDVO with IQL and fallback", () => { const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -693,7 +693,7 @@ describe("ReadAttributeRequestItemDVO with IQL and fallback", () => { expect(responseItem.attributeId).toStrictEqual(attributeResult.value[0].id); expect(displayName.value).toStrictEqual((responseItem.attribute.content.value as SurnameJSON).value); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -829,7 +829,7 @@ describe("AttributeSuccessionAcceptResponseItemDVO with IdentityAttributeQuery", const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -907,7 +907,7 @@ describe("AttributeSuccessionAcceptResponseItemDVO with IdentityAttributeQuery", expect(responseItem.successorId).toStrictEqual(successorResult.value[0].id); expect(successorName.value).toStrictEqual((responseItem.successor.content.value as GivenNameJSON).value); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); @@ -1050,7 +1050,7 @@ describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuer const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await consumptionServices2.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -1100,7 +1100,7 @@ describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuer expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("Theodor"); expect(requestItemDVO.response).toStrictEqual(responseItem); - await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); }); diff --git a/packages/runtime/test/dataViews/requestItems/ShareAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ShareAttributeRequestItemDVO.test.ts index 18e17e74c..b02f69f66 100644 --- a/packages/runtime/test/dataViews/requestItems/ShareAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ShareAttributeRequestItemDVO.test.ts @@ -1,5 +1,6 @@ -import { DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; +import { AttributesController, DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; import { AbstractStringJSON, DisplayNameJSON, ShareAttributeRequestItemJSON } from "@nmshd/content"; +import { CoreId } from "@nmshd/transport"; import { AcceptResponseItemDVO, ConsumptionServices, @@ -56,6 +57,12 @@ beforeAll(async () => { sAddress = (await sTransportServices.account.getIdentityInfo()).value.address; rAddress = (await rTransportServices.account.getIdentityInfo()).value.address; + responseItems = [{ accept: true }]; +}, 30000); + +afterAll(() => serviceProvider.stop()); + +beforeEach(async () => { const senderAttribute = await sConsumptionServices.attributes.createRepositoryAttribute({ content: { value: { @@ -78,20 +85,32 @@ beforeAll(async () => { }, peer: rAddress }; - responseItems = [{ accept: true }]; -}, 30000); -afterAll(() => serviceProvider.stop()); - -beforeEach(function () { rEventBus.reset(); sEventBus.reset(); }); +afterEach(async () => { + await cleanupAttributes(); +}); + +async function cleanupAttributes() { + await Promise.all( + [sRuntimeServices, rRuntimeServices].map(async (services) => { + const servicesAttributeController = (rRuntimeServices.consumption.attributes as any).getAttributeUseCase.attributeController as AttributesController; + + const servicesAttributesResult = await services.consumption.attributes.getAttributes({}); + for (const attribute of servicesAttributesResult.value) { + await servicesAttributeController.deleteAttributeUnsafe(CoreId.from(attribute.id)); + } + }) + ); +} + describe("ShareAttributeRequestItemDVO", () => { test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); - await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id); + await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id!); const dto = senderMessage; const dvo = (await sExpander.expandMessageDTO(senderMessage)) as RequestMessageDVO; expect(dvo).toBeDefined(); @@ -158,7 +177,7 @@ describe("ShareAttributeRequestItemDVO", () => { const recipientMessage = await exchangeMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); await rEventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); const acceptResult = await rConsumptionServices.incomingRequests.accept({ - requestId: recipientMessage.content.id, + requestId: recipientMessage.content.id!, items: responseItems }); expect(acceptResult).toBeSuccessful(); @@ -210,7 +229,7 @@ describe("ShareAttributeRequestItemDVO", () => { expect(attributeResult.value[0].id).toBeDefined(); expect((attributeResult.value[0].content.value as DisplayNameJSON).value).toBe("Dr. Theodor Munchkin von Reichenhardt"); - await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id); + await syncUntilHasMessageWithResponse(sTransportServices, recipientMessage.content.id!); await sEventBus.waitForEvent(OutgoingRequestStatusChangedEvent); }); @@ -301,7 +320,7 @@ describe("ShareAttributeRequestItemDVO", () => { const senderMessage = await exchangeAndAcceptRequestByMessage(sRuntimeServices, rRuntimeServices, requestContent, responseItems); const dvo = await sExpander.expandAddress(senderMessage.recipients[0].address); - expect(dvo.name).toStrictEqual(senderMessage.recipients[0].address.substring(3, 9)); + expect(dvo.name).toBe("i18n://dvo.identity.unknown"); expect(dvo.items).toHaveLength(0); }); }); diff --git a/packages/runtime/test/lib/testUtils.ts b/packages/runtime/test/lib/testUtils.ts index e5f1564a5..12bdb22a5 100644 --- a/packages/runtime/test/lib/testUtils.ts +++ b/packages/runtime/test/lib/testUtils.ts @@ -7,18 +7,22 @@ import { LocalRequestStatus } from "@nmshd/consumption"; import { + ArbitraryRelationshipCreationContent, + ArbitraryRelationshipCreationContentJSON, + ArbitraryRelationshipTemplateContent, + ArbitraryRelationshipTemplateContentJSON, INotificationItem, - IRelationshipCreationChangeRequestContent, - IRelationshipTemplateContent, Notification, - RelationshipCreationChangeRequestContent, - RelationshipCreationChangeRequestContentJSON, - RelationshipTemplateContent, + RelationshipCreationContentJSON, RelationshipTemplateContentJSON, RequestItemGroupJSON, - RequestItemJSONDerivations + RequestItemJSONDerivations, + RequestJSON, + ResponseWrapperJSON, + ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; -import { CoreId } from "@nmshd/transport"; +import { CoreBuffer } from "@nmshd/crypto"; +import { CoreAddress, CoreId, IdentityUtil } from "@nmshd/transport"; import fs from "fs"; import { DateTime } from "luxon"; import { @@ -33,6 +37,7 @@ import { IncomingRequestStatusChangedEvent, LocalAttributeDTO, LocalNotificationDTO, + MessageContentDerivation, MessageDTO, MessageSentEvent, NotifyPeerAboutRepositoryAttributeSuccessionRequest, @@ -107,12 +112,16 @@ export async function syncUntilHasMessage(transportServices: TransportServices, return await syncUntilHas(transportServices, "messages", (m) => m.id === messageId.toString()); } -export async function syncUntilHasMessageWithRequest(transportServices: TransportServices, requestId: string | CoreId): Promise { - return await syncUntilHas(transportServices, "messages", (m) => m.content["@type"] === "Request" && m.content.id === requestId.toString()); +export async function syncUntilHasMessageWithRequest(transportServices: TransportServices, requestId: string | CoreId): Promise { + return (await syncUntilHas(transportServices, "messages", (m) => m.content["@type"] === "Request" && m.content.id === requestId.toString())) as MessageDTO & { + content: RequestJSON; + }; } -export async function syncUntilHasMessageWithResponse(transportServices: TransportServices, requestId: string | CoreId): Promise { - return await syncUntilHas(transportServices, "messages", (m) => m.content["@type"] === "ResponseWrapper" && m.content.requestId === requestId.toString()); +export async function syncUntilHasMessageWithResponse(transportServices: TransportServices, requestId: string | CoreId): Promise { + return (await syncUntilHas(transportServices, "messages", (m) => m.content["@type"] === "ResponseWrapper" && m.content.requestId === requestId.toString())) as MessageDTO & { + content: ResponseWrapperJSON; + }; } export async function syncUntilHasMessageWithNotification(transportServices: TransportServices, notificationId: string | CoreId): Promise { @@ -171,11 +180,15 @@ export async function makeUploadRequest(values: object = {}): Promise { +export const emptyRelationshipTemplateContent: ArbitraryRelationshipTemplateContentJSON = ArbitraryRelationshipTemplateContent.from({ value: {} }).toJSON(); + +export const emptyRelationshipCreationContent: ArbitraryRelationshipCreationContentJSON = ArbitraryRelationshipCreationContent.from({ value: {} }).toJSON(); + +export async function createTemplate(transportServices: TransportServices, body?: RelationshipTemplateContentJSON): Promise { const response = await transportServices.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: body + content: body ?? emptyRelationshipTemplateContent }); expect(response).toBeSuccessful(); @@ -195,7 +208,7 @@ export async function getFileToken(transportServices: TransportServices): Promis export async function exchangeTemplate( transportServicesCreator: TransportServices, transportServicesRecipient: TransportServices, - content: RelationshipTemplateContent | {} = {} + content?: RelationshipTemplateContentJSON ): Promise { const template = await createTemplate(transportServicesCreator, content); @@ -226,7 +239,7 @@ export async function exchangeToken(transportServicesCreator: TransportServices, return response.value; } -export async function sendMessage(transportServices: TransportServices, recipient: string, content?: any, attachments?: string[]): Promise { +export async function sendMessage(transportServices: TransportServices, recipient: string, content?: MessageContentDerivation, attachments?: string[]): Promise { const response = await transportServices.messages.sendMessage({ recipients: [recipient], content: content ?? { @@ -243,7 +256,28 @@ export async function sendMessage(transportServices: TransportServices, recipien return response.value; } -export async function sendMessageWithRequest(sender: TestRuntimeServices, recipient: TestRuntimeServices, request: CreateOutgoingRequestRequest): Promise { +export async function sendMessageToMultipleRecipients(transportServices: TransportServices, recipients: string[], content?: any, attachments?: string[]): Promise { + const response = await transportServices.messages.sendMessage({ + recipients, + content: content ?? { + "@type": "Mail", + subject: "This is the mail subject", + body: "This is the mail body", + cc: [], + to: recipients + }, + attachments + }); + expect(response).toBeSuccessful(); + + return response.value; +} + +export async function sendMessageWithRequest( + sender: TestRuntimeServices, + recipient: TestRuntimeServices, + request: CreateOutgoingRequestRequest +): Promise { const createRequestResult = await sender.consumption.outgoingRequests.create(request); expect(createRequestResult).toBeSuccessful(); const sendMessageResult = await sender.transport.messages.sendMessage({ @@ -252,7 +286,7 @@ export async function sendMessageWithRequest(sender: TestRuntimeServices, recipi }); expect(sendMessageResult).toBeSuccessful(); - return sendMessageResult.value; + return sendMessageResult.value as MessageDTO & { content: RequestJSON }; } export async function exchangeMessage(transportServicesCreator: TransportServices, transportServicesRecipient: TransportServices, attachments?: string[]): Promise { @@ -267,9 +301,13 @@ export async function exchangeMessage(transportServicesCreator: TransportService return message; } -export async function exchangeMessageWithRequest(sender: TestRuntimeServices, recipient: TestRuntimeServices, request: CreateOutgoingRequestRequest): Promise { +export async function exchangeMessageWithRequest( + sender: TestRuntimeServices, + recipient: TestRuntimeServices, + request: CreateOutgoingRequestRequest +): Promise { const sentMessage = await sendMessageWithRequest(sender, recipient, request); - return await syncUntilHasMessageWithRequest(recipient.transport, sentMessage.content.id); + return await syncUntilHasMessageWithRequest(recipient.transport, sentMessage.content.id!); } export async function exchangeMessageWithAttachment(transportServicesCreator: TransportServices, transportServicesRecipient: TransportServices): Promise { @@ -288,21 +326,19 @@ export async function getRelationship(transportServices: TransportServices): Pro } export async function establishRelationship(transportServices1: TransportServices, transportServices2: TransportServices): Promise { - const template = await exchangeTemplate(transportServices1, transportServices2, {}); + const template = await exchangeTemplate(transportServices1, transportServices2); const createRelationshipResponse = await transportServices2.relationships.createRelationship({ templateId: template.id, - content: { a: "b" } + creationContent: emptyRelationshipCreationContent }); expect(createRelationshipResponse).toBeSuccessful(); const relationships = await syncUntilHasRelationships(transportServices1); expect(relationships).toHaveLength(1); - const acceptResponse = await transportServices1.relationships.acceptRelationshipChange({ - relationshipId: relationships[0].id, - changeId: relationships[0].changes[0].id, - content: { a: "b" } + const acceptResponse = await transportServices1.relationships.acceptRelationship({ + relationshipId: relationships[0].id }); expect(acceptResponse).toBeSuccessful(); @@ -314,24 +350,22 @@ export async function establishRelationship(transportServices1: TransportService export async function establishRelationshipWithContents( transportServices1: TransportServices, transportServices2: TransportServices, - templateContent: RelationshipTemplateContentJSON | RelationshipTemplateContent | IRelationshipTemplateContent, - requestContent: RelationshipCreationChangeRequestContentJSON | RelationshipCreationChangeRequestContent | IRelationshipCreationChangeRequestContent + templateContent?: RelationshipTemplateContentJSON, + creationContent?: RelationshipCreationContentJSON ): Promise { const template = await exchangeTemplate(transportServices1, transportServices2, templateContent); const createRelationshipResponse = await transportServices2.relationships.createRelationship({ templateId: template.id, - content: requestContent + creationContent: creationContent ?? emptyRelationshipCreationContent }); expect(createRelationshipResponse).toBeSuccessful(); const relationships = await syncUntilHasRelationships(transportServices1); expect(relationships).toHaveLength(1); - const acceptResponse = await transportServices1.relationships.acceptRelationshipChange({ - relationshipId: relationships[0].id, - changeId: relationships[0].changes[0].id, - content: { a: "b" } + const acceptResponse = await transportServices1.relationships.acceptRelationship({ + relationshipId: relationships[0].id }); expect(acceptResponse).toBeSuccessful(); @@ -368,7 +402,8 @@ export async function establishPendingRelationshipWithRequestFlow( await rRuntimeServices.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.request.source!.reference === template.id); const requestId = (await rRuntimeServices.consumption.incomingRequests.getRequests({ query: { "source.reference": template.id } })).value[0].id; - await rRuntimeServices.consumption.incomingRequests.accept({ requestId, items: acceptParams }); + const result = await rRuntimeServices.consumption.incomingRequests.accept({ requestId, items: acceptParams }); + expect(result).toBeSuccessful(); await rRuntimeServices.eventBus.waitForEvent(RelationshipChangedEvent); @@ -387,13 +422,32 @@ export async function ensureActiveRelationship(sTransportServices: TransportServ await establishRelationship(sTransportServices, rTransportServices); } else if (relationships[0].status === RelationshipStatus.Pending) { const relationship = relationships[0]; - await sTransportServices.relationships.acceptRelationshipChange({ relationshipId: relationship.id, changeId: relationship.changes[0].id, content: {} }); + await sTransportServices.relationships.acceptRelationship({ relationshipId: relationship.id }); await syncUntilHasRelationships(rTransportServices, 1); } return (await sTransportServices.relationships.getRelationships({})).value[0]; } +export async function ensurePendingRelationship(sTransportServices: TransportServices, rTransportServices: TransportServices): Promise { + const rTransportServicesAddress = (await rTransportServices.account.getIdentityInfo()).value.address; + const relationships = (await sTransportServices.relationships.getRelationships({ query: { peer: rTransportServicesAddress } })).value; + if (relationships.length === 0) { + const template = await exchangeTemplate(sTransportServices, rTransportServices); + + const createRelationshipResponse = await rTransportServices.relationships.createRelationship({ + templateId: template.id, + creationContent: emptyRelationshipCreationContent + }); + expect(createRelationshipResponse).toBeSuccessful(); + + const relationships = await syncUntilHasRelationships(sTransportServices); + expect(relationships).toHaveLength(1); + } + + return (await sTransportServices.relationships.getRelationships({})).value[0]; +} + export async function exchangeAndAcceptRequestByMessage( sender: TestRuntimeServices, recipient: TestRuntimeServices, @@ -479,7 +533,7 @@ export async function acceptIncomingShareAttributeRequest(sender: TestRuntimeSer await recipient.consumption.incomingRequests.accept({ requestId: requestId, items: [{ accept: true }] }); const responseMessage = await syncUntilHasMessageWithResponse(sender.transport, requestId); - const sharedAttributeId = responseMessage.content.response.items[0].attributeId; + const sharedAttributeId = (responseMessage.content.response.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; await sender.eventBus.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => { return e.data.request.id === requestId && e.data.newStatus === LocalRequestStatus.Completed; }); @@ -587,7 +641,7 @@ export async function executeFullRequestAndShareThirdPartyRelationshipAttributeF }); const responseMessage = await syncUntilHasMessageWithResponse(peer.transport, localRequest.id); - const sharedAttributeId = responseMessage.content.response.items[0].attributeId; + const sharedAttributeId = (responseMessage.content.response.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; await peer.eventBus.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => { return e.data.request.id === localRequest.id && e.data.newStatus === LocalRequestStatus.Completed; }); @@ -664,3 +718,10 @@ export async function waitForEvent( clearTimeout(timeoutId); }); } + +export async function generateAddressPseudonym(backboneBaseUrl: string): Promise { + const pseudoPublicKey = CoreBuffer.fromUtf8("deleted identity"); + const pseudonym = await IdentityUtil.createAddress({ algorithm: 1, publicKey: pseudoPublicKey }, new URL(backboneBaseUrl).hostname); + + return pseudonym; +} diff --git a/packages/runtime/test/lib/testUtilsWithInactiveModules.ts b/packages/runtime/test/lib/testUtilsWithInactiveModules.ts index 7059b8f0f..73415903a 100644 --- a/packages/runtime/test/lib/testUtilsWithInactiveModules.ts +++ b/packages/runtime/test/lib/testUtilsWithInactiveModules.ts @@ -1,4 +1,4 @@ -import { IResponse, RelationshipCreationChangeRequestContent } from "@nmshd/content"; +import { IResponse, RelationshipCreationContent, RelationshipTemplateContentJSON, ResponseWrapperJSON } from "@nmshd/content"; import { CreateOutgoingRequestRequest, LocalRequestDTO, MessageDTO, RelationshipDTO } from "src"; import { TestRuntimeServices } from "./RuntimeServiceProvider"; import { exchangeMessageWithRequest, exchangeTemplate, syncUntilHasMessageWithResponse } from "./testUtils"; @@ -9,8 +9,8 @@ export interface LocalRequestWithSource { } export interface ResponseMessagesForSenderAndRecipient { - rResponseMessage: MessageDTO; - sResponseMessage: MessageDTO; + rResponseMessage: MessageDTO & { content: ResponseWrapperJSON }; + sResponseMessage: MessageDTO & { content: ResponseWrapperJSON }; } export interface RelationshipRequestWithRelationshipIfAccepted { @@ -24,19 +24,19 @@ export async function exchangeMessageWithRequestAndRequireManualDecision( requestForCreate: CreateOutgoingRequestRequest ): Promise { const message = await exchangeMessageWithRequest(sRuntimeServices, rRuntimeServices, requestForCreate); - await sRuntimeServices.consumption.outgoingRequests.sent({ requestId: message.content.id, messageId: message.id }); + await sRuntimeServices.consumption.outgoingRequests.sent({ requestId: message.content.id!, messageId: message.id }); await rRuntimeServices.consumption.incomingRequests.received({ receivedRequest: message.content, requestSourceId: message.id }); await rRuntimeServices.consumption.incomingRequests.checkPrerequisites({ - requestId: message.content.id + requestId: message.content.id! }); return { request: ( await rRuntimeServices.consumption.incomingRequests.requireManualDecision({ - requestId: message.content.id + requestId: message.content.id! }) ).value, source: message.id @@ -70,7 +70,7 @@ export async function exchangeMessageWithRequestAndSendResponse( }, recipients: [(await sRuntimeServices.transport.account.getIdentityInfo()).value.address] }) - ).value; + ).value as MessageDTO & { content: ResponseWrapperJSON }; const sResponseMessage = await syncUntilHasMessageWithResponse(sRuntimeServices.transport, request.id); return { rResponseMessage, sResponseMessage }; } @@ -84,7 +84,7 @@ export async function exchangeTemplateAndReceiverRequiresManualDecision( const request = ( await rRuntimeServices.consumption.incomingRequests.received({ - receivedRequest: template.content.onNewRelationship, + receivedRequest: (template.content as RelationshipTemplateContentJSON).onNewRelationship, requestSourceId: template.id }) ).value; @@ -119,18 +119,17 @@ export async function exchangeTemplateAndReceiverSendsResponse( }) ).value; - let relationship; - if (actionLowerCase === "accept") { - const content = RelationshipCreationChangeRequestContent.from({ response: decidedRequest.response!.content as unknown as IResponse }); - const result = await rRuntimeServices.transport.relationships.createRelationship({ content, templateId }); + if (actionLowerCase !== "accept") return { request, relationship: undefined }; + + const creationContent = RelationshipCreationContent.from({ response: decidedRequest.response!.content as unknown as IResponse }).toJSON(); + const result = await rRuntimeServices.transport.relationships.createRelationship({ creationContent, templateId }); - expect(result).toBeSuccessful(); + expect(result).toBeSuccessful(); - relationship = result.value; + const relationship = result.value; + const receivedCreationContent = relationship.creationContent; - const rRelationshipChange = result.value.changes[0]; + expect(receivedCreationContent["@type"]).toBe("RelationshipCreationContent"); - expect(rRelationshipChange.request.content["@type"]).toBe("RelationshipCreationChangeRequestContent"); - } return { request, relationship }; } diff --git a/packages/runtime/test/modules/AttributeListenerModule.test.ts b/packages/runtime/test/modules/AttributeListenerModule.test.ts index 759bb4f33..fc5f47091 100644 --- a/packages/runtime/test/modules/AttributeListenerModule.test.ts +++ b/packages/runtime/test/modules/AttributeListenerModule.test.ts @@ -36,8 +36,8 @@ beforeAll(async () => { "@type": "RegisterAttributeListenerRequestItem", query: { "@type": "ThirdPartyRelationshipAttributeQuery", - key: "aKey", - owner: thirdParty.address, + key: "AKey", + owner: "thirdParty", thirdParty: [thirdParty.address] }, mustBeAccepted: true @@ -73,13 +73,13 @@ describe("AttributeListenerModule", () => { const attributeContent: RelationshipAttributeJSON = { "@type": "RelationshipAttribute", owner: thirdParty.address, - key: "aKey", + key: "AKey", confidentiality: RelationshipAttributeConfidentiality.Public, isTechnical: false, value: { "@type": "ProprietaryString", - title: "aTitle", - value: "aProprietaryStringValue" + title: "ATitle", + value: "AProprietaryStringValue" } }; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 3fcdc29fb..c4a67a547 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,5 +1,5 @@ import { LocalRequestStatus } from "@nmshd/consumption"; -import { Request } from "@nmshd/content"; +import { RelationshipTemplateContent, Request } from "@nmshd/content"; import { CoreDate } from "@nmshd/transport"; import { ConsumptionServices, @@ -10,7 +10,7 @@ import { RelationshipTemplateProcessedResult, TransportServices } from "../../src"; -import { establishRelationship, exchangeMessage, MockEventBus, RuntimeServiceProvider, TestRequestItem } from "../lib"; +import { MockEventBus, RuntimeServiceProvider, TestRequestItem, establishRelationship, exchangeMessage } from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); let sTransportServices: TransportServices; @@ -72,7 +72,9 @@ describe("DeciderModule", () => { const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); const template = ( await sTransportServices.relationshipTemplates.createOwnRelationshipTemplate({ - content: request, + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() }) ).value; @@ -100,7 +102,9 @@ describe("DeciderModule", () => { const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); const template = ( await sTransportServices.relationshipTemplates.createOwnRelationshipTemplate({ - content: request, + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() }) ).value; diff --git a/packages/runtime/test/modules/RequestModule.test.ts b/packages/runtime/test/modules/RequestModule.test.ts index 7bd19788a..c6ba1324e 100644 --- a/packages/runtime/test/modules/RequestModule.test.ts +++ b/packages/runtime/test/modules/RequestModule.test.ts @@ -4,11 +4,12 @@ import { IdentityAttribute, RelationshipAttribute, RelationshipAttributeConfidentiality, - RelationshipCreationChangeRequestContentJSON, - RelationshipTemplateContentJSON, + RelationshipCreationContentJSON, + RelationshipTemplateContent, ResponseItemJSON, ResponseItemResult, - ResponseResult + ResponseResult, + ResponseWrapperJSON } from "@nmshd/content"; import { CoreAddress } from "@nmshd/transport"; import { @@ -20,7 +21,7 @@ import { MessageProcessedEvent, MessageSentEvent, OutgoingRequestCreatedAndCompletedEvent, - OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent, + OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent, OutgoingRequestStatusChangedEvent, RelationshipDTO, RelationshipStatus, @@ -29,20 +30,20 @@ import { RelationshipTemplateProcessedResult } from "../../src"; import { + MockEventBus, + RuntimeServiceProvider, + TestRuntimeServices, ensureActiveRelationship, establishPendingRelationshipWithRequestFlow, exchangeAndAcceptRequestByMessage, exchangeMessageWithRequest, exchangeTemplate, executeFullCreateAndShareRelationshipAttributeFlow, - MockEventBus, - RuntimeServiceProvider, sendMessage, sendMessageWithRequest, - syncUntilHasMessages, syncUntilHasMessageWithResponse, - syncUntilHasRelationships, - TestRuntimeServices + syncUntilHasMessages, + syncUntilHasRelationships } from "../lib"; describe("RequestModule", () => { @@ -68,11 +69,10 @@ describe("RequestModule", () => { let template: RelationshipTemplateDTO; const metadata = { aMetadataKey: "aMetadataValue" }; - const templateContent: RelationshipTemplateContentJSON = { - "@type": "RelationshipTemplateContent", + const templateContent = RelationshipTemplateContent.from({ onNewRelationship: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, metadata - }; + }).toJSON(); async function getRequestIdOfTemplate(eventBus: MockEventBus, templateId: string) { let requests: LocalRequestDTO[]; @@ -90,10 +90,8 @@ describe("RequestModule", () => { const requestId = (await rRuntimeServices.consumption.incomingRequests.getRequests({ query: { "source.reference": template.id } })).value[0].id; await rRuntimeServices.consumption.incomingRequests.accept({ requestId, items: [{ accept: true }] }); const relationship = (await syncUntilHasRelationships(sRuntimeServices.transport, 1))[0]; - await sRuntimeServices.transport.relationships.acceptRelationshipChange({ - relationshipId: relationship.id, - changeId: relationship.changes[0].id, - content: {} + await sRuntimeServices.transport.relationships.acceptRelationship({ + relationshipId: relationship.id }); } } @@ -139,11 +137,10 @@ describe("RequestModule", () => { test("triggers RelationshipTemplateProcessedEvent when another Template is loaded and a pending Relationship exists", async () => { const requestId = await getRequestIdOfTemplate(rRuntimeServices.eventBus, template.id); await rRuntimeServices.consumption.incomingRequests.accept({ requestId, items: [{ accept: true }] }); - const templateContent: RelationshipTemplateContentJSON = { - "@type": "RelationshipTemplateContent", + const templateContent = RelationshipTemplateContent.from({ onNewRelationship: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, metadata - }; + }).toJSON(); await exchangeTemplate(sRuntimeServices.transport, rRuntimeServices.transport, templateContent); @@ -154,7 +151,7 @@ describe("RequestModule", () => { }); test("triggers RelationshipTemplateProcessedEvent if there is no request in the template", async () => { - await exchangeTemplate(sRuntimeServices.transport, rRuntimeServices.transport, {}); + await exchangeTemplate(sRuntimeServices.transport, rRuntimeServices.transport); await expect(rRuntimeServices.eventBus).toHavePublished(RelationshipTemplateProcessedEvent, (e) => e.data.result === RelationshipTemplateProcessedResult.NoRequest); }); @@ -183,10 +180,10 @@ describe("RequestModule", () => { const relationship = relationships[0]; - const creationChangeRequestContent = relationship.changes[0].request.content as RelationshipCreationChangeRequestContentJSON; - expect(creationChangeRequestContent["@type"]).toBe("RelationshipCreationChangeRequestContent"); + const creationContent = relationship.creationContent as RelationshipCreationContentJSON; + expect(creationContent["@type"]).toBe("RelationshipCreationContent"); - const response = creationChangeRequestContent.response; + const response = creationContent.response; const responseItems = response.items; expect(responseItems).toHaveLength(1); @@ -194,12 +191,12 @@ describe("RequestModule", () => { expect(responseItem["@type"]).toBe("AcceptResponseItem"); expect(responseItem.result).toBe(ResponseItemResult.Accepted); - await expect(sRuntimeServices.eventBus).toHavePublished(OutgoingRequestFromRelationshipCreationChangeCreatedAndCompletedEvent, (e) => e.data.id === response.requestId); + await expect(sRuntimeServices.eventBus).toHavePublished(OutgoingRequestFromRelationshipCreationCreatedAndCompletedEvent, (e) => e.data.id === response.requestId); const requestsResult = await sRuntimeServices.consumption.outgoingRequests.getRequest({ id: response.requestId }); expect(requestsResult).toBeSuccessful(); - await sRuntimeServices.transport.relationships.acceptRelationshipChange({ relationshipId: relationship.id, changeId: relationship.changes[0].id, content: {} }); + await sRuntimeServices.transport.relationships.acceptRelationship({ relationshipId: relationship.id }); await syncUntilHasRelationships(rRuntimeServices.transport, 1); }); @@ -275,7 +272,7 @@ describe("RequestModule", () => { await expect(rRuntimeServices.eventBus).toHavePublished( MessageSentEvent, - (e) => e.data.content?.response?.requestId === requestAfterReject.response!.content.requestId + (e) => (e.data.content as ResponseWrapperJSON).response.requestId === requestAfterReject.response!.content.requestId ); }); @@ -308,7 +305,7 @@ describe("RequestModule", () => { await expect(rRuntimeServices.eventBus).toHavePublished( MessageSentEvent, - (e) => e.data.content?.response?.requestId === requestAfterReject.response!.content.requestId + (e) => (e.data.content as ResponseWrapperJSON).response.requestId === requestAfterReject.response!.content.requestId ); }); @@ -329,8 +326,7 @@ describe("RequestModule", () => { }); async function exchangeRelationshipTemplate() { - const templateContent: RelationshipTemplateContentJSON = { - "@type": "RelationshipTemplateContent", + const templateContent = RelationshipTemplateContent.from({ onNewRelationship: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] @@ -342,13 +338,13 @@ describe("RequestModule", () => { "@type": "CreateAttributeRequestItem", mustBeAccepted: false, attribute: IdentityAttribute.from({ - owner: (await rRuntimeServices.transport.account.getIdentityInfo()).value.address, + owner: CoreAddress.from(""), value: GivenName.from("AGivenName").toJSON() }).toJSON() } ] } - }; + }).toJSON(); const template = await exchangeTemplate(sRuntimeServices.transport, rRuntimeServices.transport, templateContent); @@ -375,7 +371,7 @@ describe("RequestModule", () => { await sRuntimeServices.eventBus.waitForEvent(OutgoingRequestStatusChangedEvent, (event) => event.data.newStatus === LocalRequestStatus.Open); - const requestAfterAction = (await sRuntimeServices.consumption.outgoingRequests.getRequest({ id: message.content.id })).value; + const requestAfterAction = (await sRuntimeServices.consumption.outgoingRequests.getRequest({ id: message.content.id! })).value; expect(requestAfterAction.status).toBe(LocalRequestStatus.Open); }); @@ -402,12 +398,12 @@ describe("RequestModule", () => { expect(incomingRequestStatusChangedEvent.data.newStatus).toBe(LocalRequestStatus.DecisionRequired); - const requestsResult = await rRuntimeServices.consumption.incomingRequests.getRequest({ id: message.content.id }); + const requestsResult = await rRuntimeServices.consumption.incomingRequests.getRequest({ id: message.content.id! }); expect(requestsResult).toBeSuccessful(); }); test("triggers a MessageProcessedEvent when the incoming Message does not contain a Request", async () => { - await sendMessage(sRuntimeServices.transport, recipientAddress, {}); + await sendMessage(sRuntimeServices.transport, recipientAddress); await syncUntilHasMessages(rRuntimeServices.transport, 1); @@ -417,7 +413,7 @@ describe("RequestModule", () => { test("sends a message when the request is accepted", async () => { const message = await exchangeMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); await rRuntimeServices.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); - const acceptRequestResult = await rRuntimeServices.consumption.incomingRequests.accept({ requestId: message.content.id, items: [{ accept: true }] }); + const acceptRequestResult = await rRuntimeServices.consumption.incomingRequests.accept({ requestId: message.content.id!, items: [{ accept: true }] }); expect(acceptRequestResult).toBeSuccessful(); const incomingRequestStatusChangedEvent = await rRuntimeServices.eventBus.waitForEvent( @@ -482,11 +478,7 @@ describe("Handling the rejection and the revocation of a Relationship by the Req expect((await sRuntimeServices.consumption.attributes.getAttributes({})).value).toHaveLength(4); expect((await rRuntimeServices.consumption.attributes.getAttributes({})).value).toHaveLength(4); - await sRuntimeServices.transport.relationships.rejectRelationshipChange({ - relationshipId: sRelationship.id, - changeId: sRelationship.changes[0].id, - content: {} - }); + await sRuntimeServices.transport.relationships.rejectRelationship({ relationshipId: sRelationship.id }); await sRuntimeServices.eventBus.waitForRunningEventHandlers(); await syncUntilHasRelationships(rRuntimeServices.transport, 1); await rRuntimeServices.eventBus.waitForRunningEventHandlers(); @@ -500,11 +492,7 @@ describe("Handling the rejection and the revocation of a Relationship by the Req expect((await sRuntimeServices.consumption.attributes.getAttributes({})).value).toHaveLength(4); expect((await rRuntimeServices.consumption.attributes.getAttributes({})).value).toHaveLength(4); - await rRuntimeServices.transport.relationships.revokeRelationshipChange({ - relationshipId: sRelationship.id, - changeId: sRelationship.changes[0].id, - content: {} - }); + await rRuntimeServices.transport.relationships.revokeRelationship({ relationshipId: sRelationship.id }); await rRuntimeServices.eventBus.waitForRunningEventHandlers(); await syncUntilHasRelationships(sRuntimeServices.transport, 1); await sRuntimeServices.eventBus.waitForRunningEventHandlers(); @@ -530,7 +518,7 @@ describe("Handling the rejection and the revocation of a Relationship by the Req "@type": "GivenName", value: "AGivenName" }, - owner: (await rRuntimeServices.transport.account.getIdentityInfo()).value.address + owner: "" }).toJSON() }, { @@ -543,7 +531,7 @@ describe("Handling the rejection and the revocation of a Relationship by the Req value: "AStringValue", title: "ATitle" }, - owner: CoreAddress.from((await rRuntimeServices.transport.account.getIdentityInfo()).value.address), + owner: CoreAddress.from(""), confidentiality: RelationshipAttributeConfidentiality.Public }).toJSON() }, diff --git a/packages/runtime/test/transport/account.test.ts b/packages/runtime/test/transport/account.test.ts index 0fe2d41da..49fe99aff 100644 --- a/packages/runtime/test/transport/account.test.ts +++ b/packages/runtime/test/transport/account.test.ts @@ -1,7 +1,7 @@ import { CoreDate } from "@nmshd/transport"; import { DateTime } from "luxon"; import { DeviceDTO, DeviceOnboardingInfoDTO, TransportServices } from "../../src"; -import { RuntimeServiceProvider, uploadFile } from "../lib"; +import { emptyRelationshipTemplateContent, RuntimeServiceProvider, uploadFile } from "../lib"; const serviceProvider = new RuntimeServiceProvider(); let sTransportServices: TransportServices; @@ -72,9 +72,7 @@ describe("IdentityInfo", () => { expect(identityInfoResult).toBeSuccessful(); const identityInfo = identityInfoResult.value; - expect(identityInfo.address.length).toBeLessThanOrEqual(36); - expect(identityInfo.address.length).toBeGreaterThanOrEqual(35); - expect(identityInfo.address).toMatch(/^id1/); + expect(identityInfo.address).toMatch(/^did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}$/); expect(identityInfo.publicKey).toHaveLength(82); }); }); @@ -110,7 +108,7 @@ describe("LoadItemFromTruncatedReference", () => { beforeAll(async () => { const relationshipTemplate = ( await sTransportServices.relationshipTemplates.createOwnRelationshipTemplate({ - content: {}, + content: emptyRelationshipTemplateContent, expiresAt: CoreDate.utc().add({ days: 1 }).toISOString() }) ).value; diff --git a/packages/runtime/test/transport/challenges.test.ts b/packages/runtime/test/transport/challenges.test.ts index 8f2eea941..c1a84f31b 100644 --- a/packages/runtime/test/transport/challenges.test.ts +++ b/packages/runtime/test/transport/challenges.test.ts @@ -138,7 +138,7 @@ describe("Validate Challenge", () => { }); test("should return an error when the signature is invalid", async () => { - const validChallenge = { createdBy: "id1...", createdByDevice: "DVC...", expiresAt: "2022", id: "CHL...", type: "Identity" }; + const validChallenge = { createdBy: "did:e:a-domain:dids:anidentity", createdByDevice: "DVC...", expiresAt: "2022", id: "CHL...", type: "Identity" }; const validationResult = await transportServices2.challenges.validateChallenge({ challengeString: JSON.stringify(validChallenge), signature: "invalid-signature" @@ -147,7 +147,7 @@ describe("Validate Challenge", () => { }); test("should return an error when the challenge is an invalid json string", async () => { - const validChallenge = { createdBy: "id1...", createdByDevice: "DVC...", expiresAt: "2022", id: "CHL...", type: "Identity" }; + const validChallenge = { createdBy: "did:e:a-domain:dids:anidentity", createdByDevice: "DVC...", expiresAt: "2022", id: "CHL...", type: "Identity" }; const validationResult = await transportServices2.challenges.validateChallenge({ challengeString: `${JSON.stringify(validChallenge)}a`, signature: randomValidSignature diff --git a/packages/runtime/test/transport/devices.test.ts b/packages/runtime/test/transport/devices.test.ts index 78027a7e8..14f7d6371 100644 --- a/packages/runtime/test/transport/devices.test.ts +++ b/packages/runtime/test/transport/devices.test.ts @@ -1,3 +1,4 @@ +import { LanguageISO639 } from "@nmshd/content"; import { DeviceMapper, TransportServices } from "../../src"; import { RuntimeServiceProvider } from "../lib"; @@ -19,4 +20,14 @@ describe("Devices", () => { const sharedSecret = DeviceMapper.toDeviceSharedSecret(onboardingInfo); expect(sharedSecret).toBeDefined(); }); + + test("should set the communication language", async () => { + const result = await transportServices1.devices.setCommunicationLanguage({ communicationLanguage: LanguageISO639.fr }); + expect(result).toBeSuccessful(); + }); + + test("should not set the communication language with an invalid language", async () => { + const result = await transportServices1.devices.setCommunicationLanguage({ communicationLanguage: "fra" as any as LanguageISO639 }); + expect(result).toBeAnError("communicationLanguage must be equal to one of the allowed values", "error.runtime.validation.invalidPropertyValue"); + }); }); diff --git a/packages/runtime/test/transport/files.test.ts b/packages/runtime/test/transport/files.test.ts index aa4742a8e..c306600b1 100644 --- a/packages/runtime/test/transport/files.test.ts +++ b/packages/runtime/test/transport/files.test.ts @@ -82,6 +82,7 @@ describe("File upload", () => { expect(response).toBeAnError("content must be object", "error.runtime.validation.invalidPropertyValue"); }); + test("can upload same file twice", async () => { const request = await makeUploadRequest({ content: await fs.promises.readFile(`${__dirname}/../__assets__/test.txt`) }); @@ -121,6 +122,7 @@ describe("Get file", () => { beforeAll(async () => { file = await uploadFile(transportServices1); }); + test("can get file by id", async () => { const response = await transportServices1.files.getFile({ id: file.id }); diff --git a/packages/runtime/test/transport/identityDeletionProcess.test.ts b/packages/runtime/test/transport/identityDeletionProcess.test.ts index cd5cb5249..446bdf1b1 100644 --- a/packages/runtime/test/transport/identityDeletionProcess.test.ts +++ b/packages/runtime/test/transport/identityDeletionProcess.test.ts @@ -88,6 +88,7 @@ describe("IdentityDeletionProcess", () => { "error.runtime.identityDeletionProcess.activeIdentityDeletionProcessAlreadyExists" ); }); + test("should return an error trying to initiate an IdentityDeletionProcess if there already is one waiting for approval", async function () { await startIdentityDeletionProcessFromBackboneAdminApi(transportService, accountAddress); await transportService.identityDeletionProcesses.initiateIdentityDeletionProcess(); diff --git a/packages/runtime/test/transport/messages.test.ts b/packages/runtime/test/transport/messages.test.ts index 14a6dc6c4..17a77d5b8 100644 --- a/packages/runtime/test/transport/messages.test.ts +++ b/packages/runtime/test/transport/messages.test.ts @@ -1,16 +1,16 @@ import { ConsentRequestItemJSON } from "@nmshd/content"; -import { CoreDate, CoreId } from "@nmshd/transport"; +import { CoreDate } from "@nmshd/transport"; import { GetMessagesQuery, MessageReceivedEvent, MessageSentEvent, MessageWasReadAtChangedEvent } from "../../src"; import { + QueryParamConditions, + RuntimeServiceProvider, + TestRuntimeServices, ensureActiveRelationship, establishRelationship, exchangeMessage, exchangeMessageWithAttachment, - QueryParamConditions, - RuntimeServiceProvider, sendMessage, syncUntilHasMessage, - TestRuntimeServices, uploadFile } from "../lib"; @@ -139,6 +139,7 @@ describe("Message errors", () => { }); requestId = createRequestResult.value.id; }); + test("should throw correct error for empty 'to' in the Message", async () => { const result = await client1.transport.messages.sendMessage({ recipients: [client2.address], @@ -164,6 +165,14 @@ describe("Message errors", () => { expect(result).toBeAnError("Mail.to :: Value is not defined", "error.runtime.requestDeserialization"); }); + test("should throw correct error for false content type", async () => { + const result = await client1.transport.messages.sendMessage({ + recipients: [client2.address], + content: {} + }); + expect(result).toBeAnError("The content of a Message", "error.runtime.validation.invalidPropertyValue"); + }); + test("should throw correct error for missing Request ID in a Message with Request content", async () => { const result = await client1.transport.messages.sendMessage({ recipients: [client2.address], @@ -180,7 +189,7 @@ describe("Message errors", () => { recipients: [client2.address], content: { "@type": "Request", - id: CoreId.from("REQxxxxxxxxxxxxxxxxx"), + id: "REQxxxxxxxxxxxxxxxxx", items: [requestItem] } }); @@ -298,7 +307,7 @@ describe("Message query", () => { .addStringSet("recipients.relationshipId") .addSingleCondition({ key: "participant", - value: [updatedMessage.createdBy, "id111111111111111111111111111111111"], + value: [updatedMessage.createdBy, "did:e:a-domain:dids:0000000000000000000000"], expectedResult: true }); @@ -319,14 +328,8 @@ describe("Message query", () => { const relationshipToRecipient1 = await client1.transport.relationships.getRelationshipByAddress({ address: addressRecipient1 }); const relationshipToRecipient2 = await client1.transport.relationships.getRelationshipByAddress({ address: addressRecipient2 }); - await client1.transport.messages.sendMessage({ - content: {}, - recipients: [addressRecipient1] - }); - await client1.transport.messages.sendMessage({ - content: {}, - recipients: [addressRecipient2] - }); + await sendMessage(client1.transport, addressRecipient1); + await sendMessage(client1.transport, addressRecipient2); const messagesToRecipient1 = await client1.transport.messages.getMessages({ query: { "recipients.relationshipId": relationshipToRecipient1.value.id } }); const messagesToRecipient2 = await client1.transport.messages.getMessages({ query: { "recipients.relationshipId": relationshipToRecipient2.value.id } }); diff --git a/packages/runtime/test/transport/relationshipTemplates.test.ts b/packages/runtime/test/transport/relationshipTemplates.test.ts index d3a33baae..845181d98 100644 --- a/packages/runtime/test/transport/relationshipTemplates.test.ts +++ b/packages/runtime/test/transport/relationshipTemplates.test.ts @@ -1,6 +1,6 @@ import { DateTime } from "luxon"; import { GetRelationshipTemplatesQuery, OwnerRestriction, TransportServices } from "../../src"; -import { QueryParamConditions, RuntimeServiceProvider } from "../lib"; +import { emptyRelationshipTemplateContent, QueryParamConditions, RuntimeServiceProvider } from "../lib"; const serviceProvider = new RuntimeServiceProvider(); let transportServices1: TransportServices; @@ -18,7 +18,7 @@ describe("Template Tests", () => { const response = await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: { a: "b" } + content: emptyRelationshipTemplateContent }); expect(response).toBeSuccessful(); @@ -26,7 +26,7 @@ describe("Template Tests", () => { test("create a template with undefined expiresAt", async () => { const response = await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ - content: { a: "A" }, + content: emptyRelationshipTemplateContent, expiresAt: undefined as unknown as string }); @@ -35,7 +35,7 @@ describe("Template Tests", () => { test("create a template with undefined maxNumberOfAllocations", async () => { const response = await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ - content: { a: "A" }, + content: emptyRelationshipTemplateContent, expiresAt: DateTime.utc().plus({ minutes: 1 }).toString() }); @@ -48,7 +48,7 @@ describe("Template Tests", () => { test("read a template with undefined maxNumberOfAllocations", async () => { const templateWithUndefinedMaxNumberOfAllocations = ( await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ - content: { a: "A" }, + content: emptyRelationshipTemplateContent, expiresAt: DateTime.utc().plus({ minutes: 1 }).toString() }) ).value; @@ -66,7 +66,7 @@ describe("Template Tests", () => { await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: { a: "b" } + content: emptyRelationshipTemplateContent }) ).value; @@ -82,7 +82,7 @@ describe("Template Tests", () => { await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: { a: "b" } + content: emptyRelationshipTemplateContent }) ).value; @@ -92,7 +92,7 @@ describe("Template Tests", () => { test("expect a validation error for sending maxNumberOfAllocations 0", async () => { const response = await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ - content: { a: "A" }, + content: emptyRelationshipTemplateContent, expiresAt: DateTime.utc().plus({ minutes: 1 }).toString(), maxNumberOfAllocations: 0 }); @@ -100,6 +100,16 @@ describe("Template Tests", () => { expect(response.isError).toBeTruthy(); expect(response.error.code).toBe("error.runtime.validation.invalidPropertyValue"); }); + + test("expect a validation error for sending a false template content type", async () => { + const response = await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ + content: {}, + expiresAt: DateTime.utc().plus({ minutes: 1 }).toString(), + maxNumberOfAllocations: 1 + }); + + expect(response).toBeAnError("The content of a RelationshipTemplate", "error.runtime.validation.invalidPropertyValue"); + }); }); describe("Serialization Errors", () => { @@ -129,7 +139,7 @@ describe("RelationshipTemplates query", () => { await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: {} + content: emptyRelationshipTemplateContent }) ).value; const conditions = new QueryParamConditions(template, transportServices1) @@ -148,7 +158,7 @@ describe("RelationshipTemplates query", () => { await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: {} + content: emptyRelationshipTemplateContent }) ).value; const conditions = new QueryParamConditions(template, transportServices1) @@ -165,7 +175,7 @@ describe("RelationshipTemplates query", () => { await transportServices1.relationshipTemplates.createOwnRelationshipTemplate({ maxNumberOfAllocations: 1, expiresAt: DateTime.utc().plus({ minutes: 10 }).toString(), - content: {} + content: emptyRelationshipTemplateContent }) ).value; const peerTemplate = (await transportServices2.relationshipTemplates.loadPeerRelationshipTemplate({ reference: createdTemplate.truncatedReference })).value; diff --git a/packages/runtime/test/transport/relationships.test.ts b/packages/runtime/test/transport/relationships.test.ts index 3bf185db5..4780d397c 100644 --- a/packages/runtime/test/transport/relationships.test.ts +++ b/packages/runtime/test/transport/relationships.test.ts @@ -1,18 +1,38 @@ +import { ApplicationError, Result } from "@js-soft/ts-utils"; import { RelationshipAttributeConfidentiality } from "@nmshd/content"; -import { GetRelationshipsQuery, LocalAttributeDTO, OwnSharedAttributeSucceededEvent, PeerSharedAttributeSucceededEvent } from "../../src"; import { + GetRelationshipsQuery, + IncomingRequestReceivedEvent, + LocalAttributeDTO, + OwnSharedAttributeSucceededEvent, + PeerSharedAttributeSucceededEvent, + RelationshipAuditLogEntryReason, + RelationshipChangedEvent, + RelationshipDTO, + RelationshipDecomposedBySelfEvent, + RelationshipReactivationCompletedEvent, + RelationshipReactivationRequestedEvent, + RelationshipStatus +} from "../../src"; +import { + QueryParamConditions, + RuntimeServiceProvider, + TestRuntimeServices, createTemplate, + emptyRelationshipCreationContent, ensureActiveRelationship, + establishRelationship, + exchangeMessageWithRequest, exchangeTemplate, executeFullCreateAndShareRelationshipAttributeFlow, executeFullCreateAndShareRepositoryAttributeFlow, executeFullSucceedRepositoryAttributeAndNotifyPeerFlow, + generateAddressPseudonym, getRelationship, - QueryParamConditions, - RuntimeServiceProvider, + sendAndReceiveNotification, + sendMessageToMultipleRecipients, syncUntilHasMessageWithNotification, - syncUntilHasRelationships, - TestRuntimeServices + syncUntilHasRelationships } from "../lib"; const serviceProvider = new RuntimeServiceProvider(); @@ -28,6 +48,8 @@ beforeAll(async () => { afterAll(() => serviceProvider.stop()); describe("Create Relationship", () => { + let relationshipId: string; + test("load relationship Template in connector 2", async () => { const template = await createTemplate(services1.transport); @@ -35,73 +57,149 @@ describe("Create Relationship", () => { expect(response).toBeSuccessful(); }); - test("execute relationship creation flow", async () => { + test("should not create a relationship with a false creation content type", async () => { const templateId = (await exchangeTemplate(services1.transport, services2.transport)).id; const createRelationshipResponse = await services2.transport.relationships.createRelationship({ templateId: templateId, - content: { a: "b" } + creationContent: {} + }); + expect(createRelationshipResponse).toBeAnError("The creation content of a Relationship", "error.runtime.validation.invalidPropertyValue"); + }); + + test("create pending relationship", async () => { + const templateId = (await exchangeTemplate(services1.transport, services2.transport)).id; + + const createRelationshipResponse = await services2.transport.relationships.createRelationship({ + templateId: templateId, + creationContent: emptyRelationshipCreationContent }); expect(createRelationshipResponse).toBeSuccessful(); const relationships1 = await syncUntilHasRelationships(services1.transport); expect(relationships1).toHaveLength(1); - const relationshipId = relationships1[0].id; - const relationshipChangeId = relationships1[0].changes[0].id; + relationshipId = relationships1[0].id; + }); - const acceptRelationshipResponse = await services1.transport.relationships.acceptRelationshipChange({ - relationshipId: relationshipId, - changeId: relationshipChangeId, - content: { a: "b" } + describe("tests on pending relationship", () => { + test("should not accept relationship sent by yourself", async () => { + expect(await services2.transport.relationships.acceptRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.operationOnlyAllowedForPeer"); }); - expect(acceptRelationshipResponse).toBeSuccessful(); - const relationships1Response = await services1.transport.relationships.getRelationships({}); - expect(relationships1Response).toBeSuccessful(); - expect(relationships1Response.value).toHaveLength(1); + test("should not reject relationship sent by yourself", async () => { + expect(await services2.transport.relationships.rejectRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("should not revoke relationship sent by yourself", async () => { + expect(await services1.transport.relationships.revokeRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("execute further relationship creation flow", async () => { + const acceptRelationshipResponse = await services1.transport.relationships.acceptRelationship({ + relationshipId + }); + expect(acceptRelationshipResponse).toBeSuccessful(); + + const relationships1Response = await services1.transport.relationships.getRelationships({}); + expect(relationships1Response).toBeSuccessful(); + expect(relationships1Response.value).toHaveLength(1); - const relationships2 = await syncUntilHasRelationships(services2.transport); - expect(relationships2).toHaveLength(1); + const relationships2 = await syncUntilHasRelationships(services2.transport); + expect(relationships2).toHaveLength(1); - const relationships2Response = await services2.transport.relationships.getRelationships({}); - expect(relationships2Response).toBeSuccessful(); - expect(relationships2Response.value).toHaveLength(1); + const relationships2Response = await services2.transport.relationships.getRelationships({}); + expect(relationships2Response).toBeSuccessful(); + expect(relationships2Response.value).toHaveLength(1); - const relationship1Response = await services1.transport.relationships.getRelationship({ id: relationshipId }); - expect(relationship1Response).toBeSuccessful(); - expect(relationship1Response.value.status).toBe("Active"); + const relationship1Response = await services1.transport.relationships.getRelationship({ id: relationshipId }); + expect(relationship1Response).toBeSuccessful(); + expect(relationship1Response.value.status).toBe("Active"); - const relationship2Response = await services2.transport.relationships.getRelationship({ id: relationshipId }); - expect(relationship2Response).toBeSuccessful(); - expect(relationship2Response.value.status).toBe("Active"); + const relationship2Response = await services2.transport.relationships.getRelationship({ id: relationshipId }); + expect(relationship2Response).toBeSuccessful(); + expect(relationship2Response.value.status).toBe("Active"); + }); }); +}); - describe("Templator with active IdentityDeletionProcess", () => { - const serviceProvider = new RuntimeServiceProvider(); - let services1: TestRuntimeServices; - let services2: TestRuntimeServices; +describe("Relationship status validations on active relationship", () => { + let relationshipId: string; + beforeAll(async () => { + await ensureActiveRelationship(services1.transport, services2.transport); + const relationship = await getRelationship(services1.transport); + relationshipId = relationship.id; + }); - beforeAll(async () => { - const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true, enableDeciderModule: true, enableNotificationModule: true }); - services1 = runtimeServices[0]; - services2 = runtimeServices[1]; - }, 30000); + test("should not request a relationship reactivation", async () => { + expect(await services1.transport.relationships.requestRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.wrongRelationshipStatus" + ); + }); - afterAll(() => serviceProvider.stop()); + test("should not accept a relationship reactivation", async () => { + expect(await services1.transport.relationships.acceptRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.wrongRelationshipStatus" + ); + }); - test("returns error if templator has active IdentityDeletionProcess", async () => { - const templateId = (await exchangeTemplate(services1.transport, services2.transport)).id; - await services1.transport.identityDeletionProcesses.initiateIdentityDeletionProcess(); + test("should not reject a relationship reactivation", async () => { + expect(await services1.transport.relationships.rejectRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.wrongRelationshipStatus" + ); + }); - const createRelationshipResponse = await services2.transport.relationships.createRelationship({ - templateId: templateId, - content: { a: "b" } - }); - expect(createRelationshipResponse).toBeAnError( - "The Identity who created the RelationshipTemplate is currently in the process of deleting itself. Thus, it is not possible to establish a Relationship to it.", - "error.transport.relationships.activeIdentityDeletionProcessOfOwnerOfRelationshipTemplate" - ); + test("should not revoke a relationship reactivation", async () => { + expect(await services1.transport.relationships.revokeRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.wrongRelationshipStatus" + ); + }); + + test("should not accept a relationship", async () => { + expect(await services1.transport.relationships.acceptRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not reject a relationship", async () => { + expect(await services1.transport.relationships.rejectRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not revoke a relationship", async () => { + expect(await services1.transport.relationships.rejectRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not decompose a relationship", async () => { + expect(await services1.transport.relationships.decomposeRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.wrongRelationshipStatus"); + }); +}); + +describe("Templator with active IdentityDeletionProcess", () => { + const serviceProvider = new RuntimeServiceProvider(); + let services1: TestRuntimeServices; + let services2: TestRuntimeServices; + + beforeAll(async () => { + const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true, enableDeciderModule: true, enableNotificationModule: true }); + services1 = runtimeServices[0]; + services2 = runtimeServices[1]; + }, 30000); + + afterAll(() => serviceProvider.stop()); + + test("returns error if templator has active IdentityDeletionProcess", async () => { + const templateId = (await exchangeTemplate(services1.transport, services2.transport)).id; + await services1.transport.identityDeletionProcesses.initiateIdentityDeletionProcess(); + + const createRelationshipResponse = await services2.transport.relationships.createRelationship({ + templateId: templateId, + creationContent: emptyRelationshipCreationContent }); + expect(createRelationshipResponse).toBeAnError( + "The Identity who created the RelationshipTemplate is currently in the process of deleting itself. Thus, it is not possible to establish a Relationship to it.", + "error.transport.relationships.activeIdentityDeletionProcessOfOwnerOfRelationshipTemplate" + ); }); }); @@ -313,3 +411,464 @@ describe("Attributes for the relationship", () => { ); }); }); + +describe("RelationshipTermination", () => { + let relationshipId: string; + let terminationResult: Result; + beforeAll(async () => { + relationshipId = (await ensureActiveRelationship(services1.transport, services2.transport)).id; + + const requestContent = { + content: { + items: [ + { + "@type": "TestRequestItem", + mustBeAccepted: false + } + ] + }, + peer: services2.address + }; + await exchangeMessageWithRequest(services1, services2, requestContent); + + terminationResult = await services1.transport.relationships.terminateRelationship({ relationshipId }); + }); + + test("relationship status is terminated", async () => { + expect(terminationResult).toBeSuccessful(); + await expect(services1.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.Termination + ); + expect(terminationResult.value.status).toBe(RelationshipStatus.Terminated); + + const syncedRelationship = (await syncUntilHasRelationships(services2.transport))[0]; + expect(syncedRelationship.status).toBe(RelationshipStatus.Terminated); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.Termination + ); + }); + + test("should not send a message", async () => { + const result = await services1.transport.messages.sendMessage({ + recipients: [services2.address], + content: { + "@type": "Mail", + body: "b", + cc: [], + subject: "a", + to: [services2.address] + } + }); + expect(result).toBeAnError(/.*/, "error.transport.messages.missingOrInactiveRelationship"); + }); + + test("should not decide a request", async () => { + const incomingRequest = (await services2.eventBus.waitForEvent(IncomingRequestReceivedEvent)).data; + + const canAcceptResult = (await services2.consumption.incomingRequests.canAccept({ requestId: incomingRequest.id, items: [{ accept: true }] })).value; + expect(canAcceptResult.isSuccess).toBe(false); + expect(canAcceptResult.code).toBe("error.consumption.requests.wrongRelationshipStatus"); + + const canRejectResult = (await services2.consumption.incomingRequests.canReject({ requestId: incomingRequest.id, items: [{ accept: false }] })).value; + expect(canRejectResult.isSuccess).toBe(false); + expect(canRejectResult.code).toBe("error.consumption.requests.wrongRelationshipStatus"); + }); + + test("should not create a request", async () => { + const requestContent = { + content: { + items: [ + { + "@type": "TestRequestItem", + mustBeAccepted: false + } + ] + }, + peer: services2.address + }; + const result = await services1.consumption.outgoingRequests.create(requestContent); + expect(result).toBeAnError(/.*/, "error.consumption.requests.wrongRelationshipStatus"); + }); + + test("should not create a challenge for the relationship", async () => { + const result = await services1.transport.challenges.createChallenge({ + challengeType: "Relationship", + relationship: relationshipId + }); + expect(result).toBeAnError(/.*/, "error.transport.challenges.challengeTypeRequiresActiveRelationship"); + }); + + describe("Validating relationship operations on terminated relationship", () => { + test("should not terminate a relationship in status terminated again", async () => { + expect(await services1.transport.relationships.terminateRelationship({ relationshipId })).toBeAnError(/.*/, "error.transport.relationships.wrongRelationshipStatus"); + }); + + test("reactivation acceptance should fail without reactivation request", async () => { + expect(await services1.transport.relationships.acceptRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.reactivationNotRequested" + ); + }); + + test("reactivation revocation should fail without reactivation request", async () => { + expect(await services1.transport.relationships.revokeRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.reactivationNotRequested" + ); + }); + + test("reactivation rejection should fail without reactivation request", async () => { + expect(await services1.transport.relationships.rejectRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.reactivationNotRequested" + ); + }); + }); + + describe("Validating relationship operations on terminated relationship with requested reactivation", () => { + beforeAll(async () => { + await services1.transport.relationships.requestRelationshipReactivation({ relationshipId }); + await syncUntilHasRelationships(services2.transport); + }); + + afterAll(async () => { + await services1.transport.relationships.revokeRelationshipReactivation({ relationshipId }); + await syncUntilHasRelationships(services2.transport); + }); + + test("reactivation acceptance should fail when the wrong side accepts it", async () => { + expect(await services1.transport.relationships.acceptRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.operationOnlyAllowedForPeer" + ); + }); + + test("reactivation rejection should fail when the wrong side rejects it", async () => { + expect(await services1.transport.relationships.rejectRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.operationOnlyAllowedForPeer" + ); + }); + + test("reactivation revocation should fail when the wrong side revokes it", async () => { + expect(await services2.transport.relationships.revokeRelationshipReactivation({ relationshipId })).toBeAnError( + /.*/, + "error.transport.relationships.operationOnlyAllowedForPeer" + ); + }); + + test("requesting reactivation twice should fail", async () => { + expect(await services1.transport.relationships.requestRelationshipReactivation({ relationshipId })).toBeAnError( + "You have already requested the reactivation", + "error.transport.relationships.reactivationAlreadyRequested" + ); + expect(await services2.transport.relationships.requestRelationshipReactivation({ relationshipId })).toBeAnError( + "Your peer has already requested the reactivation", + "error.transport.relationships.reactivationAlreadyRequested" + ); + }); + }); + + test("should request the relationship reactivation and then revoke it", async () => { + const reactivationRequestResult = await services1.transport.relationships.requestRelationshipReactivation({ relationshipId }); + expect(reactivationRequestResult).toBeSuccessful(); + await expect(services1.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.ReactivationRequested + ); + await expect(services1.eventBus).toHavePublished( + RelationshipReactivationRequestedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.ReactivationRequested + ); + + const revocationResult = await services1.transport.relationships.revokeRelationshipReactivation({ relationshipId }); + expect(revocationResult).toBeSuccessful(); + expect(revocationResult.value.status).toBe(RelationshipStatus.Terminated); + await expect(services1.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RevocationOfReactivation + ); + await expect(services1.eventBus).toHavePublished( + RelationshipReactivationCompletedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RevocationOfReactivation + ); + + const relationship2 = (await syncUntilHasRelationships(services2.transport))[0]; + expect(relationship2.status).toBe(RelationshipStatus.Terminated); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RevocationOfReactivation + ); + await expect(services2.eventBus).toHavePublished( + RelationshipReactivationCompletedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RevocationOfReactivation + ); + }); + + test("should get the relationship reactivation request from the peer and reject it", async () => { + await services1.transport.relationships.requestRelationshipReactivation({ relationshipId }); + + await syncUntilHasRelationships(services2.transport); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.ReactivationRequested + ); + await expect(services2.eventBus).toHavePublished( + RelationshipReactivationRequestedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.ReactivationRequested + ); + const rejectionResult = await services2.transport.relationships.rejectRelationshipReactivation({ relationshipId }); + expect(rejectionResult).toBeSuccessful(); + expect(rejectionResult.value.status).toBe(RelationshipStatus.Terminated); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RejectionOfReactivation + ); + await expect(services2.eventBus).toHavePublished( + RelationshipReactivationCompletedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RejectionOfReactivation + ); + + const relationship1 = (await syncUntilHasRelationships(services1.transport))[0]; + expect(relationship1.status).toBe(RelationshipStatus.Terminated); + await expect(services1.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RejectionOfReactivation + ); + await expect(services1.eventBus).toHavePublished( + RelationshipReactivationCompletedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.RejectionOfReactivation + ); + }); + + test("should get the relationship reactivation request from the peer and accept it", async () => { + await services1.transport.relationships.requestRelationshipReactivation({ relationshipId }); + + await syncUntilHasRelationships(services2.transport); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.ReactivationRequested + ); + await expect(services2.eventBus).toHavePublished( + RelationshipReactivationRequestedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.ReactivationRequested + ); + const acceptanceResult = await services2.transport.relationships.acceptRelationshipReactivation({ relationshipId }); + expect(acceptanceResult).toBeSuccessful(); + expect(acceptanceResult.value.status).toBe(RelationshipStatus.Active); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.AcceptanceOfReactivation + ); + await expect(services2.eventBus).toHavePublished( + RelationshipReactivationCompletedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.AcceptanceOfReactivation + ); + + const relationship1 = (await syncUntilHasRelationships(services1.transport))[0]; + expect(relationship1.status).toBe(RelationshipStatus.Active); + await expect(services1.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.AcceptanceOfReactivation + ); + await expect(services1.eventBus).toHavePublished( + RelationshipReactivationCompletedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.AcceptanceOfReactivation + ); + }); +}); + +describe("RelationshipDecomposition", () => { + let services3: TestRuntimeServices; + let relationshipId: string; + let templateId: string; + let relationshipId2: string; + let templateId2: string; + let multipleRecipientsMessageId: string; + + beforeAll(async () => { + const relationship = await ensureActiveRelationship(services1.transport, services2.transport); + relationshipId = relationship.id; + templateId = relationship.template.id; + + await createRelationshipData(services1, services2); + + const runtimeServices = await serviceProvider.launch(1, { enableRequestModule: true, enableDeciderModule: true, enableNotificationModule: true }); + services3 = runtimeServices[0]; + const relationship2 = await establishRelationship(services1.transport, services3.transport); + relationshipId2 = relationship2.id; + templateId2 = relationship2.template.id; + + await createRelationshipData(services1, services3); + multipleRecipientsMessageId = (await sendMessageToMultipleRecipients(services1.transport, [services2.address, services3.address])).id; + + await services1.transport.relationships.terminateRelationship({ relationshipId }); + await services1.transport.relationships.decomposeRelationship({ relationshipId }); + await services2.eventBus.waitForEvent(RelationshipChangedEvent); + + await services1.transport.relationships.terminateRelationship({ relationshipId: relationshipId2 }); + }); + + test("relationship should be decomposed", async () => { + await expect(services1.eventBus).toHavePublished(RelationshipDecomposedBySelfEvent, (e) => e.data.relationshipId === relationshipId); + + const ownRelationship = await services1.transport.relationships.getRelationship({ id: relationshipId }); + expect(ownRelationship).toBeAnError(/.*/, "error.runtime.recordNotFound"); + + const peerRelationship = (await syncUntilHasRelationships(services2.transport))[0]; + expect(peerRelationship.status).toBe(RelationshipStatus.DeletionProposed); + await expect(services2.eventBus).toHavePublished( + RelationshipChangedEvent, + (e) => e.data.id === relationshipId && e.data.auditLog[e.data.auditLog.length - 1].reason === RelationshipAuditLogEntryReason.Decomposition + ); + }); + + test("relationship template should be deleted", async () => { + const result = await services1.transport.relationshipTemplates.getRelationshipTemplate({ id: templateId }); + expect(result).toBeAnError(/.*/, "error.runtime.recordNotFound"); + + const resultControl = await services1.transport.relationshipTemplates.getRelationshipTemplate({ id: templateId2 }); + expect(resultControl).toBeSuccessful(); + }); + + test("requests should be deleted", async () => { + const outgoingRequests = (await services1.consumption.outgoingRequests.getRequests({ query: { peer: services2.address } })).value; + expect(outgoingRequests).toHaveLength(0); + + const incomingRequests = (await services1.consumption.incomingRequests.getRequests({ query: { peer: services2.address } })).value; + expect(incomingRequests).toHaveLength(0); + + const outgoingRequestsControl = (await services1.consumption.outgoingRequests.getRequests({ query: { peer: services3.address } })).value; + expect(outgoingRequestsControl).not.toHaveLength(0); + + const incomingRequestsControl = (await services1.consumption.incomingRequests.getRequests({ query: { peer: services3.address } })).value; + expect(incomingRequestsControl).not.toHaveLength(0); + }); + + test("attributes should be deleted", async () => { + const ownSharedAttributes = (await services1.consumption.attributes.getOwnSharedAttributes({ peer: services2.address })).value; + expect(ownSharedAttributes).toHaveLength(0); + + const peerSharedAttributes = (await services1.consumption.attributes.getPeerSharedAttributes({ peer: services2.address })).value; + expect(peerSharedAttributes).toHaveLength(0); + + const ownSharedAttributesControl = (await services1.consumption.attributes.getOwnSharedAttributes({ peer: services3.address })).value; + expect(ownSharedAttributesControl).not.toHaveLength(0); + + const peerSharedAttributesControl = (await services1.consumption.attributes.getPeerSharedAttributes({ peer: services3.address })).value; + expect(peerSharedAttributesControl).not.toHaveLength(0); + }); + + test("notifications should be deleted", async () => { + const notifications = (await services1.consumption.notifications.getNotifications({ query: { peer: services2.address } })).value; + expect(notifications).toHaveLength(0); + + const notificationsControl = (await services1.consumption.notifications.getNotifications({ query: { peer: services3.address } })).value; + expect(notificationsControl).not.toHaveLength(0); + }); + + test("messages should be deleted/anonymized", async () => { + const messagesToPeer = (await services1.transport.messages.getMessages({ query: { "recipients.address": services2.address } })).value; + expect(messagesToPeer).toHaveLength(0); + + const messagesFromPeer = (await services1.transport.messages.getMessages({ query: { createdBy: services2.address } })).value; + expect(messagesFromPeer).toHaveLength(0); + + const messagesToControlPeer = (await services1.transport.messages.getMessages({ query: { "recipients.address": services3.address } })).value; + expect(messagesToControlPeer).not.toHaveLength(0); + + const messagesFromControlPeer = (await services1.transport.messages.getMessages({ query: { createdBy: services3.address } })).value; + expect(messagesFromControlPeer).not.toHaveLength(0); + + const addressPseudonym = (await generateAddressPseudonym(process.env.NMSHD_TEST_BASEURL!)).toString(); + const anonymizedMessages = (await services1.transport.messages.getMessages({ query: { "recipients.address": addressPseudonym } })).value; + expect(anonymizedMessages).toHaveLength(1); + + const anonymizedMessage = anonymizedMessages[0]; + expect(anonymizedMessage.id).toBe(multipleRecipientsMessageId); + expect(anonymizedMessage.recipients.map((r) => r.address)).toStrictEqual([addressPseudonym, services3.address]); + }); + + test("messages with multiple recipients should be deleted if all its relationships are decomposed", async () => { + await services1.transport.relationships.decomposeRelationship({ relationshipId: relationshipId2 }); + const messages = (await services1.transport.messages.getMessages({})).value; + expect(messages).toHaveLength(0); + }); + + async function createRelationshipData(services1: TestRuntimeServices, services2: TestRuntimeServices) { + const requestContent = { + content: { + items: [ + { + "@type": "TestRequestItem", + mustBeAccepted: false + } + ] + } + }; + await exchangeMessageWithRequest(services1, services2, { ...requestContent, peer: services2.address }); + await exchangeMessageWithRequest(services2, services1, { ...requestContent, peer: services1.address }); + + await sendAndReceiveNotification(services1.transport, services2.transport, services2.consumption); + + await executeFullCreateAndShareRepositoryAttributeFlow(services1, services2, { + content: { + value: { + "@type": "GivenName", + value: "Own name" + } + } + }); + + await executeFullCreateAndShareRepositoryAttributeFlow(services2, services1, { + content: { + value: { + "@type": "GivenName", + value: "Own name" + } + } + }); + } +}); + +describe("Relationship existence check", () => { + const fakeRelationshipId = "REL00000000000000000"; + + test("should not accept a relationship", async function () { + expect(await services1.transport.relationships.acceptRelationship({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not reject a relationship", async function () { + expect(await services1.transport.relationships.rejectRelationship({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not revoke a relationship", async function () { + expect(await services1.transport.relationships.revokeRelationship({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not terminate a relationship", async function () { + expect(await services1.transport.relationships.terminateRelationship({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not request a relationship reactivation", async function () { + expect(await services1.transport.relationships.requestRelationshipReactivation({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not accept a relationship reactivation", async function () { + expect(await services1.transport.relationships.acceptRelationshipReactivation({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not reject a relationship reactivation", async function () { + expect(await services1.transport.relationships.rejectRelationshipReactivation({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not revoke a relationship reactivation", async function () { + expect(await services1.transport.relationships.revokeRelationshipReactivation({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + + test("should not decompose a relationship", async function () { + expect(await services1.transport.relationships.decomposeRelationship({ relationshipId: fakeRelationshipId })).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); +}); diff --git a/packages/transport/package.json b/packages/transport/package.json index 8597c1af3..82deef5ff 100644 --- a/packages/transport/package.json +++ b/packages/transport/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/transport", - "version": "2.8.2", + "version": "5.0.0", "description": "The transport library handles backbone communication and content encryption.", "homepage": "https://enmeshed.eu", "repository": { diff --git a/packages/transport/src/core/CoreErrors.ts b/packages/transport/src/core/CoreErrors.ts index 93bdc6868..eb66c5ab3 100644 --- a/packages/transport/src/core/CoreErrors.ts +++ b/packages/transport/src/core/CoreErrors.ts @@ -1,10 +1,25 @@ import stringify from "json-stringify-safe"; -import { RelationshipChangeStatus } from "../modules/relationships/transmission/changes/RelationshipChangeStatus"; +import { RelationshipStatus } from "../modules"; import { CoreError } from "./CoreError"; class Relationships { - public wrongChangeStatus(status: RelationshipChangeStatus) { - return new CoreError("error.transport.relationships.wrongChangeStatus", `The relationship change has the wrong status (${status}) to run this operation`); + public operationOnlyAllowedForPeer(message: string) { + return new CoreError("error.transport.relationships.operationOnlyAllowedForPeer", message); + } + + public wrongRelationshipStatus(relationshipId: string, status: RelationshipStatus) { + return new CoreError( + "error.transport.relationships.wrongRelationshipStatus", + `The Relationship '${relationshipId}' has the wrong status '${status}' to run this operation.` + ); + } + + public reactivationNotRequested(relationshipId: string) { + return new CoreError("error.transport.relationships.reactivationNotRequested", `The Relationship '${relationshipId}' has no reactivation Request to respond to.`); + } + + public reactivationAlreadyRequested(message: string) { + return new CoreError("error.transport.relationships.reactivationAlreadyRequested", message); } public activeIdentityDeletionProcessOfOwnerOfRelationshipTemplate() { @@ -21,7 +36,7 @@ class Device { } public couldNotDeleteDevice(reason: string, rootCause?: any) { - return new CoreError("error.transport.devices.couldNotDeleteDevice", `Could not delete device: ${reason}`, rootCause); + return new CoreError("error.transport.devices.couldNotDeleteDevice", `Could not delete device: '${reason}'`, rootCause); } } @@ -29,56 +44,52 @@ class Messages { public plaintextMismatch(ownAddress: string) { return new CoreError( "error.transport.messages.plaintextMismatch", - `The own address ${ownAddress} was not named as a recipient within the signed MessagePlaintext. A replay attack might be the cause of this.` + `The own address '${ownAddress}' was not named as a recipient within the signed MessagePlaintext. A replay attack might be the cause of this.` ); } public signatureListMismatch(address: string) { - return new CoreError("error.transport.messages.signatureListMismatch", `The signature list didn't contain an entry for address ${address}.`); + return new CoreError("error.transport.messages.signatureListMismatch", `The signature list didn't contain an entry for address '${address}'.`); } public signatureNotValid() { return new CoreError( "error.transport.messages.signatureNotValid", - "The digital signature on this message for peer key is invalid. An impersonination attack might be the cause of this." + "The digital signature on this Message for peer key is invalid. An impersonation attack might be the cause of this." ); } public ownAddressNotInList(messageId: string) { return new CoreError( "error.transport.messages.ownAddressNotInList", - `The recipients list of message ${messageId} didn't contain an entry for the own address. This message should not have been received.` + `The recipients list of Message ${messageId} didn't contain an entry for the own address. This Message should not have been received.` ); } - public noMatchingRelationship(address: string) { - return new CoreError("error.transport.messages.noMatchingRelationship", `A Relationship with the given address '${address}' does not exist.`); + public missingOrInactiveRelationship(address: string) { + return new CoreError("error.transport.messages.missingOrInactiveRelationship", `An active Relationship with the given address '${address}' does not exist.`); } } class Secrets { public wrongSecretType(secretId?: string) { - return new CoreError("error.transport.secrets.wrongBaseKeyType", "The given secret type is not supported!", { + return new CoreError("error.transport.secrets.wrongSecretType", "The given secret type is not supported!", { secretId: secretId }); } public secretNotFound(type: string) { - return new CoreError("error.transport.secrets.secretNotFound", `secret "${type}" not found`); + return new CoreError("error.transport.secrets.secretNotFound", `Secret '${type}' not found.`); } } class Challenges { - public challengeTypeRequiresRelationship() { - return new CoreError("error.transport.challenges.challengeTypeRequiresRelationship", "The challenge type 'Relationship' requires a relationship."); + public challengeTypeRequiresActiveRelationship() { + return new CoreError("error.transport.challenges.challengeTypeRequiresActiveRelationship", "The challenge type Relationship requires an active Relationship."); } } class Datawallet { - public encryptedPayloadIsNoCipher() { - return new CoreError("error.transport.datawallet.encryptedPayloadIsNoCipher", "The given encrypted payload is no cipher."); - } - public unsupportedModification(type: "unsupportedCacheChangedModificationCollection", data: any) { const errorCode = "error.transport.datawallet.unsupportedModification"; const formattedData = data ? stringify(data) : ""; @@ -87,7 +98,7 @@ class Datawallet { case "unsupportedCacheChangedModificationCollection": return new CoreError( errorCode, - `The following collections were received in CacheChanged datawallet modifications but are not supported by the current version of this library: ${formattedData}.` + `The following collections were received in CacheChanged datawallet modifications but are not supported by the current version of this library: '${formattedData}'.` ); default: @@ -123,24 +134,20 @@ class Files { } public invalidMetadata(id: string) { - return new CoreError("error.transport.files.invalidMetadata", `The metadata of the file with id "${id}" is invalid.`); - } - - public fileContentUndefined() { - return new CoreError("error.transport.files.fileContentUndefined", "The given file content is undefined."); + return new CoreError("error.transport.files.invalidMetadata", `The metadata of the File with id '${id}' is invalid.`); } public maxFileSizeExceeded(fileSize: number, platformMaxFileSize: number) { return new CoreError( "error.transport.files.maxFileSizeExceeded", - `The given file content size (${fileSize}) exceeds the max file size the backbone accepts (${platformMaxFileSize}).` + `The given File content size (${fileSize}) exceeds the max File size the Backbone accepts (${platformMaxFileSize}).` ); } } class Tokens { public invalidTokenContent(id: string) { - return new CoreError("error.transport.tokens.invalidTokenContent", `The content of token ${id} is not of type TokenContent`); + return new CoreError("error.transport.tokens.invalidTokenContent", `The content of Token '${id}' is not of type TokenContent.`); } } @@ -173,10 +180,6 @@ class General { return new CoreError("error.transport.notSupported", "The method is not yet supported."); } - public realmLength() { - return new CoreError("error.transport.identity.realmLength", "Realm must be of length 3."); - } - public invalidTruncatedReference() { return new CoreError("error.transport.files.invalidTruncatedReference", "invalid truncated reference"); } diff --git a/packages/transport/src/core/CoreSynchronizable.ts b/packages/transport/src/core/CoreSynchronizable.ts index faa3cda33..dca14a1a9 100644 --- a/packages/transport/src/core/CoreSynchronizable.ts +++ b/packages/transport/src/core/CoreSynchronizable.ts @@ -5,6 +5,7 @@ import { CoreId, ICoreId } from "./types/CoreId"; export interface ICoreSynchronizable extends ICoreSerializable { id: ICoreId; } + export abstract class CoreSynchronizable extends CoreSerializable implements ICoreSynchronizable { public readonly technicalProperties: string[] = []; public readonly userdataProperties: string[] = []; diff --git a/packages/transport/src/core/ProgressReporter.ts b/packages/transport/src/core/ProgressReporter.ts deleted file mode 100644 index f73d11514..000000000 --- a/packages/transport/src/core/ProgressReporter.ts +++ /dev/null @@ -1,42 +0,0 @@ -export class ProgressReporter { - public constructor(private readonly callback?: (currentPercentage: number, currentStep: T) => void) {} - - public createStep(stepName: T, totalNumberOfItemsInStep = -1): ProgressReporterStep { - return new ProgressReporterStep(stepName, totalNumberOfItemsInStep, this.callback); - } -} -export class ProgressReporterStep { - private currentItem: number; - public constructor( - public readonly name: T, - private totalNumberOfItems: number, - private readonly callback?: (currentPercentage: number, currentStep: T) => void - ) { - if (totalNumberOfItems > 0) this.progressTo(0); - } - - public progress(): void { - this.progressTo(this.currentItem + 1); - } - - public progressTo(itemIndex: number): void { - this.currentItem = itemIndex; - this.callback?.(Math.round((itemIndex / this.totalNumberOfItems) * 100), this.name); - } - - public incrementTotalNumberOfItems(): void { - this.updateTotalNumberOfItems(this.totalNumberOfItems + 1); - } - - public updateTotalNumberOfItems(newValue: number): void { - this.totalNumberOfItems = newValue; - } - - public finish(): void { - if (this.currentItem < this.totalNumberOfItems) this.progressTo(this.totalNumberOfItems); - } - - public manualReport(percentage: number): void { - this.callback?.(percentage, this.name); - } -} diff --git a/packages/transport/src/core/Transport.ts b/packages/transport/src/core/Transport.ts index 519bbb9d8..b9580f67a 100644 --- a/packages/transport/src/core/Transport.ts +++ b/packages/transport/src/core/Transport.ts @@ -6,7 +6,6 @@ import { SodiumWrapper } from "@nmshd/crypto"; import { AgentOptions } from "http"; import { AgentOptions as HTTPSAgentOptions } from "https"; import _ from "lodash"; -import { Realm } from "../modules/accounts/data/Realm"; import { CoreErrors } from "./CoreErrors"; import { TransportError } from "./TransportError"; import { TransportLoggerFactory } from "./TransportLoggerFactory"; @@ -25,7 +24,7 @@ export interface IConfig { platformMaxUnencryptedFileSize: number; platformAdditionalHeaders?: Record; baseUrl: string; - realm: Realm; + addressGenerationHostnameOverride?: string; datawalletEnabled: boolean; httpAgentOptions: AgentOptions; httpsAgentOptions: HTTPSAgentOptions; @@ -42,7 +41,7 @@ export interface IConfigOverwrite { platformMaxUnencryptedFileSize?: number; platformAdditionalHeaders?: Record; baseUrl: string; - realm?: Realm; + addressGenerationHostnameOverride?: string; datawalletEnabled?: boolean; httpAgentOptions?: AgentOptions; httpsAgentOptions?: HTTPSAgentOptions; @@ -67,7 +66,6 @@ export class Transport { platformMaxRedirects: 10, platformMaxUnencryptedFileSize: 10 * 1024 * 1024, baseUrl: "", - realm: Realm.Prod, datawalletEnabled: false, httpAgentOptions: { keepAlive: true, @@ -110,10 +108,6 @@ export class Transport { if (this._config.supportedIdentityVersion < 1) { throw new TransportError("The given supported identity version is invalid. The value must be 1 or higher."); } - - if (this._config.realm.length !== 3) { - throw CoreErrors.general.realmLength(); - } } public async init(): Promise { diff --git a/packages/transport/src/core/backbone/BackboneIds.ts b/packages/transport/src/core/backbone/BackboneIds.ts index 4817cf031..7d8f30757 100644 --- a/packages/transport/src/core/backbone/BackboneIds.ts +++ b/packages/transport/src/core/backbone/BackboneIds.ts @@ -6,6 +6,5 @@ export class BackboneIds { public static readonly message = new CoreIdHelper("MSG", true); public static readonly relationshipTemplate = new CoreIdHelper("RLT", true); public static readonly token = new CoreIdHelper("TOK", true); - public static readonly relationshipChange = new CoreIdHelper("RCH", true); public static readonly device = new CoreIdHelper("DVC", true); } diff --git a/packages/transport/src/core/index.ts b/packages/transport/src/core/index.ts index 149c8fa27..649d09a3a 100644 --- a/packages/transport/src/core/index.ts +++ b/packages/transport/src/core/index.ts @@ -9,7 +9,6 @@ export * from "./CoreSynchronizable"; export * from "./CoreUtil"; export * from "./DbCollectionName"; export * from "./DependencyOverrides"; -export * from "./ProgressReporter"; export * from "./Reference"; export * from "./Transport"; export * from "./TransportController"; diff --git a/packages/transport/src/events/RelationshipDecomposedBySelfEvent.ts b/packages/transport/src/events/RelationshipDecomposedBySelfEvent.ts new file mode 100644 index 000000000..3fef9c151 --- /dev/null +++ b/packages/transport/src/events/RelationshipDecomposedBySelfEvent.ts @@ -0,0 +1,14 @@ +import { CoreId } from "../core"; +import { TransportDataEvent } from "./TransportDataEvent"; + +export interface RelationshipDecomposedBySelfEventData { + relationshipId: CoreId; +} + +export class RelationshipDecomposedBySelfEvent extends TransportDataEvent { + public static readonly namespace = "transport.relationshipDecomposedBySelf"; + + public constructor(eventTargetAddress: string, data: RelationshipDecomposedBySelfEventData) { + super(RelationshipDecomposedBySelfEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/transport/src/events/RelationshipReactivationCompletedEvent.ts b/packages/transport/src/events/RelationshipReactivationCompletedEvent.ts new file mode 100644 index 000000000..258bc221d --- /dev/null +++ b/packages/transport/src/events/RelationshipReactivationCompletedEvent.ts @@ -0,0 +1,10 @@ +import { Relationship } from "../modules/relationships/local/Relationship"; +import { TransportDataEvent } from "./TransportDataEvent"; + +export class RelationshipReactivationCompletedEvent extends TransportDataEvent { + public static readonly namespace = "transport.relationshipReactivationCompleted"; + + public constructor(eventTargetAddress: string, data: Relationship) { + super(RelationshipReactivationCompletedEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/transport/src/events/RelationshipReactivationRequestedEvent.ts b/packages/transport/src/events/RelationshipReactivationRequestedEvent.ts new file mode 100644 index 000000000..5fdecadbf --- /dev/null +++ b/packages/transport/src/events/RelationshipReactivationRequestedEvent.ts @@ -0,0 +1,10 @@ +import { Relationship } from "../modules/relationships/local/Relationship"; +import { TransportDataEvent } from "./TransportDataEvent"; + +export class RelationshipReactivationRequestedEvent extends TransportDataEvent { + public static readonly namespace = "transport.relationshipReactivationRequested"; + + public constructor(eventTargetAddress: string, data: Relationship) { + super(RelationshipReactivationRequestedEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/transport/src/events/index.ts b/packages/transport/src/events/index.ts index f4bd5ef95..2a7df2149 100644 --- a/packages/transport/src/events/index.ts +++ b/packages/transport/src/events/index.ts @@ -5,4 +5,7 @@ export * from "./MessageSentEvent"; export * from "./MessageWasReadAtChangedEvent"; export * from "./PeerRelationshipTemplateLoadedEvent"; export * from "./RelationshipChangedEvent"; +export * from "./RelationshipDecomposedBySelfEvent"; +export * from "./RelationshipReactivationCompletedEvent"; +export * from "./RelationshipReactivationRequestedEvent"; export * from "./TransportDataEvent"; diff --git a/packages/transport/src/modules/accounts/AccountController.ts b/packages/transport/src/modules/accounts/AccountController.ts index 95af89d90..14b55300c 100644 --- a/packages/transport/src/modules/accounts/AccountController.ts +++ b/packages/transport/src/modules/accounts/AccountController.ts @@ -12,30 +12,30 @@ import { CertificateController } from "../certificates/CertificateController"; import { CertificateIssuer } from "../certificates/CertificateIssuer"; import { CertificateValidator } from "../certificates/CertificateValidator"; import { ChallengeController } from "../challenges/ChallengeController"; -import { BackbonePutDevicesPushNotificationRequest, DeviceAuthClient } from "../devices/backbone/DeviceAuthClient"; -import { DeviceClient } from "../devices/backbone/DeviceClient"; import { DeviceController } from "../devices/DeviceController"; -import { DevicesController } from "../devices/DevicesController"; import { DeviceSecretType } from "../devices/DeviceSecretController"; +import { DevicesController } from "../devices/DevicesController"; +import { BackbonePutDevicesPushNotificationRequest, DeviceAuthClient } from "../devices/backbone/DeviceAuthClient"; +import { DeviceClient } from "../devices/backbone/DeviceClient"; import { Device, DeviceInfo, DeviceType } from "../devices/local/Device"; import { DeviceSecretCredentials } from "../devices/local/DeviceSecretCredentials"; import { DeviceSharedSecret } from "../devices/transmission/DeviceSharedSecret"; import { FileController } from "../files/FileController"; import { MessageController } from "../messages/MessageController"; -import { RelationshipsController } from "../relationships/RelationshipsController"; -import { RelationshipSecretController } from "../relationships/RelationshipSecretController"; import { RelationshipTemplateController } from "../relationshipTemplates/RelationshipTemplateController"; +import { RelationshipSecretController } from "../relationships/RelationshipSecretController"; +import { RelationshipsController } from "../relationships/RelationshipsController"; +import { Relationship } from "../relationships/local/Relationship"; import { SecretController } from "../secrets/SecretController"; import { ChangedItems } from "../sync/ChangedItems"; -import { SyncProgressCallback, SyncProgressReporter } from "../sync/SyncCallback"; import { SyncController } from "../sync/SyncController"; import { SynchronizedCollection } from "../sync/SynchronizedCollection"; import { TokenController } from "../tokens/TokenController"; -import { IdentityClient } from "./backbone/IdentityClient"; -import { Identity } from "./data/Identity"; import { IdentityController } from "./IdentityController"; import { IdentityDeletionProcessController } from "./IdentityDeletionProcessController"; import { IdentityUtil } from "./IdentityUtil"; +import { IdentityClient } from "./backbone/IdentityClient"; +import { Identity } from "./data/Identity"; export class AccountController { private readonly _authenticator: AbstractAuthenticator; @@ -225,18 +225,16 @@ export class AccountController { await this.syncDatawallet(); } - public async syncDatawallet(force = false, syncProgressCallback?: SyncProgressCallback): Promise { + public async syncDatawallet(force = false): Promise { if (!force && !this.autoSync) { return; } - const reporter = SyncProgressReporter.fromCallback(syncProgressCallback); - return await this.synchronization.sync("OnlyDatawallet", reporter); + return await this.synchronization.sync("OnlyDatawallet"); } - public async syncEverything(syncProgressCallback?: SyncProgressCallback): Promise { - const reporter = SyncProgressReporter.fromCallback(syncProgressCallback); - return await this.synchronization.sync("Everything", reporter); + public async syncEverything(): Promise { + return await this.synchronization.sync("Everything"); } public async getLastCompletedSyncTime(): Promise { @@ -281,7 +279,7 @@ export class AccountController { CoreCrypto.generateSecretKey(), // Generate address locally - IdentityUtil.createAddress(identityKeypair.publicKey, this._config.realm), + IdentityUtil.createAddress(identityKeypair.publicKey, this._config.addressGenerationHostnameOverride ?? new URL(this._config.baseUrl).hostname), this.fetchDeviceInfo() ]); @@ -297,12 +295,11 @@ export class AccountController { this._log.trace(`Registered identity with address ${deviceResponse.address}, device id is ${deviceResponse.device.id}.`); if (!localAddress.equals(deviceResponse.address)) { - throw new TransportError("The backbone address does not match the local address."); + throw new TransportError(`The backbone address '${deviceResponse.address}' does not match the local address '${localAddress.toString()}'.`); } const identity = Identity.from({ address: CoreAddress.from(deviceResponse.address), - realm: this._config.realm, publicKey: identityKeypair.publicKey }); @@ -431,4 +428,10 @@ export class AccountController { return new SynchronizedCollection(collection, this.config.supportedDatawalletVersion, this.unpushedDatawalletModifications); } + + public async cleanupDataOfDecomposedRelationship(relationship: Relationship): Promise { + await this.messages.cleanupMessagesOfDecomposedRelationship(relationship); + await this.relationshipTemplates.cleanupTemplatesOfDecomposedRelationship(relationship); + await this.tokens.cleanupTokensOfDecomposedRelationship(relationship.peer.address); + } } diff --git a/packages/transport/src/modules/accounts/IdentityController.ts b/packages/transport/src/modules/accounts/IdentityController.ts index 467da11cf..8242cda18 100644 --- a/packages/transport/src/modules/accounts/IdentityController.ts +++ b/packages/transport/src/modules/accounts/IdentityController.ts @@ -4,7 +4,6 @@ import { ControllerName, CoreAddress, CoreCrypto, CoreErrors, TransportControlle import { AccountController } from "../accounts/AccountController"; import { DeviceSecretType } from "../devices/DeviceSecretController"; import { Identity } from "./data/Identity"; -import { Realm } from "./data/Realm"; export class IdentityController extends TransportController { public get address(): CoreAddress { @@ -15,10 +14,6 @@ export class IdentityController extends TransportController { return this._identity.publicKey; } - public get realm(): Realm { - return this._identity.realm; - } - public get identity(): Identity { return this._identity; } diff --git a/packages/transport/src/modules/accounts/IdentityUtil.ts b/packages/transport/src/modules/accounts/IdentityUtil.ts index 9115e772c..f7b681281 100644 --- a/packages/transport/src/modules/accounts/IdentityUtil.ts +++ b/packages/transport/src/modules/accounts/IdentityUtil.ts @@ -1,56 +1,53 @@ -import { CoreBuffer, CryptoHash, CryptoHashAlgorithm, ICryptoSignaturePublicKey } from "@nmshd/crypto"; -import { CoreAddress, CoreErrors } from "../../core"; +import { CoreBuffer, CryptoHash, CryptoHashAlgorithm, Encoding, ICryptoSignaturePublicKey } from "@nmshd/crypto"; +import { CoreAddress } from "../../core"; -export class IdentityUtil { - public static async createAddress(publicKey: ICryptoSignaturePublicKey, realm: string): Promise { - if (realm.length !== 3) throw CoreErrors.general.realmLength(); +const enmeshedAddressDIDPrefix = "did:e:"; +export class IdentityUtil { + public static async createAddress(publicKey: ICryptoSignaturePublicKey, backboneHostname: string): Promise { const sha512buffer = await CryptoHash.hash(publicKey.publicKey, CryptoHashAlgorithm.SHA512); const hash = await CryptoHash.hash(sha512buffer, CryptoHashAlgorithm.SHA256); - const hashedPublicKey = new CoreBuffer(hash.buffer.slice(0, 20)); - - const checksumSource = CoreBuffer.fromUtf8(realm); - checksumSource.append(hashedPublicKey); + const hashedPublicKey = new CoreBuffer(hash.buffer.slice(0, 10)); + const identityPart = hashedPublicKey.toString(Encoding.Hex); - const addressChecksum1 = await CryptoHash.hash(checksumSource, CryptoHashAlgorithm.SHA512); - const checksumHash = await CryptoHash.hash(addressChecksum1, CryptoHashAlgorithm.SHA256); - const checksum = new CoreBuffer(checksumHash.buffer.slice(0, 4)); + const checksumSource = CoreBuffer.fromUtf8(`${enmeshedAddressDIDPrefix}${backboneHostname}:dids:${identityPart}`); + const checksumHash = await CryptoHash.hash(checksumSource, CryptoHashAlgorithm.SHA256); + const checksum = new CoreBuffer(checksumHash.buffer.slice(0, 1)); - const concatenation = hashedPublicKey; - concatenation.append(checksum); - - const addressString = realm + concatenation.toBase58(); + const addressString = `${enmeshedAddressDIDPrefix}${backboneHostname}:dids:${identityPart}${checksum.toString(Encoding.Hex)}`; const addressObj = CoreAddress.from({ address: addressString }); return addressObj; } - public static async checkAddress(address: CoreAddress, publicKey?: ICryptoSignaturePublicKey, realm = "id1"): Promise { + public static async checkAddress(address: CoreAddress, backboneHostname: string, publicKey?: ICryptoSignaturePublicKey): Promise { const str = address.toString(); - const strRealm = str.substr(0, 3); - if (realm && strRealm !== realm) { + + const prefixLength = enmeshedAddressDIDPrefix.length; + const strWithoutPrefix = str.substring(prefixLength); + if (!strWithoutPrefix.startsWith(backboneHostname)) { return false; } - const strAddress = str.substr(3); + const strAddress = str.substring(str.length - 22); + const strHashedPublicKey = strAddress.substring(0, 20); + const strPrefixRealm = str.substring(0, str.length - 22); - const addressBuffer = CoreBuffer.fromBase58(strAddress).buffer; + const addressBuffer = CoreBuffer.fromString(strAddress, Encoding.Hex).buffer; + const sha256Array = addressBuffer.slice(0, addressBuffer.byteLength - 1); + const checksumArray = addressBuffer.slice(addressBuffer.byteLength - 1, addressBuffer.byteLength); - const sha256Array = addressBuffer.slice(0, addressBuffer.byteLength - 4); - const checksumArray = addressBuffer.slice(addressBuffer.byteLength - 4, addressBuffer.byteLength); + const checksumBuffer = CoreBuffer.fromUtf8(strPrefixRealm + strHashedPublicKey); - const checksumBuffer = CoreBuffer.fromUtf8(strRealm); - checksumBuffer.append(new CoreBuffer(sha256Array)); - const addressChecksum1 = await CryptoHash.hash(checksumBuffer, CryptoHashAlgorithm.SHA512); - const addressChecksum2 = await CryptoHash.hash(addressChecksum1, CryptoHashAlgorithm.SHA256); - const firstBytesOfChecksum = new CoreBuffer(addressChecksum2.buffer.slice(0, 4)); - if (!firstBytesOfChecksum.equals(new CoreBuffer(checksumArray))) { + const addressChecksum = await CryptoHash.hash(checksumBuffer, CryptoHashAlgorithm.SHA256); + const firstByteOfChecksum = new CoreBuffer(addressChecksum.buffer.slice(0, 1)); + if (!firstByteOfChecksum.equals(new CoreBuffer(checksumArray))) { return false; } if (publicKey) { const sha512buffer = await CryptoHash.hash(publicKey.publicKey, CryptoHashAlgorithm.SHA512); let sha256buffer = await CryptoHash.hash(sha512buffer, CryptoHashAlgorithm.SHA256); - sha256buffer = new CoreBuffer(sha256buffer.buffer.slice(0, 20)); + sha256buffer = new CoreBuffer(sha256buffer.buffer.slice(0, 10)); if (!sha256buffer.equals(new CoreBuffer(sha256Array))) { // Hash doesn't match with given public key. return false; diff --git a/packages/transport/src/modules/accounts/data/Identity.ts b/packages/transport/src/modules/accounts/data/Identity.ts index db21127a4..796e67a9e 100644 --- a/packages/transport/src/modules/accounts/data/Identity.ts +++ b/packages/transport/src/modules/accounts/data/Identity.ts @@ -1,12 +1,10 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { CryptoSignaturePublicKey, ICryptoSignaturePublicKey } from "@nmshd/crypto"; import { CoreAddress, CoreSerializable, ICoreSerializable } from "../../../core"; -import { Realm } from "./Realm"; export interface IIdentity extends ICoreSerializable { address: CoreAddress; publicKey: ICryptoSignaturePublicKey; - realm: Realm; } @type("Identity") @@ -19,10 +17,6 @@ export class Identity extends CoreSerializable implements IIdentity { @serialize() public publicKey: CryptoSignaturePublicKey; - @validate() - @serialize() - public realm: Realm; - public static from(value: IIdentity): Identity { return this.fromAny(value); } diff --git a/packages/transport/src/modules/accounts/data/IdentitySecretCredentials.ts b/packages/transport/src/modules/accounts/data/IdentitySecretCredentials.ts index 3101d1ceb..57b569885 100644 --- a/packages/transport/src/modules/accounts/data/IdentitySecretCredentials.ts +++ b/packages/transport/src/modules/accounts/data/IdentitySecretCredentials.ts @@ -1,21 +1,15 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { CryptoSecretKey, CryptoSignaturePrivateKey, CryptoSignaturePublicKey, ICryptoSecretKey, ICryptoSignaturePrivateKey, ICryptoSignaturePublicKey } from "@nmshd/crypto"; import { CoreSerializable, ICoreSerializable } from "../../../core"; -import { Realm } from "./Realm"; export interface IIdentitySecretCredentials extends ICoreSerializable { publicKey?: ICryptoSignaturePublicKey; - realm: Realm; synchronizationKey: ICryptoSecretKey; privateKey?: ICryptoSignaturePrivateKey; } @type("IdentitySecretCredentials") export class IdentitySecretCredentials extends CoreSerializable implements IIdentitySecretCredentials { - @validate() - @serialize() - public realm: Realm; - @validate({ nullable: true }) @serialize() public publicKey?: CryptoSignaturePublicKey; diff --git a/packages/transport/src/modules/accounts/data/Realm.ts b/packages/transport/src/modules/accounts/data/Realm.ts deleted file mode 100644 index 07dd82402..000000000 --- a/packages/transport/src/modules/accounts/data/Realm.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Realm { - Dev = "dev", - Stage = "id0", - Prod = "id1" -} diff --git a/packages/transport/src/modules/challenges/ChallengeController.ts b/packages/transport/src/modules/challenges/ChallengeController.ts index 43a5807a7..17dd3dd80 100644 --- a/packages/transport/src/modules/challenges/ChallengeController.ts +++ b/packages/transport/src/modules/challenges/ChallengeController.ts @@ -4,6 +4,7 @@ import { CoreAddress, CoreCrypto, CoreDate, CoreErrors, CoreId } from "../../cor import { ControllerName, TransportController } from "../../core/TransportController"; import { AccountController } from "../accounts/AccountController"; import { Relationship } from "../relationships/local/Relationship"; +import { RelationshipStatus } from "../relationships/transmission/RelationshipStatus"; import { ChallengeAuthClient } from "./backbone/ChallengeAuthClient"; import { ChallengeClient } from "./backbone/ChallengeClient"; import { Challenge, ChallengeType } from "./data/Challenge"; @@ -87,8 +88,8 @@ export class ChallengeController extends TransportController { @log() public async createChallenge(type: ChallengeType = ChallengeType.Identity, relationship?: Relationship): Promise { - if (type === ChallengeType.Relationship && !relationship) { - throw CoreErrors.challenges.challengeTypeRequiresRelationship(); + if (type === ChallengeType.Relationship && relationship?.status !== RelationshipStatus.Active) { + throw CoreErrors.challenges.challengeTypeRequiresActiveRelationship(); } const backboneResponse = (await this.authClient.createChallenge()).value; diff --git a/packages/transport/src/modules/devices/DeviceController.ts b/packages/transport/src/modules/devices/DeviceController.ts index b506e9636..61432e9eb 100644 --- a/packages/transport/src/modules/devices/DeviceController.ts +++ b/packages/transport/src/modules/devices/DeviceController.ts @@ -143,6 +143,13 @@ export class DeviceController extends TransportController { }; } + public async setCommunicationLanguage(language: string): Promise { + const result = await this.parent.deviceAuthClient.updateCurrentDevice({ communicationLanguage: language }); + if (result.isError) { + throw result.error; + } + } + public async markAsOffboarded(): Promise { this.device.isOffboarded = true; await this.parent.devices.update(this.device); diff --git a/packages/transport/src/modules/devices/backbone/BackboneUpdateDevice.ts b/packages/transport/src/modules/devices/backbone/BackboneUpdateDevice.ts new file mode 100644 index 000000000..089b01106 --- /dev/null +++ b/packages/transport/src/modules/devices/backbone/BackboneUpdateDevice.ts @@ -0,0 +1,3 @@ +export interface BackboneUpdateDeviceRequest { + communicationLanguage: string; +} diff --git a/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts b/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts index 50ba697a1..11530a0ab 100644 --- a/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts +++ b/packages/transport/src/modules/devices/backbone/DeviceAuthClient.ts @@ -1,6 +1,7 @@ import { RESTClientAuthenticate, RESTClientLogDirective } from "../../../core"; import { ClientResult } from "../../../core/backbone/ClientResult"; import { BackbonePostDevicesRequest, BackbonePostDevicesResponse } from "./BackbonePostDevices"; +import { BackboneUpdateDeviceRequest } from "./BackboneUpdateDevice"; export interface BackbonePutDevicesPasswordRequest { oldPassword: string; @@ -40,4 +41,8 @@ export class DeviceAuthClient extends RESTClientAuthenticate { public async unregisterPushNotificationToken(): Promise> { return await this.delete("/api/v1/Devices/Self/PushNotifications"); } + + public async updateCurrentDevice(value: BackboneUpdateDeviceRequest): Promise> { + return await this.put("/api/v1/Devices/Self", value); + } } diff --git a/packages/transport/src/modules/devices/local/Device.ts b/packages/transport/src/modules/devices/local/Device.ts index d688fcf6b..a940d37aa 100644 --- a/packages/transport/src/modules/devices/local/Device.ts +++ b/packages/transport/src/modules/devices/local/Device.ts @@ -88,7 +88,9 @@ export class Device extends CoreSynchronizable implements IDevice { @serialize() public lastLoginAt?: CoreDate; - @validate() + @validate({ + customValidator: (v) => (!Object.values(DeviceType).includes(v) ? `must be one of: ${Object.values(DeviceType)}` : undefined) + }) @serialize() public type: DeviceType; diff --git a/packages/transport/src/modules/files/FileController.ts b/packages/transport/src/modules/files/FileController.ts index 720cbbc8a..090b9f5dc 100644 --- a/packages/transport/src/modules/files/FileController.ts +++ b/packages/transport/src/modules/files/FileController.ts @@ -48,12 +48,20 @@ export class FileController extends TransportController { const decryptionPromises = backboneFiles.map(async (f) => { const fileDoc = await this.files.read(f.id); + if (!fileDoc) { + this._log.error( + `File '${f.id}' not found in local database and the cache fetching was therefore skipped. This should not happen and might be a bug in the application logic.` + ); + return; + } + const file = File.from(fileDoc); return { id: CoreId.from(f.id), cache: await this.decryptFile(f, file.secretKey) }; }); - return await Promise.all(decryptionPromises); + const caches = await Promise.all(decryptionPromises); + return caches.filter((c) => c !== undefined); } public async updateCache(ids: string[]): Promise { diff --git a/packages/transport/src/modules/index.ts b/packages/transport/src/modules/index.ts index bb1ae3cb1..410064c57 100644 --- a/packages/transport/src/modules/index.ts +++ b/packages/transport/src/modules/index.ts @@ -4,7 +4,6 @@ export * from "./accounts/backbone/IdentityDeletionProcessClient"; export * from "./accounts/data/Identity"; export * from "./accounts/data/IdentityDeletionProcess"; export * from "./accounts/data/IdentityDeletionProcessStatus"; -export * from "./accounts/data/Realm"; export * from "./accounts/IdentityController"; export * from "./accounts/IdentityDeletionProcessController"; export * from "./accounts/IdentityUtil"; @@ -31,6 +30,7 @@ export * from "./challenges/ChallengeController"; export * from "./challenges/data/Challenge"; export * from "./challenges/data/ChallengeSigned"; export * from "./devices/backbone/BackbonePostDevices"; +export * from "./devices/backbone/BackboneUpdateDevice"; export * from "./devices/backbone/DeviceAuthClient"; export * from "./devices/backbone/DeviceClient"; export * from "./devices/DeviceController"; @@ -53,6 +53,7 @@ export * from "./messages/backbone/BackboneGetMessages"; export * from "./messages/backbone/BackbonePostMessages"; export * from "./messages/backbone/MessageClient"; export * from "./messages/local/CachedMessage"; +export * from "./messages/local/CachedMessageRecipient"; export * from "./messages/local/Message"; export * from "./messages/local/SendMessageParameters"; export * from "./messages/MessageController"; @@ -62,26 +63,23 @@ export * from "./messages/transmission/MessageEnvelopeRecipient"; export * from "./messages/transmission/MessageSignature"; export * from "./messages/transmission/MessageSigned"; export * from "./relationships/backbone/BackboneGetRelationships"; -export * from "./relationships/backbone/BackboneGetRelationshipsChanges"; -export * from "./relationships/backbone/BackbonePostRelationshipsChanges"; +export * from "./relationships/backbone/BackbonePostRelationship"; export * from "./relationships/backbone/RelationshipClient"; export * from "./relationships/local/CachedRelationship"; export * from "./relationships/local/Relationship"; +export * from "./relationships/local/RelationshipAuditLog"; +export * from "./relationships/local/RelationshipAuditLogEntry"; export * from "./relationships/local/SendRelationshipParameters"; export * from "./relationships/RelationshipsController"; export * from "./relationships/RelationshipSecretController"; -export * from "./relationships/transmission/changes/RelationshipChange"; -export * from "./relationships/transmission/changes/RelationshipChangeRequest"; -export * from "./relationships/transmission/changes/RelationshipChangeResponse"; -export * from "./relationships/transmission/changes/RelationshipChangeStatus"; -export * from "./relationships/transmission/changes/RelationshipChangeType"; +export * from "./relationships/transmission/RelationshipAuditLog"; export * from "./relationships/transmission/RelationshipStatus"; -export * from "./relationships/transmission/requests/RelationshipCreationChangeRequestCipher"; -export * from "./relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper"; -export * from "./relationships/transmission/requests/RelationshipCreationChangeRequestSigned"; -export * from "./relationships/transmission/responses/RelationshipCreationChangeResponseCipher"; -export * from "./relationships/transmission/responses/RelationshipCreationChangeResponseContentWrapper"; -export * from "./relationships/transmission/responses/RelationshipCreationChangeResponseSigned"; +export * from "./relationships/transmission/requests/RelationshipCreationContentCipher"; +export * from "./relationships/transmission/requests/RelationshipCreationContentSigned"; +export * from "./relationships/transmission/requests/RelationshipCreationContentWrapper"; +export * from "./relationships/transmission/responses/RelationshipCreationResponseContentCipher"; +export * from "./relationships/transmission/responses/RelationshipCreationResponseContentSigned"; +export * from "./relationships/transmission/responses/RelationshipCreationResponseContentWrapper"; export * from "./relationshipTemplates/backbone/BackboneGetRelationshipTemplates"; export * from "./relationshipTemplates/backbone/BackbonePostRelationshipTemplates"; export * from "./relationshipTemplates/backbone/RelationshipTemplateClient"; @@ -108,7 +106,6 @@ export * from "./sync/ChangedItems"; export * from "./sync/data/ExternalEvent"; export * from "./sync/DatawalletModificationsProcessor"; export * from "./sync/local/DatawalletModification"; -export { SyncProgressCallback as SyncPercentageCallback, SyncStep } from "./sync/SyncCallback"; export * from "./sync/SyncController"; export * from "./sync/SynchronizedCollection"; export * from "./tokens/AnonymousTokenController"; diff --git a/packages/transport/src/modules/messages/MessageController.ts b/packages/transport/src/modules/messages/MessageController.ts index 3a159d24e..afed3bb42 100644 --- a/packages/transport/src/modules/messages/MessageController.ts +++ b/packages/transport/src/modules/messages/MessageController.ts @@ -2,21 +2,24 @@ import { ISerializable } from "@js-soft/ts-serval"; import { log } from "@js-soft/ts-utils"; import { CoreBuffer, CryptoCipher, CryptoSecretKey } from "@nmshd/crypto"; import { nameof } from "ts-simple-nameof"; -import { CoreAddress, CoreCrypto, CoreDate, CoreErrors, CoreId, ICoreAddress, TransportError } from "../../core"; +import { CoreAddress, CoreCrypto, CoreDate, CoreErrors, CoreId, ICoreAddress, ICoreId, TransportError } from "../../core"; import { DbCollectionName } from "../../core/DbCollectionName"; import { ControllerName, TransportController } from "../../core/TransportController"; import { MessageSentEvent, MessageWasReadAtChangedEvent } from "../../events"; import { AccountController } from "../accounts/AccountController"; +import { IdentityUtil } from "../accounts/IdentityUtil"; import { File } from "../files/local/File"; import { FileReference } from "../files/transmission/FileReference"; -import { Relationship } from "../relationships/local/Relationship"; -import { RelationshipsController } from "../relationships/RelationshipsController"; import { RelationshipSecretController } from "../relationships/RelationshipSecretController"; +import { RelationshipsController } from "../relationships/RelationshipsController"; +import { Relationship } from "../relationships/local/Relationship"; +import { RelationshipStatus } from "../relationships/transmission/RelationshipStatus"; import { SynchronizedCollection } from "../sync/SynchronizedCollection"; import { BackboneGetMessagesResponse } from "./backbone/BackboneGetMessages"; import { BackbonePostMessagesRecipientRequest } from "./backbone/BackbonePostMessages"; import { MessageClient } from "./backbone/MessageClient"; import { CachedMessage } from "./local/CachedMessage"; +import { CachedMessageRecipient } from "./local/CachedMessageRecipient"; import { Message } from "./local/Message"; import { ISendMessageParameters, SendMessageParameters } from "./local/SendMessageParameters"; import { MessageContentWrapper } from "./transmission/MessageContentWrapper"; @@ -56,17 +59,52 @@ export class MessageController extends TransportController { public async getMessagesByRelationshipId(id: CoreId): Promise { return await this.getMessages({ - [nameof((m) => m.relationshipIds)]: { - $contains: id.toString() - } + [`${nameof((m) => m.cache)}.${nameof((m) => m.recipients)}.${nameof((m) => m.relationshipId)}`]: id.toString() }); } + public async cleanupMessagesOfDecomposedRelationship(relationship: Relationship): Promise { + const messages = await this.getMessagesByRelationshipId(relationship.id); + for (const message of messages) { + await this.cleanupMessageOfDecomposedRelationship(message.id, relationship); + } + } + + private async cleanupMessageOfDecomposedRelationship(messageId: CoreId, relationship: Relationship): Promise { + const messageDoc = await this.messages.read(messageId.toString()); + const message = Message.from(messageDoc); + + // a received message only has one recipient (yourself) so it can be deleted without further looking into the recipients + // also if the message is not own, it can be deleted when there is only one recipient + if (!message.isOwn || message.cache!.recipients.length === 1) { + await this.messages.delete(message); + return; + } + + const recipient = message.cache!.recipients.find((r) => r.relationshipId?.equals(relationship.id)); + if (!recipient) { + this.log.warn(`Recipient not found in message ${message.id.toString()}`); + return; + } + + const pseudonym = await IdentityUtil.createAddress({ algorithm: 1, publicKey: CoreBuffer.fromUtf8("deleted identity") }, new URL(this.parent.config.baseUrl).hostname); + + recipient.address = pseudonym; + recipient.relationshipId = undefined; + + if (message.cache!.recipients.every((r) => r.address.equals(pseudonym))) { + await this.messages.delete(message); + return; + } + + await this.messages.update(messageDoc, message); + } + @log() public async getMessagesByAddress(address: CoreAddress): Promise { const relationship = await this.parent.relationships.getActiveRelationshipToIdentity(address); if (!relationship) { - throw CoreErrors.messages.noMatchingRelationship(address.toString()); + throw CoreErrors.messages.missingOrInactiveRelationship(address.toString()); } return await this.getMessagesByRelationshipId(relationship.id); } @@ -102,6 +140,13 @@ export class MessageController extends TransportController { const decryptionPromises = backboneMessages.map(async (m) => { const messageDoc = await this.messages.read(m.id); + if (!messageDoc) { + this._log.error( + `Message '${m.id}' not found in local database and the cache fetching was therefore skipped. This should not happen and might be a bug in the application logic.` + ); + return; + } + const message = Message.from(messageDoc); const envelope = this.getEnvelopeFromBackboneGetMessagesResponse(m); @@ -109,7 +154,8 @@ export class MessageController extends TransportController { return { id: CoreId.from(m.id), cache: cachedMessage }; }); - return await Promise.all(decryptionPromises); + const caches = await Promise.all(decryptionPromises); + return caches.filter((c) => c !== undefined); } @log() @@ -154,8 +200,7 @@ export class MessageController extends TransportController { const message = Message.from({ id: envelope.id, isOwn: false, - secretKey: messageKey, - relationshipIds: [relationship.id] + secretKey: messageKey }); message.setCache(cachedMessage); await this.messages.create(message); @@ -250,7 +295,7 @@ export class MessageController extends TransportController { for (const recipient of parsedParams.recipients) { const relationship = await this.relationships.getActiveRelationshipToIdentity(recipient); if (!relationship) { - throw CoreErrors.general.recordNotFound(Relationship, recipient.toString()); + throw CoreErrors.messages.missingOrInactiveRelationship(recipient.toString()); } const cipherForRecipient = await this.secrets.encrypt(relationship.relationshipSecretId, serializedSecret); @@ -282,11 +327,14 @@ export class MessageController extends TransportController { const plaintextBuffer = CoreBuffer.fromUtf8(serializedPlaintext); const messageSignatures: MessageSignature[] = []; - const relationshipIds = []; + + // a object of address to relationshipId + const addressToRelationshipId: Record = {}; + for (const recipient of parsedParams.recipients) { const relationship = await this.relationships.getActiveRelationshipToIdentity(CoreAddress.from(recipient)); if (!relationship) { - throw CoreErrors.general.recordNotFound(Relationship, recipient.toString()); + throw CoreErrors.messages.missingOrInactiveRelationship(recipient.toString()); } const signature = await this.secrets.sign(relationship.relationshipSecretId, plaintextBuffer); @@ -295,7 +343,8 @@ export class MessageController extends TransportController { signature: signature }); messageSignatures.push(messageSignature); - relationshipIds.push(relationship.id); + + addressToRelationshipId[recipient.toString()] = relationship.id; } const signed = MessageSigned.from({ @@ -326,12 +375,22 @@ export class MessageController extends TransportController { }) ).value; + const recipients = envelopeRecipients.map((r) => + CachedMessageRecipient.from({ + address: r.address, + encryptedKey: r.encryptedKey, + receivedAt: r.receivedAt, + receivedByDevice: r.receivedByDevice, + relationshipId: addressToRelationshipId[r.address.toString()] + }) + ); + const cachedMessage = CachedMessage.from({ content: parsedParams.content, createdAt: CoreDate.from(response.createdAt), createdBy: this.parent.identity.identity.address, createdByDevice: this.parent.activeDevice.id, - recipients: envelopeRecipients, + recipients, attachments: publicAttachmentArray, receivedByEveryone: false }); @@ -341,8 +400,7 @@ export class MessageController extends TransportController { secretKey: secret, cache: cachedMessage, cachedAt: CoreDate.utc(), - isOwn: true, - relationshipIds: relationshipIds + isOwn: true }); await this.messages.create(message); @@ -416,6 +474,8 @@ export class MessageController extends TransportController { let plainMessage: MessageContentWrapper; let messageKey: CryptoSecretKey; + const recipients: CachedMessageRecipient[] = []; + let relationship; if (this.parent.identity.isMe(envelope.createdBy)) { if (!secretKey) { @@ -423,16 +483,50 @@ export class MessageController extends TransportController { } messageKey = secretKey; plainMessage = await this.decryptOwnEnvelope(envelope, secretKey); + + const pseudonym = await IdentityUtil.createAddress({ algorithm: 1, publicKey: CoreBuffer.fromUtf8("deleted identity") }, new URL(this.parent.config.baseUrl).hostname); + + for (const recipient of envelope.recipients) { + let relationship = await this.relationships.getRelationshipToIdentity(recipient.address); + if (relationship?.status === RelationshipStatus.Rejected || relationship?.status === RelationshipStatus.Revoked) { + // if the relationship is rejected or revoked, it should be handled like there is no relationship + relationship = undefined; + } + + recipients.push( + CachedMessageRecipient.from({ + // make sure to save the pseudonym instead of the real address if the relationship was removed + // in cases the backbone did not already process the relationship termination + address: relationship ? recipient.address : pseudonym, + encryptedKey: recipient.encryptedKey, + receivedAt: recipient.receivedAt, + receivedByDevice: recipient.receivedByDevice, + relationshipId: relationship?.id + }) + ); + } } else { relationship = await this.relationships.getActiveRelationshipToIdentity(envelope.createdBy); if (!relationship) { - throw CoreErrors.messages.noMatchingRelationship(envelope.createdBy.toString()); + throw CoreErrors.messages.missingOrInactiveRelationship(envelope.createdBy.toString()); } const [peerMessage, peerKey] = await this.decryptPeerEnvelope(envelope, relationship); plainMessage = peerMessage; messageKey = peerKey; + + // we don't care about other recipients in the envelope, because we do not need them + const currentIdentityAsRecipient = envelope.recipients.find((r) => this.parent.identity.isMe(r.address))!; + recipients.push( + CachedMessageRecipient.from({ + address: currentIdentityAsRecipient.address, + encryptedKey: currentIdentityAsRecipient.encryptedKey, + receivedAt: currentIdentityAsRecipient.receivedAt, + receivedByDevice: currentIdentityAsRecipient.receivedByDevice, + relationshipId: relationship.id + }) + ); } this.log.trace("Message is valid. Fetching attachments for message..."); @@ -449,7 +543,7 @@ export class MessageController extends TransportController { const cachedMessage = CachedMessage.from({ createdBy: envelope.createdBy, createdByDevice: envelope.createdByDevice, - recipients: envelope.recipients, + recipients, attachments: fileArray, content: plainMessage.content, createdAt: envelope.createdAt, diff --git a/packages/transport/src/modules/messages/local/CachedMessage.ts b/packages/transport/src/modules/messages/local/CachedMessage.ts index b9a94be0d..5c2408a4b 100644 --- a/packages/transport/src/modules/messages/local/CachedMessage.ts +++ b/packages/transport/src/modules/messages/local/CachedMessage.ts @@ -2,13 +2,13 @@ import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; import { CoreAddress, CoreId, CoreSerializable, ICoreAddress, ICoreId, ICoreSerializable } from "../../../core"; import { CoreDate, ICoreDate } from "../../../core/types/CoreDate"; -import { IMessageEnvelopeRecipient, MessageEnvelopeRecipient } from "../transmission/MessageEnvelopeRecipient"; +import { CachedMessageRecipient, ICachedMessageRecipient } from "./CachedMessageRecipient"; export interface ICachedMessage extends ICoreSerializable { createdBy: ICoreAddress; createdByDevice: ICoreId; - recipients: IMessageEnvelopeRecipient[]; + recipients: ICachedMessageRecipient[]; createdAt: ICoreDate; @@ -29,8 +29,8 @@ export class CachedMessage extends CoreSerializable implements ICachedMessage { public createdByDevice: CoreId; @validate() - @serialize({ type: MessageEnvelopeRecipient }) - public recipients: MessageEnvelopeRecipient[]; + @serialize({ type: CachedMessageRecipient }) + public recipients: CachedMessageRecipient[]; @validate() @serialize() diff --git a/packages/transport/src/modules/messages/local/CachedMessageRecipient.ts b/packages/transport/src/modules/messages/local/CachedMessageRecipient.ts new file mode 100644 index 000000000..3acb4f09e --- /dev/null +++ b/packages/transport/src/modules/messages/local/CachedMessageRecipient.ts @@ -0,0 +1,39 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CryptoCipher, ICryptoCipher } from "@nmshd/crypto"; +import { CoreDate, CoreId, CoreSerializable, ICoreDate, ICoreId, ICoreSerializable } from "../../../core"; +import { CoreAddress, ICoreAddress } from "../../../core/types/CoreAddress"; + +export interface ICachedMessageRecipient extends ICoreSerializable { + address: ICoreAddress; + encryptedKey: ICryptoCipher; + receivedAt?: ICoreDate; + receivedByDevice?: ICoreId; + relationshipId?: ICoreId; +} + +@type("CachedMessageRecipient") +export class CachedMessageRecipient extends CoreSerializable implements ICachedMessageRecipient { + @validate() + @serialize() + public address: CoreAddress; + + @validate() + @serialize() + public encryptedKey: CryptoCipher; + + @validate({ nullable: true }) + @serialize() + public receivedAt?: CoreDate; + + @validate({ nullable: true }) + @serialize() + public receivedByDevice?: CoreId; + + @validate({ nullable: true }) + @serialize() + public relationshipId?: CoreId; + + public static from(value: ICachedMessageRecipient): CachedMessageRecipient { + return this.fromAny(value); + } +} diff --git a/packages/transport/src/modules/messages/local/Message.ts b/packages/transport/src/modules/messages/local/Message.ts index b51f8992b..797129f7b 100644 --- a/packages/transport/src/modules/messages/local/Message.ts +++ b/packages/transport/src/modules/messages/local/Message.ts @@ -1,7 +1,7 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { CryptoSecretKey, ICryptoSecretKey } from "@nmshd/crypto"; import { nameof } from "ts-simple-nameof"; -import { CoreDate, CoreId, CoreSynchronizable, ICoreDate, ICoreId, ICoreSynchronizable } from "../../../core"; +import { CoreDate, CoreSynchronizable, ICoreDate, ICoreSynchronizable } from "../../../core"; import { CachedMessage, ICachedMessage } from "./CachedMessage"; export interface IMessage extends ICoreSynchronizable { @@ -11,19 +11,12 @@ export interface IMessage extends ICoreSynchronizable { cachedAt?: ICoreDate; metadata?: any; metadataModifiedAt?: ICoreDate; - relationshipIds: ICoreId[]; wasReadAt?: ICoreDate; } @type("Message") export class Message extends CoreSynchronizable implements IMessage { - public override readonly technicalProperties = [ - "@type", - "@context", - nameof((r) => r.secretKey), - nameof((r) => r.isOwn), - nameof((r) => r.relationshipIds) - ]; + public override readonly technicalProperties = ["@type", "@context", nameof((r) => r.secretKey), nameof((r) => r.isOwn)]; public override readonly metadataProperties = [nameof((r) => r.metadata), nameof((r) => r.metadataModifiedAt)]; @@ -53,10 +46,6 @@ export class Message extends CoreSynchronizable implements IMessage { @serialize() public metadataModifiedAt?: CoreDate; - @validate() - @serialize({ type: CoreId }) - public relationshipIds: CoreId[]; - @validate({ nullable: true }) @serialize() public wasReadAt?: CoreDate; diff --git a/packages/transport/src/modules/relationshipTemplates/RelationshipTemplateController.ts b/packages/transport/src/modules/relationshipTemplates/RelationshipTemplateController.ts index cf7e660ec..f5d36e5b8 100644 --- a/packages/transport/src/modules/relationshipTemplates/RelationshipTemplateController.ts +++ b/packages/transport/src/modules/relationshipTemplates/RelationshipTemplateController.ts @@ -6,6 +6,7 @@ import { DbCollectionName } from "../../core/DbCollectionName"; import { ControllerName, TransportController } from "../../core/TransportController"; import { PeerRelationshipTemplateLoadedEvent } from "../../events"; import { AccountController } from "../accounts/AccountController"; +import { Relationship } from "../relationships/local/Relationship"; import { RelationshipSecretController } from "../relationships/RelationshipSecretController"; import { SynchronizedCollection } from "../sync/SynchronizedCollection"; import { BackboneGetRelationshipTemplatesResponse } from "./backbone/BackboneGetRelationshipTemplates"; @@ -93,7 +94,9 @@ export class RelationshipTemplateController extends TransportController { } public async deleteRelationshipTemplate(template: RelationshipTemplate): Promise { - await this.client.deleteRelationshipTemplate(template.id.toString()); + const response = await this.client.deleteRelationshipTemplate(template.id.toString()); + if (response.isError) throw response.error; + await this.templates.delete(template); } @@ -123,12 +126,20 @@ export class RelationshipTemplateController extends TransportController { const decryptionPromises = backboneRelationships.map(async (t) => { const templateDoc = await this.templates.read(t.id); + if (!templateDoc) { + this._log.error( + `Template '${t.id}' not found in local database and the cache fetching was therefore skipped. This should not happen and might be a bug in the application logic.` + ); + return; + } + const template = RelationshipTemplate.from(templateDoc); return { id: CoreId.from(t.id), cache: await this.decryptRelationshipTemplate(t, template.secretKey) }; }); - return await Promise.all(decryptionPromises); + const caches = await Promise.all(decryptionPromises); + return caches.filter((c) => c !== undefined); } @log() @@ -246,4 +257,20 @@ export class RelationshipTemplateController extends TransportController { return relationshipTemplate; } + + public async cleanupTemplatesOfDecomposedRelationship(relationship: Relationship): Promise { + const templateOfRelationship = relationship.cache!.template; + + if (!templateOfRelationship.isOwn || templateOfRelationship.cache!.maxNumberOfAllocations === 1) { + await this.templates.delete(templateOfRelationship); + } + + const otherTemplatesOfPeer = await this.getRelationshipTemplates({ + "cache.createdBy": relationship.peer.address.toString() + }); + + for (const template of otherTemplatesOfPeer) { + await this.templates.delete(template); + } + } } diff --git a/packages/transport/src/modules/relationshipTemplates/transmission/RelationshipTemplatePublicKey.ts b/packages/transport/src/modules/relationshipTemplates/transmission/RelationshipTemplatePublicKey.ts index 8072e519d..0674d5b90 100644 --- a/packages/transport/src/modules/relationshipTemplates/transmission/RelationshipTemplatePublicKey.ts +++ b/packages/transport/src/modules/relationshipTemplates/transmission/RelationshipTemplatePublicKey.ts @@ -5,6 +5,7 @@ import { CoreId, ICoreId } from "../../../core"; export interface IRelationshipTemplatePublicKey extends ICryptoExchangePublicKey { id: ICoreId; } + export interface IRelationshipTemplatePublicKeySerialized extends ICryptoExchangePublicKeySerialized, ISerialized { id: string; } diff --git a/packages/transport/src/modules/relationships/RelationshipSecretController.ts b/packages/transport/src/modules/relationships/RelationshipSecretController.ts index 4ca286da7..094940416 100644 --- a/packages/transport/src/modules/relationships/RelationshipSecretController.ts +++ b/packages/transport/src/modules/relationships/RelationshipSecretController.ts @@ -17,7 +17,6 @@ import { CoreErrors } from "../../core/CoreErrors"; import { CoreUtil } from "../../core/CoreUtil"; import { TransportIds } from "../../core/TransportIds"; import { AccountController } from "../accounts/AccountController"; -import { Identity } from "../accounts/data/Identity"; import { CachedRelationshipTemplate } from "../relationshipTemplates/local/CachedRelationshipTemplate"; import { RelationshipTemplatePublicKey } from "../relationshipTemplates/transmission/RelationshipTemplatePublicKey"; import { SecretContainerCipher } from "../secrets/data/SecretContainerCipher"; @@ -88,7 +87,7 @@ export class RelationshipSecretController extends SecretController { } @log() - public async getPublicResponse(relationshipSecretId: CoreId): Promise { + public async getPublicCreationResponseContentCrypto(relationshipSecretId: CoreId): Promise { const secret = await this.loadActiveSecretByName(relationshipSecretId.toString()); if (!secret) { throw CoreErrors.general.recordNotFound(CryptoRelationshipSecrets, relationshipSecretId.toString()); @@ -116,8 +115,8 @@ export class RelationshipSecretController extends SecretController { return container; } - public async deleteSecretForRequest(peerIdentity: Identity): Promise { - const secret = await this.loadActiveSecretByName(`request_to_${peerIdentity.address}`); + public async deleteSecretForRelationship(relationshipSecretId: CoreId): Promise { + const secret = await this.loadActiveSecretByName(relationshipSecretId.toString()); if (!secret) { return false; } @@ -134,7 +133,7 @@ export class RelationshipSecretController extends SecretController { } @log() - public async encryptRequest(relationshipSecretId: CoreId, content: Serializable | string | CoreBuffer): Promise { + public async encryptCreationContent(relationshipSecretId: CoreId, content: Serializable | string | CoreBuffer): Promise { const buffer = CoreUtil.toBuffer(content); const secrets = await this.getSecret(relationshipSecretId); @@ -158,7 +157,7 @@ export class RelationshipSecretController extends SecretController { } @log() - public async decryptRequest(relationshipSecretId: CoreId, cipher: CryptoCipher): Promise { + public async decryptCreationContent(relationshipSecretId: CoreId, cipher: CryptoCipher): Promise { const secrets = await this.getSecret(relationshipSecretId); if (!(secrets instanceof CryptoRelationshipRequestSecrets) && !(secrets instanceof CryptoRelationshipSecrets)) { diff --git a/packages/transport/src/modules/relationships/RelationshipsController.ts b/packages/transport/src/modules/relationships/RelationshipsController.ts index f908d8f07..149fd69f4 100644 --- a/packages/transport/src/modules/relationships/RelationshipsController.ts +++ b/packages/transport/src/modules/relationships/RelationshipsController.ts @@ -2,34 +2,31 @@ import { ISerializable } from "@js-soft/ts-serval"; import { log } from "@js-soft/ts-utils"; import { CoreBuffer, CryptoSignature } from "@nmshd/crypto"; import { nameof } from "ts-simple-nameof"; -import { ControllerName, CoreAddress, CoreCrypto, CoreDate, CoreId, ICoreSerializable, TransportController, TransportError } from "../../core"; +import { ControllerName, CoreAddress, CoreCrypto, CoreDate, CoreId, TransportController, TransportError } from "../../core"; import { CoreErrors } from "../../core/CoreErrors"; import { CoreUtil } from "../../core/CoreUtil"; import { DbCollectionName } from "../../core/DbCollectionName"; import { TransportIds } from "../../core/TransportIds"; -import { RelationshipChangedEvent } from "../../events"; +import { RelationshipChangedEvent, RelationshipDecomposedBySelfEvent, RelationshipReactivationCompletedEvent, RelationshipReactivationRequestedEvent } from "../../events"; import { AccountController } from "../accounts/AccountController"; -import { Identity } from "../accounts/data/Identity"; import { RelationshipTemplate } from "../relationshipTemplates/local/RelationshipTemplate"; import { SynchronizedCollection } from "../sync/SynchronizedCollection"; -import { BackboneGetRelationshipsResponse } from "./backbone/BackboneGetRelationships"; -import { BackboneGetRelationshipsChangesResponse, BackboneGetRelationshipsChangesSingleChangeResponse } from "./backbone/BackboneGetRelationshipsChanges"; +import { RelationshipSecretController } from "./RelationshipSecretController"; +import { BackbonePutRelationshipsResponse } from "./backbone/BackbonePutRelationship"; +import { BackboneRelationship } from "./backbone/BackboneRelationship"; import { RelationshipClient } from "./backbone/RelationshipClient"; import { CachedRelationship } from "./local/CachedRelationship"; import { Relationship } from "./local/Relationship"; +import { RelationshipAuditLog } from "./local/RelationshipAuditLog"; import { ISendRelationshipParameters, SendRelationshipParameters } from "./local/SendRelationshipParameters"; -import { RelationshipSecretController } from "./RelationshipSecretController"; -import { RelationshipChange } from "./transmission/changes/RelationshipChange"; -import { RelationshipChangeResponse } from "./transmission/changes/RelationshipChangeResponse"; -import { RelationshipChangeStatus } from "./transmission/changes/RelationshipChangeStatus"; -import { RelationshipChangeType } from "./transmission/changes/RelationshipChangeType"; +import { RelationshipAuditLogEntryReason } from "./transmission/RelationshipAuditLog"; import { RelationshipStatus } from "./transmission/RelationshipStatus"; -import { RelationshipCreationChangeRequestCipher } from "./transmission/requests/RelationshipCreationChangeRequestCipher"; -import { RelationshipCreationChangeRequestContentWrapper } from "./transmission/requests/RelationshipCreationChangeRequestContentWrapper"; -import { RelationshipCreationChangeRequestSigned } from "./transmission/requests/RelationshipCreationChangeRequestSigned"; -import { RelationshipCreationChangeResponseCipher } from "./transmission/responses/RelationshipCreationChangeResponseCipher"; -import { RelationshipCreationChangeResponseContentWrapper } from "./transmission/responses/RelationshipCreationChangeResponseContentWrapper"; -import { RelationshipCreationChangeResponseSigned } from "./transmission/responses/RelationshipCreationChangeResponseSigned"; +import { RelationshipCreationContentCipher } from "./transmission/requests/RelationshipCreationContentCipher"; +import { RelationshipCreationContentSigned } from "./transmission/requests/RelationshipCreationContentSigned"; +import { RelationshipCreationContentWrapper } from "./transmission/requests/RelationshipCreationContentWrapper"; +import { RelationshipCreationResponseContentCipher } from "./transmission/responses/RelationshipCreationResponseContentCipher"; +import { RelationshipCreationResponseContentSigned } from "./transmission/responses/RelationshipCreationResponseContentSigned"; +import { RelationshipCreationResponseContentWrapper } from "./transmission/responses/RelationshipCreationResponseContentWrapper"; export class RelationshipsController extends TransportController { private client: RelationshipClient; @@ -52,7 +49,9 @@ export class RelationshipsController extends TransportController { public async getRelationships(query?: any): Promise { const relationshipDocs = await this.relationships.find(query); - return this.parseArray(relationshipDocs, Relationship); + const relationships = this.parseArray(relationshipDocs, Relationship); + + return relationships; } public async updateCache(ids: string[]): Promise { @@ -63,7 +62,7 @@ export class RelationshipsController extends TransportController { const resultItems = (await this.client.getRelationships({ ids })).value; const promises = []; for await (const resultItem of resultItems) { - promises.push(this.updateCacheOfExistingRelationshipInDb(resultItem.id, resultItem)); + promises.push(this.updateExistingRelationshipInDb(resultItem.id, resultItem)); } return await Promise.all(promises); } @@ -75,6 +74,13 @@ export class RelationshipsController extends TransportController { const decryptionPromises = backboneRelationships.map(async (r) => { const relationshipDoc = await this.relationships.read(r.id); + if (!relationshipDoc) { + this._log.error( + `Relationship '${r.id}' not found in local database and the cache fetching was therefore skipped. This should not happen and might be a bug in the application logic.` + ); + return; + } + const relationship = Relationship.from(relationshipDoc); return { @@ -83,17 +89,19 @@ export class RelationshipsController extends TransportController { }; }); - return await Promise.all(decryptionPromises); + const caches = await Promise.all(decryptionPromises); + return caches.filter((c) => c !== undefined); } @log() - private async updateCacheOfExistingRelationshipInDb(id: string, response?: BackboneGetRelationshipsResponse) { + private async updateExistingRelationshipInDb(id: string, response: BackboneRelationship) { const relationshipDoc = await this.relationships.read(id); if (!relationshipDoc) throw CoreErrors.general.recordNotFound(Relationship, id); const relationship = Relationship.from(relationshipDoc); await this.updateCacheOfRelationship(relationship, response); + relationship.status = response.status; await this.relationships.update(relationshipDoc, relationship); return relationship; } @@ -101,21 +109,16 @@ export class RelationshipsController extends TransportController { public async getRelationshipToIdentity(address: CoreAddress, status?: RelationshipStatus): Promise { const query: any = { peerAddress: address.toString() }; if (status) query[`${nameof((r) => r.status)}`] = status; - let relationshipDoc = await this.relationships.findOne(query); + const relationships = await this.relationships.find(query); - if (!relationshipDoc) { - // If we don't find the relationship by peerAddress, we have to check again by peer.address - // as the Relationship could have been created before the peerAddress was introduced - const query = { [`${nameof((r) => r.peer)}.${nameof((r) => r.address)}`]: address.toString() }; - if (status) query[`${nameof((r) => r.status)}`] = status; - relationshipDoc = await this.relationships.findOne(query); - } + if (relationships.length === 0) return undefined; + if (relationships.length === 1) return Relationship.from(relationships[0]); - if (!relationshipDoc) { - return; - } + const newestRelationship = relationships.reduce((prev, current) => { + return prev.createdAt > current.createdAt ? prev : current; + }); - return Relationship.from(relationshipDoc); + return Relationship.from(newestRelationship); } public async getActiveRelationshipToIdentity(address: CoreAddress): Promise { @@ -128,7 +131,9 @@ export class RelationshipsController extends TransportController { return; } - return Relationship.from(relationshipDoc); + const relationship = Relationship.from(relationshipDoc); + + return relationship; } public async sign(relationship: Relationship, content: CoreBuffer): Promise { @@ -152,10 +157,10 @@ export class RelationshipsController extends TransportController { const secretId = await TransportIds.relationshipSecret.generate(); - const { requestCipher, requestContent } = await this.prepareRequest(secretId, template, parameters.content); + const creationContentCipher = await this.prepareCreationContent(secretId, template, parameters.creationContent); const result = await this.client.createRelationship({ - content: requestCipher.toBase64(), + creationContent: creationContentCipher.toBase64(), relationshipTemplateId: template.id.toString() }); @@ -168,13 +173,7 @@ export class RelationshipsController extends TransportController { const backboneResponse = result.value; - const newRelationship = Relationship.fromRequestSent( - CoreId.from(backboneResponse.id), - template, - template.cache.identity, - RelationshipChange.fromBackbone(backboneResponse.changes[0], requestContent.content), - secretId - ); + const newRelationship = Relationship.fromBackboneAndCreationContent(backboneResponse, template, template.cache.identity, parameters.creationContent, secretId); await this.relationships.create(newRelationship); @@ -197,330 +196,325 @@ export class RelationshipsController extends TransportController { return relationship; } - public async acceptChange(change: RelationshipChange, content?: ICoreSerializable): Promise { - return await this.completeChange(RelationshipChangeStatus.Accepted, change, content); - } + public async accept(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Pending); - public async rejectChange(change: RelationshipChange, content?: ICoreSerializable): Promise { - return await this.completeChange(RelationshipChangeStatus.Rejected, change, content); + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (!lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.operationOnlyAllowedForPeer(`Only your peer can accept the relationship ${relationshipId.toString()}`); + } + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.AcceptanceOfCreation, relationshipId); } - public async revokeChange(change: RelationshipChange, content?: ICoreSerializable): Promise { - return await this.completeChange(RelationshipChangeStatus.Revoked, change, content); - } + public async reject(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Pending); - private async updateCacheOfRelationship(relationship: Relationship, response?: BackboneGetRelationshipsResponse) { - if (!response) { - response = (await this.client.getRelationship(relationship.id.toString())).value; + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (!lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.operationOnlyAllowedForPeer(`Only your peer can reject the relationship ${relationshipId.toString()}. Revoke the relationship instead.`); } + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.RejectionOfCreation, relationshipId); + } - const cachedRelationship = await this.decryptRelationship(response, relationship.relationshipSecretId); + public async revoke(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Pending); - relationship.setCache(cachedRelationship); + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.operationOnlyAllowedForPeer(`Only your peer can revoke the relationship ${relationshipId.toString()}. Reject the relationship instead.`); + } + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.RevocationOfCreation, relationshipId); } - private async decryptRelationship(response: BackboneGetRelationshipsResponse, relationshipSecretId: CoreId) { - const templateId = CoreId.from(response.relationshipTemplateId); + public async terminate(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Active); - this._log.trace(`Parsing relationship template ${templateId} for ${response.id}...`); - const template = await this.parent.relationshipTemplates.getRelationshipTemplate(templateId); - if (!template) { - throw CoreErrors.general.recordNotFound(RelationshipTemplate, templateId.toString()); - } + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.Termination, relationshipId); + } - this._log.trace(`Parsing relationship changes of ${response.id}...`); + public async requestReactivation(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Terminated); - const changesPromises = []; - for (const change of response.changes) { - switch (change.type) { - case RelationshipChangeType.Creation: - changesPromises.push(this.parseCreationChange(change, relationshipSecretId, templateId)); - break; - default: - break; + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (lastAuditLogEntry.reason === RelationshipAuditLogEntryReason.ReactivationRequested) { + if (lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.reactivationAlreadyRequested( + `Your peer has already requested the reactivation of the relationship ${relationshipId.toString()}. You can accept the reactivation instead.` + ); } + throw CoreErrors.relationships.reactivationAlreadyRequested(`You have already requested the reactivation of the relationship ${relationshipId.toString()}.`); } - const changes = await Promise.all(changesPromises); + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.ReactivationRequested, relationshipId); + } - const cachedRelationship = CachedRelationship.from({ - changes: changes, - template: template - }); + public async rejectReactivation(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Terminated); - return cachedRelationship; + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (lastAuditLogEntry.reason !== RelationshipAuditLogEntryReason.ReactivationRequested) { + throw CoreErrors.relationships.reactivationNotRequested(relationshipId.toString()); + } + + if (!lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.operationOnlyAllowedForPeer( + `Only your peer can reject the reactivation of the relationship ${relationshipId.toString()}. Revoke the relationship reactivation instead.` + ); + } + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.RejectionOfReactivation, relationshipId); } - private async prepareRequest( - relationshipSecretId: CoreId, - template: RelationshipTemplate, - content: ISerializable - ): Promise<{ - requestCipher: RelationshipCreationChangeRequestCipher; - requestContent: RelationshipCreationChangeRequestContentWrapper; - }> { - if (!template.cache) { - throw this.newCacheEmptyError(RelationshipTemplate, template.id.toString()); + public async revokeReactivation(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Terminated); + + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (lastAuditLogEntry.reason !== RelationshipAuditLogEntryReason.ReactivationRequested) { + throw CoreErrors.relationships.reactivationNotRequested(relationshipId.toString()); + } + if (lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.operationOnlyAllowedForPeer( + `Only your peer can revoke the reactivation of the relationship ${relationshipId.toString()}. Reject the relationship reactivation instead.` + ); } + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.RevocationOfReactivation, relationshipId); + } - const requestPublic = await this.secrets.createRequestorSecrets(template.cache, relationshipSecretId); + public async acceptReactivation(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Terminated); - const requestContent = RelationshipCreationChangeRequestContentWrapper.from({ - content: content, - identity: this.parent.identity.identity, - templateId: template.id - }); - const serializedRequest = requestContent.serialize(); - const buffer = CoreUtil.toBuffer(serializedRequest); + const lastAuditLogEntry = relationship.cache.auditLog[relationship.cache.auditLog.length - 1]; + if (lastAuditLogEntry.reason !== RelationshipAuditLogEntryReason.ReactivationRequested) { + throw CoreErrors.relationships.reactivationNotRequested(relationshipId.toString()); + } + if (!lastAuditLogEntry.createdBy.equals(relationship.peer.address)) { + throw CoreErrors.relationships.operationOnlyAllowedForPeer(`Only your peer can accept the reactivation of the relationship ${relationshipId.toString()}.`); + } - const [deviceSignature, relationshipSignature] = await Promise.all([this.parent.activeDevice.sign(buffer), this.secrets.sign(relationshipSecretId, buffer)]); + return await this.completeOperationWithBackboneCall(RelationshipAuditLogEntryReason.AcceptanceOfReactivation, relationshipId); + } - const signedRequest = RelationshipCreationChangeRequestSigned.from({ - serializedRequest: serializedRequest, - deviceSignature: deviceSignature, - relationshipSignature: relationshipSignature - }); + public async decompose(relationshipId: CoreId): Promise { + const relationship = await this.getRelationshipWithCache(relationshipId); + this.assertRelationshipStatus(relationship, RelationshipStatus.Terminated, RelationshipStatus.DeletionProposed); - const cipher = await this.secrets.encryptRequest(relationshipSecretId, signedRequest); - const requestCipher = RelationshipCreationChangeRequestCipher.from({ - cipher: cipher, - publicRequestCrypto: requestPublic - }); + const result = await this.client.decomposeRelationship(relationshipId.toString()); + if (result.isError) throw result.error; - return { requestCipher, requestContent }; - } + const isSecretDeletionSuccessful = await this.secrets.deleteSecretForRelationship(relationship.relationshipSecretId); + if (!isSecretDeletionSuccessful) { + this._log.error("Decomposition failed to delete secrets"); + } + await this.relationships.delete({ id: relationshipId }); - public async applyChangeById(changeId: string): Promise { - const relationshipChange = (await this.client.getRelationshipChange(changeId.toString())).value; - return await this.applyChange(relationshipChange); + this.eventBus.publish(new RelationshipDecomposedBySelfEvent(this.parent.identity.address.toString(), { relationshipId })); } - @log() - public async applyChange(change: BackboneGetRelationshipsChangesResponse): Promise { - switch (change.type) { - case RelationshipChangeType.Creation: - return await this.applyCreationChange(change); - case RelationshipChangeType.Termination: - case RelationshipChangeType.TerminationCancellation: - default: - throw CoreErrors.general.notSupported(); - } + private async getRelationshipWithCache(id: CoreId): Promise { + const relationship = await this.getRelationship(id); + if (!relationship) throw CoreErrors.general.recordNotFound(Relationship, id.toString()); + if (!relationship.cache) await this.updateCacheOfRelationship(relationship); + if (!relationship.cache) throw this.newCacheEmptyError(Relationship, id.toString()); + + return relationship as Relationship & { cache: CachedRelationship }; } - private async applyCreationChange(change: BackboneGetRelationshipsChangesResponse): Promise { - const relationshipDoc = await this.relationships.read(change.relationshipId); - if (relationshipDoc) { - // Incoming change from sync might still have an empty response - // This could happen if we've processed the change before (duplicate) - if (change.response) { - return await this.updatePendingRelationshipWithPeerResponse(relationshipDoc, change); - } + private assertRelationshipStatus(relationship: Relationship, ...status: RelationshipStatus[]) { + if (status.includes(relationship.status)) return; - // If we have a relationship already but no response, do nothing - return undefined; + throw CoreErrors.relationships.wrongRelationshipStatus(relationship.id.toString(), relationship.status); + } + + private async updateCacheOfRelationship(relationship: Relationship, response?: BackboneRelationship) { + if (!response) { + response = (await this.client.getRelationship(relationship.id.toString())).value; } - const newRelationship = await this.createNewRelationshipByIncomingCreationChange(change); + const cachedRelationship = await this.decryptRelationship(response, relationship.relationshipSecretId); - if (change.response) { - // The request was revoked before we fetched the creation, - // thus the creation and revoke change is one - const relationshipDoc = await this.relationships.read(change.relationshipId); - return await this.updatePendingRelationshipWithPeerResponse(relationshipDoc, change); - } - return newRelationship; + relationship.setCache(cachedRelationship); } - @log() - private async parseCreationChange(change: BackboneGetRelationshipsChangesResponse, relationshipSecretId: CoreId, templateId: CoreId) { - if (change.type !== RelationshipChangeType.Creation) this.throwWrongChangeType(change.type); - - const promises: any[] = []; - promises.push(this.decryptCreationChangeRequest(change.request, relationshipSecretId, templateId)); + private async decryptRelationship(response: BackboneRelationship, relationshipSecretId: CoreId) { + if (!response.creationContent) throw new TransportError("Creation content is missing"); - const hasRelationshipSecret = await this.secrets.hasCryptoRelationshipSecrets(relationshipSecretId); + const templateId = CoreId.from(response.relationshipTemplateId); - if (change.response && hasRelationshipSecret) { - promises.push(this.decryptCreationChangeResponse(change, relationshipSecretId)); + this._log.trace(`Parsing relationship template ${templateId} for ${response.id}...`); + const template = await this.parent.relationshipTemplates.getRelationshipTemplate(templateId); + if (!template) { + throw CoreErrors.general.recordNotFound(RelationshipTemplate, templateId.toString()); } - const [requestContent, responseContent] = await Promise.all(promises); + this._log.trace(`Parsing relationship creation content of ${response.id}...`); + + const creationContent = await this.decryptCreationContent(response.creationContent, CoreAddress.from(response.from), relationshipSecretId); + + const cachedRelationship = CachedRelationship.from({ + creationContent: creationContent.content, + template, + auditLog: RelationshipAuditLog.fromBackboneAuditLog(response.auditLog) + }); - const creationChange = RelationshipChange.fromBackbone(change, requestContent.content, responseContent?.content); - return creationChange; + return cachedRelationship; } - @log() - private async decryptCreationChangeRequest( - change: BackboneGetRelationshipsChangesSingleChangeResponse, - secretId: CoreId, - templateId: CoreId - ): Promise { - if (!change.content) throw this.newEmptyOrInvalidContentError(); + private async prepareCreationContent(relationshipSecretId: CoreId, template: RelationshipTemplate, content: ISerializable): Promise { + if (!template.cache) { + throw this.newCacheEmptyError(RelationshipTemplate, template.id.toString()); + } - const isOwnChange = this.parent.identity.isMe(CoreAddress.from(change.createdBy)); + const publicCreationContentCrypto = await this.secrets.createRequestorSecrets(template.cache, relationshipSecretId); - const requestCipher = RelationshipCreationChangeRequestCipher.fromBase64(change.content); - const signedRequestBuffer = await this.secrets.decryptRequest(secretId, requestCipher.cipher); - const signedRequest = RelationshipCreationChangeRequestSigned.deserialize(signedRequestBuffer.toUtf8()); + const creationContent: RelationshipCreationContentWrapper = RelationshipCreationContentWrapper.from({ + content, + identity: this.parent.identity.identity, + templateId: template.id + }); + const serializedCreationContent = creationContent.serialize(); + const buffer = CoreUtil.toBuffer(serializedCreationContent); - let relationshipSignatureValid; - if (isOwnChange) { - relationshipSignatureValid = await this.secrets.verifyOwn(secretId, CoreBuffer.fromUtf8(signedRequest.serializedRequest), signedRequest.relationshipSignature); - } else { - relationshipSignatureValid = await this.secrets.verifyPeer(secretId, CoreBuffer.fromUtf8(signedRequest.serializedRequest), signedRequest.relationshipSignature); - } + const [deviceSignature, relationshipSignature] = await Promise.all([this.parent.activeDevice.sign(buffer), this.secrets.sign(relationshipSecretId, buffer)]); - if (!relationshipSignatureValid) { - throw CoreErrors.general.signatureNotValid("relationshipRequest"); - } + const signedCreationContent = RelationshipCreationContentSigned.from({ + serializedCreationContent, + deviceSignature, + relationshipSignature + }); - const requestContent = RelationshipCreationChangeRequestContentWrapper.deserialize(signedRequest.serializedRequest); - if (!requestContent.templateId.equals(templateId)) { - throw new TransportError("The relationship request contains a wrong template id."); - } + const cipher = await this.secrets.encryptCreationContent(relationshipSecretId, signedCreationContent); + const creationContentCipher = RelationshipCreationContentCipher.from({ + cipher, + publicCreationContentCrypto + }); - return requestContent; + return creationContentCipher; } @log() - private async decryptCreationChangeResponse( - change: BackboneGetRelationshipsChangesResponse, - relationshipSecretId: CoreId - ): Promise { - if (!change.response) throw this.newChangeResponseMissingError(change.id); + private async updateRelationshipWithPeerResponse(relationshipDoc: any): Promise { + const relationship = Relationship.from(relationshipDoc); + + const backboneRelationship = (await this.client.getRelationship(relationship.id.toString())).value; - if (change.type !== RelationshipChangeType.Creation) this.throwWrongChangeType(change.type); + if (!(await this.secrets.hasCryptoRelationshipSecrets(relationship.relationshipSecretId)) && backboneRelationship.creationResponseContent) { + const creationResponseContent = backboneRelationship.creationResponseContent; + const cipher = RelationshipCreationResponseContentCipher.fromBase64(creationResponseContent); - if (!change.response.content) { - throw this.newEmptyOrInvalidContentError(change); + await this.secrets.convertSecrets(relationship.relationshipSecretId, cipher.publicCreationResponseContentCrypto); } + relationship.cache!.auditLog = RelationshipAuditLog.fromBackboneAuditLog(backboneRelationship.auditLog); + relationship.status = backboneRelationship.status; - const isOwnChange = this.parent.identity.isMe(CoreAddress.from(change.response.createdBy)); + await this.relationships.update(relationshipDoc, relationship); + return relationship; + } - const cipher = RelationshipCreationChangeResponseCipher.fromBase64(change.response.content); - let signedResponseBuffer; - if (change.status !== RelationshipChangeStatus.Revoked) { - if (isOwnChange) { - signedResponseBuffer = await this.secrets.decryptOwn(relationshipSecretId, cipher.cipher); - } else { - signedResponseBuffer = await this.secrets.decryptPeer(relationshipSecretId, cipher.cipher, true); - } - } else { - signedResponseBuffer = await this.secrets.decryptRequest(relationshipSecretId, cipher.cipher); - } + @log() + private async decryptCreationContent(backboneCreationContent: string, creationContentCreator: CoreAddress, secretId: CoreId): Promise { + const isOwnContent = this.parent.identity.isMe(creationContentCreator); + + const creationContentCipher = RelationshipCreationContentCipher.fromBase64(backboneCreationContent); + const signedCreationContentBuffer = await this.secrets.decryptCreationContent(secretId, creationContentCipher.cipher); + const signedCreationContent = RelationshipCreationContentSigned.deserialize(signedCreationContentBuffer.toUtf8()); - const signedResponse = RelationshipCreationChangeResponseSigned.deserialize(signedResponseBuffer.toUtf8()); let relationshipSignatureValid; - if (isOwnChange) { + if (isOwnContent) { relationshipSignatureValid = await this.secrets.verifyOwn( - relationshipSecretId, - CoreBuffer.fromUtf8(signedResponse.serializedResponse), - signedResponse.relationshipSignature + secretId, + CoreBuffer.fromUtf8(signedCreationContent.serializedCreationContent), + signedCreationContent.relationshipSignature ); } else { relationshipSignatureValid = await this.secrets.verifyPeer( - relationshipSecretId, - CoreBuffer.fromUtf8(signedResponse.serializedResponse), - signedResponse.relationshipSignature + secretId, + CoreBuffer.fromUtf8(signedCreationContent.serializedCreationContent), + signedCreationContent.relationshipSignature ); } if (!relationshipSignatureValid) { - throw CoreErrors.general.signatureNotValid("relationshipResponse"); + throw CoreErrors.general.signatureNotValid("relationshipCreationContent"); } - const responseContent = RelationshipCreationChangeResponseContentWrapper.deserialize(signedResponse.serializedResponse); + const creationContent = RelationshipCreationContentWrapper.deserialize(signedCreationContent.serializedCreationContent); - if (!responseContent.relationshipId.equals(change.relationshipId)) { - throw new TransportError("The relationship response contains a wrong relationship id."); - } - return responseContent; + return creationContent; } @log() - private async updatePendingRelationshipWithPeerResponse(relationshipDoc: any, change: BackboneGetRelationshipsChangesResponse): Promise { - const relationship = Relationship.from(relationshipDoc); + private async createNewRelationshipByIncomingCreation(relationshipId: string): Promise { + const backboneRelationship = (await this.client.getRelationship(relationshipId)).value; + if (!backboneRelationship.creationContent) throw new TransportError("Creation content is missing"); - if (relationship.status !== RelationshipStatus.Pending) { - this.log.debug("Trying to update non-pending relationship with creation change", change); - return; - } + const templateId = CoreId.from(backboneRelationship.relationshipTemplateId); + const template = await this.parent.relationshipTemplates.getRelationshipTemplate(templateId); - if (!relationship.cache) { - await this.updateCacheOfRelationship(relationship, undefined); - } + if (!template) throw CoreErrors.general.recordNotFound(RelationshipTemplate, templateId.toString()); + if (!template.cache) throw this.newCacheEmptyError(RelationshipTemplate, template.id.toString()); - if (!change.response) throw this.newChangeResponseMissingError(change.id); + const secretId = await TransportIds.relationshipSecret.generate(); + const creationContentCipher = RelationshipCreationContentCipher.fromBase64(backboneRelationship.creationContent); + await this.secrets.createTemplatorSecrets(secretId, template.cache, creationContentCipher.publicCreationContentCrypto); - if (!change.response.content) { - throw this.newEmptyOrInvalidContentError(change); - } + const creationContent = await this.decryptCreationContent(backboneRelationship.creationContent, CoreAddress.from(backboneRelationship.from), secretId); + const relationship = Relationship.fromBackboneAndCreationContent(backboneRelationship, template, creationContent.identity, creationContent.content, secretId); - const cipher = RelationshipCreationChangeResponseCipher.fromBase64(change.response.content); + await this.relationships.create(relationship); + return relationship; + } - if (change.status !== RelationshipChangeStatus.Revoked) { - if (!cipher.publicResponseCrypto) { - throw new TransportError("The response crypto is missing."); + public async applyRelationshipChangedEvent(relationshipId: string): Promise { + let relationshipDoc = await this.relationships.read(relationshipId); + if (!relationshipDoc) { + const newRelationship = await this.createNewRelationshipByIncomingCreation(relationshipId); + if (newRelationship.status === RelationshipStatus.Pending) { + return newRelationship; } - await this.secrets.convertSecrets(relationship.relationshipSecretId, cipher.publicResponseCrypto); - } - - const responseContent = await this.decryptCreationChangeResponse(change, relationship.relationshipSecretId); - - const response = RelationshipChangeResponse.fromBackbone(change.response, responseContent.content); - - if (!relationship.cache) { - throw this.newCacheEmptyError(Relationship, relationship.id.toString()); - } - relationship.cache.changes[0].status = change.status; - switch (change.status) { - case RelationshipChangeStatus.Accepted: - relationship.toActive(response); - break; - case RelationshipChangeStatus.Rejected: - relationship.toRejected(response); - break; - case RelationshipChangeStatus.Revoked: - relationship.toRevoked(response); - break; - default: - throw CoreErrors.general.notSupported(); + // this path is for a revocation that is processed before its corresponding creation + relationshipDoc = await this.relationships.read(relationshipId); } - await this.relationships.update(relationshipDoc, relationship); - return relationship; + return await this.updateRelationshipWithPeerResponse(relationshipDoc); } - @log() - private async createNewRelationshipByIncomingCreationChange(change: BackboneGetRelationshipsChangesResponse): Promise { - const backboneRelationship = (await this.client.getRelationship(change.relationshipId)).value; + private async prepareCreationResponseContent(relationship: Relationship) { + const publicCreationResponseContentCrypto = await this.secrets.getPublicCreationResponseContentCrypto(relationship.relationshipSecretId); - const templateId = CoreId.from(backboneRelationship.relationshipTemplateId); - const template = await this.parent.relationshipTemplates.getRelationshipTemplate(templateId); + const creationResponseContent = RelationshipCreationResponseContentWrapper.from({ relationshipId: relationship.id }); - if (!template) throw CoreErrors.general.recordNotFound(RelationshipTemplate, templateId.toString()); - if (!template.cache) throw this.newCacheEmptyError(RelationshipTemplate, template.id.toString()); - if (!change.request.content) throw this.newEmptyOrInvalidContentError(change); + const serializedCreationResponseContent = creationResponseContent.serialize(); + const buffer = CoreUtil.toBuffer(serializedCreationResponseContent); - const secretId = await TransportIds.relationshipSecret.generate(); - const requestCipher = RelationshipCreationChangeRequestCipher.fromBase64(change.request.content); - await this.secrets.createTemplatorSecrets(secretId, template.cache, requestCipher.publicRequestCrypto); + const [deviceSignature, relationshipSignature] = await Promise.all([this.parent.activeDevice.sign(buffer), this.secrets.sign(relationship.relationshipSecretId, buffer)]); - const requestContent = await this.decryptCreationChangeRequest(backboneRelationship.changes[0].request, secretId, templateId); - const relationshipChange = RelationshipChange.fromBackbone(change, requestContent.content); + const signedCreationResponseContent = RelationshipCreationResponseContentSigned.from({ + serializedCreationResponseContent, + deviceSignature, + relationshipSignature + }); - const relationship = Relationship.fromCreationChangeReceived(backboneRelationship, template, requestContent.identity, relationshipChange, secretId); + const cipher = await this.secrets.encrypt(relationship.relationshipSecretId, signedCreationResponseContent); + const creationResponseContentCipher = RelationshipCreationResponseContentCipher.from({ + cipher, + publicCreationResponseContentCrypto + }); - await this.relationships.create(relationship); - return relationship; + return creationResponseContentCipher.toBase64(); } @log() - private async completeChange(targetStatus: RelationshipChangeStatus, change: RelationshipChange, content?: ISerializable) { - const relationshipDoc = await this.relationships.read(change.relationshipId.toString()); + private async completeOperationWithBackboneCall(operation: RelationshipAuditLogEntryReason, id: CoreId) { + const relationshipDoc = await this.relationships.read(id.toString()); if (!relationshipDoc) { - throw CoreErrors.general.recordNotFound(Relationship, change.relationshipId.toString()); + throw CoreErrors.general.recordNotFound(Relationship, id.toString()); } const relationship = Relationship.from(relationshipDoc); @@ -530,118 +524,68 @@ export class RelationshipsController extends TransportController { } if (!relationship.cache) { - throw this.newCacheEmptyError(Relationship, relationship.id.toString()); + throw this.newCacheEmptyError(Relationship, id.toString()); } - const queriedChange = relationship.cache.changes.find((r) => r.id.toString() === change.id.toString()); - if (!queriedChange) { - throw CoreErrors.general.recordNotFound(RelationshipChange, change.id.toString()); - } + let backboneResponse: BackbonePutRelationshipsResponse; + switch (operation) { + case RelationshipAuditLogEntryReason.AcceptanceOfCreation: + const encryptedContent = await this.prepareCreationResponseContent(relationship); - if (queriedChange.status !== RelationshipChangeStatus.Pending) { - throw CoreErrors.relationships.wrongChangeStatus(queriedChange.status); - } + backboneResponse = (await this.client.acceptRelationship(id.toString(), { creationResponseContent: encryptedContent })).value; + break; - let encryptedContent; - if (content) { - encryptedContent = - targetStatus === RelationshipChangeStatus.Revoked - ? await this.encryptRevokeContent(relationship, content) - : await this.encryptAcceptRejectContent(relationship, content); - } + case RelationshipAuditLogEntryReason.RejectionOfCreation: + backboneResponse = (await this.client.rejectRelationship(id.toString())).value; + break; + + case RelationshipAuditLogEntryReason.RevocationOfCreation: + backboneResponse = (await this.client.revokeRelationship(id.toString())).value; + break; + + case RelationshipAuditLogEntryReason.Termination: + backboneResponse = (await this.client.terminateRelationship(id.toString())).value; + break; + + case RelationshipAuditLogEntryReason.ReactivationRequested: + backboneResponse = (await this.client.reactivateRelationship(id.toString())).value; + break; - let backboneResponse: BackboneGetRelationshipsResponse; - switch (targetStatus) { - case RelationshipChangeStatus.Accepted: - backboneResponse = (await this.client.acceptRelationshipChange(relationship.id.toString(), change.id.toString(), encryptedContent)).value; + case RelationshipAuditLogEntryReason.AcceptanceOfReactivation: + backboneResponse = (await this.client.acceptRelationshipReactivation(id.toString())).value; break; - case RelationshipChangeStatus.Rejected: - backboneResponse = (await this.client.rejectRelationshipChange(relationship.id.toString(), change.id.toString(), encryptedContent)).value; + case RelationshipAuditLogEntryReason.RejectionOfReactivation: + backboneResponse = (await this.client.rejectRelationshipReactivation(id.toString())).value; break; - case RelationshipChangeStatus.Revoked: - backboneResponse = (await this.client.revokeRelationshipChange(relationship.id.toString(), change.id.toString(), encryptedContent)).value; + case RelationshipAuditLogEntryReason.RevocationOfReactivation: + backboneResponse = (await this.client.revokeRelationshipReactivation(id.toString())).value; break; default: - throw new TransportError("target change status not supported"); + throw new TransportError("operation not supported"); } - const backboneChange = backboneResponse.changes[backboneResponse.changes.length - 1]; - - queriedChange.response = RelationshipChangeResponse.fromBackbone(backboneResponse.changes[backboneResponse.changes.length - 1].response!, content); - queriedChange.status = backboneChange.status; relationship.status = backboneResponse.status; + relationship.cache.auditLog = RelationshipAuditLog.fromBackboneAuditLog(backboneResponse.auditLog); await this.relationships.update(relationshipDoc, relationship); - - this.eventBus.publish(new RelationshipChangedEvent(this.parent.identity.address.toString(), relationship)); - + this.publishEventAfterCompletedOperation(operation, relationship); return relationship; } - private async encryptRevokeContent(relationship: Relationship, content: ICoreSerializable) { - const responseContent = RelationshipCreationChangeResponseContentWrapper.from({ - relationshipId: relationship.id, - content: content - }); - - const serializedResponse = responseContent.serialize(); - const buffer = CoreUtil.toBuffer(serializedResponse); - - const [deviceSignature, relationshipSignature] = await Promise.all([this.parent.activeDevice.sign(buffer), this.secrets.sign(relationship.relationshipSecretId, buffer)]); - - const signedResponse = RelationshipCreationChangeResponseSigned.from({ - serializedResponse: serializedResponse, - deviceSignature: deviceSignature, - relationshipSignature: relationshipSignature - }); - - const cipher = await this.secrets.encryptRequest(relationship.relationshipSecretId, signedResponse); - const responseCipher = RelationshipCreationChangeResponseCipher.from({ - cipher: cipher - }); - - return responseCipher.toBase64(); - } - - private async encryptAcceptRejectContent(relationship: Relationship, content: ICoreSerializable) { - const publicResponseCrypto = await this.secrets.getPublicResponse(relationship.relationshipSecretId); - - const responseContent = RelationshipCreationChangeResponseContentWrapper.from({ - relationshipId: relationship.id, - content: content - }); - - const serializedResponse = responseContent.serialize(); - const buffer = CoreUtil.toBuffer(serializedResponse); - - const [deviceSignature, relationshipSignature] = await Promise.all([this.parent.activeDevice.sign(buffer), this.secrets.sign(relationship.relationshipSecretId, buffer)]); - - const signedResponse = RelationshipCreationChangeResponseSigned.from({ - serializedResponse: serializedResponse, - deviceSignature: deviceSignature, - relationshipSignature: relationshipSignature - }); - - const cipher = await this.secrets.encrypt(relationship.relationshipSecretId, signedResponse); - const responseCipher = RelationshipCreationChangeResponseCipher.from({ - cipher: cipher, - publicResponseCrypto: publicResponseCrypto - }); - - return responseCipher.toBase64(); - } - - private throwWrongChangeType(type: RelationshipChangeType) { - throw new TransportError(`The relationship change has the wrong type (${type}) to run this operation`); - } - - private newChangeResponseMissingError(changeId: string) { - return new TransportError(`The response of the relationship change (${changeId}) is missing`); - } - - private newEmptyOrInvalidContentError(change?: RelationshipChange | BackboneGetRelationshipsChangesResponse) { - return new TransportError(`The content property of the relationship change ${change?.id} is missing or invalid`); + private publishEventAfterCompletedOperation(operation: RelationshipAuditLogEntryReason, relationship: Relationship) { + this.eventBus.publish(new RelationshipChangedEvent(this.parent.identity.address.toString(), relationship)); + switch (operation) { + case RelationshipAuditLogEntryReason.ReactivationRequested: + this.eventBus.publish(new RelationshipReactivationRequestedEvent(this.parent.identity.address.toString(), relationship)); + break; + case RelationshipAuditLogEntryReason.RevocationOfReactivation: + case RelationshipAuditLogEntryReason.AcceptanceOfReactivation: + case RelationshipAuditLogEntryReason.RejectionOfReactivation: + this.eventBus.publish(new RelationshipReactivationCompletedEvent(this.parent.identity.address.toString(), relationship)); + break; + default: + } } } diff --git a/packages/transport/src/modules/relationships/backbone/BackboneGetRelationships.ts b/packages/transport/src/modules/relationships/backbone/BackboneGetRelationships.ts index 11e002ece..752ac6fb3 100644 --- a/packages/transport/src/modules/relationships/backbone/BackboneGetRelationships.ts +++ b/packages/transport/src/modules/relationships/backbone/BackboneGetRelationships.ts @@ -1,19 +1,10 @@ -import { RelationshipStatus } from "../transmission/RelationshipStatus"; -import { BackboneGetRelationshipsChangesResponse } from "./BackboneGetRelationshipsChanges"; +import { BackboneRelationship } from "./BackboneRelationship"; export interface BackboneGetRelationshipsRequest { ids: string[]; } -export interface BackboneGetRelationshipsResponse { - id: string; - relationshipTemplateId: string; - from: string; - to: string; - changes: BackboneGetRelationshipsChangesResponse[]; - createdAt: string; - status: RelationshipStatus; -} +export type BackboneGetRelationshipResponse = BackboneRelationship; export interface BackboneGetRelationshipsDateRange { from?: T; diff --git a/packages/transport/src/modules/relationships/backbone/BackboneGetRelationshipsChanges.ts b/packages/transport/src/modules/relationships/backbone/BackboneGetRelationshipsChanges.ts deleted file mode 100644 index 0cb75f017..000000000 --- a/packages/transport/src/modules/relationships/backbone/BackboneGetRelationshipsChanges.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RelationshipChangeStatus } from "../transmission/changes/RelationshipChangeStatus"; -import { RelationshipChangeType } from "../transmission/changes/RelationshipChangeType"; - -export interface BackboneGetRelationshipsChangesRequest { - ids: string[]; -} -export interface BackboneGetRelationshipsChangesResponse { - id: string; - relationshipId: string; - request: BackboneGetRelationshipsChangesSingleChangeResponse; - response: BackboneGetRelationshipsChangesSingleChangeResponse | null; - status: RelationshipChangeStatus; - type: RelationshipChangeType; -} -export interface BackboneGetRelationshipsChangesSingleChangeResponse { - createdBy: string; - createdByDevice: string; - createdAt: string; - content: string | null; -} -export interface BackboneGetRelationshipsChangesDateRange { - from?: T; - to?: T; -} diff --git a/packages/transport/src/modules/relationships/backbone/BackbonePostRelationship.ts b/packages/transport/src/modules/relationships/backbone/BackbonePostRelationship.ts new file mode 100644 index 000000000..ab03abe86 --- /dev/null +++ b/packages/transport/src/modules/relationships/backbone/BackbonePostRelationship.ts @@ -0,0 +1,8 @@ +import { BackboneRelationship } from "./BackboneRelationship"; + +export interface BackbonePostRelationshipsRequest { + relationshipTemplateId: string; + creationContent: string; +} + +export type BackbonePostRelationshipsResponse = Omit; diff --git a/packages/transport/src/modules/relationships/backbone/BackbonePostRelationshipsChanges.ts b/packages/transport/src/modules/relationships/backbone/BackbonePostRelationshipsChanges.ts deleted file mode 100644 index a3941dfbc..000000000 --- a/packages/transport/src/modules/relationships/backbone/BackbonePostRelationshipsChanges.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RelationshipChangeType } from "../transmission/changes/RelationshipChangeType"; - -export interface BackbonePostRelationshipsRequest { - relationshipTemplateId: string; - content: any; -} - -export interface BackbonePostRelationshipsChangesRequest { - type: RelationshipChangeType; -} diff --git a/packages/transport/src/modules/relationships/backbone/BackbonePutRelationship.ts b/packages/transport/src/modules/relationships/backbone/BackbonePutRelationship.ts new file mode 100644 index 000000000..f3a23cd55 --- /dev/null +++ b/packages/transport/src/modules/relationships/backbone/BackbonePutRelationship.ts @@ -0,0 +1,7 @@ +import { BackboneRelationship } from "./BackboneRelationship"; + +export interface BackboneAcceptRelationshipsRequest { + creationResponseContent: string; +} + +export type BackbonePutRelationshipsResponse = Omit; diff --git a/packages/transport/src/modules/relationships/backbone/BackboneRelationship.ts b/packages/transport/src/modules/relationships/backbone/BackboneRelationship.ts new file mode 100644 index 000000000..da60d34c6 --- /dev/null +++ b/packages/transport/src/modules/relationships/backbone/BackboneRelationship.ts @@ -0,0 +1,15 @@ +import { BackboneRelationshipAuditLog } from "../transmission/RelationshipAuditLog"; +import { RelationshipStatus } from "../transmission/RelationshipStatus"; + +export interface BackboneRelationship { + id: string; + relationshipTemplateId: string; + from: string; + to: string; + + createdAt: string; + status: RelationshipStatus; + auditLog: BackboneRelationshipAuditLog; + creationContent?: string; + creationResponseContent?: string; +} diff --git a/packages/transport/src/modules/relationships/backbone/RelationshipClient.ts b/packages/transport/src/modules/relationships/backbone/RelationshipClient.ts index 06cf5d1c0..5fcfdf3b2 100644 --- a/packages/transport/src/modules/relationships/backbone/RelationshipClient.ts +++ b/packages/transport/src/modules/relationships/backbone/RelationshipClient.ts @@ -1,51 +1,56 @@ import { ClientResult } from "../../../core/backbone/ClientResult"; import { Paginator } from "../../../core/backbone/Paginator"; import { RESTClientAuthenticate } from "../../../core/backbone/RESTClientAuthenticate"; -import { BackboneGetRelationshipsRequest, BackboneGetRelationshipsResponse } from "./BackboneGetRelationships"; -import { BackboneGetRelationshipsChangesRequest, BackboneGetRelationshipsChangesResponse } from "./BackboneGetRelationshipsChanges"; -import { BackbonePostRelationshipsChangesRequest, BackbonePostRelationshipsRequest } from "./BackbonePostRelationshipsChanges"; +import { BackboneGetRelationshipResponse, BackboneGetRelationshipsRequest } from "./BackboneGetRelationships"; +import { BackbonePostRelationshipsRequest, BackbonePostRelationshipsResponse } from "./BackbonePostRelationship"; +import { BackboneAcceptRelationshipsRequest, BackbonePutRelationshipsResponse } from "./BackbonePutRelationship"; export class RelationshipClient extends RESTClientAuthenticate { - public async createRelationship(request: BackbonePostRelationshipsRequest): Promise> { - return await this.post("/api/v1/Relationships", request); + public async createRelationship(request: BackbonePostRelationshipsRequest): Promise> { + return await this.post("/api/v1/Relationships", request); } - public async createRelationshipChange(id: string, request: BackbonePostRelationshipsChangesRequest): Promise> { - return await this.post(`/api/v1/Relationships/${id}/Changes`, request); + public async acceptRelationship(relationshipId: string, request: BackboneAcceptRelationshipsRequest): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Accept`, request); } - public async acceptRelationshipChange(relationshipId: string, changeId: string, content?: any): Promise> { - return await this.put(`/api/v1/Relationships/${relationshipId}/Changes/${changeId}/Accept`, { - content: content - }); + public async rejectRelationship(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Reject`, {}); } - public async rejectRelationshipChange(relationshipId: string, changeId: string, content?: any): Promise> { - return await this.put(`/api/v1/Relationships/${relationshipId}/Changes/${changeId}/Reject`, { - content: content - }); + public async revokeRelationship(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Revoke`, {}); } - public async revokeRelationshipChange(relationshipId: string, changeId: string, content?: any): Promise> { - return await this.put(`/api/v1/Relationships/${relationshipId}/Changes/${changeId}/Revoke`, { - content: content - }); + public async terminateRelationship(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Terminate`, {}); } - public async getRelationships(request?: BackboneGetRelationshipsRequest): Promise>> { - return await this.getPaged("/api/v1/Relationships", request); + public async reactivateRelationship(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Reactivate`, {}); } - public async getRelationship(relationshipId: string): Promise> { - return await this.get(`/api/v1/Relationships/${relationshipId}`); + public async acceptRelationshipReactivation(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Reactivate/Accept`, {}); } - public async getRelationshipChanges(request?: BackboneGetRelationshipsChangesRequest): Promise>> { - return await this.getPaged("/api/v1/Relationships/Changes", request); + public async rejectRelationshipReactivation(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Reactivate/Reject`, {}); } - public async getRelationshipChange(relationshipChangeId: string): Promise> { - const change = await this.get(`/api/v1/Relationships/Changes/${relationshipChangeId}`); - return change; + public async revokeRelationshipReactivation(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Reactivate/Revoke`, {}); + } + + public async decomposeRelationship(relationshipId: string): Promise> { + return await this.put(`/api/v1/Relationships/${relationshipId}/Decompose`, {}); + } + + public async getRelationships(request?: BackboneGetRelationshipsRequest): Promise>> { + return await this.getPaged("/api/v1/Relationships", request); + } + + public async getRelationship(relationshipId: string): Promise> { + return await this.get(`/api/v1/Relationships/${relationshipId}`); } } diff --git a/packages/transport/src/modules/relationships/local/CachedRelationship.ts b/packages/transport/src/modules/relationships/local/CachedRelationship.ts index 507b96949..f277b1b45 100644 --- a/packages/transport/src/modules/relationships/local/CachedRelationship.ts +++ b/packages/transport/src/modules/relationships/local/CachedRelationship.ts @@ -1,14 +1,15 @@ -import { serialize, type, validate } from "@js-soft/ts-serval"; +import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; import { CoreDate, CoreSerializable, ICoreDate, ICoreSerializable } from "../../../core"; import { IRelationshipTemplate, RelationshipTemplate } from "../../relationshipTemplates/local/RelationshipTemplate"; -import { IRelationshipChange, RelationshipChange } from "../transmission/changes/RelationshipChange"; +import { IRelationshipAuditLogEntry, RelationshipAuditLogEntry } from "./RelationshipAuditLogEntry"; export interface ICachedRelationship extends ICoreSerializable { template: IRelationshipTemplate; - changes: IRelationshipChange[]; + creationContent: ISerializable; lastMessageSentAt?: ICoreDate; lastMessageReceivedAt?: ICoreDate; + auditLog: IRelationshipAuditLogEntry[]; } @type("CachedRelationship") @@ -18,8 +19,8 @@ export class CachedRelationship extends CoreSerializable implements ICachedRelat public template: RelationshipTemplate; @validate() - @serialize({ type: RelationshipChange }) - public changes: RelationshipChange[]; + @serialize() + public creationContent: Serializable; @validate({ nullable: true }) @serialize() @@ -29,9 +30,9 @@ export class CachedRelationship extends CoreSerializable implements ICachedRelat @serialize() public lastMessageReceivedAt?: CoreDate; - public get creationChange(): RelationshipChange { - return this.changes[0]; - } + @validate() + @serialize({ type: RelationshipAuditLogEntry }) + public auditLog: RelationshipAuditLogEntry[]; public static from(value: ICachedRelationship): CachedRelationship { return this.fromAny(value); diff --git a/packages/transport/src/modules/relationships/local/Relationship.ts b/packages/transport/src/modules/relationships/local/Relationship.ts index 65d4fffb4..2cb95082d 100644 --- a/packages/transport/src/modules/relationships/local/Relationship.ts +++ b/packages/transport/src/modules/relationships/local/Relationship.ts @@ -1,13 +1,12 @@ -import { serialize, type, validate } from "@js-soft/ts-serval"; +import { ISerializable, serialize, type, validate } from "@js-soft/ts-serval"; import { nameof } from "ts-simple-nameof"; import { CoreDate, CoreId, CoreSynchronizable, ICoreId, ICoreSynchronizable, TransportError } from "../../../core"; import { Identity, IIdentity } from "../../accounts/data/Identity"; import { IRelationshipTemplate } from "../../relationshipTemplates/local/RelationshipTemplate"; -import { BackboneGetRelationshipsResponse } from "../backbone/BackboneGetRelationships"; -import { IRelationshipChange } from "../transmission/changes/RelationshipChange"; -import { RelationshipChangeResponse } from "../transmission/changes/RelationshipChangeResponse"; +import { BackboneGetRelationshipResponse } from "../backbone/BackboneGetRelationships"; import { RelationshipStatus } from "../transmission/RelationshipStatus"; import { CachedRelationship, ICachedRelationship } from "./CachedRelationship"; +import { RelationshipAuditLog } from "./RelationshipAuditLog"; export interface IRelationship extends ICoreSynchronizable { relationshipSecretId: ICoreId; @@ -72,32 +71,17 @@ export class Relationship extends CoreSynchronizable implements IRelationship { return json; } - public static fromRequestSent(id: CoreId, template: IRelationshipTemplate, peer: IIdentity, creationChange: IRelationshipChange, relationshipSecretId: CoreId): Relationship { - const cache = CachedRelationship.from({ - changes: [creationChange], - template: template - }); - - return Relationship.from({ - id: id, - peer: peer, - status: RelationshipStatus.Pending, - cache: cache, - cachedAt: CoreDate.utc(), - relationshipSecretId: relationshipSecretId - }); - } - - public static fromCreationChangeReceived( - response: BackboneGetRelationshipsResponse, + public static fromBackboneAndCreationContent( + response: BackboneGetRelationshipResponse, template: IRelationshipTemplate, peer: IIdentity, - creationChange: IRelationshipChange, + creationContent: ISerializable, relationshipSecretId: CoreId ): Relationship { const cache = CachedRelationship.from({ - changes: [creationChange], - template: template + creationContent, + template: template, + auditLog: RelationshipAuditLog.fromBackboneAuditLog(response.auditLog) }); return Relationship.from({ id: CoreId.from(response.id), @@ -109,27 +93,6 @@ export class Relationship extends CoreSynchronizable implements IRelationship { }); } - public toActive(response: RelationshipChangeResponse): void { - if (!this.cache) throw this.newCacheEmptyError(); - - this.cache.changes[0].response = response; - this.status = RelationshipStatus.Active; - } - - public toRejected(response: RelationshipChangeResponse): void { - if (!this.cache) throw this.newCacheEmptyError(); - - this.cache.changes[0].response = response; - this.status = RelationshipStatus.Rejected; - } - - public toRevoked(response: RelationshipChangeResponse): void { - if (!this.cache) throw this.newCacheEmptyError(); - - this.cache.changes[0].response = response; - this.status = RelationshipStatus.Revoked; - } - public static from(value: IRelationship): Relationship { return this.fromAny(value); } diff --git a/packages/transport/src/modules/relationships/local/RelationshipAuditLog.ts b/packages/transport/src/modules/relationships/local/RelationshipAuditLog.ts new file mode 100644 index 000000000..5923d31eb --- /dev/null +++ b/packages/transport/src/modules/relationships/local/RelationshipAuditLog.ts @@ -0,0 +1,21 @@ +import _ from "lodash"; +import { CoreAddress, CoreDate, CoreId } from "../../../core"; +import { BackboneRelationshipAuditLog as BackboneAuditLog } from "../transmission/RelationshipAuditLog"; +import { RelationshipAuditLogEntry } from "./RelationshipAuditLogEntry"; + +export class RelationshipAuditLog { + public static fromBackboneAuditLog(backboneAuditLog: BackboneAuditLog): RelationshipAuditLogEntry[] { + const auditLog = backboneAuditLog.map((entry) => { + return RelationshipAuditLogEntry.from({ + createdAt: CoreDate.from(entry.createdAt), + createdBy: CoreAddress.from(entry.createdBy), + createdByDevice: CoreId.from(entry.createdByDevice), + reason: entry.reason, + oldStatus: entry.oldStatus, + newStatus: entry.newStatus + }); + }); + + return _.orderBy(auditLog, ["createdAt"], ["asc"]); + } +} diff --git a/packages/transport/src/modules/relationships/local/RelationshipAuditLogEntry.ts b/packages/transport/src/modules/relationships/local/RelationshipAuditLogEntry.ts new file mode 100644 index 000000000..114fa1fc3 --- /dev/null +++ b/packages/transport/src/modules/relationships/local/RelationshipAuditLogEntry.ts @@ -0,0 +1,51 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CoreAddress, CoreDate, CoreId, CoreSerializable, ICoreAddress, ICoreDate, ICoreId } from "../../../core"; +import { RelationshipAuditLogEntryReason } from "../transmission/RelationshipAuditLog"; +import { RelationshipStatus } from "../transmission/RelationshipStatus"; + +export interface IRelationshipAuditLogEntry { + createdAt: ICoreDate; + createdBy: ICoreAddress; + createdByDevice: ICoreId; + reason: RelationshipAuditLogEntryReason; + oldStatus?: RelationshipStatus; + newStatus: RelationshipStatus; +} + +@type("RelationshipAuditLogEntry") +export class RelationshipAuditLogEntry extends CoreSerializable implements IRelationshipAuditLogEntry { + @validate() + @serialize() + public createdAt: CoreDate; + + @validate() + @serialize() + public createdBy: CoreAddress; + + @validate() + @serialize() + public createdByDevice: CoreId; + + @validate({ + customValidator: (v) => (!Object.values(RelationshipAuditLogEntryReason).includes(v) ? `must be one of: ${Object.values(RelationshipAuditLogEntryReason)}` : undefined) + }) + @serialize() + public reason: RelationshipAuditLogEntryReason; + + @validate({ + nullable: true, + customValidator: (v) => (!Object.values(RelationshipStatus).includes(v) ? `must be one of: ${Object.values(RelationshipStatus)}` : undefined) + }) + @serialize() + public oldStatus?: RelationshipStatus; + + @validate({ + customValidator: (v) => (!Object.values(RelationshipStatus).includes(v) ? `must be one of: ${Object.values(RelationshipStatus)}` : undefined) + }) + @serialize() + public newStatus: RelationshipStatus; + + public static from(value: IRelationshipAuditLogEntry): RelationshipAuditLogEntry { + return this.fromAny({ ...value, oldStatus: value.oldStatus ?? undefined }); + } +} diff --git a/packages/transport/src/modules/relationships/local/SendRelationshipParameters.ts b/packages/transport/src/modules/relationships/local/SendRelationshipParameters.ts index 16c5d647a..7ac9af12c 100644 --- a/packages/transport/src/modules/relationships/local/SendRelationshipParameters.ts +++ b/packages/transport/src/modules/relationships/local/SendRelationshipParameters.ts @@ -3,7 +3,7 @@ import { CoreSerializable, ICoreSerializable } from "../../../core"; import { IRelationshipTemplate, RelationshipTemplate } from "../../relationshipTemplates/local/RelationshipTemplate"; export interface ISendRelationshipParameters extends ICoreSerializable { - content: ISerializable; + creationContent: ISerializable; template: IRelationshipTemplate; } @@ -11,7 +11,7 @@ export interface ISendRelationshipParameters extends ICoreSerializable { export class SendRelationshipParameters extends CoreSerializable implements ISendRelationshipParameters { @validate() @serialize() - public content: Serializable; + public creationContent: Serializable; @validate() @serialize() diff --git a/packages/transport/src/modules/relationships/transmission/RelationshipAuditLog.ts b/packages/transport/src/modules/relationships/transmission/RelationshipAuditLog.ts new file mode 100644 index 000000000..b91d8477c --- /dev/null +++ b/packages/transport/src/modules/relationships/transmission/RelationshipAuditLog.ts @@ -0,0 +1,25 @@ +import { RelationshipStatus } from "./RelationshipStatus"; + +export interface BackboneRelationshipAuditLog extends Array {} + +export interface BackboneRelationshipAuditLogEntry { + createdAt: string; + createdBy: string; + createdByDevice: string; + reason: RelationshipAuditLogEntryReason; + oldStatus?: RelationshipStatus; + newStatus: RelationshipStatus; +} + +export enum RelationshipAuditLogEntryReason { + Creation = "Creation", + AcceptanceOfCreation = "AcceptanceOfCreation", + RejectionOfCreation = "RejectionOfCreation", + RevocationOfCreation = "RevocationOfCreation", + Termination = "Termination", + ReactivationRequested = "ReactivationRequested", + AcceptanceOfReactivation = "AcceptanceOfReactivation", + RejectionOfReactivation = "RejectionOfReactivation", + RevocationOfReactivation = "RevocationOfReactivation", + Decomposition = "Decomposition" +} diff --git a/packages/transport/src/modules/relationships/transmission/RelationshipStatus.ts b/packages/transport/src/modules/relationships/transmission/RelationshipStatus.ts index 4931732d9..afc69707d 100644 --- a/packages/transport/src/modules/relationships/transmission/RelationshipStatus.ts +++ b/packages/transport/src/modules/relationships/transmission/RelationshipStatus.ts @@ -3,6 +3,6 @@ export enum RelationshipStatus { Active = "Active", Rejected = "Rejected", Revoked = "Revoked", - Terminating = "Terminating", - Terminated = "Terminated" + Terminated = "Terminated", + DeletionProposed = "DeletionProposed" } diff --git a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChange.ts b/packages/transport/src/modules/relationships/transmission/changes/RelationshipChange.ts deleted file mode 100644 index f84f269e9..000000000 --- a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChange.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ISerializable, serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreSerializable, ICoreSerializable } from "../../../../core"; -import { CoreId, ICoreId } from "../../../../core/types/CoreId"; -import { BackboneGetRelationshipsChangesResponse } from "../../backbone/BackboneGetRelationshipsChanges"; -import { IRelationshipChangeRequest, RelationshipChangeRequest } from "./RelationshipChangeRequest"; -import { IRelationshipChangeResponse, RelationshipChangeResponse } from "./RelationshipChangeResponse"; -import { RelationshipChangeStatus } from "./RelationshipChangeStatus"; -import { RelationshipChangeType } from "./RelationshipChangeType"; - -export interface IRelationshipChange extends ICoreSerializable { - id: ICoreId; - relationshipId: ICoreId; - request: IRelationshipChangeRequest; - response?: IRelationshipChangeResponse; - status: RelationshipChangeStatus; - type: RelationshipChangeType; -} - -@type("RelationshipChange") -export class RelationshipChange extends CoreSerializable implements IRelationshipChange { - @validate() - @serialize() - public id: CoreId; - - @validate() - @serialize() - public relationshipId: CoreId; - - @validate() - @serialize() - public request: RelationshipChangeRequest; - - @validate({ nullable: true }) - @serialize() - public response?: RelationshipChangeResponse; - - @validate() - @serialize() - public status: RelationshipChangeStatus; - - @validate() - @serialize() - public type: RelationshipChangeType; - - public static fromBackbone(backboneChange: BackboneGetRelationshipsChangesResponse, requestContent?: ISerializable, responseContent?: ISerializable): RelationshipChange { - const relationshipChange = this.from({ - id: CoreId.from(backboneChange.id), - relationshipId: CoreId.from(backboneChange.relationshipId), - type: backboneChange.type, - status: backboneChange.status, - request: RelationshipChangeRequest.fromBackbone(backboneChange.request, requestContent) - }); - - if (backboneChange.response) { - relationshipChange.response = RelationshipChangeResponse.fromBackbone(backboneChange.response, responseContent); - } - - return relationshipChange; - } - - public static from(value: IRelationshipChange): RelationshipChange { - return this.fromAny(value); - } -} diff --git a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeRequest.ts b/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeRequest.ts deleted file mode 100644 index 2f84e8059..000000000 --- a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeRequest.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreAddress, CoreDate, CoreSerializable, ICoreAddress } from "../../../../core"; -import { ICoreDate } from "../../../../core/types/CoreDate"; -import { CoreId, ICoreId } from "../../../../core/types/CoreId"; -import { BackboneGetRelationshipsChangesSingleChangeResponse } from "../../backbone/BackboneGetRelationshipsChanges"; - -export interface IRelationshipChangeRequest { - createdBy: ICoreAddress; - createdByDevice: ICoreId; - createdAt: ICoreDate; - content?: ISerializable; -} - -@type("RelationshipChangeRequest") -export class RelationshipChangeRequest extends CoreSerializable implements IRelationshipChangeRequest { - @validate() - @serialize() - public createdBy: CoreAddress; - - @validate() - @serialize() - public createdByDevice: CoreId; - - @validate() - @serialize() - public createdAt: CoreDate; - - @validate({ nullable: true }) - @serialize() - public content?: Serializable; - - public static fromBackbone(backboneChange: BackboneGetRelationshipsChangesSingleChangeResponse, content?: ISerializable): RelationshipChangeRequest { - return this.from({ - createdBy: CoreAddress.from(backboneChange.createdBy), - createdByDevice: CoreId.from(backboneChange.createdByDevice), - createdAt: CoreDate.from(backboneChange.createdAt), - content: content - }); - } - - public static from(value: IRelationshipChangeRequest): RelationshipChangeRequest { - return this.fromAny(value); - } -} diff --git a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeResponse.ts b/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeResponse.ts deleted file mode 100644 index 2a8200e0c..000000000 --- a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeResponse.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreAddress, CoreDate, CoreSerializable, ICoreAddress, ICoreSerializable } from "../../../../core"; -import { ICoreDate } from "../../../../core/types/CoreDate"; -import { CoreId, ICoreId } from "../../../../core/types/CoreId"; -import { BackboneGetRelationshipsChangesSingleChangeResponse } from "../../backbone/BackboneGetRelationshipsChanges"; - -export interface IRelationshipChangeResponse extends ICoreSerializable { - createdBy: ICoreAddress; - createdByDevice: ICoreId; - createdAt: ICoreDate; - content?: ISerializable; -} - -@type("RelationshipChangeResponse") -export class RelationshipChangeResponse extends CoreSerializable implements IRelationshipChangeResponse { - @validate() - @serialize() - public createdBy: CoreAddress; - - @validate() - @serialize() - public createdByDevice: CoreId; - - @validate() - @serialize() - public createdAt: CoreDate; - - @validate({ nullable: true }) - @serialize() - public content?: Serializable; - - public static fromBackbone(backboneChange: BackboneGetRelationshipsChangesSingleChangeResponse, content?: ISerializable): RelationshipChangeResponse { - return this.from({ - createdBy: CoreAddress.from(backboneChange.createdBy), - createdByDevice: CoreId.from(backboneChange.createdByDevice), - createdAt: CoreDate.from(backboneChange.createdAt), - content: content - }); - } - - public static from(value: IRelationshipChangeResponse): RelationshipChangeResponse { - return this.fromAny(value); - } -} diff --git a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeStatus.ts b/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeStatus.ts deleted file mode 100644 index 3e207dd8b..000000000 --- a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeStatus.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum RelationshipChangeStatus { - Pending = "Pending", - Rejected = "Rejected", - Revoked = "Revoked", - Accepted = "Accepted" -} diff --git a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeType.ts b/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeType.ts deleted file mode 100644 index 5ee635209..000000000 --- a/packages/transport/src/modules/relationships/transmission/changes/RelationshipChangeType.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum RelationshipChangeType { - Creation = "Creation", - Termination = "Termination", - TerminationCancellation = "TerminationCancellation" -} diff --git a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestCipher.ts b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestCipher.ts deleted file mode 100644 index ec8a4547e..000000000 --- a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestCipher.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { serialize, type, validate } from "@js-soft/ts-serval"; -import { CryptoCipher, CryptoRelationshipPublicRequest, ICryptoCipher, ICryptoRelationshipPublicRequest } from "@nmshd/crypto"; -import { CoreSerializable, ICoreSerializable } from "../../../../core"; - -export interface IRelationshipCreationChangeRequestCipher extends ICoreSerializable { - cipher: ICryptoCipher; - publicRequestCrypto: ICryptoRelationshipPublicRequest; -} - -@type("RelationshipCreationChangeRequestCipher") -export class RelationshipCreationChangeRequestCipher extends CoreSerializable implements IRelationshipCreationChangeRequestCipher { - @validate() - @serialize() - public cipher: CryptoCipher; - - @validate() - @serialize() - public publicRequestCrypto: CryptoRelationshipPublicRequest; - - public static from(value: IRelationshipCreationChangeRequestCipher): RelationshipCreationChangeRequestCipher { - return this.fromAny(value); - } - - public static fromBase64(value: string): RelationshipCreationChangeRequestCipher { - return super.fromBase64T(value); - } -} diff --git a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentCipher.ts b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentCipher.ts new file mode 100644 index 000000000..7d7c32d42 --- /dev/null +++ b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentCipher.ts @@ -0,0 +1,27 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CryptoCipher, CryptoRelationshipPublicRequest, ICryptoCipher, ICryptoRelationshipPublicRequest } from "@nmshd/crypto"; +import { CoreSerializable, ICoreSerializable } from "../../../../core"; + +export interface IRelationshipCreationContentCipher extends ICoreSerializable { + cipher: ICryptoCipher; + publicCreationContentCrypto: ICryptoRelationshipPublicRequest; +} + +@type("RelationshipCreationContentCipher") +export class RelationshipCreationContentCipher extends CoreSerializable implements IRelationshipCreationContentCipher { + @validate() + @serialize() + public cipher: CryptoCipher; + + @validate() + @serialize() + public publicCreationContentCrypto: CryptoRelationshipPublicRequest; + + public static from(value: IRelationshipCreationContentCipher): RelationshipCreationContentCipher { + return this.fromAny(value); + } + + public static fromBase64(value: string): RelationshipCreationContentCipher { + return super.fromBase64T(value); + } +} diff --git a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestSigned.ts b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentSigned.ts similarity index 54% rename from packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestSigned.ts rename to packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentSigned.ts index 5dab25686..8a8407f1c 100644 --- a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestSigned.ts +++ b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentSigned.ts @@ -2,17 +2,17 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { CryptoSignature, ICryptoSignature } from "@nmshd/crypto"; import { CoreSerializable, ICoreSerializable } from "../../../../core"; -export interface IRelationshipCreationChangeRequestSigned extends ICoreSerializable { - serializedRequest: string; +export interface IRelationshipCreationContentSigned extends ICoreSerializable { + serializedCreationContent: string; deviceSignature: ICryptoSignature; relationshipSignature: ICryptoSignature; } -@type("RelationshipCreationChangeRequestSigned") -export class RelationshipCreationChangeRequestSigned extends CoreSerializable implements IRelationshipCreationChangeRequestSigned { +@type("RelationshipCreationContentSigned") +export class RelationshipCreationContentSigned extends CoreSerializable implements IRelationshipCreationContentSigned { @validate() @serialize() - public serializedRequest: string; + public serializedCreationContent: string; @validate() @serialize() @@ -22,7 +22,7 @@ export class RelationshipCreationChangeRequestSigned extends CoreSerializable im @serialize() public relationshipSignature: CryptoSignature; - public static from(value: IRelationshipCreationChangeRequestSigned): RelationshipCreationChangeRequestSigned { + public static from(value: IRelationshipCreationContentSigned): RelationshipCreationContentSigned { return this.fromAny(value); } } diff --git a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentWrapper.ts similarity index 57% rename from packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts rename to packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentWrapper.ts index 2ad252588..0250057f5 100644 --- a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts +++ b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationContentWrapper.ts @@ -2,14 +2,14 @@ import { ISerializable, Serializable, serialize, type, validate } from "@js-soft import { CoreId, CoreSerializable, ICoreId, ICoreSerializable } from "../../../../core"; import { Identity, IIdentity } from "../../../accounts/data/Identity"; -export interface IRelationshipCreationChangeRequestContentWrapper extends ICoreSerializable { +export interface IRelationshipCreationContentWrapper extends ICoreSerializable { identity: IIdentity; content: ISerializable; templateId: ICoreId; } -@type("RelationshipCreationChangeRequestContentWrapper") -export class RelationshipCreationChangeRequestContentWrapper extends CoreSerializable implements IRelationshipCreationChangeRequestContentWrapper { +@type("RelationshipCreationContentWrapper") +export class RelationshipCreationContentWrapper extends CoreSerializable implements IRelationshipCreationContentWrapper { @validate() @serialize() public identity: Identity; @@ -22,7 +22,7 @@ export class RelationshipCreationChangeRequestContentWrapper extends CoreSeriali @serialize() public templateId: CoreId; - public static from(value: IRelationshipCreationChangeRequestContentWrapper): RelationshipCreationChangeRequestContentWrapper { + public static from(value: IRelationshipCreationContentWrapper): RelationshipCreationContentWrapper { return this.fromAny(value); } } diff --git a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseCipher.ts b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseCipher.ts deleted file mode 100644 index d6e1b0d3e..000000000 --- a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseCipher.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { serialize, type, validate } from "@js-soft/ts-serval"; -import { CryptoCipher, CryptoRelationshipPublicResponse, ICryptoCipher, ICryptoRelationshipPublicResponse } from "@nmshd/crypto"; -import { CoreSerializable, ICoreSerializable } from "../../../../core"; - -export interface IRelationshipCreationChangeResponseCipher extends ICoreSerializable { - cipher: ICryptoCipher; - publicResponseCrypto?: ICryptoRelationshipPublicResponse; -} - -@type("RelationshipCreationChangeResponseCipher") -export class RelationshipCreationChangeResponseCipher extends CoreSerializable implements IRelationshipCreationChangeResponseCipher { - @validate() - @serialize() - public cipher: CryptoCipher; - - @validate({ nullable: true }) - @serialize() - public publicResponseCrypto?: CryptoRelationshipPublicResponse; - - public static from(value: IRelationshipCreationChangeResponseCipher): RelationshipCreationChangeResponseCipher { - return this.fromAny(value); - } - - public static fromBase64(value: string): RelationshipCreationChangeResponseCipher { - return super.fromBase64T(value); - } -} diff --git a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseContentWrapper.ts b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseContentWrapper.ts deleted file mode 100644 index e9c5ca0f2..000000000 --- a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseContentWrapper.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreId, CoreSerializable, ICoreId, ICoreSerializable } from "../../../../core"; - -export interface IRelationshipCreationChangeResponseContentWrapper extends ICoreSerializable { - content: ISerializable; - relationshipId: ICoreId; -} - -@type("RelationshipCreationChangeResponseContentWrapper") -export class RelationshipCreationChangeResponseContentWrapper extends CoreSerializable implements IRelationshipCreationChangeResponseContentWrapper { - @validate() - @serialize() - public content: Serializable; - - @validate() - @serialize() - public relationshipId: CoreId; - - public static from(value: IRelationshipCreationChangeResponseContentWrapper): RelationshipCreationChangeResponseContentWrapper { - return this.fromAny(value); - } -} diff --git a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentCipher.ts b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentCipher.ts new file mode 100644 index 000000000..b7b6c5b4a --- /dev/null +++ b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentCipher.ts @@ -0,0 +1,27 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CryptoCipher, CryptoRelationshipPublicResponse, ICryptoCipher, ICryptoRelationshipPublicResponse } from "@nmshd/crypto"; +import { CoreSerializable, ICoreSerializable } from "../../../../core"; + +export interface IRelationshipCreationResponseContentCipher extends ICoreSerializable { + cipher: ICryptoCipher; + publicCreationResponseContentCrypto: ICryptoRelationshipPublicResponse; +} + +@type("RelationshipCreationResponseContentCipher") +export class RelationshipCreationResponseContentCipher extends CoreSerializable implements IRelationshipCreationResponseContentCipher { + @validate() + @serialize() + public cipher: CryptoCipher; + + @validate() + @serialize() + public publicCreationResponseContentCrypto: CryptoRelationshipPublicResponse; + + public static from(value: IRelationshipCreationResponseContentCipher): RelationshipCreationResponseContentCipher { + return this.fromAny(value); + } + + public static fromBase64(value: string): RelationshipCreationResponseContentCipher { + return super.fromBase64T(value); + } +} diff --git a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseSigned.ts b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentSigned.ts similarity index 51% rename from packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseSigned.ts rename to packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentSigned.ts index 35c7bc61c..75721bcad 100644 --- a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationChangeResponseSigned.ts +++ b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentSigned.ts @@ -2,17 +2,17 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { CryptoSignature, ICryptoSignature } from "@nmshd/crypto"; import { CoreSerializable, ICoreSerializable } from "../../../../core"; -export interface IRelationshipCreationChangeResponseSigned extends ICoreSerializable { - serializedResponse: string; +export interface IRelationshipCreationResponseContentSigned extends ICoreSerializable { + serializedCreationResponseContent: string; deviceSignature: ICryptoSignature; relationshipSignature: ICryptoSignature; } -@type("RelationshipCreationChangeResponseSigned") -export class RelationshipCreationChangeResponseSigned extends CoreSerializable implements IRelationshipCreationChangeResponseSigned { +@type("RelationshipCreationResponseContentSigned") +export class RelationshipCreationResponseContentSigned extends CoreSerializable implements IRelationshipCreationResponseContentSigned { @validate() @serialize() - public serializedResponse: string; + public serializedCreationResponseContent: string; @validate() @serialize() @@ -22,7 +22,7 @@ export class RelationshipCreationChangeResponseSigned extends CoreSerializable i @serialize() public relationshipSignature: CryptoSignature; - public static from(value: IRelationshipCreationChangeResponseSigned): RelationshipCreationChangeResponseSigned { + public static from(value: IRelationshipCreationResponseContentSigned): RelationshipCreationResponseContentSigned { return this.fromAny(value); } } diff --git a/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentWrapper.ts b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentWrapper.ts new file mode 100644 index 000000000..f137a2eba --- /dev/null +++ b/packages/transport/src/modules/relationships/transmission/responses/RelationshipCreationResponseContentWrapper.ts @@ -0,0 +1,17 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CoreId, CoreSerializable, ICoreId, ICoreSerializable } from "../../../../core"; + +export interface IRelationshipCreationResponseContentWrapper extends ICoreSerializable { + relationshipId: ICoreId; +} + +@type("RelationshipCreationResponseContentWrapper") +export class RelationshipCreationResponseContentWrapper extends CoreSerializable implements IRelationshipCreationResponseContentWrapper { + @validate() + @serialize() + public relationshipId: CoreId; + + public static from(value: IRelationshipCreationResponseContentWrapper): RelationshipCreationResponseContentWrapper { + return this.fromAny(value); + } +} diff --git a/packages/transport/src/modules/sync/DatawalletModificationsProcessor.ts b/packages/transport/src/modules/sync/DatawalletModificationsProcessor.ts index 5462a0a7d..cb46b9a80 100644 --- a/packages/transport/src/modules/sync/DatawalletModificationsProcessor.ts +++ b/packages/transport/src/modules/sync/DatawalletModificationsProcessor.ts @@ -24,14 +24,12 @@ import { CachedToken } from "../tokens/local/CachedToken"; import { Token } from "../tokens/local/Token"; import { TokenController } from "../tokens/TokenController"; import { DatawalletModification, DatawalletModificationType } from "./local/DatawalletModification"; -import { SyncProgressReporter, SyncProgressReporterStep, SyncStep } from "./SyncCallback"; export class DatawalletModificationsProcessor { private readonly creates: DatawalletModification[]; private readonly updates: DatawalletModification[]; private readonly deletes: DatawalletModification[]; private readonly cacheChanges: DatawalletModification[]; - private readonly syncStep: SyncProgressReporterStep; public get log(): ILogger { return this.logger; @@ -41,8 +39,7 @@ export class DatawalletModificationsProcessor { modifications: DatawalletModification[], private readonly cacheFetcher: CacheFetcher, private readonly collectionProvider: IDatabaseCollectionProvider, - private readonly logger: ILogger, - reporter: SyncProgressReporter + private readonly logger: ILogger ) { const modificationsGroupedByType = _.groupBy(modifications, (m) => m.type); @@ -50,9 +47,6 @@ export class DatawalletModificationsProcessor { this.updates = modificationsGroupedByType[DatawalletModificationType.Update] ?? []; this.deletes = modificationsGroupedByType[DatawalletModificationType.Delete] ?? []; this.cacheChanges = modificationsGroupedByType[DatawalletModificationType.CacheChanged] ?? []; - - const totalItems = this.creates.length + this.updates.length + this.deletes.length + this.cacheChanges.length; - this.syncStep = reporter.createStep(SyncStep.DatawalletSyncProcessing, totalItems); } private readonly collectionsWithCacheableItems: string[] = [ @@ -67,12 +61,8 @@ export class DatawalletModificationsProcessor { public async execute(): Promise { await this.applyCreates(); await this.applyUpdates(); - await this.applyCacheChanges(); await this.applyDeletes(); - - // cache-fills are optimized by the backbone, so it is possible that the processedItemCount is - // lower than the total number of items - in this case the 100% callback is triggered here - this.syncStep.finish(); + await this.applyCacheChanges(); } private async applyCreates() { @@ -105,8 +95,6 @@ export class DatawalletModificationsProcessor { await this.simulateCacheChangeForCreate(targetCollectionName, objectIdentifier); await targetCollection.create(newObject); } - - this.syncStep.progress(); } } @@ -125,7 +113,6 @@ export class DatawalletModificationsProcessor { }); this.cacheChanges.push(modification); - this.syncStep.incrementTotalNumberOfItems(); } private async applyUpdates() { @@ -145,7 +132,6 @@ export class DatawalletModificationsProcessor { const newObject = { ...oldObject.toJSON(), ...updateModification.payload }; await targetCollection.update(oldDoc, newObject); - this.syncStep.progress(); } } @@ -156,7 +142,8 @@ export class DatawalletModificationsProcessor { this.ensureAllItemsAreCacheable(); - const cacheChangesGroupedByCollection = this.groupCacheChangesByCollection(this.cacheChanges); + const cacheChangesWithoutDeletes = this.cacheChanges.filter((c) => !this.deletes.some((d) => d.objectIdentifier.equals(c.objectIdentifier))); + const cacheChangesGroupedByCollection = this.groupCacheChangesByCollection(cacheChangesWithoutDeletes); const caches = await this.cacheFetcher.fetchCacheFor({ files: cacheChangesGroupedByCollection.fileIds, @@ -216,7 +203,6 @@ export class DatawalletModificationsProcessor { const item = (constructorOfT as any).from(itemDoc); item.setCache(c.cache); await collection.update(itemDoc, item); - this.syncStep.progress(); }) ); } @@ -229,7 +215,6 @@ export class DatawalletModificationsProcessor { for (const deleteModification of this.deletes) { const targetCollection = await this.collectionProvider.getCollection(deleteModification.collection); await targetCollection.delete({ id: deleteModification.objectIdentifier.toString() }); - this.syncStep.progress(); } } } diff --git a/packages/transport/src/modules/sync/SyncCallback.ts b/packages/transport/src/modules/sync/SyncCallback.ts deleted file mode 100644 index 4ce0c2b1e..000000000 --- a/packages/transport/src/modules/sync/SyncCallback.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ProgressReporter, ProgressReporterStep } from "../../core"; - -export type SyncProgressCallback = (percentage: number, step: SyncStep) => void; - -export class SyncProgressReporter extends ProgressReporter { - public static fromCallback(callback?: SyncProgressCallback): SyncProgressReporter { - return new SyncProgressReporter(callback); - } -} -export class SyncProgressReporterStep extends ProgressReporterStep {} - -export enum SyncStep { - Sync = "sync", - DatawalletSync = "sync:datawallet", - DatawalletSyncDownloading = "sync:datawallet:downloading", - DatawalletSyncDecryption = "sync:datawallet:decrypting", - DatawalletSyncProcessing = "sync:datawallet:processing", - ExternalEventsSync = "sync:externalEvents", - ExternalEventsSyncDownloading = "sync:externalEvents:downloading", - ExternalEventsProcessing = "sync:externalEvents:processing" -} diff --git a/packages/transport/src/modules/sync/SyncController.ts b/packages/transport/src/modules/sync/SyncController.ts index 4b6d00e84..ea966443e 100644 --- a/packages/transport/src/modules/sync/SyncController.ts +++ b/packages/transport/src/modules/sync/SyncController.ts @@ -3,22 +3,21 @@ import { log } from "@js-soft/ts-utils"; import { ControllerName, CoreDate, CoreError, CoreErrors, CoreId, RequestError, TransportController, TransportError, TransportLoggerFactory } from "../../core"; import { DependencyOverrides } from "../../core/DependencyOverrides"; import { AccountController } from "../accounts/AccountController"; +import { ChangedItems } from "./ChangedItems"; +import { DatawalletModificationMapper } from "./DatawalletModificationMapper"; +import { CacheFetcher, DatawalletModificationsProcessor } from "./DatawalletModificationsProcessor"; +import { WhatToSync } from "./WhatToSync"; import { BackboneDatawalletModification } from "./backbone/BackboneDatawalletModification"; import { BackboneSyncRun } from "./backbone/BackboneSyncRun"; import { CreateDatawalletModificationsRequestItem } from "./backbone/CreateDatawalletModifications"; import { FinalizeSyncRunRequestExternalEventResult } from "./backbone/FinalizeSyncRun"; import { StartSyncRunStatus, SyncRunType } from "./backbone/StartSyncRun"; import { ISyncClient, SyncClient } from "./backbone/SyncClient"; -import { ChangedItems } from "./ChangedItems"; import { ExternalEvent } from "./data/ExternalEvent"; -import { DatawalletModificationMapper } from "./DatawalletModificationMapper"; -import { CacheFetcher, DatawalletModificationsProcessor } from "./DatawalletModificationsProcessor"; import { ExternalEventProcessorRegistry } from "./externalEventProcessors"; import { DatawalletModification } from "./local/DatawalletModification"; import { DeviceMigrations } from "./migrations/DeviceMigrations"; import { IdentityMigrations } from "./migrations/IdentityMigrations"; -import { SyncProgressReporter, SyncStep } from "./SyncCallback"; -import { WhatToSync } from "./WhatToSync"; export class SyncController extends TransportController { private syncInfo: IDatabaseMap; @@ -67,10 +66,10 @@ export class SyncController extends TransportController { private currentSync?: LocalSyncRun; private currentSyncRun?: BackboneSyncRun; - public async sync(whatToSync: "OnlyDatawallet", reporter: SyncProgressReporter): Promise; - public async sync(whatToSync: "Everything", reporter: SyncProgressReporter): Promise; - public async sync(whatToSync: WhatToSync, reporter: SyncProgressReporter): Promise; - public async sync(whatToSync: WhatToSync = "Everything", reporter: SyncProgressReporter): Promise { + public async sync(whatToSync: "OnlyDatawallet"): Promise; + public async sync(whatToSync: "Everything"): Promise; + public async sync(whatToSync: WhatToSync): Promise; + public async sync(whatToSync: WhatToSync = "Everything"): Promise { if (this.currentSync?.includes(whatToSync)) { return await this.currentSync.promise; } @@ -79,10 +78,10 @@ export class SyncController extends TransportController { await this.currentSync.promise.catch(() => { // ignore this error because we only want to wait for the current sync to finish before we are starting a new one }); - return await this.sync(whatToSync, reporter); + return await this.sync(whatToSync); } - const syncPromise = this._syncAndResyncDatawallet(whatToSync, reporter); + const syncPromise = this._syncAndResyncDatawallet(whatToSync); this.currentSync = new LocalSyncRun(syncPromise, whatToSync); try { @@ -92,27 +91,24 @@ export class SyncController extends TransportController { } } - private async _syncAndResyncDatawallet(whatToSync: WhatToSync = "Everything", reporter: SyncProgressReporter) { + private async _syncAndResyncDatawallet(whatToSync: WhatToSync = "Everything") { try { - return await this._sync(whatToSync, reporter); + return await this._sync(whatToSync); } finally { if (this.datawalletEnabled && (await this.unpushedDatawalletModifications.exists())) { - await this.syncDatawallet(reporter).catch((e) => this.log.error(e)); + await this.syncDatawallet().catch((e) => this.log.error(e)); } } } @log() - private async _sync(whatToSync: WhatToSync, reporter: SyncProgressReporter): Promise { - const syncStep = reporter.createStep(SyncStep.Sync, 1); - + private async _sync(whatToSync: WhatToSync): Promise { if (whatToSync === "OnlyDatawallet") { - const value = await this.syncDatawallet(reporter); - syncStep.finish(); + const value = await this.syncDatawallet(); return value; } - const externalEventSyncResult = await this.syncExternalEvents(reporter); + const externalEventSyncResult = await this.syncExternalEvents(); await this.setLastCompletedSyncTime(); @@ -126,36 +122,34 @@ export class SyncController extends TransportController { ); } - syncStep.finish(); - if (this.datawalletEnabled && (await this.unpushedDatawalletModifications.exists())) { - await this.syncDatawallet(reporter).catch((e) => this.log.error(e)); + await this.syncDatawallet().catch((e) => this.log.error(e)); } return externalEventSyncResult.changedItems; } - private async syncExternalEvents(reporter: SyncProgressReporter): Promise<{ + private async syncExternalEvents(): Promise<{ externalEventResults: FinalizeSyncRunRequestExternalEventResult[]; changedItems: ChangedItems; }> { const syncRunWasStarted = await this.startExternalEventsSyncRun(); if (!syncRunWasStarted) { - await this.syncDatawallet(reporter); + await this.syncDatawallet(); return { changedItems: new ChangedItems(), externalEventResults: [] }; } - await this.applyIncomingDatawalletModifications(reporter); - const result = await this.applyIncomingExternalEvents(reporter); + await this.applyIncomingDatawalletModifications(); + const result = await this.applyIncomingExternalEvents(); await this.finalizeExternalEventsSyncRun(result.externalEventResults); return result; } @log() - private async syncDatawallet(reporter: SyncProgressReporter) { + private async syncDatawallet() { if (!this.datawalletEnabled) { return; } @@ -170,7 +164,7 @@ export class SyncController extends TransportController { this.log.trace("Synchronization of Datawallet events started..."); try { - await this.applyIncomingDatawalletModifications(reporter); + await this.applyIncomingDatawalletModifications(); await this.pushLocalDatawalletModifications(); await this.setLastCompletedDatawalletSyncTime(); @@ -256,22 +250,15 @@ export class SyncController extends TransportController { } } - private async applyIncomingDatawalletModifications(reporter: SyncProgressReporter) { - const datawalletSyncStep = reporter.createStep(SyncStep.DatawalletSync, 1); - - const downloadingStep = reporter.createStep(SyncStep.DatawalletSyncDownloading); - const getDatawalletModificationsResult = await this.client.getDatawalletModifications( - { localIndex: await this.getLocalDatawalletModificationIndex() }, - (percentage: number) => downloadingStep.manualReport(percentage) - ); + private async applyIncomingDatawalletModifications() { + const getDatawalletModificationsResult = await this.client.getDatawalletModifications({ localIndex: await this.getLocalDatawalletModificationIndex() }); const encryptedIncomingModifications = await getDatawalletModificationsResult.value.collect(); if (encryptedIncomingModifications.length === 0) { - datawalletSyncStep.finish(); return; } - const incomingModifications = await this.decryptDatawalletModifications(encryptedIncomingModifications, reporter); + const incomingModifications = await this.decryptDatawalletModifications(encryptedIncomingModifications); this.log.trace(`${incomingModifications.length} incoming modifications found`); @@ -279,8 +266,7 @@ export class SyncController extends TransportController { incomingModifications, this.cacheFetcher, this.db, - TransportLoggerFactory.getLogger(DatawalletModificationsProcessor), - reporter + TransportLoggerFactory.getLogger(DatawalletModificationsProcessor) ); await datawalletModificationsProcessor.execute(); @@ -288,8 +274,6 @@ export class SyncController extends TransportController { this.log.trace(`${incomingModifications.length} incoming modifications executed`, incomingModifications); await this.updateLocalDatawalletModificationIndex(encryptedIncomingModifications.sort(descending)[0].index); - - datawalletSyncStep.finish(); } private async promiseAllWithProgess(promises: Promise[], callback: (percentage: number) => void) { @@ -309,11 +293,9 @@ export class SyncController extends TransportController { return await Promise.all(promises); } - private async decryptDatawalletModifications(encryptedModifications: BackboneDatawalletModification[], reporter: SyncProgressReporter): Promise { + private async decryptDatawalletModifications(encryptedModifications: BackboneDatawalletModification[]): Promise { const promises = encryptedModifications.map((m) => this.decryptDatawalletModification(m)); - const step = reporter.createStep(SyncStep.DatawalletSyncDecryption); - - return await this.promiseAllWithProgess(promises, (p: number) => step.manualReport(p)); + return await Promise.all(promises); } private async decryptDatawalletModification(encryptedModification: BackboneDatawalletModification) { @@ -399,19 +381,13 @@ export class SyncController extends TransportController { return this.currentSyncRun !== undefined; } - private async applyIncomingExternalEvents(reporter: SyncProgressReporter) { - const externalEventStep = reporter.createStep(SyncStep.ExternalEventsSync, 1); - - const downloadingStep = reporter.createStep(SyncStep.ExternalEventsSyncDownloading); - const getExternalEventsResult = await this.client.getExternalEventsOfSyncRun(this.currentSyncRun!.id.toString(), (percentage: number) => - downloadingStep.manualReport(percentage) - ); + private async applyIncomingExternalEvents() { + const getExternalEventsResult = await this.client.getExternalEventsOfSyncRun(this.currentSyncRun!.id.toString()); if (getExternalEventsResult.isError) throw getExternalEventsResult.error; const externalEvents = await getExternalEventsResult.value.collect(); - const syncStep = reporter.createStep(SyncStep.ExternalEventsProcessing, externalEvents.length); const results: FinalizeSyncRunRequestExternalEventResult[] = []; const changedItems = new ChangedItems(); @@ -441,13 +417,9 @@ export class SyncController extends TransportController { externalEventId: externalEvent.id, errorCode: errorCode }); - } finally { - syncStep.progress(); } } - externalEventStep.finish(); - return { externalEventResults: results, changedItems: changedItems @@ -461,10 +433,11 @@ export class SyncController extends TransportController { const { backboneModifications, localModificationIds } = await this.prepareLocalDatawalletModificationsForPush(); - await this.client.finalizeExternalEventSync(this.currentSyncRun.id.toString(), { + const response = await this.client.finalizeExternalEventSync(this.currentSyncRun.id.toString(), { datawalletModifications: backboneModifications, externalEventResults: externalEventResults }); + if (response.isError) throw response.error; await this.deleteUnpushedDatawalletModifications(localModificationIds); diff --git a/packages/transport/src/modules/sync/externalEventProcessors/ExternalEventProcessorRegistry.ts b/packages/transport/src/modules/sync/externalEventProcessors/ExternalEventProcessorRegistry.ts index d2e21dadb..0383239c4 100644 --- a/packages/transport/src/modules/sync/externalEventProcessors/ExternalEventProcessorRegistry.ts +++ b/packages/transport/src/modules/sync/externalEventProcessors/ExternalEventProcessorRegistry.ts @@ -4,16 +4,18 @@ import { IdentityDeletionProcessChangedEventProcessor } from "./IdentityDeletion import { IdentityDeletionProcessStartedEventProcessor } from "./IdentityDeletionProcessStartedEventProcessor"; import { MessageDeliveredExternalEventProcessor } from "./MessageDeliveredExternalEventProcessor"; import { MessageReceivedExternalEventProcessor } from "./MessageReceivedExternalEventProcessor"; -import { RelationshipChangeCompletedExternalEventProcessor } from "./RelationshipChangeCompletedExternalEventProcessor"; -import { RelationshipChangeCreatedExternalEventProcessor } from "./RelationshipChangeCreatedExternalEventProcessor"; +import { RelationshipReactivationCompletedExternalEventProcessor } from "./RelationshipReactivationCompletedExternalEventProcessor"; +import { RelationshipReactivationRequestedExternalEventProcessor } from "./RelationshipReactivationRequestedExternalEventProcessor"; +import { RelationshipStatusChangedExternalEventProcessor } from "./RelationshipStatusChangedExternalEventProcessor"; export class ExternalEventProcessorRegistry { private readonly processors = new Map(); public constructor() { this.registerProcessor("MessageReceived", MessageReceivedExternalEventProcessor); this.registerProcessor("MessageDelivered", MessageDeliveredExternalEventProcessor); - this.registerProcessor("RelationshipChangeCreated", RelationshipChangeCreatedExternalEventProcessor); - this.registerProcessor("RelationshipChangeCompleted", RelationshipChangeCompletedExternalEventProcessor); + this.registerProcessor("RelationshipStatusChanged", RelationshipStatusChangedExternalEventProcessor); + this.registerProcessor("RelationshipReactivationRequested", RelationshipReactivationRequestedExternalEventProcessor); + this.registerProcessor("RelationshipReactivationCompleted", RelationshipReactivationCompletedExternalEventProcessor); this.registerProcessor("IdentityDeletionProcessStarted", IdentityDeletionProcessStartedEventProcessor); this.registerProcessor("IdentityDeletionProcessStatusChanged", IdentityDeletionProcessChangedEventProcessor); } diff --git a/packages/transport/src/modules/sync/externalEventProcessors/RelationshipChangeCompletedExternalEventProcessor.ts b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipChangeCompletedExternalEventProcessor.ts deleted file mode 100644 index 40c146f2d..000000000 --- a/packages/transport/src/modules/sync/externalEventProcessors/RelationshipChangeCompletedExternalEventProcessor.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Serializable, serialize, validate } from "@js-soft/ts-serval"; -import { RelationshipChangedEvent } from "../../../events"; -import { Relationship } from "../../relationships/local/Relationship"; -import { ExternalEvent } from "../data/ExternalEvent"; -import { ExternalEventProcessor } from "./ExternalEventProcessor"; - -class RelationshipChangeCompletedExternalEventData extends Serializable { - @serialize() - @validate() - public changeId: string; -} - -export class RelationshipChangeCompletedExternalEventProcessor extends ExternalEventProcessor { - public override async execute(externalEvent: ExternalEvent): Promise { - const payload = RelationshipChangeCompletedExternalEventData.fromAny(externalEvent.payload); - const relationship = await this.accountController.relationships.applyChangeById(payload.changeId); - - if (relationship) { - this.eventBus.publish(new RelationshipChangedEvent(this.ownAddress, relationship)); - return relationship; - } - return; - } -} diff --git a/packages/transport/src/modules/sync/externalEventProcessors/RelationshipReactivationCompletedExternalEventProcessor.ts b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipReactivationCompletedExternalEventProcessor.ts new file mode 100644 index 000000000..0a1328227 --- /dev/null +++ b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipReactivationCompletedExternalEventProcessor.ts @@ -0,0 +1,22 @@ +import { Serializable, serialize, validate } from "@js-soft/ts-serval"; +import { RelationshipChangedEvent, RelationshipReactivationCompletedEvent } from "../../../events"; +import { Relationship } from "../../relationships/local/Relationship"; +import { ExternalEvent } from "../data/ExternalEvent"; +import { ExternalEventProcessor } from "./ExternalEventProcessor"; + +class RelationshipReactivationCompletedExternalEventData extends Serializable { + @serialize() + @validate() + public relationshipId: string; +} + +export class RelationshipReactivationCompletedExternalEventProcessor extends ExternalEventProcessor { + public override async execute(externalEvent: ExternalEvent): Promise { + const payload = RelationshipReactivationCompletedExternalEventData.fromAny(externalEvent.payload); + const relationship = await this.accountController.relationships.applyRelationshipChangedEvent(payload.relationshipId); + + this.eventBus.publish(new RelationshipReactivationCompletedEvent(this.ownAddress, relationship)); + this.eventBus.publish(new RelationshipChangedEvent(this.ownAddress, relationship)); + return relationship; + } +} diff --git a/packages/transport/src/modules/sync/externalEventProcessors/RelationshipReactivationRequestedExternalEventProcessor.ts b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipReactivationRequestedExternalEventProcessor.ts new file mode 100644 index 000000000..9815ac52e --- /dev/null +++ b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipReactivationRequestedExternalEventProcessor.ts @@ -0,0 +1,22 @@ +import { Serializable, serialize, validate } from "@js-soft/ts-serval"; +import { RelationshipChangedEvent, RelationshipReactivationRequestedEvent } from "../../../events"; +import { Relationship } from "../../relationships/local/Relationship"; +import { ExternalEvent } from "../data/ExternalEvent"; +import { ExternalEventProcessor } from "./ExternalEventProcessor"; + +class RelationshipReactivationRequestedExternalEventData extends Serializable { + @serialize() + @validate() + public relationshipId: string; +} + +export class RelationshipReactivationRequestedExternalEventProcessor extends ExternalEventProcessor { + public override async execute(externalEvent: ExternalEvent): Promise { + const payload = RelationshipReactivationRequestedExternalEventData.fromAny(externalEvent.payload); + const relationship = await this.accountController.relationships.applyRelationshipChangedEvent(payload.relationshipId); + + this.eventBus.publish(new RelationshipReactivationRequestedEvent(this.ownAddress, relationship)); + this.eventBus.publish(new RelationshipChangedEvent(this.ownAddress, relationship)); + return relationship; + } +} diff --git a/packages/transport/src/modules/sync/externalEventProcessors/RelationshipChangeCreatedExternalEventProcessor.ts b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipStatusChangedExternalEventProcessor.ts similarity index 57% rename from packages/transport/src/modules/sync/externalEventProcessors/RelationshipChangeCreatedExternalEventProcessor.ts rename to packages/transport/src/modules/sync/externalEventProcessors/RelationshipStatusChangedExternalEventProcessor.ts index 065e0d82a..bedbb0453 100644 --- a/packages/transport/src/modules/sync/externalEventProcessors/RelationshipChangeCreatedExternalEventProcessor.ts +++ b/packages/transport/src/modules/sync/externalEventProcessors/RelationshipStatusChangedExternalEventProcessor.ts @@ -4,25 +4,18 @@ import { Relationship } from "../../relationships/local/Relationship"; import { ExternalEvent } from "../data/ExternalEvent"; import { ExternalEventProcessor } from "./ExternalEventProcessor"; -class RelationshipChangeCreatedExternalEventData extends Serializable { - @serialize() - @validate() - public changeId: string; - +class RelationshipStatusChangedExternalEventData extends Serializable { @serialize() @validate() public relationshipId: string; } -export class RelationshipChangeCreatedExternalEventProcessor extends ExternalEventProcessor { +export class RelationshipStatusChangedExternalEventProcessor extends ExternalEventProcessor { public override async execute(externalEvent: ExternalEvent): Promise { - const payload = RelationshipChangeCreatedExternalEventData.fromAny(externalEvent.payload); - const relationship = await this.accountController.relationships.applyChangeById(payload.changeId); + const payload = RelationshipStatusChangedExternalEventData.fromAny(externalEvent.payload); + const relationship = await this.accountController.relationships.applyRelationshipChangedEvent(payload.relationshipId); - if (relationship) { - this.eventBus.publish(new RelationshipChangedEvent(this.ownAddress, relationship)); - return relationship; - } - return; + this.eventBus.publish(new RelationshipChangedEvent(this.ownAddress, relationship)); + return relationship; } } diff --git a/packages/transport/src/modules/sync/externalEventProcessors/index.ts b/packages/transport/src/modules/sync/externalEventProcessors/index.ts index 459a24f96..0ef994097 100644 --- a/packages/transport/src/modules/sync/externalEventProcessors/index.ts +++ b/packages/transport/src/modules/sync/externalEventProcessors/index.ts @@ -2,5 +2,6 @@ export * from "./ExternalEventProcessorRegistry"; export * from "./IdentityDeletionProcessStartedEventProcessor"; export * from "./MessageDeliveredExternalEventProcessor"; export * from "./MessageReceivedExternalEventProcessor"; -export * from "./RelationshipChangeCompletedExternalEventProcessor"; -export * from "./RelationshipChangeCreatedExternalEventProcessor"; +export * from "./RelationshipReactivationCompletedExternalEventProcessor"; +export * from "./RelationshipReactivationRequestedExternalEventProcessor"; +export * from "./RelationshipStatusChangedExternalEventProcessor"; diff --git a/packages/transport/src/modules/tokens/TokenController.ts b/packages/transport/src/modules/tokens/TokenController.ts index 500b03f9f..84b19d0f1 100644 --- a/packages/transport/src/modules/tokens/TokenController.ts +++ b/packages/transport/src/modules/tokens/TokenController.ts @@ -115,12 +115,20 @@ export class TokenController extends TransportController { const decryptionPromises = backboneTokens.map(async (t) => { const tokenDoc = await this.tokens.read(t.id); + if (!tokenDoc) { + this._log.error( + `Token '${t.id}' not found in local database and the cache fetching was therefore skipped. This should not happen and might be a bug in the application logic.` + ); + return; + } + const token = Token.from(tokenDoc); return { id: CoreId.from(t), cache: await this.decryptToken(t, token.secretKey) }; }); - return await Promise.all(decryptionPromises); + const caches = await Promise.all(decryptionPromises); + return caches.filter((c) => c !== undefined); } @log() @@ -212,4 +220,12 @@ export class TokenController extends TransportController { return token; } + + public async cleanupTokensOfDecomposedRelationship(peer: CoreAddress): Promise { + const tokenDocs = await this.getTokens({ "cache.createdBy": peer.toString() }); + const tokens = this.parseArray(tokenDocs, Token); + for await (const token of tokens) { + await this.tokens.delete(token); + } + } } diff --git a/packages/transport/src/util/Random.ts b/packages/transport/src/util/Random.ts index 2fdf92b46..9290ac270 100644 --- a/packages/transport/src/util/Random.ts +++ b/packages/transport/src/util/Random.ts @@ -26,6 +26,7 @@ export interface RandomCharacterBucket { } export interface IRandom {} + export interface IRandomStatic { new (): IRandom; bytes(length: number): Promise; diff --git a/packages/transport/test/core/backbone/Authentication.test.ts b/packages/transport/test/core/backbone/Authentication.test.ts index 82607f06e..5a2d55df1 100644 --- a/packages/transport/test/core/backbone/Authentication.test.ts +++ b/packages/transport/test/core/backbone/Authentication.test.ts @@ -96,6 +96,7 @@ describe("AuthenticationTest", function () { expect(requests[0].method).toBe("post"); expect(requests[0].url).toMatch(/^\/connect\/token/); }); + test("should throw correct error on authentication issues", async function () { setAuthTokenToExpired(testAccount); diff --git a/packages/transport/test/end2end/End2End.test.ts b/packages/transport/test/end2end/End2End.test.ts index 333b5b169..31c153ab5 100644 --- a/packages/transport/test/end2end/End2End.test.ts +++ b/packages/transport/test/end2end/End2End.test.ts @@ -1,16 +1,7 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { JSONWrapper, Serializable } from "@js-soft/ts-serval"; import { CoreBuffer } from "@nmshd/crypto"; -import { - AccountController, - CoreDate, - FileReference, - RelationshipChangeStatus, - RelationshipChangeType, - RelationshipStatus, - TokenContentRelationshipTemplate, - Transport -} from "../../src"; +import { AccountController, CoreDate, CoreId, FileReference, RelationshipAuditLogEntryReason, RelationshipStatus, TokenContentRelationshipTemplate, Transport } from "../../src"; import { TestUtil } from "../testHelpers/TestUtil"; describe("AccountTest", function () { @@ -92,10 +83,9 @@ describe("RelationshipTest: Accept", function () { expect(templateContent.value).toHaveProperty("mycontent"); expect(templateContent.value.mycontent).toBe("template"); - // Send Request const request = await to.relationships.sendRelationship({ template: templateTo, - content: { + creationContent: { mycontent: "request" } }); @@ -107,11 +97,11 @@ describe("RelationshipTest: Accept", function () { expect(request.cache!.template.id.toString()).toStrictEqual(templateTo.id.toString()); expect(request.cache!.template.isOwn).toBe(false); - expect(request.cache!.creationChange.type).toStrictEqual(RelationshipChangeType.Creation); - expect(request.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Pending); expect(request.status).toStrictEqual(RelationshipStatus.Pending); - // Accept relationship + expect(request.cache?.auditLog).toHaveLength(1); + expect(request.cache!.auditLog[0].newStatus).toBe(RelationshipStatus.Pending); + const syncedRelationships = await TestUtil.syncUntilHasRelationships(from); expect(syncedRelationships).toHaveLength(1); const pendingRelationship = syncedRelationships[0]; @@ -123,37 +113,28 @@ describe("RelationshipTest: Accept", function () { expect(templateResponseContent.value).toHaveProperty("mycontent"); expect(templateResponseContent.value.mycontent).toBe("template"); - expect(pendingRelationship.cache!.creationChange.type).toStrictEqual(RelationshipChangeType.Creation); - expect(pendingRelationship.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Pending); expect(pendingRelationship.status).toStrictEqual(RelationshipStatus.Pending); - const acceptedRelationshipFromSelf = await from.relationships.acceptChange(pendingRelationship.cache!.creationChange, { - mycontent: "acceptContent" - }); + const acceptedRelationshipFromSelf = await from.relationships.accept(relationshipId); expect(acceptedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); expect(acceptedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Active); - expect(acceptedRelationshipFromSelf.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Accepted); - expect(acceptedRelationshipFromSelf.peer).toBeDefined(); - expect(acceptedRelationshipFromSelf.peer.address.toString()).toStrictEqual(acceptedRelationshipFromSelf.cache!.creationChange.request.createdBy.toString()); - const acceptedContentSelf = acceptedRelationshipFromSelf.cache!.creationChange.response?.content as any; - expect(acceptedContentSelf).toBeInstanceOf(JSONWrapper); - expect(acceptedContentSelf.value.mycontent).toBe("acceptContent"); + expect(acceptedRelationshipFromSelf.cache?.auditLog).toHaveLength(2); + expect(acceptedRelationshipFromSelf.cache!.auditLog[1].newStatus).toBe(RelationshipStatus.Active); + + expect(acceptedRelationshipFromSelf.peer).toBeDefined(); - // Get accepted relationship const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(to); expect(syncedRelationshipsPeer).toHaveLength(1); const acceptedRelationshipPeer = syncedRelationshipsPeer[0]; expect(acceptedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); expect(acceptedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Active); - expect(acceptedRelationshipPeer.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Accepted); + + expect(acceptedRelationshipPeer.cache?.auditLog).toHaveLength(2); + expect(acceptedRelationshipPeer.cache!.auditLog[1].newStatus).toBe(RelationshipStatus.Active); expect(acceptedRelationshipPeer.peer).toBeDefined(); expect(acceptedRelationshipPeer.peer.address.toString()).toStrictEqual(templateTo.cache?.identity.address.toString()); - - const acceptedContentPeer = acceptedRelationshipPeer.cache!.creationChange.response?.content as any; - expect(acceptedContentPeer).toBeInstanceOf(JSONWrapper); - expect(acceptedContentPeer.value.mycontent).toBe("acceptContent"); }); }); @@ -218,7 +199,7 @@ describe("RelationshipTest: Reject", function () { const request = await to.relationships.sendRelationship({ template: templateTo, - content: { + creationContent: { mycontent: "request" } }); @@ -230,11 +211,8 @@ describe("RelationshipTest: Reject", function () { expect(request.cache!.template.id.toString()).toStrictEqual(templateTo.id.toString()); expect(request.cache!.template.isOwn).toBe(false); - expect(request.cache!.creationChange.type).toStrictEqual(RelationshipChangeType.Creation); - expect(request.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Pending); expect(request.status).toStrictEqual(RelationshipStatus.Pending); - // Reject relationship const syncedRelationships = await TestUtil.syncUntilHasRelationships(from); expect(syncedRelationships).toHaveLength(1); const pendingRelationship = syncedRelationships[0]; @@ -245,24 +223,16 @@ describe("RelationshipTest: Reject", function () { expect(templateResponseContent.value).toHaveProperty("mycontent"); expect(templateResponseContent.value.mycontent).toBe("template"); - expect(pendingRelationship.cache!.creationChange.type).toStrictEqual(RelationshipChangeType.Creation); - expect(pendingRelationship.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Pending); expect(pendingRelationship.status).toStrictEqual(RelationshipStatus.Pending); - const rejectedRelationshipFromSelf = await from.relationships.rejectChange(pendingRelationship.cache!.creationChange, { - mycontent: "rejectContent" - }); + const rejectedRelationshipFromSelf = await from.relationships.reject(relationshipId); expect(rejectedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); expect(rejectedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Rejected); - expect(rejectedRelationshipFromSelf.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Rejected); - expect(rejectedRelationshipFromSelf.peer).toBeDefined(); - expect(rejectedRelationshipFromSelf.peer.address.toString()).toStrictEqual(rejectedRelationshipFromSelf.cache!.creationChange.request.createdBy.toString()); + expect(rejectedRelationshipFromSelf.cache?.auditLog).toHaveLength(2); + expect(rejectedRelationshipFromSelf.cache!.auditLog[1].newStatus).toBe(RelationshipStatus.Rejected); - const rejectionContentSelf = rejectedRelationshipFromSelf.cache!.creationChange.response?.content as any; - expect(rejectionContentSelf).toBeInstanceOf(JSONWrapper); - expect(rejectionContentSelf.value.mycontent).toBe("rejectContent"); + expect(rejectedRelationshipFromSelf.peer).toBeDefined(); - // Get accepted relationship const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(to); expect(syncedRelationshipsPeer).toHaveLength(1); const rejectedRelationshipPeer = syncedRelationshipsPeer[0]; @@ -270,13 +240,10 @@ describe("RelationshipTest: Reject", function () { expect(rejectedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); expect(rejectedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Rejected); - expect(rejectedRelationshipPeer.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Rejected); + expect(rejectedRelationshipPeer.cache?.auditLog).toHaveLength(2); + expect(rejectedRelationshipPeer.cache!.auditLog[1].newStatus).toBe(RelationshipStatus.Rejected); expect(rejectedRelationshipPeer.peer).toBeDefined(); expect(rejectedRelationshipPeer.peer.address.toString()).toStrictEqual(templateTo.cache?.identity.address.toString()); - - const rejectionContentPeer = rejectedRelationshipPeer.cache!.creationChange.response?.content as any; - expect(rejectionContentPeer).toBeInstanceOf(JSONWrapper); - expect(rejectionContentPeer.value.mycontent).toBe("rejectContent"); }); }); @@ -344,7 +311,7 @@ describe("RelationshipTest: Revoke", function () { const request = await requestor.relationships.sendRelationship({ template: templateRequestor, - content: { + creationContent: { mycontent: "request" } }); @@ -357,11 +324,8 @@ describe("RelationshipTest: Revoke", function () { expect(request.cache!.template.id.toString()).toStrictEqual(templateRequestor.id.toString()); expect(request.cache!.template.isOwn).toBe(false); - expect(request.cache!.creationChange.type).toStrictEqual(RelationshipChangeType.Creation); - expect(request.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Pending); expect(request.status).toStrictEqual(RelationshipStatus.Pending); - // Revoke relationship const syncedRelationships = await TestUtil.syncUntilHasRelationships(templator); expect(syncedRelationships).toHaveLength(1); const pendingRelationship = syncedRelationships[0]; @@ -373,39 +337,27 @@ describe("RelationshipTest: Revoke", function () { const templateResponseContent = pendingRelationship.cache!.template.cache!.content as JSONWrapper; expect(templateResponseContent.value).toHaveProperty("mycontent"); expect(templateResponseContent.value.mycontent).toBe("template"); - - expect(pendingRelationship.cache!.creationChange.type).toStrictEqual(RelationshipChangeType.Creation); - expect(pendingRelationship.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Pending); expect(pendingRelationship.status).toStrictEqual(RelationshipStatus.Pending); - const revokedRelationshipSelf = await requestor.relationships.revokeChange(pendingRelationship.cache!.creationChange, { - mycontent: "revokeContent" - }); + const revokedRelationshipSelf = await requestor.relationships.revoke(relationshipId); expect(revokedRelationshipSelf.status).toStrictEqual(RelationshipStatus.Revoked); expect(revokedRelationshipSelf.id.toString()).toStrictEqual(relationshipId.toString()); expect(revokedRelationshipSelf.status).toStrictEqual(RelationshipStatus.Revoked); - expect(revokedRelationshipSelf.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Revoked); + expect(revokedRelationshipSelf.cache?.auditLog).toHaveLength(2); + expect(revokedRelationshipSelf.cache!.auditLog[1].newStatus).toBe(RelationshipStatus.Revoked); expect(revokedRelationshipSelf.peer).toBeDefined(); expect(revokedRelationshipSelf.peer.address.toString()).toStrictEqual(revokedRelationshipSelf.cache!.template.cache?.identity.address.toString()); - const revocationContentSelf = revokedRelationshipSelf.cache!.creationChange.response?.content as any; - expect(revocationContentSelf).toBeInstanceOf(JSONWrapper); - expect(revocationContentSelf.value.mycontent).toBe("revokeContent"); - // Get revoked relationship const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(templator); expect(syncedRelationshipsPeer).toHaveLength(1); const revokedRelationshipPeer = syncedRelationshipsPeer[0]; expect(revokedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Revoked); expect(revokedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); expect(revokedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Revoked); - expect(revokedRelationshipPeer.cache!.creationChange.status).toStrictEqual(RelationshipChangeStatus.Revoked); + expect(revokedRelationshipPeer.cache?.auditLog).toHaveLength(2); + expect(revokedRelationshipPeer.cache!.auditLog[1].newStatus).toBe(RelationshipStatus.Revoked); expect(revokedRelationshipPeer.peer).toBeDefined(); - expect(revokedRelationshipPeer.peer.address.toString()).toStrictEqual(revokedRelationshipPeer.cache!.creationChange.request.createdBy.toString()); - - const revocationContentPeer = revokedRelationshipPeer.cache!.creationChange.response?.content as any; - expect(revocationContentPeer).toBeInstanceOf(JSONWrapper); - expect(revocationContentPeer.value.mycontent).toBe("revokeContent"); }); test("should handle an incoming relationship request which was already revoked by the sender", async function () { @@ -443,29 +395,567 @@ describe("RelationshipTest: Revoke", function () { const pendingRelationship = await requestor.relationships.sendRelationship({ template: templateRequestor, - content: { + creationContent: { mycontent: "request" } }); - // Revoke relationship - const revokedRelationshipSelf = await requestor.relationships.revokeChange(pendingRelationship.cache!.creationChange, { - mycontent: "revokeContent" - }); + const revokedRelationshipSelf = await requestor.relationships.revoke(pendingRelationship.id); expect(revokedRelationshipSelf.status).toStrictEqual(RelationshipStatus.Revoked); - const revocationContentSelf = revokedRelationshipSelf.cache!.creationChange.response?.content as any; - expect(revocationContentSelf).toBeInstanceOf(JSONWrapper); - expect(revocationContentSelf.value.mycontent).toBe("revokeContent"); - // Get revoked relationship - const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(templator); - expect(syncedRelationshipsPeer).toHaveLength(1); - const revokedRelationshipPeer = syncedRelationshipsPeer[0]; + await TestUtil.syncUntilHasRelationships(templator, 2); // wait for pending and revoked + const relationshipsPeer = await templator.relationships.getRelationships({}); + expect(relationshipsPeer).toHaveLength(1); + const revokedRelationshipPeer = relationshipsPeer[0]; expect(revokedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Revoked); + }); +}); + +describe("RelationshipTest: Terminate", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should terminate a relationship between two accounts", async function () { + const relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + + const terminatedRelationshipFromSelf = await from.relationships.terminate(relationshipId); + expect(terminatedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); + expect(terminatedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Terminated); + expect(terminatedRelationshipFromSelf.cache?.auditLog).toHaveLength(3); + expect(terminatedRelationshipFromSelf.cache!.auditLog[2].reason).toBe(RelationshipAuditLogEntryReason.Termination); + + const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(to); + expect(syncedRelationshipsPeer).toHaveLength(1); + const terminatedRelationshipPeer = syncedRelationshipsPeer[0]; + + expect(terminatedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); + expect(terminatedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Terminated); + expect(terminatedRelationshipPeer.cache?.auditLog).toHaveLength(3); + expect(terminatedRelationshipPeer.cache!.auditLog[2].reason).toBe(RelationshipAuditLogEntryReason.Termination); + }); +}); + +describe("RelationshipTest: Request Reactivation", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + await from.relationships.terminate(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should request reactivating a relationship between two accounts", async function () { + const reactivationRequestedRelationshipFromSelf = await from.relationships.requestReactivation(relationshipId); + + expect(reactivationRequestedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); + expect(reactivationRequestedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Terminated); + expect(reactivationRequestedRelationshipFromSelf.cache?.auditLog).toHaveLength(4); + expect(reactivationRequestedRelationshipFromSelf.cache!.auditLog[3].reason).toBe(RelationshipAuditLogEntryReason.ReactivationRequested); + + const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(to); + expect(syncedRelationshipsPeer).toHaveLength(1); + const reactivationRequestedRelationshipPeer = syncedRelationshipsPeer[0]; + + expect(reactivationRequestedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); + expect(reactivationRequestedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Terminated); + expect(reactivationRequestedRelationshipPeer.cache?.auditLog).toHaveLength(4); + expect(reactivationRequestedRelationshipPeer.cache!.auditLog[3].reason).toBe(RelationshipAuditLogEntryReason.ReactivationRequested); + }); +}); + +describe("RelationshipTest: Accept Reactivation", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + await from.relationships.terminate(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + await from.relationships.requestReactivation(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should accept the reactivation of a relationship between two accounts", async function () { + const acceptedReactivatedRelationshipPeer = await to.relationships.acceptReactivation(relationshipId); + expect(acceptedReactivatedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); + expect(acceptedReactivatedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Active); + expect(acceptedReactivatedRelationshipPeer.cache?.auditLog).toHaveLength(5); + expect(acceptedReactivatedRelationshipPeer.cache!.auditLog[4].reason).toBe(RelationshipAuditLogEntryReason.AcceptanceOfReactivation); + + const syncedRelationshipsFromSelf = await TestUtil.syncUntilHasRelationships(from); + expect(syncedRelationshipsFromSelf).toHaveLength(1); + const acceptedReactivatedRelationshipFromSelf = syncedRelationshipsFromSelf[0]; + + expect(acceptedReactivatedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); + expect(acceptedReactivatedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Active); + expect(acceptedReactivatedRelationshipFromSelf.cache?.auditLog).toHaveLength(5); + expect(acceptedReactivatedRelationshipFromSelf.cache!.auditLog[4].reason).toBe(RelationshipAuditLogEntryReason.AcceptanceOfReactivation); + }); +}); + +describe("RelationshipTest: Reject Reactivation", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + await from.relationships.terminate(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + await from.relationships.requestReactivation(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should reject the reactivation of a relationship between two accounts", async function () { + const rejectedReactivatedRelationshipPeer = await to.relationships.rejectReactivation(relationshipId); + expect(rejectedReactivatedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); + expect(rejectedReactivatedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Terminated); + expect(rejectedReactivatedRelationshipPeer.cache?.auditLog).toHaveLength(5); + expect(rejectedReactivatedRelationshipPeer.cache!.auditLog[4].reason).toBe(RelationshipAuditLogEntryReason.RejectionOfReactivation); + + const syncedRelationshipsFromSelf = await TestUtil.syncUntilHasRelationships(from); + expect(syncedRelationshipsFromSelf).toHaveLength(1); + const rejectedReactivatedRelationshipFromSelf = syncedRelationshipsFromSelf[0]; + + expect(rejectedReactivatedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); + expect(rejectedReactivatedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Terminated); + expect(rejectedReactivatedRelationshipFromSelf.cache?.auditLog).toHaveLength(5); + expect(rejectedReactivatedRelationshipFromSelf.cache!.auditLog[4].reason).toBe(RelationshipAuditLogEntryReason.RejectionOfReactivation); + }); +}); + +describe("RelationshipTest: Revoke Reactivation", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + await from.relationships.terminate(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + await from.relationships.requestReactivation(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should revoke the reactivation of a relationship between two accounts", async function () { + const revokedReactivatedRelationshipFromSelf = await from.relationships.revokeReactivation(relationshipId); + expect(revokedReactivatedRelationshipFromSelf.id.toString()).toStrictEqual(relationshipId.toString()); + expect(revokedReactivatedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Terminated); + expect(revokedReactivatedRelationshipFromSelf.cache?.auditLog).toHaveLength(5); + expect(revokedReactivatedRelationshipFromSelf.cache!.auditLog[4].reason).toBe(RelationshipAuditLogEntryReason.RevocationOfReactivation); + + const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(to); + expect(syncedRelationshipsPeer).toHaveLength(1); + const revokedReactivatedRelationshipPeer = syncedRelationshipsPeer[0]; + + expect(revokedReactivatedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); + expect(revokedReactivatedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Terminated); + expect(revokedReactivatedRelationshipPeer.cache?.auditLog).toHaveLength(5); + expect(revokedReactivatedRelationshipPeer.cache!.auditLog[4].reason).toBe(RelationshipAuditLogEntryReason.RevocationOfReactivation); + }); +}); + +describe("RelationshipTest: Decompose", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should request decomposing a relationship", async function () { + const relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + await from.relationships.terminate(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + + await from.relationships.decompose(relationshipId); + const decomposedRelationship = await from.relationships.getRelationship(relationshipId); + expect(decomposedRelationship).toBeUndefined(); + + const syncedRelationshipsPeer = await TestUtil.syncUntilHasRelationships(to); + expect(syncedRelationshipsPeer).toHaveLength(1); + const decomposedRelationshipPeer = syncedRelationshipsPeer[0]; + + expect(decomposedRelationshipPeer.id.toString()).toStrictEqual(relationshipId.toString()); + expect(decomposedRelationshipPeer.status).toStrictEqual(RelationshipStatus.DeletionProposed); + expect(decomposedRelationshipPeer.cache?.auditLog).toHaveLength(4); + expect(decomposedRelationshipPeer.cache!.auditLog[3].reason).toBe(RelationshipAuditLogEntryReason.Decomposition); + + await expect(to.relationships.decompose(relationshipId)).resolves.not.toThrow(); + }); +}); + +describe("RelationshipTest: validations for non-existent record", function () { + let connection: IDatabaseConnection; + + let transport: Transport; + let from: AccountController; + const fakeRelationshipId = CoreId.from("REL00000000000000000"); + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 1); + from = accounts[0]; + }); + + afterAll(async function () { + await from.close(); + await connection.close(); + }); + + test("should not accept a relationship", async function () { + await expect(from.relationships.accept(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not reject a relationship", async function () { + await expect(from.relationships.reject(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not revoke a relationship", async function () { + await expect(from.relationships.revoke(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not terminate a relationship", async function () { + await expect(from.relationships.terminate(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not request a relationship reactivation", async function () { + await expect(from.relationships.requestReactivation(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not accept a relationship reactivation", async function () { + await expect(from.relationships.acceptReactivation(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not reject a relationship reactivation", async function () { + await expect(from.relationships.rejectReactivation(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not revoke a relationship reactivation", async function () { + await expect(from.relationships.revokeReactivation(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should not decompose a relationship", async function () { + await expect(from.relationships.decompose(fakeRelationshipId)).rejects.toThrow("error.transport.recordNotFound"); + }); +}); + +describe("RelationshipTest: validations (on terminated relationship)", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + await from.relationships.terminate(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should not terminate a relationship in status terminated again", async function () { + await expect(from.relationships.terminate(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("reactivation revocation should fail without reactivation request", async function () { + await expect(from.relationships.rejectReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.reactivationNotRequested"); + }); + + test("reactivation acceptance should fail without reactivation request", async function () { + await expect(from.relationships.acceptReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.reactivationNotRequested"); + }); + + test("reactivation rejection should fail without reactivation request", async function () { + await expect(from.relationships.rejectReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.reactivationNotRequested"); + }); + + describe("after reactivation request", function () { + beforeAll(async function () { + await from.relationships.requestReactivation(relationshipId); + await TestUtil.syncUntilHasRelationships(to); + }); + + test("reactivation revocation should fail when the wrong side revokes it", async function () { + await expect(to.relationships.revokeReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("reactivation acceptance should fail when the wrong side accepts it", async function () { + await expect(from.relationships.acceptReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("reactivation rejection should fail when the wrong side rejects it", async function () { + await expect(from.relationships.rejectReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("requesting reactivation twice should fail", async function () { + await expect(from.relationships.requestReactivation(relationshipId)).rejects.toThrow( + "error.transport.relationships.reactivationAlreadyRequested: 'You have already requested the reactivation" + ); + + await expect(to.relationships.requestReactivation(relationshipId)).rejects.toThrow( + "error.transport.relationships.reactivationAlreadyRequested: 'Your peer has already requested the reactivation" + ); + }); + }); +}); + +describe("RelationshipTest: operation executioner validation (on pending relationship)", function () { + let connection: IDatabaseConnection; + let transport: Transport; + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + + const templateFrom = await from.relationshipTemplates.sendRelationshipTemplate({ + content: { + mycontent: "template" + }, + expiresAt: CoreDate.utc().add({ minutes: 5 }), + maxNumberOfAllocations: 1 + }); + + const templateToken = TokenContentRelationshipTemplate.from({ + templateId: templateFrom.id, + secretKey: templateFrom.secretKey + }); + + const token = await from.tokens.sendToken({ + content: templateToken, + expiresAt: CoreDate.utc().add({ hours: 12 }), + ephemeral: false + }); + + const tokenRef = token.truncate(); + + const receivedToken = await to.tokens.loadPeerTokenByTruncated(tokenRef, false); + + if (!(receivedToken.cache!.content instanceof TokenContentRelationshipTemplate)) { + throw new Error("token content not instanceof TokenContentRelationshipTemplate"); + } + + const templateTo = await to.relationshipTemplates.loadPeerRelationshipTemplate(receivedToken.cache!.content.templateId, receivedToken.cache!.content.secretKey); + const request = await to.relationships.sendRelationship({ + template: templateTo, + creationContent: { + mycontent: "request" + } + }); + relationshipId = request.id; + await TestUtil.syncUntilHasRelationships(from); + }); + + afterAll(async function () { + await from.close(); + await to.close(); + + await connection.close(); + }); + + test("you should not accept your own relationship", async function () { + await expect(to.relationships.accept(relationshipId)).rejects.toThrow("error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("you should not reject your own relationship", async function () { + await expect(to.relationships.reject(relationshipId)).rejects.toThrow("error.transport.relationships.operationOnlyAllowedForPeer"); + }); + + test("you should not revoke your peer's relationship", async function () { + await expect(from.relationships.revoke(relationshipId)).rejects.toThrow("error.transport.relationships.operationOnlyAllowedForPeer"); + }); +}); + +describe("RelationshipTest: relationship status validation (on active relationship)", function () { + let connection: IDatabaseConnection; + let transport: Transport; + + let from: AccountController; + let to: AccountController; + let relationshipId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 2); + from = accounts[0]; + to = accounts[1]; + relationshipId = (await TestUtil.addRelationship(from, to)).acceptedRelationshipFromSelf.id; + }); + + afterAll(async function () { + await from.close(); + await to.close(); + await connection.close(); + }); + + test("should not reactivate a relationship", async function () { + await expect(from.relationships.requestReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not accept a relationship reactivation", async function () { + await expect(from.relationships.acceptReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not reject a relationship reactivation", async function () { + await expect(from.relationships.rejectReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not revoke a relationship reactivation", async function () { + await expect(from.relationships.revokeReactivation(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not decompose a relationship", async function () { + await expect(from.relationships.decompose(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not accept a relationship", async function () { + await expect(from.relationships.accept(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); + + test("should not reject a relationship", async function () { + await expect(from.relationships.reject(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); + }); - const revocationContentPeer = revokedRelationshipPeer.cache!.creationChange.response?.content as any; - expect(revocationContentPeer).toBeInstanceOf(JSONWrapper); - expect(revocationContentPeer.value.mycontent).toBe("revokeContent"); + test("should not revoke a relationship", async function () { + await expect(from.relationships.revoke(relationshipId)).rejects.toThrow("error.transport.relationships.wrongRelationshipStatus"); }); }); diff --git a/packages/transport/test/modules/PublicAPI.test.ts b/packages/transport/test/modules/PublicAPI.test.ts index 09c228e12..f05c8ba47 100644 --- a/packages/transport/test/modules/PublicAPI.test.ts +++ b/packages/transport/test/modules/PublicAPI.test.ts @@ -108,23 +108,23 @@ publicFunctions[RelationshipsController.name] = [ nameof((r) => r.verify), nameof((r) => r.verifyIdentity), nameof((r) => r.sendRelationship), - nameof((r) => r.acceptChange), - nameof((r) => r.rejectChange), - nameof((r) => r.revokeChange), + nameof((r) => r.accept), + nameof((r) => r.reject), + nameof((r) => r.revoke), nameof((r) => r.updateCache) ]; publicFunctions[RelationshipSecretController.name] = [ nameof((r) => r.init), nameof((r) => r.createRequestorSecrets), nameof((r) => r.createTemplatorSecrets), - nameof((r) => r.getPublicResponse), + nameof((r) => r.getPublicCreationResponseContentCrypto), nameof((r) => r.convertSecrets), - nameof((r) => r.deleteSecretForRequest), + nameof((r) => r.deleteSecretForRelationship), nameof((r) => r.decryptTemplate), nameof((r) => r.verifyTemplate), - nameof((r) => r.encryptRequest), + nameof((r) => r.encryptCreationContent), nameof((r) => r.encrypt), - nameof((r) => r.decryptRequest), + nameof((r) => r.decryptCreationContent), nameof((r) => r.createTemplateKey), nameof((r) => r.decryptPeer), nameof((r) => r.verifyOwn), diff --git a/packages/transport/test/modules/challenges/Challenges.test.ts b/packages/transport/test/modules/challenges/Challenges.test.ts index 99a39c64a..44ad045cf 100644 --- a/packages/transport/test/modules/challenges/Challenges.test.ts +++ b/packages/transport/test/modules/challenges/Challenges.test.ts @@ -54,4 +54,11 @@ describe("ChallengeTest", function () { expect(validationResult.isValid).toBe(true); expect(validationResult.correspondingRelationship).toBeDefined(); }); + + test("should not create a relationship challenge on terminated relationship", async function () { + const terminatedRelationship = (await TestUtil.terminateRelationship(recipient, sender)).terminatedRelationshipPeer; + await expect(sender.challenges.createChallenge(ChallengeType.Relationship, terminatedRelationship)).rejects.toThrow( + "error.transport.challenges.challengeTypeRequiresActiveRelationship" + ); + }); }); diff --git a/packages/transport/test/modules/devices/DeviceDeletion.test.ts b/packages/transport/test/modules/devices/DeviceDeletion.test.ts index 2da426a4f..405181509 100644 --- a/packages/transport/test/modules/devices/DeviceDeletion.test.ts +++ b/packages/transport/test/modules/devices/DeviceDeletion.test.ts @@ -46,7 +46,7 @@ describe("Device Deletion", function () { await accountController.syncDatawallet(); await deviceTest.onboardDevice(await accountController.devices.getSharedSecret(newDevice.id)); - await expect(accountController.devices.delete(newDevice)).rejects.toThrow("Could not delete device: Backbone did not authorize deletion."); + await expect(accountController.devices.delete(newDevice)).rejects.toThrow("Could not delete device: 'Backbone did not authorize deletion.'"); const devices = await accountController.devices.list(); const deviceIds = devices.map((d) => d.id.toString()); @@ -60,7 +60,7 @@ describe("Device Deletion", function () { await accountController.syncDatawallet(); const deviceToDelete = await accountController.devices.get(newDevice.id); - await expect(accountController.devices.delete(deviceToDelete!)).rejects.toThrow("Could not delete device: Device is already onboarded."); + await expect(accountController.devices.delete(deviceToDelete!)).rejects.toThrow("Could not delete device: 'Device is already onboarded.'"); const devices = await accountController.devices.list(); const deviceIds = devices.map((d) => d.id.toString()); diff --git a/packages/transport/test/modules/devices/SetCommunicationLanguage.test.ts b/packages/transport/test/modules/devices/SetCommunicationLanguage.test.ts new file mode 100644 index 000000000..8f3461044 --- /dev/null +++ b/packages/transport/test/modules/devices/SetCommunicationLanguage.test.ts @@ -0,0 +1,38 @@ +import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { AccountController } from "../../../src"; +import { AppDeviceTest } from "../../testHelpers/AppDeviceTest"; +import { DeviceTestParameters } from "../../testHelpers/DeviceTestParameters"; +import { TestUtil } from "../../testHelpers/TestUtil"; + +describe("set the communication language", function () { + let connection: IDatabaseConnection; + + let deviceTest: AppDeviceTest; + + let deviceAccount: AccountController; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + + const parameters = new DeviceTestParameters({ ...TestUtil.createConfig(), datawalletEnabled: true }, connection, TestUtil.loggerFactory); + deviceTest = new AppDeviceTest(parameters); + + await deviceTest.init(); + + deviceAccount = await deviceTest.createAccount(); + }); + + afterAll(async function () { + await deviceTest.close(); + + await connection.close(); + }); + + test("should set the communication language", async function () { + await expect(deviceAccount.activeDevice.setCommunicationLanguage("fr")).resolves.not.toThrow(); + }); + + test("should not set a false communication language", async function () { + await expect(deviceAccount.activeDevice.setCommunicationLanguage("fra")).rejects.toThrow("error.platform.validation.invalidDeviceCommunicationLanguage (400)"); + }); +}); diff --git a/packages/transport/test/modules/messages/MessageContent.test.ts b/packages/transport/test/modules/messages/MessageContent.test.ts index 35fe1cc6b..4ac8219f3 100644 --- a/packages/transport/test/modules/messages/MessageContent.test.ts +++ b/packages/transport/test/modules/messages/MessageContent.test.ts @@ -40,6 +40,7 @@ describe("MessageContent", function () { expect(value).toBeInstanceOf(JSONWrapper); await TestUtil.sendMessage(sender, recipient1, value); }); + test("should correctly store the message (sender)", async function () { const messages = await sender.messages.getMessagesByAddress(recipient1.identity.address); expect(messages).toHaveLength(1); @@ -94,6 +95,7 @@ describe("MessageContent", function () { expect(message).toBeDefined(); }); + test("should correctly store the me4ssage (sender)", async function () { const messages = await sender.messages.getMessagesByAddress(recipient1.identity.address); expect(messages).toHaveLength(2); diff --git a/packages/transport/test/modules/messages/MessageController.test.ts b/packages/transport/test/modules/messages/MessageController.test.ts index ca1427a28..c0d1ad9bb 100644 --- a/packages/transport/test/modules/messages/MessageController.test.ts +++ b/packages/transport/test/modules/messages/MessageController.test.ts @@ -1,5 +1,5 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; -import { AccountController, CoreDate, CoreId, Message, Transport } from "../../../src"; +import { AccountController, CoreDate, CoreId, Message, Relationship, Transport } from "../../../src"; import { TestUtil } from "../../testHelpers/TestUtil"; describe("MessageController", function () { @@ -9,16 +9,19 @@ describe("MessageController", function () { let sender: AccountController; let recipient: AccountController; + let recipient2: AccountController; let tempId1: CoreId; let tempId2: CoreId; let tempDate: CoreDate; + let relationship: Relationship; let relationshipId: CoreId; + let relationship2: Relationship; function expectValidMessages(sentMessage: Message, receivedMessage: Message, nowMinusSeconds: CoreDate) { expect(sentMessage.id.toString()).toBe(receivedMessage.id.toString()); - const sentRelIds = sentMessage.relationshipIds.map((id) => id.toString()); - const receivedRelIds = receivedMessage.relationshipIds.map((id) => id.toString()); - expect(sentRelIds.join()).toBe(receivedRelIds.join()); + const sentRelIds = sentMessage.cache!.recipients.map((recipient) => recipient.relationshipId!.toString()); + const receivedRelIds = receivedMessage.cache!.recipients.map((recipient) => recipient.relationshipId?.toString()); + expect(sentRelIds).toContainEqual(receivedRelIds[0]); expect(sentMessage.cache).toBeDefined(); expect(sentMessage.cachedAt?.isSameOrAfter(nowMinusSeconds)).toBe(true); expect(sentMessage.cache?.createdBy.toString()).toBe(sender.identity.address.toString()); @@ -29,7 +32,7 @@ describe("MessageController", function () { expect(receivedMessage.cache?.createdBy.toString()).toBe(sender.identity.address.toString()); expect(receivedMessage.cache?.createdByDevice.toString()).toBe(sender.activeDevice.id.toString()); expect(receivedMessage.cache?.createdAt.isSameOrAfter(nowMinusSeconds)).toBe(true); - expect(sentMessage.cache!.recipients.map((r) => r.toString())).toStrictEqual(receivedMessage.cache!.recipients.map((r) => r.toString())); + expect(sentMessage.cache!.recipients.map((r) => r.toString())).toContainEqual(receivedMessage.cache!.recipients[0].toString()); expect(sentMessage.cache!.attachments.map((r) => r.toString())).toStrictEqual(receivedMessage.cache!.attachments.map((r) => r.toString())); expect(sentMessage.cache!.receivedByEveryone).toBe(receivedMessage.cache!.receivedByEveryone); expect(sentMessage.cache!.content.serialize()).toBe(receivedMessage.cache!.content.serialize()); @@ -41,11 +44,13 @@ describe("MessageController", function () { await transport.init(); - const accounts = await TestUtil.provideAccounts(transport, 2); + const accounts = await TestUtil.provideAccounts(transport, 3); sender = accounts[0]; recipient = accounts[1]; - const rels = await TestUtil.addRelationship(sender, recipient); - relationshipId = rels[0].id; + recipient2 = accounts[2]; + relationship = (await TestUtil.addRelationship(sender, recipient)).acceptedRelationshipFromSelf; + relationship2 = (await TestUtil.addRelationship(sender, recipient2)).acceptedRelationshipFromSelf; + relationshipId = relationship.id; }); afterAll(async function () { @@ -94,8 +99,8 @@ describe("MessageController", function () { const relationship = await recipient.relationships.getRelationshipToIdentity(receivedMessage.cache!.createdBy); expectValidMessages(sentMessage, receivedMessage, tempDate); - expect(receivedMessage.relationshipIds[0].toString()).toStrictEqual(relationship!.id.toString()); - expect(sentMessage.relationshipIds[0].toString()).toStrictEqual(relationship!.id.toString()); + expect(receivedMessage.cache!.recipients[0].relationshipId!.toString()).toStrictEqual(relationship!.id.toString()); + expect(sentMessage.cache!.recipients[0].relationshipId!.toString()).toStrictEqual(relationship!.id.toString()); expect(receivedMessage.cache!.recipients[0].receivedByDevice?.toString()).toBe(recipient.activeDevice.id.toString()); }); @@ -179,4 +184,46 @@ describe("MessageController", function () { const unreadMessage = await recipient.messages.markMessageAsUnread(readMessage.id); expect(unreadMessage.wasReadAt).toBeUndefined(); }); + + test("should send and receive a Message (multiple recipients)", async function () { + tempDate = CoreDate.utc().subtract(TestUtil.tempDateThreshold); + const sentMessage = await TestUtil.sendMessage(sender, [recipient, recipient2]); + + const messages = await TestUtil.syncUntilHasMessages(recipient, 1); + const receivedMessage = messages[0]; + + const messages2 = await TestUtil.syncUntilHasMessages(recipient2, 1); + const receivedMessage2 = messages2[0]; + + tempId1 = sentMessage.id; + + expectValidMessages(sentMessage, receivedMessage, tempDate); + expectValidMessages(sentMessage, receivedMessage2, tempDate); + }); + + test("should delete / pseudonymize messages", async function () { + const multipleRecipientsMessage = await TestUtil.sendMessage(sender, [recipient, recipient2]); + const message = await TestUtil.sendMessage(sender, recipient); + await sender.messages.cleanupMessagesOfDecomposedRelationship(relationship); + + expect(await sender.messages.getMessage(message.id)).toBeUndefined(); + const pseudonymizedMessage = await sender.messages.getMessage(multipleRecipientsMessage.id); + expect(pseudonymizedMessage).toBeDefined(); + expect(pseudonymizedMessage!.cache!.recipients.map((r) => [r.address, r.relationshipId])).toStrictEqual( + expect.arrayContaining([ + [await TestUtil.generateAddressPseudonym(process.env.NMSHD_TEST_BASEURL!), undefined], + [recipient2.identity.address, relationship2.id] + ]) + ); + }); + + describe("Relationship Termination", function () { + beforeAll(async function () { + await TestUtil.terminateRelationship(sender, recipient); + }); + + test("should not send a message on a terminated relationship", async function () { + await expect(TestUtil.sendMessage(sender, recipient)).rejects.toThrow("error.transport.messages.missingOrInactiveRelationship"); + }); + }); }); diff --git a/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateController.test.ts b/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateController.test.ts index 8788ffc2b..3aec40aa7 100644 --- a/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateController.test.ts +++ b/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateController.test.ts @@ -133,4 +133,18 @@ describe("RelationshipTemplateController", function () { expectValidRelationshipTemplates(sentRelationshipTemplate, receivedRelationshipTemplate, tempDate); }); + + test("should clean up relationship templates of a relationship", async function () { + const relationship = (await TestUtil.addRelationship(sender, recipient)).acceptedRelationshipFromSelf; + + const tokenReference = await TestUtil.sendRelationshipTemplateAndToken(recipient); + const templateId = (await TestUtil.fetchRelationshipTemplateFromTokenReference(sender, tokenReference)).id; + + await sender.relationshipTemplates.cleanupTemplatesOfDecomposedRelationship(relationship); + + const templateForRelationship = await sender.relationshipTemplates.getRelationshipTemplate(relationship.cache!.template.id); + const otherTemplate = await sender.relationshipTemplates.getRelationshipTemplate(templateId); + expect(templateForRelationship).toBeUndefined(); + expect(otherTemplate).toBeUndefined(); + }); }); diff --git a/packages/transport/test/modules/relationships/RelationshipsController.test.ts b/packages/transport/test/modules/relationships/RelationshipsController.test.ts index bfda40207..a309993f8 100644 --- a/packages/transport/test/modules/relationships/RelationshipsController.test.ts +++ b/packages/transport/test/modules/relationships/RelationshipsController.test.ts @@ -1,19 +1,5 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; -import { - AccountController, - CachedRelationship, - CoreDate, - CoreId, - Identity, - Relationship, - RelationshipChangeRequest, - RelationshipChangeResponse, - RelationshipChangeStatus, - RelationshipChangeType, - RelationshipStatus, - RelationshipTemplate, - Transport -} from "../../../src"; +import { AccountController, CachedRelationship, CoreDate, CoreId, Identity, Relationship, RelationshipStatus, RelationshipTemplate, Transport } from "../../../src"; import { TestUtil } from "../../testHelpers/TestUtil"; describe("RelationshipsController", function () { @@ -37,29 +23,11 @@ describe("RelationshipsController", function () { expect(relationship.peer.address).toStrictEqual(peerAccount.identity.address); expect(relationship.cache!.template).toBeInstanceOf(RelationshipTemplate); - expect(relationship.cache!.changes).toHaveLength(1); expect(relationship.cache).toBeInstanceOf(CachedRelationship); expect(relationship.cachedAt).toBeInstanceOf(CoreDate); expect(relationship.cachedAt!.isWithin(TestUtil.tempDateThreshold, TestUtil.tempDateThreshold, creationTime)).toBe(true); - - const creation = relationship.cache!.changes[0]; - expect(creation.relationshipId.toString()).toBe(relationship.id.toString()); - expect(creation.status).toStrictEqual(RelationshipChangeStatus.Accepted); - expect(creation.type).toStrictEqual(RelationshipChangeType.Creation); - - expect(creation.request).toBeInstanceOf(RelationshipChangeRequest); - expect(creation.request.createdAt.isWithin(TestUtil.tempDateThreshold, TestUtil.tempDateThreshold, creationTime)).toBe(true); - if (creation.request.createdBy.equals(ownAccount.identity.address)) { - expect(creation.request.createdBy.toString()).toBe(ownAccount.identity.address.toString()); - expect(creation.response?.createdBy.toString()).toBe(peerAccount.identity.address.toString()); - } else { - expect(creation.response?.createdBy.toString()).toBe(ownAccount.identity.address.toString()); - expect(creation.request.createdBy.toString()).toBe(peerAccount.identity.address.toString()); - } - - expect(creation.response).toBeInstanceOf(RelationshipChangeResponse); - expect(creation.response!.createdAt.isWithin(TestUtil.tempDateThreshold, TestUtil.tempDateThreshold, creationTime)).toBe(true); + expect(relationship.cache!.creationContent).toBeDefined(); expect(relationship.cache!.lastMessageReceivedAt).toBeUndefined(); expect(relationship.cache!.lastMessageSentAt).toBeUndefined(); diff --git a/packages/transport/test/modules/relationships/RelationshipsCustomContent.test.ts b/packages/transport/test/modules/relationships/RelationshipsCustomContent.test.ts index e5d90abbf..f88fc574e 100644 --- a/packages/transport/test/modules/relationships/RelationshipsCustomContent.test.ts +++ b/packages/transport/test/modules/relationships/RelationshipsCustomContent.test.ts @@ -1,6 +1,6 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { JSONWrapper, Serializable } from "@js-soft/ts-serval"; -import { AccountController, RelationshipChangeRequest, Transport } from "../../../src"; +import { AccountController, Transport } from "../../../src"; import { TestUtil } from "../../testHelpers/TestUtil"; describe("Relationships Custom Content", function () { @@ -28,23 +28,21 @@ describe("Relationships Custom Content", function () { await connection.close(); }); + test("should create a relationship with custom content", async function () { const tokenReference = await TestUtil.sendRelationshipTemplateAndToken(sender); const template = await TestUtil.fetchRelationshipTemplateFromTokenReference(recipient, tokenReference); const customContent = Serializable.fromAny({ content: "TestToken" }); const relRecipient = await TestUtil.sendRelationship(recipient, template, customContent); - const relRecipientRequest = relRecipient.cache!.creationChange.request; + const relRecipientContent = relRecipient.cache!.creationContent; const relSender = await TestUtil.syncUntilHasRelationships(sender); - const relSenderRequest = relSender[0].cache!.creationChange.request; - - expect(relRecipientRequest).toBeInstanceOf(RelationshipChangeRequest); - expect(relSenderRequest).toBeInstanceOf(RelationshipChangeRequest); + const relSenderRequest = relSender[0].cache!.creationContent; - expect(relRecipientRequest.content).toBeInstanceOf(JSONWrapper); - const recipientToken = relRecipientRequest.content as JSONWrapper; - expect(relSenderRequest.content).toBeInstanceOf(JSONWrapper); - const senderToken = relSenderRequest.content as JSONWrapper; + expect(relRecipientContent).toBeInstanceOf(JSONWrapper); + const recipientToken = relRecipientContent as JSONWrapper; + expect(relSenderRequest).toBeInstanceOf(JSONWrapper); + const senderToken = relSenderRequest as JSONWrapper; expect((recipientToken.toJSON() as any).content).toBe("TestToken"); expect((senderToken.toJSON() as any).content).toBe("TestToken"); diff --git a/packages/transport/test/modules/relationships/decomposition.test.ts b/packages/transport/test/modules/relationships/decomposition.test.ts new file mode 100644 index 000000000..985aaabd0 --- /dev/null +++ b/packages/transport/test/modules/relationships/decomposition.test.ts @@ -0,0 +1,119 @@ +import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { Serializable } from "@js-soft/ts-serval"; +import { AccountController, CoreDate, CoreId, Relationship, Transport } from "../../../src"; +import { TestUtil } from "../../testHelpers/TestUtil"; + +describe("Data cleanup after relationship decomposition", function () { + let connection: IDatabaseConnection; + + let transport: Transport; + + let sender: AccountController; + let recipient1: AccountController; + let recipient2: AccountController; + + let relationship: Relationship; + let relationship2Id: CoreId; + + let tokenId: CoreId; + let templateId: CoreId; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + const accounts = await TestUtil.provideAccounts(transport, 3); + sender = accounts[0]; + recipient1 = accounts[1]; + recipient2 = accounts[2]; + + relationship = (await TestUtil.addRelationship(sender, recipient1)).acceptedRelationshipFromSelf; + relationship2Id = (await TestUtil.addRelationship(sender, recipient2)).acceptedRelationshipFromSelf.id; + + await TestUtil.sendMessage(sender, [recipient1, recipient2]); + await TestUtil.syncUntilHasMessages(recipient1); + + // generate another template not used for a relationship + const tokenReference = await TestUtil.sendRelationshipTemplateAndToken(recipient1); + templateId = (await TestUtil.fetchRelationshipTemplateFromTokenReference(sender, tokenReference)).id; + + const expiresAt = CoreDate.utc().add({ minutes: 5 }); + const content = Serializable.fromAny({ content: "TestToken" }); + const sentToken = await recipient1.tokens.sendToken({ + content, + expiresAt, + ephemeral: false + }); + const reference = sentToken.toTokenReference().truncate(); + tokenId = (await sender.tokens.loadPeerTokenByTruncated(reference, false)).id; + + await TestUtil.terminateRelationship(sender, recipient1); + await TestUtil.decomposeRelationship(sender, recipient1); + }); + + afterAll(async function () { + await sender.close(); + await recipient1.close(); + await recipient2.close(); + await connection.close(); + }); + + test("templates should be deleted", async function () { + const templateForRelationship = await sender.relationshipTemplates.getRelationshipTemplate(relationship.cache!.template.id); + const otherTemplate = await sender.relationshipTemplates.getRelationshipTemplate(templateId); + expect(templateForRelationship).toBeUndefined(); + expect(otherTemplate).toBeUndefined(); + }); + + test("token should be deleted", async function () { + const token = await sender.tokens.getToken(tokenId); + expect(token).toBeUndefined(); + }); + + test("messages should be deleted/pseudonymized", async function () { + const messages = await sender.messages.getMessages(); + expect(messages).toHaveLength(1); + + expect(messages[0].cache!.recipients.map((r) => [r.address, r.relationshipId])).toStrictEqual( + expect.arrayContaining([ + [await TestUtil.generateAddressPseudonym(process.env.NMSHD_TEST_BASEURL!), undefined], + [recipient2.identity.address, relationship2Id] + ]) + ); + }); + + test("should sync the pseudonymization", async function () { + const { device1: sender1, device2: sender2 } = await TestUtil.createIdentityWithTwoDevices(connection, { + datawalletEnabled: true + }); + + const accounts = await TestUtil.provideAccounts(transport, 2); + const recipient1 = accounts[0]; + const recipient2 = accounts[1]; + + await TestUtil.addRelationship(sender1, recipient1); + const relationship2Id = (await TestUtil.addRelationship(sender1, recipient2)).acceptedRelationshipPeer.id; + + await sender2.syncDatawallet(); + await TestUtil.sendMessage(sender1, [recipient1, recipient2]); + + await sender1.syncDatawallet(); + await sender2.syncDatawallet(); + + await TestUtil.terminateRelationship(sender1, recipient1); + await TestUtil.decomposeRelationship(sender1, recipient1); + + await sender1.syncDatawallet(); + await sender2.syncDatawallet(); + + const sender2Messages = await sender2.messages.getMessages(); + expect(sender2Messages[0].cache?.recipients.map((r) => [r.address, r.relationshipId])).toStrictEqual( + expect.arrayContaining([ + [await TestUtil.generateAddressPseudonym(process.env.NMSHD_TEST_BASEURL!), undefined], + [recipient2.identity.address, relationship2Id] + ]) + ); + }); +}); diff --git a/packages/transport/test/modules/sync/SyncController.callback.test.ts b/packages/transport/test/modules/sync/SyncController.callback.test.ts deleted file mode 100644 index 31f9f9047..000000000 --- a/packages/transport/test/modules/sync/SyncController.callback.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; -import { sleep } from "@js-soft/ts-utils"; -import _ from "lodash"; -import { AccountController } from "../../../src"; -import { TestUtil } from "../../testHelpers/TestUtil"; - -interface CallbackObject { - percentage: number; - process: string; -} - -describe("SyncControllerCallback", function () { - let connection: IDatabaseConnection; - - let a1: AccountController; - let b1: AccountController; - let b2: AccountController; - - beforeAll(async () => { - connection = await TestUtil.createDatabaseConnection(); - a1 = await TestUtil.createIdentityWithOneDevice(connection, { datawalletEnabled: true }); - ({ device1: b1, device2: b2 } = await TestUtil.createIdentityWithTwoDevices(connection, { - datawalletEnabled: true - })); - }); - - afterAll(async function () { - await a1.close(); - await b1.close(); - await b2.close(); - - await connection.close(); - }); - - test("should execute the callback during syncEverything", async function () { - await TestUtil.addRelationship(a1, b1); - - await TestUtil.sendMessage(a1, b1); - - const events: CallbackObject[] = []; - - await sleep(1000); - - await b2.syncEverything((percentage: number, process: string) => { - events.push({ percentage, process }); - }); - - expect(events).toHaveLength(30); - - const grouped = _.groupBy(events, "process"); - for (const key in grouped) { - const percentages = grouped[key].map((e) => e.percentage); - expect(percentages).toContain(0); - expect(percentages).toContain(100); - } - }); -}); diff --git a/packages/transport/test/modules/sync/SyncController.error.test.ts b/packages/transport/test/modules/sync/SyncController.error.test.ts index 210ebdf1b..75232279a 100644 --- a/packages/transport/test/modules/sync/SyncController.error.test.ts +++ b/packages/transport/test/modules/sync/SyncController.error.test.ts @@ -35,7 +35,7 @@ describe("SyncController.error", function () { await requestorDevice.relationships.sendRelationship({ template: templateOnRequestorDevice, - content: { someMessageContent: "someMessageContent" } + creationContent: { someMessageContent: "someMessageContent" } }); const error = await TestUtil.syncUntilHasError(templatorDevice2); diff --git a/packages/transport/test/modules/sync/SyncController.relationships.test.ts b/packages/transport/test/modules/sync/SyncController.relationships.test.ts index c6f73e6a4..ba0abee2c 100644 --- a/packages/transport/test/modules/sync/SyncController.relationships.test.ts +++ b/packages/transport/test/modules/sync/SyncController.relationships.test.ts @@ -31,7 +31,7 @@ describe("RelationshipSync", function () { const createdRelationship = await requestorDevice1.relationships.sendRelationship({ template: templateOnRequestorDevice1, - content: { someMessageContent: "someMessageContent" } + creationContent: { someMessageContent: "someMessageContent" } }); await requestorDevice1.syncDatawallet(); @@ -49,10 +49,7 @@ describe("RelationshipSync", function () { await TestUtil.syncUntilHasRelationships(templatorDevice); - const relationshipOnTemplatorDevice = await templatorDevice.relationships.getRelationship(createdRelationship.id); - await templatorDevice.relationships.acceptChange(relationshipOnTemplatorDevice!.cache!.creationChange, { - someResponseContent: "someResponseContent" - }); + await templatorDevice.relationships.accept(createdRelationship.id); let relationshipOnRequestorDevice1 = (await requestorDevice2.relationships.getRelationship(createdRelationship.id))!; expect(relationshipOnRequestorDevice1.status).toStrictEqual(RelationshipStatus.Pending); @@ -89,7 +86,7 @@ describe("RelationshipSync", function () { const createdRelationship = await requestorDevice1.relationships.sendRelationship({ template: templateOnRequestorDevice1, - content: { someMessageContent: "someMessageContent" } + creationContent: { someMessageContent: "someMessageContent" } }); await requestorDevice1.syncDatawallet(); @@ -100,9 +97,7 @@ describe("RelationshipSync", function () { const relationships = await TestUtil.syncUntilHasRelationships(templatorDevice); const relationshipOnTemplatorDevice = relationships[0]; - await templatorDevice.relationships.acceptChange(relationshipOnTemplatorDevice.cache!.creationChange, { - someResponseContent: "someResponseContent" - }); + await templatorDevice.relationships.accept(relationshipOnTemplatorDevice.id); relationshipOnRequestorDevice2 = await requestorDevice2.relationships.getRelationship(createdRelationship.id); expect(relationshipOnRequestorDevice2).toBeUndefined(); @@ -140,7 +135,7 @@ describe("RelationshipSync", function () { const createdRelationship = await requestorDevice.relationships.sendRelationship({ template: templateOnRequestorDevice1, - content: { someMessageContent: "someMessageContent" } + creationContent: { someMessageContent: "someMessageContent" } }); await requestorDevice.syncDatawallet(); @@ -153,10 +148,7 @@ describe("RelationshipSync", function () { await TestUtil.syncUntilHasRelationships(templatorDevice2); - const relationshipOnTemplatorDevice = await templatorDevice2.relationships.getRelationship(createdRelationship.id); - await templatorDevice2.relationships.acceptChange(relationshipOnTemplatorDevice!.cache!.creationChange, { - someResponseContent: "someResponseContent" - }); + await templatorDevice2.relationships.accept(createdRelationship.id); await templatorDevice2.syncDatawallet(); diff --git a/packages/transport/test/modules/tokens/TokenController.test.ts b/packages/transport/test/modules/tokens/TokenController.test.ts index a81dd2f63..78ea382c1 100644 --- a/packages/transport/test/modules/tokens/TokenController.test.ts +++ b/packages/transport/test/modules/tokens/TokenController.test.ts @@ -149,4 +149,20 @@ describe("TokenController", function () { testTokens(sentTokens[0], receivedTokens[0], tempDate); testTokens(sentTokens[1], receivedTokens[1], tempDate); }); + + test("should delete a token", async function () { + const expiresAt = CoreDate.utc().add({ minutes: 5 }); + const content = Serializable.fromAny({ content: "TestToken" }); + const sentToken = await recipient.tokens.sendToken({ + content, + expiresAt, + ephemeral: false + }); + const reference = sentToken.toTokenReference().truncate(); + const tokenId = (await sender.tokens.loadPeerTokenByTruncated(reference, false)).id; + + await sender.tokens.cleanupTokensOfDecomposedRelationship(recipient.identity.address); + const token = await sender.tokens.getToken(tokenId); + expect(token).toBeUndefined(); + }); }); diff --git a/packages/transport/test/testHelpers/TestUtil.ts b/packages/transport/test/testHelpers/TestUtil.ts index e99684c5e..686591405 100644 --- a/packages/transport/test/testHelpers/TestUtil.ts +++ b/packages/transport/test/testHelpers/TestUtil.ts @@ -22,6 +22,7 @@ import { IChangedItems, IConfigOverwrite, IdentityDeletionProcess, + IdentityUtil, ISendFileParameters, Message, Relationship, @@ -318,7 +319,7 @@ export class TestUtil { await to.relationships.sendRelationship({ template: templateTo, - content: { + creationContent: { mycontent: "request" } }); @@ -329,7 +330,7 @@ export class TestUtil { const pendingRelationship = syncedRelationships[0]; expect(pendingRelationship.status).toStrictEqual(RelationshipStatus.Pending); - const rejectedRelationshipFromSelf = await from.relationships.rejectChange(pendingRelationship.cache!.creationChange, {}); + const rejectedRelationshipFromSelf = await from.relationships.reject(pendingRelationship.id); expect(rejectedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Rejected); // Get accepted relationship @@ -339,7 +340,10 @@ export class TestUtil { expect(acceptedRelationshipPeer.status).toStrictEqual(RelationshipStatus.Rejected); } - public static async addRelationship(from: AccountController, to: AccountController): Promise { + public static async addRelationship( + from: AccountController, + to: AccountController + ): Promise<{ acceptedRelationshipFromSelf: Relationship; acceptedRelationshipPeer: Relationship }> { const templateFrom = await from.relationshipTemplates.sendRelationshipTemplate({ content: { mycontent: "template" @@ -352,7 +356,7 @@ export class TestUtil { const relRequest = await to.relationships.sendRelationship({ template: templateTo, - content: { + creationContent: { mycontent: "request" } }); @@ -363,7 +367,7 @@ export class TestUtil { const pendingRelationship = syncedRelationships[0]; expect(pendingRelationship.status).toStrictEqual(RelationshipStatus.Pending); - const acceptedRelationshipFromSelf = await from.relationships.acceptChange(pendingRelationship.cache!.creationChange, {}); + const acceptedRelationshipFromSelf = await from.relationships.accept(pendingRelationship.id); expect(acceptedRelationshipFromSelf.status).toStrictEqual(RelationshipStatus.Active); // Get accepted relationship @@ -378,7 +382,34 @@ export class TestUtil { expect(relRequest.id.toString()).toBe(acceptedRelationshipFromSelf.id.toString()); expect(relRequest.id.toString()).toBe(acceptedRelationshipPeer.id.toString()); - return [acceptedRelationshipFromSelf, acceptedRelationshipPeer]; + return { acceptedRelationshipFromSelf, acceptedRelationshipPeer }; + } + + public static async terminateRelationship( + from: AccountController, + to: AccountController + ): Promise<{ terminatedRelationshipFromSelf: Relationship; terminatedRelationshipPeer: Relationship }> { + const relationshipId = (await from.relationships.getRelationshipToIdentity(to.identity.address))!.id; + const terminatedRelationshipFromSelf = await from.relationships.terminate(relationshipId); + const terminatedRelationshipPeer = (await TestUtil.syncUntil(to, (syncResult) => syncResult.relationships.length > 0)).relationships[0]; + + return { terminatedRelationshipFromSelf, terminatedRelationshipPeer }; + } + + public static async decomposeRelationship(from: AccountController, to: AccountController): Promise { + const relationship = (await from.relationships.getRelationshipToIdentity(to.identity.address))!; + await from.relationships.decompose(relationship.id); + await from.cleanupDataOfDecomposedRelationship(relationship); + const decomposedRelationshipPeer = (await TestUtil.syncUntil(to, (syncResult) => syncResult.relationships.length > 0)).relationships[0]; + + return decomposedRelationshipPeer; + } + + public static async generateAddressPseudonym(backboneBaseUrl: string): Promise { + const pseudoPublicKey = CoreBuffer.fromUtf8("deleted identity"); + const pseudonym = await IdentityUtil.createAddress({ algorithm: 1, publicKey: pseudoPublicKey }, new URL(backboneBaseUrl).hostname); + + return pseudonym; } /** @@ -412,8 +443,8 @@ export class TestUtil { return syncResult[key]; } - public static async syncUntilHasMany(accountController: AccountController, key: T, expectedNumberOfMessages = 1): Promise { - const syncResult = await TestUtil.syncUntil(accountController, (syncResult) => syncResult[key].length >= expectedNumberOfMessages); + public static async syncUntilHasMany(accountController: AccountController, key: T, expectedNumberOfItems = 1): Promise { + const syncResult = await TestUtil.syncUntil(accountController, (syncResult) => syncResult[key].length >= expectedNumberOfItems); return syncResult[key]; } @@ -426,8 +457,8 @@ export class TestUtil { return await TestUtil.syncUntilHasMany(accountController, "identityDeletionProcesses"); } - public static async syncUntilHasRelationships(accountController: AccountController): Promise { - return await TestUtil.syncUntilHasMany(accountController, "relationships"); + public static async syncUntilHasRelationships(accountController: AccountController, expectedNumberOfRelationships?: number): Promise { + return await TestUtil.syncUntilHasMany(accountController, "relationships", expectedNumberOfRelationships); } public static async syncUntilHasRelationship(accountController: AccountController, id: CoreId): Promise { @@ -499,7 +530,7 @@ export class TestUtil { } return await account.relationships.sendRelationship({ template: template, - content: body + creationContent: body }); } @@ -514,8 +545,8 @@ export class TestUtil { return template; } - public static async sendMessage(from: AccountController, to: AccountController, content?: Serializable): Promise { - return await this.sendMessagesWithFiles(from, [to], [], content); + public static async sendMessage(from: AccountController, to: AccountController | AccountController[], content?: Serializable): Promise { + return await this.sendMessagesWithFiles(from, Array.isArray(to) ? to : [to], [], content); } public static async sendMessageWithFile(from: AccountController, to: AccountController, file: File, content?: Serializable): Promise { diff --git a/packages/transport/test/utils/IdentityGenerator.test.ts b/packages/transport/test/utils/IdentityGenerator.test.ts index 6a1517019..867379145 100644 --- a/packages/transport/test/utils/IdentityGenerator.test.ts +++ b/packages/transport/test/utils/IdentityGenerator.test.ts @@ -9,10 +9,10 @@ describe("IdentityGeneratorTest", function () { }); test("should create a correct address object", async function () { - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); expect(address).toBeDefined(); expect(address.address).toBeDefined(); - expect(address.address.substr(0, 3)).toBe("id1"); + expect(address.address.startsWith("did:e:prod.enmeshed.eu:dids:")).toBe(true); }); test("should create a correct address object (test 0)", async function () { @@ -22,63 +22,63 @@ describe("IdentityGeneratorTest", function () { publicKey: buf, algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519 }); - const address = await IdentityUtil.createAddress(pk, "id1"); + const address = await IdentityUtil.createAddress(pk, "prod.enmeshed.eu"); expect(address).toBeDefined(); expect(address.address).toBeDefined(); - expect(address.address).toBe("id18uSgVGTSNqECvt1DJM3bZg6U8p6RSjott"); + expect(address.address).toBe("did:e:prod.enmeshed.eu:dids:56b3f2a0c202e27229aa87"); }); test("should create a correct address object (testcases)", async function () { const addresses = [ { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "fj0o9eOiPRswTZL6j9lE9TRvpDDnPRMF0gJeahz/W2c=", - address: "id1QF24Gk2DfqCywRS7NpeH5iu7D4xvu6qv1" + address: "did:e:prod.enmeshed.eu:dids:fef1992c5e529adc413288" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "jRxGfZtQ8a90TmKCGk+dhuX1CBjgoXuldhNPwrjpWsw=", - address: "id1HwY1TuyVBp3CmY3h18yTt1CKyu5qwB9wj" + address: "did:e:prod.enmeshed.eu:dids:b9d25bd0a2bbd3aa48437c" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "PEODpwvi7KxIVa4qeUXia9apMFvPMktdDHiDitlfbjE=", - address: "id1LMp4k1XwxZ3WFXdAn9y12tv1ofe5so4kM" + address: "did:e:prod.enmeshed.eu:dids:d459ff2144f0eac7aff554" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "mJGmNbxiVZAPToRuk9O3NvdfsWl6V+7wzIc+/57bU08=", - address: "id1McegXycvRoiJppS2LG25phn3jNveckFUL" + address: "did:e:prod.enmeshed.eu:dids:e2208784ee2769c5d9684d" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "l68K/zdNp1VLoswcHAqN6QUFwCMU6Yvzf7XiW2m1hRY=", - address: "id193k6K5cJr94WJEWYb6Kei8zp5CGPyrQLS" + address: "did:e:prod.enmeshed.eu:dids:5845cf29fbda2897892a9a" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "Gl8XTo8qFuUM+ksXixwp4g/jf3H/hU1F8ETuYaHCM5I=", - address: "id1BLrHAgDpimtLcGJGssMSm7bJHsvVe7CN" + address: "did:e:prod.enmeshed.eu:dids:01f4bab09d757578bb4994" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "rIS4kAzHXT7GgCA6Qm1ANlwM3x12QMSkeprHb6tjPyc=", - address: "id1NjGvLfWPrQ34PXWRBNiTfXv9DFiDQHExx" + address: "did:e:prod.enmeshed.eu:dids:ee5966a158f1dc4de5bd5c" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "hg/cbeBvfNrMiJ0dW1AtWC4IQwG4gkuhzG2+z6bAoRU=", - address: "id1Gda4aTXiBX9Pyc8UnmLaG44cX46umjnea" + address: "did:e:prod.enmeshed.eu:dids:ab7475ba4070f29ce286fd" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "kId+qWen/lKeTdyxcIQhkzvvvTU8wIJECfWUWbmRQRY=", - address: "id17RDEphijMPFGLbhqLWWgJfatBANMruC8f" + address: "did:e:prod.enmeshed.eu:dids:4664f42d7ca6480db07fdb" }, { - realm: "id1", + backboneHostname: "prod.enmeshed.eu", publicKey: "NcqlzTEpSlKX9gmNBv41EjPRHpaNYwt0bxqh1bgyJzA=", - address: "id19meHs4Di7JYNXoRPx9bFD6FUcpHFo3mBi" + address: "did:e:prod.enmeshed.eu:dids:60326ff5075e0d7378990c" } ]; @@ -89,37 +89,37 @@ describe("IdentityGeneratorTest", function () { publicKey: buf, algorithm: CryptoSignatureAlgorithm.ECDSA_ED25519 }); - const address = await IdentityUtil.createAddress(pk, testcase.realm); + const address = await IdentityUtil.createAddress(pk, testcase.backboneHostname); expect(address.toString()).toStrictEqual(testcase.address); } }); - test("should positively check a correct address object (without giving public key and realm)", async function () { - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); - const valid = await IdentityUtil.checkAddress(address); + test("should positively check a correct address object (without giving public key)", async function () { + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); + const valid = await IdentityUtil.checkAddress(address, "prod.enmeshed.eu"); expect(valid).toBe(true); }); test("should positively check a correct address object (giving public key)", async function () { - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); - const valid = await IdentityUtil.checkAddress(address, kp.publicKey); + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); + const valid = await IdentityUtil.checkAddress(address, "prod.enmeshed.eu", kp.publicKey); expect(valid).toBe(true); }); - test("should positively check a correct address object (giving public key and realm)", async function () { - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); - const valid = await IdentityUtil.checkAddress(address, kp.publicKey, "id1"); + test("should positively check a correct address object (giving public key and backboneHostname)", async function () { + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); + const valid = await IdentityUtil.checkAddress(address, "prod.enmeshed.eu", kp.publicKey); expect(valid).toBe(true); }); - test("should negatively check an incorrect address object (wrong realm)", async function () { - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); - const valid = await IdentityUtil.checkAddress(address, kp.publicKey, "id2"); + test("should negatively check an incorrect address object (wrong backboneHostname)", async function () { + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); + const valid = await IdentityUtil.checkAddress(address, "dev.enmeshed.eu", kp.publicKey); expect(valid).toBe(false); }); test("should negatively check an incorrect address object (wrong checksum)", async function () { - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); const index = 5; let replaceWith = "b"; const currentString = address.address.substr(index, replaceWith.length); @@ -128,14 +128,14 @@ describe("IdentityGeneratorTest", function () { } const wrongaddress = address.address.substr(0, index) + replaceWith + address.address.substr(index + replaceWith.length); address.address = wrongaddress; - const valid = await IdentityUtil.checkAddress(address, kp.publicKey, "id1"); + const valid = await IdentityUtil.checkAddress(address, "prod.enmeshed.eu", kp.publicKey); expect(valid).toBe(false); }); test("should negatively check an incorrect address object (wrong publicKey)", async function () { const kp2 = await CoreCrypto.generateSignatureKeypair(); - const address = await IdentityUtil.createAddress(kp.publicKey, "id1"); - const valid = await IdentityUtil.checkAddress(address, kp2.publicKey, "id1"); + const address = await IdentityUtil.createAddress(kp.publicKey, "prod.enmeshed.eu"); + const valid = await IdentityUtil.checkAddress(address, "prod.enmeshed.eu", kp2.publicKey); expect(valid).toBe(false); }); // eslint-disable-next-line jest/no-commented-out-tests diff --git a/packages/transport/test/utils/PasswordGenerator.test.ts b/packages/transport/test/utils/PasswordGenerator.test.ts index 72c8b3ed7..604917049 100644 --- a/packages/transport/test/utils/PasswordGenerator.test.ts +++ b/packages/transport/test/utils/PasswordGenerator.test.ts @@ -29,12 +29,14 @@ describe("PasswordGeneratorTest", function () { expect(password.length).toBeLessThanOrEqual(12); } }); + test("should return a random password with the correct given fix length", async function () { for (let i = 1; i < 20; i++) { const pass = await PasswordGenerator.createStrongPassword(50, 50); expect(pass).toHaveLength(50); } }); + test("should return a random password with the correct given length interval", async function () { for (let i = 1; i < 20; i++) { const pass = await PasswordGenerator.createStrongPassword(20, 50); @@ -43,6 +45,7 @@ describe("PasswordGeneratorTest", function () { expect(pass.length).toBeLessThanOrEqual(51); } }); + test("should throw an error if minLength is too low", async function () { for (let i = 1; i < 20; i++) { await TestUtil.expectThrowsAsync(PasswordGenerator.createStrongPassword(2, 20), "Minimum password length for a strong password should be 8 characters."); diff --git a/packages/transport/test/utils/Reflection.test.ts b/packages/transport/test/utils/Reflection.test.ts index 0c82f747f..d31234280 100644 --- a/packages/transport/test/utils/Reflection.test.ts +++ b/packages/transport/test/utils/Reflection.test.ts @@ -31,15 +31,12 @@ import { MessageEnvelopeRecipient, MessageSigned, Relationship, - RelationshipChange, - RelationshipChangeRequest, - RelationshipChangeResponse, - RelationshipCreationChangeRequestCipher, - RelationshipCreationChangeRequestContentWrapper, - RelationshipCreationChangeRequestSigned, - RelationshipCreationChangeResponseCipher, - RelationshipCreationChangeResponseContentWrapper, - RelationshipCreationChangeResponseSigned, + RelationshipCreationContentCipher, + RelationshipCreationContentSigned, + RelationshipCreationContentWrapper, + RelationshipCreationResponseContentCipher, + RelationshipCreationResponseContentSigned, + RelationshipCreationResponseContentWrapper, RelationshipTemplate, RelationshipTemplateContentWrapper, RelationshipTemplatePublicKey, @@ -83,16 +80,13 @@ const transportClassNames: string[] = [ `${Message.name}@1`, `${CachedRelationshipTemplate.name}@1`, `${Relationship.name}@1`, - `${RelationshipChange.name}@1`, - `${RelationshipChangeRequest.name}@1`, - `${RelationshipChangeResponse.name}@1`, `${RelationshipTemplate.name}@1`, - `${RelationshipCreationChangeRequestCipher.name}@1`, - `${RelationshipCreationChangeRequestContentWrapper.name}@1`, - `${RelationshipCreationChangeRequestSigned.name}@1`, - `${RelationshipCreationChangeResponseCipher.name}@1`, - `${RelationshipCreationChangeResponseContentWrapper.name}@1`, - `${RelationshipCreationChangeResponseSigned.name}@1`, + `${RelationshipCreationContentCipher.name}@1`, + `${RelationshipCreationContentWrapper.name}@1`, + `${RelationshipCreationContentSigned.name}@1`, + `${RelationshipCreationResponseContentCipher.name}@1`, + `${RelationshipCreationResponseContentWrapper.name}@1`, + `${RelationshipCreationResponseContentSigned.name}@1`, `${RelationshipTemplateContentWrapper.name}@1`, `${RelationshipTemplatePublicKey.name}@1`, `${RelationshipTemplateSigned.name}@1`,