proxy-pool/internal/adapters/postgresadmin/rollback_integration_test.go
2026-08-02 15:14:20 +08:00

200 lines
7.7 KiB
Go

//go:build integration
package postgresadmin
import (
"context"
"errors"
"strings"
"testing"
"time"
"proxy-pool/internal/domain/adminstate"
)
func TestMutationRollsBackWhenAuditInsertFails(t *testing.T) {
fixture := newPostgresTestFixture(t)
now := integrationNow()
commitIntegrationConfig(t, fixture.Store, now)
installRejectInsertTrigger(t, fixture, "admin_audit_log", "reject_admin_audit")
_, err := fixture.Store.SetUpstreamEnabled(context.Background(), adminstate.SetUpstreamCommand{
RequestID: "req-audit-failure", Actor: integrationActor(), OccurredAt: now.Add(time.Second),
Name: "provider-a", Enabled: false,
})
if !errors.Is(err, adminstate.ErrUnavailable) {
t.Fatalf("SetUpstreamEnabled(audit failure) error = %v, want ErrUnavailable", err)
}
assertFailedMutationLeftBaseline(t, fixture)
dropRejectInsertTrigger(t, fixture, "admin_audit_log", "reject_admin_audit")
result, err := fixture.Store.SetUpstreamEnabled(context.Background(), adminstate.SetUpstreamCommand{
RequestID: "req-after-audit-failure", Actor: integrationActor(), OccurredAt: now.Add(2 * time.Second),
Name: "provider-a", Enabled: false,
})
if err != nil || !result.Changed || result.Revision != 2 {
t.Fatalf("SetUpstreamEnabled(after rollback) = %+v, %v", result, err)
}
}
func TestMutationRollsBackWhenOutboxInsertFails(t *testing.T) {
fixture := newPostgresTestFixture(t)
now := integrationNow()
commitIntegrationConfig(t, fixture.Store, now)
installRejectInsertTrigger(t, fixture, "admin_outbox", "reject_admin_outbox")
_, err := fixture.Store.SetUpstreamEnabled(context.Background(), adminstate.SetUpstreamCommand{
RequestID: "req-outbox-failure", Actor: integrationActor(), OccurredAt: now.Add(time.Second),
Name: "provider-a", Enabled: false,
})
if !errors.Is(err, adminstate.ErrUnavailable) {
t.Fatalf("SetUpstreamEnabled(outbox failure) error = %v, want ErrUnavailable", err)
}
assertFailedMutationLeftBaseline(t, fixture)
dropRejectInsertTrigger(t, fixture, "admin_outbox", "reject_admin_outbox")
result, err := fixture.Store.SetUpstreamEnabled(context.Background(), adminstate.SetUpstreamCommand{
RequestID: "req-after-outbox-failure", Actor: integrationActor(), OccurredAt: now.Add(2 * time.Second),
Name: "provider-a", Enabled: false,
})
if err != nil || !result.Changed || result.Revision != 2 {
t.Fatalf("SetUpstreamEnabled(after rollback) = %+v, %v", result, err)
}
}
func TestDisableRoutingRollsBackWhenAuditOrOutboxInsertFails(t *testing.T) {
for _, target := range []struct {
name string
table string
}{
{name: "audit", table: "admin_audit_log"},
{name: "outbox", table: "admin_outbox"},
} {
t.Run(target.name, func(t *testing.T) {
fixture := newPostgresTestFixture(t)
now := integrationNow()
commitIntegrationConfig(t, fixture.Store, now)
installRejectInsertTrigger(t, fixture, target.table, "reject_routing_disable")
_, err := fixture.Store.DisableRouting(context.Background(), adminstate.DisableRoutingCommand{
RequestID: "req-routing-disable-failure", Actor: integrationActor(), OccurredAt: now.Add(time.Second),
Name: "checkout", ExpectedCurrent: "provider-a", Reason: "terminal empty results",
})
if !errors.Is(err, adminstate.ErrUnavailable) {
t.Fatalf("DisableRouting(%s failure) error = %v, want ErrUnavailable", target.name, err)
}
assertFailedRoutingDisableLeftBaseline(t, fixture)
dropRejectInsertTrigger(t, fixture, target.table, "reject_routing_disable")
result, err := fixture.Store.DisableRouting(context.Background(), adminstate.DisableRoutingCommand{
RequestID: "req-routing-disable-after-failure", Actor: integrationActor(), OccurredAt: now.Add(2 * time.Second),
Name: "checkout", ExpectedCurrent: "provider-a", Reason: "terminal empty results",
})
if err != nil || !result.Changed || result.Revision != 2 {
t.Fatalf("DisableRouting(after rollback) = %+v, %v", result, err)
}
})
}
}
func assertFailedMutationLeftBaseline(t *testing.T, fixture postgresTestFixture) {
t.Helper()
snapshot, err := fixture.Store.Snapshot(context.Background())
if err != nil || snapshot.Revision != 1 || !integrationUpstreamEnabled(snapshot, "provider-a") {
t.Fatalf("Snapshot(after failed mutation) = %+v, %v", snapshot, err)
}
audits, err := fixture.Store.ReadAudit(context.Background(), adminstate.AuditQuery{Limit: 10})
if err != nil || len(audits) != 1 {
t.Fatalf("ReadAudit(after failed mutation) = %+v, %v", audits, err)
}
for table, want := range map[string]int{"control_revisions": 1, "admin_outbox": 1} {
var count int
if err := fixture.Pool.QueryRow(context.Background(), "SELECT COUNT(*) FROM "+table).Scan(&count); err != nil {
t.Fatalf("count %s: %v", table, err)
}
if count != want {
t.Fatalf("%s row count = %d, want %d", table, count, want)
}
}
}
func assertFailedRoutingDisableLeftBaseline(t *testing.T, fixture postgresTestFixture) {
t.Helper()
snapshot, err := fixture.Store.Snapshot(context.Background())
if err != nil || snapshot.Revision != 1 || len(snapshot.Routings) != 1 || !snapshot.Routings[0].Enabled ||
snapshot.Routings[0].CurrentUpstream != "provider-a" {
t.Fatalf("Snapshot(after failed DisableRouting) = %+v, %v", snapshot, err)
}
audits, err := fixture.Store.ReadAudit(context.Background(), adminstate.AuditQuery{Limit: 10})
if err != nil || len(audits) != 1 {
t.Fatalf("ReadAudit(after failed DisableRouting) = %+v, %v", audits, err)
}
for table, want := range map[string]int{"control_revisions": 1, "admin_outbox": 1} {
var count int
if err := fixture.Pool.QueryRow(context.Background(), "SELECT COUNT(*) FROM "+table).Scan(&count); err != nil {
t.Fatalf("count %s: %v", table, err)
}
if count != want {
t.Fatalf("%s row count = %d, want %d", table, count, want)
}
}
}
func installRejectInsertTrigger(t *testing.T, fixture postgresTestFixture, table, trigger string) {
t.Helper()
function := trigger + "_fn"
statement := "CREATE FUNCTION " + function + `() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'injected management write failure'; END
$$;
CREATE TRIGGER ` + trigger + " BEFORE INSERT ON " + table +
" FOR EACH ROW EXECUTE FUNCTION " + function + "()"
if _, err := fixture.Pool.Exec(context.Background(), statement); err != nil {
t.Fatalf("install %s trigger: %v", trigger, err)
}
}
func dropRejectInsertTrigger(t *testing.T, fixture postgresTestFixture, table, trigger string) {
t.Helper()
statement := "DROP TRIGGER " + trigger + " ON " + table + "; DROP FUNCTION " + trigger + "_fn()"
if _, err := fixture.Pool.Exec(context.Background(), statement); err != nil {
t.Fatalf("drop %s trigger: %v", trigger, err)
}
}
func commitIntegrationConfig(t *testing.T, store adminstate.Store, now time.Time) {
t.Helper()
result, err := store.CommitConfig(context.Background(), adminstate.CommitConfigCommand{
RequestID: "req-config", Actor: integrationActor(), OccurredAt: now,
ConfigVersion: "cfg-1", Checksum: strings.Repeat("a", adminstate.SHA256HexBytes),
Source: "configs/proxy-pool.yaml",
Upstreams: []adminstate.UpstreamDefinition{
{Name: "provider-a", Enabled: true},
{Name: "provider-b", Enabled: true},
},
Routings: []adminstate.RoutingDefinition{{
Name: "checkout", Enabled: true, Upstreams: []string{"provider-a", "provider-b"},
CurrentUpstream: "provider-a",
}},
})
if err != nil || !result.Changed || result.Revision != 1 {
t.Fatalf("CommitConfig() = %+v, %v", result, err)
}
}
func integrationActor() adminstate.Actor {
return adminstate.Actor{ID: "admin-a", SourceIP: "192.0.2.10"}
}
func integrationNow() time.Time {
return time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC)
}
func integrationUpstreamEnabled(snapshot adminstate.Snapshot, name string) bool {
for _, upstream := range snapshot.Upstreams {
if upstream.Name == name {
return upstream.Enabled
}
}
return false
}