Initial commit
This commit is contained in:
39
tests/contract/sync-engine.test.ts
Normal file
39
tests/contract/sync-engine.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
computeBinaryHash,
|
||||
decryptBytes,
|
||||
encryptBytes,
|
||||
generateVaultKey,
|
||||
mergeTextRevisions
|
||||
} from "../../packages/sync-engine/src";
|
||||
|
||||
test("encryptBytes round-trips binary payloads", async () => {
|
||||
const vaultKey = await generateVaultKey("test-key");
|
||||
const original = new Uint8Array([1, 2, 3, 4, 5]).buffer;
|
||||
|
||||
const encrypted = await encryptBytes(original, vaultKey);
|
||||
const decrypted = await decryptBytes(encrypted, vaultKey);
|
||||
|
||||
assert.deepEqual([...new Uint8Array(decrypted)], [1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test("computeBinaryHash is stable for identical binary payloads", async () => {
|
||||
const first = await computeBinaryHash(new Uint8Array([9, 8, 7, 6]).buffer);
|
||||
const second = await computeBinaryHash(new Uint8Array([9, 8, 7, 6]).buffer);
|
||||
|
||||
assert.equal(first, second);
|
||||
});
|
||||
|
||||
test("mergeTextRevisions reports conflicts when both sides diverge", () => {
|
||||
const result = mergeTextRevisions({
|
||||
base: "alpha",
|
||||
local: "alpha local",
|
||||
remote: "alpha remote"
|
||||
});
|
||||
|
||||
assert.equal(result.status, "conflict");
|
||||
assert.match(result.content, /<<<<<<< LOCAL/);
|
||||
assert.match(result.content, />>>>>>> REMOTE/);
|
||||
});
|
||||
174
tests/e2e/multi-device-store.test.ts
Normal file
174
tests/e2e/multi-device-store.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { createFileSyncStore } from "../../apps/sync-server/src/store/fileSyncStore";
|
||||
|
||||
test("file-backed store survives restart and serves another device", () => {
|
||||
const tempDirectory = mkdtempSync(path.join(tmpdir(), "obsidian-sync-"));
|
||||
const storePath = path.join(tempDirectory, "sync-state.json");
|
||||
|
||||
try {
|
||||
const serverA = createFileSyncStore(storePath);
|
||||
const deviceA = serverA.registerDevice({
|
||||
vaultId: "vault-1",
|
||||
deviceName: "Desktop"
|
||||
});
|
||||
const deviceB = serverA.registerDevice({
|
||||
vaultId: "vault-1",
|
||||
deviceName: "Phone"
|
||||
});
|
||||
|
||||
const pushResponse = serverA.push({
|
||||
vaultId: "vault-1",
|
||||
deviceId: deviceA.deviceId,
|
||||
knownServerRevision: 0,
|
||||
files: [
|
||||
{
|
||||
manifest: {
|
||||
path: "notes/hello.md",
|
||||
kind: "text",
|
||||
contentHash: "hash-1",
|
||||
revisionId: "rev-1",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
sizeBytes: 12,
|
||||
deviceId: deviceA.deviceId
|
||||
},
|
||||
envelope: {
|
||||
algorithm: "AES-GCM-256",
|
||||
keyId: "key-1",
|
||||
iv: "iv-1",
|
||||
ciphertext: "cipher-1"
|
||||
}
|
||||
}
|
||||
],
|
||||
tombstones: []
|
||||
});
|
||||
|
||||
assert.deepEqual(pushResponse.acceptedFilePaths, ["notes/hello.md"]);
|
||||
|
||||
const serverB = createFileSyncStore(storePath);
|
||||
assert.equal(serverB.authenticate("vault-1", deviceB.deviceId, deviceB.token), true);
|
||||
|
||||
const pullResponse = serverB.pull("vault-1", 0);
|
||||
assert.equal(pullResponse.changes.length, 1);
|
||||
assert.equal(pullResponse.changes[0].file?.manifest.path, "notes/hello.md");
|
||||
assert.equal(pullResponse.serverRevision, 1);
|
||||
assert.equal(pullResponse.hasMore, false);
|
||||
assert.equal(pullResponse.nextSinceServerRevision, 1);
|
||||
} finally {
|
||||
rmSync(tempDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("file-backed store paginates pull results", () => {
|
||||
const tempDirectory = mkdtempSync(path.join(tmpdir(), "obsidian-sync-page-"));
|
||||
const storePath = path.join(tempDirectory, "sync-state.json");
|
||||
|
||||
try {
|
||||
const store = createFileSyncStore(storePath);
|
||||
const device = store.registerDevice({
|
||||
vaultId: "vault-2",
|
||||
deviceName: "Desktop"
|
||||
});
|
||||
|
||||
store.push({
|
||||
vaultId: "vault-2",
|
||||
deviceId: device.deviceId,
|
||||
knownServerRevision: 0,
|
||||
files: [
|
||||
{
|
||||
manifest: {
|
||||
path: "a.md",
|
||||
kind: "text",
|
||||
contentHash: "hash-a",
|
||||
revisionId: "rev-a",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
sizeBytes: 1,
|
||||
deviceId: device.deviceId
|
||||
},
|
||||
envelope: {
|
||||
algorithm: "AES-GCM-256",
|
||||
keyId: "key-a",
|
||||
iv: "iv-a",
|
||||
ciphertext: "cipher-a"
|
||||
}
|
||||
}
|
||||
],
|
||||
tombstones: []
|
||||
});
|
||||
|
||||
store.push({
|
||||
vaultId: "vault-2",
|
||||
deviceId: device.deviceId,
|
||||
knownServerRevision: 1,
|
||||
files: [
|
||||
{
|
||||
manifest: {
|
||||
path: "b.md",
|
||||
kind: "text",
|
||||
contentHash: "hash-b",
|
||||
revisionId: "rev-b",
|
||||
updatedAt: "2026-04-08T00:01:00.000Z",
|
||||
sizeBytes: 1,
|
||||
deviceId: device.deviceId
|
||||
},
|
||||
envelope: {
|
||||
algorithm: "AES-GCM-256",
|
||||
keyId: "key-a",
|
||||
iv: "iv-b",
|
||||
ciphertext: "cipher-b"
|
||||
}
|
||||
}
|
||||
],
|
||||
tombstones: []
|
||||
});
|
||||
|
||||
const firstPage = store.pull("vault-2", 0, 1);
|
||||
assert.equal(firstPage.changes.length, 1);
|
||||
assert.equal(firstPage.hasMore, true);
|
||||
assert.equal(firstPage.nextSinceServerRevision, 1);
|
||||
|
||||
const secondPage = store.pull("vault-2", firstPage.nextSinceServerRevision, 1);
|
||||
assert.equal(secondPage.changes.length, 1);
|
||||
assert.equal(secondPage.hasMore, false);
|
||||
assert.equal(secondPage.nextSinceServerRevision, 2);
|
||||
assert.equal(secondPage.changes[0].file?.manifest.path, "b.md");
|
||||
} finally {
|
||||
rmSync(tempDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("device revocation and key rotation state are durable", () => {
|
||||
const tempDirectory = mkdtempSync(path.join(tmpdir(), "obsidian-sync-device-"));
|
||||
const storePath = path.join(tempDirectory, "sync-state.json");
|
||||
|
||||
try {
|
||||
const store = createFileSyncStore(storePath);
|
||||
const desktop = store.registerDevice({
|
||||
vaultId: "vault-3",
|
||||
deviceName: "Desktop"
|
||||
});
|
||||
const phone = store.registerDevice({
|
||||
vaultId: "vault-3",
|
||||
deviceName: "Phone"
|
||||
});
|
||||
|
||||
const rotation = store.rotateVaultKey("vault-3", "key-rotated");
|
||||
assert.equal(rotation.activeKeyId, "key-rotated");
|
||||
|
||||
const revoked = store.revokeDevice("vault-3", phone.deviceId);
|
||||
assert.equal(revoked.targetDeviceId, phone.deviceId);
|
||||
assert.equal(store.authenticate("vault-3", phone.deviceId, phone.token), false);
|
||||
|
||||
const restartedStore = createFileSyncStore(storePath);
|
||||
const devices = restartedStore.listDevices("vault-3");
|
||||
assert.equal(devices.activeKeyId, "key-rotated");
|
||||
assert.equal(devices.devices.find((device) => device.deviceId === phone.deviceId)?.revokedAt !== undefined, true);
|
||||
assert.equal(restartedStore.authenticate("vault-3", desktop.deviceId, desktop.token), true);
|
||||
} finally {
|
||||
rmSync(tempDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user