174 lines
5.4 KiB
TypeScript
174 lines
5.4 KiB
TypeScript
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 });
|
|
}
|
|
}); |