go-crypt/trust/approval_test.go
2026-02-20 04:02:47 +00:00

296 lines
7.2 KiB
Go

package trust
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- ApprovalStatus ---
func TestApprovalStatusString_Good(t *testing.T) {
assert.Equal(t, "pending", ApprovalPending.String())
assert.Equal(t, "approved", ApprovalApproved.String())
assert.Equal(t, "denied", ApprovalDenied.String())
}
func TestApprovalStatusString_Bad_Unknown(t *testing.T) {
assert.Contains(t, ApprovalStatus(99).String(), "unknown")
}
// --- Submit ---
func TestApprovalSubmit_Good(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
assert.NotEmpty(t, id)
assert.Equal(t, 1, q.Len())
}
func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) {
q := NewApprovalQueue()
id1, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
id2, err := q.Submit("Hypnos", CapMergePR, "host-uk/docs")
require.NoError(t, err)
assert.NotEqual(t, id1, id2, "each request should get a unique ID")
assert.Equal(t, 2, q.Len())
}
func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "")
require.NoError(t, err)
assert.NotEmpty(t, id)
req := q.Get(id)
require.NotNil(t, req)
assert.Empty(t, req.Repo)
}
func TestApprovalSubmit_Bad_EmptyAgent(t *testing.T) {
q := NewApprovalQueue()
_, err := q.Submit("", CapMergePR, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "agent name is required")
}
func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
q := NewApprovalQueue()
_, err := q.Submit("Clotho", "", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "capability is required")
}
// --- Get ---
func TestApprovalGet_Good(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
req := q.Get(id)
require.NotNil(t, req)
assert.Equal(t, id, req.ID)
assert.Equal(t, "Clotho", req.Agent)
assert.Equal(t, CapMergePR, req.Cap)
assert.Equal(t, "host-uk/core", req.Repo)
assert.Equal(t, ApprovalPending, req.Status)
assert.False(t, req.RequestedAt.IsZero())
assert.True(t, req.ReviewedAt.IsZero())
}
func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
req := q.Get(id)
require.NotNil(t, req)
req.Status = ApprovalApproved // Mutate the copy
// Original should be unchanged.
original := q.Get(id)
assert.Equal(t, ApprovalPending, original.Status)
}
func TestApprovalGet_Bad_NotFound(t *testing.T) {
q := NewApprovalQueue()
assert.Nil(t, q.Get("nonexistent"))
}
// --- Approve ---
func TestApprovalApprove_Good(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
err := q.Approve(id, "admin", "looks good")
require.NoError(t, err)
req := q.Get(id)
require.NotNil(t, req)
assert.Equal(t, ApprovalApproved, req.Status)
assert.Equal(t, "admin", req.ReviewedBy)
assert.Equal(t, "looks good", req.Reason)
assert.False(t, req.ReviewedAt.IsZero())
}
func TestApprovalApprove_Bad_NotFound(t *testing.T) {
q := NewApprovalQueue()
err := q.Approve("nonexistent", "admin", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, q.Approve(id, "admin", ""))
err := q.Approve(id, "admin2", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "already approved")
}
func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, q.Deny(id, "admin", "nope"))
err := q.Approve(id, "admin2", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "already denied")
}
// --- Deny ---
func TestApprovalDeny_Good(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
err := q.Deny(id, "admin", "not appropriate")
require.NoError(t, err)
req := q.Get(id)
require.NotNil(t, req)
assert.Equal(t, ApprovalDenied, req.Status)
assert.Equal(t, "admin", req.ReviewedBy)
assert.Equal(t, "not appropriate", req.Reason)
assert.False(t, req.ReviewedAt.IsZero())
}
func TestApprovalDeny_Bad_NotFound(t *testing.T) {
q := NewApprovalQueue()
err := q.Deny("nonexistent", "admin", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, q.Deny(id, "admin", ""))
err := q.Deny(id, "admin2", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "already denied")
}
// --- Pending ---
func TestApprovalPending_Good(t *testing.T) {
q := NewApprovalQueue()
q.Submit("Clotho", CapMergePR, "host-uk/core")
q.Submit("Hypnos", CapMergePR, "host-uk/docs")
id3, _ := q.Submit("Darbs", CapMergePR, "host-uk/tools")
q.Approve(id3, "admin", "")
pending := q.Pending()
assert.Len(t, pending, 2)
}
func TestApprovalPending_Good_Empty(t *testing.T) {
q := NewApprovalQueue()
assert.Empty(t, q.Pending())
}
// --- Concurrent operations ---
func TestApprovalConcurrent_Good(t *testing.T) {
q := NewApprovalQueue()
const n = 10
var wg sync.WaitGroup
wg.Add(n)
ids := make([]string, n)
var mu sync.Mutex
// Submit concurrently
for i := 0; i < n; i++ {
go func(idx int) {
defer wg.Done()
id, err := q.Submit(
fmt.Sprintf("agent-%d", idx),
CapMergePR,
"host-uk/core",
)
assert.NoError(t, err)
mu.Lock()
ids[idx] = id
mu.Unlock()
}(i)
}
wg.Wait()
assert.Equal(t, n, q.Len())
// Approve/deny concurrently
wg.Add(n)
for i := 0; i < n; i++ {
go func(idx int) {
defer wg.Done()
mu.Lock()
id := ids[idx]
mu.Unlock()
if idx%2 == 0 {
_ = q.Approve(id, "admin", "ok")
} else {
_ = q.Deny(id, "admin", "no")
}
}(i)
}
wg.Wait()
assert.Empty(t, q.Pending())
}
// --- Integration: PolicyEngine + ApprovalQueue ---
func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) {
pe := newTestEngine(t)
q := NewApprovalQueue()
// Clotho (Tier 2) tries to merge a PR — should get NeedsApproval
result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core")
assert.Equal(t, NeedsApproval, result.Decision)
// Submit an approval request
id, err := q.Submit(result.Agent, result.Cap, "host-uk/core")
require.NoError(t, err)
// Admin approves
err = q.Approve(id, "Virgil", "PR reviewed, merge approved")
require.NoError(t, err)
// Verify approval
req := q.Get(id)
require.NotNil(t, req)
assert.Equal(t, ApprovalApproved, req.Status)
assert.Equal(t, "Virgil", req.ReviewedBy)
}
func TestApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) {
pe := newTestEngine(t)
q := NewApprovalQueue()
result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core")
assert.Equal(t, NeedsApproval, result.Decision)
id, err := q.Submit(result.Agent, result.Cap, "host-uk/core")
require.NoError(t, err)
err = q.Deny(id, "Virgil", "needs more review")
require.NoError(t, err)
req := q.Get(id)
require.NotNil(t, req)
assert.Equal(t, ApprovalDenied, req.Status)
}