From e9e6ad4e177112cb49b6b422ea554a558644d053 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Wed, 25 Feb 2026 21:47:01 -0800 Subject: [PATCH] fix(test): resolve nil-pointer panic in TestCommandSide_ChangeUserHuman (#11695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved 1. `nx run @zitadel/api:test-unit` panics in `TestCommandSide_ChangeUserHuman` due to two test cases (added in 0261536) missing the required `loginPaths` field. Since the field type is `func(*testing.T) LoginPaths`, its zero value is `nil`, and calling it causes a SIGSEGV. 2. Three targets in `apps/api/project.json` (`test-unit`, `build`, `build-linux`) were silently non-cacheable because NX does not merge `cache: true` from `targetDefaults` when a project-level target overrides other properties like `dependsOn` or `inputs`. 3. `TestServer_AuthorizeOrDenyDeviceAuthorization` integration test is flaky — it uses hardcoded `5*time.Second` timeouts for `EventuallyWithT` polling, while the rest of the file uses `WaitForAndTickWithMaxDuration(ctx, time.Minute)`. Under CI load, 5 seconds is insufficient and the empty ID cascades into a validation error. # How the Problems Are Solved **Test panic fix:** - Added missing `loginPaths: expectLoginPathsNoCall` to both broken test cases ("change human email verified (self-management), not allowed" and "change human phone verified (self-management), not allowed"). **NX cache fix:** - Added explicit `"cache": true` to `test-unit`, `build`, and `build-linux` targets in `apps/api/project.json`. - Verified with `pnpm nx show project @zitadel/api --json` that all three targets now resolve with `cache: true`. **Integration test flakiness fix:** - Replaced all 6 hardcoded `assert.EventuallyWithT(t, ..., 5*time.Second, 100*time.Millisecond)` calls in `TestServer_AuthorizeOrDenyDeviceAuthorization` with `require.EventuallyWithT(t, ..., retryDuration, tick)` using `integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute)`. - Changed from `assert` (non-fatal) to `require` (fatal) so timeout failures stop the test immediately instead of cascading with empty IDs. # Additional Context - The broken unit test landed on main because CI skips `lint_test_build` on pushes to main (`if: github.ref != 'refs/heads/main'`). A follow-up issue was created: #11696. - The integration test flakiness was missed by the previous fix in #10752. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Livio Spring --- apps/api/project.json | 3 ++ .../oidc/v2/integration_test/oidc_test.go | 30 +++++++++++-------- internal/command/user_v2_human_test.go | 2 ++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/api/project.json b/apps/api/project.json index c4c3d2221..b9940f9b0 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -66,6 +66,7 @@ "generate", "build-console" ], + "cache": true, "command": "bash -c 'CGO_ENABLED=0 go build -o .artifacts/bin/$(go env GOOS)/$(go env GOARCH)/zitadel.local -ldflags=\"-s -w\"'", "inputs": [ { @@ -89,6 +90,7 @@ "generate", "build-console" ], + "cache": true, "command": "bash -c 'CGO_ENABLED=0 GOOS=linux go build -o .artifacts/bin/linux/$(go env GOARCH)/zitadel.local -ldflags=\"-s -w\"'", "inputs": [ { @@ -161,6 +163,7 @@ "dependsOn": [ "generate" ], + "cache": true, "command": "go test -race -coverprofile=profile.api.test-unit.cov -coverpkg=./internal/...,./backend/... ./...", "inputs": [ "sources", diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index 57664af9d..6771cb18a 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -752,13 +752,14 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") require.NoError(t, err) var id string - assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ UserCode: req.UserCode, }) assert.NoError(collectT, err) id = resp.GetDeviceAuthorizationRequest().GetId() - }, 5*time.Second, 100*time.Millisecond) + }, retryDuration, tick) return id }(), Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ @@ -778,13 +779,14 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") require.NoError(t, err) var id string - assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ UserCode: req.UserCode, }) assert.NoError(collectT, err) id = resp.GetDeviceAuthorizationRequest().GetId() - }, 5*time.Second, 100*time.Millisecond) + }, retryDuration, tick) return id }(), Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ @@ -804,13 +806,14 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") require.NoError(t, err) var id string - assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ UserCode: req.UserCode, }) assert.NoError(collectT, err) id = resp.GetDeviceAuthorizationRequest().GetId() - }, 5*time.Second, 100*time.Millisecond) + }, retryDuration, tick) return id }(), Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{}, @@ -826,13 +829,14 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") require.NoError(t, err) var id string - assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ UserCode: req.UserCode, }) assert.NoError(collectT, err) id = resp.GetDeviceAuthorizationRequest().GetId() - }, 5*time.Second, 100*time.Millisecond) + }, retryDuration, tick) return id }(), Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{}, @@ -848,13 +852,14 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") require.NoError(t, err) var id string - assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ UserCode: req.UserCode, }) assert.NoError(collectT, err) id = resp.GetDeviceAuthorizationRequest().GetId() - }, 5*time.Second, 100*time.Millisecond) + }, retryDuration, tick) return id }(), Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ @@ -874,13 +879,14 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") require.NoError(t, err) var id string - assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ UserCode: req.UserCode, }) assert.NoError(collectT, err) id = resp.GetDeviceAuthorizationRequest().GetId() - }, 5*time.Second, 100*time.Millisecond) + }, retryDuration, tick) return id }(), Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index f16f594fc..9c1459097 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -2711,6 +2711,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), ), checkPermission: newMockPermissionCheckNotAllowed(), + loginPaths: expectLoginPathsNoCall, tarpit: expectTarpit(0), }, args: args{ @@ -3060,6 +3061,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), ), checkPermission: newMockPermissionCheckNotAllowed(), + loginPaths: expectLoginPathsNoCall, tarpit: expectTarpit(0), }, args: args{