Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ task license-fix # Add missing license headers
| `httperr` | Wrap errors with HTTP status codes; use `WithCode()`, `Code()`, `New()` |
| `logging` | Pre-configured `*slog.Logger` factory with consistent ToolHive defaults (Alpha) |
| `oci/skills` | OCI artifact types, media types, and registry operations for ToolHive skills (Alpha) |
| `postgres` | PostgreSQL connection pool with optional AWS RDS IAM dynamic auth (Alpha) |
| `recovery` | HTTP panic recovery middleware (Beta) |
| `validation/http` | RFC 7230/8707 compliant HTTP header and URI validation |
| `validation/group` | Group name validation (lowercase alphanumeric, underscore, dash, space) |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The ToolHive ecosystem spans multiple Go repositories, and several of these proj
| `httperr` | Stable | Wrap errors with HTTP status codes |
| `logging` | Alpha | Pre-configured `*slog.Logger` factory with consistent ToolHive defaults |
| `oci/skills` | Alpha | OCI artifact types, media types, and registry operations for skills |
| `postgres` | Alpha | PostgreSQL connection pool with optional AWS RDS IAM dynamic auth |
| `recovery` | Beta | HTTP panic recovery middleware |
| `validation/http` | Stable | RFC 7230/8707 compliant HTTP header and URI validation |
| `validation/group` | Stable | Group name validation |
Expand Down
19 changes: 19 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ go 1.26
require (
github.com/adrg/xdg v0.5.3
github.com/alicebob/miniredis/v2 v2.38.0
github.com/aws/aws-sdk-go-v2/config v1.32.17
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.23
github.com/google/cel-go v0.28.1
github.com/google/go-containerregistry v0.21.6
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/mark3labs/mcp-go v0.54.0
github.com/modelcontextprotocol/registry v1.7.9
github.com/opencontainers/go-digest v1.0.0
Expand All @@ -28,6 +32,18 @@ require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down Expand Up @@ -66,6 +82,9 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/in-toto/attestation v1.1.2 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down
60 changes: 32 additions & 28 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,36 +42,38 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8=
github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.23 h1:jPWBQFmN0v3kiumSS/4ES5rupdfR5jFi5fHwilsX+KY=
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.23/go.mod h1:M0EHmcAard72YjeRQYxTbWkTUY8TXG0WHbtODbM/kzY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/kms v1.49.1 h1:U0asSZ3ifpuIehDPkRI2rxHbmFUMplDA2VeR9Uogrmw=
github.com/aws/aws-sdk-go-v2/service/kms v1.49.1/go.mod h1:NZo9WJqQ0sxQ1Yqu1IwCHQFQunTms2MlVgejg16S1rY=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
Expand Down Expand Up @@ -325,6 +327,7 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
Expand Down Expand Up @@ -430,6 +433,7 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
Expand Down
65 changes: 65 additions & 0 deletions postgres/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package postgres

import (
"context"
"errors"
"fmt"

"github.com/jackc/pgx/v5"
)

// BeforeConnectFn rewrites a connection config (typically by replacing the
// password) immediately before pgx dials. It is the contract used by
// pgxpool.Config.BeforeConnect and by this package's dynamic-auth backends.
type BeforeConnectFn func(ctx context.Context, conn *pgx.ConnConfig) error

// NewAuthToken returns a short-lived password for user produced by the
// dynamic-authentication backend configured in cfg.DynamicAuth. When
// DynamicAuth is nil, the empty string is returned and no error is raised —
// this lets callers fall back to a static Password or PGPASSFILE.
//
// This entry point is intended for short-lived connections (for example,
// running migrations) where pgxpool's BeforeConnect hook is not available.
// For pooled connections, prefer NewDynamicAuthFunc.
func NewAuthToken(ctx context.Context, cfg *Config, user string) (string, error) {
if cfg == nil {
return "", errors.New("config is nil")
}
if cfg.DynamicAuth == nil {
return "", nil
}
if cfg.DynamicAuth.AWSRDSIAM != nil {
return awsRDSIAMToken(ctx, cfg, user)
}
return "", errors.New("dynamicAuth is set but no supported auth method (e.g., awsRdsIam) is configured")
}

// NewDynamicAuthFunc returns a BeforeConnect hook that resolves a fresh
// dynamic-auth credential on every connection attempt. The returned hook
// writes the resolved token into connConfig.Password.
//
// Returns an error when cfg.DynamicAuth is nil — callers that may or may
// not be configured for dynamic auth should branch on cfg.DynamicAuth
// before calling this constructor, or use NewPool which handles both
// shapes transparently.
func NewDynamicAuthFunc(ctx context.Context, cfg *Config, user string) (BeforeConnectFn, error) {
if cfg == nil {
return nil, errors.New("config is nil")
}
if cfg.DynamicAuth == nil {
return nil, errors.New("dynamic authentication is not configured")
}
if cfg.DynamicAuth.AWSRDSIAM != nil {
return awsRDSIAMBeforeConnect(ctx, cfg, user)
}
return nil, errors.New("dynamicAuth is set but no supported auth method (e.g., awsRdsIam) is configured")
}

// wrapAuthError prefixes dynamic-auth errors with a consistent label so they
// are easy to spot in pool startup logs.
func wrapAuthError(backend string, err error) error {
return fmt.Errorf("dynamic auth (%s): %w", backend, err)
}
177 changes: 177 additions & 0 deletions postgres/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package postgres

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewAuthToken(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg *Config
wantToken string
wantErr string
}{
{
name: testCaseNilConfig,
cfg: nil,
wantErr: testErrConfigNil,
},
{
name: "no dynamic auth returns empty token without error",
cfg: &Config{Host: "h", Port: 5432, User: "u", Database: "d"},
wantToken: "",
},
{
name: testCaseNoBackend,
cfg: &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{},
},
wantErr: testErrNoSupportedAuth,
},
{
name: "AWS RDS IAM without region propagates error",
cfg: &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{
AWSRDSIAM: &DynamicAuthAWSRDSIAM{},
},
},
wantErr: testErrRegionMissing,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
token, err := NewAuthToken(t.Context(), tt.cfg, "user")
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
assert.Empty(t, token)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantToken, token)
})
}
}

func TestNewDynamicAuthFunc(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg *Config
wantErr string
}{
{
name: testCaseNilConfig,
cfg: nil,
wantErr: testErrConfigNil,
},
{
name: "no dynamic auth returns explicit error",
cfg: &Config{Host: "h", Port: 5432, User: "u", Database: "d"},
wantErr: "dynamic authentication is not configured",
},
{
name: testCaseNoBackend,
cfg: &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{},
},
wantErr: testErrNoSupportedAuth,
},
{
name: "AWS RDS IAM without region",
cfg: &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{
AWSRDSIAM: &DynamicAuthAWSRDSIAM{},
},
},
wantErr: testErrRegionMissing,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fn, err := NewDynamicAuthFunc(t.Context(), tt.cfg, "user")
require.Error(t, err)
assert.Nil(t, fn)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}

func TestResolveAWSRegion_Static(t *testing.T) {
t.Parallel()
cfg := &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{
AWSRDSIAM: &DynamicAuthAWSRDSIAM{Region: "us-west-2"},
},
}
region, err := resolveAWSRegion(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, "us-west-2", region)
}

func TestResolveAWSRegion_EmptyRegion(t *testing.T) {
t.Parallel()
cfg := &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{AWSRDSIAM: &DynamicAuthAWSRDSIAM{}},
}
_, err := resolveAWSRegion(context.Background(), cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), testErrRegionMissing)
}

// TestResolveAWSRegion_DetectFailsWithoutIMDS exercises the IMDS path. The
// test deadline must elapse before imdsRegionTimeout fires so we get a
// deterministic ctx-cancellation error rather than a flaky one.
func TestResolveAWSRegion_DetectFailsWithoutIMDS(t *testing.T) {
t.Parallel()
cfg := &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{
AWSRDSIAM: &DynamicAuthAWSRDSIAM{Region: "detect"},
},
}
// Use an already-cancelled context so the IMDS call fails immediately
// without depending on whether 169.254.169.254 is routable in CI.
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := resolveAWSRegion(ctx, cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "IMDS")
}

// TestAwsRDSIAMBeforeConnect_ReturnsHookForStaticRegion verifies the
// constructor returns a non-nil hook when the region is statically
// configured. Actually invoking the hook would require AWS credentials and
// is out of scope for unit tests.
func TestAwsRDSIAMBeforeConnect_ReturnsHookForStaticRegion(t *testing.T) {
t.Parallel()
cfg := &Config{
Host: "h", Port: 5432, User: "u", Database: "d",
DynamicAuth: &DynamicAuthConfig{
AWSRDSIAM: &DynamicAuthAWSRDSIAM{Region: "us-west-2"},
},
}
fn, err := awsRDSIAMBeforeConnect(context.Background(), cfg, "appuser")
require.NoError(t, err)
assert.NotNil(t, fn)
}
Loading
Loading