package debug import ( "context" "strings" "testing" mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" ) func TestRedactArgs(t *testing.T) { in := map[string]any{ "owner": "octocat", "repo": "hello", "token": "ghp_secret", "password": "hunter2", "nested": map[string]any{ "api_key": "abc", "safe": "value", "authHeader": "Bearer xyz", "client_id": "ok-to-log", "credentials": "shh", }, "items": []any{ map[string]any{"secret": "zzz", "ok": "yes"}, }, } out := RedactArgs(in).(map[string]any) if out["owner"] != "octocat" { t.Fatalf("owner should be unchanged, got %v", out["owner"]) } if out["token"] != "[REDACTED]" { t.Fatalf("token should be redacted, got %v", out["token"]) } if out["password"] != "[REDACTED]" { t.Fatalf("password should be redacted, got %v", out["password"]) } nested := out["nested"].(map[string]any) if nested["api_key"] != "[REDACTED]" || nested["authHeader"] != "[REDACTED]" || nested["credentials"] != "[REDACTED]" { t.Fatalf("nested sensitive keys not redacted: %+v", nested) } if nested["safe"] != "value" { t.Fatalf("safe key should be unchanged: %+v", nested) } // client_id does not match any sensitive substring; "id" alone shouldn't trip. if nested["client_id"] != "ok-to-log" { t.Fatalf("client_id should be unchanged: %+v", nested) } item := out["items"].([]any)[0].(map[string]any) if item["secret"] != "[REDACTED]" { t.Fatalf("list item secret not redacted: %+v", item) } if item["ok"] != "yes" { t.Fatalf("safe list item key changed: %+v", item) } // Original input must not be mutated. if in["token"] != "ghp_secret" { t.Fatalf("RedactArgs mutated input: %+v", in) } } func TestRedactArgs_NonMap(t *testing.T) { for _, v := range []any{"plain", 42, nil, true} { if got := RedactArgs(v); got != v { t.Fatalf("non-map value should pass through unchanged: got %v want %v", got, v) } } } func TestStripQueryTokens(t *testing.T) { in := "https://gitea.example.com/api/v1/repos/x/y?token=abc&page=1&access_token=def" got := StripQueryTokens(in) if strings.Contains(got, "abc") || strings.Contains(got, "def") { t.Fatalf("token value leaked through: %s", got) } if !strings.Contains(got, "page=1") { t.Fatalf("non-sensitive params should be preserved: %s", got) } } func TestStripQueryTokens_NoChange(t *testing.T) { in := "https://gitea.example.com/api/v1/repos/x/y?page=1" if got := StripQueryTokens(in); got != in { t.Fatalf("non-sensitive URL was modified: %s -> %s", in, got) } } func TestTruncatePreview(t *testing.T) { long := strings.Repeat("a", MaxResultPreview+50) got := TruncatePreview(long) if !strings.HasSuffix(got, "...") { t.Fatalf("truncated preview should end with ellipsis: %q", got[len(got)-10:]) } if len(got) > MaxResultPreview+5 { t.Fatalf("preview not bounded: len=%d", len(got)) } short := "hi" if got := TruncatePreview(short); got != short { t.Fatalf("short string should be returned as-is, got %q", got) } } func TestTruncatePreview_MultibyteSafe(t *testing.T) { // Make a string with multi-byte runes long enough to trip naive byte-slicing. r := strings.Repeat("日", MaxResultPreview+10) got := TruncatePreview(r) // Must remain valid UTF-8: a naive byte slice on this would split a rune. for _, b := range got { _ = b // ranging validates that decoding doesn't panic } } func TestRequestIDFromContext(t *testing.T) { if got := RequestIDFromContext(context.Background()); got != "" { t.Fatalf("absent request id should return \"\", got %q", got) } ctx := context.WithValue(context.Background(), mcpContext.RequestIDContextKey, "abc123") if got := RequestIDFromContext(ctx); got != "abc123" { t.Fatalf("got %q, want abc123", got) } } func TestNewRequestID_UniqueAndHex(t *testing.T) { seen := map[string]bool{} for i := 0; i < 100; i++ { id := NewRequestID() if id == "" { t.Fatal("empty request id") } if seen[id] { t.Fatalf("duplicate request id: %s", id) } seen[id] = true } }