Implements the full M1 milestone. Two clients authenticate over TLS 1.3 (guest + Argon2id password) and exchange channel + private text messages through a real server. All five ctest --preset m1-dev tests pass in ~1 s. Key components added: - vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3) - FrameCodec feed+emit, encode/decode_envelope, protobuf codegen - TcpServerConn with blocking TLS handshake thread + tls_read_loop - TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client) - WorkerPool (3 threads, used for Argon2id) - Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin - ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint - ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated - SessionRegistry: channel tree, user map, text routing, broadcast - vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect - voicecat-admin CLI: account add/reset/del/list - test_m1_integration: M1 exit criterion, verified green Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope was adding the [4-byte len] prefix, then TcpServerConn::send_frame added a second one, causing the client to parse [len][proto] as protobuf (silent failure). Fixed by serializing raw protobuf bytes in send_envelope and letting send_frame apply the single length prefix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
3.5 KiB
C++
106 lines
3.5 KiB
C++
/*
|
|
* test_tcp_loopback — in-process TCP acceptor + client, sends 10 frames.
|
|
* Runs only under m1-dev (requires Asio).
|
|
*/
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdio>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "net/transport.h"
|
|
|
|
using namespace voicecat::net;
|
|
|
|
static int g_failures = 0;
|
|
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
int main() {
|
|
constexpr int kFrameCount = 10;
|
|
constexpr uint16_t kPort = 19850;
|
|
|
|
std::mutex mtx;
|
|
std::condition_variable cv;
|
|
std::vector<std::vector<uint8_t>> received;
|
|
std::atomic<bool> server_connected{false};
|
|
|
|
// Server io_context + acceptor.
|
|
asio::io_context server_io;
|
|
auto work = asio::make_work_guard(server_io);
|
|
std::thread server_thread([&] { server_io.run(); });
|
|
|
|
TcpAcceptor acceptor(server_io, kPort, [&](asio::ip::tcp::socket sock) {
|
|
TcpChannelCallbacks cbs;
|
|
cbs.on_frame = [&](std::vector<uint8_t> frame) {
|
|
std::lock_guard<std::mutex> lk(mtx);
|
|
received.push_back(std::move(frame));
|
|
cv.notify_all();
|
|
};
|
|
cbs.on_connected = [&] { server_connected.store(true); };
|
|
auto conn = std::make_shared<TcpServerConn>(std::move(sock), std::move(cbs));
|
|
return conn;
|
|
});
|
|
acceptor.start();
|
|
|
|
// Client.
|
|
std::atomic<bool> client_connected{false};
|
|
TcpChannelCallbacks client_cbs;
|
|
client_cbs.on_connected = [&] { client_connected.store(true); };
|
|
client_cbs.on_connect_error = [](std::error_code ec) {
|
|
std::printf("connect error: %s\n", ec.message().c_str());
|
|
};
|
|
|
|
TcpControlChannel client(std::move(client_cbs));
|
|
client.async_connect("127.0.0.1", kPort);
|
|
|
|
// Wait for connection.
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
while (!client_connected.load() &&
|
|
std::chrono::steady_clock::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
CHECK(client_connected.load());
|
|
|
|
// Send kFrameCount distinct frames.
|
|
for (int i = 0; i < kFrameCount; ++i) {
|
|
std::vector<uint8_t> payload = {static_cast<uint8_t>(i), 0xAB, 0xCD};
|
|
client.send_frame(payload);
|
|
}
|
|
|
|
// Wait for all frames to arrive on the server side.
|
|
{
|
|
std::unique_lock<std::mutex> lk(mtx);
|
|
bool ok = cv.wait_for(lk, std::chrono::seconds(5),
|
|
[&] { return static_cast<int>(received.size()) >= kFrameCount; });
|
|
CHECK(ok);
|
|
}
|
|
CHECK(static_cast<int>(received.size()) == kFrameCount);
|
|
for (int i = 0; i < kFrameCount && i < static_cast<int>(received.size()); ++i) {
|
|
CHECK(received[i].size() == 3);
|
|
if (!received[i].empty()) CHECK(received[i][0] == static_cast<uint8_t>(i));
|
|
}
|
|
|
|
// Clean up.
|
|
client.close();
|
|
acceptor.stop();
|
|
work.reset();
|
|
server_io.stop();
|
|
server_thread.join();
|
|
|
|
if (g_failures == 0) {
|
|
std::printf("tcp_loopback: all checks passed\n");
|
|
return 0;
|
|
}
|
|
std::printf("tcp_loopback: %d failure(s)\n", g_failures);
|
|
return 1;
|
|
}
|