package apiclient import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestGradeHappyPathRelaysBodyVerbatim(t *testing.T) { // Deliberately odd formatting/key order: Grade must hand back the exact // bytes, never a re-marshalled reshaping of them. const body = `{ "content":null, "embeds": [ {"title":"Phoenix"} ] }` var gotPath, gotToken, gotQuery string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotToken = r.Header.Get("X-Thermograph-Internal-Token") var req map[string]string if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Errorf("decode request: %v", err) } gotQuery = req["query"] w.Header().Set("Content-Type", "application/json") if _, err := w.Write([]byte(body)); err != nil { t.Errorf("write response: %v", err) } })) defer srv.Close() c := New(srv.URL, "tok123") raw, err := c.Grade(context.Background(), "phoenix") if err != nil { t.Fatalf("Grade: %v", err) } if string(raw) != body { t.Errorf("body not relayed verbatim:\n got %q\nwant %q", raw, body) } if gotPath != "/internal/discord/grade" { t.Errorf("path = %q", gotPath) } if gotToken != "tok123" { t.Errorf("token header = %q, want %q", gotToken, "tok123") } if gotQuery != "phoenix" { t.Errorf("query = %q", gotQuery) } } func TestJobsSendTokenAndHitRightPaths(t *testing.T) { paths := map[string]bool{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %s, want POST", r.Method) } if r.Header.Get("X-Thermograph-Internal-Token") != "tok" { t.Errorf("missing/wrong token header on %s", r.URL.Path) } paths[r.URL.Path] = true if _, err := w.Write([]byte(`{"ok":true}`)); err != nil { t.Errorf("write response: %v", err) } })) defer srv.Close() c := New(srv.URL+"/", "tok") // trailing slash must not produce "//internal" if err := c.WarmCities(context.Background()); err != nil { t.Fatalf("WarmCities: %v", err) } if err := c.IndexNow(context.Background()); err != nil { t.Fatalf("IndexNow: %v", err) } for _, p := range []string{"/internal/jobs/warm-cities", "/internal/jobs/indexnow"} { if !paths[p] { t.Errorf("path %s was never hit (saw %v)", p, paths) } } } func TestNon2xxReturnsStatusAndTruncatedBody(t *testing.T) { long := strings.Repeat("x", 2000) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, long, http.StatusBadGateway) })) defer srv.Close() c := New(srv.URL, "tok") _, err := c.Grade(context.Background(), "phoenix") if err == nil { t.Fatal("expected error on 502, got nil") } msg := err.Error() if !strings.Contains(msg, "502") { t.Errorf("error should include status, got: %s", msg) } if !strings.Contains(msg, "truncated") { t.Errorf("error should mark truncation, got %d chars: %.100s...", len(msg), msg) } if len(msg) > 1000 { t.Errorf("error message not truncated: %d chars", len(msg)) } } func TestContextCancellation(t *testing.T) { block := make(chan struct{}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-block // hold the request open until the test ends })) defer srv.Close() defer close(block) c := New(srv.URL, "tok") ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() _, err := c.Grade(ctx, "phoenix") if err == nil { t.Fatal("expected error from cancelled context, got nil") } if !strings.Contains(err.Error(), "deadline") && !strings.Contains(err.Error(), "cancel") { t.Errorf("expected a context-shaped error, got: %v", err) } }