diff --git a/tests/stress/locustfile.py b/tests/stress/locustfile.py index bf5089e..fae5692 100644 --- a/tests/stress/locustfile.py +++ b/tests/stress/locustfile.py @@ -24,7 +24,9 @@ class DecnetUser(HttpUser): wait_time = between(0.01, 0.05) # near-zero think time — max pressure def _login_with_retry(self): - """Login with exponential backoff — handles connection storms.""" + """Login with exponential backoff — handles connection storms. + + Returns (access_token, must_change_password).""" for attempt in range(_MAX_LOGIN_RETRIES): resp = self.client.post( "/api/v1/auth/login", @@ -32,7 +34,8 @@ class DecnetUser(HttpUser): name="/api/v1/auth/login [on_start]", ) if resp.status_code == 200: - return resp.json()["access_token"] + body = resp.json() + return body["access_token"], bool(body.get("must_change_password", False)) # Status 0 = connection refused, retry with backoff if resp.status_code == 0 or resp.status_code >= 500: time.sleep(_LOGIN_BACKOFF_BASE * (2 ** attempt)) @@ -41,16 +44,20 @@ class DecnetUser(HttpUser): raise RuntimeError(f"Login failed after {_MAX_LOGIN_RETRIES} retries (last status: {resp.status_code})") def on_start(self): - token = self._login_with_retry() + token, must_change = self._login_with_retry() - # Clear must_change_password - self.client.post( - "/api/v1/auth/change-password", - json={"old_password": ADMIN_PASS, "new_password": ADMIN_PASS}, - headers={"Authorization": f"Bearer {token}"}, - ) - # Re-login for a clean token - self.token = self._login_with_retry() + # Only pay the change-password + re-login cost on the very first run + # against a fresh DB. Every run after that, must_change_password is + # already False — skip it or the login path becomes a bcrypt storm. + if must_change: + self.client.post( + "/api/v1/auth/change-password", + json={"old_password": ADMIN_PASS, "new_password": ADMIN_PASS}, + headers={"Authorization": f"Bearer {token}"}, + ) + token, _ = self._login_with_retry() + + self.token = token self.client.headers.update({"Authorization": f"Bearer {self.token}"}) # --- Read-hot paths (high weight) ---