package frame import ( "bytes" "strings" "testing" ) func TestMarshalUnmarshalRoundTrip(t *testing.T) { orig := Frame{ Type: PUB, Subject: "room.general", Sender: "abc123", MsgID: "01J000000000000000000000", Epoch: 3, Nonce: []byte{1, 2, 3, 4}, Payload: []byte("ciphertext-bytes"), Blob: &BlobRef{Hash: "deadbeef", Nonce: []byte{9, 8, 7}, Size: 42}, Sig: []byte{0xaa, 0xbb}, } b, err := orig.Marshal() if err != nil { t.Fatalf("Marshal: %v", err) } got, err := Unmarshal(b) if err != nil { t.Fatalf("Unmarshal: %v", err) } if got.Type != orig.Type || got.Subject != orig.Subject || got.Sender != orig.Sender || got.MsgID != orig.MsgID || got.Epoch != orig.Epoch { t.Fatalf("envelope mismatch: got %+v want %+v", got, orig) } if !bytes.Equal(got.Nonce, orig.Nonce) || !bytes.Equal(got.Payload, orig.Payload) || !bytes.Equal(got.Sig, orig.Sig) { t.Fatalf("byte fields mismatch") } if got.Blob == nil || got.Blob.Hash != orig.Blob.Hash || got.Blob.Size != orig.Blob.Size || !bytes.Equal(got.Blob.Nonce, orig.Blob.Nonce) { t.Fatalf("blob ref mismatch: %+v", got.Blob) } } // TestThreadingRoundTrip (golden) verifies that the additive threading fields // survive a marshal/unmarshal cycle and that a REACT frame keeps its target. func TestThreadingRoundTrip(t *testing.T) { orig := Frame{ Type: REACT, Subject: "room.general", Sender: "abc123", MsgID: "01J000000000000000000002", Epoch: 1, ThreadID: "01J000000000000000000000", ReplyTo: "01J000000000000000000001", Payload: []byte("👍"), } b, err := orig.Marshal() if err != nil { t.Fatalf("Marshal: %v", err) } got, err := Unmarshal(b) if err != nil { t.Fatalf("Unmarshal: %v", err) } if got.Type != REACT { t.Fatalf("type mismatch: got %d want REACT(%d)", got.Type, REACT) } if got.ThreadID != orig.ThreadID || got.ReplyTo != orig.ReplyTo { t.Fatalf("threading fields lost: got thr=%q re=%q", got.ThreadID, got.ReplyTo) } if !bytes.Equal(got.Payload, orig.Payload) { t.Fatalf("reaction payload mismatch: got %q", got.Payload) } } // TestNonThreadedWireBackCompat (edge) asserts that a frame without threading // metadata serializes with NO thr/re keys at all, so its bytes — and therefore // its signature — are identical to a pre-threading frame. This is the // guarantee that makes the new fields a non-breaking, additive change. func TestNonThreadedWireBackCompat(t *testing.T) { f := Frame{Type: PUB, Subject: "room.general", Sender: "x", MsgID: "id", Epoch: 2, Payload: []byte("hi")} b, err := f.Marshal() if err != nil { t.Fatalf("Marshal: %v", err) } s := string(b) if strings.Contains(s, "\"thr\"") || strings.Contains(s, "\"re\"") { t.Fatalf("threading keys leaked into a non-threaded frame: %s", s) } // SigningBytes of a non-threaded frame must also be free of the keys, so old // signatures over equivalent frames still verify. if sb := f.SigningBytes(); strings.Contains(string(sb), "\"thr\"") || strings.Contains(string(sb), "\"re\"") { t.Fatalf("threading keys leaked into SigningBytes: %s", sb) } } // TestUnmarshalRejectsGarbage (error path) verifies that malformed wire bytes // surface as an error rather than a silently zero-valued frame. func TestUnmarshalRejectsGarbage(t *testing.T) { if _, err := Unmarshal([]byte("{not valid json")); err == nil { t.Fatalf("expected error unmarshaling garbage, got nil") } } func TestEndpointIDDeterministic(t *testing.T) { pub := []byte("some-ed25519-public-key-bytes-32") a := EndpointID(pub) b := EndpointID(pub) if a != b { t.Fatalf("EndpointID not deterministic: %q != %q", a, b) } if a == "" { t.Fatalf("EndpointID returned empty string") } // Different inputs must produce different ids. if EndpointID([]byte("other-key")) == a { t.Fatalf("EndpointID collision for different inputs") } } func TestSigningBytesExcludesSig(t *testing.T) { withSig := Frame{Type: PUB, Subject: "s", Sender: "x", MsgID: "id", Epoch: 1, Payload: []byte("p"), Sig: []byte{1, 2, 3}} noSig := withSig noSig.Sig = nil if !bytes.Equal(withSig.SigningBytes(), noSig.SigningBytes()) { t.Fatalf("SigningBytes should be identical regardless of Sig field") } // And SigningBytes must not be affected by mutating Sig afterward (value receiver). sb := withSig.SigningBytes() if bytes.Contains(sb, []byte{1, 2, 3}) { t.Fatalf("SigningBytes leaked the Sig bytes") } }