@@ -256,3 +256,36 @@ def test_tokens_from_response_rejects_bool_expires_in(monkeypatch) -> None:
256256
257257 assert bool_expiry .expires_at is None
258258 assert valid_expiry .expires_at == 1060.0
259+
260+
261+ def test_login_rejects_callback_without_oauth_state (monkeypatch ) -> None :
262+ class CallbackWithoutState :
263+ def __init__ (self , port : int ) -> None :
264+ del port
265+
266+ def start (self ) -> None :
267+ pass
268+
269+ def serve_once (self , timeout_seconds : int ) -> tuple [str , None ]:
270+ del timeout_seconds
271+ return "authorization-code" , None
272+
273+ def close (self ) -> None :
274+ pass
275+
276+ monkeypatch .setattr (codex_oauth , "_CallbackServer" , CallbackWithoutState )
277+ monkeypatch .setattr (codex_oauth .console , "ensure_blocking_console" , lambda : None )
278+ monkeypatch .setattr (codex_oauth .console .console , "print" , lambda * args , ** kwargs : None )
279+ monkeypatch .setattr (
280+ codex_oauth ,
281+ "exchange_code_for_tokens" ,
282+ lambda code , verifier : pytest .fail ("token exchange must not run without OAuth state" ),
283+ )
284+ monkeypatch .setattr (
285+ codex_oauth ,
286+ "save_codex_tokens" ,
287+ lambda tokens : pytest .fail ("tokens must not be saved without OAuth state" ),
288+ )
289+
290+ with pytest .raises (codex_oauth .ProviderKeyError , match = "State parameter mismatch" ):
291+ codex_oauth .login_codex_oauth ()
0 commit comments