143 lines
5.3 KiB
Go
143 lines
5.3 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 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 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
|
|
}
|