mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-29 14:57:05 +00:00
Add browser P2P connection check
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
|
||||
import { DEFAULT_SETTINGS, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
P2P_CHECK_APP_ID,
|
||||
generateP2PCheckSetup,
|
||||
resolveLocalP2PCheckRelayOverride,
|
||||
} from "@/apps/webpeer/src/P2PCheckSetup";
|
||||
|
||||
describe("P2P connection-check setup", () => {
|
||||
it.each(["desktop", "mobile"] as const)(
|
||||
"creates an isolated, disposable %s Setup URI and diagnostic browser peer",
|
||||
async (target) => {
|
||||
const generated = await generateP2PCheckSetup(target);
|
||||
const decoded = await decodeSettingsFromSetupURI(generated.setupURI, generated.setupPassphrase);
|
||||
|
||||
expect(decoded).not.toBe(false);
|
||||
if (decoded === false) {
|
||||
throw new Error("The generated Setup URI could not be decoded");
|
||||
}
|
||||
const effective = { ...DEFAULT_SETTINGS, ...decoded };
|
||||
|
||||
expect(generated.target).toBe(target);
|
||||
expect(generated.setupPassphrase).toMatch(/^[a-z2-9]{4}(?:-[a-z2-9]{4}){3}$/);
|
||||
expect(generated.setupURI).toMatch(/^obsidian:\/\/setuplivesync\?settings=/);
|
||||
expect(effective).toEqual(
|
||||
expect.objectContaining({
|
||||
remoteType: REMOTE_P2P,
|
||||
isConfigured: true,
|
||||
encrypt: true,
|
||||
usePathObfuscation: true,
|
||||
P2P_Enabled: true,
|
||||
P2P_AppID: P2P_CHECK_APP_ID,
|
||||
P2P_roomID: generated.groupId,
|
||||
P2P_AutoStart: true,
|
||||
P2P_AutoBroadcast: false,
|
||||
})
|
||||
);
|
||||
expect(decoded.P2P_DevicePeerName).toBeUndefined();
|
||||
expect(decoded.P2P_useDiagRTC).toBeUndefined();
|
||||
expect(effective.passphrase).toHaveLength(32);
|
||||
expect(effective.P2P_passphrase).toHaveLength(32);
|
||||
expect(effective.passphrase).not.toBe(effective.P2P_passphrase);
|
||||
expect(effective.P2P_AutoAccepting).toBe(DEFAULT_SETTINGS.P2P_AutoAccepting);
|
||||
expect(effective.P2P_AutoSyncPeers).toBe("");
|
||||
expect(effective.P2P_AutoWatchPeers).toBe("");
|
||||
expect(effective.P2P_SyncOnReplication).toBe("");
|
||||
|
||||
const remoteConfigurations = Object.values(decoded.remoteConfigurations ?? {});
|
||||
expect(remoteConfigurations).toHaveLength(1);
|
||||
expect(decoded.activeConfigurationId).toBe(remoteConfigurations[0].id);
|
||||
expect(decoded.P2P_ActiveRemoteConfigurationId).toBe(remoteConfigurations[0].id);
|
||||
const deviceRemote = ConnectionStringParser.parse(remoteConfigurations[0].uri);
|
||||
expect(deviceRemote).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "p2p",
|
||||
settings: expect.objectContaining({
|
||||
P2P_AutoStart: true,
|
||||
P2P_AutoBroadcast: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect("P2P_useDiagRTC" in deviceRemote.settings).toBe(false);
|
||||
|
||||
expect(generated.browserSettings).toEqual(
|
||||
expect.objectContaining({
|
||||
remoteType: REMOTE_P2P,
|
||||
P2P_Enabled: true,
|
||||
P2P_AppID: P2P_CHECK_APP_ID,
|
||||
P2P_roomID: effective.P2P_roomID,
|
||||
P2P_passphrase: effective.P2P_passphrase,
|
||||
passphrase: effective.passphrase,
|
||||
P2P_AutoStart: false,
|
||||
P2P_AutoBroadcast: false,
|
||||
P2P_useDiagRTC: true,
|
||||
})
|
||||
);
|
||||
expect(generated.browserDeviceName).toMatch(/^p2p-check-browser-(?:desktop|mobile)-/);
|
||||
const browserConfigurations = Object.values(generated.browserSettings.remoteConfigurations ?? {});
|
||||
expect(browserConfigurations).toHaveLength(1);
|
||||
const browserRemote = ConnectionStringParser.parse(browserConfigurations[0].uri);
|
||||
expect(browserRemote).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "p2p",
|
||||
settings: expect.objectContaining({
|
||||
P2P_AutoStart: false,
|
||||
P2P_AutoBroadcast: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect("P2P_useDiagRTC" in browserRemote.settings).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it("creates independent rooms and secrets for separate checks", async () => {
|
||||
const first = await generateP2PCheckSetup("desktop");
|
||||
const second = await generateP2PCheckSetup("desktop");
|
||||
|
||||
expect(second.groupId).not.toBe(first.groupId);
|
||||
expect(second.setupPassphrase).not.toBe(first.setupPassphrase);
|
||||
expect(second.browserSettings.P2P_passphrase).not.toBe(first.browserSettings.P2P_passphrase);
|
||||
expect(second.browserSettings.passphrase).not.toBe(first.browserSettings.passphrase);
|
||||
});
|
||||
|
||||
it("uses the same explicitly selected relay for the Setup URI and browser peer", async () => {
|
||||
const relay = "ws://127.0.0.1:4010/";
|
||||
const generated = await generateP2PCheckSetup("desktop", { relay });
|
||||
const decoded = await decodeSettingsFromSetupURI(generated.setupURI, generated.setupPassphrase);
|
||||
|
||||
expect(decoded).not.toBe(false);
|
||||
if (decoded === false) {
|
||||
throw new Error("The generated Setup URI could not be decoded");
|
||||
}
|
||||
expect(generated.relay).toBe(relay);
|
||||
expect(decoded.P2P_relays).toBe(relay);
|
||||
expect(generated.browserSettings.P2P_relays).toBe(relay);
|
||||
});
|
||||
|
||||
it("accepts a relay query override only from a loopback-served check page", () => {
|
||||
const search = "?relay=ws%3A%2F%2F127.0.0.1%3A4010%2F";
|
||||
|
||||
expect(resolveLocalP2PCheckRelayOverride({ hostname: "127.0.0.1", search })).toBe("ws://127.0.0.1:4010/");
|
||||
expect(resolveLocalP2PCheckRelayOverride({ hostname: "localhost", search })).toBe("ws://127.0.0.1:4010/");
|
||||
expect(resolveLocalP2PCheckRelayOverride({ hostname: "example.com", search })).toBeUndefined();
|
||||
expect(
|
||||
resolveLocalP2PCheckRelayOverride({ hostname: "127.0.0.1", search: "?relay=https%3A%2F%2Fexample.com" })
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
EMPTY_P2P_CHECK_DIAGNOSTICS,
|
||||
P2P_CHECK_OBSERVATION_MILLISECONDS,
|
||||
captureP2PAdditionalCheckBaseline,
|
||||
deriveP2PAdditionalCheckProgress,
|
||||
deriveP2PCheckOutcome,
|
||||
} from "@/apps/webpeer/src/P2PCheckState";
|
||||
|
||||
describe("P2P connection-check outcome", () => {
|
||||
it("waits for a target after the browser monitor joins the room", () => {
|
||||
expect(deriveP2PCheckOutcome(EMPTY_P2P_CHECK_DIAGNOSTICS, true, 5_000)).toBe("waiting");
|
||||
});
|
||||
|
||||
it("reports an active negotiation before any connection succeeds", () => {
|
||||
expect(
|
||||
deriveP2PCheckOutcome(
|
||||
{
|
||||
...EMPTY_P2P_CHECK_DIAGNOSTICS,
|
||||
totalNewConnections: 1,
|
||||
details: {
|
||||
"rtc-1": {
|
||||
connectionState: "connecting",
|
||||
iceConnectionState: "checking",
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
10_000
|
||||
)
|
||||
).toBe("connecting");
|
||||
});
|
||||
|
||||
it("keeps a failed attempt retryable until the observation period expires", () => {
|
||||
const diagnostics = {
|
||||
...EMPTY_P2P_CHECK_DIAGNOSTICS,
|
||||
totalNewConnections: 1,
|
||||
totalFailedConnections: 1,
|
||||
};
|
||||
|
||||
expect(deriveP2PCheckOutcome(diagnostics, true, 15_000)).toBe("retrying");
|
||||
expect(deriveP2PCheckOutcome(diagnostics, true, P2P_CHECK_OBSERVATION_MILLISECONDS)).toBe("inconclusive");
|
||||
});
|
||||
|
||||
it("lets a later success take precedence over failures and closure", () => {
|
||||
expect(
|
||||
deriveP2PCheckOutcome(
|
||||
{
|
||||
...EMPTY_P2P_CHECK_DIAGNOSTICS,
|
||||
totalNewConnections: 2,
|
||||
totalFailedConnections: 1,
|
||||
totalSuccessfulConnections: 1,
|
||||
totalClosedConnections: 1,
|
||||
},
|
||||
true,
|
||||
P2P_CHECK_OBSERVATION_MILLISECONDS * 2
|
||||
)
|
||||
).toBe("connected");
|
||||
});
|
||||
|
||||
it("distinguishes an idle page and a monitor-start error", () => {
|
||||
expect(deriveP2PCheckOutcome(EMPTY_P2P_CHECK_DIAGNOSTICS, false, 0)).toBe("idle");
|
||||
expect(deriveP2PCheckOutcome(EMPTY_P2P_CHECK_DIAGNOSTICS, false, 0, true)).toBe("error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("same-room additional-device progress", () => {
|
||||
const firstDeviceDiagnostics = {
|
||||
...EMPTY_P2P_CHECK_DIAGNOSTICS,
|
||||
totalNewConnections: 1,
|
||||
totalSuccessfulConnections: 1,
|
||||
details: {
|
||||
"rtc-a": {
|
||||
connectionState: "connected" as const,
|
||||
iceConnectionState: "connected" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("captures the counter and currently active connection baseline", () => {
|
||||
expect(captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics)).toEqual({
|
||||
activeConnectionIds: ["rtc-a"],
|
||||
totalClosedConnections: 0,
|
||||
totalFailedConnections: 0,
|
||||
totalNewConnections: 1,
|
||||
totalSuccessfulConnections: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not mistake a reconnect from the first peer for an additional device", () => {
|
||||
const baseline = captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics);
|
||||
const progress = deriveP2PAdditionalCheckProgress(
|
||||
{
|
||||
...firstDeviceDiagnostics,
|
||||
totalNewConnections: 2,
|
||||
totalSuccessfulConnections: 2,
|
||||
details: {
|
||||
"rtc-a": {
|
||||
connectionState: "closed",
|
||||
iceConnectionState: "closed",
|
||||
},
|
||||
"rtc-b": {
|
||||
connectionState: "connected",
|
||||
iceConnectionState: "connected",
|
||||
},
|
||||
},
|
||||
},
|
||||
baseline,
|
||||
10_000
|
||||
);
|
||||
|
||||
expect(progress).toEqual({
|
||||
activeConnections: 0,
|
||||
closedConnections: 0,
|
||||
failedConnections: 0,
|
||||
newConnections: 1,
|
||||
newActiveConnectionIds: ["rtc-b"],
|
||||
outcome: "negotiating",
|
||||
successfulConnections: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("requires both a new successful state and another simultaneous active connection", () => {
|
||||
const baseline = captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics);
|
||||
|
||||
expect(
|
||||
deriveP2PAdditionalCheckProgress(
|
||||
{
|
||||
...firstDeviceDiagnostics,
|
||||
details: {
|
||||
...firstDeviceDiagnostics.details,
|
||||
"rtc-b": {
|
||||
connectionState: "connected",
|
||||
iceConnectionState: "connected",
|
||||
},
|
||||
},
|
||||
},
|
||||
baseline,
|
||||
10_000
|
||||
).outcome
|
||||
).toBe("negotiating");
|
||||
|
||||
expect(
|
||||
deriveP2PAdditionalCheckProgress(
|
||||
{
|
||||
...firstDeviceDiagnostics,
|
||||
totalNewConnections: 2,
|
||||
totalSuccessfulConnections: 2,
|
||||
details: {
|
||||
...firstDeviceDiagnostics.details,
|
||||
"rtc-b": {
|
||||
connectionState: "connected",
|
||||
iceConnectionState: "connected",
|
||||
},
|
||||
},
|
||||
},
|
||||
baseline,
|
||||
10_000
|
||||
)
|
||||
).toEqual({
|
||||
activeConnections: 1,
|
||||
closedConnections: 0,
|
||||
failedConnections: 0,
|
||||
newConnections: 1,
|
||||
newActiveConnectionIds: ["rtc-b"],
|
||||
outcome: "connected",
|
||||
successfulConnections: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("marks an additional-device attempt inconclusive after its own observation period", () => {
|
||||
const baseline = captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics);
|
||||
expect(
|
||||
deriveP2PAdditionalCheckProgress(firstDeviceDiagnostics, baseline, P2P_CHECK_OBSERVATION_MILLISECONDS)
|
||||
.outcome
|
||||
).toBe("inconclusive");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { DEFAULT_SETTINGS, SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -73,4 +73,28 @@ describe("WebPeer runtime composition", () => {
|
||||
|
||||
await expect(runtime.start()).rejects.toThrow("WebPeer local database could not be opened");
|
||||
});
|
||||
|
||||
it("isolates a specialised runtime and applies its device name before opening the database", async () => {
|
||||
const runtime = new WebPeerRuntime({
|
||||
store: createMemoryStore(),
|
||||
deviceName: " p2p-check-browser-desktop-abc ",
|
||||
systemVaultName: "p2p-check-vault",
|
||||
});
|
||||
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
|
||||
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
P2P_AutoStart: false,
|
||||
P2P_Enabled: false,
|
||||
});
|
||||
const setSmallConfig = vi.spyOn(runtime.services.config, "setSmallConfig").mockImplementation(() => undefined);
|
||||
const openDatabase = vi.spyOn(runtime.services.database, "openDatabase").mockImplementation(async () => {
|
||||
expect(setSmallConfig).toHaveBeenCalledWith(SETTING_KEY_P2P_DEVICE_NAME, "p2p-check-browser-desktop-abc");
|
||||
return true;
|
||||
});
|
||||
|
||||
await runtime.start();
|
||||
|
||||
expect(runtime.services.API.getSystemVaultName()).toBe("p2p-check-vault");
|
||||
expect(openDatabase).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user