Files
Agent-System/internal/sysadmin/policy_test.go
T
weichenandCursor 430a47ad8c test(sysadmin): cover author-only CAS, audit rollback, and HTTP gates
Verify concurrent revision conflicts, null/string rejection, plain-user
denials, and PG lock/read paths for the new policy key.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 09:15:15 +00:00

177 lines
5.5 KiB
Go

package sysadmin
import (
"context"
"encoding/json"
"errors"
"sync"
"testing"
)
func seedAuthorOnlyPolicy(m *memStore, allowed bool, revision int64) {
raw := json.RawMessage(`false`)
if allowed {
raw = json.RawMessage(`true`)
}
uid := int64(0)
m.mu.Lock()
defer m.mu.Unlock()
m.items[SettingKeyAllowAuthorOnlyApproval] = memEntry{
value: append(json.RawMessage(nil), raw...),
updatedBy: &uid,
updatedAt: m.items[SettingKeyAllowAuthorOnlyApproval].updatedAt,
revision: revision,
}
}
func TestAuthorOnlyPolicy_DefaultClosedWhenMissing(t *testing.T) {
svc := NewService(nil, Options{Store: newMemStore()})
pol, err := svc.GetAuthorOnlyApprovalPolicy(context.Background())
if err != nil {
t.Fatalf("GetAuthorOnlyApprovalPolicy: %v", err)
}
if pol.Allowed || pol.Revision != 0 || pol.Key != SettingKeyAllowAuthorOnlyApproval {
t.Fatalf("expected closed default, got %+v", pol)
}
}
func TestAuthorOnlyPolicy_StoreUnavailable(t *testing.T) {
svc := NewService(nil, Options{})
_, err := svc.GetAuthorOnlyApprovalPolicy(context.Background())
if !errors.Is(err, ErrStoreUnavailable) {
t.Fatalf("expected ErrStoreUnavailable, got %v", err)
}
}
func TestUpdateAuthorOnly_RequiresExpectedRevision(t *testing.T) {
store := newMemStore()
seedAuthorOnlyPolicy(store, false, 0)
svc := NewService(nil, Options{Store: store})
_, err := svc.UpdateSetting(context.Background(), SettingKeyAllowAuthorOnlyApproval, json.RawMessage(`true`), 1)
if !errors.Is(err, ErrExpectedRevisionRequired) {
t.Fatalf("expected ErrExpectedRevisionRequired, got %v", err)
}
}
func TestUpdateAuthorOnly_RejectsNullAndStringBool(t *testing.T) {
store := newMemStore()
seedAuthorOnlyPolicy(store, false, 0)
svc := NewService(nil, Options{Store: store})
zero := int64(0)
for _, raw := range []string{`null`, `"true"`, `1`, `yes`} {
_, err := svc.UpdateSettingEx(context.Background(), SettingUpdate{
Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(raw),
UpdatedBy: 1, ExpectedRevision: &zero,
})
if !errors.Is(err, ErrInvalidSettingValue) {
t.Fatalf("%s: expected ErrInvalidSettingValue, got %v", raw, err)
}
}
}
func TestUpdateAuthorOnly_CASSuccessAndConflict(t *testing.T) {
store := newMemStore()
seedAuthorOnlyPolicy(store, false, 0)
svc := NewService(nil, Options{Store: store})
zero := int64(0)
setting, err := svc.UpdateSettingEx(context.Background(), SettingUpdate{
Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`true`),
UpdatedBy: 9, ExpectedRevision: &zero, RequestID: "req-1",
})
if err != nil {
t.Fatalf("first CAS: %v", err)
}
if setting.Value != true || setting.Revision != 1 {
t.Fatalf("expected true@rev1, got value=%v rev=%d", setting.Value, setting.Revision)
}
if len(store.audits) != 1 || store.audits[0].afterRev != 1 || store.audits[0].requestID != "req-1" {
t.Fatalf("expected one audit with request id, got %+v", store.audits)
}
pol, err := svc.GetAuthorOnlyApprovalPolicy(context.Background())
if err != nil || !pol.Allowed || pol.Revision != 1 {
t.Fatalf("policy after enable: %+v err=%v", pol, err)
}
_, err = svc.UpdateSettingEx(context.Background(), SettingUpdate{
Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`false`),
UpdatedBy: 9, ExpectedRevision: &zero,
})
if !errors.Is(err, ErrSettingRevisionConflict) {
t.Fatalf("expected conflict on stale revision, got %v", err)
}
one := int64(1)
setting, err = svc.UpdateSettingEx(context.Background(), SettingUpdate{
Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`false`),
UpdatedBy: 10, ExpectedRevision: &one,
})
if err != nil {
t.Fatalf("second CAS: %v", err)
}
if setting.Value != false || setting.Revision != 2 {
t.Fatalf("expected false@rev2, got value=%v rev=%d", setting.Value, setting.Revision)
}
}
func TestUpdateAuthorOnly_AuditFailureRollsBack(t *testing.T) {
store := newMemStore()
seedAuthorOnlyPolicy(store, false, 0)
store.failAudit = true
svc := NewService(nil, Options{Store: store})
zero := int64(0)
_, err := svc.UpdateSettingEx(context.Background(), SettingUpdate{
Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`true`),
UpdatedBy: 1, ExpectedRevision: &zero,
})
if err == nil {
t.Fatal("expected audit failure")
}
pol, err := svc.GetAuthorOnlyApprovalPolicy(context.Background())
if err != nil || pol.Allowed || pol.Revision != 0 {
t.Fatalf("setting must remain closed after audit failure: %+v err=%v", pol, err)
}
}
func TestUpdateAuthorOnly_ConcurrentCASOnlyOneWins(t *testing.T) {
store := newMemStore()
seedAuthorOnlyPolicy(store, false, 0)
svc := NewService(nil, Options{Store: store})
zero := int64(0)
var wg sync.WaitGroup
results := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := svc.UpdateSettingEx(context.Background(), SettingUpdate{
Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`true`),
UpdatedBy: 1, ExpectedRevision: &zero,
})
results <- err
}()
}
wg.Wait()
close(results)
var okCount, conflictCount int
for err := range results {
switch {
case err == nil:
okCount++
case errors.Is(err, ErrSettingRevisionConflict):
conflictCount++
default:
t.Fatalf("unexpected error: %v", err)
}
}
if okCount != 1 || conflictCount != 1 {
t.Fatalf("expected 1 success + 1 conflict, got ok=%d conflict=%d", okCount, conflictCount)
}
pol, err := svc.GetAuthorOnlyApprovalPolicy(context.Background())
if err != nil || !pol.Allowed || pol.Revision != 1 {
t.Fatalf("expected single winner true@1, got %+v err=%v", pol, err)
}
}