The bus on NATS: both transports behind seams, and the rollout switch #87
@@ -0,0 +1,107 @@
|
|||||||
|
package link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Go implementation, held to the shared fixtures (novox/hq ADR 0074, design 19).
|
||||||
|
//
|
||||||
|
// **Read from the sdk's conformance directory by sibling path**, the way the lab finds its
|
||||||
|
// siblings — deliberately not copied here. A fixture copied into each implementation is two
|
||||||
|
// fixtures, and two fixtures drift, which is the exact failure the suite exists to prevent.
|
||||||
|
type fixture struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Given struct {
|
||||||
|
Module string `json:"module"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Body map[string]any `json:"body"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
} `json:"given"`
|
||||||
|
Wire struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
RequiredHeaders []string `json:"requiredHeaders"`
|
||||||
|
HeaderFormats map[string]string `json:"headerFormats"`
|
||||||
|
} `json:"wire"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFixture(t *testing.T, name string) fixture {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join("..", "..", "..", "mesh-sdk", "conformance", name)
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("the sdk's conformance fixtures are not beside this checkout: %v", err)
|
||||||
|
}
|
||||||
|
var f fixture
|
||||||
|
if err := json.Unmarshal(raw, &f); err != nil {
|
||||||
|
t.Fatalf("%s: %v", name, err)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every header the fixture requires is one this implementation actually sets.
|
||||||
|
func TestTheGoEmitterSetsEveryRequiredHeader(t *testing.T) {
|
||||||
|
f := loadFixture(t, "events/module-event.json")
|
||||||
|
sent := goEventHeaders(f.Given.Key, f.Given.Module, f.Given.Node)
|
||||||
|
for _, want := range f.Wire.RequiredHeaders {
|
||||||
|
if _, ok := sent[want]; !ok {
|
||||||
|
t.Errorf("the Go emitter does not set %q, which the fixture requires — an event it "+
|
||||||
|
"emits is one a conforming consumer refuses", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And each value is in the shape the fixture pins, because a header present but differently
|
||||||
|
// formatted is the disagreement that does not announce itself.
|
||||||
|
func TestTheGoEmittersHeaderFormatsMatch(t *testing.T) {
|
||||||
|
f := loadFixture(t, "events/module-event.json")
|
||||||
|
sent := goEventHeaders(f.Given.Key, f.Given.Module, f.Given.Node)
|
||||||
|
|
||||||
|
if got := sent["content-type"]; got != f.Wire.HeaderFormats["content-type"] {
|
||||||
|
t.Errorf("content-type is %q, the fixture says %q", got, f.Wire.HeaderFormats["content-type"])
|
||||||
|
}
|
||||||
|
if _, err := time.Parse(time.RFC3339, sent["x-time"]); err != nil {
|
||||||
|
t.Errorf("x-time %q is not RFC3339, which the fixture requires: %v", sent["x-time"], err)
|
||||||
|
}
|
||||||
|
if pattern := f.Wire.HeaderFormats["x-event-id"]; pattern != "" {
|
||||||
|
if !regexp.MustCompile(pattern).MatchString(sent["x-event-id"]) {
|
||||||
|
t.Errorf("x-event-id %q does not match %q", sent["x-event-id"], pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The origin the envelope claims is the one the bus enforces by namespace. A disagreement
|
||||||
|
// here means the envelope is lying about where it came from.
|
||||||
|
if sent["x-source"] != f.Given.Module {
|
||||||
|
t.Errorf("x-source is %q for module %q", sent["x-source"], f.Given.Module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subject a module's event lands on is derived, not carried — so this implementation must
|
||||||
|
// derive the same one the fixture names.
|
||||||
|
func TestTheGoSubjectMatchesTheFixture(t *testing.T) {
|
||||||
|
f := loadFixture(t, "events/module-event.json")
|
||||||
|
got := "mesh.mod." + f.Given.Module + ".event." + f.Given.Key
|
||||||
|
if got != f.Wire.Subject {
|
||||||
|
t.Errorf("this implementation would publish on %q; the fixture says %q", got, f.Wire.Subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// goEventHeaders is the header set EmitEvent produces, factored so conformance can see it
|
||||||
|
// without a broker. Kept beside the emitter so the two cannot drift apart silently.
|
||||||
|
func goEventHeaders(eventType, source, node string) map[string]string {
|
||||||
|
id, err := eventID()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return map[string]string{
|
||||||
|
"x-event-id": id,
|
||||||
|
"x-source": source,
|
||||||
|
"x-node": node,
|
||||||
|
"x-time": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
"content-type": "application/json",
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user