Files
egress-proxies-operator/internal/provider/errors_test.go
Jan Novak a4a483acbc Add the provider contract, error taxonomy, naming, and config (Step 2)
The Provider interface (Create/Get/Delete/ListByTag), Instance, and
CreateRequest that every cloud backend implements — kept independent of
api/v1alpha1 so this package has no CRD-type coupling.

Error taxonomy (ErrNotFound/ErrQuotaExceeded/ErrTransient/ErrPermanent)
wrapped via a multi-error Unwrap() []error, so errors.Is and errors.As
both work off the same value: the reconciler branches on classification,
logs keep the underlying SDK error. Unclassified errors default to
ErrTransient — retrying is always safer than latching Failed.

Deterministic instance naming (SHA-256 -> base32 -> 16 chars, 22 total
with the "proxy-" prefix) satisfying GCP's RFC1035 name rules with
headroom, and idempotency-tested across 10k UIDs with zero collisions.

--providers-config YAML parsing (config.go) with fail-fast validation:
unknown type, duplicate name, missing gcp.project, mismatched
type/config-block, and strict-mode rejection of unknown keys.

internal/provider/registry/registry.go takes its type->constructor map
as a parameter rather than hardcoding it, so the package has zero import
on internal/provider/mock or internal/provider/gcp (neither exists yet —
mock is Step 3, gcp is Step 8) and compiles today. Explicit wiring moves
to the composition root in cmd/main.go (Step 10).

Deferred internal/provider/metrics.go (the WithMetrics decorator) to
Step 9, where the Prometheus vectors it needs actually get built —
nothing in this step depends on it.

internal/provider at 96.2% coverage, internal/provider/registry at 100%.
make test green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 22:44:22 +02:00

71 lines
2.0 KiB
Go

package provider
import (
"errors"
"fmt"
"testing"
)
// sdkError simulates an underlying provider SDK error type, so tests can
// assert errors.As reaches through the wrapper to it.
type sdkError struct{ code int }
func (e *sdkError) Error() string { return fmt.Sprintf("sdk error %d", e.code) }
func TestError_IsAndAs(t *testing.T) {
t.Parallel()
underlying := &sdkError{code: 429}
err := Wrap(ErrQuotaExceeded, "create", "gcp-eu", "zones/z/instances/x", underlying)
if !errors.Is(err, ErrQuotaExceeded) {
t.Error("errors.Is(err, ErrQuotaExceeded) = false, want true")
}
if errors.Is(err, ErrNotFound) {
t.Error("errors.Is(err, ErrNotFound) = true, want false")
}
var sdk *sdkError
if !errors.As(err, &sdk) {
t.Fatal("errors.As(err, &sdk) = false, want true")
}
if sdk.code != 429 {
t.Errorf("recovered sdkError.code = %d, want 429", sdk.code)
}
}
func TestError_WrapWithNilUnderlying(t *testing.T) {
t.Parallel()
err := Wrap(ErrPermanent, "get", "mock", "id", nil)
if !errors.Is(err, ErrPermanent) {
t.Error("errors.Is(err, ErrPermanent) = false, want true")
}
if err.Error() == "" {
t.Error("Error() returned an empty string")
}
}
func TestClass(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
want error
}{
{"nil returns nil", nil, nil},
{"wrapped not found", Wrap(ErrNotFound, "get", "mock", "id", nil), ErrNotFound},
{"wrapped quota", Wrap(ErrQuotaExceeded, "create", "gcp", "id", nil), ErrQuotaExceeded},
{"wrapped permanent", Wrap(ErrPermanent, "create", "gcp", "id", nil), ErrPermanent},
{"wrapped transient", Wrap(ErrTransient, "create", "gcp", "id", nil), ErrTransient},
{"unclassified defaults to transient", errors.New("boom"), ErrTransient},
{"further-wrapped preserves classification", fmt.Errorf("outer: %w", Wrap(ErrQuotaExceeded, "create", "gcp", "id", nil)), ErrQuotaExceeded},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := Class(tc.err); got != tc.want {
t.Errorf("Class(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}