test: cover unauth rule + heartbeat failures
This commit is contained in:
196
tests/connection-heartbeat-failures.test.ts
Normal file
196
tests/connection-heartbeat-failures.test.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { buildHeartbeat, decodeBuiltin, encodeBuiltin } from "../../Yonexus.Protocol/src/index.js";
|
||||||
|
import { createYonexusServerRuntime } from "../plugin/core/runtime.js";
|
||||||
|
import { createClientRecord, type ClientRecord } from "../plugin/core/persistence.js";
|
||||||
|
import type { YonexusServerStore } from "../plugin/core/store.js";
|
||||||
|
import type { ClientConnection, ServerTransport } from "../plugin/core/transport.js";
|
||||||
|
|
||||||
|
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((record) => [record.identifier, record]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath: "/tmp/yonexus-server-connection-heartbeat-failures.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 client of clients) {
|
||||||
|
persisted.set(client.identifier, client);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockTransport() {
|
||||||
|
const sent: Array<{ connection: ClientConnection; message: string }> = [];
|
||||||
|
|
||||||
|
const transport: ServerTransport = {
|
||||||
|
isRunning: false,
|
||||||
|
connections: new Map(),
|
||||||
|
start: vi.fn(),
|
||||||
|
stop: vi.fn(),
|
||||||
|
send: vi.fn(),
|
||||||
|
sendToConnection: vi.fn((connection: ClientConnection, message: string) => {
|
||||||
|
sent.push({ connection, message });
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
broadcast: vi.fn(),
|
||||||
|
closeConnection: vi.fn(),
|
||||||
|
promoteToAuthenticated: vi.fn(),
|
||||||
|
removeTempConnection: vi.fn(),
|
||||||
|
assignIdentifierToTemp: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
return { transport, sent };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("YNX-1105d: Connection & Heartbeat Failure Paths", () => {
|
||||||
|
let now = 1_710_000_000;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
now = 1_710_000_000;
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("CF-06: unauthenticated rule message closes connection", async () => {
|
||||||
|
const record = createClientRecord("client-a");
|
||||||
|
const store = createMockStore([record]);
|
||||||
|
const { transport } = createMockTransport();
|
||||||
|
|
||||||
|
const runtime = createYonexusServerRuntime({
|
||||||
|
config: {
|
||||||
|
followerIdentifiers: ["client-a"],
|
||||||
|
notifyBotToken: "stub-token",
|
||||||
|
adminUserId: "admin-user",
|
||||||
|
listenHost: "127.0.0.1",
|
||||||
|
listenPort: 8787
|
||||||
|
},
|
||||||
|
store,
|
||||||
|
transport,
|
||||||
|
now: () => now
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.start();
|
||||||
|
|
||||||
|
const connection = createConnection("client-a");
|
||||||
|
runtime.state.registry.sessions.set("client-a", {
|
||||||
|
identifier: "client-a",
|
||||||
|
socket: connection.ws,
|
||||||
|
isAuthenticated: false,
|
||||||
|
connectedAt: now,
|
||||||
|
lastActivityAt: now,
|
||||||
|
publicKey: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.handleMessage(connection, "chat::hello");
|
||||||
|
|
||||||
|
expect(connection.ws.close).toHaveBeenCalledWith(1008, "Not authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("HF-03: heartbeat before auth returns error", async () => {
|
||||||
|
const record = createClientRecord("client-a");
|
||||||
|
const store = createMockStore([record]);
|
||||||
|
const { transport, sent } = createMockTransport();
|
||||||
|
|
||||||
|
const runtime = createYonexusServerRuntime({
|
||||||
|
config: {
|
||||||
|
followerIdentifiers: ["client-a"],
|
||||||
|
notifyBotToken: "stub-token",
|
||||||
|
adminUserId: "admin-user",
|
||||||
|
listenHost: "127.0.0.1",
|
||||||
|
listenPort: 8787
|
||||||
|
},
|
||||||
|
store,
|
||||||
|
transport,
|
||||||
|
now: () => now
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.start();
|
||||||
|
|
||||||
|
const connection = createConnection("client-a");
|
||||||
|
runtime.state.registry.sessions.set("client-a", {
|
||||||
|
identifier: "client-a",
|
||||||
|
socket: connection.ws,
|
||||||
|
isAuthenticated: false,
|
||||||
|
connectedAt: now,
|
||||||
|
lastActivityAt: now,
|
||||||
|
publicKey: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.handleMessage(
|
||||||
|
connection,
|
||||||
|
encodeBuiltin(
|
||||||
|
buildHeartbeat(
|
||||||
|
{ identifier: "client-a", status: "alive" },
|
||||||
|
{ requestId: "req-hb-early", timestamp: now }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
||||||
|
expect(lastMessage.type).toBe("error");
|
||||||
|
expect(lastMessage.payload).toMatchObject({
|
||||||
|
code: "AUTH_FAILED"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("HF-04: heartbeat without session returns error", async () => {
|
||||||
|
const record = createClientRecord("client-a");
|
||||||
|
const store = createMockStore([record]);
|
||||||
|
const { transport, sent } = createMockTransport();
|
||||||
|
|
||||||
|
const runtime = createYonexusServerRuntime({
|
||||||
|
config: {
|
||||||
|
followerIdentifiers: ["client-a"],
|
||||||
|
notifyBotToken: "stub-token",
|
||||||
|
adminUserId: "admin-user",
|
||||||
|
listenHost: "127.0.0.1",
|
||||||
|
listenPort: 8787
|
||||||
|
},
|
||||||
|
store,
|
||||||
|
transport,
|
||||||
|
now: () => now
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.start();
|
||||||
|
|
||||||
|
const connection = createConnection("client-a");
|
||||||
|
|
||||||
|
await runtime.handleMessage(
|
||||||
|
connection,
|
||||||
|
encodeBuiltin(
|
||||||
|
buildHeartbeat(
|
||||||
|
{ identifier: "client-a", status: "alive" },
|
||||||
|
{ requestId: "req-hb-unauth", timestamp: now }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
||||||
|
expect(lastMessage.type).toBe("error");
|
||||||
|
expect(lastMessage.payload).toMatchObject({
|
||||||
|
code: "AUTH_FAILED"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user