All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 31s
CI / build-ai-gateway (push) Successful in 1m8s
CI / build-ha-gateway (push) Successful in 1m19s
CI / build-discord-bot (push) Successful in 59s
CI / build-alexa-bridge (push) Successful in 1m8s
CI / build-alert-bridge (push) Successful in 1m17s
CI / build-tts-gateway (push) Successful in 1m10s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
New standalone service that receives authenticated HTTP POSTs (e.g. from a cronjob elsewhere on the home network) and posts them to a Discord channel via an Incoming Webhook. Fire-and-forget, no dependency on discord-bot's bot session at all - mirrors alexa-bridge's precedent of a small bridge service with its own HTTP listener and auth rather than adding an inbound edge to an existing internal service. Wires alert-bridge into go.work, CI (changes filter, test job, new build-alert-bridge job), and the other five Dockerfiles' COPY lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
|
)
|
|
|
|
type fakeAlertHandler struct {
|
|
handleFn func(ctx context.Context, alert domain.Alert) error
|
|
got domain.Alert
|
|
called bool
|
|
}
|
|
|
|
func (f *fakeAlertHandler) Handle(ctx context.Context, alert domain.Alert) error {
|
|
f.called = true
|
|
f.got = alert
|
|
if f.handleFn != nil {
|
|
return f.handleFn(ctx, alert)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func doRequest(h *Handler, apiKey, body string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodPost, "/alerts", strings.NewReader(body))
|
|
if apiKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func TestHandler_Unauthorized(t *testing.T) {
|
|
fake := &fakeAlertHandler{}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
rec := doRequest(h, "wrong-key", `{"source":"x","message":"y"}`)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
if fake.called {
|
|
t.Fatal("app should not be called when unauthorized")
|
|
}
|
|
}
|
|
|
|
func TestHandler_MissingAuthHeader(t *testing.T) {
|
|
fake := &fakeAlertHandler{}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/alerts", strings.NewReader(`{"source":"x","message":"y"}`))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandler_BadJSON(t *testing.T) {
|
|
fake := &fakeAlertHandler{}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
rec := doRequest(h, "secret", `not json`)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandler_MissingFields(t *testing.T) {
|
|
fake := &fakeAlertHandler{}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
rec := doRequest(h, "secret", `{"source":"","message":""}`)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandler_InvalidLevel(t *testing.T) {
|
|
fake := &fakeAlertHandler{}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
rec := doRequest(h, "secret", `{"source":"x","message":"y","level":"critical"}`)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandler_DefaultsLevelToInfo(t *testing.T) {
|
|
fake := &fakeAlertHandler{}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
rec := doRequest(h, "secret", `{"source":"ba-cronjob","message":"Started"}`)
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d", rec.Code)
|
|
}
|
|
if fake.got.Level != domain.LevelInfo {
|
|
t.Fatalf("expected default level info, got %q", fake.got.Level)
|
|
}
|
|
if fake.got.Source != "ba-cronjob" || fake.got.Message != "Started" {
|
|
t.Fatalf("unexpected alert passed through: %+v", fake.got)
|
|
}
|
|
}
|
|
|
|
func TestHandler_NotifierFailure(t *testing.T) {
|
|
fake := &fakeAlertHandler{handleFn: func(ctx context.Context, alert domain.Alert) error {
|
|
return errors.New("discord unreachable")
|
|
}}
|
|
h := NewHandler(fake, "secret")
|
|
|
|
rec := doRequest(h, "secret", `{"source":"x","message":"y","level":"error"}`)
|
|
if rec.Code != http.StatusBadGateway {
|
|
t.Fatalf("expected 502, got %d", rec.Code)
|
|
}
|
|
}
|