233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
package postgresadmin_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"proxy-pool/internal/adapters/postgresadmin"
|
|
"proxy-pool/internal/domain/adminstate"
|
|
)
|
|
|
|
func TestNewRejectsNilPool(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store, err := postgresadmin.New(nil)
|
|
if store != nil {
|
|
t.Fatalf("New(nil) store = %T, want nil", store)
|
|
}
|
|
if !errors.Is(err, adminstate.ErrInvalidCommand) {
|
|
t.Fatalf("New(nil) error = %v, want ErrInvalidCommand", err)
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsTypedNilPool(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var pool *pgxpool.Pool
|
|
store, err := postgresadmin.New(pool)
|
|
if store != nil || !errors.Is(err, adminstate.ErrInvalidCommand) {
|
|
t.Fatalf("New(typed nil) = %T, %v, want nil, ErrInvalidCommand", store, err)
|
|
}
|
|
}
|
|
|
|
func TestOperationsPrioritizeCanceledContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pool := &countingPool{}
|
|
store, err := postgresadmin.New(pool)
|
|
if err != nil {
|
|
t.Fatalf("New(): %v", err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
operations := []struct {
|
|
name string
|
|
run func() error
|
|
}{
|
|
{name: "commit config", run: func() error {
|
|
_, operationErr := store.CommitConfig(ctx, adminstate.CommitConfigCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "set upstream", run: func() error {
|
|
_, operationErr := store.SetUpstreamEnabled(ctx, adminstate.SetUpstreamCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "switch routing", run: func() error {
|
|
_, operationErr := store.SwitchRouting(ctx, adminstate.SwitchRoutingCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "snapshot", run: func() error {
|
|
_, operationErr := store.Snapshot(ctx)
|
|
return operationErr
|
|
}},
|
|
{name: "read audit", run: func() error {
|
|
_, operationErr := store.ReadAudit(ctx, adminstate.AuditQuery{})
|
|
return operationErr
|
|
}},
|
|
{name: "claim", run: func() error {
|
|
_, operationErr := store.Claim(ctx, adminstate.ClaimCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "acknowledge", run: func() error {
|
|
return store.Acknowledge(ctx, adminstate.AcknowledgeCommand{})
|
|
}},
|
|
}
|
|
|
|
for _, operation := range operations {
|
|
operation := operation
|
|
t.Run(operation.name, func(t *testing.T) {
|
|
if operationErr := operation.run(); !errors.Is(operationErr, context.Canceled) {
|
|
t.Fatalf("operation error = %v, want context.Canceled", operationErr)
|
|
}
|
|
})
|
|
}
|
|
if begins := pool.begins.Load(); begins != 0 {
|
|
t.Fatalf("BeginTx calls = %d, want 0", begins)
|
|
}
|
|
}
|
|
|
|
func TestInvalidOperationsDoNotBeginTransaction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pool := &countingPool{}
|
|
store, err := postgresadmin.New(pool)
|
|
if err != nil {
|
|
t.Fatalf("New(): %v", err)
|
|
}
|
|
operations := []struct {
|
|
name string
|
|
run func() error
|
|
}{
|
|
{name: "commit config", run: func() error {
|
|
_, operationErr := store.CommitConfig(context.Background(), adminstate.CommitConfigCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "set upstream", run: func() error {
|
|
_, operationErr := store.SetUpstreamEnabled(context.Background(), adminstate.SetUpstreamCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "switch routing", run: func() error {
|
|
_, operationErr := store.SwitchRouting(context.Background(), adminstate.SwitchRoutingCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "read audit", run: func() error {
|
|
_, operationErr := store.ReadAudit(context.Background(), adminstate.AuditQuery{})
|
|
return operationErr
|
|
}},
|
|
{name: "claim", run: func() error {
|
|
_, operationErr := store.Claim(context.Background(), adminstate.ClaimCommand{})
|
|
return operationErr
|
|
}},
|
|
{name: "acknowledge", run: func() error {
|
|
return store.Acknowledge(context.Background(), adminstate.AcknowledgeCommand{})
|
|
}},
|
|
}
|
|
for _, operation := range operations {
|
|
operation := operation
|
|
t.Run(operation.name, func(t *testing.T) {
|
|
if operationErr := operation.run(); !errors.Is(operationErr, adminstate.ErrInvalidCommand) {
|
|
t.Fatalf("operation error = %v, want ErrInvalidCommand", operationErr)
|
|
}
|
|
})
|
|
}
|
|
if begins := pool.begins.Load(); begins != 0 {
|
|
t.Fatalf("BeginTx calls = %d, want 0", begins)
|
|
}
|
|
}
|
|
|
|
func TestDatabaseErrorsAreMappedAndRedacted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want error
|
|
}{
|
|
{
|
|
name: "connection",
|
|
err: errors.New("postgres://admin:secret@database/private SQL payload"),
|
|
want: adminstate.ErrUnavailable,
|
|
},
|
|
{
|
|
name: "unique constraint",
|
|
err: &pgconn.PgError{
|
|
Code: "23505",
|
|
Message: "duplicate cfg-sensitive at postgres://admin:secret@database",
|
|
},
|
|
want: adminstate.ErrConflict,
|
|
},
|
|
{
|
|
name: "foreign key constraint",
|
|
err: &pgconn.PgError{
|
|
Code: "23503",
|
|
Message: "backend revision reference failed with secret details",
|
|
},
|
|
want: adminstate.ErrUnavailable,
|
|
},
|
|
{
|
|
name: "check constraint",
|
|
err: &pgconn.PgError{
|
|
Code: "23514",
|
|
Message: "backend invariant failed with secret details",
|
|
},
|
|
want: adminstate.ErrUnavailable,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
store, err := postgresadmin.New(&errorPool{err: test.err})
|
|
if err != nil {
|
|
t.Fatalf("New(): %v", err)
|
|
}
|
|
_, operationErr := store.CommitConfig(context.Background(), validConfigCommand())
|
|
if !errors.Is(operationErr, test.want) {
|
|
t.Fatalf("CommitConfig() error = %v, want %v", operationErr, test.want)
|
|
}
|
|
for _, secret := range []string{"secret", "private SQL", "cfg-sensitive", "postgres://"} {
|
|
if strings.Contains(operationErr.Error(), secret) {
|
|
t.Fatalf("CommitConfig() error leaked %q: %v", secret, operationErr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type countingPool struct {
|
|
begins atomic.Int64
|
|
}
|
|
|
|
func (pool *countingPool) BeginTx(context.Context, pgx.TxOptions) (pgx.Tx, error) {
|
|
pool.begins.Add(1)
|
|
return nil, errors.New("unexpected transaction")
|
|
}
|
|
|
|
type errorPool struct {
|
|
err error
|
|
}
|
|
|
|
func (pool *errorPool) BeginTx(context.Context, pgx.TxOptions) (pgx.Tx, error) {
|
|
return nil, pool.err
|
|
}
|
|
|
|
func validConfigCommand() adminstate.CommitConfigCommand {
|
|
return adminstate.CommitConfigCommand{
|
|
RequestID: "request-a",
|
|
Actor: adminstate.Actor{ID: "admin-a", SourceIP: "192.0.2.10"},
|
|
OccurredAt: time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC),
|
|
ConfigVersion: "cfg-sensitive",
|
|
Checksum: strings.Repeat("a", adminstate.SHA256HexBytes),
|
|
Source: "configs/proxy-pool.yaml",
|
|
}
|
|
}
|