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 TestListPolicyAudits_MemoryStore(t *testing.T) { store := newMemStore() seedAuthorOnlyPolicy(store, false, 0) svc := NewService(nil, Options{Store: store}) zero := int64(0) if _, err := svc.UpdateSettingEx(context.Background(), SettingUpdate{ Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`true`), UpdatedBy: 9, ExpectedRevision: &zero, RequestID: "audit-list-1", }); err != nil { t.Fatalf("CAS: %v", err) } one := int64(1) if _, err := svc.UpdateSettingEx(context.Background(), SettingUpdate{ Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`false`), UpdatedBy: 10, ExpectedRevision: &one, RequestID: "audit-list-2", }); err != nil { t.Fatalf("second CAS: %v", err) } audits, err := svc.ListPolicyAudits(context.Background(), SettingKeyAllowAuthorOnlyApproval, 10) if err != nil { t.Fatalf("ListPolicyAudits: %v", err) } if len(audits) != 2 { t.Fatalf("expected 2 audits, got %d", len(audits)) } if audits[0].RequestID != "audit-list-2" || audits[0].AfterRevision != 2 { t.Fatalf("newest audit first: %+v", audits[0]) } if audits[1].RequestID != "audit-list-1" || audits[1].AfterRevision != 1 { t.Fatalf("older audit second: %+v", audits[1]) } limited, err := svc.ListPolicyAudits(context.Background(), SettingKeyAllowAuthorOnlyApproval, 1) if err != nil || len(limited) != 1 || limited[0].RequestID != "audit-list-2" { t.Fatalf("limit=1: got %+v err=%v", limited, err) } if _, err := svc.ListPolicyAudits(context.Background(), "no.such.key", 10); !errors.Is(err, ErrUnknownSetting) { t.Fatalf("unknown key: %v", err) } svcNil := NewService(nil, Options{}) if _, err := svcNil.ListPolicyAudits(context.Background(), SettingKeyAllowAuthorOnlyApproval, 10); !errors.Is(err, ErrStoreUnavailable) { t.Fatalf("nil store: %v", err) } } 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) } }