package inbox import ( "encoding/json" "os" "path/filepath" "strings" "sync" "testing" ) func TestWriter_Write_BasicShape(t *testing.T) { dir := t.TempDir() w, err := NewWriter(dir) if err != nil { t.Fatal(err) } if err := w.Write("bob", &Event{ From: "foreman", Type: "INFO", Priority: "normal", Payload: "hi", }); err != nil { t.Fatal(err) } got := readLines(t, filepath.Join(dir, "bob.inbox")) if len(got) != 1 { t.Fatalf("want 1 line, got %d", len(got)) } var ev Event if err := json.Unmarshal([]byte(got[0]), &ev); err != nil { t.Fatalf("not JSON: %v", err) } if ev.To != "bob" { t.Errorf("To = %q, want bob", ev.To) } if ev.From != "foreman" { t.Errorf("From = %q, want foreman", ev.From) } if ev.Payload != "hi" { t.Errorf("Payload = %q", ev.Payload) } if !strings.HasPrefix(ev.ID, "ping-") { t.Errorf("ID %q missing ping- prefix", ev.ID) } if len(ev.ID) != len("ping-")+8 { t.Errorf("ID %q wrong length", ev.ID) } if ev.TS == "" { t.Errorf("TS unset") } } func TestWriter_Write_PreservesProvidedIDAndTS(t *testing.T) { dir := t.TempDir() w, err := NewWriter(dir) if err != nil { t.Fatal(err) } want := &Event{ TS: "2026-05-06T12:00:00Z", ID: "ping-deadbeef", From: "x", Type: "INFO", Priority: "normal", Payload: "p", } if err := w.Write("r", want); err != nil { t.Fatal(err) } got := readLines(t, filepath.Join(dir, "r.inbox")) var ev Event json.Unmarshal([]byte(got[0]), &ev) if ev.ID != want.ID { t.Errorf("ID overwritten: got %q want %q", ev.ID, want.ID) } if ev.TS != want.TS { t.Errorf("TS overwritten: got %q want %q", ev.TS, want.TS) } } func TestWriter_Write_OmitsEmptyOptionalFields(t *testing.T) { dir := t.TempDir() w, _ := NewWriter(dir) w.Write("r", &Event{From: "f", Type: "INFO", Priority: "normal", Payload: "p"}) line := readLines(t, filepath.Join(dir, "r.inbox"))[0] if strings.Contains(line, "sentinel") { t.Errorf("empty sentinel leaked into JSON: %s", line) } if strings.Contains(line, "source") { t.Errorf("empty source leaked into JSON: %s", line) } } func TestWriter_Write_PingShapeMatchesCLI(t *testing.T) { // The ping CLI emits JSON with separators=(",", ":") (compact) and these // keys in this order: ts, id, from, to, type, priority, payload [, sentinel]. // Verify our marshaled output preserves the key set and compactness. dir := t.TempDir() w, _ := NewWriter(dir) w.Write("r", &Event{ From: "f", Type: "INFO", Priority: "normal", Payload: "p", Sentinel: "/x", }) line := strings.TrimSpace(readLines(t, filepath.Join(dir, "r.inbox"))[0]) wantKeys := []string{`"ts":`, `"id":`, `"from":`, `"to":`, `"type":`, `"priority":`, `"payload":`, `"sentinel":`} for _, k := range wantKeys { if !strings.Contains(line, k) { t.Errorf("missing key %s in: %s", k, line) } } // compact (no spaces around colons or commas) if strings.Contains(line, ": ") || strings.Contains(line, ", ") { t.Errorf("non-compact JSON: %s", line) } } func TestWriter_Write_ConcurrentSameRecipient(t *testing.T) { // 100 goroutines writing to the same inbox should produce 100 valid JSON // lines, none torn or interleaved. dir := t.TempDir() w, _ := NewWriter(dir) const n = 100 var wg sync.WaitGroup for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() w.Write("r", &Event{From: "f", Type: "INFO", Priority: "normal", Payload: "x"}) }(i) } wg.Wait() lines := readLines(t, filepath.Join(dir, "r.inbox")) if len(lines) != n { t.Fatalf("got %d lines, want %d (torn writes?)", len(lines), n) } for i, l := range lines { var ev Event if err := json.Unmarshal([]byte(l), &ev); err != nil { t.Errorf("line %d not JSON: %v: %s", i, err, l) } } } func TestWriter_Write_RejectsBadInputs(t *testing.T) { w, _ := NewWriter(t.TempDir()) if err := w.Write("", &Event{}); err == nil { t.Error("empty recipient: expected error") } if err := w.Write("r", nil); err == nil { t.Error("nil event: expected error") } } func TestNewWriter_RejectsEmptyDir(t *testing.T) { if _, err := NewWriter(""); err == nil { t.Error("expected error for empty dir") } } func readLines(t *testing.T, path string) []string { t.Helper() b, err := os.ReadFile(path) if err != nil { t.Fatal(err) } out := []string{} for _, l := range strings.Split(strings.TrimRight(string(b), "\n"), "\n") { if l != "" { out = append(out, l) } } return out }