package audit import ( "context" "errors" "os" "sync" "testing" "github.com/jackc/pgx/v5/pgxpool" ) func setupFourEyesTest(t *testing.T) (*FourEyes, func()) { t.Helper() adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } if _, err := pool.Exec(ctx, ` CREATE TABLE IF NOT EXISTS security_confirmations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action TEXT NOT NULL, target TEXT NOT NULL, requested_by TEXT NOT NULL, code_hash BYTEA NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'rejected')), confirmed_by TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), confirmed_at TIMESTAMPTZ )`); err != nil { t.Fatalf("schema: %v", err) } cleanup := func() { _, _ = pool.Exec(ctx, `DELETE FROM security_confirmations WHERE action LIKE 'test.%'`) pool.Close() } return NewFourEyes(pool), cleanup } func TestFourEyes_RequestAndConfirm(t *testing.T) { fe, cleanup := setupFourEyesTest(t) defer cleanup() ctx := context.Background() id, code, err := fe.Request(ctx, "test.tenant_delete", "tenant:acme", "alice@example.com") if err != nil { t.Fatalf("request: %v", err) } if err := fe.Confirm(ctx, id, "bob@example.com", code); err != nil { t.Fatalf("confirm: %v", err) } } func TestFourEyes_RejectsSameActor(t *testing.T) { fe, cleanup := setupFourEyesTest(t) defer cleanup() ctx := context.Background() id, code, err := fe.Request(ctx, "test.tenant_delete", "tenant:acme", "alice@example.com") if err != nil { t.Fatalf("request: %v", err) } if err := fe.Confirm(ctx, id, "alice@example.com", code); !errors.Is(err, ErrSameActor) { t.Fatalf("erwartet ErrSameActor, habe %v", err) } } func TestFourEyes_RejectsWrongCode(t *testing.T) { fe, cleanup := setupFourEyesTest(t) defer cleanup() ctx := context.Background() id, _, err := fe.Request(ctx, "test.tenant_delete", "tenant:acme", "alice@example.com") if err != nil { t.Fatalf("request: %v", err) } if err := fe.Confirm(ctx, id, "bob@example.com", "falscher-code"); !errors.Is(err, ErrInvalidCode) { t.Fatalf("erwartet ErrInvalidCode, habe %v", err) } } // Akzeptanzkriterium 2 + Pruefung 2: FOR-UPDATE-Lock unter parallelen // Anfragen race-frei — von zwei gleichzeitigen Bestaetigungsversuchen fuer // denselben Vorgang darf genau einer durchgehen. func TestFourEyes_ConcurrentConfirmIsRaceFree(t *testing.T) { fe, cleanup := setupFourEyesTest(t) defer cleanup() ctx := context.Background() id, code, err := fe.Request(ctx, "test.tenant_delete", "tenant:acme", "alice@example.com") if err != nil { t.Fatalf("request: %v", err) } var wg sync.WaitGroup results := make([]error, 2) confirmers := []string{"bob@example.com", "carol@example.com"} for i := 0; i < 2; i++ { wg.Add(1) go func(i int) { defer wg.Done() results[i] = fe.Confirm(ctx, id, confirmers[i], code) }(i) } wg.Wait() successCount := 0 for _, err := range results { if err == nil { successCount++ } else if !errors.Is(err, ErrAlreadyDecided) { t.Fatalf("unerwarteter fehler: %v", err) } } if successCount != 1 { t.Fatalf("erwartet genau eine erfolgreiche bestaetigung, habe %d", successCount) } }