#include "test_client_cli.h" #include "client_validation.h" #include "config.h" #include "log.h" #include "test_utils.h" #include "utils.h" #include #include #include /* Declaration of parse_args from client_cli.c */ int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count); static Config* valid_client_config() { Config* cfg = config_create(); if (!cfg) return NULL; cfg->send_directory = str_dup("/src"); cfg->receive_root_directory = str_dup("/dst"); return cfg; } static void test_validate_config_required_paths() { Config* cfg = config_create(); EXPECT_FALSE(validate_config(cfg)); cfg->send_directory = str_dup("/src"); EXPECT_FALSE(validate_config(cfg)); config_delete(cfg); } static void test_validate_config_incompatible_options() { Config* cfg = valid_client_config(); cfg->use_sendfile = true; cfg->use_compression = true; EXPECT_FALSE(validate_config(cfg)); cfg->use_compression = false; cfg->use_incremental = true; cfg->use_chunk_serialization = true; EXPECT_FALSE(validate_config(cfg)); config_delete(cfg); } static void test_validate_config_tls_requirements() { Config* cfg = valid_client_config(); cfg->use_tls = true; EXPECT_FALSE(validate_config(cfg)); cfg->tls_cert = str_dup("cert.pem"); EXPECT_FALSE(validate_config(cfg)); cfg->tls_key = str_dup("key.pem"); EXPECT_FALSE(validate_config(cfg)); cfg->tls_ca = str_dup("ca.pem"); EXPECT_TRUE(validate_config(cfg)); config_delete(cfg); } static void test_validate_config_delta_sendfile_constraints() { Config* cfg = valid_client_config(); cfg->use_delta = true; EXPECT_FALSE(validate_config(cfg)); cfg->use_incremental = true; cfg->use_sendfile = true; EXPECT_FALSE(validate_config(cfg)); /* Whole-file makes delta selection inactive, so these combinations are valid. */ cfg->whole_file = true; EXPECT_TRUE(validate_config(cfg)); cfg->use_incremental = false; EXPECT_TRUE(validate_config(cfg)); config_delete(cfg); } /* Test main() with --help flag (early return path, no server connection needed) */ static void test_cli_help() { /* We can't easily call main() because it calls send_files which needs a server. * Instead, test the argument parsing logic by testing that config_create works * with the same parameters client_cli uses, and that config_delete cleans up * properly when send_directory and receive_root_directory are NULL. */ /* This matches what client_cli does at startup */ Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); EXPECT_NULL(cfg->send_directory); EXPECT_NULL(cfg->receive_root_directory); EXPECT_FALSE(cfg->save_to_disk); EXPECT_EQ_INT(cfg->compression_level, 5); config_delete(cfg); } /* Test that --archive sets compression, multithreading, and metadata */ static void test_cli_archive_flags() { Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); /* Simulate --archive flag */ cfg->use_compression = true; cfg->use_multithreading = true; cfg->use_metadata = true; EXPECT_TRUE(cfg->use_compression); EXPECT_TRUE(cfg->use_multithreading); EXPECT_TRUE(cfg->use_metadata); config_delete(cfg); } /* Test that --dry-run sets dry_run flag */ static void test_cli_dry_run() { Config* cfg = config_create(); cfg->dry_run = true; EXPECT_TRUE(cfg->dry_run); config_delete(cfg); } static void test_cli_remove_source_files() { Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); EXPECT_FALSE(cfg->remove_source_files); cfg->remove_source_files = true; EXPECT_TRUE(cfg->remove_source_files); config_delete(cfg); } static void test_parse_args_remove_source_files() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--remove-source-files", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 4, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, 0); EXPECT_TRUE(cfg->remove_source_files); config_delete(cfg); } /* Test that --delete sets use_delete */ static void test_cli_delete_flag() { Config* cfg = config_create(); cfg->use_delete = true; EXPECT_TRUE(cfg->use_delete); config_delete(cfg); } /* Test exclude pattern handling */ static void test_cli_exclude_patterns() { Config* cfg = config_create(); EXPECT_NOT_NULL(cfg); /* Simulate --exclude "*.log" --exclude "tmp/" */ cfg->exclude_patterns = malloc(2 * sizeof(char*)); EXPECT_NOT_NULL(cfg->exclude_patterns); cfg->exclude_patterns[0] = str_dup("*.log"); cfg->exclude_patterns[1] = str_dup("tmp/"); cfg->exclude_count = 2; EXPECT_EQ_STR(cfg->exclude_patterns[0], "*.log"); EXPECT_EQ_STR(cfg->exclude_patterns[1], "tmp/"); EXPECT_EQ_INT(cfg->exclude_count, 2); config_delete(cfg); } /* Test parse_args with --help returns 1 (clean exit) */ static void test_parse_args_help() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--help"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 2, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, 1); config_delete(cfg); } /* Test parse_args with -V/--version returns 1 */ static void test_parse_args_version() { Config* cfg = config_create(); char* argv_short[] = {"fastsync", "-V"}; char* argv_long[] = {"fastsync", "--version"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 2, argv_short, positional_args, &positional_count); EXPECT_EQ_INT(ret, 1); ret = parse_args(cfg, 2, argv_long, positional_args, &positional_count); EXPECT_EQ_INT(ret, 1); config_delete(cfg); } /* Test parse_args with valid port */ static void test_parse_args_valid_port() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-p", "2222", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, 0); EXPECT_EQ_INT(cfg->ssh_port, 2222); EXPECT_EQ_INT(positional_count, 2); config_delete(cfg); } /* Test parse_args with --size-only. */ static void test_parse_args_size_only() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--size-only", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->size_only); EXPECT_EQ_INT(positional_count, 2); config_delete(cfg); } /* Test parse_args rejects port > 65535 */ static void test_parse_args_invalid_port() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-p", "99999", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, -1); config_delete(cfg); } /* Test parse_args rejects non-numeric port */ static void test_parse_args_non_numeric_port() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-p", "abc", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, -1); config_delete(cfg); } /* Test parse_args rejects server port > 65535 */ static void test_parse_args_invalid_server_port() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--server-port", "70000", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, -1); config_delete(cfg); } /* Test parse_args rejects invalid compression level */ static void test_parse_args_invalid_compression_level() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-c", "25", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, -1); config_delete(cfg); } /* Test parse_args accepts valid compression level */ static void test_parse_args_valid_compression_level() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-c", "10", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 5, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, 0); EXPECT_EQ_INT(cfg->compression_level, 10); config_delete(cfg); } static void test_parse_args_debug_flags() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--debug=io,proto,pack,util", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_EQ_INT(cfg->debug_level, LOG_DEBUG_ALL); EXPECT_EQ_INT(get_log_debug_flags(), LOG_DEBUG_ALL); config_delete(cfg); } static void test_parse_args_debug_help() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--debug=help"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 2, argv, positional_args, &positional_count), 1); config_delete(cfg); } static void test_parse_args_debug_flags_validation() { static const char* const values[] = {"", "io,", ",io", "io,,proto", "acl", "tls", "unknown"}; for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) { Config* cfg = config_create(); char option[64]; snprintf(option, sizeof(option), "--debug=%s", values[i]); char* argv[] = {"fastsync", option, "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1); config_delete(cfg); } } static void test_parse_args_modify_window() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--modify-window=3", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_EQ_INT(cfg->modify_window, 3); config_delete(cfg); cfg = config_create(); char* short_argv[] = {"fastsync", "-@", "7", "/src", "/dst"}; positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 5, short_argv, positional_args, &positional_count), 0); EXPECT_EQ_INT(cfg->modify_window, 7); config_delete(cfg); cfg = config_create(); char* attached_argv[] = {"fastsync", "-@11", "/src", "/dst"}; positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, attached_argv, positional_args, &positional_count), 0); EXPECT_EQ_INT(cfg->modify_window, 11); config_delete(cfg); } static void test_parse_args_rejects_invalid_modify_window() { const char* values[] = {"-1", "not-a-number", ""}; for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) { Config* cfg = config_create(); char* argv[] = {"fastsync", "--modify-window", (char*)values[i], "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1); config_delete(cfg); } } /* Test parse_args unknown option returns error */ static void test_parse_args_unknown_option() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--nonexistent", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 4, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, -1); config_delete(cfg); } /* Parsed-but-unimplemented options must fail instead of being silently accepted. */ static void test_parse_args_rejects_unimplemented_options() { static const char* const options[] = {"--silent", "--queue-size", "-H", "--hard-links", "-A", "--acls", "-X", "--xattrs", "-D", "--devices", "-i", "--itemize-changes", "--out-format", "--info", "--list-only", "-u", "--update", "--append", "--append-verify", "--delete-excluded", "--delete-after", "--max-delete", "--filter", "--files-from", "--cvs-exclude", "--prune-empty-dirs", "-R", "--relative", "-e", "--rsh", "--rsync-path", "--temp-dir", "--compare-dest", "--copy-dest", "--link-dest", "--delete-before", "--address", "--bind-address", "--ipv6", "--ipv4", "--daemon", "--config", "--server", "--compress-choice"}; for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { Config* cfg = config_create(); char* argv[] = {"fastsync", (char*)options[i], "dummy", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1); config_delete(cfg); } } /* Test both rsync-compatible quiet spellings and option ordering. */ static void test_parse_args_quiet() { static const char* const options[][2] = { {"-q", "-v"}, {"-v", "-q"}, {"--quiet", "-v"}, {"-v", "--quiet"}}; for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { Config* cfg = config_create(); char* argv[] = {"fastsync", (char*)options[i][0], (char*)options[i][1], "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->quiet); config_delete(cfg); } } static void test_parse_args_human_readable() { static const char* const options[] = {"-h", "--human-readable"}; for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { Config* cfg = config_create(); char* argv[] = {"fastsync", (char*)options[i], "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->human_readable); config_delete(cfg); } } /* Test parse_args with --archive flag */ static void test_parse_args_archive() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--archive", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; int ret = parse_args(cfg, 4, argv, positional_args, &positional_count); EXPECT_EQ_INT(ret, 0); EXPECT_TRUE(cfg->use_compression); EXPECT_TRUE(cfg->use_multithreading); EXPECT_TRUE(cfg->use_metadata); config_delete(cfg); } static void test_parse_args_fsync() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--fsync", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->use_fsync); config_delete(cfg); } static void test_parse_args_ignore_times() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-I", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->ignore_times); config_delete(cfg); cfg = config_create(); char* long_argv[] = {"fastsync", "--ignore-times", "/src", "/dst"}; positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, long_argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->ignore_times); config_delete(cfg); } /* --secluded-args is accepted for compatibility but has no effect. */ static void test_parse_args_secluded_args() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--secluded-args", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_FALSE(cfg->use_chunk_serialization); config_delete(cfg); } static void test_parse_args_short_s_remains_chunk_serialization() { Config* cfg = config_create(); char* argv[] = {"fastsync", "-s", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->use_chunk_serialization); config_delete(cfg); } static void test_parse_args_8_bit_output() { Config* cfg = config_create(); char* long_argv[] = {"fastsync", "--8-bit-output", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, long_argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->eight_bit_output); config_delete(cfg); cfg = config_create(); char* short_argv[] = {"fastsync", "-8", "/src", "/dst"}; positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, short_argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->eight_bit_output); config_delete(cfg); } static void test_parse_args_stderr_modes() { static const char* const modes[] = {"errors", "all", "e", "a"}; static const LogStderrMode expected[] = {LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_ERRORS, LOG_STDERR_ALL}; for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { Config* cfg = config_create(); char option[32]; snprintf(option, sizeof(option), "--stderr=%s", modes[i]); char* argv[] = {"fastsync", option, "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); EXPECT_EQ_INT(log_get_stderr_mode(), expected[i]); config_delete(cfg); } log_set_stderr_mode(LOG_STDERR_ERRORS); } static void test_parse_args_rejects_unsupported_stderr_modes() { static const char* const modes[] = {"client", "c", "invalid"}; for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { Config* cfg = config_create(); char option[32]; snprintf(option, sizeof(option), "--stderr=%s", modes[i]); char* argv[] = {"fastsync", option, "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1); config_delete(cfg); } log_set_stderr_mode(LOG_STDERR_ERRORS); } /* Test both whole-file spellings and its precedence over delta selection. */ static void test_parse_args_whole_file() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--delta", "--incremental", "-W", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 6, argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->whole_file); EXPECT_TRUE(cfg->use_delta); config_delete(cfg); cfg = config_create(); char* long_argv[] = {"fastsync", "--whole-file", "/src", "/dst"}; positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 4, long_argv, positional_args, &positional_count), 0); EXPECT_TRUE(cfg->whole_file); config_delete(cfg); } void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); test_validate_config_tls_requirements(); test_validate_config_delta_sendfile_constraints(); test_cli_help(); test_cli_archive_flags(); test_cli_dry_run(); test_cli_remove_source_files(); test_parse_args_remove_source_files(); test_cli_delete_flag(); test_cli_exclude_patterns(); test_parse_args_help(); test_parse_args_version(); test_parse_args_valid_port(); test_parse_args_size_only(); test_parse_args_invalid_port(); test_parse_args_non_numeric_port(); test_parse_args_invalid_server_port(); test_parse_args_invalid_compression_level(); test_parse_args_valid_compression_level(); test_parse_args_debug_flags(); test_parse_args_debug_help(); test_parse_args_debug_flags_validation(); test_parse_args_modify_window(); test_parse_args_rejects_invalid_modify_window(); test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_quiet(); test_parse_args_human_readable(); test_parse_args_archive(); test_parse_args_fsync(); test_parse_args_ignore_times(); test_parse_args_8_bit_output(); test_parse_args_stderr_modes(); test_parse_args_rejects_unsupported_stderr_modes(); test_parse_args_secluded_args(); test_parse_args_short_s_remains_chunk_serialization(); test_parse_args_whole_file(); }