Files
petrbalvin 3a38f00dc0
Test / test (push) Successful in 2m1s
Release / gates (push) Successful in 1m57s
Release / build (amd64, freebsd) (push) Successful in 1m26s
Release / build (amd64, linux) (push) Successful in 1m30s
Release / build (arm64, freebsd) (push) Successful in 1m28s
Release / build (arm64, linux) (push) Successful in 1m49s
Release / build (loong64, linux) (push) Successful in 1m30s
Release / build (riscv64, linux) (push) Successful in 1m29s
Release / release (push) Successful in 41s
feat: contact form backend for linux and freebsd servers
Assisted-by: GLM 5.3 Flash
2026-09-29 00:32:56 +02:00

128 lines
3.7 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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 <jane@example.com>",
"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())
}
}