// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT //go:build linux || freebsd package telegram import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "sourcedock.dev/petrbalvin/nuntius/internal/contactform" ) var testRequest = contactform.Request{ Name: "Jane Doe", Email: "jane@example.com", Service: "architecture", Message: "Hello, I would like to discuss an engagement.", } // apiCall carries what the fake API saw; the channel establishes the // happens-before edge the HTTP response alone does not. type apiCall struct { req *http.Request body string } // fakeAPI answers with the given payload and records the request; the // returned function waits for the call and yields it. func fakeAPI(t *testing.T, status int, payload string) (*Notifier, func() apiCall) { t.Helper() calls := make(chan apiCall, 1) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) calls <- apiCall{req: r, body: string(b)} w.WriteHeader(status) _, _ = w.Write([]byte(payload)) })) t.Cleanup(srv.Close) n := New("123:secret", "-100200300", 5*time.Second) n.apiURL = srv.URL return n, func() apiCall { select { case c := <-calls: return c case <-time.After(5 * time.Second): t.Fatal("the fake API never saw the request") return apiCall{} } } } func TestNotifyPostsTheSummary(t *testing.T) { n, call := fakeAPI(t, http.StatusOK, `{"ok":true}`) if err := n.Notify("contact", testRequest); err != nil { t.Fatalf("notify: %v", err) } got := call() if p := got.req.URL.Path; p != "/bot123:secret/sendMessage" { t.Errorf("path = %q, want the bot token and sendMessage", p) } var payload struct { ChatID string `json:"chat_id"` Text string `json:"text"` } if err := json.Unmarshal([]byte(got.body), &payload); err != nil { t.Fatalf("decode payload: %v", err) } if payload.ChatID != "-100200300" { t.Errorf("chat_id = %q, want the configured chat", payload.ChatID) } for _, want := range []string{ "New message on contact", "From: Jane Doe ", "Service interest: architecture", "Hello, I would like to discuss an engagement.", } { if !strings.Contains(payload.Text, want) { t.Errorf("summary missing %q", want) } } } func TestNotifyReportsAPIRefusal(t *testing.T) { // The API refuses with a non-200 status and an ok:false body; both // shapes must surface as an error. n, _ := fakeAPI(t, http.StatusUnauthorized, `{"ok":false,"description":"Unauthorized"}`) if err := n.Notify("contact", testRequest); err == nil { t.Errorf("notify over HTTP 401 succeeded, want an error") } n, _ = fakeAPI(t, http.StatusOK, `{"ok":false,"description":"chat not found"}`) if err := n.Notify("contact", testRequest); err == nil { t.Errorf("notify over an ok:false body succeeded, want an error") } } // TestNotifyRedactsTheToken pins the credential rule: the token never // reaches an error string, and error text is what the log carries. func TestNotifyRedactsTheToken(t *testing.T) { var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // A connection reset surfaces as an URL error carrying the full // request URL, token included. srv.CloseClientConnections() })) t.Cleanup(srv.Close) n := New("123:secret", "-100200300", 5*time.Second) n.apiURL = srv.URL err := n.Notify("contact", testRequest) if err == nil { t.Fatalf("notify over a killed connection succeeded, want an error") } if strings.Contains(err.Error(), "123:secret") { t.Errorf("error text carries the bot token: %q", err.Error()) } }