@@ -429,3 +429,128 @@ TEST(ProviderInboundTest, AgentDrainIsIdempotent) {
429429
430430} // namespace
431431} // namespace croupier::sdk::test
432+
433+ namespace croupier ::sdk::test {
434+
435+ // ===== F:Provider 侧入站 payload 校验 =====
436+
437+ TEST (ProviderInboundTest, InputValidationRejectsInvalidPayload) {
438+ RawFakeAgent agent;
439+ std::thread agent_thread ([&] { agent.AcceptAndHandshake (); });
440+
441+ ClientConfig config = ProviderConfig (agent.address ());
442+ config.validate_input_payloads = true ;
443+ CroupierClient client (config);
444+ FunctionDescriptor desc;
445+ desc.id = " player.ban" ;
446+ desc.version = " 1.0.0" ;
447+ desc.capability = " action" ;
448+ desc.risk = " high" ;
449+ desc.input_schema =
450+ R"( {"type":"object","properties":{"id":{"type":"string"}},"required":["id"]})" ;
451+ std::atomic<int > calls{0 };
452+ ASSERT_TRUE (client.RegisterFunction (
453+ desc, [&](const std::string&, const std::string&) {
454+ calls.fetch_add (1 );
455+ return std::string (" ok" );
456+ }));
457+ ASSERT_TRUE (client.Connect ());
458+ agent_thread.join ();
459+
460+ // 缺 required 字段:回错误 payload,handler 不被调用
461+ agent.PushRequest (protocol::MSG_INVOKE_REQUEST , 9101 , InvokeBody (" player.ban" , " {}" ));
462+ auto resp = agent.ReadResponseFor (9101 );
463+ v1::InvokeResponse parsed;
464+ ASSERT_TRUE (parsed.ParseFromArray (resp.body .data (), static_cast <int >(resp.body .size ())));
465+ EXPECT_NE (parsed.payload ().find (" payload validation failed" ), std::string::npos)
466+ << " payload=" << parsed.payload ();
467+ EXPECT_EQ (calls.load (), 0 );
468+
469+ client.Close ();
470+ }
471+
472+ TEST (ProviderInboundTest, InputValidationPassesValidPayload) {
473+ RawFakeAgent agent;
474+ std::thread agent_thread ([&] { agent.AcceptAndHandshake (); });
475+
476+ ClientConfig config = ProviderConfig (agent.address ());
477+ config.validate_input_payloads = true ;
478+ CroupierClient client (config);
479+ FunctionDescriptor desc;
480+ desc.id = " player.ban" ;
481+ desc.version = " 1.0.0" ;
482+ desc.input_schema =
483+ R"( {"type":"object","properties":{"id":{"type":"string"}},"required":["id"]})" ;
484+ ASSERT_TRUE (client.RegisterFunction (
485+ desc, [&](const std::string&, const std::string& payload) {
486+ return " ban:" + payload;
487+ }));
488+ ASSERT_TRUE (client.Connect ());
489+ agent_thread.join ();
490+
491+ agent.PushRequest (protocol::MSG_INVOKE_REQUEST , 9102 ,
492+ InvokeBody (" player.ban" , R"( {"id":"p1"})" ));
493+ auto resp = agent.ReadResponseFor (9102 );
494+ v1::InvokeResponse parsed;
495+ ASSERT_TRUE (parsed.ParseFromArray (resp.body .data (), static_cast <int >(resp.body .size ())));
496+ EXPECT_EQ (parsed.payload (), " ban:{\" id\" :\" p1\" }" );
497+
498+ client.Close ();
499+ }
500+
501+ TEST (ProviderInboundTest, InputValidationDisabledKeepsLegacyBehavior) {
502+ RawFakeAgent agent;
503+ std::thread agent_thread ([&] { agent.AcceptAndHandshake (); });
504+
505+ ClientConfig config = ProviderConfig (agent.address ());
506+ // validate_input_payloads 默认 false
507+ CroupierClient client (config);
508+ FunctionDescriptor desc;
509+ desc.id = " player.ban" ;
510+ desc.version = " 1.0.0" ;
511+ desc.input_schema =
512+ R"( {"type":"object","properties":{"id":{"type":"string"}},"required":["id"]})" ;
513+ std::atomic<int > calls{0 };
514+ ASSERT_TRUE (client.RegisterFunction (
515+ desc, [&](const std::string&, const std::string&) {
516+ calls.fetch_add (1 );
517+ return std::string (" ok" );
518+ }));
519+ ASSERT_TRUE (client.Connect ());
520+ agent_thread.join ();
521+
522+ agent.PushRequest (protocol::MSG_INVOKE_REQUEST , 9103 , InvokeBody (" player.ban" , " {}" ));
523+ auto resp = agent.ReadResponseFor (9103 );
524+ v1::InvokeResponse parsed;
525+ ASSERT_TRUE (parsed.ParseFromArray (resp.body .data (), static_cast <int >(resp.body .size ())));
526+ EXPECT_EQ (parsed.payload (), " ok" );
527+ EXPECT_GE (calls.load (), 1 );
528+
529+ client.Close ();
530+ }
531+
532+ TEST (ProviderInboundTest, InputValidationSkipsWhenSchemaMissing) {
533+ RawFakeAgent agent;
534+ std::thread agent_thread ([&] { agent.AcceptAndHandshake (); });
535+
536+ ClientConfig config = ProviderConfig (agent.address ());
537+ config.validate_input_payloads = true ;
538+ CroupierClient client (config);
539+ FunctionDescriptor desc;
540+ desc.id = " player.free" ; // 未声明 input_schema
541+ desc.version = " 1.0.0" ;
542+ ASSERT_TRUE (client.RegisterFunction (
543+ desc, [](const std::string&, const std::string&) { return std::string (" ok" ); }));
544+ ASSERT_TRUE (client.Connect ());
545+ agent_thread.join ();
546+
547+ agent.PushRequest (protocol::MSG_INVOKE_REQUEST , 9104 , InvokeBody (" player.free" , " {}" ));
548+ auto resp = agent.ReadResponseFor (9104 );
549+ v1::InvokeResponse parsed;
550+ ASSERT_TRUE (parsed.ParseFromArray (resp.body .data (), static_cast <int >(resp.body .size ())));
551+ EXPECT_EQ (parsed.payload (), " ok" );
552+
553+ client.Close ();
554+ }
555+
556+ } // namespace croupier::sdk::test
0 commit comments