package service import ( "encoding/json" "io" "log" "testing" "github.com/seifghazi/claude-code-monitor/internal/config" "github.com/seifghazi/claude-code-monitor/internal/model" ) func TestPrepareRequestBodyForStorage(t *testing.T) { t.Parallel() tests := []struct { name string cfg *config.StorageConfig body interface{} assert func(t *testing.T, got interface{}) }{ { name: "metadata only returns placeholder", cfg: &config.StorageConfig{ MetadataOnly: true, }, body: map[string]interface{}{"secret": "value"}, assert: func(t *testing.T, got interface{}) { t.Helper() body, ok := got.(map[string]interface{}) if !ok || body["_storage_mode"] != "metadata_only" { t.Fatalf("expected metadata_only placeholder, got %#v", got) } }, }, { name: "request capture disabled returns placeholder", cfg: &config.StorageConfig{ CaptureRequestBody: false, }, body: map[string]interface{}{"secret": "value"}, assert: func(t *testing.T, got interface{}) { t.Helper() body, ok := got.(map[string]interface{}) if !ok || body["_storage_mode"] != "request_body_disabled" { t.Fatalf("expected request_body_disabled placeholder, got %#v", got) } }, }, { name: "redacts nested fields", cfg: &config.StorageConfig{ CaptureRequestBody: true, RedactedFields: []string{"authorization", "password"}, }, body: map[string]interface{}{ "authorization": "top-secret", "nested": map[string]interface{}{ "password": "hide-me", "keep": "visible", }, }, assert: func(t *testing.T, got interface{}) { t.Helper() body := got.(map[string]interface{}) if body["authorization"] != redactionPlaceholder { t.Fatalf("expected top-level field redacted, got %#v", body["authorization"]) } nested := body["nested"].(map[string]interface{}) if nested["password"] != redactionPlaceholder { t.Fatalf("expected nested field redacted, got %#v", nested["password"]) } if nested["keep"] != "visible" { t.Fatalf("expected keep field preserved, got %#v", nested["keep"]) } }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := prepareRequestBodyForStorage(tt.cfg, tt.body) if err != nil { t.Fatalf("prepareRequestBodyForStorage() error = %v", err) } tt.assert(t, got) }) } } func TestPrepareResponseForStorage(t *testing.T) { t.Parallel() logger := log.New(io.Discard, "", 0) t.Run("metadata only strips body fields", func(t *testing.T) { t.Parallel() got, err := prepareResponseForStorage(&config.StorageConfig{MetadataOnly: true}, logger, &model.ResponseLog{ Body: json.RawMessage(`{"secret":"value"}`), BodyText: "raw", StreamingChunks: []string{"chunk"}, ChunkTimings: []model.ChunkTiming{{Index: 0}}, }) if err != nil { t.Fatalf("prepareResponseForStorage() error = %v", err) } if got == nil { t.Fatal("expected response clone") } if got.Body != nil || got.BodyText != "" || got.StreamingChunks != nil || got.ChunkTimings != nil { t.Fatalf("expected body fields stripped, got %#v", got) } }) t.Run("redacts json response bodies", func(t *testing.T) { t.Parallel() got, err := prepareResponseForStorage(&config.StorageConfig{ CaptureResponseBody: true, RedactedFields: []string{"api_key"}, }, logger, &model.ResponseLog{ Body: json.RawMessage(`{"api_key":"secret","nested":{"keep":"ok"}}`), }) if err != nil { t.Fatalf("prepareResponseForStorage() error = %v", err) } var body map[string]interface{} if err := json.Unmarshal(got.Body, &body); err != nil { t.Fatalf("unmarshal redacted body: %v", err) } if body["api_key"] != redactionPlaceholder { t.Fatalf("expected api_key redacted, got %#v", body["api_key"]) } nested := body["nested"].(map[string]interface{}) if nested["keep"] != "ok" { t.Fatalf("expected nested field preserved, got %#v", nested["keep"]) } }) t.Run("preserves non json body bytes", func(t *testing.T) { t.Parallel() original := json.RawMessage(`not-json`) got, err := prepareResponseForStorage(&config.StorageConfig{ CaptureResponseBody: true, RedactedFields: []string{"token"}, }, logger, &model.ResponseLog{ Body: original, }) if err != nil { t.Fatalf("prepareResponseForStorage() error = %v", err) } if string(got.Body) != string(original) { t.Fatalf("expected original non-json body preserved, got %q", string(got.Body)) } }) }