Skip to content

refactor(test): migrate E2E tests from gRPC to ConnectRPC clients#1389

Open
whoAbhishekSah wants to merge 3 commits intomainfrom
refactor/migrate-e2e-to-connectrpc
Open

refactor(test): migrate E2E tests from gRPC to ConnectRPC clients#1389
whoAbhishekSah wants to merge 3 commits intomainfrom
refactor/migrate-e2e-to-connectrpc

Conversation

@whoAbhishekSah
Copy link
Member

@whoAbhishekSah whoAbhishekSah commented Feb 18, 2026

Summary

Migrates all E2E tests from gRPC clients to ConnectRPC clients and replaces the identity proxy header (X-Frontier-Email) auth mechanism with proper mail OTP + session cookie authentication.

Why this migration matters

The ConnectRPC server is Frontier's primary API server going forward. The gRPC-gateway server is deprecated. E2E tests need to exercise the ConnectRPC code path to catch real bugs — and this migration already surfaced several behavioral differences between the two servers (see below).


Key changes

1. Authentication: identity proxy header → mail OTP sessions (most important)

The old tests used IdentityProxyHeader (X-Frontier-Email) to authenticate — a pass-through header that the ConnectRPC server intentionally does not support. All tests now authenticate via the proper mail OTP flow using the test_users config:

Authenticate(mailotp, email) → AuthCallback(otp="123456") → Set-Cookie: sid=<encrypted> → pass Cookie header

New helpers in testbench/helper.go:

  • AuthenticateUser(ctx, client, email) → (cookieString, error) — runs the full OTP flow, returns session cookie
  • ContextWithAuth(ctx, cookie) → context.Context — wraps cookie into request headers
  • TestOTP = "123456" — fixed OTP for test_users config (no SMTP needed)

All SetupSuite functions and ~70 ContextWithHeaders(IdentityHeader) call sites were updated. The passthrough_header_test.go file was deleted as it tested the identity proxy mechanism.

2. Client migration: gRPC → ConnectRPC

  • TestBench.Client / TestBench.AdminClient types changed to ConnectRPC interfaces
  • All call sites updated: client.Method(ctx, &req)client.Method(ctx, connect.NewRequest(&req))
  • Response access: res.GetField()res.Msg.GetField()
  • Error codes: codes.X / status.Convert(err)connect.CodeX / connect.CodeOf(err)
  • Headers: grpc.Header(&md)resp.Header().Get(key)

3. Behavioral differences uncovered between gRPC and ConnectRPC handlers

These are not bugs in the tests — they reflect real differences in the ConnectRPC handlers that the tests now correctly accommodate:

Difference gRPC handler ConnectRPC handler Test fix
Billing: ProjectId resolution Resolves ProjectIdOrgId internally Always requires OrgId directly Added OrgId to all billing calls that only had ProjectId
Webhook errors Returns specific Stripe error messages Wraps with generic ErrInternalServerError Changed assertion to check connect.CodeInternal
Invitation duplicate Returns invitation.ErrAlreadyMember message Returns handler-level ErrAlreadyMember (different text) Changed to check connect.CodeAlreadyExists instead of message text
JWT auth header Reads gateway-user-token metadata key Reads Authorization: Bearer HTTP header Changed to Authorization: Bearer header

4. Pre-existing test bug fixed

Removed a sub-test in onboarding_test.go step 6 that asserted creating a role with Permissions: nil should fail. Both gRPC and ConnectRPC handlers pass nil permissions straight through to roleService.Upsert, which skips its for range loop on nil and succeeds. The service layer (core/role/service.go:57) and repository have no validation rejecting empty permissions. This test was incorrect on main as well — it was not a ConnectRPC behavioral difference.


Files changed

File Change
testbench/helper.go New auth helpers (AuthenticateUser, ContextWithAuth), updated Bootstrap functions to accept session cookie instead of email, removed IdentityHeader
testbench/testbench.go Switched to ConnectRPC client types and connect port
regression/api_test.go Config + auth migration (~25 call sites), invitation error fix, user domain fix
regression/authentication_test.go JWT auth header fix, mail OTP cookie extraction fix
regression/billing_test.go Config + auth migration, added OrgId to ProjectId-only billing calls, webhook assertion fix
regression/onboarding_test.go Config + auth migration, removed empty-permissions role test
regression/service_registration_test.go Config + auth migration
regression/serviceusers_test.go Config + auth migration (~21 call sites)
regression/passthrough_header_test.go Deleted (tested identity proxy mechanism)
smoke/ping_test.go Removed IdentityProxyHeader from config

Test plan

  • go build ./test/e2e/... passes
  • go vet ./test/e2e/... passes
  • All regression tests pass (API, Authentication, Billing, Onboarding, ServiceRegistration, ServiceUsers)
  • Smoke tests pass

Note: The ConnectRPC billing handlers do not support ProjectId-based billing resolution (unlike the legacy gRPC handlers). Tests were updated to always provide OrgId. If ProjectIdOrgId resolution is needed in the ConnectRPC handlers, that should be a separate change.

🤖 Generated with Claude Code

@vercel
Copy link

vercel bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Feb 19, 2026 6:12am

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Warning

Rate limit exceeded

@whoAbhishekSah has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 10 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

E2E tests and testbench migrated from gRPC clients to Connect RPC: requests now use connect.NewRequest, responses use the Msg wrapper, gRPC metadata-based contexts replaced with header helpers, a Connect port/config was added, and one passthrough header test was removed.

Changes

Cohort / File(s) Summary
Regression tests — connect migration
test/e2e/regression/authentication_test.go, test/e2e/regression/billing_test.go, test/e2e/regression/onboarding_test.go, test/e2e/regression/service_registration_test.go, test/e2e/regression/serviceusers_test.go
Replaced protobuf/gRPC client calls with connect.NewRequest(...) wrappers and updated response access to resp.Msg.GetXxx(). Replaced metadata-based contexts with testbench.ContextWithHeaders / testbench.ContextWithAuth. Added Connect port wiring to app config and adjusted auth header/cookie propagation.
Removed test
test/e2e/regression/passthrough_header_test.go
Deleted the passthrough header E2E regression test file and its suite.
Testbench helpers and wiring
test/e2e/testbench/helper.go, test/e2e/testbench/testbench.go
Switched to generated Connect clients (frontierv1beta1connect.*), added CreateClient/CreateAdminClient, introduced ContextWithHeaders, ContextWithAuth, HeadersFromContext, headerInterceptor, and AuthenticateUser. Updated bootstrap helpers to accept session cookie and Connect clients; updated TestBench struct and init to use Connect host/port.
Minor config change
test/e2e/smoke/ping_test.go
Removed IdentityProxyHeader assignment from GRPC config in the smoke test setup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana

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.

Copy link

@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)
test/e2e/testbench/testbench.go (1)

106-133: Redundant first te.close assignment.

The first assignment to te.close at lines 106-110 is immediately overwritten by the second assignment at lines 128-133. The first assignment is unreachable dead code.

♻️ Proposed fix to remove redundant assignment
 	if err = MigrateFrontier(logger, appConfig); err != nil {
 		return nil, err
 	}

-	te.close = func() error {
-		err1 := pgResource.Close()
-		err2 := spiceDBClose()
-		return errors.Join(err1, err2)
-	}
-
 	StartFrontier(logger, appConfig)

 	// create ConnectRPC clients using the connect port
test/e2e/testbench/helper.go (1)

80-94: Consider simplifying return signature since error is never returned.

Both CreateClient and CreateAdminClient always return nil for the error value. The error return is reasonable for future-proofing, but worth noting that callers could potentially ignore the error check.

Current implementation is acceptable for consistency with common client factory patterns.

@coveralls
Copy link

coveralls commented Feb 18, 2026

Pull Request Test Coverage Report for Build 22170736074

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 38.457%

Totals Coverage Status
Change from base Build 22170433359: 0.0%
Covered Lines: 16200
Relevant Lines: 42125

💛 - Coveralls

Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/e2e/regression/authentication_test.go (1)

211-263: ⚠️ Potential issue | 🟡 Minor

mailOTPCtx is nil if test 3 fails, which would panic test 4.

mailOTPCtx (declared at line 211) is only assigned at line 263 upon successful completion of sub-test 3. If test 3 fails or is skipped, test 4 (line 267) will use a nil context.Context, causing a nil-pointer panic. This is likely a pre-existing issue carried over from the gRPC version, but worth hardening.

🛡️ Suggested guard
 	s.Run("4. authenticate a service user successfully using jwt", func() {
+		if mailOTPCtx == nil {
+			s.T().Skip("skipping: mailOTPCtx not set (test 3 must have failed)")
+		}
 		// create organization via session
 		createOrgResp, err := s.testBench.Client.CreateOrganization(mailOTPCtx, connect.NewRequest(&frontierv1beta1.CreateOrganizationRequest{
🧹 Nitpick comments (3)
test/e2e/testbench/helper.go (3)

41-50: ContextWithHeaders silently overwrites any previously stored headers.

Calling ContextWithHeaders (or ContextWithAuth) on a context that already carries headers replaces them entirely because context.WithValue with the same key shadows the prior value. Currently all call sites create a fresh context, so this isn't broken today, but it's a subtle footgun for future use (e.g., adding an Authorization header on top of a Cookie-carrying context).

Consider merging with existing headers if present:

♻️ Suggested defensive merge
 func ContextWithHeaders(ctx context.Context, headers map[string]string) context.Context {
+	if existing := HeadersFromContext(ctx); existing != nil {
+		merged := make(map[string]string, len(existing)+len(headers))
+		for k, v := range existing {
+			merged[k] = v
+		}
+		for k, v := range headers {
+			merged[k] = v
+		}
+		return context.WithValue(ctx, headersKey{}, merged)
+	}
 	return context.WithValue(ctx, headersKey{}, headers)
 }

122-136: CreateClient / CreateAdminClient always return nil error — consider simplifying the signature.

Both functions are infallible now (the ConnectRPC constructor doesn't return an error). Keeping the error return adds clutter at every call site (_, err := …; require.NoError(err)). If backward-compatible removal isn't feasible right now, a // TODO would be helpful.


138-271: Bootstrap functions: redundant ContextWithAuth calls inside loops.

Each iteration creates a new authCtx (e.g., line 145, 173, 211, 250) that is identical to the one created outside the loop (lines 154, 182, 220, 260). Since the context and cookie don't change between iterations, you could create authCtx once before the loop and reuse it.

whoAbhishekSah and others added 2 commits February 19, 2026 11:32
Replace gRPC client infrastructure with ConnectRPC in all E2E tests:
- Add ContextWithHeaders helper and ConnectRPC interceptor in testbench
- Switch TestBench client types to ConnectRPC interfaces
- Connect to ConnectRPC port instead of gRPC port
- Replace metadata.NewOutgoingContext with testbench.ContextWithHeaders
- Wrap all RPC request args with connect.NewRequest()
- Access responses via .Msg. prefix
- Replace gRPC error codes (codes.X/status.Convert) with connect equivalents

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…kie auth

Replace identity proxy header (X-Frontier-Email) authentication with
proper mail OTP + session cookie auth flow using test_users config.
This fixes all E2E test failures after migrating to ConnectRPC clients,
since the ConnectRPC server doesn't support identity proxy headers.

Key changes:
- Add AuthenticateUser() and ContextWithAuth() helpers in testbench
- Update all Bootstrap functions to accept session cookie instead of email
- Configure test_users + mail OTP in all test suite configs
- Add Session.Validity to prevent immediate session expiry
- Fix authentication_test.go to use Set-Cookie and Bearer token headers
- Fix non-raystack.org email domains to work with test_users config
- Delete passthrough_header_test.go (no longer applicable)
- Remove IdentityHeader constant (no longer needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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 (3)
test/e2e/testbench/testbench.go (1)

106-133: te.close is overwritten — intermediate failures leak the Stripe mock.

If CreateClient or CreateAdminClient fails between lines 116–126, the function returns nil, err using the first te.close (line 106) that omits stripeClose. Because the caller receives nil, it cannot call Close(), so Stripe mock resources leak. This is likely pre-existing but now more visible.

Consider assigning te.close once at the end or using a single deferred cleanup-on-error pattern.

♻️ Suggested single-assignment approach
+	var stripeCloser = stripeClose // capture for use in close func
+
 	te.close = func() error {
 		err1 := pgResource.Close()
 		err2 := spiceDBClose()
-		return errors.Join(err1, err2)
-	}
-
-	StartFrontier(logger, appConfig)
-
-	// create ConnectRPC clients using the connect port
-	connectHost := net.JoinHostPort(appConfig.App.Host, strconv.Itoa(appConfig.App.Connect.Port))
-	sClient, err := CreateClient(connectHost)
-	if err != nil {
-		return nil, err
-	}
-	te.Client = sClient
-
-	adClient, err := CreateAdminClient(connectHost)
-	if err != nil {
-		return nil, err
-	}
-	te.AdminClient = adClient
-
-	te.close = func() error {
-		err1 := pgResource.Close()
-		err2 := spiceDBClose()
-		err3 := stripeClose()
+		err3 := stripeCloser()
 		return errors.Join(err1, err2, err3)
 	}
+
+	StartFrontier(logger, appConfig)
+
+	// create ConnectRPC clients using the connect port
+	connectHost := net.JoinHostPort(appConfig.App.Host, strconv.Itoa(appConfig.App.Connect.Port))
+	sClient, err := CreateClient(connectHost)
+	if err != nil {
+		return nil, err
+	}
+	te.Client = sClient
+
+	adClient, err := CreateAdminClient(connectHost)
+	if err != nil {
+		return nil, err
+	}
+	te.AdminClient = adClient
test/e2e/testbench/helper.go (2)

41-50: ContextWithHeaders calls don't compose — later call shadows earlier headers.

If ContextWithHeaders is called on a context that already carries headers (or if ContextWithAuth is called after ContextWithHeaders), the second call shadows the first map entirely instead of merging. Currently tests only call one or the other, so this is not an active bug, but it is a latent footgun for future test authors.

Consider documenting this or merging with existing headers:

♻️ Optional: merge-on-write variant
 func ContextWithHeaders(ctx context.Context, headers map[string]string) context.Context {
+	if existing := HeadersFromContext(ctx); existing != nil {
+		merged := make(map[string]string, len(existing)+len(headers))
+		for k, v := range existing {
+			merged[k] = v
+		}
+		for k, v := range headers {
+			merged[k] = v
+		}
+		return context.WithValue(ctx, headersKey{}, merged)
+	}
 	return context.WithValue(ctx, headersKey{}, headers)
 }

122-136: CreateClient / CreateAdminClient never return an error — signatures could be simplified.

Both functions construct ConnectRPC clients that always succeed (no I/O at construction time), so the error return is always nil. The current signature is fine for forward compatibility, but if you want to keep it, consider adding a brief comment explaining the intent.

Add OrgId to CreateBillingUsage/RevertBillingUsage/CheckFeatureEntitlement
calls that only provided ProjectId, since ConnectRPC handlers always resolve
billing via GetOrgId(). Update webhook error assertion to match ConnectRPC's
generic error wrapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments