package sysadmin import ( "context" "encoding/json" "errors" "testing" "chat-agent-system/internal/db" "github.com/jackc/pgx/v5/pgxpool" ) func TestPGSettingsStore_RoundTrip(t *testing.T) { dsn := db.SkipIfNoTestDatabaseURL(t) db.RefuseSharedEnvDSN(t, dsn) ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("connect: %v", err) } defer pool.Close() if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { t.Fatalf("migrate: %v", err) } store := newPGSettingsStore(pool) key := "ui.maintenance_notice" if _, _, _, _, err := store.Get(ctx, key); err != ErrSettingNotFound { t.Fatalf("expected ErrSettingNotFound, got %v", err) } if _, _, err := store.Upsert(ctx, key, json.RawMessage(`"维护中"`), 1); err != nil { t.Fatalf("upsert: %v", err) } raw, updatedBy, updatedAt, revision, err := store.Get(ctx, key) if err != nil { t.Fatalf("get: %v", err) } if string(raw) != `"维护中"` { t.Fatalf("unexpected value: %s", raw) } if updatedBy == nil || *updatedBy != 1 { t.Fatalf("expected updatedBy=1, got %+v", updatedBy) } if updatedAt == nil { t.Fatal("expected updatedAt") } if revision != 0 { t.Fatalf("plain upsert must not bump revision, got %d", revision) } if _, _, err := store.Upsert(ctx, key, json.RawMessage(`"恢复"`), 2); err != nil { t.Fatalf("second upsert: %v", err) } raw, updatedBy, _, _, err = store.Get(ctx, key) if err != nil { t.Fatalf("get after overwrite: %v", err) } if string(raw) != `"恢复"` || updatedBy == nil || *updatedBy != 2 { t.Fatalf("expected overwrite, got %s by %+v", raw, updatedBy) } } func TestPGSettingsStore_AuthorOnlyCASAndAudit(t *testing.T) { dsn := db.SkipIfNoTestDatabaseURL(t) db.RefuseSharedEnvDSN(t, dsn) ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("connect: %v", err) } defer pool.Close() if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { t.Fatalf("migrate: %v", err) } store := newPGSettingsStore(pool) svc := NewService(nil, Options{Store: store}) pol, err := svc.GetAuthorOnlyApprovalPolicy(ctx) if err != nil { t.Fatalf("get policy: %v", err) } if pol.Allowed { t.Fatalf("seeded policy must default closed, got %+v", pol) } rev := pol.Revision setting, err := svc.UpdateSettingEx(ctx, SettingUpdate{ Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`true`), UpdatedBy: 11, ExpectedRevision: &rev, RequestID: "pg-cas-1", }) if err != nil { t.Fatalf("enable: %v", err) } if setting.Value != true || setting.Revision != rev+1 { t.Fatalf("expected true@%d, got value=%v rev=%d", rev+1, setting.Value, setting.Revision) } _, err = svc.UpdateSettingEx(ctx, SettingUpdate{ Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`false`), UpdatedBy: 11, ExpectedRevision: &rev, }) if !errors.Is(err, ErrSettingRevisionConflict) { t.Fatalf("expected conflict, got %v", err) } var auditCount int if err := pool.QueryRow(ctx, ` SELECT COUNT(*) FROM system_setting_policy_audits WHERE setting_key = $1 AND request_id = 'pg-cas-1' `, SettingKeyAllowAuthorOnlyApproval).Scan(&auditCount); err != nil { t.Fatalf("count audits: %v", err) } if auditCount != 1 { t.Fatalf("expected 1 audit row, got %d", auditCount) } tx, err := pool.Begin(ctx) if err != nil { t.Fatalf("begin: %v", err) } defer tx.Rollback(ctx) locked, err := svc.LockAuthorOnlyApprovalPolicy(ctx, tx) if err != nil { t.Fatalf("lock: %v", err) } if !locked.Allowed || locked.Revision != rev+1 { t.Fatalf("locked snapshot mismatch: %+v", locked) } if err := tx.Commit(ctx); err != nil { t.Fatalf("commit: %v", err) } // 复位为关闭,避免污染同库其它测试对默认态的假设 next := setting.Revision if _, err := svc.UpdateSettingEx(ctx, SettingUpdate{ Key: SettingKeyAllowAuthorOnlyApproval, Value: json.RawMessage(`false`), UpdatedBy: 11, ExpectedRevision: &next, }); err != nil { t.Fatalf("reset closed: %v", err) } }