667 lines
22 KiB
TypeScript
667 lines
22 KiB
TypeScript
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
decodeBuiltin,
|
|
encodeBuiltin,
|
|
buildHello,
|
|
buildHelloAck,
|
|
buildPairRequest,
|
|
buildPairConfirm,
|
|
buildPairFailed,
|
|
buildPairSuccess,
|
|
type PairConfirmPayload,
|
|
type PairFailedPayload,
|
|
YONEXUS_PROTOCOL_VERSION,
|
|
ProtocolErrorCode
|
|
} from "../../Yonexus.Protocol/src/index.js";
|
|
import { createYonexusServerRuntime } from "../../Yonexus.Server/plugin/core/runtime.js";
|
|
import type { ClientRecord } from "../../Yonexus.Server/plugin/core/persistence.js";
|
|
import type { YonexusServerStore } from "../../Yonexus.Server/plugin/core/store.js";
|
|
import type { ClientConnection, ServerTransport } from "../../Yonexus.Server/plugin/core/transport.js";
|
|
|
|
/**
|
|
* YNX-1105b: Pairing Failure Path Tests
|
|
*
|
|
* Covers:
|
|
* - PF-01: Invalid pairing code
|
|
* - PF-02: Expired pairing code
|
|
* - PF-03: Identifier not in allowlist
|
|
* - PF-04: Admin notification failed (partial - notification stub)
|
|
* - PF-05: Empty pairing code
|
|
* - PF-06: Malformed pair_confirm payload
|
|
* - PF-07: Double pairing attempt
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Test Utilities
|
|
// ============================================================================
|
|
|
|
function createMockSocket() {
|
|
return { close: vi.fn() } as unknown as ClientConnection["ws"];
|
|
}
|
|
|
|
function createConnection(identifier: string | null = null): ClientConnection {
|
|
return {
|
|
identifier,
|
|
ws: createMockSocket(),
|
|
connectedAt: 1_710_000_000,
|
|
isAuthenticated: false
|
|
};
|
|
}
|
|
|
|
function createMockStore(initialClients: ClientRecord[] = []): YonexusServerStore {
|
|
const persisted = new Map(initialClients.map((r) => [r.identifier, r]));
|
|
return {
|
|
filePath: "/tmp/test.json",
|
|
load: vi.fn(async () => ({
|
|
version: 1,
|
|
persistedAt: 1_710_000_000,
|
|
clients: new Map(persisted)
|
|
})),
|
|
save: vi.fn(async (clients: Iterable<ClientRecord>) => {
|
|
persisted.clear();
|
|
for (const c of clients) persisted.set(c.identifier, c);
|
|
})
|
|
};
|
|
}
|
|
|
|
function createMockTransport() {
|
|
const sent: Array<{ connection: ClientConnection; message: string }> = [];
|
|
const closed: Array<{ identifier: string; code?: number; reason?: string }> = [];
|
|
|
|
const transport: ServerTransport = {
|
|
isRunning: false,
|
|
connections: new Map(),
|
|
start: vi.fn(),
|
|
stop: vi.fn(),
|
|
send: vi.fn((id: string, msg: string) => { sent.push({ connection: { identifier: id } as ClientConnection, message: msg }); return true; }),
|
|
sendToConnection: vi.fn((conn: ClientConnection, msg: string) => { sent.push({ connection: conn, message: msg }); return true; }),
|
|
broadcast: vi.fn(),
|
|
closeConnection: vi.fn((id: string, code?: number, reason?: string) => { closed.push({ identifier: id, code, reason }); return true; }),
|
|
promoteToAuthenticated: vi.fn(),
|
|
removeTempConnection: vi.fn(),
|
|
assignIdentifierToTemp: vi.fn()
|
|
};
|
|
|
|
return { transport, sent, closed };
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pairing Failure Path Tests
|
|
// ============================================================================
|
|
|
|
describe("YNX-1105b: Pairing Failure Paths", () => {
|
|
let now = 1_710_000_000;
|
|
|
|
beforeEach(() => {
|
|
now = 1_710_000_000;
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("PF-01: Invalid pairing code", () => {
|
|
it("returns pair_failed(invalid_code) when wrong code submitted", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
// Start pairing flow
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const pairingCode = runtime.state.registry.clients.get("client-a")?.pairingCode;
|
|
expect(pairingCode).toBeDefined();
|
|
|
|
// Submit wrong code
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: "WRONG-CODE-999" },
|
|
{ timestamp: now + 10 }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
expect((lastMessage.payload as PairFailedPayload).reason).toBe("invalid_code");
|
|
|
|
// Client remains in pending state, can retry
|
|
expect(runtime.state.registry.clients.get("client-a")?.pairingStatus).toBe("pending");
|
|
});
|
|
|
|
it("allows retry after invalid code failure", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const correctCode = runtime.state.registry.clients.get("client-a")?.pairingCode;
|
|
|
|
// First attempt: wrong code
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: "WRONG" },
|
|
{ timestamp: now + 10 }
|
|
)));
|
|
expect(decodeBuiltin(sent.at(-1)!.message).type).toBe("pair_failed");
|
|
|
|
// Second attempt: correct code
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: correctCode! },
|
|
{ timestamp: now + 20 }
|
|
)));
|
|
expect(decodeBuiltin(sent.at(-1)!.message).type).toBe("pair_success");
|
|
});
|
|
});
|
|
|
|
describe("PF-02: Expired pairing code", () => {
|
|
it("returns pair_failed(expired) when code submitted after expiry", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const pairingCode = runtime.state.registry.clients.get("client-a")?.pairingCode;
|
|
const expiresAt = runtime.state.registry.clients.get("client-a")?.pairingExpiresAt;
|
|
expect(expiresAt).toBeDefined();
|
|
|
|
// Advance time past expiry
|
|
now = expiresAt! + 1;
|
|
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: pairingCode! },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
expect((lastMessage.payload as PairFailedPayload).reason).toBe("expired");
|
|
|
|
// Pairing state reset to allow new pairing
|
|
expect(runtime.state.registry.clients.get("client-a")?.pairingStatus).toBe("unpaired");
|
|
expect(runtime.state.registry.clients.get("client-a")?.pairingCode).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("PF-03: Identifier not in allowlist", () => {
|
|
it("rejects hello from unknown identifier", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["allowed-client"], // Only this one is allowed
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "unknown-client", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Should receive hello_ack with rejected or an error
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("error");
|
|
// Identifier should not be registered
|
|
expect(runtime.state.registry.clients.has("unknown-client")).toBe(false);
|
|
});
|
|
|
|
it("rejects pair_confirm from unknown identifier even if somehow received", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["allowed-client"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
// Try to send pair_confirm for unknown client
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "unknown-client", pairingCode: "SOME-CODE" },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
expect((lastMessage.payload as PairFailedPayload).reason).toBe("identifier_not_allowed");
|
|
});
|
|
});
|
|
|
|
describe("PF-04: Admin notification failure", () => {
|
|
it("fails pairing when notification cannot be sent", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "", // Empty token should cause notification failure
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Check the pair_request indicates notification failure
|
|
const pairRequest = sent.find(m => decodeBuiltin(m.message).type === "pair_request");
|
|
expect(pairRequest).toBeDefined();
|
|
|
|
// Should not have created a valid pending pairing
|
|
const record = runtime.state.registry.clients.get("client-a");
|
|
if (record?.pairingStatus === "pending") {
|
|
// If notification failed, pairing should indicate this
|
|
expect(record.pairingNotifyStatus).toBe("failed");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("PF-05: Empty pairing code", () => {
|
|
it("rejects empty pairing code", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Submit empty code
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: "" },
|
|
{ timestamp: now + 10 }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
expect((lastMessage.payload as PairFailedPayload).reason).toBe("invalid_code");
|
|
});
|
|
|
|
it("rejects whitespace-only pairing code", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Submit whitespace code
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: " \t\n " },
|
|
{ timestamp: now + 10 }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
});
|
|
});
|
|
|
|
describe("PF-06: Malformed pair_confirm payload", () => {
|
|
it("handles missing identifier in pair_confirm", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Send malformed payload (missing fields)
|
|
await runtime.handleMessage(conn, encodeBuiltin({
|
|
type: "pair_confirm",
|
|
timestamp: now,
|
|
payload: { pairingCode: "SOME-CODE" } // Missing identifier
|
|
}));
|
|
|
|
// Should receive an error response
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("error");
|
|
});
|
|
|
|
it("handles missing pairingCode in pair_confirm", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Send malformed payload (missing pairingCode)
|
|
await runtime.handleMessage(conn, encodeBuiltin({
|
|
type: "pair_confirm",
|
|
timestamp: now,
|
|
payload: { identifier: "client-a" } // Missing pairingCode
|
|
}));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
});
|
|
});
|
|
|
|
describe("PF-07: Double pairing attempt", () => {
|
|
it("rejects pair_confirm for already paired client", async () => {
|
|
const store = createMockStore([{
|
|
identifier: "client-a",
|
|
pairingStatus: "paired",
|
|
publicKey: "existing-key",
|
|
secret: "existing-secret",
|
|
status: "offline",
|
|
recentNonces: [],
|
|
recentHandshakeAttempts: [],
|
|
createdAt: now - 1000,
|
|
updatedAt: now - 500,
|
|
pairedAt: now - 500
|
|
}]);
|
|
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
// Try to pair an already paired client
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: "SOME-CODE" },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Should reject since already paired
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
|
|
// Existing trust material preserved
|
|
const record = runtime.state.registry.clients.get("client-a");
|
|
expect(record?.pairingStatus).toBe("paired");
|
|
expect(record?.secret).toBe("existing-secret");
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("PF-08: pairing attempt during an active paired session is rejected without losing trust", async () => {
|
|
const store = createMockStore([{
|
|
identifier: "client-a",
|
|
pairingStatus: "paired",
|
|
publicKey: "existing-key",
|
|
secret: "existing-secret",
|
|
status: "online",
|
|
recentNonces: [],
|
|
recentHandshakeAttempts: [],
|
|
createdAt: now - 1000,
|
|
updatedAt: now - 10,
|
|
pairedAt: now - 500,
|
|
lastAuthenticatedAt: now - 5,
|
|
lastHeartbeatAt: now - 5
|
|
}]);
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection("client-a");
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: "NEW-PAIR-CODE" },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
expect((lastMessage.payload as PairFailedPayload).reason).toBe("internal_error");
|
|
|
|
const record = runtime.state.registry.clients.get("client-a");
|
|
expect(record).toMatchObject({
|
|
pairingStatus: "paired",
|
|
secret: "existing-secret",
|
|
publicKey: "existing-key",
|
|
status: "online"
|
|
});
|
|
});
|
|
|
|
it("handles concurrent pair_confirm from different connections with same identifier", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
// First connection starts pairing
|
|
const conn1 = createConnection();
|
|
await runtime.handleMessage(conn1, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const pairingCode = runtime.state.registry.clients.get("client-a")?.pairingCode;
|
|
|
|
// Second connection tries to pair with same identifier
|
|
const conn2 = createConnection();
|
|
await runtime.handleMessage(conn2, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: pairingCode! },
|
|
{ timestamp: now + 10 }
|
|
)));
|
|
|
|
// Should succeed - pairing is identifier-based, not connection-based
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_success");
|
|
});
|
|
|
|
it("cleans up pending pairing state on expiry", async () => {
|
|
const store = createMockStore();
|
|
const { transport, sent } = createMockTransport();
|
|
const runtime = createYonexusServerRuntime({
|
|
config: {
|
|
followerIdentifiers: ["client-a"],
|
|
notifyBotToken: "test-token",
|
|
adminUserId: "admin",
|
|
listenHost: "127.0.0.1",
|
|
listenPort: 8787
|
|
},
|
|
store,
|
|
transport,
|
|
now: () => now
|
|
});
|
|
|
|
await runtime.start();
|
|
|
|
const conn = createConnection();
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildHello(
|
|
{ identifier: "client-a", hasSecret: false, hasKeyPair: true, protocolVersion: YONEXUS_PROTOCOL_VERSION },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
// Verify pending state exists
|
|
const recordBefore = runtime.state.registry.clients.get("client-a");
|
|
expect(recordBefore?.pairingStatus).toBe("pending");
|
|
expect(recordBefore?.pairingCode).toBeDefined();
|
|
|
|
// Expire and try to use old code
|
|
now += 400; // Past default TTL
|
|
await runtime.handleMessage(conn, encodeBuiltin(buildPairConfirm(
|
|
{ identifier: "client-a", pairingCode: recordBefore?.pairingCode! },
|
|
{ timestamp: now }
|
|
)));
|
|
|
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
|
expect(lastMessage.type).toBe("pair_failed");
|
|
expect((lastMessage.payload as PairFailedPayload).reason).toBe("expired");
|
|
|
|
// State cleaned up
|
|
const recordAfter = runtime.state.registry.clients.get("client-a");
|
|
expect(recordAfter?.pairingStatus).toBe("unpaired");
|
|
expect(recordAfter?.pairingCode).toBeUndefined();
|
|
});
|
|
});
|
|
});
|