@@ -85,6 +85,7 @@ def test_clawbot_message_routes_to_analysis_and_formats_text():
8585 return_value = analysis_result ,
8686 ) as handle_analysis :
8787 response = handle_clawbot_message (
88+ _build_request (),
8889 ClawBotMessageRequest (message = "帮我分析贵州茅台" , mode = "analysis" )
8990 )
9091
@@ -114,6 +115,7 @@ def test_clawbot_message_routes_to_agent_with_stable_session_id():
114115 with patch ("api.v1.endpoints.clawbot.get_config" , return_value = config ), \
115116 patch ("api.v1.endpoints.clawbot._build_executor" , return_value = executor ):
116117 response = handle_clawbot_message (
118+ _build_request (),
117119 ClawBotMessageRequest (
118120 message = "用缠论分析 600519" ,
119121 mode = "agent" ,
@@ -148,6 +150,7 @@ def test_clawbot_message_auto_mode_falls_back_to_agent_for_plain_english_text():
148150 patch ("api.v1.endpoints.clawbot.CommandDispatcher._resolve_stock_code_from_text" , return_value = None ), \
149151 patch ("api.v1.endpoints.clawbot._handle_sync_analysis" ) as handle_analysis :
150152 response = handle_clawbot_message (
153+ _build_request (),
151154 ClawBotMessageRequest (message = "I need advice" , mode = "auto" , user_id = "wx_user_002" )
152155 )
153156
@@ -173,6 +176,7 @@ def test_clawbot_message_auto_mode_falls_back_to_agent_for_uppercase_english_tex
173176 patch ("api.v1.endpoints.clawbot.CommandDispatcher._resolve_stock_code_from_text" , return_value = None ), \
174177 patch ("api.v1.endpoints.clawbot._handle_sync_analysis" ) as handle_analysis :
175178 response = handle_clawbot_message (
179+ _build_request (),
176180 ClawBotMessageRequest (message = "I NEED ADVICE" , mode = "auto" , user_id = "wx_user_004" )
177181 )
178182
@@ -210,6 +214,7 @@ def test_clawbot_message_auto_mode_routes_direct_ascii_ticker_to_analysis(
210214 return_value = analysis_result ,
211215 ) as handle_analysis :
212216 response = handle_clawbot_message (
217+ _build_request (),
213218 ClawBotMessageRequest (message = message , mode = "auto" )
214219 )
215220
@@ -250,6 +255,7 @@ def test_clawbot_message_auto_mode_routes_english_request_with_ascii_ticker_to_a
250255 return_value = analysis_result ,
251256 ) as handle_analysis :
252257 response = handle_clawbot_message (
258+ _build_request (),
253259 ClawBotMessageRequest (message = message , mode = "auto" )
254260 )
255261
@@ -267,6 +273,7 @@ def test_clawbot_analysis_mode_rejects_plain_english_text():
267273 """mode=analysis should return unresolved_stock for non-stock plain text."""
268274 try :
269275 handle_clawbot_message (
276+ _build_request (),
270277 ClawBotMessageRequest (message = "I need advice" , mode = "analysis" )
271278 )
272279 assert False , "Expected HTTPException"
@@ -282,6 +289,7 @@ def test_clawbot_message_returns_consistent_error_when_agent_unavailable():
282289 with patch ("api.v1.endpoints.clawbot._build_executor" ) as build_executor :
283290 try :
284291 handle_clawbot_message (
292+ _build_request (),
285293 ClawBotMessageRequest (message = "用缠论分析 600519" , mode = "agent" )
286294 )
287295 assert False , "Expected HTTPException"
@@ -312,6 +320,7 @@ def test_clawbot_message_validation_handler_returns_error_response_shape():
312320def test_clawbot_message_http_handler_normalizes_analysis_validation_error ():
313321 try :
314322 handle_clawbot_message (
323+ _build_request (),
315324 ClawBotMessageRequest (message = "??" , mode = "analysis" , stock_code = "??" )
316325 )
317326 assert False , "Expected HTTPException"
@@ -331,6 +340,7 @@ def test_clawbot_message_http_handler_wraps_executor_exception_as_agent_failed()
331340 patch ("api.v1.endpoints.clawbot._build_executor" , side_effect = RuntimeError ("executor boom" )):
332341 try :
333342 handle_clawbot_message (
343+ _build_request (),
334344 ClawBotMessageRequest (
335345 message = "用缠论分析 600519" ,
336346 mode = "agent" ,
@@ -362,6 +372,7 @@ def test_clawbot_auto_mode_rejects_plain_english_word_as_direct_ticker(word: str
362372 patch ("api.v1.endpoints.clawbot.CommandDispatcher._resolve_stock_code_from_text" , return_value = None ), \
363373 patch ("api.v1.endpoints.clawbot._handle_sync_analysis" ) as handle_analysis :
364374 response = handle_clawbot_message (
375+ _build_request (),
365376 ClawBotMessageRequest (message = word , mode = "auto" )
366377 )
367378
@@ -384,6 +395,7 @@ def test_clawbot_analysis_mode_resolves_word_like_ticker():
384395 return_value = analysis_result ,
385396 ) as handle_analysis :
386397 response = handle_clawbot_message (
398+ _build_request (),
387399 ClawBotMessageRequest (message = "SHOP" , mode = "analysis" )
388400 )
389401
@@ -410,6 +422,7 @@ def test_clawbot_auto_mode_routes_analyze_word_like_ticker_to_analysis():
410422 return_value = analysis_result ,
411423 ) as handle_analysis :
412424 response = handle_clawbot_message (
425+ _build_request (),
413426 ClawBotMessageRequest (message = "analyze SHOP" , mode = "auto" )
414427 )
415428
@@ -446,6 +459,7 @@ def test_clawbot_auto_mode_routes_single_letter_ticker_in_text(
446459 return_value = analysis_result ,
447460 ) as handle_analysis :
448461 response = handle_clawbot_message (
462+ _build_request (),
449463 ClawBotMessageRequest (message = message , mode = "auto" )
450464 )
451465
@@ -481,9 +495,73 @@ def test_clawbot_auto_mode_resolves_lowercase_ticker_in_free_text(
481495 return_value = analysis_result ,
482496 ) as handle_analysis :
483497 response = handle_clawbot_message (
498+ _build_request (),
484499 ClawBotMessageRequest (message = message , mode = "auto" )
485500 )
486501
487502 assert response .mode == "analysis"
488503 assert response .stock_code == expected_code
489504 handle_analysis .assert_called_once ()
505+
506+
507+ def test_clawbot_message_rejects_request_when_secret_mismatch ():
508+ """When CLAWBOT_SECRET is set, mismatched header should return 401."""
509+ import os
510+ from starlette .datastructures import Headers
511+
512+ def _build_request_with_header (secret_value : str ) -> Request :
513+ encoded = secret_value .encode ()
514+ return Request (
515+ {
516+ "type" : "http" ,
517+ "http_version" : "1.1" ,
518+ "method" : "POST" ,
519+ "scheme" : "http" ,
520+ "path" : "/api/v1/clawbot/message" ,
521+ "raw_path" : b"/api/v1/clawbot/message" ,
522+ "query_string" : b"" ,
523+ "headers" : [(b"x-clawbot-secret" , encoded )],
524+ "client" : ("testclient" , 50000 ),
525+ "server" : ("testserver" , 80 ),
526+ }
527+ )
528+
529+ with patch .dict (os .environ , {"CLAWBOT_SECRET" : "correct_secret" }):
530+ # Wrong secret → 401
531+ try :
532+ handle_clawbot_message (
533+ _build_request_with_header ("wrong_secret" ),
534+ ClawBotMessageRequest (message = "分析茅台" , mode = "auto" ),
535+ )
536+ assert False , "Expected HTTPException 401"
537+ except HTTPException as exc :
538+ assert exc .status_code == 401
539+ assert exc .detail ["error" ] == "unauthorized"
540+
541+ # Correct secret → proceeds past auth (agent unavailable → 400 is fine)
542+ config = SimpleNamespace (is_agent_available = lambda : False )
543+ with patch ("api.v1.endpoints.clawbot.get_config" , return_value = config ):
544+ try :
545+ handle_clawbot_message (
546+ _build_request_with_header ("correct_secret" ),
547+ ClawBotMessageRequest (message = "分析茅台" , mode = "auto" ),
548+ )
549+ except HTTPException as exc :
550+ assert exc .status_code != 401 , "Should not get 401 with correct secret"
551+
552+
553+ def test_clawbot_message_allows_request_when_no_secret_configured ():
554+ """When CLAWBOT_SECRET is not set, all requests should proceed past auth."""
555+ import os
556+
557+ env_without_secret = {k : v for k , v in os .environ .items () if k != "CLAWBOT_SECRET" }
558+ with patch .dict (os .environ , env_without_secret , clear = True ):
559+ config = SimpleNamespace (is_agent_available = lambda : False )
560+ with patch ("api.v1.endpoints.clawbot.get_config" , return_value = config ):
561+ try :
562+ handle_clawbot_message (
563+ _build_request (),
564+ ClawBotMessageRequest (message = "分析茅台" , mode = "auto" ),
565+ )
566+ except HTTPException as exc :
567+ assert exc .status_code != 401 , "No secret configured, should not get 401"
0 commit comments