@@ -336,8 +336,9 @@ def test_offline_with_require_approval_policy_warns(self, monkeypatch, tmp_path)
336336class TestSaaSRoute :
337337 """Tests for the NODE9_API_KEY → SaaS routing path."""
338338
339- def test_saas_http_error_raises_runtime_error (self , monkeypatch , tmp_path ):
339+ def test_saas_http_error_raises_runtime_error (self , monkeypatch ):
340340 """SaaS HTTP errors (e.g. 401, 500) must propagate as RuntimeError, not auto-approve."""
341+ import http .client
341342 import urllib .error
342343 monkeypatch .setenv ("NODE9_API_KEY" , "test-key" )
343344 monkeypatch .setenv ("NODE9_API_URL" , "https://api.node9.ai/api/v1/intercept" )
@@ -346,7 +347,7 @@ def test_saas_http_error_raises_runtime_error(self, monkeypatch, tmp_path):
346347 url = "https://api.node9.ai/api/v1/intercept" ,
347348 code = 401 ,
348349 msg = "Unauthorized" ,
349- hdrs = None , # type: ignore[arg-type]
350+ hdrs = http . client . HTTPMessage (),
350351 fp = None ,
351352 )
352353 with patch ("urllib.request.urlopen" , side_effect = http_error ):
@@ -363,17 +364,45 @@ def test_saas_url_error_raises_runtime_error(self, monkeypatch):
363364 with pytest .raises (RuntimeError , match = "Failed to reach node9 SaaS" ):
364365 evaluate ("bash" , {"command" : "ls" })
365366
366- def test_saas_route_taken_when_api_key_set (self , monkeypatch ):
367- """When NODE9_API_KEY is set the SaaS path is taken regardless of daemon state ."""
368- monkeypatch .setenv ("NODE9_API_KEY" , "test-key" )
367+ def test_saas_route_taken_and_bearer_token_sent (self , monkeypatch ):
368+ """When NODE9_API_KEY is set: SaaS path is taken and Bearer token is in the request ."""
369+ monkeypatch .setenv ("NODE9_API_KEY" , "test-key-abc " )
369370 monkeypatch .setenv ("NODE9_API_URL" , "https://api.node9.ai/api/v1/intercept" )
370371
372+ captured_headers : list [dict ] = []
371373 approve_resp = _make_response ({"approved" : True })
372- with patch ("urllib.request.urlopen" , return_value = approve_resp ):
374+
375+ def capturing_urlopen (req , timeout ):
376+ captured_headers .append (dict (req .headers ))
377+ return approve_resp
378+
379+ with patch ("urllib.request.urlopen" , side_effect = capturing_urlopen ):
373380 with patch ("node9._client._daemon_reachable" ) as mock_check :
374381 evaluate ("bash" , {"command" : "ls" })
375- # daemon reachability must NOT be checked when API key is present
382+
376383 mock_check .assert_not_called ()
384+ assert captured_headers , "urlopen was never called"
385+ auth = captured_headers [0 ].get ("Authorization" , "" )
386+ assert auth == "Bearer test-key-abc" , f"Expected Bearer token, got: { auth !r} "
387+
388+ def test_saas_run_id_forwarded (self , monkeypatch ):
389+ """run_id is included in the SaaS request payload."""
390+ monkeypatch .setenv ("NODE9_API_KEY" , "test-key" )
391+ monkeypatch .setenv ("NODE9_API_URL" , "https://api.node9.ai/api/v1/intercept" )
392+
393+ sent : list [dict ] = []
394+ approve_resp = _make_response ({"approved" : True })
395+
396+ def capturing_urlopen (req , timeout ):
397+ if hasattr (req , "data" ) and req .data :
398+ sent .append (json .loads (req .data ))
399+ return approve_resp
400+
401+ with patch ("urllib.request.urlopen" , side_effect = capturing_urlopen ):
402+ evaluate ("bash" , {"command" : "ls" }, run_id = "saas-run-xyz" )
403+
404+ assert sent , "No request was sent"
405+ assert sent [0 ].get ("runId" ) == "saas-run-xyz"
377406
378407
379408class TestConfigure :
0 commit comments