This commit is contained in:
Tomas Dvorak
2026-02-22 15:41:27 +01:00
parent 0b88627e54
commit 409acd2e08
84 changed files with 65382 additions and 27475 deletions
+601
View File
@@ -0,0 +1,601 @@
package quality
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)
func TestNewStateManager(t *testing.T) {
dataDir := "/tmp/test_state"
sm := NewStateManager(dataDir)
if sm == nil {
t.Error("NewStateManager() should not return nil")
}
if sm.dataDir != dataDir {
t.Errorf("NewStateManager() dataDir = %v, want %v", sm.dataDir, dataDir)
}
expectedStateFile := filepath.Join(dataDir, "state.json")
if sm.stateFile != expectedStateFile {
t.Errorf("NewStateManager() stateFile = %v, want %v", sm.stateFile, expectedStateFile)
}
expectedHistoryDir := filepath.Join(dataDir, "history")
if sm.historyDir != expectedHistoryDir {
t.Errorf("NewStateManager() historyDir = %v, want %v", sm.historyDir, expectedHistoryDir)
}
}
func TestStateManager_Load(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "state_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
sm := NewStateManager(tmpDir)
// Test loading non-existent file
state, err := sm.Load()
if err != nil {
t.Errorf("Load() should not error for non-existent file: %v", err)
}
if state == nil {
t.Error("Load() should return empty state for non-existent file")
}
if len(state.Findings) != 0 {
t.Errorf("Load() should return empty findings for non-existent file, got %d", len(state.Findings))
}
if state.Metadata == nil {
t.Error("Load() should initialize metadata map")
}
// Test loading existing file
testState := &State{
Findings: []Finding{
{ID: "test1", Type: "test", Title: "Test Finding 1", Status: StatusOpen},
{ID: "test2", Type: "test", Title: "Test Finding 2", Status: StatusFixed},
},
ScanCount: 5,
Metadata: map[string]string{"env": "test"},
}
data, _ := json.Marshal(testState)
stateFile := sm.stateFile
os.WriteFile(stateFile, data, 0644)
loadedState, err := sm.Load()
if err != nil {
t.Errorf("Load() failed: %v", err)
}
if loadedState.ScanCount != 5 {
t.Errorf("Load() ScanCount = %v, want 5", loadedState.ScanCount)
}
if len(loadedState.Findings) != 2 {
t.Errorf("Load() findings count = %v, want 2", len(loadedState.Findings))
}
if loadedState.Metadata["env"] != "test" {
t.Errorf("Load() metadata = %v, want test", loadedState.Metadata["env"])
}
}
func TestStateManager_Load_InvalidJSON(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "state_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
sm := NewStateManager(tmpDir)
// Write invalid JSON
stateFile := sm.stateFile
os.WriteFile(stateFile, []byte("{ invalid json"), 0644)
_, err = sm.Load()
if err == nil {
t.Error("Load() should error for invalid JSON")
}
}
func TestStateManager_Save(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "state_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
sm := NewStateManager(tmpDir)
state := &State{
Findings: []Finding{
{ID: "test1", Type: "test", Title: "Test Finding", Status: StatusOpen},
},
Scorecard: &Scorecard{TotalScore: 100, StrictScore: 50},
ScanCount: 1,
Metadata: map[string]string{"env": "test"},
}
err = sm.Save(state)
if err != nil {
t.Errorf("Save() failed: %v", err)
}
// Verify file was created
if _, err := os.Stat(sm.stateFile); err != nil {
t.Errorf("Save() should create state file: %v", err)
}
// Verify content hash was calculated
if state.ContentHash == "" {
t.Error("Save() should calculate content hash")
}
// Load and verify
loadedState, err := sm.Load()
if err != nil {
t.Errorf("Save() failed to load saved state: %v", err)
}
if len(loadedState.Findings) != 1 {
t.Errorf("Save() should save findings, got %d", len(loadedState.Findings))
}
if loadedState.ScanCount != 1 {
t.Errorf("Save() should increment scan count, got %d", loadedState.ScanCount)
}
}
func TestStateManager_Save_History(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "state_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
sm := NewStateManager(tmpDir)
state := &State{
Findings: []Finding{{ID: "test", Type: "test", Title: "Test", Status: StatusOpen}},
Scorecard: &Scorecard{TotalScore: 100, StrictScore: 50},
}
err = sm.Save(state)
if err != nil {
t.Errorf("Save() failed: %v", err)
}
// Check history directory was created
if _, err := os.Stat(sm.historyDir); err != nil {
t.Errorf("Save() should create history directory: %v", err)
}
// Check history file was created
historyFiles, err := filepath.Glob(filepath.Join(sm.historyDir, "*.json"))
if err != nil {
t.Errorf("Save() failed to list history files: %v", err)
}
if len(historyFiles) != 1 {
t.Errorf("Save() should create 1 history file, got %d", len(historyFiles))
}
}
func TestStateManager_Merge(t *testing.T) {
sm := NewStateManager("/tmp")
existingState := &State{
Findings: []Finding{
{ID: "existing1", Type: "test", Title: "Existing 1", Status: StatusOpen, Score: 5},
{ID: "existing2", Type: "test", Title: "Existing 2", Status: StatusOpen, Score: 10},
{ID: "existing3", Type: "test", Title: "Existing 3", Status: StatusOpen, Score: 15},
},
}
newFindings := []Finding{
{ID: "existing1", Type: "test", Title: "Existing 1 Changed", Status: StatusOpen, Score: 5}, // Changed
{ID: "new1", Type: "test", Title: "New Finding", Status: StatusOpen, Score: 20}, // Added
{ID: "new2", Type: "test", Title: "New Finding 2", Status: StatusOpen, Score: 25}, // Added
}
diff := sm.Merge(existingState, newFindings)
if len(diff.Added) != 2 {
t.Errorf("Merge() added count = %v, want 2", len(diff.Added))
}
if len(diff.Changed) != 1 {
t.Errorf("Merge() changed count = %v, want 1", len(diff.Changed))
}
if len(diff.Resolved) != 2 {
t.Errorf("Merge() resolved count = %v, want 2", len(diff.Resolved))
}
if len(existingState.Findings) != 3 {
t.Errorf("Merge() should update state findings count to %d, got %d", len(newFindings), len(existingState.Findings))
}
if existingState.ScanCount != 1 {
t.Errorf("Merge() should increment scan count to 1, got %d", existingState.ScanCount)
}
}
func TestStateManager_Merge_Resolved(t *testing.T) {
sm := NewStateManager("/tmp")
existingState := &State{
Findings: []Finding{
{ID: "open1", Type: "test", Title: "Open Finding", Status: StatusOpen},
{ID: "open2", Type: "test", Title: "Open Finding 2", Status: StatusOpen},
},
}
newFindings := []Finding{
{ID: "open1", Type: "test", Title: "Open Finding", Status: StatusOpen}, // Kept
{ID: "new1", Type: "test", Title: "New Finding", Status: StatusOpen}, // Added
}
diff := sm.Merge(existingState, newFindings)
if len(diff.Added) != 1 {
t.Errorf("Merge() added count = %v, want 1", len(diff.Added))
}
if len(diff.Resolved) != 1 {
t.Errorf("Merge() resolved count = %v, want 1", len(diff.Resolved))
}
if diff.Resolved[0].ID != "open2" {
t.Errorf("Merge() resolved wrong finding: %s", diff.Resolved[0].ID)
}
}
func TestStateManager_Diff(t *testing.T) {
sm := NewStateManager("/tmp")
oldState := &State{
Findings: []Finding{
{ID: "old1", Type: "test", Title: "Old Finding", Status: StatusOpen},
{ID: "old2", Type: "test", Title: "Old Finding 2", Status: StatusFixed},
},
}
newState := &State{
Findings: []Finding{
{ID: "old1", Type: "test", Title: "Old Finding Changed", Status: StatusOpen}, // Changed
{ID: "new1", Type: "test", Title: "New Finding", Status: StatusOpen}, // Added
{ID: "old2", Type: "test", Title: "Old Finding 2", Status: StatusOpen}, // Regression
},
}
diff := sm.Diff(oldState, newState)
if len(diff.Added) != 1 {
t.Errorf("Diff() added count = %v, want 1", len(diff.Added))
}
if len(diff.Changed) != 2 {
t.Errorf("Diff() changed count = %v, want 2", len(diff.Changed))
}
if len(diff.Removed) != 0 {
t.Errorf("Diff() removed count = %v, want 0", len(diff.Removed))
}
if len(diff.Regressions) != 1 {
t.Errorf("Diff() regressions count = %v, want 1", len(diff.Regressions))
}
}
func TestStateManager_calculateHash(t *testing.T) {
sm := NewStateManager("/tmp")
findings := []Finding{
{ID: "test1", Type: "test", Title: "Test 1", Status: StatusOpen},
{ID: "test2", Type: "test", Title: "Test 2", Status: StatusOpen},
}
hash1 := sm.calculateHash(findings)
hash2 := sm.calculateHash(findings)
if hash1 != hash2 {
t.Errorf("calculateHash() should be deterministic, got %s and %s", hash1, hash2)
}
if len(hash1) != 16 {
t.Errorf("calculateHash() should return 16 character hash, got %d", len(hash1))
}
// Test with different order
reversed := []Finding{findings[1], findings[0]}
hash3 := sm.calculateHash(reversed)
if hash1 != hash3 {
t.Errorf("calculateHash() should be order-independent, got %s and %s", hash1, hash3)
}
}
func TestStateManager_saveHistory(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "state_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
sm := NewStateManager(tmpDir)
state := &State{
Findings: []Finding{{ID: "test", Type: "test", Title: "Test", Status: StatusOpen}},
Scorecard: &Scorecard{TotalScore: 100, StrictScore: 50},
ContentHash: "testhash",
}
err = sm.saveHistory(state)
if err != nil {
t.Errorf("saveHistory() failed: %v", err)
}
// Check history file was created
files, err := filepath.Glob(filepath.Join(sm.historyDir, "*.json"))
if err != nil {
t.Errorf("saveHistory() failed to list files: %v", err)
}
if len(files) != 1 {
t.Errorf("saveHistory() should create 1 file, got %d", len(files))
}
// Verify snapshot content
snapshotFile := files[0]
data, err := os.ReadFile(snapshotFile)
if err != nil {
t.Errorf("saveHistory() failed to read snapshot file: %v", err)
}
// The file contains the full state, not a snapshot
var savedState State
if err := json.Unmarshal(data, &savedState); err != nil {
t.Errorf("saveHistory() failed to parse saved state: %v", err)
}
if len(savedState.Findings) != 1 {
t.Errorf("saveHistory() saved state findings count = %v, want 1", len(savedState.Findings))
}
if savedState.Scorecard.TotalScore != 100 {
t.Errorf("saveHistory() saved state score = %v, want 100", savedState.Scorecard.TotalScore)
}
if savedState.ContentHash != "testhash" {
t.Errorf("saveHistory() saved state hash = %v, want testhash", savedState.ContentHash)
}
}
func TestStateManager_ResolveFinding(t *testing.T) {
sm := NewStateManager("/tmp")
state := &State{
Findings: []Finding{
{ID: "test1", Type: "test", Title: "Test Finding", Status: StatusOpen},
{ID: "test2", Type: "test", Title: "Test Finding 2", Status: StatusOpen},
},
}
// Resolve existing finding
err := sm.ResolveFinding(state, "test1", StatusFixed, "Fixed the issue")
if err != nil {
t.Errorf("ResolveFinding() failed: %v", err)
}
if state.Findings[0].Status != StatusFixed {
t.Errorf("ResolveFinding() status = %v, want Fixed", state.Findings[0].Status)
}
if state.Findings[0].Metadata["resolution_note"] != "Fixed the issue" {
t.Errorf("ResolveFinding() resolution_note = %v, want 'Fixed the issue'", state.Findings[0].Metadata["resolution_note"])
}
// Try to resolve non-existent finding
err = sm.ResolveFinding(state, "nonexistent", StatusFixed, "note")
if err == nil {
t.Error("ResolveFinding() should error for non-existent finding")
}
}
func TestStateManager_GetFinding(t *testing.T) {
sm := NewStateManager("/tmp")
state := &State{
Findings: []Finding{
{ID: "test1", Type: "test", Title: "Test Finding", Status: StatusOpen},
{ID: "test2", Type: "test", Title: "Test Finding 2", Status: StatusOpen},
},
}
// Get existing finding
finding := sm.GetFinding(state, "test1")
if finding == nil {
t.Error("GetFinding() should return finding for existing ID")
}
if finding.ID != "test1" {
t.Errorf("GetFinding() returned wrong finding ID: %s", finding.ID)
}
// Get non-existent finding
finding = sm.GetFinding(state, "nonexistent")
if finding != nil {
t.Error("GetFinding() should return nil for non-existent ID")
}
}
func TestStateManager_GetOpenFindings(t *testing.T) {
sm := NewStateManager("/tmp")
state := &State{
Findings: []Finding{
{ID: "open1", Type: "test", Title: "Open Finding", Status: StatusOpen},
{ID: "fixed1", Type: "test", Title: "Fixed Finding", Status: StatusFixed},
{ID: "open2", Type: "test", Title: "Open Finding 2", Status: StatusOpen},
},
}
open := sm.GetOpenFindings(state)
if len(open) != 2 {
t.Errorf("GetOpenFindings() count = %v, want 2", len(open))
}
for _, f := range open {
if f.Status != StatusOpen {
t.Errorf("GetOpenFindings() should only return open findings, got %v", f.Status)
}
}
}
func TestStateManager_GetFindingsByTier(t *testing.T) {
sm := NewStateManager("/tmp")
state := &State{
Findings: []Finding{
{ID: "t1", Type: "test", Title: "T1 Finding", Status: StatusOpen, Severity: SeverityT1},
{ID: "t2", Type: "test", Title: "T2 Finding", Status: StatusOpen, Severity: SeverityT2},
{ID: "t3", Type: "test", Title: "T3 Finding", Status: StatusOpen, Severity: SeverityT3},
{ID: "t4", Type: "test", Title: "T4 Finding", Status: StatusOpen, Severity: SeverityT4},
},
}
byTier := sm.GetFindingsByTier(state)
if len(byTier) != 4 {
t.Errorf("GetFindingsByTier() should return 4 tiers, got %d", len(byTier))
}
if len(byTier[SeverityT1]) != 1 {
t.Errorf("GetFindingsByTier() T1 count = %v, want 1", len(byTier[SeverityT1]))
}
if len(byTier[SeverityT4]) != 1 {
t.Errorf("GetFindingsByTier() T4 count = %v, want 1", len(byTier[SeverityT4]))
}
}
func TestStateManager_GetTrend(t *testing.T) {
sm := NewStateManager("/tmp")
state := &State{
History: []StateSnapshot{
{Timestamp: time.Now().Add(-4 * time.Hour), Score: 100, Findings: 10},
{Timestamp: time.Now().Add(-3 * time.Hour), Score: 90, Findings: 12},
{Timestamp: time.Now().Add(-2 * time.Hour), Score: 80, Findings: 15},
{Timestamp: time.Now().Add(-1 * time.Hour), Score: 70, Findings: 18},
{Timestamp: time.Now(), Score: 60, Findings: 20},
},
}
// Get last 3 snapshots
trend := sm.GetTrend(state, 3)
if len(trend) != 3 {
t.Errorf("GetTrend() should return 3 snapshots, got %d", len(trend))
}
// Verify order (should be chronological, oldest first)
if trend[0].Score != 80 {
t.Errorf("GetTrend() first snapshot should be oldest: %d", trend[0].Score)
}
if trend[2].Score != 60 {
t.Errorf("GetTrend() last snapshot should be most recent: %d", trend[2].Score)
}
// Request more than available
allTrend := sm.GetTrend(state, 10)
if len(allTrend) != 5 {
t.Errorf("GetTrend() should return all available snapshots when requesting more than available: %d", len(allTrend))
}
}
func TestFindingsEqual(t *testing.T) {
finding1 := Finding{
ID: "test", Type: "test", Title: "Test", File: "test.go", Line: 10,
Severity: SeverityT2, Score: 5, Status: StatusOpen,
}
finding2 := Finding{
ID: "test", Type: "test", Title: "Test", File: "test.go", Line: 10,
Severity: SeverityT2, Score: 5, Status: StatusOpen,
}
finding3 := Finding{
ID: "different", Type: "test", Title: "Different", File: "test.go", Line: 10,
Severity: SeverityT2, Score: 5, Status: StatusOpen,
}
if !findingsEqual(finding1, finding2) {
t.Error("findingsEqual() should return true for equal findings")
}
if findingsEqual(finding1, finding3) {
t.Error("findingsEqual() should return false for different findings")
}
}
func TestFindingsEqual_DifferentStatus(t *testing.T) {
finding1 := Finding{ID: "test", Type: "test", Status: StatusOpen}
finding2 := Finding{ID: "test", Type: "test", Status: StatusFixed}
if findingsEqual(finding1, finding2) {
t.Error("findingsEqual() should return false for different status")
}
}
func TestFormatDiff(t *testing.T) {
diff := &StateDiff{
Added: []Finding{
{ID: "new1", Title: "New Finding 1"},
{ID: "new2", Title: "New Finding 2"},
},
Removed: []Finding{
{ID: "old1", Title: "Old Finding 1"},
},
Changed: []Finding{
{ID: "changed1", Title: "Changed Finding 1"},
},
Resolved: []Finding{
{ID: "resolved1", Title: "Resolved Finding 1"},
},
Regressions: []Finding{
{ID: "regression1", Title: "Regression Finding 1"},
},
}
output := FormatDiff(diff)
expected := "[+] Added: 2 findings\n - new1: New Finding 1\n - new2: New Finding 2\n[-] Removed: 1 findings\n - old1: Old Finding 1\n[~] Changed: 1 findings\n - changed1: Changed Finding 1\n[OK] Resolved: 1 findings\n - resolved1: Resolved Finding 1\n[!] Regressions: 1 findings\n - regression1: Regression Finding 1\n"
if output != expected {
t.Errorf("FormatDiff() output mismatch:\nGot:\n%s\nExpected:\n%s", output, expected)
}
}
func TestFormatDiff_Empty(t *testing.T) {
diff := &StateDiff{}
output := FormatDiff(diff)
expected := "No changes detected\n"
if output != expected {
t.Errorf("FormatDiff() empty diff output mismatch:\nGot:\n%s\nExpected:\n%s", output, expected)
}
}