carlos / platform Public

host: request records end-to-end through the real sink and recorder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4aa577870611f94f9933948ed143e7856675e861 parent 0449605
1 file changed, +145 −0
  • internal/host/reqrec_e2e_test.go +145 −0
diff --git a/internal/host/reqrec_e2e_test.go b/internal/host/reqrec_e2e_test.go
new file mode 100644
index 000000000..1c5e04b59
--- /dev/null
+++ b/internal/host/reqrec_e2e_test.go
@@ -0,0 +1,145 @@
+package host
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/applog"
+ "github.com/carlosframework/platform/internal/edge"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+// TestRequestRecordsEndToEnd drives the production wiring end to end: a real
+// registry and edge, the recorder newRequestRecorder builds against a real
+// applog.Sink, and reqrecLoop closing the minute. Nothing here is a fixture
+// of the recorder's inputs, so a seam that drops a field, a stream or the
+// salt shows up as a failed assertion.
+//
+// The traffic is the shape the feature exists for: one password-reset link
+// with its secret in the path, then a scanner hammering a missing page.
+func TestRequestRecordsEndToEnd(t *testing.T) {
+ const token = "3f9a0c1d2e4b5a6978877665544332211"
+
+ backend := httptest.NewServer(http.NotFoundHandler())
+ t.Cleanup(backend.Close)
+
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatalf("open registry: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "shop.example.com", Kind: "service", Account: "bab", App: "shop",
+ Addr: backend.Listener.Addr().String(), Status: "ready", Version: "v1",
+ }); err != nil {
+ t.Fatalf("upsert: %v", err)
+ }
+ e, err := edge.New(reg, nil)
+ if err != nil {
+ t.Fatalf("edge.New: %v", err)
+ }
+
+ sink := applog.NewSink("box1", time.Now)
+ rec := newRequestRecorder(sink, t.TempDir(), func(string) string { return "" })
+ if rec == nil {
+ t.Fatal("newRequestRecorder returned nil with a sink and no kill switch")
+ }
+ e.EnableRequestRecords(rec)
+
+ get := func(path string) {
+ t.Helper()
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://shop.example.com"+path, nil)
+ r.RemoteAddr = "203.0.113.7:51234"
+ e.ServeHTTP(w, r)
+ if w.Code != http.StatusNotFound {
+ t.Fatalf("GET %s = %d, want 404 from the backend", path, w.Code)
+ }
+ }
+ // The reset link goes first so it is inside the 60-record sample, which
+ // is the case where the token would leak if masking were not applied.
+ get("/reset/" + token)
+ for i := 0; i < 70; i++ {
+ get("/wp-login.php")
+ }
+
+ tick := make(chan time.Time, 1)
+ tick <- time.Now().Add(time.Minute)
+ close(tick)
+ reqrecLoop(rec, tick)
+
+ recs := sink.Drain()[applog.AppKey{Acct: "bab", App: "shop"}]
+ var notFound, minutes []applog.Record
+ for _, r := range recs {
+ if r.Stream != applog.StreamEdge {
+ t.Fatalf("record on stream %q, want the edge stream: %+v", r.Stream, r)
+ }
+ switch r.Fields["kind"] {
+ case "notfound":
+ notFound = append(notFound, r)
+ case "minute":
+ minutes = append(minutes, r)
+ }
+ }
+ if len(notFound) != 60 {
+ t.Errorf("%d notfound records, want 60 (the per-minute cap)", len(notFound))
+ }
+ if len(minutes) != 1 {
+ t.Fatalf("%d minute records, want 1 (records: %d)", len(minutes), len(recs))
+ }
+ if got := minutes[0].Fields["s404"]; got != "71" {
+ t.Errorf("minute s404 = %q, want 71", got)
+ }
+ if got := minutes[0].Fields["dropped_notfound"]; got != "11" {
+ t.Errorf("minute dropped_notfound = %q, want 11", got)
+ }
+
+ // The secret must not appear in any field, key or message.
+ leaks := func(recs []applog.Record, where string) {
+ t.Helper()
+ for _, r := range recs {
+ if strings.Contains(r.Msg, token) {
+ t.Errorf("%s: raw token in message %q", where, r.Msg)
+ }
+ for k, v := range r.Fields {
+ if strings.Contains(k, token) || strings.Contains(v, token) {
+ t.Errorf("%s: raw token in field %s=%q", where, k, v)
+ }
+ }
+ }
+ }
+ // Masking must have happened rather than the record being dropped: the
+ // reset link is in the sample, with its secret segment replaced.
+ masked := false
+ for _, r := range notFound {
+ if r.Fields["path"] == "/reset/[masked]" {
+ masked = true
+ }
+ }
+ if !masked {
+ t.Error("no notfound record carries the masked reset path")
+ }
+ leaks(recs, "drained")
+
+ // What ships is the encoded chunk, so the records must survive that trip
+ // unchanged and the token must be absent from the bytes themselves.
+ b, err := applog.EncodeChunk(recs)
+ if err != nil {
+ t.Fatalf("EncodeChunk: %v", err)
+ }
+ if strings.Contains(string(b), token) {
+ t.Error("raw token in the encoded chunk")
+ }
+ back, err := applog.DecodeChunk(b)
+ if err != nil {
+ t.Fatalf("DecodeChunk: %v", err)
+ }
+ if !reflect.DeepEqual(back, recs) {
+ t.Errorf("records changed across EncodeChunk/DecodeChunk:\n got %+v\nwant %+v", back, recs)
+ }
+ leaks(back, "decoded")
+}