diff --git a/tests/integration/test_preflight.py b/tests/integration/test_preflight.py index 6acf868..7f333ae 100644 --- a/tests/integration/test_preflight.py +++ b/tests/integration/test_preflight.py @@ -2,10 +2,11 @@ import subprocess import sys import os +import shutil import pytest sys.path.insert(0, os.path.dirname(__file__)) -from common import BUILD_DIR, CLIENT_CMD, SERVER_CMD +from common import BUILD_DIR, CLIENT_CMD, SERVER_CMD, TEST_DATA_DIR, run_client, verify_transfer class TestHelp: @@ -79,3 +80,58 @@ class TestServerPort: finally: proc.terminate() proc.wait(timeout=5) + + +def _seed_protocol_source(source): + os.makedirs(source, exist_ok=True) + with open(os.path.join(source, "p.txt"), "w") as fh: + fh.write("protocol test\n") + os.makedirs(os.path.join(source, "nested"), exist_ok=True) + with open(os.path.join(source, "nested", "deep.txt"), "w") as fh: + fh.write("deep file\n") + + +class TestProtocol: + @pytest.mark.ci + def test_protocol_current_version_accepted(self, shared_server): + """--protocol=2.16.0 (the current PROTOCOL_VERSION) is accepted and the + transfer completes normally.""" + source = os.path.join(TEST_DATA_DIR, "proto_ok_src") + dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst") + shutil.rmtree(dest, ignore_errors=True) + os.makedirs(dest) + _seed_protocol_source(source) + result, _ = run_client(source, dest, flags=["--protocol=2.16.0"], + port=shared_server.port) + assert result.returncode == 0, \ + f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}" + received = os.path.join(dest, os.path.abspath(source).lstrip(os.sep)) + mismatches, missing = verify_transfer(source, received) + assert not mismatches and not missing, \ + f"transfer mismatch: missing={missing} mismatches={mismatches}" + + @pytest.mark.ci + def test_protocol_rejects_other_versions(self, shared_server): + """Other versions are rejected up front, before connecting.""" + source = os.path.join(TEST_DATA_DIR, "proto_reject_src") + dest = os.path.join(TEST_DATA_DIR, "proto_reject_dst") + shutil.rmtree(dest, ignore_errors=True) + os.makedirs(dest) + _seed_protocol_source(source) + for bad in ("2.15.0", "2.17.0", "216", "31"): + result, _ = run_client(source, dest, flags=[f"--protocol={bad}"], + port=shared_server.port) + assert result.returncode != 0, f"--protocol={bad} should be rejected" + + @pytest.mark.ci + def test_protocol_rejects_garbage(self, shared_server): + """Garbage/empty --protocol values are rejected up front.""" + source = os.path.join(TEST_DATA_DIR, "proto_garbage_src") + dest = os.path.join(TEST_DATA_DIR, "proto_garbage_dst") + shutil.rmtree(dest, ignore_errors=True) + os.makedirs(dest) + _seed_protocol_source(source) + for bad in ("abc", ""): + result, _ = run_client(source, dest, flags=[f"--protocol={bad}"], + port=shared_server.port) + assert result.returncode != 0, f"--protocol={bad} should be rejected" diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 9bbe79d..a022e89 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -210,6 +210,66 @@ static void test_parse_args_version() { config_delete(cfg); } +/* --protocol=NUM forces the wire protocol version: the current PROTOCOL_VERSION + * is accepted (stored into config->version, which the config frame transmits), + * and any other value is rejected. Client-only: no server-side flag exists. */ +static void test_parse_args_protocol_accept_current() { + Config* cfg = valid_client_config(); + EXPECT_NOT_NULL(cfg); + char* argv_equals[] = {"fastsync", "--source-dir", "/src", + "--dest-dir", "/dst", "--protocol=2.16.0"}; + int positional_args[2]; + int positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 6, argv_equals, positional_args, &positional_count), 0); + EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION); + config_delete(cfg); + + cfg = valid_client_config(); + EXPECT_NOT_NULL(cfg); + char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir", + "/dst", "--protocol", "2.16.0"}; + positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0); + EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION); + config_delete(cfg); +} + +/* Any --protocol value other than the current PROTOCOL_VERSION must end in + * failure (parse_args simply stores it; validate_config rejects it up front). */ +static void test_parse_args_protocol_rejects_other_versions() { + static const char* const bad_versions[] = {"2.16", "2.15.0", "2.17.0", "216", "31", "abc", ""}; + for (size_t i = 0; i < sizeof(bad_versions) / sizeof(bad_versions[0]); i++) { + Config* cfg = valid_client_config(); + EXPECT_NOT_NULL(cfg); + char arg[64]; + snprintf(arg, sizeof(arg), "--protocol=%s", bad_versions[i]); + char* argv[] = {"fastsync", "--source-dir", "/src", "--dest-dir", "/dst", arg}; + int positional_args[2]; + int positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 6, argv, positional_args, &positional_count), 0); + EXPECT_TRUE(strcmp(cfg->version, PROTOCOL_VERSION) != 0); + EXPECT_FALSE(validate_config(cfg)); + config_delete(cfg); + } +} + +/* validate_config accepts the current PROTOCOL_VERSION (the default) and rejects + * a version that does not equal it -- the honest post-parse enforcement. */ +static void test_validate_config_protocol_version() { + Config* cfg = valid_client_config(); + EXPECT_NOT_NULL(cfg); + EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION); + EXPECT_TRUE(validate_config(cfg)); + config_delete(cfg); + + cfg = valid_client_config(); + EXPECT_NOT_NULL(cfg); + free(cfg->version); + cfg->version = str_dup("2.15.0"); + EXPECT_FALSE(validate_config(cfg)); + config_delete(cfg); +} + /* --xattrs/-X and --acls/-A preserve per-file xattrs and both imply metadata * transmission (the xattr block rides the metadata/per-file frame); each is * individually negatable and the derived use_xattrs follows the flags. */ @@ -2850,6 +2910,9 @@ void test_client_cli() { test_cli_exclude_patterns(); test_parse_args_help(); test_parse_args_version(); + test_parse_args_protocol_accept_current(); + test_parse_args_protocol_rejects_other_versions(); + test_validate_config_protocol_version(); test_parse_args_valid_port(); test_parse_args_size_only(); test_parse_args_ignore_existing();