diff --git a/src/domain/buildingBlocks/Pluto.ts b/src/domain/buildingBlocks/Pluto.ts index 950b681ff..726fe2777 100644 --- a/src/domain/buildingBlocks/Pluto.ts +++ b/src/domain/buildingBlocks/Pluto.ts @@ -33,11 +33,18 @@ export namespace Pluto { * preferred underlying storage technology, most appropriate for your use case. */ export interface Pluto { + // TODO extend Startable.Controller (changes the interface) + /** - * Pluto initialise function + * Handle startup. */ start(): Promise; + /** + * Handle teardown. + */ + stop?(): Promise; + /** * create a Backup object from the stored data */ diff --git a/src/domain/buildingBlocks/Pollux.ts b/src/domain/buildingBlocks/Pollux.ts index 6e60d4e25..60e0c79a8 100644 --- a/src/domain/buildingBlocks/Pollux.ts +++ b/src/domain/buildingBlocks/Pollux.ts @@ -17,8 +17,8 @@ export type CredentialOfferJWTBasePayload = { options: { challenge: string; domain: string; - } -} + }; +}; export type CredentialOfferPayloads = { [CredentialType.AnonCreds]: Anoncreds.CredentialOfferType; @@ -48,9 +48,8 @@ export type ProcessedCredentialOfferPayloads = { * handle Credential related tasks */ export interface Pollux { - revealCredentialFields: (credential: Credential, fields: string[], linkSecret: string) => Promise<{ - [name: string]: any + [name: string]: any; }>; isCredentialRevoked: (credential: Credential) => Promise; @@ -71,12 +70,12 @@ export interface Pollux { presentationDefinition: PresentationDefinitionRequest, credential: Credential, privateKey: PrivateKey - ): Promise> + ): Promise>; createPresentationSubmission( presentationDefinition: PresentationDefinitionRequest, credential: Credential, privateKey: LinkSecret - ): Promise> + ): Promise>; /** * Process a PresentationSubmission, resolve the issuer did and verify the credential and the holder signature @@ -88,15 +87,15 @@ export interface Pollux { verifyPresentationSubmission( presentationSubmission: PresentationSubmission, options?: Pollux.verifyPresentationSubmission.options.JWT - ): Promise + ): Promise; verifyPresentationSubmission( presentationSubmission: PresentationSubmission, options?: Pollux.verifyPresentationSubmission.options.Anoncreds - ): Promise + ): Promise; verifyPresentationSubmission( presentationSubmission: PresentationSubmission, options?: Pollux.verifyPresentationSubmission.options.JWT | Pollux.verifyPresentationSubmission.options.Anoncreds - ): Promise + ): Promise; /** * Creates a PresentationDefinitionRequest object for oob Verifications @@ -108,7 +107,7 @@ export interface Pollux { type: T, claims: PresentationClaims, options: PresentationOptions - ): Promise> + ): Promise>; diff --git a/src/edge-agent/Agent.ts b/src/edge-agent/Agent.ts index 84bafec11..4dea74c8e 100644 --- a/src/edge-agent/Agent.ts +++ b/src/edge-agent/Agent.ts @@ -7,13 +7,8 @@ import { SignWithDID } from "./didFunctions/Sign"; import { CreatePrismDID } from "./didFunctions/CreatePrismDID"; import { FetchApi } from "./helpers/FetchApi"; import { Task } from "../utils/tasks"; - -enum AgentState { - STOPPED = "stopped", - STARTING = "starting", - RUNNING = "running", - STOPPING = "stopping", -} +import { Startable } from "../utils/startable"; +import { notNil } from "../utils"; /** * Edge agent implementation @@ -22,14 +17,14 @@ enum AgentState { * @class Agent * @typedef {Agent} */ -export default class Agent { +export default class Agent implements Startable.Controller { /** * Agent state * * @public - * @type {AgentState} + * @type {Startable.State} */ - public state: AgentState = AgentState.STOPPED; + public state = Startable.State.STOPPED; public backup: AgentBackup; public readonly pollux: Pollux; @@ -86,30 +81,28 @@ export default class Agent { /** * Asyncronously start the agent * - * @async * @returns {Promise} */ - async start(): Promise { - if (this.state === AgentState.STOPPED) { - this.state = AgentState.STARTING; + start(): Promise { + return Startable.start(this, async () => { await this.pluto.start(); await this.pollux.start(); - } - - return this.state; + }); } /** * Asyncronously stop the agent and any side task that is running * - * @async - * @returns {Promise} + * @returns {Promise} */ - async stop(): Promise { - if (this.state !== AgentState.RUNNING) { - return; - } - this.state = AgentState.STOPPED; + stop(): Promise { + return Startable.stop(this, async () => { + await this.pollux.stop(); + + if (notNil(this.pluto.stop)) { + await this.pluto.stop(); + } + }); } /** diff --git a/src/edge-agent/didcomm/Agent.ts b/src/edge-agent/didcomm/Agent.ts index 717fcc39c..c2866cf21 100644 --- a/src/edge-agent/didcomm/Agent.ts +++ b/src/edge-agent/didcomm/Agent.ts @@ -37,13 +37,8 @@ import { FetchApi } from "../helpers/FetchApi"; import { ParsePrismInvitation } from "./ParsePrismInvitation"; import { ParseInvitation } from "./ParseInvitation"; import { HandleOOBInvitation } from "./HandleOOBInvitation"; - -enum AgentState { - STOPPED = "stopped", - STARTING = "starting", - RUNNING = "running", - STOPPING = "stopping", -} +import { Startable } from "../../utils/startable"; +import { notNil } from "../../utils"; /** * Edge agent implementation @@ -52,14 +47,8 @@ enum AgentState { * @class Agent * @typedef {Agent} */ -export default class DIDCommAgent { - /** - * Agent state - * - * @public - * @type {AgentState} - */ - public state: AgentState = AgentState.STOPPED; +export default class DIDCommAgent implements Startable.Controller { + public state = Startable.State.STOPPED; public backup: AgentBackup; public readonly pollux: Pollux; @@ -148,64 +137,55 @@ export default class DIDCommAgent { return agent; } + start(): Promise { + return Startable.start(this, async () => { + try { + this.state = Startable.State.STARTING; + await this.pluto.start(); + await this.pollux.start(); + await this.connectionManager.startMediator(); + } + catch (e) { + if (e instanceof Domain.AgentError.NoMediatorAvailableError) { + const hostDID = await this.createNewPeerDID([], false); + await this.connectionManager.registerMediator(hostDID); + } + else { + throw e; + } + } - /** - * Asyncronously start the agent - * - * @async - * @returns {Promise} - */ - async start(): Promise { - if (this.state !== AgentState.STOPPED) { - return this.state; - } - - try { - this.state = AgentState.STARTING; - await this.pluto.start(); - await this.pollux.start(); - await this.connectionManager.startMediator(); - } catch (e) { - if (e instanceof Domain.AgentError.NoMediatorAvailableError) { - const hostDID = await this.createNewPeerDID([], false); - - await this.connectionManager.registerMediator(hostDID); - - } else throw e; - } - - if (this.connectionManager.mediationHandler.mediator !== undefined) { - await this.connectionManager.startFetchingMessages(5); - this.state = AgentState.RUNNING; - } else { - throw new Domain.AgentError.MediationRequestFailedError("Mediation failed"); - } - - const storedLinkSecret = await this.pluto.getLinkSecret(); - if (storedLinkSecret == null) { - const secret = this.pollux.anoncreds.createLinksecret(); - const linkSecret = new Domain.LinkSecret(secret); - await this.pluto.storeLinkSecret(linkSecret); - } + if (this.connectionManager.mediationHandler.mediator !== undefined) { + await this.connectionManager.startFetchingMessages(5); + } + else { + throw new Domain.AgentError.MediationRequestFailedError("Mediation failed"); + } - return this.state; + const storedLinkSecret = await this.pluto.getLinkSecret(); + if (storedLinkSecret == null) { + const secret = this.pollux.anoncreds.createLinksecret(); + const linkSecret = new Domain.LinkSecret(secret); + await this.pluto.storeLinkSecret(linkSecret); + } + }); } /** - * Asyncronously stop the agent and any side task that is running + * Asyncronously stop the agent and dependencies * - * @async * @returns {Promise} */ - async stop(): Promise { - if (this.state !== AgentState.RUNNING) { - return; - } - this.state = AgentState.STOPPING; - await this.connectionManager.stopAllEvents(); - await this.connectionManager.stopFetchingMessages(); - // await this.agent.stop(); - this.state = AgentState.STOPPED; + stop(): Promise { + return Startable.stop(this, async () => { + await this.connectionManager.stopAllEvents(); + await this.connectionManager.stopFetchingMessages(); + await this.pollux.stop(); + + if (notNil(this.pluto.stop)) { + await this.pluto.stop(); + } + }); } /** diff --git a/src/edge-agent/oidc/Agent.ts b/src/edge-agent/oidc/Agent.ts index 8da6d830b..92e3b8119 100644 --- a/src/edge-agent/oidc/Agent.ts +++ b/src/edge-agent/oidc/Agent.ts @@ -12,7 +12,8 @@ import { Task } from "../../utils/tasks"; import * as DIDfns from "../didFunctions"; import * as Tasks from "./tasks"; import * as Errors from "./errors"; -import { JsonObj, expect } from "../../utils"; +import { JsonObj, expect, notNil } from "../../utils"; +import { Startable } from "../../utils/startable"; /** * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html @@ -24,20 +25,12 @@ class Connection { public readonly issuerMeta: OIDC.IssuerMetadata, public readonly scopes: string[], public readonly tokenResponse: TokenResponse, - ) { } + ) {} } -// TODO make startable interface -enum AgentState { - STOPPED = "stopped", - STARTING = "starting", - RUNNING = "running", - STOPPING = "stopping", -} - -export class OIDCAgent { +export class OIDCAgent implements Startable.Controller { private connections: Connection[] = []; - public state = AgentState.STOPPED; + public state = Startable.State.STOPPED; public readonly pollux: Pollux; constructor( @@ -81,18 +74,21 @@ export class OIDCAgent { return agent; } - async start() { - if (this.state === AgentState.STOPPED) { - this.state = AgentState.STARTING; + start(): Promise { + return Startable.start(this, async () => { await this.pluto.start(); await this.pollux.start(); - this.state = AgentState.RUNNING; - } - return this.state; + }); } - async stop(): Promise { - this.state = AgentState.STOPPED + async stop(): Promise { + return Startable.stop(this, async () => { + await this.pollux.stop(); + + if (notNil(this.pluto.stop)) { + await this.pluto.stop(); + } + }); } private runTask(task: Task): Promise { diff --git a/src/pluto/Pluto.ts b/src/pluto/Pluto.ts index e6fa65597..55f04b212 100644 --- a/src/pluto/Pluto.ts +++ b/src/pluto/Pluto.ts @@ -4,7 +4,8 @@ import * as Models from "./models"; import { PeerDID } from "../peer-did/PeerDID"; import { BackupManager } from "./backup/BackupManager"; import { PlutoRepositories, repositoryFactory } from "./repositories"; -import { Arrayable, asArray } from "../utils"; +import { Arrayable, asArray, notNil } from "../utils"; +import { Startable } from "../utils/startable"; /** @@ -49,6 +50,11 @@ export namespace Pluto { */ start?(): Promise; + /** + * Handle any necessary teardown. + */ + stop?(): Promise; + /** * Run a query to fetch data from the Store * @@ -103,6 +109,7 @@ export namespace Pluto { } export class Pluto implements Domain.Pluto { + public state = Startable.State.STOPPED; public BackupMgr: BackupManager; private Repositories: PlutoRepositories; @@ -114,10 +121,20 @@ export class Pluto implements Domain.Pluto { this.BackupMgr = new BackupManager(this, this.Repositories); } - async start(): Promise { - if (this.store.start !== undefined) { - await this.store.start(); - } + async start() { + await Startable.start(this, async () => { + if (notNil(this.store.start)) { + await this.store.start(); + } + }); + } + + async stop() { + await Startable.stop(this, async () => { + if (notNil(this.store.stop)) { + await this.store.stop(); + } + }); } /** Backups **/ @@ -227,7 +244,7 @@ export class Pluto implements Domain.Pluto { for (const did of dids) { const dbDids = await this.getPrismDIDS(did.uuid); for (const prismDID of dbDids) { - prismDIDS.push(prismDID) + prismDIDS.push(prismDID); } } return prismDIDS; @@ -245,7 +262,7 @@ export class Pluto implements Domain.Pluto { const prismDID = new Domain.PrismDID(did, key, link.alias); return prismDID; }) - ) + ); } diff --git a/src/pluto/rxdb/Store.ts b/src/pluto/rxdb/Store.ts index 696c63bc5..33a835c34 100644 --- a/src/pluto/rxdb/Store.ts +++ b/src/pluto/rxdb/Store.ts @@ -44,6 +44,11 @@ export class RxdbStore implements Pluto.Store { } } + async stop(): Promise { + await this.db.destroy(); + delete this._db; + } + async update(name: string, model: T): Promise { const table = this.getCollection(name); const row = await table.findOne({ diff --git a/src/pollux/Pollux.ts b/src/pollux/Pollux.ts index ef1d02de4..606bdd598 100644 --- a/src/pollux/Pollux.ts +++ b/src/pollux/Pollux.ts @@ -61,6 +61,7 @@ import { JsonLd, RemoteDocument } from "jsonld/jsonld-spec"; import { VerificationKeyType } from "../castor/types"; import { revocationJsonldDocuments } from "../domain/models/revocation"; import { Bitstring } from "./utils/Bitstring"; +import { Startable } from "../utils/startable"; /** * Implementation of Pollux @@ -69,10 +70,11 @@ import { Bitstring } from "./utils/Bitstring"; * @class Pollux * @typedef {Pollux} */ -export default class Pollux implements IPollux { +export default class Pollux implements IPollux, Startable.Controller { private _anoncreds: AnoncredsLoader | undefined; private _jwe: typeof import("jwe-wasm") | undefined; private _pako = pako; + public state = Startable.State.STOPPED; constructor( private apollo: Apollo, @@ -97,6 +99,21 @@ export default class Pollux implements IPollux { return this._jwe; } + start(): Promise { + return Startable.start(this, async () => { + this._anoncreds = await AnoncredsLoader.getInstance(); + this._jwe ??= await import("jwe-wasm").then(async module => { + const wasmInstance = module.initSync({ module: wasmBuffer }); + await module.default(wasmInstance); + return module; + }); + }); + } + + async stop(): Promise { + return Startable.stop(this, async () => {}); + } + async revealCredentialFields (credential: Credential, fields: string[], linkSecret?: string) { @@ -835,15 +852,6 @@ export default class Pollux implements IPollux { ); } - async start() { - this._anoncreds = await AnoncredsLoader.getInstance(); - this._jwe ??= await import("jwe-wasm").then(async module => { - const wasmInstance = module.initSync({ module: wasmBuffer }); - await module.default(wasmInstance); - return module; - }); - } - private isOfferPayload(offer: any, type: Type): offer is CredentialOfferPayloads[CredentialType.JWT] { if (type === CredentialType.JWT) { if (!offer || !offer.options) { diff --git a/src/utils/startable.ts b/src/utils/startable.ts new file mode 100644 index 000000000..13d6000c7 --- /dev/null +++ b/src/utils/startable.ts @@ -0,0 +1,76 @@ +/** + * Define controls for managing entity lifecycle. + */ +export namespace Startable { + /** + * states for a Startable entity + */ + export enum State { + STOPPED = "stopped", + STARTING = "starting", + RUNNING = "running", + STOPPING = "stopping", + } + + /** + * + */ + export interface Controller { + /** + * current status of the entity + */ + state: State; + + /** + * handle startup if entity is in STOPPED state + * + * + * @returns {Promise} + */ + start(): Promise; + + /** + * handle teardown if entity is in RUNNING state + * + * @returns {Promise} + */ + stop(): Promise; + } + + /** + * handle the startup of a Controller + * updating state lifecycle around running the delegate + * + * @param obj + * @param delegate + * @returns + */ + export const start = async (obj: Pick, delegate: () => Promise): Promise => { + if (obj.state === Startable.State.STOPPED) { + obj.state = Startable.State.STARTING; + await delegate(); + obj.state = Startable.State.RUNNING; + } + + return obj.state; + }; + + /** + * handle the teardown of a Controller + * updating state lifecycle around running the delegate + * + * @param obj + * @param delegate + * @returns + */ + export const stop = async (obj: Pick, delegate: () => Promise): Promise => { + if (obj.state === Startable.State.RUNNING) { + obj.state = Startable.State.STOPPING; + await delegate(); + obj.state = Startable.State.STOPPED; + } + + return obj.state; + }; + +} diff --git a/tests/agent/Agent.functional.test.ts b/tests/agent/Agent.functional.test.ts index 3836b28af..aff8a9196 100644 --- a/tests/agent/Agent.functional.test.ts +++ b/tests/agent/Agent.functional.test.ts @@ -1,43 +1,69 @@ -import { vi, describe, it, expect, test, beforeEach, afterEach } from 'vitest'; -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import * as sinon from "sinon"; -import SinonChai from "sinon-chai"; +import { vi, describe, it, expect, test, beforeEach } from 'vitest'; import Agent from "../../src/edge-agent/Agent"; import { Pluto } from "../../src"; import { mockPluto } from "../fixtures/inmemory/factory"; import * as Fixtures from "../fixtures"; -chai.use(SinonChai); -chai.use(chaiAsPromised); describe("Agent", () => { let agent: Agent; let pluto: Pluto; - let sandbox: sinon.SinonSandbox; - describe("Functional Tests", () => { - - afterEach(async () => { - vi.useRealTimers(); - sandbox.restore(); - }); - beforeEach(async () => { - vi.useFakeTimers(); - sandbox = sinon.createSandbox(); pluto = mockPluto(); agent = Agent.initialize({ pluto }); - await pluto.start(); }); describe("createPrismDID", () => { it("default parameters - should return unique DIDs", async () => { + await agent.start(); const first = await agent.createNewPrismDID("a"); const second = await agent.createNewPrismDID("a"); expect(first).to.not.deep.eq(second); }); }); + + describe("Persistence", () => { + test("start() called for Startable dependencies", async () => { + const spyPluto = vi.spyOn(agent.pluto, "start"); + const spyPollux = vi.spyOn(agent.pollux, "start"); + + await agent.start(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + }); + + test("stop() called for Startable dependencies", async () => { + const spyPluto = vi.spyOn(agent.pluto, "stop"); + const spyPollux = vi.spyOn(agent.pollux, "stop"); + + await agent.start(); + await agent.stop(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + }); + + + test("Start > Stop > Start - should run without errors", async () => { + await agent.start(); + await agent.stop(); + await agent.start(); + + expect(agent.state).toBe("running"); + }); + + test("db persists after restart", async () => { + await agent.start(); + await agent.pluto.storeMessage(Fixtures.Messages.ConnectionResponse); + await agent.stop(); + await agent.start(); + const result = await agent.pluto.getAllMessages(); + + expect(result).toHaveLength(1); + }); + }); }); }); diff --git a/tests/agent/didcomm/Agent.functional.test.ts b/tests/agent/didcomm/Agent.functional.test.ts new file mode 100644 index 000000000..baac20d86 --- /dev/null +++ b/tests/agent/didcomm/Agent.functional.test.ts @@ -0,0 +1,92 @@ +import { vi, describe, expect, test, beforeEach } from 'vitest'; +import Agent from "../../../src/edge-agent/didcomm/Agent"; +import { DID } from '../../../src/domain'; +import { Pluto } from "../../../src"; +import { mockPluto } from "../../fixtures/inmemory/factory"; +import * as Fixtures from "../../fixtures"; + +describe("Agent", () => { + let agent: Agent; + let pluto: Pluto; + + describe("Functional Tests", () => { + beforeEach(async () => { + pluto = mockPluto(); + const mediatorDID = DID.from("did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuNDQ6ODA4MCIsImEiOlsiZGlkY29tbS92MiJdfX0.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzOi8vMTkyLjE2OC4xLjQ0OjgwODAvd3MiLCJhIjpbImRpZGNvbW0vdjIiXX19"); + agent = Agent.initialize({ pluto, mediatorDID }); + + vi.spyOn(agent.connectionManager, "sendMessage").mockResolvedValue(undefined); + vi.spyOn(agent.mediationHandler, "pickupUnreadMessages").mockResolvedValue([]); + + vi.spyOn(agent.pluto, "getAllMediators").mockResolvedValue([{ + hostDID: DID.from("did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuNDQ6ODA4MCIsImEiOlsiZGlkY29tbS92MiJdfX0.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzOi8vMTkyLjE2OC4xLjQ0OjgwODAvd3MiLCJhIjpbImRpZGNvbW0vdjIiXX19"), + mediatorDID: DID.from("did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuNDQ6ODA4MCIsImEiOlsiZGlkY29tbS92MiJdfX0.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzOi8vMTkyLjE2OC4xLjQ0OjgwODAvd3MiLCJhIjpbImRpZGNvbW0vdjIiXX19"), + routingDID: DID.from("did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuNDQ6ODA4MCIsImEiOlsiZGlkY29tbS92MiJdfX0.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzOi8vMTkyLjE2OC4xLjQ0OjgwODAvd3MiLCJhIjpbImRpZGNvbW0vdjIiXX19"), + }]); + }); + + describe("Persistence", () => { + test("start() called for Startable dependencies", async () => { + const spyPluto = vi.spyOn(agent.pluto, "start"); + const spyPollux = vi.spyOn(agent.pollux, "start"); + const spyMediator = vi.spyOn(agent.connectionManager, "startMediator"); + const spyMessages = vi.spyOn(agent.connectionManager, "startFetchingMessages"); + + await agent.start(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + expect(spyMediator).toHaveBeenCalledOnce(); + expect(spyMessages).toHaveBeenCalledOnce(); + }); + + test("calling start() twice should not throw", async () => { + const spyPluto = vi.spyOn(agent.pluto, "start"); + const spyPollux = vi.spyOn(agent.pollux, "start"); + const spyMediator = vi.spyOn(agent.connectionManager, "startMediator"); + const spyMessages = vi.spyOn(agent.connectionManager, "startFetchingMessages"); + + await agent.start(); + await agent.start(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + expect(spyMediator).toHaveBeenCalledOnce(); + expect(spyMessages).toHaveBeenCalledOnce(); + }); + + test("stop() called for Startable dependencies", async () => { + const spyPluto = vi.spyOn(agent.pluto, "stop"); + const spyPollux = vi.spyOn(agent.pollux, "stop"); + const spyEvents = vi.spyOn(agent.connectionManager, "stopAllEvents"); + const spyMessages = vi.spyOn(agent.connectionManager, "stopFetchingMessages"); + + await agent.start(); + await agent.stop(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + expect(spyEvents).toHaveBeenCalledOnce(); + expect(spyMessages).toHaveBeenCalledOnce(); + }); + + test("Start > Stop > Start - should run without errors", async () => { + await agent.start(); + await agent.stop(); + await agent.start(); + + expect(agent.state).toBe("running"); + }); + + test("db persists after restart", async () => { + await agent.start(); + await agent.pluto.storeMessage(Fixtures.Messages.ConnectionResponse); + await agent.stop(); + await agent.start(); + const result = await agent.pluto.getAllMessages(); + + expect(result).toHaveLength(1); + }); + }); + }); +}); diff --git a/tests/agent/oidc/Agent.functional.test.ts b/tests/agent/oidc/Agent.functional.test.ts new file mode 100644 index 000000000..eb5ca2c1f --- /dev/null +++ b/tests/agent/oidc/Agent.functional.test.ts @@ -0,0 +1,70 @@ +import { vi, describe, expect, test, beforeEach } from 'vitest'; +import { OIDCAgent as Agent } from "../../../src/edge-agent/oidc/Agent"; +import { DID } from '../../../src/domain'; +import { Pluto } from "../../../src"; +import { mockPluto } from "../../fixtures/inmemory/factory"; +import * as Fixtures from "../../fixtures"; + +describe("Agent", () => { + let agent: Agent; + let pluto: Pluto; + + describe("Functional Tests", () => { + beforeEach(async () => { + pluto = mockPluto(); + agent = Agent.initialize({ pluto }); + }); + + describe("Persistence", () => { + test("start() called for Startable dependencies", async () => { + const spyPluto = vi.spyOn(agent.pluto, "start"); + const spyPollux = vi.spyOn(agent.pollux, "start"); + + await agent.start(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + }); + + test("calling start() twice should not throw", async () => { + const spyPluto = vi.spyOn(agent.pluto, "start"); + const spyPollux = vi.spyOn(agent.pollux, "start"); + + await agent.start(); + await agent.start(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + }); + + test("stop() called for Startable dependencies", async () => { + const spyPluto = vi.spyOn(agent.pluto, "stop"); + const spyPollux = vi.spyOn(agent.pollux, "stop"); + + await agent.start(); + await agent.stop(); + + expect(spyPluto).toHaveBeenCalledOnce(); + expect(spyPollux).toHaveBeenCalledOnce(); + }); + + test("Start > Stop > Start - should run without errors", async () => { + await agent.start(); + await agent.stop(); + await agent.start(); + + expect(agent.state).toBe("running"); + }); + + test("db persists after restart", async () => { + await agent.start(); + await agent.pluto.storeMessage(Fixtures.Messages.ConnectionResponse); + await agent.stop(); + await agent.start(); + const result = await agent.pluto.getAllMessages(); + + expect(result).toHaveLength(1); + }); + }); + }); +}); diff --git a/tests/agent/oidc/Agent.test.ts b/tests/agent/oidc/Agent.test.ts index cff528f9a..a15bebf8b 100644 --- a/tests/agent/oidc/Agent.test.ts +++ b/tests/agent/oidc/Agent.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, it, expect, test, beforeEach, afterEach, MockInstance } from 'vitest'; +import { vi, describe, expect, test, beforeEach, afterEach, MockInstance } from 'vitest'; import { Apollo, OIDCAgent, Pluto } from "../../../src"; import { mockPluto } from "../../fixtures/inmemory/factory"; import { ExpectError } from "../../../src/domain/models/errors/Common"; diff --git a/tests/fixtures/index.ts b/tests/fixtures/index.ts index 3ea4f2784..8e1f663f6 100644 --- a/tests/fixtures/index.ts +++ b/tests/fixtures/index.ts @@ -3,5 +3,6 @@ export * as Credentials from "./credentials"; export * as DIDs from "./dids"; export * as Keys from "./keys"; +export * as Messages from "./messages"; export * as OIDC from "./oidc"; export * as PresentationRequests from "./presentationRequests";