- 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
261 lines
5.7 KiB
Go
261 lines
5.7 KiB
Go
package discord
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.daviestechlabs.io/daviestechlabs/ntfy-discord/internal/ntfy"
|
|
)
|
|
|
|
func TestClient_Send(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
msg ntfy.Message
|
|
wantStatus int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "successful send",
|
|
msg: ntfy.Message{
|
|
ID: "test-id",
|
|
Topic: "alerts",
|
|
Title: "Test Alert",
|
|
Message: "This is a test message",
|
|
Priority: 3,
|
|
Time: time.Now().Unix(),
|
|
},
|
|
wantStatus: http.StatusNoContent,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "high priority message",
|
|
msg: ntfy.Message{
|
|
ID: "high-priority",
|
|
Topic: "urgent",
|
|
Title: "Urgent Alert",
|
|
Message: "Critical issue detected",
|
|
Priority: 5,
|
|
Tags: []string{"warning", "fire"},
|
|
},
|
|
wantStatus: http.StatusNoContent,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "server error",
|
|
msg: ntfy.Message{
|
|
ID: "error-test",
|
|
Topic: "test",
|
|
},
|
|
wantStatus: http.StatusInternalServerError,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var receivedPayload WebhookPayload
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("expected application/json, got %s", r.Header.Get("Content-Type"))
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&receivedPayload); err != nil {
|
|
t.Errorf("failed to decode payload: %v", err)
|
|
}
|
|
|
|
w.WriteHeader(tt.wantStatus)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient()
|
|
err := client.Send(context.Background(), server.URL, tt.msg)
|
|
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Send() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
|
|
if !tt.wantErr && len(receivedPayload.Embeds) != 1 {
|
|
t.Errorf("expected 1 embed, got %d", len(receivedPayload.Embeds))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_Send_NoWebhookURL(t *testing.T) {
|
|
client := NewClient()
|
|
err := client.Send(context.Background(), "", ntfy.Message{})
|
|
|
|
if err == nil {
|
|
t.Error("expected error for empty webhook URL")
|
|
}
|
|
}
|
|
|
|
func TestClient_buildEmbed(t *testing.T) {
|
|
client := NewClient()
|
|
|
|
tests := []struct {
|
|
name string
|
|
msg ntfy.Message
|
|
wantColor int
|
|
wantEmoji string
|
|
}{
|
|
{
|
|
name: "default priority",
|
|
msg: ntfy.Message{
|
|
Title: "Test",
|
|
Message: "Hello",
|
|
Priority: 3,
|
|
},
|
|
wantColor: 3066993, // Blue
|
|
wantEmoji: "",
|
|
},
|
|
{
|
|
name: "max priority with warning tag",
|
|
msg: ntfy.Message{
|
|
Title: "Alert",
|
|
Message: "Critical",
|
|
Priority: 5,
|
|
Tags: []string{"warning"},
|
|
},
|
|
wantColor: 15158332, // Red
|
|
wantEmoji: "⚠️",
|
|
},
|
|
{
|
|
name: "low priority",
|
|
msg: ntfy.Message{
|
|
Title: "Info",
|
|
Message: "Low priority",
|
|
Priority: 2,
|
|
},
|
|
wantColor: 9807270, // Gray
|
|
wantEmoji: "",
|
|
},
|
|
{
|
|
name: "with check tag",
|
|
msg: ntfy.Message{
|
|
Title: "Success",
|
|
Message: "Completed",
|
|
Priority: 3,
|
|
Tags: []string{"check", "success"},
|
|
},
|
|
wantColor: 3066993,
|
|
wantEmoji: "✅",
|
|
},
|
|
{
|
|
name: "no title uses topic",
|
|
msg: ntfy.Message{
|
|
Topic: "alerts",
|
|
Message: "No title",
|
|
},
|
|
wantColor: 3066993,
|
|
wantEmoji: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
embed := client.buildEmbed(tt.msg)
|
|
|
|
if embed.Color != tt.wantColor {
|
|
t.Errorf("Color = %d, want %d", embed.Color, tt.wantColor)
|
|
}
|
|
|
|
if tt.wantEmoji != "" {
|
|
if len(embed.Title) < 2 || embed.Title[:len(tt.wantEmoji)] != tt.wantEmoji {
|
|
t.Errorf("Title should start with emoji %s, got %s", tt.wantEmoji, embed.Title)
|
|
}
|
|
}
|
|
|
|
if embed.Description != tt.msg.Message {
|
|
t.Errorf("Description = %s, want %s", embed.Description, tt.msg.Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_extractEmoji(t *testing.T) {
|
|
client := NewClient()
|
|
|
|
tests := []struct {
|
|
name string
|
|
tags []string
|
|
want string
|
|
}{
|
|
{"warning tag", []string{"warning"}, "⚠️"},
|
|
{"check tag", []string{"check"}, "✅"},
|
|
{"fire tag", []string{"fire"}, "🔥"},
|
|
{"rocket tag", []string{"rocket"}, "🚀"},
|
|
{"unknown tag", []string{"unknown"}, ""},
|
|
{"empty tags", []string{}, ""},
|
|
{"multiple tags first match", []string{"unknown", "fire", "warning"}, "🔥"},
|
|
{"case insensitive", []string{"WARNING"}, "⚠️"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := client.extractEmoji(tt.tags)
|
|
if got != tt.want {
|
|
t.Errorf("extractEmoji(%v) = %s, want %s", tt.tags, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityColors(t *testing.T) {
|
|
expected := map[int]int{
|
|
1: 12370112, // Light Gray
|
|
2: 9807270, // Gray
|
|
3: 3066993, // Blue
|
|
4: 15105570, // Orange
|
|
5: 15158332, // Red
|
|
}
|
|
|
|
for priority, color := range expected {
|
|
if priorityColors[priority] != color {
|
|
t.Errorf("priorityColors[%d] = %d, want %d", priority, priorityColors[priority], color)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWebhookPayload_JSON(t *testing.T) {
|
|
payload := WebhookPayload{
|
|
Embeds: []Embed{
|
|
{
|
|
Title: "Test",
|
|
Description: "Hello World",
|
|
Color: 3066993,
|
|
Fields: []Field{
|
|
{Name: "Topic", Value: "alerts", Inline: true},
|
|
},
|
|
Footer: &Footer{Text: "ntfy"},
|
|
},
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal: %v", err)
|
|
}
|
|
|
|
var decoded WebhookPayload
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("failed to unmarshal: %v", err)
|
|
}
|
|
|
|
if len(decoded.Embeds) != 1 {
|
|
t.Errorf("expected 1 embed, got %d", len(decoded.Embeds))
|
|
}
|
|
|
|
if decoded.Embeds[0].Title != "Test" {
|
|
t.Errorf("Title = %s, want Test", decoded.Embeds[0].Title)
|
|
}
|
|
}
|