mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
47 test cases covering: - NewUpdateHistory: directory creation, loading existing entries - CreateEntry: event ID generation, timestamp handling, persistence - UpdateEntry: updates, persistence verification, error handling - GetEntry: retrieval by ID, non-existent entry errors - ListEntries: filtering by status/action/deployment type, limits - GetLatestSuccessful: finding recent successful entries - Cache management: trimming when exceeding max size - LoadCache: malformed JSON handling, empty lines - Type definitions and constant validation
791 lines
21 KiB
Go
791 lines
21 KiB
Go
package updates
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewUpdateHistory(t *testing.T) {
|
|
t.Run("creates directory if it doesn't exist", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dataDir := filepath.Join(tmpDir, "subdir", "nested")
|
|
|
|
h, err := NewUpdateHistory(dataDir)
|
|
if err != nil {
|
|
t.Fatalf("NewUpdateHistory() error = %v", err)
|
|
}
|
|
if h == nil {
|
|
t.Fatal("NewUpdateHistory() returned nil")
|
|
}
|
|
|
|
// Directory should exist
|
|
if _, err := os.Stat(dataDir); os.IsNotExist(err) {
|
|
t.Error("Expected directory to be created")
|
|
}
|
|
})
|
|
|
|
t.Run("uses default directory when empty", func(t *testing.T) {
|
|
// Skip if we can't create /var/lib/pulse
|
|
if _, err := os.Stat("/var/lib"); os.IsNotExist(err) {
|
|
t.Skip("Skipping test that requires /var/lib")
|
|
}
|
|
|
|
// Just verify it doesn't panic; we can't test the actual default path
|
|
// without potentially modifying system directories
|
|
})
|
|
|
|
t.Run("initializes with empty cache", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, err := NewUpdateHistory(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("NewUpdateHistory() error = %v", err)
|
|
}
|
|
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) != 0 {
|
|
t.Errorf("Expected empty cache, got %d entries", len(entries))
|
|
}
|
|
})
|
|
|
|
t.Run("loads existing entries from file", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
logPath := filepath.Join(tmpDir, "update-history.jsonl")
|
|
|
|
// Write some entries
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "test-event-1",
|
|
Timestamp: time.Now(),
|
|
Action: ActionUpdate,
|
|
VersionFrom: "1.0.0",
|
|
VersionTo: "1.1.0",
|
|
Status: StatusSuccess,
|
|
}
|
|
data, _ := json.Marshal(entry)
|
|
if err := os.WriteFile(logPath, append(data, '\n'), 0644); err != nil {
|
|
t.Fatalf("Failed to write test file: %v", err)
|
|
}
|
|
|
|
h, err := NewUpdateHistory(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("NewUpdateHistory() error = %v", err)
|
|
}
|
|
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) != 1 {
|
|
t.Errorf("Expected 1 entry, got %d", len(entries))
|
|
}
|
|
if entries[0].EventID != "test-event-1" {
|
|
t.Errorf("Expected event ID 'test-event-1', got %q", entries[0].EventID)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_CreateEntry(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("creates entry with generated event ID", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
entry := UpdateHistoryEntry{
|
|
Action: ActionUpdate,
|
|
VersionFrom: "1.0.0",
|
|
VersionTo: "1.1.0",
|
|
Status: StatusInProgress,
|
|
}
|
|
|
|
eventID, err := h.CreateEntry(ctx, entry)
|
|
if err != nil {
|
|
t.Fatalf("CreateEntry() error = %v", err)
|
|
}
|
|
if eventID == "" {
|
|
t.Error("Expected non-empty event ID")
|
|
}
|
|
|
|
// Verify entry is in cache
|
|
retrieved, err := h.GetEntry(eventID)
|
|
if err != nil {
|
|
t.Fatalf("GetEntry() error = %v", err)
|
|
}
|
|
if retrieved.VersionFrom != "1.0.0" {
|
|
t.Errorf("VersionFrom = %q, want '1.0.0'", retrieved.VersionFrom)
|
|
}
|
|
})
|
|
|
|
t.Run("uses provided event ID", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "my-custom-id",
|
|
Action: ActionUpdate,
|
|
Status: StatusInProgress,
|
|
}
|
|
|
|
eventID, err := h.CreateEntry(ctx, entry)
|
|
if err != nil {
|
|
t.Fatalf("CreateEntry() error = %v", err)
|
|
}
|
|
if eventID != "my-custom-id" {
|
|
t.Errorf("EventID = %q, want 'my-custom-id'", eventID)
|
|
}
|
|
})
|
|
|
|
t.Run("sets timestamp if not provided", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
before := time.Now()
|
|
entry := UpdateHistoryEntry{
|
|
Action: ActionUpdate,
|
|
Status: StatusInProgress,
|
|
}
|
|
|
|
eventID, err := h.CreateEntry(ctx, entry)
|
|
if err != nil {
|
|
t.Fatalf("CreateEntry() error = %v", err)
|
|
}
|
|
after := time.Now()
|
|
|
|
retrieved, _ := h.GetEntry(eventID)
|
|
if retrieved.Timestamp.Before(before) || retrieved.Timestamp.After(after) {
|
|
t.Errorf("Timestamp not set correctly: %v", retrieved.Timestamp)
|
|
}
|
|
})
|
|
|
|
t.Run("preserves provided timestamp", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
customTime := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
entry := UpdateHistoryEntry{
|
|
Timestamp: customTime,
|
|
Action: ActionUpdate,
|
|
Status: StatusInProgress,
|
|
}
|
|
|
|
eventID, _ := h.CreateEntry(ctx, entry)
|
|
retrieved, _ := h.GetEntry(eventID)
|
|
|
|
if !retrieved.Timestamp.Equal(customTime) {
|
|
t.Errorf("Timestamp = %v, want %v", retrieved.Timestamp, customTime)
|
|
}
|
|
})
|
|
|
|
t.Run("persists to file", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "persist-test",
|
|
Action: ActionUpdate,
|
|
Status: StatusSuccess,
|
|
}
|
|
|
|
_, err := h.CreateEntry(ctx, entry)
|
|
if err != nil {
|
|
t.Fatalf("CreateEntry() error = %v", err)
|
|
}
|
|
|
|
// Read file directly
|
|
logPath := filepath.Join(tmpDir, "update-history.jsonl")
|
|
data, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read history file: %v", err)
|
|
}
|
|
|
|
if len(data) == 0 {
|
|
t.Error("History file is empty")
|
|
}
|
|
|
|
// Parse and verify
|
|
var parsed UpdateHistoryEntry
|
|
if err := json.Unmarshal(data[:len(data)-1], &parsed); err != nil {
|
|
t.Fatalf("Failed to parse persisted entry: %v", err)
|
|
}
|
|
if parsed.EventID != "persist-test" {
|
|
t.Errorf("Persisted EventID = %q, want 'persist-test'", parsed.EventID)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_UpdateEntry(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("updates existing entry", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
// Create initial entry
|
|
entry := UpdateHistoryEntry{
|
|
Action: ActionUpdate,
|
|
Status: StatusInProgress,
|
|
}
|
|
eventID, _ := h.CreateEntry(ctx, entry)
|
|
|
|
// Update it
|
|
err := h.UpdateEntry(ctx, eventID, func(e *UpdateHistoryEntry) error {
|
|
e.Status = StatusSuccess
|
|
e.DurationMs = 5000
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateEntry() error = %v", err)
|
|
}
|
|
|
|
// Verify update
|
|
retrieved, _ := h.GetEntry(eventID)
|
|
if retrieved.Status != StatusSuccess {
|
|
t.Errorf("Status = %v, want %v", retrieved.Status, StatusSuccess)
|
|
}
|
|
if retrieved.DurationMs != 5000 {
|
|
t.Errorf("DurationMs = %d, want 5000", retrieved.DurationMs)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for non-existent entry", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
err := h.UpdateEntry(ctx, "non-existent", func(e *UpdateHistoryEntry) error {
|
|
return nil
|
|
})
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent entry")
|
|
}
|
|
})
|
|
|
|
t.Run("persists update to file", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
// Create and update entry
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "update-persist-test",
|
|
Action: ActionUpdate,
|
|
Status: StatusInProgress,
|
|
}
|
|
eventID, _ := h.CreateEntry(ctx, entry)
|
|
|
|
h.UpdateEntry(ctx, eventID, func(e *UpdateHistoryEntry) error {
|
|
e.Status = StatusFailed
|
|
e.Error = &UpdateError{Message: "test error"}
|
|
return nil
|
|
})
|
|
|
|
// Create new history instance to read from file
|
|
h2, _ := NewUpdateHistory(tmpDir)
|
|
retrieved, err := h2.GetEntry(eventID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get entry from new instance: %v", err)
|
|
}
|
|
if retrieved.Status != StatusFailed {
|
|
t.Errorf("Persisted Status = %v, want %v", retrieved.Status, StatusFailed)
|
|
}
|
|
if retrieved.Error == nil || retrieved.Error.Message != "test error" {
|
|
t.Errorf("Persisted Error not correct")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_GetEntry(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("returns entry by ID", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "get-test",
|
|
Action: ActionUpdate,
|
|
VersionFrom: "1.0.0",
|
|
VersionTo: "2.0.0",
|
|
Status: StatusSuccess,
|
|
}
|
|
h.CreateEntry(ctx, entry)
|
|
|
|
retrieved, err := h.GetEntry("get-test")
|
|
if err != nil {
|
|
t.Fatalf("GetEntry() error = %v", err)
|
|
}
|
|
if retrieved.VersionFrom != "1.0.0" {
|
|
t.Errorf("VersionFrom = %q, want '1.0.0'", retrieved.VersionFrom)
|
|
}
|
|
if retrieved.VersionTo != "2.0.0" {
|
|
t.Errorf("VersionTo = %q, want '2.0.0'", retrieved.VersionTo)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for non-existent entry", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
_, err := h.GetEntry("non-existent")
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent entry")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_ListEntries(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("returns all entries in reverse order", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
// Create entries
|
|
for i := 1; i <= 3; i++ {
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "event-" + string(rune('0'+i)),
|
|
Action: ActionUpdate,
|
|
Status: StatusSuccess,
|
|
}
|
|
h.CreateEntry(ctx, entry)
|
|
}
|
|
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) != 3 {
|
|
t.Fatalf("Expected 3 entries, got %d", len(entries))
|
|
}
|
|
|
|
// Should be in reverse order (newest first)
|
|
if entries[0].EventID != "event-3" {
|
|
t.Errorf("First entry should be event-3, got %s", entries[0].EventID)
|
|
}
|
|
if entries[2].EventID != "event-1" {
|
|
t.Errorf("Last entry should be event-1, got %s", entries[2].EventID)
|
|
}
|
|
})
|
|
|
|
t.Run("filters by status", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
// Create entries with different statuses
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e1", Status: StatusSuccess})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e2", Status: StatusFailed})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e3", Status: StatusSuccess})
|
|
|
|
entries := h.ListEntries(HistoryFilter{Status: StatusSuccess})
|
|
if len(entries) != 2 {
|
|
t.Errorf("Expected 2 success entries, got %d", len(entries))
|
|
}
|
|
for _, e := range entries {
|
|
if e.Status != StatusSuccess {
|
|
t.Errorf("Entry %s has status %v, want %v", e.EventID, e.Status, StatusSuccess)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("filters by action", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e1", Action: ActionUpdate})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e2", Action: ActionRollback})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e3", Action: ActionUpdate})
|
|
|
|
entries := h.ListEntries(HistoryFilter{Action: ActionRollback})
|
|
if len(entries) != 1 {
|
|
t.Errorf("Expected 1 rollback entry, got %d", len(entries))
|
|
}
|
|
if entries[0].EventID != "e2" {
|
|
t.Errorf("Expected e2, got %s", entries[0].EventID)
|
|
}
|
|
})
|
|
|
|
t.Run("filters by deployment type", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e1", DeploymentType: "docker"})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e2", DeploymentType: "bare-metal"})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e3", DeploymentType: "docker"})
|
|
|
|
entries := h.ListEntries(HistoryFilter{DeploymentType: "docker"})
|
|
if len(entries) != 2 {
|
|
t.Errorf("Expected 2 docker entries, got %d", len(entries))
|
|
}
|
|
})
|
|
|
|
t.Run("respects limit", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "event"})
|
|
}
|
|
|
|
entries := h.ListEntries(HistoryFilter{Limit: 5})
|
|
if len(entries) != 5 {
|
|
t.Errorf("Expected 5 entries with limit, got %d", len(entries))
|
|
}
|
|
})
|
|
|
|
t.Run("combines filters", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e1", Action: ActionUpdate, Status: StatusSuccess})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e2", Action: ActionUpdate, Status: StatusFailed})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e3", Action: ActionRollback, Status: StatusSuccess})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e4", Action: ActionUpdate, Status: StatusSuccess})
|
|
|
|
entries := h.ListEntries(HistoryFilter{Action: ActionUpdate, Status: StatusSuccess})
|
|
if len(entries) != 2 {
|
|
t.Errorf("Expected 2 entries, got %d", len(entries))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_GetLatestSuccessful(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("returns latest successful entry", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e1", Status: StatusSuccess})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e2", Status: StatusFailed})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e3", Status: StatusSuccess})
|
|
|
|
entry, err := h.GetLatestSuccessful()
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSuccessful() error = %v", err)
|
|
}
|
|
if entry.EventID != "e3" {
|
|
t.Errorf("Expected e3, got %s", entry.EventID)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error when no successful entries", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e1", Status: StatusFailed})
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "e2", Status: StatusInProgress})
|
|
|
|
_, err := h.GetLatestSuccessful()
|
|
if err == nil {
|
|
t.Error("Expected error when no successful entries")
|
|
}
|
|
})
|
|
|
|
t.Run("returns error when cache is empty", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
_, err := h.GetLatestSuccessful()
|
|
if err == nil {
|
|
t.Error("Expected error when cache is empty")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_CacheManagement(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("trims cache when exceeding max size", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
|
|
// h.maxCache is 100, add more than that
|
|
for i := 0; i < 110; i++ {
|
|
h.CreateEntry(ctx, UpdateHistoryEntry{EventID: "event"})
|
|
}
|
|
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) > 100 {
|
|
t.Errorf("Cache should be trimmed to 100, got %d", len(entries))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistory_LoadCache(t *testing.T) {
|
|
t.Run("handles malformed JSON lines", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
logPath := filepath.Join(tmpDir, "update-history.jsonl")
|
|
|
|
// Write valid entry followed by invalid line
|
|
validEntry := UpdateHistoryEntry{EventID: "valid", Status: StatusSuccess}
|
|
validData, _ := json.Marshal(validEntry)
|
|
content := string(validData) + "\n{invalid json}\n"
|
|
os.WriteFile(logPath, []byte(content), 0644)
|
|
|
|
// Should load without error, skipping invalid line
|
|
h, err := NewUpdateHistory(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("NewUpdateHistory() error = %v", err)
|
|
}
|
|
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) != 1 {
|
|
t.Errorf("Expected 1 valid entry, got %d", len(entries))
|
|
}
|
|
})
|
|
|
|
t.Run("handles empty lines", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
logPath := filepath.Join(tmpDir, "update-history.jsonl")
|
|
|
|
entry := UpdateHistoryEntry{EventID: "test", Status: StatusSuccess}
|
|
data, _ := json.Marshal(entry)
|
|
content := "\n" + string(data) + "\n\n"
|
|
os.WriteFile(logPath, []byte(content), 0644)
|
|
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) != 1 {
|
|
t.Errorf("Expected 1 entry, got %d", len(entries))
|
|
}
|
|
})
|
|
|
|
t.Run("trims entries exceeding max cache on load", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
logPath := filepath.Join(tmpDir, "update-history.jsonl")
|
|
|
|
// Write 150 entries (more than maxCache of 100)
|
|
var content string
|
|
for i := 0; i < 150; i++ {
|
|
entry := UpdateHistoryEntry{EventID: "event", Status: StatusSuccess}
|
|
data, _ := json.Marshal(entry)
|
|
content += string(data) + "\n"
|
|
}
|
|
os.WriteFile(logPath, []byte(content), 0644)
|
|
|
|
h, _ := NewUpdateHistory(tmpDir)
|
|
entries := h.ListEntries(HistoryFilter{})
|
|
if len(entries) != 100 {
|
|
t.Errorf("Expected 100 entries after load, got %d", len(entries))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateHistoryEntry_Fields(t *testing.T) {
|
|
t.Run("all fields serialize correctly", func(t *testing.T) {
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "test-123",
|
|
Timestamp: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC),
|
|
Action: ActionUpdate,
|
|
Channel: "stable",
|
|
VersionFrom: "1.0.0",
|
|
VersionTo: "2.0.0",
|
|
DeploymentType: "docker",
|
|
InitiatedBy: InitiatedByUser,
|
|
InitiatedVia: InitiatedViaUI,
|
|
Status: StatusSuccess,
|
|
DurationMs: 5000,
|
|
BackupPath: "/backups/backup.tar.gz",
|
|
LogPath: "/logs/update.log",
|
|
Error: nil,
|
|
DownloadBytes: 1048576,
|
|
RelatedEventID: "related-456",
|
|
Notes: "Test update",
|
|
}
|
|
|
|
data, err := json.Marshal(entry)
|
|
if err != nil {
|
|
t.Fatalf("Marshal error: %v", err)
|
|
}
|
|
|
|
var parsed UpdateHistoryEntry
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("Unmarshal error: %v", err)
|
|
}
|
|
|
|
if parsed.EventID != entry.EventID {
|
|
t.Errorf("EventID mismatch")
|
|
}
|
|
if parsed.Action != entry.Action {
|
|
t.Errorf("Action mismatch")
|
|
}
|
|
if parsed.Status != entry.Status {
|
|
t.Errorf("Status mismatch")
|
|
}
|
|
if parsed.DurationMs != entry.DurationMs {
|
|
t.Errorf("DurationMs mismatch")
|
|
}
|
|
if parsed.DownloadBytes != entry.DownloadBytes {
|
|
t.Errorf("DownloadBytes mismatch")
|
|
}
|
|
})
|
|
|
|
t.Run("error field serializes correctly", func(t *testing.T) {
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "err-test",
|
|
Status: StatusFailed,
|
|
Error: &UpdateError{
|
|
Message: "download failed",
|
|
Code: "DOWNLOAD_ERROR",
|
|
Details: "connection timeout",
|
|
},
|
|
}
|
|
|
|
data, _ := json.Marshal(entry)
|
|
var parsed UpdateHistoryEntry
|
|
json.Unmarshal(data, &parsed)
|
|
|
|
if parsed.Error == nil {
|
|
t.Fatal("Error should not be nil")
|
|
}
|
|
if parsed.Error.Message != "download failed" {
|
|
t.Errorf("Error.Message = %q, want 'download failed'", parsed.Error.Message)
|
|
}
|
|
if parsed.Error.Code != "DOWNLOAD_ERROR" {
|
|
t.Errorf("Error.Code = %q, want 'DOWNLOAD_ERROR'", parsed.Error.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("optional fields omit when empty", func(t *testing.T) {
|
|
entry := UpdateHistoryEntry{
|
|
EventID: "minimal",
|
|
Status: StatusSuccess,
|
|
}
|
|
|
|
data, _ := json.Marshal(entry)
|
|
str := string(data)
|
|
|
|
// These optional fields should not appear
|
|
if contains(str, "backup_path") {
|
|
t.Error("backup_path should be omitted when empty")
|
|
}
|
|
if contains(str, "log_path") {
|
|
t.Error("log_path should be omitted when empty")
|
|
}
|
|
if contains(str, "error") {
|
|
t.Error("error should be omitted when nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConstants(t *testing.T) {
|
|
t.Run("UpdateAction constants", func(t *testing.T) {
|
|
if ActionUpdate != "update" {
|
|
t.Errorf("ActionUpdate = %q, want 'update'", ActionUpdate)
|
|
}
|
|
if ActionRollback != "rollback" {
|
|
t.Errorf("ActionRollback = %q, want 'rollback'", ActionRollback)
|
|
}
|
|
})
|
|
|
|
t.Run("UpdateStatusType constants", func(t *testing.T) {
|
|
if StatusInProgress != "in_progress" {
|
|
t.Errorf("StatusInProgress = %q, want 'in_progress'", StatusInProgress)
|
|
}
|
|
if StatusSuccess != "success" {
|
|
t.Errorf("StatusSuccess = %q, want 'success'", StatusSuccess)
|
|
}
|
|
if StatusFailed != "failed" {
|
|
t.Errorf("StatusFailed = %q, want 'failed'", StatusFailed)
|
|
}
|
|
if StatusRolledBack != "rolled_back" {
|
|
t.Errorf("StatusRolledBack = %q, want 'rolled_back'", StatusRolledBack)
|
|
}
|
|
if StatusCancelled != "cancelled" {
|
|
t.Errorf("StatusCancelled = %q, want 'cancelled'", StatusCancelled)
|
|
}
|
|
})
|
|
|
|
t.Run("InitiatedBy constants", func(t *testing.T) {
|
|
if InitiatedByUser != "user" {
|
|
t.Errorf("InitiatedByUser = %q, want 'user'", InitiatedByUser)
|
|
}
|
|
if InitiatedByAuto != "auto" {
|
|
t.Errorf("InitiatedByAuto = %q, want 'auto'", InitiatedByAuto)
|
|
}
|
|
if InitiatedByAPI != "api" {
|
|
t.Errorf("InitiatedByAPI = %q, want 'api'", InitiatedByAPI)
|
|
}
|
|
})
|
|
|
|
t.Run("InitiatedVia constants", func(t *testing.T) {
|
|
if InitiatedViaUI != "ui" {
|
|
t.Errorf("InitiatedViaUI = %q, want 'ui'", InitiatedViaUI)
|
|
}
|
|
if InitiatedViaAPI != "api" {
|
|
t.Errorf("InitiatedViaAPI = %q, want 'api'", InitiatedViaAPI)
|
|
}
|
|
if InitiatedViaCLI != "cli" {
|
|
t.Errorf("InitiatedViaCLI = %q, want 'cli'", InitiatedViaCLI)
|
|
}
|
|
if InitiatedViaScript != "script" {
|
|
t.Errorf("InitiatedViaScript = %q, want 'script'", InitiatedViaScript)
|
|
}
|
|
if InitiatedViaWebhook != "webhook" {
|
|
t.Errorf("InitiatedViaWebhook = %q, want 'webhook'", InitiatedViaWebhook)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHistoryFilter(t *testing.T) {
|
|
t.Run("zero value filter matches all", func(t *testing.T) {
|
|
filter := HistoryFilter{}
|
|
if filter.Status != "" {
|
|
t.Error("Status should be empty by default")
|
|
}
|
|
if filter.Action != "" {
|
|
t.Error("Action should be empty by default")
|
|
}
|
|
if filter.DeploymentType != "" {
|
|
t.Error("DeploymentType should be empty by default")
|
|
}
|
|
if filter.Limit != 0 {
|
|
t.Error("Limit should be 0 by default")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateError(t *testing.T) {
|
|
t.Run("all fields serialize", func(t *testing.T) {
|
|
err := UpdateError{
|
|
Message: "Something went wrong",
|
|
Code: "ERR_001",
|
|
Details: "Additional details",
|
|
}
|
|
|
|
data, _ := json.Marshal(err)
|
|
var parsed UpdateError
|
|
json.Unmarshal(data, &parsed)
|
|
|
|
if parsed.Message != err.Message {
|
|
t.Errorf("Message mismatch")
|
|
}
|
|
if parsed.Code != err.Code {
|
|
t.Errorf("Code mismatch")
|
|
}
|
|
if parsed.Details != err.Details {
|
|
t.Errorf("Details mismatch")
|
|
}
|
|
})
|
|
|
|
t.Run("optional fields omit when empty", func(t *testing.T) {
|
|
err := UpdateError{
|
|
Message: "Error message",
|
|
}
|
|
|
|
data, _ := json.Marshal(err)
|
|
str := string(data)
|
|
|
|
if contains(str, "code") {
|
|
t.Error("code should be omitted when empty")
|
|
}
|
|
if contains(str, "details") {
|
|
t.Error("details should be omitted when empty")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Helper function
|
|
func contains(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|