From a7e1a9c210d28f09f764509e9223dc913e0497b7 Mon Sep 17 00:00:00 2001 From: nav Date: Thu, 9 Apr 2026 00:36:37 +0000 Subject: [PATCH] test(protocol): add codec coverage --- .gitignore | 4 + tests/codec.test.ts | 433 +++++++++++++++++++++++--------------------- 2 files changed, 228 insertions(+), 209 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6d5efa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +coverage/ +*.log diff --git a/tests/codec.test.ts b/tests/codec.test.ts index 29cf1fe..75cab18 100644 --- a/tests/codec.test.ts +++ b/tests/codec.test.ts @@ -1,237 +1,252 @@ -: -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, expect, it } from "vitest"; + import { - encodeBuiltin, + buildAuthFailed, + buildAuthRequest, + buildAuthSuccess, + buildDisconnectNotice, + buildError, + buildHeartbeat, + buildHeartbeatAck, + buildHello, + buildHelloAck, + buildPairConfirm, + buildPairFailed, + buildPairRequest, + buildPairSuccess, + buildRePairRequired, + buildStatusUpdate, + CodecError, decodeBuiltin, - parseRuleMessage, - encodeRuleMessage, - parseRewrittenRuleMessage, + encodeBuiltin, encodeRewrittenRuleMessage, + encodeRuleMessage, isBuiltinMessage, - CodecError -} from "../src/codec.js"; -import type { BuiltinEnvelope, HelloPayload, HelloAckPayload } from "../src/types.js"; + parseRewrittenRuleMessage, + parseRuleMessage +} from "../src/index.js"; -describe("Protocol Codec", () => { - describe("encodeBuiltin / decodeBuiltin", () => { - it("should encode and decode a hello message", () => { - const envelope: BuiltinEnvelope<"hello", HelloPayload> = { - type: "hello", - requestId: "req_001", - timestamp: 1711886400, +describe("Protocol Codec - encodeBuiltin / decodeBuiltin", () => { + it("encodes and decodes a hello envelope", () => { + const envelope = buildHello( + { + identifier: "client-a", + hasSecret: true, + hasKeyPair: true, + publicKey: "pk-test", + protocolVersion: "1" + }, + { requestId: "req-001" } + ); + + const encoded = encodeBuiltin(envelope); + expect(encoded.startsWith("builtin::")).toBe(true); + + const decoded = decodeBuiltin(encoded); + expect(decoded.type).toBe("hello"); + expect(decoded.requestId).toBe("req-001"); + expect(decoded.payload).toMatchObject({ + identifier: "client-a", + hasSecret: true, + hasKeyPair: true, + publicKey: "pk-test", + protocolVersion: "1" + }); + }); + + it("encodes and decodes all builtin message types", () => { + const testCases = [ + { + builder: buildHelloAck, + payload: { identifier: "client-a", nextAction: "pair_required" as const } + }, + { + builder: buildPairRequest, payload: { identifier: "client-a", - hasSecret: true, - hasKeyPair: true, - publicKey: "pk_test", - protocolVersion: "1" + expiresAt: 1_710_000_000, + ttlSeconds: 300, + adminNotification: "sent" as const, + codeDelivery: "out_of_band" as const } - }; - - const encoded = encodeBuiltin(envelope); - expect(encoded).toBe( - 'builtin::{"type":"hello","requestId":"req_001","timestamp":1711886400,"payload":{"identifier":"client-a","hasSecret":true,"hasKeyPair":true,"publicKey":"pk_test","protocolVersion":"1"}}' - ); - - const decoded = decodeBuiltin(encoded); - expect(decoded).toEqual(envelope); - }); - - it("should encode and decode a hello_ack message", () => { - const envelope: BuiltinEnvelope<"hello_ack", HelloAckPayload> = { - type: "hello_ack", - requestId: "req_001", - timestamp: 1711886401, + }, + { + builder: buildPairConfirm, + payload: { identifier: "client-a", pairingCode: "ABCD-1234-XYZ" } + }, + { + builder: buildPairSuccess, + payload: { identifier: "client-a", secret: "secret-xyz", pairedAt: 1_710_000_000 } + }, + { + builder: buildPairFailed, + payload: { identifier: "client-a", reason: "expired" as const } + }, + { + builder: buildAuthRequest, payload: { identifier: "client-a", - nextAction: "pair_required" + nonce: "RANDOM24CHARACTERSTRINGX", + proofTimestamp: 1_710_000_000, + signature: "sig-base64", + publicKey: "pk-test" } - }; + }, + { + builder: buildAuthSuccess, + payload: { identifier: "client-a", authenticatedAt: 1_710_000_000, status: "online" as const } + }, + { + builder: buildAuthFailed, + payload: { identifier: "client-a", reason: "invalid_signature" as const } + }, + { + builder: buildRePairRequired, + payload: { identifier: "client-a", reason: "nonce_collision" as const } + }, + { + builder: buildHeartbeat, + payload: { identifier: "client-a", status: "alive" as const } + }, + { + builder: buildHeartbeatAck, + payload: { identifier: "client-a", status: "online" as const } + }, + { + builder: buildStatusUpdate, + payload: { identifier: "client-a", status: "unstable" as const, reason: "heartbeat_timeout_7m" } + }, + { + builder: buildDisconnectNotice, + payload: { identifier: "client-a", reason: "heartbeat_timeout_11m" } + }, + { + builder: buildError, + payload: { code: "MALFORMED_MESSAGE" as const, message: "Invalid JSON" } + } + ]; + for (const { builder, payload } of testCases) { + const envelope = builder(payload as never, { requestId: "test-req" }); const encoded = encodeBuiltin(envelope); const decoded = decodeBuiltin(encoded); - expect(decoded).toEqual(envelope); - }); - it("should handle envelope without optional fields", () => { - const envelope: BuiltinEnvelope = { - type: "heartbeat" - }; - - const encoded = encodeBuiltin(envelope); - expect(encoded).toBe('builtin::{"type":"heartbeat"}'); - - const decoded = decodeBuiltin(encoded); - expect(decoded).toEqual(envelope); - }); - - it("should throw CodecError for malformed builtin message", () => { - expect(() => decodeBuiltin("invalid")).toThrow(CodecError); - expect(() => decodeBuiltin("builtin::")).toThrow(CodecError); - expect(() => decodeBuiltin("builtin::invalid json")).toThrow(CodecError); - }); - - it("should throw CodecError for non-builtin message prefix", () => { - expect(() => decodeBuiltin("hello::{\"type\":\"test\"}")).toThrow(CodecError); - }); + expect(decoded.type).toBe(envelope.type); + expect(decoded.requestId).toBe("test-req"); + expect(decoded.payload).toMatchObject(payload); + } }); - describe("isBuiltinMessage", () => { - it("should return true for builtin messages", () => { - expect(isBuiltinMessage('builtin::{"type":"hello"}')).toBe(true); - expect(isBuiltinMessage("builtin::{}")).toBe(true); - }); - - it("should return false for non-builtin messages", () => { - expect(isBuiltinMessage("hello::world")).toBe(false); - expect(isBuiltinMessage("rule::content")).toBe(false); - expect(isBuiltinMessage("")).toBe(false); - }); + it("throws CodecError for malformed messages", () => { + expect(() => decodeBuiltin("not-builtin::{")).toThrow(CodecError); + expect(() => decodeBuiltin("builtin::not-json")).toThrow(CodecError); + expect(() => decodeBuiltin("builtin::{}")).toThrow(CodecError); // missing type + expect(() => decodeBuiltin("no-delimiter")).toThrow(CodecError); + expect(() => decodeBuiltin("")).toThrow(CodecError); }); - describe("parseRuleMessage", () => { - it("should parse simple rule message", () => { - const result = parseRuleMessage("chat_sync::{\"body\":\"hello\"}"); - expect(result).toEqual({ - rule: "chat_sync", - content: '{"body":"hello"}' - }); - }); - - it("should parse rule message with content containing ::", () => { - const result = parseRuleMessage("chat_sync::sender::extra::{\"body\":\"hello::world\"}"); - expect(result).toEqual({ - rule: "chat_sync", - content: 'sender::extra::{"body":"hello::world"}' - }); - }); - - it("should throw CodecError for malformed rule message", () => { - expect(() => parseRuleMessage("invalid")).toThrow(CodecError); - expect(() => parseRuleMessage("")).toThrow(CodecError); - }); - - it("should reject builtin as rule identifier", () => { - expect(() => parseRuleMessage('builtin::{"type":"hello"}')).toThrow(CodecError); - }); - }); - - describe("encodeRuleMessage", () => { - it("should encode rule message", () => { - const encoded = encodeRuleMessage("chat_sync", '{"body":"hello"}'); - expect(encoded).toBe('chat_sync::{"body":"hello"}'); - }); - - it("should reject builtin as rule", () => { - expect(() => encodeRuleMessage("builtin", "content")).toThrow(CodecError); - }); - }); - - describe("parseRewrittenRuleMessage", () => { - it("should parse rewritten server-side message", () => { - const result = parseRewrittenRuleMessage("chat_sync::client-a::{\"body\":\"hello\"}"); - expect(result).toEqual({ - rule: "chat_sync", - sender: "client-a", - content: '{"body":"hello"}' - }); - }); - - it("should handle content with :: delimiters", () => { - const result = parseRewrittenRuleMessage("chat_sync::client-a::extra::{\"body\":\"hello::world\"}"); - expect(result).toEqual({ - rule: "chat_sync", - sender: "client-a", - content: 'extra::{"body":"hello::world"}' - }); - }); - - it("should throw CodecError for malformed rewritten message", () => { - expect(() => parseRewrittenRuleMessage("rule::content")).toThrow(CodecError); - expect(() => parseRewrittenRuleMessage("invalid")).toThrow(CodecError); - }); - }); - - describe("encodeRewrittenRuleMessage", () => { - it("should encode rewritten message", () => { - const encoded = encodeRewrittenRuleMessage("chat_sync", "client-a", '{"body":"hello"}'); - expect(encoded).toBe('chat_sync::client-a::{"body":"hello"}'); - }); - - it("should reject builtin as rule", () => { - expect(() => encodeRewrittenRuleMessage("builtin", "sender", "content")).toThrow(CodecError); - }); + it("throws CodecError for invalid envelope input", () => { + expect(() => encodeBuiltin(null as never)).toThrow(CodecError); + expect(() => encodeBuiltin({} as never)).toThrow(CodecError); + expect(() => encodeBuiltin({ type: 123 } as never)).toThrow(CodecError); }); }); -describe("Protocol Message Examples", () => { - describe("Hello Flow", () => { - it("should demonstrate complete hello flow", () => { - // Client sends hello - const hello: BuiltinEnvelope<"hello", HelloPayload> = { - type: "hello", - requestId: "req_001", - timestamp: Math.floor(Date.now() / 1000), - payload: { - identifier: "client-a", - hasSecret: false, - hasKeyPair: true, - publicKey: "ed25519_pk_test123", - protocolVersion: "1" - } - }; - - const helloWire = encodeBuiltin(hello); - expect(isBuiltinMessage(helloWire)).toBe(true); - - // Server parses hello - const parsedHello = decodeBuiltin(helloWire); - expect(parsedHello.type).toBe("hello"); - expect((parsedHello.payload as HelloPayload).identifier).toBe("client-a"); - - // Server responds with hello_ack - const helloAck: BuiltinEnvelope<"hello_ack", HelloAckPayload> = { - type: "hello_ack", - requestId: hello.requestId, - timestamp: Math.floor(Date.now() / 1000), - payload: { - identifier: "client-a", - nextAction: "pair_required" - } - }; - - const ackWire = encodeBuiltin(helloAck); - const parsedAck = decodeBuiltin(ackWire); - expect(parsedAck.type).toBe("hello_ack"); - expect((parsedAck.payload as HelloAckPayload).nextAction).toBe("pair_required"); - }); +describe("Protocol Codec - Rule Message Parsing", () => { + it("parses rule messages correctly", () => { + const parsed = parseRuleMessage("chat_sync::{\"body\":\"hello\"}"); + expect(parsed.ruleIdentifier).toBe("chat_sync"); + expect(parsed.content).toBe('{"body":"hello"}'); }); - describe("Rule Message Flow", () => { - it("should demonstrate client to server message", () => { - const message = encodeRuleMessage("chat_sync", '{"conversationId":"abc","body":"hello"}'); - expect(message).toBe('chat_sync::{"conversationId":"abc","body":"hello"}'); - - // Server parses - const parsed = parseRuleMessage(message); - expect(parsed.rule).toBe("chat_sync"); - expect(parsed.content).toBe('{"conversationId":"abc","body":"hello"}'); - }); + it("handles content containing :: delimiters", () => { + const parsed = parseRuleMessage("rule::a::b::c"); + expect(parsed.ruleIdentifier).toBe("rule"); + expect(parsed.content).toBe("a::b::c"); + }); - it("should demonstrate server-side rewriting", () => { - // Server rewrites incoming message - const rewritten = encodeRewrittenRuleMessage( - "chat_sync", - "client-a", - '{"conversationId":"abc","body":"hello"}' - ); - expect(rewritten).toBe('chat_sync::client-a::{"conversationId":"abc","body":"hello"}'); + it("throws CodecError for invalid rule messages", () => { + expect(() => parseRuleMessage("no-delimiter")).toThrow(CodecError); + expect(() => parseRuleMessage(":")).toThrow(CodecError); + expect(() => parseRuleMessage("")).toThrow(CodecError); + expect(() => parseRuleMessage("::content")).toThrow(CodecError); // empty identifier + }); - // Server-side handler parses - const parsed = parseRewrittenRuleMessage(rewritten); - expect(parsed.rule).toBe("chat_sync"); - expect(parsed.sender).toBe("client-a"); - expect(parsed.content).toBe('{"conversationId":"abc","body":"hello"}'); - }); + it("throws CodecError for reserved builtin identifier", () => { + expect(() => parseRuleMessage("builtin::something")).toThrow( + /reserved/ + ); + }); + + it("throws CodecError for invalid rule identifier characters", () => { + expect(() => parseRuleMessage("rule with spaces::content")).toThrow(CodecError); + // Note: special chars in content are allowed - only the rule identifier is validated + expect(() => parseRuleMessage("rule::with::special!::chars")).not.toThrow(); // content can contain special chars + }); + + it("isBuiltinMessage identifies builtin messages", () => { + expect(isBuiltinMessage("builtin::{\"type\":\"hello\"}")).toBe(true); + expect(isBuiltinMessage("rule::content")).toBe(false); + expect(isBuiltinMessage("not-builtin::content")).toBe(false); + expect(isBuiltinMessage("")).toBe(false); + }); +}); + +describe("Protocol Codec - Server Rewritten Messages", () => { + it("parses rewritten messages with sender identifier", () => { + const parsed = parseRewrittenRuleMessage("chat_sync::client-a::{\"body\":\"hello\"}"); + expect(parsed.ruleIdentifier).toBe("chat_sync"); + expect(parsed.senderIdentifier).toBe("client-a"); + expect(parsed.messageContent).toBe('{"body":"hello"}'); + expect(parsed.content).toBe("client-a::{\"body\":\"hello\"}"); + }); + + it("handles complex content with multiple delimiters", () => { + const parsed = parseRewrittenRuleMessage("rule::sender::a::b::c"); + expect(parsed.ruleIdentifier).toBe("rule"); + expect(parsed.senderIdentifier).toBe("sender"); + expect(parsed.messageContent).toBe("a::b::c"); + }); + + it("throws CodecError for malformed rewritten messages", () => { + expect(() => parseRewrittenRuleMessage("no-delimiters")).toThrow(CodecError); + expect(() => parseRewrittenRuleMessage("rule::only-one")).toThrow(/sender/); + expect(() => parseRewrittenRuleMessage("::sender::content")).toThrow(/Empty/); + // Note: "rule:::content" has empty sender which is caught by "missing sender identifier delimiter" check + expect(() => parseRewrittenRuleMessage("rule:::content")).toThrow(/sender/); + }); +}); + +describe("Protocol Codec - encodeRuleMessage", () => { + it("encodes rule messages correctly", () => { + const encoded = encodeRuleMessage("chat_sync", '{"body":"hello"}'); + expect(encoded).toBe('chat_sync::{"body":"hello"}'); + }); + + it("throws CodecError for invalid rule identifiers", () => { + expect(() => encodeRuleMessage("", "content")).toThrow(CodecError); + expect(() => encodeRuleMessage("builtin", "content")).toThrow(/reserved/); + expect(() => encodeRuleMessage("rule with spaces", "content")).toThrow(CodecError); + expect(() => encodeRuleMessage("rule!", "content")).toThrow(CodecError); + }); + + it("throws CodecError for non-string content", () => { + expect(() => encodeRuleMessage("rule", null as never)).toThrow(CodecError); + }); +}); + +describe("Protocol Codec - encodeRewrittenRuleMessage", () => { + it("encodes rewritten messages correctly", () => { + const encoded = encodeRewrittenRuleMessage("chat_sync", "client-a", '{"body":"hello"}'); + expect(encoded).toBe('chat_sync::client-a::{"body":"hello"}'); + }); + + it("throws CodecError for reserved rule identifier", () => { + expect(() => encodeRewrittenRuleMessage("builtin", "sender", "content")).toThrow(/reserved/); + }); + + it("throws CodecError for empty identifiers", () => { + expect(() => encodeRewrittenRuleMessage("", "sender", "content")).toThrow(CodecError); + expect(() => encodeRewrittenRuleMessage("rule", "", "content")).toThrow(CodecError); }); });