Pulse/internal/config/persistence_extended_test.go
2026-03-18 16:06:30 +00:00

214 lines
6.2 KiB
Go

package config_test
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
)
func TestConfigPersistence_DataDir(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if cp.DataDir() != tempDir {
t.Errorf("Expected %s, got %s", tempDir, cp.DataDir())
}
}
func TestConfigPersistence_MigrateWebhooksIfNeeded(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
// 1. Create legacy file
legacyFile := filepath.Join(tempDir, "webhooks.json")
legacyWebhooks := []notifications.WebhookConfig{
{ID: "webhook-1", URL: "http://example.com/legacy"},
}
data, _ := json.Marshal(legacyWebhooks)
_ = os.WriteFile(legacyFile, data, 0644)
// 2. Migrate
if err := cp.MigrateWebhooksIfNeeded(); err != nil {
t.Fatalf("MigrateWebhooksIfNeeded failed: %v", err)
}
// 3. Verify encrypted file exists (since SaveWebhooks uses encryption if key exists)
// NewConfigPersistence generates a key if it doesn't exist, so encryption IS enabled by default.
loaded, err := cp.LoadWebhooks()
if err != nil {
t.Fatalf("LoadWebhooks failed: %v", err)
}
if len(loaded) != 1 || loaded[0].URL != "http://example.com/legacy" {
t.Errorf("Migration failed to preserve data: %+v", loaded)
}
}
func TestConfigPersistence_PatrolRunHistory(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
runs := []config.PatrolRunRecord{
{
ID: "run-1",
StartedAt: time.Now().Add(-1 * time.Hour),
CompletedAt: time.Now().Add(-59 * time.Minute),
DurationMs: 60000,
Type: "quick",
AlertIdentifier: "instance:node:100::metric/cpu",
ResourcesChecked: 10,
NewFindings: 2,
},
}
if err := cp.SavePatrolRunHistory(runs); err != nil {
t.Fatalf("SavePatrolRunHistory failed: %v", err)
}
history, err := cp.LoadPatrolRunHistory()
if err != nil {
t.Fatalf("LoadPatrolRunHistory failed: %v", err)
}
if len(history.Runs) != 1 || history.Runs[0].ID != "run-1" {
t.Errorf("Patrol history mismatch: %+v", history)
}
if history.Runs[0].AlertIdentifier != "instance:node:100::metric/cpu" {
t.Errorf("Expected canonical alert identifier after load, got %q", history.Runs[0].AlertIdentifier)
}
// Test non-existent file
cp2 := config.NewConfigPersistence(t.TempDir())
history2, err := cp2.LoadPatrolRunHistory()
if err != nil {
t.Fatalf("LoadPatrolRunHistory on empty dir failed: %v", err)
}
if len(history2.Runs) != 0 {
t.Errorf("Expected 0 runs, got %d", len(history2.Runs))
}
}
func TestConfigPersistence_AIChatSessionsMigratesPlaintextFile(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
chatFile := filepath.Join(tempDir, "ai_chat_sessions.json")
plaintext := config.AIChatSessionsData{
Version: 1,
LastSaved: time.Now().UTC(),
Sessions: map[string]*config.AIChatSession{
"session-1": {
ID: "session-1",
Title: "Legacy chat",
UpdatedAt: time.Now().UTC(),
},
},
}
raw, err := json.MarshalIndent(plaintext, "", " ")
if err != nil {
t.Fatalf("marshal plaintext chat sessions: %v", err)
}
if err := os.WriteFile(chatFile, raw, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
loaded, err := cp.LoadAIChatSessions()
if err != nil {
t.Fatalf("LoadAIChatSessions failed: %v", err)
}
if len(loaded.Sessions) != 1 {
t.Fatalf("expected 1 session, got %d", len(loaded.Sessions))
}
rewritten, err := os.ReadFile(chatFile)
if err != nil {
t.Fatalf("ReadFile rewritten chat sessions: %v", err)
}
if bytes.Equal(rewritten, raw) {
t.Fatalf("expected plaintext ai chat sessions file to be rewritten encrypted")
}
}
func TestPatrolRunRecordJSONCanonicalOutput(t *testing.T) {
record := config.PatrolRunRecord{
ID: "run-1",
AlertIdentifier: "instance:node:100::metric/cpu",
}
raw, err := json.Marshal(record)
if err != nil {
t.Fatalf("marshal patrol run record: %v", err)
}
var payload map[string]interface{}
if err := json.Unmarshal(raw, &payload); err != nil {
t.Fatalf("decode patrol run payload: %v", err)
}
if payload["alert_identifier"] != "instance:node:100::metric/cpu" {
t.Fatalf("expected canonical alert_identifier, got %#v", payload["alert_identifier"])
}
if _, ok := payload["legacy_alert_id"]; ok {
t.Fatalf("did not expect legacy_alert_id in canonical payload, got %#v", payload["legacy_alert_id"])
}
if _, ok := payload["alert_id"]; ok {
t.Fatalf("did not expect alert_id in canonical payload, got %#v", payload["alert_id"])
}
var decoded config.PatrolRunRecord
if err := json.Unmarshal([]byte(`{
"id":"run-1",
"alert_identifier":"instance:node:100::metric/cpu"
}`), &decoded); err != nil {
t.Fatalf("unmarshal canonical patrol run: %v", err)
}
if decoded.AlertIdentifier != "instance:node:100::metric/cpu" {
t.Fatalf("expected canonical alert_identifier to load, got %q", decoded.AlertIdentifier)
}
}
func TestConfigPersistence_UpdateEnvFile(t *testing.T) {
tempDir := t.TempDir()
envFile := filepath.Join(tempDir, ".env")
initialContent := `UPDATE_CHANNEL=stable
AUTO_UPDATE_ENABLED=false
POLLING_INTERVAL=10
CUSTOM_VAR=value`
_ = os.WriteFile(envFile, []byte(initialContent), 0644)
cp := config.NewConfigPersistence(tempDir)
settings := config.SystemSettings{
UpdateChannel: "beta",
AutoUpdateEnabled: true,
AutoUpdateCheckInterval: 3600,
}
if err := cp.SaveSystemSettings(settings); err != nil {
t.Fatalf("SaveSystemSettings failed: %v", err)
}
updatedData, err := os.ReadFile(envFile)
if err != nil {
t.Fatalf("Failed to read .env file: %v", err)
}
updatedContent := string(updatedData)
if !strings.Contains(updatedContent, "UPDATE_CHANNEL=beta") {
t.Errorf("UPDATE_CHANNEL not updated. Content: %s", updatedContent)
}
if !strings.Contains(updatedContent, "AUTO_UPDATE_ENABLED=true") {
t.Errorf("AUTO_UPDATE_ENABLED not updated. Content: %s", updatedContent)
}
if strings.Contains(updatedContent, "POLLING_INTERVAL=") {
t.Error("POLLING_INTERVAL should have been removed")
}
if !strings.Contains(updatedContent, "CUSTOM_VAR=value") {
t.Error("CUSTOM_VAR should have been preserved")
}
}