Files
obsidian-livesync/src/common/replicatorProviders.unit.spec.ts
T

249 lines
12 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
import {
CAPABILITY_SUPPORT_KINDS,
NO_INTERACTION,
REPLICATION_COMPLETED,
REPLICATION_PROGRESS_PRESENTATIONS,
REMOTE_RESOURCE_KINDS,
USER_INITIATED_REPLICATION_AUTHORITY,
} from "@vrtmrz/livesync-commonlib/replication";
const constructorMocks = vi.hoisted(() => ({
couchDB: vi.fn(),
couchDBOneShot: vi.fn(async (..._args: unknown[]) => REPLICATION_COMPLETED),
objectStorage: vi.fn(),
objectStorageOneShot: vi.fn(async (..._args: unknown[]) => REPLICATION_COMPLETED),
}));
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
LiveSyncCouchDBReplicator: class {
constructor(host: unknown) {
constructorMocks.couchDB(host);
}
openOneShotReplicationWithOutcome(...args: unknown[]) {
return constructorMocks.couchDBOneShot(...args);
}
},
}));
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator", () => ({
LiveSyncJournalReplicator: class {
constructor(host: unknown) {
constructorMocks.objectStorage(host);
}
openOneShotReplicationWithOutcome(...args: unknown[]) {
return constructorMocks.objectStorageOneShot(...args);
}
},
}));
import { createCentralReplicatorProviderDefinitions } from "./replicatorProviders";
describe("central Replicator provider definitions", () => {
it("keeps the retained remote-resource catalogue bounded", () => {
expect
.soft(Object.values(REMOTE_RESOURCE_KINDS).sort())
.toEqual(["connection", "preferred-tweak", "security-seed", "synchronisation-information"].sort());
});
it("composes CouchDB and Object Storage policies outside LiveSyncBaseCore", async () => {
const host = {} as Parameters<typeof createCentralReplicatorProviderDefinitions>[0];
const definitions = createCentralReplicatorProviderDefinitions(host);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const objectStorage = definitions.get(REMOTE_MINIO)!;
expect([...definitions.keys()]).toEqual([REMOTE_COUCHDB, REMOTE_MINIO]);
expect("sameKindReconciliation" in couchDB).toBe(false);
expect("sameKindReconciliation" in objectStorage).toBe(false);
expect(
couchDB.isConfigured(
Object.assign(createNewVaultSettings(), {
remoteType: REMOTE_COUCHDB,
couchDB_URI: "https://couch.example.test",
couchDB_DBNAME: "vault",
})
)
).toBe(true);
expect(
objectStorage.isConfigured(
Object.assign(createNewVaultSettings(), {
remoteType: REMOTE_MINIO,
endpoint: "https://objects.example.test",
bucket: "vault",
})
)
).toBe(true);
await couchDB.create(createNewVaultSettings());
await objectStorage.create(createNewVaultSettings());
expect(constructorMocks.couchDB).toHaveBeenCalledWith(host);
expect(constructorMocks.objectStorage).toHaveBeenCalledWith(host);
});
it("rejects incomplete and wrong-kind settings before construction", () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const objectStorage = definitions.get(REMOTE_MINIO)!;
expect(couchDB.isConfigured(Object.assign(createNewVaultSettings(), { remoteType: REMOTE_MINIO }))).toBe(false);
expect(
objectStorage.isConfigured(Object.assign(createNewVaultSettings(), { remoteType: REMOTE_COUCHDB }))
).toBe(false);
});
it("declares the retained owned resources and cohesive optional administration", () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchResources = definitions.get(REMOTE_COUCHDB)?.remoteResources;
const objectResources = definitions.get(REMOTE_MINIO)?.remoteResources;
const couchAdministration = definitions.get(REMOTE_COUCHDB)?.centralRemoteAdministration;
const objectAdministration = definitions.get(REMOTE_MINIO)?.centralRemoteAdministration;
expect(Object.keys(couchResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
expect(Object.keys(objectResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
expect(couchResources?.[REMOTE_RESOURCE_KINDS.CONNECTION].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(couchResources?.[REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(couchResources?.[REMOTE_RESOURCE_KINDS.SECURITY_SEED].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(couchResources?.[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION].kind).toBe(
CAPABILITY_SUPPORT_KINDS.SUPPORTED
);
expect(objectResources?.[REMOTE_RESOURCE_KINDS.SECURITY_SEED].kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(objectResources?.[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION].kind).toBe(
CAPABILITY_SUPPORT_KINDS.NOT_APPLICABLE
);
expect(couchAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(objectAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect("activeRemoteReads" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
expect("fullTransfers" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
});
it("dispatches central finite work through provider-local attempt results", async () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const objectStorage = definitions.get(REMOTE_MINIO)!;
const setting = createNewVaultSettings();
const couchInstance = await couchDB.create(setting);
const objectInstance = await objectStorage.create(setting);
if (!couchInstance || !objectInstance) throw new Error("Provider construction failed");
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("CouchDB OneShot is unavailable");
}
if (objectStorage.unattendedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("Object Storage OneShot is unavailable");
}
await expect(
couchDB.userInitiatedOneShot.run(couchInstance, setting, {
trigger: "manual",
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
})
).resolves.toBe(REPLICATION_COMPLETED);
await expect(
objectStorage.unattendedOneShot.run(objectInstance, setting, {
trigger: "resume",
interaction: NO_INTERACTION,
})
).resolves.toBe(REPLICATION_COMPLETED);
expect(constructorMocks.couchDBOneShot).toHaveBeenCalledWith(setting, true);
expect(constructorMocks.objectStorageOneShot).toHaveBeenCalledWith(setting, false);
});
it.each([
["CouchDB", REMOTE_COUCHDB],
["Object Storage", REMOTE_MINIO],
] as const)("maps %s progress presentation independently of recovery authority", async (label, remoteType) => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const definition = definitions.get(remoteType)!;
if (definition.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error(`${label} OneShot is unavailable`);
}
const setting = Object.assign(createNewVaultSettings(), { remoteType });
const openOneShotReplicationWithOutcome = vi.fn(async () => REPLICATION_COMPLETED);
const instance = {
initializeDatabaseForReplication: vi.fn(async () => true),
openReplication: vi.fn(async () => true),
terminateSync: vi.fn(),
closeReplication: vi.fn(),
openOneShotReplicationWithOutcome,
};
await definition.userInitiatedOneShot.run(instance, setting, {
trigger: "manual",
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
});
await definition.userInitiatedOneShot.run(instance, setting, {
trigger: "manual",
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
});
expect(openOneShotReplicationWithOutcome).toHaveBeenNthCalledWith(1, setting, false);
expect(openOneShotReplicationWithOutcome).toHaveBeenNthCalledWith(2, setting, true);
});
it("dispatches central finite work through the declared operation rather than constructor identity", async () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const objectStorage = definitions.get(REMOTE_MINIO)!;
const setting = createNewVaultSettings();
const createStructuralOneShotReplicator = () => ({
initializeDatabaseForReplication: vi.fn(async () => true),
openReplication: vi.fn(async () => true),
terminateSync: vi.fn(),
closeReplication: vi.fn(),
openOneShotReplicationWithOutcome: vi.fn(async () => REPLICATION_COMPLETED),
});
const couchInstance = createStructuralOneShotReplicator();
const objectStorageInstance = createStructuralOneShotReplicator();
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("CouchDB OneShot is unavailable");
}
if (objectStorage.unattendedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("Object Storage OneShot is unavailable");
}
const couchOutcome = await couchDB.userInitiatedOneShot.run(couchInstance, setting, {
trigger: "manual",
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
});
const objectStorageOutcome = await objectStorage.unattendedOneShot.run(objectStorageInstance, setting, {
trigger: "resume",
interaction: NO_INTERACTION,
});
expect.soft(couchOutcome).toBe(REPLICATION_COMPLETED);
expect.soft(objectStorageOutcome).toBe(REPLICATION_COMPLETED);
expect(couchInstance.openOneShotReplicationWithOutcome).toHaveBeenCalledWith(setting, true);
expect(objectStorageInstance.openOneShotReplicationWithOutcome).toHaveBeenCalledWith(setting, false);
});
it("rejects a one-shot adapter whose Replicator does not declare the required operation", async () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const setting = createNewVaultSettings();
const incompleteInstance = {
initializeDatabaseForReplication: vi.fn(async () => true),
openReplication: vi.fn(async () => true),
terminateSync: vi.fn(),
closeReplication: vi.fn(),
};
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("CouchDB OneShot is unavailable");
}
const outcome = await couchDB.userInitiatedOneShot.run(incompleteInstance, setting, {
trigger: "manual",
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
});
expect(outcome.status).toBe("failed");
expect(incompleteInstance.openReplication).not.toHaveBeenCalled();
});
});