Files
ntfy-discord/internal/ntfy/client_test.go
Billy D. f97ad0e7cb
Some checks failed
Build and Push / build (push) Failing after 4m36s
Build and Push / test (push) Has been cancelled
feat: implement ntfy-discord bridge in Go
- SSE subscription to ntfy with auto-reconnect
- Discord webhook integration with embed formatting
- Priority to color mapping, tag to emoji conversion
- Native HashiCorp Vault support (Kubernetes + token auth)
- Hot reload secrets via fsnotify or Vault polling
- Prometheus metrics (/metrics endpoint)
- Health/ready endpoints for Kubernetes probes
- Comprehensive unit tests and fuzz tests
- Multi-stage Docker build (~10MB scratch image)
- CI/CD pipeline for Gitea Actions
2026-02-02 18:13:55 -05:00

228 lines
5.5 KiB
Go

package ntfy
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestNewClient(t *testing.T) {
client := NewClient("http://ntfy.example.com", []string{"alerts", "updates"})
if client.baseURL != "http://ntfy.example.com" {
t.Errorf("baseURL = %s, want http://ntfy.example.com", client.baseURL)
}
if len(client.topics) != 2 {
t.Errorf("topics length = %d, want 2", len(client.topics))
}
}
func TestNewClient_TrimsTrailingSlash(t *testing.T) {
client := NewClient("http://ntfy.example.com/", []string{"test"})
if client.baseURL != "http://ntfy.example.com" {
t.Errorf("baseURL = %s, want http://ntfy.example.com (trailing slash removed)", client.baseURL)
}
}
func TestMessage_JSON(t *testing.T) {
msg := Message{
ID: "test-123",
Time: 1706803200,
Event: "message",
Topic: "alerts",
Title: "Test Alert",
Message: "This is a test",
Priority: 4,
Tags: []string{"warning", "fire"},
Click: "https://example.com",
}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var decoded Message
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if decoded.ID != msg.ID {
t.Errorf("ID = %s, want %s", decoded.ID, msg.ID)
}
if decoded.Priority != msg.Priority {
t.Errorf("Priority = %d, want %d", decoded.Priority, msg.Priority)
}
if len(decoded.Tags) != 2 {
t.Errorf("Tags length = %d, want 2", len(decoded.Tags))
}
}
func TestClient_Subscribe_ReceivesMessages(t *testing.T) {
messages := []Message{
{Event: "open"},
{Event: "message", ID: "msg1", Topic: "test", Title: "First", Message: "Hello"},
{Event: "keepalive"},
{Event: "message", ID: "msg2", Topic: "test", Title: "Second", Message: "World"},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
for _, msg := range messages {
data, _ := json.Marshal(msg)
fmt.Fprintf(w, "%s\n", data)
}
}))
defer server.Close()
client := NewClient(server.URL, []string{"test"})
msgCh := make(chan Message, 10)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go client.Subscribe(ctx, msgCh)
// Should receive only "message" events
received := make([]Message, 0)
timeout := time.After(1 * time.Second)
loop:
for {
select {
case msg := <-msgCh:
received = append(received, msg)
if len(received) >= 2 {
break loop
}
case <-timeout:
break loop
}
}
if len(received) != 2 {
t.Errorf("received %d messages, want 2", len(received))
}
if len(received) > 0 && received[0].ID != "msg1" {
t.Errorf("first message ID = %s, want msg1", received[0].ID)
}
if len(received) > 1 && received[1].ID != "msg2" {
t.Errorf("second message ID = %s, want msg2", received[1].ID)
}
}
func TestClient_Subscribe_FilterEvents(t *testing.T) {
messages := []Message{
{Event: "open"},
{Event: "keepalive"},
{Event: "message", ID: "actual", Topic: "test"},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
for _, msg := range messages {
data, _ := json.Marshal(msg)
fmt.Fprintf(w, "%s\n", data)
}
}))
defer server.Close()
client := NewClient(server.URL, []string{"test"})
msgCh := make(chan Message, 10)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go client.Subscribe(ctx, msgCh)
select {
case msg := <-msgCh:
if msg.Event != "message" {
t.Errorf("received event = %s, want message", msg.Event)
}
if msg.ID != "actual" {
t.Errorf("message ID = %s, want actual", msg.ID)
}
case <-time.After(500 * time.Millisecond):
t.Error("timeout waiting for message")
}
}
func TestClient_Subscribe_ContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Slow server - keeps connection open
time.Sleep(10 * time.Second)
}))
defer server.Close()
client := NewClient(server.URL, []string{"test"})
msgCh := make(chan Message, 10)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
client.Subscribe(ctx, msgCh)
close(done)
}()
// Cancel quickly
time.Sleep(50 * time.Millisecond)
cancel()
// Should exit promptly
select {
case <-done:
// Success
case <-time.After(2 * time.Second):
t.Error("Subscribe did not exit after context cancellation")
}
}
func TestClient_connect_ServerError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal error"))
}))
defer server.Close()
client := NewClient(server.URL, []string{"test"})
msgCh := make(chan Message, 10)
err := client.connect(context.Background(), msgCh)
if err == nil {
t.Error("expected error for server error response")
}
}
func TestClient_connect_URLConstruction(t *testing.T) {
var requestedURL string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestedURL = r.URL.Path
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewClient(server.URL, []string{"alerts", "updates"})
client.connect(context.Background(), make(chan Message))
expected := "/alerts,updates/json"
if requestedURL != expected {
t.Errorf("URL path = %s, want %s", requestedURL, expected)
}
}