Files
ntfy-discord/internal/config/config_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

224 lines
5.2 KiB
Go

package config
import (
"context"
"os"
"path/filepath"
"sync"
"testing"
)
func TestGetEnv(t *testing.T) {
// Set a test env var
os.Setenv("TEST_CONFIG_VAR", "test_value")
defer os.Unsetenv("TEST_CONFIG_VAR")
tests := []struct {
name string
key string
defaultVal string
want string
}{
{"existing var", "TEST_CONFIG_VAR", "default", "test_value"},
{"non-existing var", "NON_EXISTING_VAR", "default", "default"},
{"empty default", "NON_EXISTING_VAR_2", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getEnv(tt.key, tt.defaultVal)
if got != tt.want {
t.Errorf("getEnv(%s, %s) = %s, want %s", tt.key, tt.defaultVal, got, tt.want)
}
})
}
}
func TestConfig_WebhookURL_ThreadSafe(t *testing.T) {
cfg := &Config{}
cfg.webhookURL = "https://discord.com/api/webhooks/test"
// Test concurrent reads and writes
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(2)
go func() {
defer wg.Done()
_ = cfg.WebhookURL()
}()
go func() {
defer wg.Done()
cfg.mu.Lock()
cfg.webhookURL = "updated"
cfg.mu.Unlock()
}()
}
wg.Wait()
// Should not race
}
func TestConfig_LoadWebhookFromSecret(t *testing.T) {
// Create temp directory with secret file
tmpDir := t.TempDir()
secretPath := filepath.Join(tmpDir, "webhook-url")
webhookURL := "https://discord.com/api/webhooks/123/abc"
if err := os.WriteFile(secretPath, []byte(webhookURL+"\n"), 0644); err != nil {
t.Fatalf("failed to write secret file: %v", err)
}
cfg := &Config{
SecretsPath: tmpDir,
}
if err := cfg.loadWebhookFromSecret(); err != nil {
t.Fatalf("loadWebhookFromSecret() error = %v", err)
}
if cfg.webhookURL != webhookURL {
t.Errorf("webhookURL = %s, want %s", cfg.webhookURL, webhookURL)
}
}
func TestConfig_LoadWebhookFromSecret_NotFound(t *testing.T) {
cfg := &Config{
SecretsPath: "/nonexistent/path",
}
err := cfg.loadWebhookFromSecret()
if err == nil {
t.Error("expected error for non-existent secret")
}
}
func TestConfig_LoadWebhookFromSecret_TrimsWhitespace(t *testing.T) {
tmpDir := t.TempDir()
secretPath := filepath.Join(tmpDir, "webhook-url")
// Write with extra whitespace
if err := os.WriteFile(secretPath, []byte(" https://example.com \n\n"), 0644); err != nil {
t.Fatalf("failed to write secret file: %v", err)
}
cfg := &Config{SecretsPath: tmpDir}
if err := cfg.loadWebhookFromSecret(); err != nil {
t.Fatalf("loadWebhookFromSecret() error = %v", err)
}
if cfg.webhookURL != "https://example.com" {
t.Errorf("webhookURL = %q, want %q", cfg.webhookURL, "https://example.com")
}
}
func TestLoad_ParsesTopics(t *testing.T) {
os.Setenv("NTFY_TOPICS", "alerts, updates , notifications")
os.Setenv("VAULT_ENABLED", "false")
defer func() {
os.Unsetenv("NTFY_TOPICS")
os.Unsetenv("VAULT_ENABLED")
}()
cfg, err := Load(context.Background())
if err != nil {
t.Fatalf("Load() error = %v", err)
}
expected := []string{"alerts", "updates", "notifications"}
if len(cfg.NtfyTopics) != len(expected) {
t.Errorf("NtfyTopics length = %d, want %d", len(cfg.NtfyTopics), len(expected))
}
for i, topic := range cfg.NtfyTopics {
if topic != expected[i] {
t.Errorf("NtfyTopics[%d] = %s, want %s", i, topic, expected[i])
}
}
}
func TestLoad_Defaults(t *testing.T) {
// Clear any existing env vars
os.Unsetenv("NTFY_URL")
os.Unsetenv("HTTP_PORT")
os.Unsetenv("VAULT_ENABLED")
cfg, err := Load(context.Background())
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.NtfyURL != "http://ntfy.observability.svc.cluster.local" {
t.Errorf("NtfyURL = %s, want default", cfg.NtfyURL)
}
if cfg.HTTPPort != "8080" {
t.Errorf("HTTPPort = %s, want 8080", cfg.HTTPPort)
}
if cfg.VaultEnabled {
t.Error("VaultEnabled should be false by default")
}
}
func TestLoad_VaultEnabled(t *testing.T) {
os.Setenv("VAULT_ENABLED", "true")
defer os.Unsetenv("VAULT_ENABLED")
// This will fail to init Vault (no server), but should gracefully fall back
cfg, err := Load(context.Background())
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if !cfg.VaultEnabled {
t.Error("VaultEnabled should be true")
}
// vaultClient should be nil (failed to connect)
if cfg.vaultClient != nil {
t.Error("vaultClient should be nil when Vault unavailable")
}
}
func TestLoad_FallsBackToEnvVar(t *testing.T) {
webhookURL := "https://discord.com/api/webhooks/env/test"
os.Setenv("DISCORD_WEBHOOK_URL", webhookURL)
os.Setenv("VAULT_ENABLED", "false")
defer func() {
os.Unsetenv("DISCORD_WEBHOOK_URL")
os.Unsetenv("VAULT_ENABLED")
}()
cfg, err := Load(context.Background())
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.WebhookURL() != webhookURL {
t.Errorf("WebhookURL() = %s, want %s", cfg.WebhookURL(), webhookURL)
}
}
func TestConfig_Close_NoVault(t *testing.T) {
cfg := &Config{}
// Should not panic with nil vaultClient
err := cfg.Close()
if err != nil {
t.Errorf("Close() error = %v", err)
}
}
func TestConfig_WatchSecrets_NoPath(t *testing.T) {
cfg := &Config{
SecretsPath: "",
vaultClient: nil,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Should return immediately, not block or panic
cfg.WatchSecrets(ctx)
}