- 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
228 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|