Flaky: UserControllerSpec > User list in grails-test-examples/scaffolding submits the login form without a session cookie
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
`grails-test-examples/scaffolding` fails intermittently in CI, on four occasions between 24 and 25 August 2026, on four unrelated branches. The root cause is not yet known; this issue records what is established so far.
## The test
`com.example.UserControllerSpec > User list` — `grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy`
```groovy
void "User list"() {
when: 'an unauthenticated user requests the user list and signs in when prompted'
via(UserListPage)
at(LoginPage).login()
then: 'the saved request redirects to the user list'
at(UserListPage)
scaffoldTable
}
```
It fails inside `LoginPage.login()`, on the wait that follows the click:
```groovy
void login(String username = 'test@grails.org', String password = 'letmein') {
this.username = username
this.password = password
loginButton.click()
waitFor { title != pageTitle && $('input', name: 'username').empty } // times out here
}
```
```
geb.waiting.WaitTimeoutException: condition did not pass in 30 seconds (failed with exception)
at com.example.pages.LoginPage.login(LoginPage.groovy:39)
at com.example.UserControllerSpec.User list(UserControllerSpec.groovy:48)
Caused by: Assertion failed:
title != pageTitle && $('input', name: 'username').empty
| | | |
| | | false
| | 'Please sign in'
| false
'Please sign in'
```
That is the trace from #16150, #16184 and #16209, which are identical to each other: the same
condition, the same thirty seconds, `LoginPage.groovy:39` in `login_closure1`, and
`UserControllerSpec.groovy:48`.
The fourth, #16215, differs in one respect: `LoginPage.groovy:43`, in `login_closure2`. That branch
carries a proposed change to this page object which adds lines above the wait, so the same wait on
the same condition reports a different line and closure. It is the same failure, not a different
one - and worth noting because it shows the wait that fails is the one after the click, on a branch
where the form is verified as filled before clicking.
## Occurrences
| Date (UTC) | PR | Job | Duration |
|---|---|---|---|
| 2026-08-24 05:34 | #16150 | [Build Grails with Groovy snapshot (shard 2)](https://github.com/apache/grails-core/actions/runs/32693786704/job/97332626174) | 55m |
| 2026-08-24 05:34 | #16184 | [Functional Tests (Java 25, indy=false)](https://github.com/apache/grails-core/actions/runs/32693980319/job/97332597354) | 59m |
| 2026-08-24 21:35 | #16209 | [Functional Tests (Java 25, indy=false)](https://github.com/apache/grails-core/actions/runs/32780264043/job/97600561629) | 51m |
| 2026-08-24 23:29 | #16215 | [Functional Tests (Java 21, indy=true)](https://github.com/apache/grails-core/actions/runs/32789589183/job/97628349227) | 43m |
None of those four branches touches this application, Spring Security, or Geb. It has been seen on Java 21 and Java 25, with indy on and off, and in two different workflows. In each run the other two specifications of the same application - `BookControllerSpec` and `UserCommunityControllerSpec`, which sign in the same way - passed.
## What the browser was looking at
`@ContainerGebConfiguration(reporting = true)` means Geb captures the page at the failure. Three of
the four runs upload it - #16184, #16209 and #16215 - and those three are the same to the byte. The
#16150 run is in a workflow that uploads no Geb report, so there is no capture for it:
- 1089 bytes, `Please sign in` - Spring Security's generated login page
- both fields **empty**, showing their placeholders
- the focus ring still on the username field, where `autofocus` puts it on load
- **no error**: not `/login?error`, no "Bad credentials" banner
- a `_csrf` hidden field present
Two things follow. The page was loaded *after* the credentials were typed, because a page that had been typed into would hold the values and would have the focus on the second field. And the request that produced it was not a rejected authentication attempt, because that renders `?error` with a banner.
## What the failure is not
Determined by injecting faults locally into the same test and comparing the captured page:
| Injected fault | Page produced | Matches CI |
|---|---|---|
| Session cookie deleted between filling and submitting | 1089 bytes, login page, no error, autofocus | **yes, byte for byte** |
| Valid session, `_csrf` field overwritten with a stale value | 283 bytes, `Whitelabel Error Page` (403) | no |
So the form reaches the server **without a session cookie**, rather than with a stale or mismatched CSRF token. Missing session and wrong token are answered differently: with no session Spring Security treats the caller as anonymous and returns the login page, while a wrong token on a live session is denied with a 403.
It is also not the credentials or the user data: `UserService.loadUserByUsername` looks the user up by email, and a lookup failure would produce `?error`, which is absent.
## What is still unknown
Why the browser has no session cookie when the form is submitted. The specification clears cookies in `setup()`:
```groovy
void setup() {
clearCookiesQuietly()
}
```
and `ContainerGebSpec` holds `@Shared static GebTestManager testManager`, so the browser is shared between specifications in a worker. Whether the cookie is cleared, never set, scoped to a different host than the one the form posts to, or lost some other way is not established.
A consequence worth noting for anyone attempting a fix: if the session is genuinely gone, Spring Security's saved request goes with it, so simply submitting the form again is not enough - the login then succeeds but lands on `/` rather than the user list, and the specification fails on `at(UserListPage)` instead. Confirmed locally.
## Reproducing
Not reproduced on demand. Four consecutive local runs of `:grails-test-examples-scaffolding:integrationTest` passed. The failure signature can be produced deliberately by deleting the browser's cookies between filling the form and clicking, which is how the comparison above was made.
## Suggested next step
Capture the browser's cookies and `currentUrl` into the Geb report when that wait fails. One further occurrence would then distinguish "no cookie at all" from "a cookie scoped to another host", which is what the fix depends on.
## Other CI flakes tracked separately
- #16218 - the whole-repository build stops after its last task and holds a runner until the six hour kill.
- #16219 - `AsyncPromiseSpec` times out waiting for a response from an endpoint that is bounded well below the client timeout.
Different tests and different mechanisms; listed so that anyone working through CI flakiness can see what else is open.
Contributor guide
Research direction
Start with `grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy`, `LoginPage.groovy`, and the shared `ContainerGebSpec` test manager; read the failing wait and cookie-clearing setup. Run `:grails-test-examples-scaffolding:integrationTest` and, if the failure recurs, capture cookies and `currentUrl` in the Geb report. Done means identifying why the form submission lacks its session cookie and fixing the failure without losing the saved-request redirect to the user list.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100