Skip to content

Commit 27e6d1b

Browse files
committed
test(sdk): 补齐新增功能的测试缺口——C++ manifest 帧回路 / Java 回包路径 / C# 派发接线
- C++:ManifestUploadTest.UploadsGzippedManifestToControlPlane—— RawFakeAgent 握手 + 独立控制面监听,真实 TCP 帧收发 + gzip 解压 断言 manifest 结构(此前仅编译验证的已知边界补齐) - Java:InboundInvokeResponseTest——handleInvokeRequest 反射路径, 违规 payload 回错误帧且 handler 不调用、合法调用、关闭兼容(3 例) - C#:InboundValidationDispatchTests——HandleInboundRequestAsync 反射派发路径,开关/错误响应/handler 调用断言(4 例)
1 parent 02a2e89 commit 27e6d1b

3 files changed

Lines changed: 350 additions & 0 deletions

File tree

sdks/cpp/tests/test_provider_inbound.cpp

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <gtest/gtest.h>
88
#include "croupier/sdk/croupier_client.h"
99
#include "croupier/sdk/protocol.h"
10+
#include "croupier/agent/v1/register.pb.h"
1011
#include "croupier/sdk/v1/provider.pb.h"
1112
#include "croupier/sdk/v1/invocation.pb.h"
1213

@@ -17,6 +18,7 @@
1718
#include <string>
1819
#include <thread>
1920
#include <vector>
21+
#include <zlib.h>
2022

2123
#ifdef _WIN32
2224
#include <winsock2.h>
@@ -428,6 +430,143 @@ TEST(ProviderInboundTest, AgentDrainIsIdempotent) {
428430
}
429431

430432
} // namespace
433+
434+
// ===== F:控制面 manifest 上传——端到端帧回路 =====
435+
436+
namespace {
437+
438+
socket_t listen_tcp(unsigned short* out_port) {
439+
socket_t fd = ::socket(AF_INET, SOCK_STREAM, 0);
440+
if (fd == INVALID_SOCK) return INVALID_SOCK;
441+
sockaddr_in addr{};
442+
addr.sin_family = AF_INET;
443+
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
444+
addr.sin_port = 0;
445+
if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0 ||
446+
::listen(fd, 1) != 0) {
447+
closesocket(fd);
448+
return INVALID_SOCK;
449+
}
450+
sockaddr_in bound{};
451+
socklen_t blen = sizeof(bound);
452+
::getsockname(fd, reinterpret_cast<sockaddr*>(&bound), &blen);
453+
*out_port = ntohs(bound.sin_port);
454+
return fd;
455+
}
456+
457+
std::vector<uint8_t> recv_exact(socket_t conn, size_t len) {
458+
std::vector<uint8_t> out(len);
459+
size_t got = 0;
460+
while (got < len) {
461+
int n = static_cast<int>(::recv(conn, reinterpret_cast<char*>(out.data()) + got,
462+
static_cast<int>(len - got), 0));
463+
if (n <= 0) return {};
464+
got += static_cast<size_t>(n);
465+
}
466+
return out;
467+
}
468+
469+
std::string gzip_decompress(const std::string& compressed) {
470+
std::string out;
471+
z_stream stream{};
472+
if (inflateInit2(&stream, 15 + 16) != Z_OK) return out;
473+
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(compressed.data()));
474+
stream.avail_in = static_cast<uInt>(compressed.size());
475+
char buffer[4096];
476+
int status = Z_OK;
477+
do {
478+
stream.next_out = reinterpret_cast<Bytef*>(buffer);
479+
stream.avail_out = sizeof(buffer);
480+
status = inflate(&stream, Z_NO_FLUSH);
481+
if (status != Z_OK && status != Z_STREAM_END && status != Z_BUF_ERROR) break;
482+
out.append(buffer, sizeof(buffer) - stream.avail_out);
483+
} while (status != Z_STREAM_END);
484+
inflateEnd(&stream);
485+
return out;
486+
}
487+
488+
} // namespace
489+
490+
TEST(ManifestUploadTest, UploadsGzippedManifestToControlPlane) {
491+
// Agent(复用 RawFakeAgent 完整握手)+ 控制面(独立监听)
492+
RawFakeAgent agent;
493+
std::thread agent_thread([&] { agent.AcceptAndHandshake(); });
494+
495+
unsigned short control_port = 0;
496+
socket_t control_fd = listen_tcp(&control_port);
497+
ASSERT_NE(control_fd, INVALID_SOCK);
498+
499+
std::atomic<bool> got_manifest{false};
500+
std::thread control_thread([&] {
501+
socket_t conn = ::accept(control_fd, nullptr, nullptr);
502+
if (conn == INVALID_SOCK) return;
503+
auto header = recv_exact(conn, 4);
504+
if (header.empty()) return;
505+
uint32_t len = (uint32_t(header[0]) << 24) | (uint32_t(header[1]) << 16) |
506+
(uint32_t(header[2]) << 8) | uint32_t(header[3]);
507+
auto frame_body = recv_exact(conn, len);
508+
constexpr size_t kHeaderSize = 8; // version(1) + msg_id(3) + req_id(4)
509+
if (frame_body.size() < kHeaderSize) return;
510+
uint32_t msg_id = protocol::GetMsgID(frame_body.data() + 1);
511+
ASSERT_EQ(msg_id, static_cast<unsigned>(protocol::MSG_REGISTER_CAPABILITIES_REQ));
512+
uint32_t req_id = (uint32_t(frame_body[4]) << 24) | (uint32_t(frame_body[5]) << 16) |
513+
(uint32_t(frame_body[6]) << 8) | uint32_t(frame_body[7]);
514+
515+
::croupier::agent::v1::RegisterCapabilitiesRequest req;
516+
ASSERT_TRUE(req.ParseFromArray(frame_body.data() + kHeaderSize,
517+
static_cast<int>(frame_body.size() - kHeaderSize)));
518+
std::string decompressed =
519+
gzip_decompress(std::string(req.manifest_json_gz().begin(),
520+
req.manifest_json_gz().end()));
521+
EXPECT_NE(decompressed.find("\"provider\""), std::string::npos);
522+
EXPECT_NE(decompressed.find("player.ban"), std::string::npos);
523+
got_manifest.store(true);
524+
525+
// 回确认帧
526+
::croupier::agent::v1::RegisterCapabilitiesResponse ack;
527+
std::string ack_out;
528+
ack.SerializeToString(&ack_out);
529+
auto resp_frame = protocol::NewMessage(
530+
protocol::GetResponseMsgID(msg_id), req_id,
531+
std::vector<uint8_t>(ack_out.begin(), ack_out.end()));
532+
std::vector<uint8_t> wrapped(4 + resp_frame.size());
533+
wrapped[0] = static_cast<uint8_t>((resp_frame.size() >> 24) & 0xFF);
534+
wrapped[1] = static_cast<uint8_t>((resp_frame.size() >> 16) & 0xFF);
535+
wrapped[2] = static_cast<uint8_t>((resp_frame.size() >> 8) & 0xFF);
536+
wrapped[3] = static_cast<uint8_t>(resp_frame.size() & 0xFF);
537+
std::memcpy(wrapped.data() + 4, resp_frame.data(), resp_frame.size());
538+
::send(conn, reinterpret_cast<const char*>(wrapped.data()),
539+
static_cast<int>(wrapped.size()), 0);
540+
closesocket(conn);
541+
});
542+
543+
ClientConfig config;
544+
config.agent_addr = agent.address();
545+
config.control_addr = "127.0.0.1:" + std::to_string(control_port);
546+
config.service_id = "cpp-manifest-test";
547+
config.game_id = "game-test";
548+
config.env = "development";
549+
config.timeout_seconds = 5;
550+
config.disable_logging = true;
551+
552+
CroupierClient client(config);
553+
FunctionDescriptor desc;
554+
desc.id = "player.ban";
555+
desc.version = "1.0.0";
556+
desc.input_schema = R"({"type":"object","properties":{"id":{"type":"string"}}})";
557+
ASSERT_TRUE(client.RegisterFunction(desc, [](const std::string&, const std::string&) {
558+
return std::string("ok");
559+
}));
560+
561+
// Connect 内部:agent 握手成功后向控制面上传 manifest(best-effort)
562+
ASSERT_TRUE(client.Connect());
563+
control_thread.join();
564+
agent_thread.join();
565+
client.Close();
566+
closesocket(control_fd);
567+
ASSERT_TRUE(got_manifest.load());
568+
}
569+
431570
} // namespace croupier::sdk::test
432571

433572
namespace croupier::sdk::test {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2025 Croupier Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System.Text.Json;
16+
using Croupier.Sdk.Models;
17+
using Croupier.Sdk.Transport;
18+
using Croupier.Sdk.V1;
19+
using Google.Protobuf;
20+
using Xunit;
21+
22+
namespace Croupier.Sdk.Tests;
23+
24+
/// <summary>
25+
/// F:Provider 侧入站校验的派发接线(HandleInboundRequestAsync 路径)。
26+
/// </summary>
27+
public class InboundValidationDispatchTests
28+
{
29+
private sealed class CountingHandler : IFunctionHandler
30+
{
31+
public int Calls;
32+
public Task<string> HandleAsync(FunctionContext context, string payload)
33+
{
34+
Calls++;
35+
return Task.FromResult("ok");
36+
}
37+
}
38+
39+
private static CroupierClient NewClient(bool validate, out CountingHandler handler)
40+
{
41+
handler = new CountingHandler();
42+
var config = new ClientConfig
43+
{
44+
ValidateInputPayloads = validate,
45+
};
46+
var client = new CroupierClient(config);
47+
client.RegisterFunction(new FunctionDescriptor
48+
{
49+
Id = "player.ban",
50+
Version = "1.0.0",
51+
InputSchema = "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}",
52+
}, handler);
53+
return client;
54+
}
55+
56+
private static byte[] InvokeBody(string functionId, string payload)
57+
{
58+
var request = new InvokeRequest
59+
{
60+
FunctionId = functionId,
61+
Payload = ByteString.CopyFromUtf8(payload),
62+
};
63+
return Google.Protobuf.MessageExtensions.ToByteArray(request);
64+
}
65+
66+
private static async Task<byte[]> DispatchAsync(CroupierClient client, byte[] body)
67+
{
68+
var method = typeof(CroupierClient).GetMethod("HandleInboundRequestAsync",
69+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
70+
Assert.NotNull(method);
71+
var task = (Task<byte[]>?)method!.Invoke(client, new object[] { Protocol.MsgInvokeRequest, 1, body });
72+
Assert.NotNull(task);
73+
return await task;
74+
}
75+
76+
private static async Task<JsonElement?> PayloadAsJsonAsync(byte[] response)
77+
{
78+
var invokeResponse = InvokeResponse.Parser.ParseFrom(response);
79+
var text = invokeResponse.Payload.ToStringUtf8();
80+
if (text.Length == 0 || text[0] != '{') return null;
81+
return JsonDocument.Parse(text).RootElement;
82+
}
83+
84+
[Fact]
85+
public async Task Config_Defaults_To_Off()
86+
{
87+
Assert.False(new ClientConfig().ValidateInputPayloads);
88+
}
89+
90+
[Fact]
91+
public async Task InvalidPayload_ReturnsErrorAndSkipsHandler()
92+
{
93+
var client = NewClient(validate: true, out var handler);
94+
var response = await DispatchAsync(client, InvokeBody("player.ban", "{}"));
95+
var payload = await PayloadAsJsonAsync(response);
96+
Assert.NotNull(payload);
97+
Assert.True((payload!.Value.TryGetProperty("error", out var error)
98+
&& error.GetString()!.Contains("payload validation failed")),
99+
"expected validation error, got: " + payload);
100+
Assert.Equal(0, handler.Calls);
101+
}
102+
103+
[Fact]
104+
public async Task ValidPayload_InvokesHandler()
105+
{
106+
var client = NewClient(validate: true, out var handler);
107+
var response = await DispatchAsync(client, InvokeBody("player.ban", "{\"id\":\"p1\"}"));
108+
var payload = await PayloadAsJsonAsync(response);
109+
Assert.Null(payload);
110+
Assert.Equal(1, handler.Calls);
111+
}
112+
113+
[Fact]
114+
public async Task DisabledFlag_KeepsLegacyBehavior()
115+
{
116+
var client = NewClient(validate: false, out var handler);
117+
var response = await DispatchAsync(client, InvokeBody("player.ban", "{}"));
118+
var payload = await PayloadAsJsonAsync(response);
119+
Assert.Null(payload);
120+
Assert.Equal(1, handler.Calls);
121+
}
122+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.github.cuihairu.croupier.sdk;
2+
3+
import io.github.cuihairu.croupier.sdk.transport.Protocol;
4+
import io.github.cuihairu.croupier.sdk.wire.SdkWireMessages;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.Timeout;
7+
8+
import java.lang.reflect.Method;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Map;
11+
import java.util.concurrent.atomic.AtomicInteger;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertFalse;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
/**
18+
* F:入站校验的 invokeInbound 回包路径——违规 payload 回
19+
* {"error":"payload validation failed: …"},handler 不被调用。
20+
*/
21+
public class InboundInvokeResponseTest {
22+
23+
private CroupierClientImpl newClient(boolean validate, FunctionDescriptor descriptor,
24+
FunctionHandler handler) throws CroupierException {
25+
ClientConfig config = new ClientConfig();
26+
config.setValidateInputPayloads(validate);
27+
CroupierClientImpl client = new CroupierClientImpl(config);
28+
if (descriptor != null) {
29+
client.registerFunction(descriptor, handler);
30+
}
31+
return client;
32+
}
33+
34+
private byte[] handleInvoke(CroupierClientImpl client, String functionId, String payload)
35+
throws Exception {
36+
Method method = CroupierClientImpl.class.getDeclaredMethod("handleInvokeRequest", byte[].class);
37+
method.setAccessible(true);
38+
return (byte[]) method.invoke(client, SdkWireMessages.encodeInvokeRequest(
39+
new SdkWireMessages.InvokeRequest(functionId, "",
40+
payload.getBytes(StandardCharsets.UTF_8), Map.of())));
41+
}
42+
43+
private FunctionDescriptor descriptorWithRequiredId() {
44+
FunctionDescriptor descriptor = new FunctionDescriptor("player.ban", "1.0.0");
45+
descriptor.setInputSchema(
46+
"{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}");
47+
return descriptor;
48+
}
49+
50+
@Test
51+
@Timeout(5)
52+
public void invalidPayloadReturnsErrorAndSkipsHandler() throws Exception {
53+
AtomicInteger calls = new AtomicInteger();
54+
CroupierClientImpl client = newClient(true, descriptorWithRequiredId(),
55+
(metadata, payload) -> {
56+
calls.incrementAndGet();
57+
return "ok";
58+
});
59+
60+
byte[] response = handleInvoke(client, "player.ban", "{}");
61+
String body = new String(response, StandardCharsets.UTF_8);
62+
assertTrue(body.contains("payload validation failed"), "body=" + body);
63+
assertFalse(body.contains("ok"));
64+
assertEquals(0, calls.get(), "handler must not be invoked");
65+
}
66+
67+
@Test
68+
@Timeout(5)
69+
public void validPayloadInvokesHandler() throws Exception {
70+
CroupierClientImpl client = newClient(true, descriptorWithRequiredId(),
71+
(metadata, payload) -> "done");
72+
byte[] response = handleInvoke(client, "player.ban", "{\"id\":\"p1\"}");
73+
assertEquals("done", new String(SdkWireMessages.decodeInvokeResponse(response).payload, StandardCharsets.UTF_8));
74+
}
75+
76+
@Test
77+
@Timeout(5)
78+
public void disabledFlagKeepsLegacyBehavior() throws Exception {
79+
AtomicInteger calls = new AtomicInteger();
80+
CroupierClientImpl client = newClient(false, descriptorWithRequiredId(),
81+
(metadata, payload) -> {
82+
calls.incrementAndGet();
83+
return "ok";
84+
});
85+
byte[] response = handleInvoke(client, "player.ban", "{}");
86+
assertEquals("ok", new String(SdkWireMessages.decodeInvokeResponse(response).payload, StandardCharsets.UTF_8));
87+
assertEquals(1, calls.get());
88+
}
89+
}

0 commit comments

Comments
 (0)