- Add .golangci.yml with v2 config (errcheck, govet, staticcheck, misspell, etc.) - Fix 32 errcheck issues across config, discord, ntfy, server packages - Fix misspelling: cancelled → canceled - Fix staticcheck: use append(slice...) instead of loop - Fix staticcheck: remove empty error branch - Use t.Setenv instead of os.Setenv/Unsetenv in tests - Update CI workflow: add lint job, release tagging, ntfy notifications
214 lines
4.9 KiB
Go
214 lines
4.9 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestGetEnv(t *testing.T) {
|
|
// Set a test env var
|
|
t.Setenv("TEST_CONFIG_VAR", "test_value")
|
|
|
|
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) {
|
|
t.Setenv("NTFY_TOPICS", "alerts, updates , notifications")
|
|
t.Setenv("VAULT_ENABLED", "false")
|
|
|
|
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
|
|
t.Setenv("NTFY_URL", "")
|
|
t.Setenv("HTTP_PORT", "")
|
|
t.Setenv("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) {
|
|
t.Setenv("VAULT_ENABLED", "true")
|
|
|
|
// 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"
|
|
t.Setenv("DISCORD_WEBHOOK_URL", webhookURL)
|
|
t.Setenv("VAULT_ENABLED", "false")
|
|
|
|
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)
|
|
}
|