test(login): avoid assert.Never race in forged registration check (#12548)

# Which Problems Are Solved

- `TestExternalNotFoundOption_ForgedRegistration_IsRejected` flakes
across unrelated PRs: the test reaches PASS, then panics with `Log in
goroutine after Test... has completed` and `rpc error: code = Canceled
desc = context canceled`.

# How the Problems Are Solved

- Replaces `assert.Never` with a synchronous poll helper
(`requireNeverUserByEmail`) so `ListUsers` and `require` run on the test
goroutine and finish before `TestMain` cancels `CTX`.
- Keeps the same “must stay absent for a window” polarity (not
`Eventually`), so a late-projected forged user still fails the test.

# Additional Changes

- None.

# Additional Context

- CI examples: [run
30980062306](https://github.com/zitadel/zitadel/actions/runs/30980062306),
[run
30837302974](https://github.com/zitadel/zitadel/actions/runs/30837302974)

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Livio Spring
2026-08-06 07:50:21 +02:00
committed by GitHub
co-authored by Cursor
parent 0a18aba91c
commit be5b278069
@@ -190,13 +190,10 @@ func TestExternalNotFoundOption_ForgedRegistration_IsRejected(t *testing.T) {
assert.Contains(t, body, "LOGIN-Dju3f",
"forged registration should be rejected by the LinkingUsers guard; got a page without that rejection (attack likely succeeded)")
// The load-bearing assertion: no account may exist for the attacker's forged identity. We use
// assert.Never (not Eventually) so that if the account IS created but its projection lags, the user
// still surfaces within the window and fails the test - which is exactly the unpatched behavior.
assert.Never(t, func() bool {
return userCountByEmail(t, forgedEmail) > 0
}, 5*time.Second, 250*time.Millisecond,
"a user was created for the attacker-forged external identity %q - account pre-hijack succeeded", forgedEmail)
// No account may exist for the forged identity. Poll on the test goroutine for a window so a
// late-projected user still fails the test (assert.Never is unsafe here: its condition runs in a
// background goroutine that can outlive the test and panic on CTX cancel from TestMain).
requireNeverUserByEmail(t, forgedEmail, 5*time.Second, 250*time.Millisecond)
}
// --- helpers ------------------------------------------------------------------------------------
@@ -248,14 +245,22 @@ func doPostForm(t *testing.T, c *http.Client, rawURL string, form url.Values) st
return string(b)
}
// userCountByEmail returns how many users match the given email address (via the User v2 API).
func userCountByEmail(t *testing.T, email string) int {
// requireNeverUserByEmail fails if a user with the given email appears within waitFor.
func requireNeverUserByEmail(t *testing.T, email string, waitFor, tick time.Duration) {
t.Helper()
resp, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{
Queries: []*user.SearchQuery{
{Query: &user.SearchQuery_EmailQuery{EmailQuery: &user.EmailQuery{EmailAddress: email}}},
},
})
require.NoError(t, err)
return len(resp.GetResult())
deadline := time.Now().Add(waitFor)
for {
resp, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{
Queries: []*user.SearchQuery{
{Query: &user.SearchQuery_EmailQuery{EmailQuery: &user.EmailQuery{EmailAddress: email}}},
},
})
require.NoError(t, err)
require.Empty(t, resp.GetResult(),
"a user was created for the attacker-forged external identity %q - account pre-hijack succeeded", email)
if !time.Now().Before(deadline) {
return
}
time.Sleep(tick)
}
}