OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_regression.cpp

323 lines · cpp ref:
// Regression test: Verify non-DHT direct-connection paths still work.
// Tests server/client UDX stream connect and packet send/receive.
//
// Build:
//   cd /storage/projects/code/godot/hyperswarm-peer/test
//   g++ -std=c++17 -I../src -I../thirdparty/libudx/include \
//       test_regression.cpp ../src/noise_ik.cpp ../src/noise_handshake.cpp \
//       ../src/noise_stream.cpp ../src/holepuncher.cpp ../src/dht_rpc.cpp \
//       ../src/hyperdht.cpp ../src/dht_connector.cpp \
//       ../thirdparty/libudx/src/udx.c ../thirdparty/libudx/src/udx_bbr.c \
//       ../thirdparty/libudx/src/udx_rate.c ../thirdparty/libudx/src/cirbuf.c \
//       ../thirdparty/libudx/src/endian.c ../thirdparty/libudx/src/io_posix.c \
//       ../thirdparty/libudx/src/queue.c ../thirdparty/libudx/src/win_filter.c \
//       ../thirdparty/libudx/src/win_filter_f64.c \
//       -luv -lsodium -lpthread -o test_regression
//
// Run: ./test_regression

#include "noise_handshake.h"
#include "noise_stream.h"
#include "packet_queue.h"
#include "udx_transport.h"

#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>

#include <sodium.h>

// ---- Test harness ----

static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;

static void check(bool cond, const char *name) {
	tests_run++;
	if (cond) {
		tests_passed++;
		printf("  PASS: %s\n", name);
	} else {
		tests_failed++;
		printf("  FAIL: %s\n", name);
	}
}

static void sleep_ms(int ms) {
	std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}

// ---- Test: Unencrypted Server/Client ----

static void test_unencrypted_connection() {
	printf("\n--- Unencrypted Server/Client Connection ---\n");

	UdxTransport server;
	UdxTransport client;

	// Start server on random port.
	bool ok = server.start_server(0);
	check(ok, "Server started");
	sleep_ms(50); // Let thread bind.

	int port = server.bound_port();
	printf("  Server bound to port %d\n", port);
	check(port > 0, "Server has bound port");

	// Connect client.
	ok = client.start_client("127.0.0.1", port);
	check(ok, "Client started");
	sleep_ms(100); // Let connection establish.

	// Check for connected event on server side.
	// In the direct-connection model, the server sees the client when
	// UDX streams are connected. However, our current server doesn't
	// auto-create peers from raw packets (that's the DHT's job).
	// So for this test, verify that the transport is running.
	check(server.is_running(), "Server is running");
	check(client.is_running(), "Client is running");

	// Client sends a packet.
	const uint8_t test_data[] = "hello server";
	client.send(1, test_data, sizeof(test_data), PACKET_RELIABLE, 0);
	sleep_ms(50);

	// Verify no crashes occurred.
	check(server.is_running(), "Server still running after client send");
	check(client.is_running(), "Client still running after send");

	// Stop both.
	client.stop();
	server.stop();

	check(!server.is_running(), "Server stopped");
	check(!client.is_running(), "Client stopped");
}

// ---- Test: Encrypted Server/Client ----

static void test_encrypted_connection() {
	printf("\n--- Encrypted Server/Client Connection ---\n");

	sodium_init();

	UdxTransport server;
	UdxTransport client;

	// Generate server keypair.
	NoiseKeypair server_kp = NoiseHandshake::generate_keypair();

	// Start encrypted server.
	bool ok = server.start_server_encrypted(0, server_kp);
	check(ok, "Encrypted server started");
	sleep_ms(50);

	int port = server.bound_port();
	printf("  Server bound to port %d\n", port);
	check(port > 0, "Encrypted server has bound port");

	// Connect encrypted client (knows server's public key).
	ok = client.start_client_encrypted("127.0.0.1", port, server_kp.public_key);
	check(ok, "Encrypted client started");
	sleep_ms(200); // Handshake takes a bit longer.

	check(server.is_running(), "Encrypted server running");
	check(client.is_running(), "Encrypted client running");

	// Stop both.
	client.stop();
	server.stop();

	check(!server.is_running(), "Encrypted server stopped");
	check(!client.is_running(), "Encrypted client stopped");
}

// ---- Test: DHT start without connect ----

static void test_dht_start_standalone() {
	printf("\n--- DHT Start Standalone ---\n");

	UdxTransport transport;

	// Generate Ed25519 keypair.
	Ed25519KeyPair kp;
	crypto_sign_keypair(kp.public_key, kp.secret_key);

	// Start DHT (this should start the thread and bind a socket).
	// Use invalid bootstrap to avoid actual network traffic.
	const char *bootstrap[] = { "127.0.0.1:1" }; // Won't respond.
	transport.start_dht(kp, bootstrap, 1);

	sleep_ms(100); // Let thread start and init.

	check(transport.is_running(), "Transport running after start_dht");
	check(transport.bound_port() > 0, "DHT has bound port");

	// DHT should be active after init command processes.
	sleep_ms(200);
	check(transport.is_dht_active(), "DHT is active");

	// Stop.
	transport.stop();
	check(!transport.is_running(), "Transport stopped");
	check(!transport.is_dht_active(), "DHT not active after stop");
}

// ---- Test: Multiple start/stop cycles ----

static void test_start_stop_cycles() {
	printf("\n--- Start/Stop Cycles ---\n");

	UdxTransport transport;

	for (int i = 0; i < 3; i++) {
		bool ok = transport.start_server(0);
		check(ok, "Start cycle ok");
		sleep_ms(50);
		check(transport.is_running(), "Running in cycle");
		transport.stop();
		check(!transport.is_running(), "Stopped in cycle");
	}

	check(true, "3 start/stop cycles completed without crash");
}

// ---- Test: PacketQueue ----

static void test_packet_queue() {
	printf("\n--- PacketQueue ---\n");

	PacketQueue queue;
	check(queue.size() == 0, "Queue empty initially");

	// Push some packets.
	uint8_t data1[] = { 1, 2, 3 };
	uint8_t data2[] = { 4, 5, 6, 7, 8 };

	Packet p1(data1, 3, 1, 0, PACKET_RELIABLE);
	Packet p2(data2, 5, 2, 1, PACKET_UNRELIABLE);

	queue.push(std::move(p1));
	queue.push(std::move(p2));

	check(queue.size() == 2, "Queue has 2 packets");

	// Peek.
	Packet peeked;
	bool ok = queue.peek(peeked);
	check(ok, "Peek succeeded");
	check(peeked.peer_id == 1, "Peeked packet has correct peer_id");
	check(queue.size() == 2, "Queue still has 2 after peek");

	// Pop.
	Packet popped;
	ok = queue.pop(popped);
	check(ok, "Pop succeeded");
	check(popped.peer_id == 1, "Popped first packet");
	check(popped.data.size() == 3, "First packet data size");
	check(popped.data[0] == 1 && popped.data[1] == 2 && popped.data[2] == 3,
		  "First packet data content");
	check(popped.mode == PACKET_RELIABLE, "First packet mode");

	ok = queue.pop(popped);
	check(ok, "Second pop succeeded");
	check(popped.peer_id == 2, "Second packet peer_id");
	check(popped.channel == 1, "Second packet channel");
	check(popped.mode == PACKET_UNRELIABLE, "Second packet mode");

	check(queue.size() == 0, "Queue empty after pops");

	ok = queue.pop(popped);
	check(!ok, "Pop from empty queue returns false");
}

// ---- Test: NoiseStream encrypt/decrypt ----

static void test_noise_stream() {
	printf("\n--- NoiseStream Encrypt/Decrypt ---\n");

	uint8_t tx_key[32], rx_key[32];
	randombytes_buf(tx_key, 32);
	randombytes_buf(rx_key, 32);

	NoiseStream sender;
	NoiseStream receiver;

	// Sender initializes TX.
	sender.init_tx(tx_key);
	check(sender.tx_ready(), "Sender TX ready");

	// Receiver initializes TX with different key (for their outbound).
	receiver.init_tx(rx_key);
	check(receiver.tx_ready(), "Receiver TX ready");

	// Exchange headers: sender's TX header → receiver's RX.
	bool ok = receiver.init_rx(tx_key, sender.tx_header());
	check(ok, "Receiver RX initialized with sender's header");
	check(receiver.rx_ready(), "Receiver RX ready");

	// Receiver's TX header → sender's RX.
	ok = sender.init_rx(rx_key, receiver.tx_header());
	check(ok, "Sender RX initialized with receiver's header");
	check(sender.rx_ready(), "Sender RX ready");

	// Send a message: sender → receiver.
	uint8_t message[] = "Hello, encrypted world!";
	auto ciphertext = sender.encrypt(message, sizeof(message));
	check(ciphertext.size() > sizeof(message), "Ciphertext larger than plaintext");

	auto plaintext = receiver.decrypt(ciphertext.data(), ciphertext.size());
	check(plaintext.size() == sizeof(message), "Decrypted size matches");
	check(memcmp(plaintext.data(), message, sizeof(message)) == 0, "Decrypted content matches");

	// Send multiple messages to test nonce advancement.
	for (int i = 0; i < 10; i++) {
		char msg[64];
		sprintf(msg, "Message %d", i);
		auto ct = sender.encrypt((uint8_t *)msg, strlen(msg) + 1);
		auto pt = receiver.decrypt(ct.data(), ct.size());
		check(pt.size() == strlen(msg) + 1 && strcmp((char *)pt.data(), msg) == 0,
			  i == 0 ? "Stream message 0" :
			  i == 9 ? "Stream message 9" : nullptr);
		if (i != 0 && i != 9) {
			// Silent pass — only log first and last.
			tests_run++;
			tests_passed++;
		}
	}

	// Bidirectional: receiver → sender.
	uint8_t reply[] = "Reply from receiver";
	auto ct_reply = receiver.encrypt(reply, sizeof(reply));
	auto pt_reply = sender.decrypt(ct_reply.data(), ct_reply.size());
	check(pt_reply.size() == sizeof(reply), "Bidirectional decrypt size");
	check(memcmp(pt_reply.data(), reply, sizeof(reply)) == 0, "Bidirectional content");
}

// ---- Main ----

int main() {
	printf("=== Regression Tests ===\n");
	printf("Verify non-DHT paths still work after Phase 4 integration.\n");

	if (sodium_init() < 0) {
		printf("FATAL: sodium_init failed\n");
		return 1;
	}

	test_packet_queue();
	test_noise_stream();
	test_unencrypted_connection();
	test_encrypted_connection();
	test_dht_start_standalone();
	test_start_stop_cycles();

	printf("\n=== Summary: %d/%d passed", tests_passed, tests_run);
	if (tests_failed > 0) printf(", %d FAILED", tests_failed);
	printf(" ===\n\n");

	return tests_failed > 0 ? 1 : 0;
}