Skip to content

feat(modules/dex): add Dex OIDC provider module#3659

Merged
mdelapenya merged 24 commits into
testcontainers:mainfrom
guilycst:subtree-sync/ecc9b3dd7df0
May 11, 2026
Merged

feat(modules/dex): add Dex OIDC provider module#3659
mdelapenya merged 24 commits into
testcontainers:mainfrom
guilycst:subtree-sync/ecc9b3dd7df0

Conversation

@guilycst
Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a new dex module under modules/dex/, providing a testcontainers wrapper for Dex, a CNCF-sandbox OIDC provider. Tests can spin up a real Dex instance and exercise OAuth2/OIDC flows end-to-end instead of mocking the token endpoint.

Shape

  • DexContainer embeds testcontainers.Container.
  • Run(ctx, image, opts...) entry point (tc-go v0.34+ convention).
  • Client / User are opaque value types (private fields); construct with NewClient(id, opts...) (Client, error) and NewUser(email, username, password, opts...) (User, error). Invalid input is rejected at call-site rather than at Run.
  • Client options are variadic where applicable (WithClientRedirectURIs(...string), WithClientGrantTypes(...string)).
  • Module options return error (type Option func(*options) error); Run propagates option errors so blank id/name/URL inputs surface immediately.
  • Typed Storage (StorageSQLite / StorageMemory) — no stringly-typed storage kind.
  • WithLogLevel takes slog.Level; internally mapped to Dex's level vocabulary.
  • GRPCEndpoint(ctx) (string, error) — errors propagate from the Docker API; no private shadow method that swallows them.
  • Runtime gRPC admin API: AddClient, RemoveClient, AddUser, RemoveUser with ErrClientExists / ErrUserExists sentinels.
  • Supported grants pre-start via WithClientGrantTypes: authorization_code, refresh_token, password. client_credentials behind WithEnableClientCredentials() (Dex ≥ v2.46.0 or dexidp/dex:master).

Chicken-and-egg handling

Dex refuses to start without a YAML config, but the default issuer depends on the host-mapped port, only known after the container starts. The module follows the same pattern as the Kafka module: override the image entrypoint with a sh -c "while [ ! -f /etc/dex/dex.yml ]; do sleep 0.1; done; exec dex serve /etc/dex/dex.yml" loop, then write the rendered YAML in a PostStarts lifecycle hook via CopyToContainer. Readiness probes (/dex/.well-known/openid-configuration + gRPC listener) run inside the hook after the write.

YAML rendering

Config is marshaled with gopkg.in/yaml.v3 from a typed struct (not text/template) so every string field is escaped by the marshaler — no injection risk from client names / user emails / connector IDs. Bcrypt hashing uses cost 10 (Dex enforces ≥ 10).

Why is it important?

  • Tests that need OIDC today either (a) mock the token endpoint (diverges from prod behavior; JWKS/signatures aren't real), (b) use navikt/mock-oauth2-server (minimal config surface, non-real grant semantics), or (c) hand-roll a Dex testcontainer with copy-pasted boilerplate.
  • A first-party dex module closes the prod/test parity gap for every Go project that ships Dex or stands up a Dex-compatible OIDC provider for its tests.

Related issues

  • Supersedes feat: add Dex IdP module #3216 (closed unmerged, April 2025). This PR internalises the review feedback from that thread: constructor-based Client/User with private fields + error return (avoids zero-value struct surprises), func(*options) error for all module options (validation at option-site), slog.Level for logging, typed Storage, single GRPCEndpoint(ctx) (string, error), variadic list setters, no hand-rolled image-tag semver parsing in the module.

How to test this PR

cd modules/dex
go test -v -timeout 20m ./...

30 tests (unit + integration) including YAML rendering with injection safety, container lifecycle (default + WithIssuer), discovery doc sanity, gRPC add/remove for clients and users (with sentinel idempotency), auth_code + password connector, refresh_token via offline_access, multiple redirect URIs on one client, ROPC, multi-client on one instance, mockCallback connector, runtime gRPC add end-to-end, WithIssuer cross-container reachability via network.New alias, ID token verification via coreos/go-oidc/v3, client_credentials rejected by local connectors without flag, client_credentials issued when flag is set on :master, WithLogger captures container output.

Integration tests pull dexidp/dex:v2.45.1; one test (TestClientCredentials_WithFeatureFlag) pulls dexidp/dex:master to exercise the CC grant feature flag. Full suite runs in ~85s against a warm Docker cache.

go vet ./... clean, gofmt -l . clean, two runnable godoc examples (ExampleRun_authorizationCode, ExampleRun_passwordGrant).

Follow-ups (intentionally deferred)

  • gRPC over TLSWithGRPCTLS(...) option. Dex gRPC plaintext is fine inside a test network; not day-one scope.
  • Raw connector escape hatchWithRawConnectors([]map[string]any) for LDAP / SAML / upstream OIDC. Can land when a real consumer asks.
  • device_code and token-exchange grants — declarable in YAML today, but no integration test; flip to first-class support once a consumer appears.

guilycst and others added 3 commits April 21, 2026 00:39
Upstream-merge-grade module with: Run() + endpoint getters, functional options
(WithClient, WithUser, WithConnector, WithIssuer, WithSkipApprovalScreen,
WithStorage, WithLogger, WithLogLevel, WithEnableClientCredentials), gRPC
admin mutation (AddClient, RemoveClient, AddUser, RemoveUser), slog-backed
log consumer, password DB + mockCallback connectors, and feature-flag support
for the client_credentials grant.

36 tests cover YAML rendering, container lifecycle, gRPC mutation, auth_code
+ refresh + multi-redirect, ROPC, multi-client, mock connector, cross-container
reachability via network alias, and consumer JWT verification via coreos/go-oidc.

Monorepo origin: github.com/guilycst/totvs-work PR testcontainers#19 (merge commit 0764128de).
… distribution

While the module incubates on the fork before upstream merge, consumers
import it at github.com/guilycst/testcontainers-go/modules/dex so Go can
resolve the package from the fork repo without a replace directive.

Reverts to github.com/testcontainers/testcontainers-go/modules/dex once
upstreamed.
…factor, constructors, validation, and logger support
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 24, 2026

Deploy Preview for testcontainers-go ready!

Name Link
🔨 Latest commit 95679fc
🔍 Latest deploy log https://app.netlify.com/projects/testcontainers-go/deploys/6a016b488338be00081491b7
😎 Deploy Preview https://deploy-preview-3659--testcontainers-go.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Testcontainers Go module for Dex (OIDC provider): exported types, option API, config renderer, container runner, gRPC admin client, logging consumer, docs, Make/VSCode/Dependabot entries, examples, and extensive unit/integration tests.

Changes

Cohort / File(s) Summary
Repo & Editor config
​.github/dependabot.yml, ​.vscode/.testcontainers-go.code-workspace
Enable Dependabot monitoring for /modules/dex and add VS Code workspace folder for the new module.
Docs & site nav
docs/modules/dex.md, mkdocs.yml, modules/dex/README.md
Add comprehensive Dex module documentation, README, and register the docs page in MkDocs navigation.
Build targets & module manifest
modules/dex/Makefile, modules/dex/go.mod
Add module-specific make targets and a new Go module with pinned dependencies for Dex, gRPC, OIDC, and tests.
Public models & validation
modules/dex/types.go
Introduce exported types Client, User, enums (ConnectorType, Storage), constructors with validation, and sentinel errors (ErrClientExists, ErrUserNotFound, ErrNoAuthSource, etc.).
Options API
modules/dex/options.go, modules/dex/options_test.go
Add functional Option type, With* helpers, defaults, Customize adapter for testcontainers, and tests validating guards and behavior.
Config rendering
modules/dex/config.go, modules/dex/config_test.go
Render Dex YAML from options (issuer, storage, clients, users), bcrypt-hash passwords at cost 10, compute grants, marshal YAML; tests cover validation, YAML injection safety, and log-level mapping.
Runtime container & helpers
modules/dex/dex.go, modules/dex/examples_test.go
Add Run(ctx,img,opts...) returning *DexContainer, copy generated config, set env flags, wait for discovery and gRPC readiness, and expose endpoint helper methods plus doc examples.
gRPC admin client
modules/dex/grpc.go
Add DexContainer methods AddClient, RemoveClient, AddUser, RemoveUser with per-call dial, error mapping to sentinel errors, and contextual error wrapping.
Logging bridge
modules/dex/log.go, modules/dex/log_test.go
Add slogConsumer parsing Dex logfmt into slog records, map textual levels (promote STDERR to WARN), and tests for parsing and level mapping.
Integration & test helpers
modules/dex/dex_test.go, modules/dex/dextest_helpers_test.go, modules/dex/examples_test.go
Add comprehensive integration tests exercising OIDC/OAuth flows, runtime gRPC ops, token verification via go-oidc, login helper, and doc-style examples.
Test scaffolding
modules/dex/config_test.go, modules/dex/options_test.go, modules/dex/log_test.go, modules/dex/dextest_helpers_test.go, modules/dex/examples_test.go
Extensive unit and integration tests validating config rendering, options, logging parsing, end-to-end flows, and helpers.

Sequence Diagram(s)

sequenceDiagram
    participant Test
    participant Run as dex.Run()
    participant Config as config.render()
    participant Container as DexContainer
    participant Dex as Dex Server
    participant Wait as Readiness Check

    Test->>Run: Run(ctx, image, opts...)
    Run->>Config: render(options)
    Config->>Config: validate issuer & auth sources
    Config->>Config: bcrypt-hash users & marshal YAML
    Config-->>Run: dex.yml
    Run->>Container: create container, copy dex.yml, set env (client_credentials flag if enabled)
    Container->>Dex: start
    Run->>Wait: poll discovery endpoint & dial gRPC
    Wait->>Dex: GET /.well-known/openid-configuration
    Wait->>Dex: Dial gRPC admin port
    Dex-->>Wait: ready
    Wait-->>Run: healthy
    Run-->>Test: return *DexContainer
Loading
sequenceDiagram
    participant Test
    participant DexC as DexContainer
    participant OAuth as oauth2.Config
    participant Dex as Dex Provider
    participant Token as Token Endpoint

    Test->>DexC: IssuerURL() / AuthEndpoint() / TokenEndpoint()
    DexC-->>Test: URLs
    Test->>OAuth: build auth URL
    OAuth-->>Test: redirect to Dex /auth
    Test->>Dex: GET /auth (login)
    Dex-->>Test: login form (password or mock)
    Test->>Dex: POST credentials
    Dex-->>Test: redirect with code to configured redirect URI
    Test->>OAuth: Exchange(ctx, code)
    OAuth->>Token: POST /token
    Token-->>OAuth: id_token + access_token
    OAuth-->>Test: tokens
    Test->>Test: verify ID token claims (using OIDC verifier)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

feature

Suggested reviewers

  • mdelapenya

Poem

🐰 I stitched a Dex config bright,

bcrypt hops beneath moonlight,
gRPC taps, logs sing in tune,
tokens twinkle—tests commune,
docs and examples nibble delight. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new Dex OIDC provider module to the testcontainers-go project.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining what the Dex module does, its design rationale, and how to test it.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@guilycst
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit.

@guilycst guilycst marked this pull request as ready for review April 24, 2026 07:50
@guilycst guilycst requested a review from a team as a code owner April 24, 2026 07:50
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (16)
modules/dex/dextest_helpers_test.go (1)

45-65: Nit: prefer http.MethodGet / http.MethodPost over string literals.

Idiomatic Go uses the http.Method* constants in net/http for request methods.

Proposed diff
-	req, err := http.NewRequestWithContext(ctx, "GET", authURL, nil)
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL, nil)
@@
-	postReq, err := http.NewRequestWithContext(ctx, "POST", actionURL.String(), strings.NewReader(form.Encode()))
+	postReq, err := http.NewRequestWithContext(ctx, http.MethodPost, actionURL.String(), strings.NewReader(form.Encode()))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dextest_helpers_test.go` around lines 45 - 65, Replace the string
literals "GET" and "POST" passed to http.NewRequestWithContext with the net/http
constants http.MethodGet and http.MethodPost respectively; locate the calls to
http.NewRequestWithContext that create req and postReq in
dextest_helpers_test.go and update their method arguments to use http.MethodGet
and http.MethodPost to follow idiomatic Go usage.
modules/dex/options.go (4)

147-152: WithLogger(nil) clobbers a previously-set logger.

Because options are applied sequentially from the slice, calling WithLogger(logger) followed by WithLogger(nil) will unset the logger silently (and dex.go then skips LogConsumers because settings.logger != nil is false). That matches the godoc ("When nil or not called, Dex container logs are discarded"), but it makes the nil argument a destructive action rather than a no-op. If you prefer the no-op reading, guard with if logger != nil { o.logger = logger }. Otherwise leave as-is and call out in the doc that order-dependent overrides apply.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/options.go` around lines 147 - 152, The WithLogger option
currently unconditionally assigns o.logger, so calling WithLogger(nil) will
unset a previously-set logger; update the WithLogger function (the Option
returned by WithLogger) to only assign o.logger when the provided logger
argument is non-nil (guard with if logger != nil { ... }) so passing nil becomes
a no-op; reference the WithLogger function and the options struct (o.logger)
when making this change, or alternatively document that WithLogger(nil)
intentionally overrides earlier values if you decide to keep the current
behavior.

86-100: WithConnector(ConnectorPassword, …) swallows blank id/name silently.

The short-circuit on Line 88–89 returns before the blank-id/name checks, so WithConnector(ConnectorPassword, "", "") succeeds (as the test on options_test.go:61 confirms). That's probably intentional — the password DB is auto-configured elsewhere — but a caller who expects the blank-field guard to apply uniformly may be surprised. If the behavior is deliberate, consider a one-line godoc addition: "For ConnectorPassword, id/name are ignored and validation is skipped." Otherwise, move the blank checks above the early return.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/options.go` around lines 86 - 100, Update the behavior
documentation for WithConnector to clarify that when t == ConnectorPassword the
id/name parameters are ignored and validation is intentionally skipped; add a
one-line Godoc comment above the WithConnector function stating "For
ConnectorPassword, id and name are ignored and blank-field validation is
skipped." This makes the early return (if t == ConnectorPassword) explicit to
callers without changing the existing logic used in options_test.go.

40-54: Customize no-op means Option can't be forwarded through generic tc-go helpers.

Any wrapper that only calls Customize on customizers (rather than type-asserting to Option) will silently drop your settings — Run is the only place that actually applies Option functions. This is documented ("Real state mutation happens in Run"), but a user who reasonably passes dex.WithClient(...) to, say, a network-level customization pipeline that re-emits customizers via Customize will get zero error and zero effect. Consider at minimum adding a short godoc NOTE: that Options must be passed directly to dex.Run, not to arbitrary tc-go helpers. Low priority, more of a trap than a bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/options.go` around lines 40 - 54, The current Option type
implements Customize as a no-op (Option.Customize) so any generic testcontainers
helper that only calls Customize will silently drop dex Options; update the
documentation and API surface to prevent this trap by adding a clear godoc NOTE
on the Option type (and/or Customize) that states Options (e.g., dex.WithClient)
must be passed directly to dex.Run and not forwarded through generic tc-go
customizer pipelines, and include a short example or sentence indicating that
real mutation happens only in Run; reference Option, Customize, Run and a
typical Option helper like WithClient so callers can find and apply the
guidance.

164-177: Module doesn't validate image tag — fragile for users.

Per PR objectives and README, WithEnableClientCredentials() silently yields a runtime unsupported_grant_type error on Dex < v2.46.0. The option sets an env var unconditionally, so users on v2.45.x get no compile/runtime hint that they picked an incompatible image. A best-effort warning when Dex's discovery doc doesn't advertise client_credentials in grant_types_supported would catch this early — but that crosses into Run/lifecycle territory and is reasonably deferred given the known-limitation caveat in the docs.

modules/dex/config.go (1)

187-197: newUUIDv4 panics on randomness failure in a container-startup path.

render is invoked from the PostStart hook (per modules/dex/dex.go), and AddUser uses the same helper on the request goroutine (modules/dex/grpc.go:96). A panic on crypto/rand.Read failure propagates as an unrecoverable goroutine crash rather than a wrapped error the caller can retry. Returning (string, error) and surfacing via fmt.Errorf("dex: read randomness: %w", err) would keep error handling uniform with the rest of the module (both call sites already return errors). Failure is vanishingly rare on modern kernels, so treat this as cleanup rather than a bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/config.go` around lines 187 - 197, The function newUUIDv4
currently panics on crypto/rand.Read failure; change its signature to
newUUIDv4() (string, error) and return an empty string plus fmt.Errorf("dex:
read randomness: %w", err) on read failure instead of panicking, and on success
return the UUID string with nil error; update callers (render and AddUser) to
handle the error return (propagate it up or wrap it where appropriate) so
failures become recoverable errors rather than panics.
modules/dex/dex_test.go (4)

538-547: Ignored error from cookiejar.New.

cookiejar.New(nil) never returns a non-nil error today, but ignoring it here is inconsistent with the rest of the test file, which uses require.NoError on every call. Swapping in require.NoError(t, err) (or t.Fatal) keeps the pattern uniform and guards against future signature changes.

🧹 Proposed fix
-	jar, _ := cookiejar.New(nil)
+	jar, err := cookiejar.New(nil)
+	require.NoError(t, err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 538 - 547, The call to cookiejar.New
ignores its error; change jar, _ := cookiejar.New(nil) to capture the error
(jar, err := cookiejar.New(nil)) and assert no error using require.NoError(t,
err) before using jar so the test follows the file's pattern and will fail if
the constructor signature changes; update any downstream usage in the test that
references jar if needed.

198-212: Log-capture poll window may flake under load.

The 5 s deadline starts running the instant Run returns, but PostStart's readiness probe can satisfy HTTP/gRPC probes before Dex's "listening on" line has propagated all the way through the LogConsumer goroutine (especially on slow/loaded CI runners). Consider bumping this to 30 s (aligned with the in-flight startup timeouts in dex.go) or plumbing a channel-based signal from the consumer instead of polling a string — otherwise any slowdown surfaces as a flake rather than a real bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 198 - 212, The test's 5s polling
deadline (deadline := time.Now().Add(5 * time.Second)) can flake under load
because Run returns before the LogConsumer goroutine has propagated the
"listening on" line; change the deadline to 30s (e.g., time.Now().Add(30 *
time.Second)) or, preferably, wire a deterministic signal from the LogConsumer
to the test (a done/ready channel the consumer closes when it has emitted the
"listening on" line) and make the test wait on that channel instead of polling
buf.String() — locate the polling loop around buf.String(), Run, PostStart and
LogConsumer and implement one of these two fixes.

556-573: body read-and-discard looks like dead code.

Lines 552–553 drain the GET response and Lines 559 does _ = body. Either use the body to confirm the form renders as expected (asserting state machine progression) or drop the read entirely — the current form is confusing and will prompt future "is this needed?" questions. The manual login dance here is already documented as "do it manually to avoid require.NoError aborting on the expected failure"; pruning the dead read would sharpen that intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 556 - 573, The GET response body is
read into the variable named "body" and then discarded ("_ = body"), which is
dead code; remove the initial read-and-discard (the io.ReadAll call that sets
"body" and the "_ = body" line) or instead assert on that body to validate the
login form; specifically either delete the code that reads the GET response into
"body" (keep only the POST using client.Post to loginURL and the existing checks
on "body2"/r2) or replace the discard with an assertion that the GET response
contains expected form fields before proceeding to client.Post, referencing the
variables "body", "body2", "client.Post", and "loginURL" to locate the code to
change.

31-31: dexidp/dex:master tag introduces non-deterministic CI behavior.

Using the floating :master tag means the image contents change without any version pin, so this single test is vulnerable to upstream regressions, breaking image layouts, or Docker Hub rate limits unrelated to this PR. The PR notes call this out as a deliberate trade-off until v2.46.0 ships. Two hardening options:

  • Gate the test behind an env var (e.g., skip unless DEX_TEST_MASTER=1 is set) so the default CI run is deterministic and the master-image test is opt-in.
  • Once v2.46.0 is released, replace dexImageWithCC with the pinned tag and delete the feature-flag caveat in the README.

Also applies to: 620-644

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` at line 31, The test is using an unstable floating
image tag via the dexImageWithCC constant; make running that master-image test
opt-in by gating it behind an env var: in dex_test.go detect
os.Getenv("DEX_TEST_MASTER") != "1" and call t.Skipf(...) at the start of the
test(s) that reference dexImageWithCC (or in TestMain to skip those cases),
keeping the existing dexImageWithCC name but ensuring CI doesn't run it by
default; when v2.46.0 is released replace dexImageWithCC with the pinned release
tag and remove the env-var gating and README note.
modules/dex/README.md (2)

52-58: Documentation will age quickly — "not yet released at time of writing."

As of April 2026, the latest tagged Dex release is still v2.45.1 (cut March 3, 2026), so this wording remains accurate for now. Once v2.46.0 lands, please revisit this paragraph (and the parallel Known limitations entry on Lines 160–162) so new readers don't have to reason about stale tense. Consider rewording to something like "requires Dex ≥ v2.46.0 (or dexidp/dex:master until that release ships)" which stays correct either way.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/README.md` around lines 52 - 58, Update the README wording to
avoid time-sensitive phrasing: change the sentence that currently reads
"requires Dex ≥ v2.46.0 (not yet released at time of writing)..." to a timeless
form such as "requires Dex ≥ v2.46.0 (or use dexidp/dex:master until that
release ships)" and apply the same rewording to the corresponding Known
limitations entry; keep the note that callers must pin a compatible image and
preserve the reference to WithEnableClientCredentials() so readers can find the
option.

142-145: Signature style inconsistency in endpoint getters list.

GRPCEndpoint(ctx) (string, error) is shown with its full signature while the others are bare names. Consider either dropping the signature (it's documented in godoc) or showing all signatures for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/README.md` around lines 142 - 145, The README endpoint getters
list is inconsistent: all functions are listed by bare name (IssuerURL,
ConfigEndpoint, JWKSEndpoint, TokenEndpoint, AuthEndpoint) but GRPCEndpoint is
shown with a full signature (GRPCEndpoint(ctx) (string, error)); update the list
to be consistent by either removing the signature from GRPCEndpoint (make it
just GRPCEndpoint) or by adding full signatures for all entries (e.g.,
IssuerURL() string, ConfigEndpoint() string, JWKSEndpoint() string,
TokenEndpoint() string, AuthEndpoint() string, GRPCEndpoint(ctx) (string,
error)), and ensure the chosen style matches godoc conventions used elsewhere in
the repo.
modules/dex/examples_test.go (1)

71-78: Defer ordering inconsistent with the first example.

ExampleRun_authorizationCode places defer testcontainers.TerminateContainer(c) immediately after dex.Run (Line 35) — the canonical testcontainers-go pattern, which works because TerminateContainer guards against a nil container. Here the defer is registered only after the error check (Line 78), so a partially-initialized container leaked by a failed Run would never be cleaned up. Move the defer above the if err != nil to match the first example.

🧹 Proposed fix
 	c, err := dex.Run(ctx, "dexidp/dex:v2.45.1",
 		dex.WithClient(svc),
 		dex.WithUser(user),
 	)
+	defer testcontainers.TerminateContainer(c)
 	if err != nil {
 		log.Fatalf("run: %v", err)
 	}
-	defer testcontainers.TerminateContainer(c)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/examples_test.go` around lines 71 - 78, The defer ordering can
leak a partially-initialized container if dex.Run returns an error; move the
defer testcontainers.TerminateContainer(c) to immediately follow the call to
dex.Run (right after obtaining c and err) and before the if err != nil check so
the container is always registered for cleanup even when dex.Run fails; locate
the callsite using the dex.Run invocation and the variable c to update the defer
placement.
modules/dex/log.go (1)

97-109: Quoted-value parser retains backslash escapes.

The tokenizer skips \x inside quoted values (so the terminating " is handled correctly), but the returned slice line[vStart:i] still contains the raw backslashes — the comment on Line 50 says values are "unquoted," which implies unescaping. For Dex's typical level=info msg="..." lines this is harmless, but a msg containing \" or \\ will round-trip with the escapes intact in the resulting slog.Attr. Acceptable if intentional; otherwise a short strings.NewReplacer("\\\"", "\"", "\\\\", "\\").Replace(...) on quoted values would fix it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/log.go` around lines 97 - 109, The quoted-value parser currently
appends the raw substring line[vStart:i] into the kv entry, retaining backslash
escapes; change this so the extracted quoted value is unescaped before
appending. Locate the code that builds kv{k, line[vStart:i]} (uses variables k,
vStart, i and the kv type) and replace the raw slice with an unescaped string
(for example run it through strings.NewReplacer("\\\"", "\"", "\\\\",
"\\").Replace(...) or use strconv.Unquote on `"`+line[vStart:i]+`"`), then
append kv{k, unescapedValue} to out.
modules/dex/types.go (1)

86-96: Validate grant-type values at construction time.

The docstring enumerates four valid values (authorization_code, refresh_token, client_credentials, password) but the option only rejects blanks. Typos slip through NewClient and surface much later — either at Run (when Dex rejects the rendered YAML) or, worse, as unsupported_grant_type at token exchange, well after the test has already set up state. Since one of the stated goals of the constructor is to "surface invalid configuration at call-site rather than at Run", enforcing the accepted set here aligns with that intent.

♻️ Proposed fix
+var validGrantTypes = map[string]struct{}{
+	"authorization_code": {},
+	"refresh_token":      {},
+	"client_credentials": {},
+	"password":           {},
+}
+
 func WithClientGrantTypes(grants ...string) ClientOption {
 	return func(c *Client) error {
 		for _, g := range grants {
 			if g == "" {
 				return errors.New("dex: client grant type must not be blank")
 			}
+			if _, ok := validGrantTypes[g]; !ok {
+				return fmt.Errorf("dex: unsupported grant type %q", g)
+			}
 		}
 		c.grantTypes = append(c.grantTypes, grants...)
 		return nil
 	}
 }

(Adds fmt to the import set.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/types.go` around lines 86 - 96, In WithClientGrantTypes (the
ClientOption constructor), validate each provided grant string against the
allowed set
{"authorization_code","refresh_token","client_credentials","password"} (in
addition to rejecting blank values) and return an error immediately for any
invalid value so typos are surfaced at construction; update the error message to
include the offending grant value(s) and ensure you append to c.grantTypes only
after validation (use the function name WithClientGrantTypes and the
Client.grantTypes field as references).
modules/dex/grpc.go (1)

67-69: Add ErrClientNotFound / ErrUserNotFound sentinels for symmetry with the "exists" sentinels.

AddClient / AddUser return the ErrClientExists / ErrUserExists sentinels so callers can errors.Is(...), but the matching RemoveClient / RemoveUser "not found" paths return ad-hoc formatted errors. Callers that want to tolerate "already gone" on cleanup have to string-match. Promoting these to sentinels (wrapped with the id/email for context) keeps the runtime admin API consistent.

♻️ Proposed change (types.go + grpc.go)

In modules/dex/types.go:

 var (
 	// ErrClientExists is returned by AddClient when a client with the given
 	// ID is already registered.
 	ErrClientExists = errors.New("dex: client already exists")
 	// ErrUserExists is returned by AddUser when a user with the given email
 	// is already registered.
 	ErrUserExists = errors.New("dex: user already exists")
+	// ErrClientNotFound is returned by RemoveClient when no client matches
+	// the given ID.
+	ErrClientNotFound = errors.New("dex: client not found")
+	// ErrUserNotFound is returned by RemoveUser when no user matches the
+	// given email.
+	ErrUserNotFound = errors.New("dex: user not found")

In modules/dex/grpc.go:

-	if resp.NotFound {
-		return fmt.Errorf("dex: client %q not found", id)
-	}
+	if resp.NotFound {
+		return fmt.Errorf("%w: %q", ErrClientNotFound, id)
+	}
-	if resp.NotFound {
-		return fmt.Errorf("dex: user %q not found", email)
-	}
+	if resp.NotFound {
+		return fmt.Errorf("%w: %q", ErrUserNotFound, email)
+	}

Also applies to: 131-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/grpc.go` around lines 67 - 69, Create exported sentinel errors
ErrClientNotFound and ErrUserNotFound in modules/dex/types.go (e.g. var
ErrClientNotFound = errors.New("client not found"), var ErrUserNotFound =
errors.New("user not found")), then update the "not found" return paths in
modules/dex/grpc.go (the branches that currently do `return fmt.Errorf("dex:
client %q not found", id)` and the analogous user case around lines referenced)
to wrap the sentinel so callers can use errors.Is — e.g. return fmt.Errorf("dex:
client %q not found: %w", id, ErrClientNotFound) and similarly for users (and
update the second occurrence at the other referenced lines to use
ErrUserNotFound/ErrClientNotFound as appropriate).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/modules/dex.md`:
- Around line 15-17: Update the fenced code block that contains the line "go get
github.com/testcontainers/testcontainers-go/modules/dex" to include a language
identifier (use "bash") so the fence becomes ```bash; locate the fenced block in
docs/modules/dex.md and change the opening backticks to include bash for
consistent syntax highlighting with other module docs.

In `@modules/dex/config_test.go`:
- Around line 248-261: The doc comment for dexLogLevel in config.go is
incorrect; update the comment near the dexLogLevel description to state that
values between slog's fixed levels round up (not down) — e.g. change the example
from "slog.LevelInfo+1 → 'info'" to "slog.LevelInfo+1 → 'warn'" or alternatively
use "slog.LevelInfo-1 → 'debug'" so the comment matches the dexLogLevel
implementation and the TestDexLogLevel expectations.

In `@modules/dex/config.go`:
- Around line 77-82: defaultGrantTypes currently always includes
"client_credentials" which breaks Dex v2.45.1 unless the env flag is set; update
the logic so "client_credentials" is omitted from defaultGrantTypes and only
added when the config rendering uses o.enableClientCredentials=true. Concretely,
remove "client_credentials" from the static var defaultGrantTypes and, in the
config build/render path that references defaultGrantTypes (the code using
o.enableClientCredentials), append "client_credentials" into the grantTypes
slice only when o.enableClientCredentials is true so the emitted
oauth2.grantTypes matches runtime capability.

In `@modules/dex/dex.go`:
- Around line 87-93: The readiness probe currently hard-codes
"/dex/.well-known/openid-configuration"; change it to compute the discovery path
from the configured issuer: parse the issuer URL (using net/url) and build
discoveryPath := strings.TrimRight(parsed.Path, "/") +
"/.well-known/openid-configuration" (ensure it starts with "/" if parsed.Path is
empty). Replace the literal in wait.ForHTTP(...) with that discoveryPath (keep
WithPort(httpPort) and WithStartupTimeout), and add imports for "net/url" and
"strings"; reference the code that constructs the ready wait (the wait.ForAll /
wait.ForHTTP call) and the variable that holds the configured issuer (the
function/variable supplying the issuer value used by WithIssuer).

In `@modules/dex/README.md`:
- Around line 11-13: The fenced code block containing the line "go get
github.com/testcontainers/testcontainers-go/modules/dex" lacks a language hint;
update that block to use a language specifier (e.g., ```bash) so markdownlint
MD040 is satisfied and syntax highlighting matches other code blocks in the
README.

---

Nitpick comments:
In `@modules/dex/config.go`:
- Around line 187-197: The function newUUIDv4 currently panics on
crypto/rand.Read failure; change its signature to newUUIDv4() (string, error)
and return an empty string plus fmt.Errorf("dex: read randomness: %w", err) on
read failure instead of panicking, and on success return the UUID string with
nil error; update callers (render and AddUser) to handle the error return
(propagate it up or wrap it where appropriate) so failures become recoverable
errors rather than panics.

In `@modules/dex/dex_test.go`:
- Around line 538-547: The call to cookiejar.New ignores its error; change jar,
_ := cookiejar.New(nil) to capture the error (jar, err := cookiejar.New(nil))
and assert no error using require.NoError(t, err) before using jar so the test
follows the file's pattern and will fail if the constructor signature changes;
update any downstream usage in the test that references jar if needed.
- Around line 198-212: The test's 5s polling deadline (deadline :=
time.Now().Add(5 * time.Second)) can flake under load because Run returns before
the LogConsumer goroutine has propagated the "listening on" line; change the
deadline to 30s (e.g., time.Now().Add(30 * time.Second)) or, preferably, wire a
deterministic signal from the LogConsumer to the test (a done/ready channel the
consumer closes when it has emitted the "listening on" line) and make the test
wait on that channel instead of polling buf.String() — locate the polling loop
around buf.String(), Run, PostStart and LogConsumer and implement one of these
two fixes.
- Around line 556-573: The GET response body is read into the variable named
"body" and then discarded ("_ = body"), which is dead code; remove the initial
read-and-discard (the io.ReadAll call that sets "body" and the "_ = body" line)
or instead assert on that body to validate the login form; specifically either
delete the code that reads the GET response into "body" (keep only the POST
using client.Post to loginURL and the existing checks on "body2"/r2) or replace
the discard with an assertion that the GET response contains expected form
fields before proceeding to client.Post, referencing the variables "body",
"body2", "client.Post", and "loginURL" to locate the code to change.
- Line 31: The test is using an unstable floating image tag via the
dexImageWithCC constant; make running that master-image test opt-in by gating it
behind an env var: in dex_test.go detect os.Getenv("DEX_TEST_MASTER") != "1" and
call t.Skipf(...) at the start of the test(s) that reference dexImageWithCC (or
in TestMain to skip those cases), keeping the existing dexImageWithCC name but
ensuring CI doesn't run it by default; when v2.46.0 is released replace
dexImageWithCC with the pinned release tag and remove the env-var gating and
README note.

In `@modules/dex/dextest_helpers_test.go`:
- Around line 45-65: Replace the string literals "GET" and "POST" passed to
http.NewRequestWithContext with the net/http constants http.MethodGet and
http.MethodPost respectively; locate the calls to http.NewRequestWithContext
that create req and postReq in dextest_helpers_test.go and update their method
arguments to use http.MethodGet and http.MethodPost to follow idiomatic Go
usage.

In `@modules/dex/examples_test.go`:
- Around line 71-78: The defer ordering can leak a partially-initialized
container if dex.Run returns an error; move the defer
testcontainers.TerminateContainer(c) to immediately follow the call to dex.Run
(right after obtaining c and err) and before the if err != nil check so the
container is always registered for cleanup even when dex.Run fails; locate the
callsite using the dex.Run invocation and the variable c to update the defer
placement.

In `@modules/dex/grpc.go`:
- Around line 67-69: Create exported sentinel errors ErrClientNotFound and
ErrUserNotFound in modules/dex/types.go (e.g. var ErrClientNotFound =
errors.New("client not found"), var ErrUserNotFound = errors.New("user not
found")), then update the "not found" return paths in modules/dex/grpc.go (the
branches that currently do `return fmt.Errorf("dex: client %q not found", id)`
and the analogous user case around lines referenced) to wrap the sentinel so
callers can use errors.Is — e.g. return fmt.Errorf("dex: client %q not found:
%w", id, ErrClientNotFound) and similarly for users (and update the second
occurrence at the other referenced lines to use
ErrUserNotFound/ErrClientNotFound as appropriate).

In `@modules/dex/log.go`:
- Around line 97-109: The quoted-value parser currently appends the raw
substring line[vStart:i] into the kv entry, retaining backslash escapes; change
this so the extracted quoted value is unescaped before appending. Locate the
code that builds kv{k, line[vStart:i]} (uses variables k, vStart, i and the kv
type) and replace the raw slice with an unescaped string (for example run it
through strings.NewReplacer("\\\"", "\"", "\\\\", "\\").Replace(...) or use
strconv.Unquote on `"`+line[vStart:i]+`"`), then append kv{k, unescapedValue} to
out.

In `@modules/dex/options.go`:
- Around line 147-152: The WithLogger option currently unconditionally assigns
o.logger, so calling WithLogger(nil) will unset a previously-set logger; update
the WithLogger function (the Option returned by WithLogger) to only assign
o.logger when the provided logger argument is non-nil (guard with if logger !=
nil { ... }) so passing nil becomes a no-op; reference the WithLogger function
and the options struct (o.logger) when making this change, or alternatively
document that WithLogger(nil) intentionally overrides earlier values if you
decide to keep the current behavior.
- Around line 86-100: Update the behavior documentation for WithConnector to
clarify that when t == ConnectorPassword the id/name parameters are ignored and
validation is intentionally skipped; add a one-line Godoc comment above the
WithConnector function stating "For ConnectorPassword, id and name are ignored
and blank-field validation is skipped." This makes the early return (if t ==
ConnectorPassword) explicit to callers without changing the existing logic used
in options_test.go.
- Around line 40-54: The current Option type implements Customize as a no-op
(Option.Customize) so any generic testcontainers helper that only calls
Customize will silently drop dex Options; update the documentation and API
surface to prevent this trap by adding a clear godoc NOTE on the Option type
(and/or Customize) that states Options (e.g., dex.WithClient) must be passed
directly to dex.Run and not forwarded through generic tc-go customizer
pipelines, and include a short example or sentence indicating that real mutation
happens only in Run; reference Option, Customize, Run and a typical Option
helper like WithClient so callers can find and apply the guidance.

In `@modules/dex/README.md`:
- Around line 52-58: Update the README wording to avoid time-sensitive phrasing:
change the sentence that currently reads "requires Dex ≥ v2.46.0 (not yet
released at time of writing)..." to a timeless form such as "requires Dex ≥
v2.46.0 (or use dexidp/dex:master until that release ships)" and apply the same
rewording to the corresponding Known limitations entry; keep the note that
callers must pin a compatible image and preserve the reference to
WithEnableClientCredentials() so readers can find the option.
- Around line 142-145: The README endpoint getters list is inconsistent: all
functions are listed by bare name (IssuerURL, ConfigEndpoint, JWKSEndpoint,
TokenEndpoint, AuthEndpoint) but GRPCEndpoint is shown with a full signature
(GRPCEndpoint(ctx) (string, error)); update the list to be consistent by either
removing the signature from GRPCEndpoint (make it just GRPCEndpoint) or by
adding full signatures for all entries (e.g., IssuerURL() string,
ConfigEndpoint() string, JWKSEndpoint() string, TokenEndpoint() string,
AuthEndpoint() string, GRPCEndpoint(ctx) (string, error)), and ensure the chosen
style matches godoc conventions used elsewhere in the repo.

In `@modules/dex/types.go`:
- Around line 86-96: In WithClientGrantTypes (the ClientOption constructor),
validate each provided grant string against the allowed set
{"authorization_code","refresh_token","client_credentials","password"} (in
addition to rejecting blank values) and return an error immediately for any
invalid value so typos are surfaced at construction; update the error message to
include the offending grant value(s) and ensure you append to c.grantTypes only
after validation (use the function name WithClientGrantTypes and the
Client.grantTypes field as references).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58368487-5191-4973-8304-f4e73cc60d20

📥 Commits

Reviewing files that changed from the base of the PR and between b8fc680 and c2652db.

⛔ Files ignored due to path filters (1)
  • modules/dex/go.sum is excluded by !**/*.sum
📒 Files selected for processing (19)
  • .github/dependabot.yml
  • .vscode/.testcontainers-go.code-workspace
  • docs/modules/dex.md
  • mkdocs.yml
  • modules/dex/Makefile
  • modules/dex/README.md
  • modules/dex/config.go
  • modules/dex/config_test.go
  • modules/dex/dex.go
  • modules/dex/dex_test.go
  • modules/dex/dextest_helpers_test.go
  • modules/dex/examples_test.go
  • modules/dex/go.mod
  • modules/dex/grpc.go
  • modules/dex/log.go
  • modules/dex/log_test.go
  • modules/dex/options.go
  • modules/dex/options_test.go
  • modules/dex/types.go

Comment thread docs/modules/dex.md Outdated
Comment thread modules/dex/config_test.go
Comment thread modules/dex/config.go Outdated
Comment thread modules/dex/dex.go
Comment thread modules/dex/README.md Outdated
Actionable:
- config.go: omit client_credentials from server-level grantTypes unless
  WithEnableClientCredentials() is set
- dex.go: derive readiness probe path from configured issuer
- config.go: fix dexLogLevel doc comment (rounds UP)
- docs/modules/dex.md, README.md: add bash hint to go-get fenced block

Nits accepted: Option doc note, WithLogger(nil) no-op, WithConnector
ConnectorPassword doc, newUUIDv4 returns error, ErrClientNotFound /
ErrUserNotFound sentinels, cookiejar error captured, 5s->30s log poll,
dead `_ = body` removed, DEX_TEST_MASTER gate, http.Method* constants,
examples_test.go defer ordering, README timeless wording + endpoint
style.

Deferred: WithClientGrantTypes value whitelist (too restrictive),
log.go quoted-value unescape (low value).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
modules/dex/grpc.go (1)

87-98: Use testBcryptCost for consistency with the YAML path (and faster tests).

config.go hashes static users at cost 10 (testBcryptCost) with the rationale that cost 10 is Dex's minimum and ~10× faster than the default. This runtime AddUser path uses bcrypt.DefaultCost (14), which:

  • makes gRPC-added users hash ~20× slower than YAML-added users (tangible in suites that add multiple users),
  • produces structurally different hashes for the same input across the two paths — a surprising inconsistency in a test-only module.
♻️ Proposed fix
-	hash, err := bcrypt.GenerateFromPassword([]byte(u.password), bcrypt.DefaultCost)
+	hash, err := bcrypt.GenerateFromPassword([]byte(u.password), testBcryptCost)
 	if err != nil {
 		return fmt.Errorf("dex: bcrypt: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/grpc.go` around lines 87 - 98, The bcrypt cost used when hashing
runtime-added users is bcrypt.DefaultCost, causing slower and inconsistent
hashes vs the YAML path; update the AddUser hashing call
(bcrypt.GenerateFromPassword with u.password) to use the testBcryptCost constant
instead of bcrypt.DefaultCost so runtime gRPC-added users use the same cost as
config.go/test users and speed up tests; leave the surrounding error handling
and userID generation (userID, newUUIDv4) unchanged.
modules/dex/dex_test.go (1)

148-173: Tighten not-found assertions with ErrorIs against the sentinel errors.

grpc.go wraps ErrClientNotFound / ErrUserNotFound with %w, so the assertions can be specific and would catch a regression where the sentinel is replaced with a generic gRPC error. The existing ErrorIs checks for the Exists sentinels on Add already do this — worth matching.

♻️ Proposed tightening
 	// Second remove errors (not-found).
 	err = c.RemoveClient(ctx, cl.ID())
-	assert.Error(t, err)
+	assert.ErrorIs(t, err, dex.ErrClientNotFound)
 }
 	// Second removal errors.
 	err = c.RemoveUser(ctx, u.Email())
-	assert.Error(t, err)
+	assert.ErrorIs(t, err, dex.ErrUserNotFound)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 148 - 173, The second removal assertion
in TestGRPC_AddRemoveUser is too loose; replace the generic assert.Error(t, err)
after the second call to c.RemoveUser(ctx, u.Email()) with assert.ErrorIs(t,
err, dex.ErrUserNotFound) so the test checks the wrapped sentinel error
(ErrUserNotFound) returned by the gRPC wrapper; locate the failing assertion in
TestGRPC_AddRemoveUser and change it accordingly to use ErrorIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@modules/dex/dex_test.go`:
- Around line 148-173: The second removal assertion in TestGRPC_AddRemoveUser is
too loose; replace the generic assert.Error(t, err) after the second call to
c.RemoveUser(ctx, u.Email()) with assert.ErrorIs(t, err, dex.ErrUserNotFound) so
the test checks the wrapped sentinel error (ErrUserNotFound) returned by the
gRPC wrapper; locate the failing assertion in TestGRPC_AddRemoveUser and change
it accordingly to use ErrorIs.

In `@modules/dex/grpc.go`:
- Around line 87-98: The bcrypt cost used when hashing runtime-added users is
bcrypt.DefaultCost, causing slower and inconsistent hashes vs the YAML path;
update the AddUser hashing call (bcrypt.GenerateFromPassword with u.password) to
use the testBcryptCost constant instead of bcrypt.DefaultCost so runtime
gRPC-added users use the same cost as config.go/test users and speed up tests;
leave the surrounding error handling and userID generation (userID, newUUIDv4)
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d11e486-ed32-4d42-9eae-6c5e0fae84c3

📥 Commits

Reviewing files that changed from the base of the PR and between c2652db and 2f99c33.

📒 Files selected for processing (10)
  • docs/modules/dex.md
  • modules/dex/README.md
  • modules/dex/config.go
  • modules/dex/dex.go
  • modules/dex/dex_test.go
  • modules/dex/dextest_helpers_test.go
  • modules/dex/examples_test.go
  • modules/dex/grpc.go
  • modules/dex/options.go
  • modules/dex/types.go
✅ Files skipped from review due to trivial changes (2)
  • docs/modules/dex.md
  • modules/dex/README.md

- AddUser hashes with testBcryptCost (consistency + speed)
- RemoveClient/RemoveUser tests use ErrorIs vs sentinels
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
modules/dex/grpc.go (1)

43-45: Optional: wrap ErrClientExists/ErrUserExists with the identifier for symmetry.

RemoveClient/RemoveUser wrap their sentinels with the id/email ("%w: %q") so callers get useful error text, while AddClient/AddUser return the bare sentinel (ErrClientExists at line 44, ErrUserExists at line 112). Mirroring the same pattern here would make error messages more actionable while still being errors.Is-compatible.

♻️ Proposed consistency tweak
 	if resp.AlreadyExists {
-		return ErrClientExists
+		return fmt.Errorf("%w: %q", ErrClientExists, cl.id)
 	}
 	if resp.AlreadyExists {
-		return ErrUserExists
+		return fmt.Errorf("%w: %q", ErrUserExists, u.email)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/grpc.go` around lines 43 - 45, The AddClient/AddUser handlers
currently return bare sentinels (ErrClientExists/ErrUserExists); make them
mirror RemoveClient/RemoveUser by wrapping the sentinel with the identifier
(e.g. fmt.Errorf("%w: %q", ErrClientExists, id) or the email) before returning
so callers get actionable error text while preserving errors.Is compatibility;
update the return in the AddClient path (resp.AlreadyExists) and the AddUser
path where ErrUserExists is returned to use this wrapped format and reference
the corresponding id/email variable used in those functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/dex/dex_test.go`:
- Around line 611-614: The test calls sidecar.Logs(ctx) which returns an
io.ReadCloser (assigned to logs) but never closes it, leaking the Docker log
stream; fix by closing the stream after reading—e.g., after obtaining logs from
sidecar.Logs(ctx) ensure you call logs.Close() (or defer logs.Close()
immediately after the call) so the ReadCloser is properly released; update the
block around the logs variable in the test that reads with io.ReadAll to close
the stream when done.

---

Nitpick comments:
In `@modules/dex/grpc.go`:
- Around line 43-45: The AddClient/AddUser handlers currently return bare
sentinels (ErrClientExists/ErrUserExists); make them mirror
RemoveClient/RemoveUser by wrapping the sentinel with the identifier (e.g.
fmt.Errorf("%w: %q", ErrClientExists, id) or the email) before returning so
callers get actionable error text while preserving errors.Is compatibility;
update the return in the AddClient path (resp.AlreadyExists) and the AddUser
path where ErrUserExists is returned to use this wrapped format and reference
the corresponding id/email variable used in those functions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a13c6a8-f127-403f-9730-f5c291500951

📥 Commits

Reviewing files that changed from the base of the PR and between 2f99c33 and fabe7d6.

📒 Files selected for processing (2)
  • modules/dex/dex_test.go
  • modules/dex/grpc.go

Comment thread modules/dex/dex_test.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
modules/dex/dex_test.go (2)

480-480: Nit: use http.MethodGet instead of the string literal "GET".

Consistent with the rest of the file (which uses http.MethodPost via helpers) and idiomatic Go.

✏️ Proposed tweak
-	req, err := http.NewRequestWithContext(ctx, "GET", authURL, nil)
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL, nil)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` at line 480, Replace the string literal "GET" with
the constant http.MethodGet in the http.NewRequestWithContext call (the req, err
:= http.NewRequestWithContext(ctx, "GET", authURL, nil) invocation) to match
idiomatic Go and the rest of the test file; update that call to use
http.MethodGet so it is consistent and avoids raw verb strings.

72-75: HTTP calls ignore the test's context deadline.

http.Get and http.Client.Get/Post use http.DefaultClient semantics that do not honor ctx. If Dex becomes ready but later wedges mid-response, these calls will hang past the 3-minute context timeout (until go test’s global timeout fires). The same pattern appears at lines 112, 552, and 562.

Prefer http.NewRequestWithContext(ctx, ...) + client.Do(req) so the ctx cancellation actually aborts in-flight requests.

♻️ Example refactor for lines 72-75
-	resp, err := http.Get(c.ConfigEndpoint())
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.ConfigEndpoint(), nil)
+	require.NoError(t, err)
+	resp, err := http.DefaultClient.Do(req)
 	require.NoError(t, err)
 	defer resp.Body.Close()
 	require.Equal(t, 200, resp.StatusCode, "discovery endpoint must return 200")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 72 - 75, Replace plain http.Get calls
that ignore the test context with context-aware requests: create a request via
http.NewRequestWithContext(ctx, "GET", c.ConfigEndpoint(), nil) and execute it
with an HTTP client (e.g., http.DefaultClient.Do(req)) so the test's ctx cancels
in-flight calls; ensure you still handle resp and defer resp.Body.Close() and
assert resp.StatusCode. Apply the same refactor for the other occurrences
referenced (lines using http.Get at the same test: the calls around
c.ConfigEndpoint() and the instances at the locations flagged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@modules/dex/dex_test.go`:
- Line 480: Replace the string literal "GET" with the constant http.MethodGet in
the http.NewRequestWithContext call (the req, err :=
http.NewRequestWithContext(ctx, "GET", authURL, nil) invocation) to match
idiomatic Go and the rest of the test file; update that call to use
http.MethodGet so it is consistent and avoids raw verb strings.
- Around line 72-75: Replace plain http.Get calls that ignore the test context
with context-aware requests: create a request via
http.NewRequestWithContext(ctx, "GET", c.ConfigEndpoint(), nil) and execute it
with an HTTP client (e.g., http.DefaultClient.Do(req)) so the test's ctx cancels
in-flight calls; ensure you still handle resp and defer resp.Body.Close() and
assert resp.StatusCode. Apply the same refactor for the other occurrences
referenced (lines using http.Get at the same test: the calls around
c.ConfigEndpoint() and the instances at the locations flagged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4dcc950-e0d0-461c-9a33-c70719a5197b

📥 Commits

Reviewing files that changed from the base of the PR and between fabe7d6 and 17ab589.

📒 Files selected for processing (1)
  • modules/dex/dex_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
modules/dex/dex_test.go (2)

315-344: Consider t.Run subtests per redirect URI.

Minor readability / reporting nit: the for _, uri := range uris loop runs both URIs as a single test, so a failure on the second URI surfaces without isolation and short-circuits the first failure’s reporting. Wrapping each iteration in t.Run(uri, ...) gives per-URI pass/fail lines and lets -run target a specific one.

♻️ Proposed refactor
 	for _, uri := range uris {
-		cfg := oauth2.Config{
-			ClientID:     "e2e",
-			ClientSecret: "s",
-			RedirectURL:  uri,
-			Endpoint:     oauth2.Endpoint{AuthURL: c.AuthEndpoint(), TokenURL: c.TokenEndpoint()},
-			Scopes:       []string{"openid"},
-		}
-		tok := drivePasswordAuthCode(t, ctx, cfg, "a@e.com", "p")
-		assert.NotEmpty(t, tok.AccessToken, "uri=%s", uri)
+		uri := uri
+		t.Run(uri, func(t *testing.T) {
+			cfg := oauth2.Config{
+				ClientID:     "e2e",
+				ClientSecret: "s",
+				RedirectURL:  uri,
+				Endpoint:     oauth2.Endpoint{AuthURL: c.AuthEndpoint(), TokenURL: c.TokenEndpoint()},
+				Scopes:       []string{"openid"},
+			}
+			tok := drivePasswordAuthCode(t, ctx, cfg, "a@e.com", "p")
+			assert.NotEmpty(t, tok.AccessToken)
+		})
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 315 - 344, In
TestAuthCode_MultipleRedirectURIs, wrap the existing for _, uri := range uris
loop body in a t.Run subtest to isolate each redirect URI; inside the loop do
uri := uri then t.Run(uri, func(t *testing.T) { /* move the oauth2.Config
creation, drivePasswordAuthCode call and assertions here */ }), so each
iteration (referencing uri and the loop that currently contains oauth2.Config,
drivePasswordAuthCode and assert.NotEmpty) produces a separate subtest result
and avoids variable capture issues.

499-506: Minor: loc := resp.Request.URL default is effectively dead on the success path.

With CheckRedirect returning http.ErrUseLastResponse on the redirect to cfg.RedirectURL, resp will always be a 3xx whose Location header carries the ?code=.... The initial assignment only survives on the failure path (where require.NotEmpty(t, code, ...) fails anyway with a reasonable message). Not a bug — but extracting the code directly from the Location header (and asserting the 3xx up front) would make the control flow obvious and eliminate the misleading default:

♻️ Proposed refactor
-	loc := resp.Request.URL
-	if resp.StatusCode >= 300 && resp.StatusCode < 400 {
-		parsed, perr := url.Parse(resp.Header.Get("Location"))
-		require.NoError(t, perr)
-		loc = parsed
-	}
-	code := loc.Query().Get("code")
-	require.NotEmpty(t, code, "mockCallback should redirect with ?code=...; got %q", loc.String())
+	require.GreaterOrEqual(t, resp.StatusCode, 300, "expected redirect to %s", cfg.RedirectURL)
+	require.Less(t, resp.StatusCode, 400, "expected redirect to %s", cfg.RedirectURL)
+	loc, err := url.Parse(resp.Header.Get("Location"))
+	require.NoError(t, err)
+	code := loc.Query().Get("code")
+	require.NotEmpty(t, code, "mockCallback should redirect with ?code=...; got %q", loc.String())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/dex/dex_test.go` around lines 499 - 506, The test currently assigns
loc := resp.Request.URL which never applies on the success path because
CheckRedirect returns http.ErrUseLastResponse causing resp to be a 3xx; instead,
assert that resp.StatusCode is in the 300-399 range, parse the redirect target
directly from resp.Header.Get("Location") (using url.Parse) and extract the code
from the parsed URL query; update the require checks to require.NoError for the
parse and require.NotEmpty for the code so the control flow is clear
(references: resp, resp.Header.Get("Location"), CheckRedirect behavior,
cfg.RedirectURL).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@modules/dex/dex_test.go`:
- Around line 315-344: In TestAuthCode_MultipleRedirectURIs, wrap the existing
for _, uri := range uris loop body in a t.Run subtest to isolate each redirect
URI; inside the loop do uri := uri then t.Run(uri, func(t *testing.T) { /* move
the oauth2.Config creation, drivePasswordAuthCode call and assertions here */
}), so each iteration (referencing uri and the loop that currently contains
oauth2.Config, drivePasswordAuthCode and assert.NotEmpty) produces a separate
subtest result and avoids variable capture issues.
- Around line 499-506: The test currently assigns loc := resp.Request.URL which
never applies on the success path because CheckRedirect returns
http.ErrUseLastResponse causing resp to be a 3xx; instead, assert that
resp.StatusCode is in the 300-399 range, parse the redirect target directly from
resp.Header.Get("Location") (using url.Parse) and extract the code from the
parsed URL query; update the require checks to require.NoError for the parse and
require.NotEmpty for the code so the control flow is clear (references: resp,
resp.Header.Get("Location"), CheckRedirect behavior, cfg.RedirectURL).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 756fecc3-037b-4a10-a2d3-afa127d1112a

📥 Commits

Reviewing files that changed from the base of the PR and between 17ab589 and 1194dce.

📒 Files selected for processing (1)
  • modules/dex/dex_test.go

Comment thread docs/modules/dex.md Outdated
Comment thread modules/dex/dex.go Outdated
…ule convention (PR testcontainers#3659)

Module guide (https://golang.testcontainers.org/modules/) states: "Use the
name Container, not a module-specific name like PostgresContainer or
RedisContainer". Reviewer @mdelapenya requested this explicitly on
modules/dex/dex.go:46.

Pure rename — type declaration, constructor return, and all method
receivers in dex.go and grpc.go. Test files use type inference
(c, err := dex.Run(...)) and need no edits. Doc comment in dex.md Run
signature updated to match.
…efs (PR testcontainers#3659)

Reviewer @mdelapenya flagged hardcoded v0.42.0 ("applies to more places
below"): the contributing guide requires the "main" placeholder so release
automation rewrites it at release time
(https://golang.testcontainers.org/contributing/#adding-functional-options).

Replaces 20 refs in docs/modules/dex.md and 1 in README.md, dropping the
/releases/tag/v0.42.0 href suffix and swapping ":material-tag: v0.42.0"
for ":material-tag: main".
… per module convention (PR testcontainers#3659)

Module guide: "Testable examples use blocks marked with
inside_block:runContainer for automatic documentation inclusion via
codeinclude" (https://golang.testcontainers.org/modules/). The label in
examples_test.go and the corresponding inside_block reference in dex.md
must match — renaming both.
…tters in README (PR testcontainers#3659 nit)

Round-1 review (CodeRabbit nit, README.md:142-145): the prior list named
five getters bare and showed GRPCEndpoint with prose about its signature,
which the reviewer flagged as inconsistent. Switching to a uniform
bullet list with full signatures keeps godoc and README aligned and is
clearer than the mixed-style note.
…oted values (PR testcontainers#3659 nit)

Round-1 review (CodeRabbit nit, log.go:97-109): the quoted-value branch
of tokenizeLogfmt advanced past escape sequences but appended the raw
substring, so a Dex `msg=` field containing \" or \\ surfaced with the
literal backslash in the resulting slog attr.

Added a package-level strings.NewReplacer that expands the two escapes
logfmt allows and a regression test covering both.
…owlist (PR testcontainers#3659 nit)

Round-1 review (CodeRabbit nit, types.go:86-96): WithClientGrantTypes
only rejected blanks, so typos like "authorisation_code" passed
construction and surfaced much later — either when Dex parsed the
rendered YAML or, worse, as unsupported_grant_type at the token
endpoint. The constructor's stated goal is to surface invalid
configuration at call-site, so add an allowlist matching the docstring.

The allowlist mirrors Dex's four supported grants
(authorization_code, refresh_token, client_credentials, password) and
is kept as a package-level set so the docstring and check stay in sync.
Table-driven test covers the supported set plus typo / unknown / mixed
cases.
…d/email for symmetry (PR testcontainers#3659 nit)

Round-3 review (CodeRabbit nit, grpc.go:43-45/111-113): RemoveClient and
RemoveUser already return fmt.Errorf("%w: %q", ErrClientNotFound, id)
(and the user variant), but AddClient and AddUser returned the bare
sentinel — callers had no identifier in the error text.

Mirrors the wrapping pattern. errors.Is checks against the bare
sentinels (assert.ErrorIs in dex_test.go) keep working because %w
preserves the chain.
….Run subtests (PR testcontainers#3659 nit)

Round-5 review (CodeRabbit nit, dex_test.go:315-344):
TestAuthCode_MultipleRedirectURIs ran two URIs in a bare for loop, so a
failure on the second short-circuited the first's reporting and -run
couldn't target a single URI. Wrap each iteration in t.Run(uri, …).

Drop the "uri=%s" annotation on assert.NotEmpty — the subtest name now
identifies the iteration. Go ≥ 1.22 fixed loop-variable capture, so no
local-shadow needed.
…ivePasswordAuthCode (PR testcontainers#3659 nit)

Round-5 review (CodeRabbit nit, dex_test.go:499-506): the helper assigned
loc := resp.Request.URL as a fallback, but CheckRedirect returns
http.ErrUseLastResponse so resp is always a 3xx on the success path —
the assignment never applied. The flow read like it handled both
redirect and non-redirect outcomes when in practice the non-redirect
branch always failed at require.NotEmpty(code, ...).

Replace with explicit 3xx require + direct Location parse so the
control flow matches the actual contract.
@guilycst
Copy link
Copy Markdown
Contributor Author

guilycst commented Apr 24, 2026

@mdelapenya round-3 push (f58a75d..dc57f25) addresses both your review comments plus six remaining CodeRabbit nits. Ready for re-review.

Your feedback:

  • Container rename (f58a75d): `DexContainer` → `Container` per the module guide. Includes all method receivers in `dex.go` and `grpc.go` plus the doc signature and the codeinclude block label (dd3289f: `runDexContainer` → `runContainer`, also per guide).
  • `main` placeholder (fcdb2ce): every `v0.42.0` ref in `docs/modules/dex.md` (20) and `modules/dex/README.md` (1) now uses the `main` placeholder so release automation rewrites it.

@guilycst guilycst requested a review from mdelapenya April 25, 2026 04:55
@mdelapenya
Copy link
Copy Markdown
Member

@guilycst could you run make lint in the new module? The CI is failing there

@guilycst
Copy link
Copy Markdown
Contributor Author

@guilycst could you run make lint in the new module? The CI is failing there

@mdelapenya FIxed the linting issues that appeared locally

@guilycst
Copy link
Copy Markdown
Contributor Author

guilycst commented May 6, 2026

Hey @mdelapenya, hope you are doing well. Any update on this PR?

Copy link
Copy Markdown
Member

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guilycst just an update in the docs, about the next-release placeholder

Comment thread docs/modules/dex.md Outdated
Comment thread docs/modules/dex.md Outdated
guilycst and others added 3 commits May 8, 2026 11:08
Co-authored-by: Manuel de la Peña <social.mdelapenya@gmail.com>
Co-authored-by: Manuel de la Peña <social.mdelapenya@gmail.com>
Comment thread docs/modules/dex.md Outdated

#### Users

- Since <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @guilycst I should have been more explicit, my fault 🙏

Could you apply this to all the occurrences?

Copy link
Copy Markdown
Contributor Author

@guilycst guilycst May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good @mdelapenya, I just pushed the docs changes

@guilycst guilycst requested a review from mdelapenya May 8, 2026 20:43
@mdelapenya
Copy link
Copy Markdown
Member

@guilycst to avoid more ping-pong from my side, I'm adding a commit on top to fix this:

  • prefer require over assert in tests
  • proper container cleanup in tests: call testcontainers.CleanupContainer(t, ctr) right after Run and before require.Error, because Run can return both an error and a partially-started container. If require.NoError fails first, the cleanup never gets registered and the container leaks.

Once this is merged, I can merge

@mdelapenya mdelapenya self-assigned this May 11, 2026
@mdelapenya mdelapenya added the enhancement New feature or request label May 11, 2026
Copy link
Copy Markdown
Member

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@mdelapenya mdelapenya merged commit 35ae2ad into testcontainers:main May 11, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants