feat(d5-daemon-core): daemon lifecycle, module config, ::dest, PROTOCOL 2.15.0

This commit is contained in:
2026-09-09 17:48:07 +02:00
parent 9d17e951f1
commit b3d7d64347
18 changed files with 2229 additions and 102 deletions
+215
View File
@@ -0,0 +1,215 @@
"""Daemon mode (--daemon + module config + host::module/path destinations) tests.
These exercise the Wave A daemon foundation end to end: a fastsync-server
started with --daemon reads a FastSync-native module config file, the client
asks for a module with a host::module/path destination, and the transfer lands
in the configured module root only. Read-only modules, unknown modules, and
auth-required modules are all refused cleanly before any data moves.
"""
import os
import shutil
import signal
import subprocess
import sys
import time
import pytest
sys.path.insert(0, os.path.dirname(__file__))
from common import (
TEST_DATA_DIR,
CLIENT_CMD,
SERVER_CMD,
generate_test_files,
run_client,
get_dest_received_dir,
verify_transfer,
_find_free_port,
_wait_for_port,
)
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "daemon_source")
MODULE_ROOT = os.path.join(TEST_DATA_DIR, "daemon_modules")
FILES_MODULE = os.path.join(MODULE_ROOT, "files")
READONLY_MODULE = os.path.join(MODULE_ROOT, "readonly")
AUTH_MODULE = os.path.join(MODULE_ROOT, "auth")
CONF_FILE = os.path.join(TEST_DATA_DIR, "fastsyncd.conf")
class DaemonManager:
"""Boots one fastsync-server --daemon from a config file and tears it down
(including its accept-loop children) on exit."""
def __init__(self):
self._proc = None
self._port = None
def start(self, config_path, port_override=None):
self.stop()
# When no override is given the daemon binds the config file's `port`
# (the plain config-port path); with an override the --dparam path.
self._port = port_override if port_override is not None else _config_port(config_path)
cmd = (SERVER_CMD + ["--daemon", "--config", config_path, "--allow-unauthenticated",
"--no-detach"])
if port_override is not None:
cmd += ["--dparam", f"port={port_override}"]
log_path = os.path.join(TEST_DATA_DIR, "fastsyncd.log")
log = open(log_path, "w")
self._proc = subprocess.Popen(
cmd, stdout=log, stderr=log, stdin=subprocess.DEVNULL, start_new_session=True)
_wait_for_port(self._port, timeout=10)
def stop(self):
if self._proc:
try:
os.killpg(self._proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
self._proc.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(self._proc.pid, signal.SIGKILL)
self._proc.wait()
self._proc = None
@property
def port(self):
return self._port
def __enter__(self):
return self
def __exit__(self, *args):
self.stop()
def __del__(self):
self.stop()
def _config_port(config_path):
"""Read the explicit `port = N` line out of the daemon config file."""
with open(config_path) as f:
for line in f:
stripped = line.strip()
if stripped.startswith("port") and "=" in stripped:
return int(stripped.split("=", 1)[1].strip())
raise RuntimeError(f"no port= in {config_path}")
@pytest.fixture(scope="module", autouse=True)
def daemon_env():
for d in (MODULE_ROOT, FILES_MODULE, READONLY_MODULE, AUTH_MODULE):
shutil.rmtree(d, ignore_errors=True)
os.makedirs(d, exist_ok=True)
generate_test_files(SOURCE_DIR, full=False)
# The config's port is a free port chosen per worker; the `daemon` fixture
# boots on it (the config-port path) and the --dparam override test boots a
# second daemon on a different port.
config_port = _find_free_port()
with open(CONF_FILE, "w") as f:
f.write(
"# FastSync-native daemon config (Wave A grammar)\n"
"port = %d\n"
"\n"
"[files]\n"
"path = %s\n"
"\n"
"[readonly]\n"
"path = %s\n"
"read only = yes\n"
"\n"
"[locked]\n"
"path = %s\n"
"auth users = alice\n"
% (config_port, FILES_MODULE, READONLY_MODULE, AUTH_MODULE))
yield
shutil.rmtree(MODULE_ROOT, ignore_errors=True)
shutil.rmtree(SOURCE_DIR, ignore_errors=True)
@pytest.fixture(scope="module")
def daemon():
d = DaemonManager()
d.start(CONF_FILE)
yield d
d.stop()
def _push(dest, port):
result, _ = run_client(SOURCE_DIR, dest, port=port)
return result
class TestDaemonModuleSelection:
@pytest.mark.ci
def test_module_transfer(self, daemon):
"""A host::module/path destination lands inside the module root only."""
result = _push("127.0.0.1::files", daemon.port)
assert result.returncode == 0, result.stderr or result.stdout
received = get_dest_received_dir(FILES_MODULE, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing, f"missing: {missing[:5]}"
assert not mismatches, f"mismatch: {mismatches[:5]}"
def test_module_subtree(self, daemon):
"""The /path part of host::module/path is relative inside the module."""
sub = os.path.join(FILES_MODULE, "subtree")
os.makedirs(sub, exist_ok=True)
result = _push("127.0.0.1::files/subtree", daemon.port)
assert result.returncode == 0, result.stderr or result.stdout
received = get_dest_received_dir(sub, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing, f"missing: {missing[:5]}"
assert not mismatches, f"mismatch: {mismatches[:5]}"
class TestDaemonRejection:
def test_read_only_module_blocked(self, daemon):
result = _push("127.0.0.1::readonly", daemon.port)
assert result.returncode != 0
file_count = sum(len(files) for _, _, files in os.walk(READONLY_MODULE))
assert file_count == 0, "read-only module must not receive any file"
def test_unknown_module_rejected(self, daemon):
result = _push("127.0.0.1::no-such-module", daemon.port)
assert result.returncode != 0
def test_auth_required_module_rejected(self, daemon):
result = _push("127.0.0.1::locked", daemon.port)
assert result.returncode != 0
file_count = sum(len(files) for _, _, files in os.walk(AUTH_MODULE))
assert file_count == 0
def test_plaintext_requires_allow_unauthenticated(self):
"""Secure default: a daemon started WITHOUT --allow-unauthenticated must
refuse a plaintext client (same posture as the standalone server)."""
d = DaemonManager()
port = _find_free_port()
log_path = os.path.join(TEST_DATA_DIR, "fastsyncd_noauth.log")
log = open(log_path, "w")
cmd = SERVER_CMD + ["--daemon", "--config", CONF_FILE, "--no-detach",
"--dparam", f"port={port}"]
d._proc = subprocess.Popen(cmd, stdout=log, stderr=log, stdin=subprocess.DEVNULL,
start_new_session=True)
d._port = port
_wait_for_port(port, timeout=10)
try:
result = _push("127.0.0.1::files", port)
assert result.returncode != 0
finally:
d.stop()
def test_dparam_port_override(self):
"""--dparam port=N overrides the config's port and the daemon serves on N."""
override = _find_free_port()
d = DaemonManager()
d.start(CONF_FILE, port_override=override)
try:
result = _push("127.0.0.1::files", override)
assert result.returncode == 0, result.stderr or result.stdout
received = get_dest_received_dir(FILES_MODULE, SOURCE_DIR)
_, missing = verify_transfer(SOURCE_DIR, received)
assert not missing, f"missing: {missing[:5]}"
finally:
d.stop()
+4
View File
@@ -6,6 +6,7 @@
#include "test_compression.h"
#include "test_config.h"
#include "test_data.h"
#include "test_daemon_conf.h"
#include "test_delay_updates.h"
#include "test_delta.h"
#include "test_file.h"
@@ -21,6 +22,7 @@
#include "test_robustness.h"
#include "test_scanner.h"
#include "test_server.h"
#include "test_server_cli.h"
#include "test_shared_utils.h"
#include "test_stress.h"
#include "test_transport_tcp.h"
@@ -68,6 +70,8 @@ int main() {
RUN_TEST(test_transport_tls);
RUN_TEST(test_client_cli);
RUN_TEST(test_server);
RUN_TEST(test_daemon_conf);
RUN_TEST(test_server_cli);
RUN_TEST(test_fuzz_smoke);
RUN_TEST(test_xattr);
+213
View File
@@ -83,6 +83,210 @@ static void test_config_ssh_dest_no_user() {
config_delete(cfg);
}
static void test_config_daemon_dest_parse() {
Config* cfg = make_config("1.0", "/src", "dahost::files/sub/dir", true, false, false, false,
false, 1, false, 0);
int ret = config_parse_daemon_dest(cfg);
EXPECT_EQ_INT(ret, 1);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
EXPECT_EQ_STR(cfg->server_host, "dahost");
EXPECT_EQ_STR(cfg->module, "files");
EXPECT_EQ_STR(cfg->receive_root_directory, "sub/dir");
config_delete(cfg);
}
static void test_config_daemon_dest_no_path() {
Config* cfg =
make_config("1.0", "/src", "dahost::files", true, false, false, false, false, 1, false, 0);
int ret = config_parse_daemon_dest(cfg);
EXPECT_EQ_INT(ret, 1);
EXPECT_EQ_STR(cfg->server_host, "dahost");
EXPECT_EQ_STR(cfg->module, "files");
EXPECT_EQ_STR(cfg->receive_root_directory, "");
config_delete(cfg);
}
static void test_config_daemon_dest_double_slash_normalized() {
Config* cfg = make_config("1.0", "/src", "dahost::files//sub", true, false, false, false, false,
1, false, 0);
int ret = config_parse_daemon_dest(cfg);
EXPECT_EQ_INT(ret, 1);
EXPECT_EQ_STR(cfg->module, "files");
EXPECT_EQ_STR(cfg->receive_root_directory, "sub");
config_delete(cfg);
}
static void test_config_daemon_dest_bad() {
/* Missing module name after "::". */
Config* cfg =
make_config("1.0", "/src", "dahost::", true, false, false, false, false, 1, false, 0);
EXPECT_EQ_INT(config_parse_daemon_dest(cfg), -1);
config_delete(cfg);
/* Invalid module name. */
cfg =
make_config("1.0", "/src", "dahost::bad name", true, false, false, false, false, 1, false, 0);
EXPECT_EQ_INT(config_parse_daemon_dest(cfg), -1);
config_delete(cfg);
/* Traversal path rejected. */
cfg = make_config("1.0", "/src", "dahost::mod/../../etc", true, false, false, false, false, 1,
false, 0);
EXPECT_EQ_INT(config_parse_daemon_dest(cfg), -1);
config_delete(cfg);
/* user@host::module is not yet supported. */
cfg =
make_config("1.0", "/src", "user@dahost::mod", true, false, false, false, false, 1, false, 0);
EXPECT_EQ_INT(config_parse_daemon_dest(cfg), -1);
config_delete(cfg);
/* A non-daemon destination is untouched (returns 0). */
cfg = make_config("1.0", "/src", "plain:path", true, false, false, false, false, 1, false, 0);
EXPECT_EQ_INT(config_parse_daemon_dest(cfg), 0);
EXPECT_EQ_STR(cfg->receive_root_directory, "plain:path");
config_delete(cfg);
}
static void test_config_transport_dest_daemon_beats_ssh() {
/* host::module selects daemon TCP; host:path still selects SSH. */
Config* cfg = make_config("1.0", "/src", "h::m/x", true, false, false, false, false, 1, false, 0);
int ret = config_parse_transport_dest(cfg);
EXPECT_EQ_INT(ret, 1);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
EXPECT_EQ_STR(cfg->module, "m");
config_delete(cfg);
cfg = make_config("1.0", "/src", "h:dst", true, false, false, false, false, 1, false, 0);
ret = config_parse_transport_dest(cfg);
EXPECT_EQ_INT(ret, 0);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
EXPECT_EQ_STR(cfg->receive_root_directory, "dst");
config_delete(cfg);
}
static void test_config_is_daemon_dest() {
EXPECT_TRUE(config_is_daemon_dest("host::mod"));
EXPECT_TRUE(config_is_daemon_dest("host::mod/path"));
EXPECT_FALSE(config_is_daemon_dest("host:path"));
EXPECT_FALSE(config_is_daemon_dest("/local/path"));
/* A colon inside the module-relative path does not change the detection. */
EXPECT_TRUE(config_is_daemon_dest("host::mod/single:colon"));
EXPECT_FALSE(config_is_daemon_dest(NULL));
}
static void test_config_module_wire_roundtrip() {
Config* send_cfg = config_create();
EXPECT_NOT_NULL(send_cfg);
send_cfg->send_directory = str_dup("/src");
send_cfg->receive_root_directory = str_dup("rel/path");
send_cfg->module = str_dup("backup");
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
Config* recv_cfg = config_receive(p[0]);
bool ok = recv_cfg != NULL && recv_cfg->module != NULL &&
strcmp(recv_cfg->module, "backup") == 0 &&
strcmp(recv_cfg->receive_root_directory, "rel/path") == 0;
config_delete(recv_cfg);
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
close(p[1]);
config_delete(send_cfg);
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
static void test_config_module_wire_empty_canonicalizes_to_null() {
Config* send_cfg = config_create();
EXPECT_NOT_NULL(send_cfg);
send_cfg->send_directory = str_dup("/src");
send_cfg->receive_root_directory = str_dup("/dst");
/* module left NULL -> serialized as "" -> received back as NULL. */
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
Config* recv_cfg = config_receive(p[0]);
bool ok = recv_cfg != NULL && recv_cfg->module == NULL;
config_delete(recv_cfg);
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
close(p[1]);
config_delete(send_cfg);
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
/* A module gate that rejects any connection that names a module. */
static const char* reject_named_module_gate(const Config* config, void* context) {
(void)context;
if (config && config->module && config->module[0] != '\0')
return "test rejection";
return NULL;
}
static void test_config_receive_with_validate_rejects() {
Config* send_cfg = config_create();
EXPECT_NOT_NULL(send_cfg);
send_cfg->send_directory = str_dup("/src");
send_cfg->receive_root_directory = str_dup("/dst");
send_cfg->module = str_dup("any-module");
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
Config* recv = config_receive_with_validate(p[0], reject_named_module_gate, NULL);
bool ok = recv == NULL;
config_delete(recv);
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
close(p[1]);
config_delete(send_cfg);
EXPECT_FALSE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
static void test_pipeline_sender_lifecycle() {
Config* cfg = make_config("2.0", "/src2", "/dst2", false, false, true, true, false, 1, false, 0);
Queue* q1 = queue_create(5, NULL);
@@ -1284,6 +1488,12 @@ void test_config() {
test_config_ssh_dest();
test_config_ssh_dest_local_path();
test_config_ssh_dest_no_user();
test_config_daemon_dest_parse();
test_config_daemon_dest_no_path();
test_config_daemon_dest_double_slash_normalized();
test_config_daemon_dest_bad();
test_config_transport_dest_daemon_beats_ssh();
test_config_is_daemon_dest();
test_config_trust_sender_default_false();
test_pipeline_sender_lifecycle();
test_pipeline_receiver_lifecycle();
@@ -1312,6 +1522,9 @@ void test_config() {
test_config_devices_wire_roundtrip();
test_config_preallocate_wire_roundtrip();
test_config_phase4_xattr_wire_roundtrip();
test_config_module_wire_roundtrip();
test_config_module_wire_empty_canonicalizes_to_null();
test_config_receive_with_validate_rejects();
}
test_config_delete_timing_early_helper();
test_config_is_remote_dest();
+342
View File
@@ -0,0 +1,342 @@
#include "test_daemon_conf.h"
#include "daemon_conf.h"
#include "test_utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Write a config body into a fresh temp file and return its path in out_path
* (heap-allocated; caller frees). Returns 0 on success. */
static int write_conf(const char* body, char** out_path) {
char tmpl[] = "/tmp/fastsync_daemon_conf_XXXXXX";
int fd = mkstemp(tmpl);
if (fd < 0)
return -1;
size_t len = strlen(body);
if (write(fd, body, len) != (ssize_t)len) {
close(fd);
unlink(tmpl);
return -1;
}
close(fd);
*out_path = strdup(tmpl);
return *out_path ? 0 : -1;
}
static void test_daemon_conf_create_defaults() {
DaemonConf* conf = daemon_conf_create();
EXPECT_NOT_NULL(conf);
EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT);
EXPECT_NULL(conf->global.motd_file);
EXPECT_NULL(conf->global.address);
EXPECT_EQ_INT(conf->module_count, 0);
daemon_conf_free(conf);
}
static void test_daemon_conf_full_parse() {
char* path;
EXPECT_EQ_INT(write_conf("port = 8734\n"
"motd file = /etc/fastsync/motd\n"
"address = 127.0.0.1\n"
"\n"
"[backup]\n"
"path = /srv/backup\n"
"read only = yes\n"
"auth users = alice, bob\n",
&path),
0);
char err[256];
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_EQ_INT(conf->global.port, 8734);
EXPECT_EQ_STR(conf->global.motd_file, "/etc/fastsync/motd");
EXPECT_EQ_STR(conf->global.address, "127.0.0.1");
EXPECT_EQ_INT(conf->module_count, 1);
EXPECT_EQ_STR(conf->modules[0].name, "backup");
EXPECT_EQ_STR(conf->modules[0].path, "/srv/backup");
EXPECT_TRUE(conf->modules[0].read_only);
EXPECT_EQ_INT(conf->modules[0].auth_user_count, 2);
EXPECT_EQ_STR(conf->modules[0].auth_users[0], "alice");
EXPECT_EQ_STR(conf->modules[0].auth_users[1], "bob");
daemon_conf_free(conf);
}
static void test_daemon_conf_comments_and_blank_lines() {
char* path;
EXPECT_EQ_INT(write_conf("# a full-line comment\n"
"; a semicolon comment\n"
" # indented comment\n"
" \n"
"\t; another\n"
"[alpha]\n"
"path = /a\n"
"\n"
"[beta]\n"
"path = /b\n",
&path),
0);
char err[256];
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_EQ_INT(conf->module_count, 2);
EXPECT_EQ_STR(conf->modules[0].name, "alpha");
EXPECT_EQ_STR(conf->modules[1].name, "beta");
daemon_conf_free(conf);
}
static void test_daemon_conf_case_insensitive_and_bool_variants() {
char* path;
EXPECT_EQ_INT(write_conf("PORT = 9001\n"
"[CaseMod]\n"
"PATH = /cm\n"
"READ ONLY = True\n",
&path),
0);
char err[256];
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_EQ_INT(conf->global.port, 9001);
EXPECT_EQ_INT(conf->module_count, 1);
EXPECT_EQ_STR(conf->modules[0].name, "CaseMod");
EXPECT_TRUE(conf->modules[0].read_only);
daemon_conf_free(conf);
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nread only = 0\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_FALSE(conf->modules[0].read_only);
daemon_conf_free(conf);
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nread only = false\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_FALSE(conf->modules[0].read_only);
daemon_conf_free(conf);
}
static void test_daemon_conf_quoted_value() {
char* path;
EXPECT_EQ_INT(write_conf("[m]\npath = \"/srv/my dir/mod\"\n", &path), 0);
char err[256];
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_EQ_STR(conf->modules[0].path, "/srv/my dir/mod");
daemon_conf_free(conf);
}
static void test_daemon_conf_global_defaults_when_absent() {
char* path;
EXPECT_EQ_INT(write_conf("[m]\npath = /x\n", &path), 0);
char err[256];
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT); /* 873 */
EXPECT_NULL(conf->global.motd_file);
EXPECT_NULL(conf->global.address);
daemon_conf_free(conf);
}
static void test_daemon_conf_unknown_key_rejected() {
char* path;
char err[256];
EXPECT_EQ_INT(write_conf("bogus_key = 1\n", &path), 0);
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "unknown global key") != NULL);
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nflavor = van\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "unknown key 'flavor'") != NULL);
}
static void test_daemon_conf_malformed_rejected() {
char* path;
char err[256];
DaemonConf* conf;
EXPECT_EQ_INT(write_conf("port 8734\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "expected 'key = value'") != NULL);
EXPECT_EQ_INT(write_conf("[m]\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "no 'path'") != NULL);
EXPECT_EQ_INT(write_conf("[m\npath = /x\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "unterminated module header") != NULL);
EXPECT_EQ_INT(write_conf("[m] trailing\npath = /x\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "after module header") != NULL);
EXPECT_EQ_INT(write_conf("port = notanumber\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "invalid port") != NULL);
EXPECT_EQ_INT(write_conf("port = 70000\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "invalid port") != NULL);
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nread only = maybe\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "read only") != NULL);
EXPECT_EQ_INT(write_conf("= value\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "empty key") != NULL);
EXPECT_EQ_INT(write_conf("[]\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "invalid module name") != NULL);
EXPECT_EQ_INT(write_conf("[bad/name]\npath = /x\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_EQ_INT(write_conf("[m]\npath = \"/unterminated\n", &path), 0);
conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "unterminated quoted value") != NULL);
}
static void test_daemon_conf_duplicate_module_rejected() {
char* path;
char err[256];
EXPECT_EQ_INT(write_conf("[m]\npath = /a\n[m]\npath = /b\n", &path), 0);
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "duplicate module") != NULL);
}
static void test_daemon_conf_long_line_rejected() {
char* path;
char err[256];
char body[4600];
memset(body, 'a', sizeof(body) - 1);
memcpy(body, "[m]\npath = /x\nport = ", 21);
body[sizeof(body) - 1] = '\0';
EXPECT_EQ_INT(write_conf(body, &path), 0);
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "exceeds the") != NULL);
}
static void test_daemon_conf_missing_file_rejected() {
char err[256];
DaemonConf* conf = daemon_conf_load("/nonexistent/fastsync_daemon_conf_zzz", err, sizeof(err));
EXPECT_NULL(conf);
EXPECT_TRUE(strstr(err, "cannot open") != NULL);
}
static void test_daemon_conf_find_module() {
char* path;
EXPECT_EQ_INT(write_conf("[known]\npath = /rooted\n[m]\npath = /other\n", &path), 0);
char err[256];
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
free(path);
EXPECT_NOT_NULL(conf);
const DaemonModule* found = daemon_conf_find_module(conf, "known");
EXPECT_NOT_NULL(found);
EXPECT_EQ_STR(found->path, "/rooted");
EXPECT_NULL(daemon_conf_find_module(conf, "nope"));
/* Case-sensitive like rsync module names. */
EXPECT_NULL(daemon_conf_find_module(conf, "Known"));
daemon_conf_free(conf);
}
static void test_daemon_conf_dparam_override() {
DaemonConf* conf = daemon_conf_create();
EXPECT_NOT_NULL(conf);
char err[256];
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=8734", err, sizeof(err)), 0);
EXPECT_EQ_INT(conf->global.port, 8734);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "motd file=/tmp/motd", err, sizeof(err)), 0);
EXPECT_EQ_STR(conf->global.motd_file, "/tmp/motd");
/* Keys are case-insensitive. */
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "ADDRESS=127.0.0.1", err, sizeof(err)), 0);
EXPECT_EQ_STR(conf->global.address, "127.0.0.1");
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port = 9000", err, sizeof(err)), 0);
EXPECT_EQ_INT(conf->global.port, 9000);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=notaport", err, sizeof(err)), -1);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "bogus=1", err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "unknown global key") != NULL);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port", err, sizeof(err)), -1);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "=1", err, sizeof(err)), -1);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=", err, sizeof(err)), -1);
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "", err, sizeof(err)), -1);
daemon_conf_free(conf);
}
static void test_daemon_module_name_valid() {
EXPECT_TRUE(daemon_module_name_valid("backup"));
EXPECT_TRUE(daemon_module_name_valid("Backup_2"));
EXPECT_TRUE(daemon_module_name_valid("a.b-c"));
EXPECT_FALSE(daemon_module_name_valid(""));
EXPECT_FALSE(daemon_module_name_valid("with space"));
EXPECT_FALSE(daemon_module_name_valid("with/slash"));
EXPECT_FALSE(daemon_module_name_valid("with\t\ttab"));
EXPECT_FALSE(daemon_module_name_valid("bracket]"));
{
char long_name[DAEMON_MAX_MODULE_NAME + 2];
memset(long_name, 'a', sizeof(long_name) - 1);
long_name[sizeof(long_name) - 1] = '\0';
EXPECT_FALSE(daemon_module_name_valid(long_name));
}
}
void test_daemon_conf() {
test_daemon_conf_create_defaults();
test_daemon_conf_full_parse();
test_daemon_conf_comments_and_blank_lines();
test_daemon_conf_case_insensitive_and_bool_variants();
test_daemon_conf_quoted_value();
test_daemon_conf_global_defaults_when_absent();
test_daemon_conf_unknown_key_rejected();
test_daemon_conf_malformed_rejected();
test_daemon_conf_duplicate_module_rejected();
test_daemon_conf_long_line_rejected();
test_daemon_conf_missing_file_rejected();
test_daemon_conf_find_module();
test_daemon_conf_dparam_override();
test_daemon_module_name_valid();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_DAEMON_CONF_H
#define TEST_DAEMON_CONF_H
void test_daemon_conf();
#endif
+161
View File
@@ -0,0 +1,161 @@
#include "test_server_cli.h"
#include "server_cli.h"
#include "test_utils.h"
#include <string.h>
#include <sys/socket.h>
static int parse_ok(const char* const* args, int count, ServerCliOptions* opts) {
char err[512];
int r = server_cli_parse(count, (char**)args, opts, err, sizeof(err));
if (r == 0)
return 0;
if (r < 0)
return -1;
return 1;
}
static void test_server_cli_defaults() {
const char* args[] = {"fastsync-server"};
ServerCliOptions opts;
EXPECT_EQ_INT(parse_ok(args, 1, &opts), 0);
EXPECT_FALSE(opts.stdio_mode);
EXPECT_FALSE(opts.daemon_mode);
EXPECT_FALSE(opts.no_detach);
EXPECT_FALSE(opts.verbose);
EXPECT_FALSE(opts.use_tls);
EXPECT_EQ_INT(opts.port, 8080);
EXPECT_FALSE(opts.port_set);
EXPECT_EQ_STR(opts.destination_root, ".");
EXPECT_NULL(opts.config_path);
EXPECT_EQ_INT(opts.dparam_count, 0);
EXPECT_EQ_INT(opts.bind_family, AF_UNSPEC);
EXPECT_FALSE(opts.allow_delete);
EXPECT_FALSE(opts.allow_unauthenticated);
server_cli_options_free(&opts);
}
static void test_server_cli_daemon_flags() {
const char* args[] = {"fastsync-server", "--daemon", "--no-detach", "--allow-unauthenticated"};
ServerCliOptions opts;
EXPECT_EQ_INT(parse_ok(args, 4, &opts), 0);
EXPECT_TRUE(opts.daemon_mode);
EXPECT_TRUE(opts.no_detach);
EXPECT_TRUE(opts.allow_unauthenticated);
server_cli_options_free(&opts);
}
static void test_server_cli_config_and_dparam_forms() {
const char* args[] = {"fastsync-server", "--daemon", "--config=/tmp/x.conf",
"--dparam=port=8734", "--dparam", "address=127.0.0.1"};
ServerCliOptions opts;
EXPECT_EQ_INT(parse_ok(args, 6, &opts), 0);
EXPECT_EQ_STR(opts.config_path, "/tmp/x.conf");
EXPECT_EQ_INT(opts.dparam_count, 2);
EXPECT_EQ_STR(opts.dparams[0], "port=8734");
EXPECT_EQ_STR(opts.dparams[1], "address=127.0.0.1");
const char* args2[] = {"fastsync-server", "--daemon", "--config", "/tmp/y.conf"};
ServerCliOptions opts2;
EXPECT_EQ_INT(parse_ok(args2, 4, &opts2), 0);
EXPECT_EQ_STR(opts2.config_path, "/tmp/y.conf");
server_cli_options_free(&opts);
server_cli_options_free(&opts2);
}
static void test_server_cli_preserves_existing_flags() {
const char* args[] = {"fastsync-server",
"-p",
"9000",
"--tls",
"--cert",
"/c",
"--key",
"/k",
"--ca",
"/ca",
"--client-cn",
"cn",
"--allow-delete",
"--trust-sender",
"--address",
"127.0.0.1",
"-6",
"--destination-root",
"/srv"};
ServerCliOptions opts;
EXPECT_EQ_INT(parse_ok(args, 19, &opts), 0);
EXPECT_EQ_INT(opts.port, 9000);
EXPECT_TRUE(opts.port_set);
EXPECT_TRUE(opts.use_tls);
EXPECT_EQ_STR(opts.tls_cert, "/c");
EXPECT_EQ_STR(opts.tls_key, "/k");
EXPECT_EQ_STR(opts.tls_ca, "/ca");
EXPECT_EQ_STR(opts.client_cn, "cn");
EXPECT_TRUE(opts.allow_delete);
EXPECT_TRUE(opts.trust_sender);
EXPECT_EQ_STR(opts.bind_address, "127.0.0.1");
EXPECT_EQ_INT(opts.bind_family, AF_INET6);
EXPECT_TRUE(opts.destination_root_set);
EXPECT_EQ_STR(opts.destination_root, "/srv");
server_cli_options_free(&opts);
}
static void test_server_cli_conflicts() {
char err[256];
ServerCliOptions opts;
const char* a1[] = {"s", "--daemon", "--stdio"};
EXPECT_EQ_INT(server_cli_parse(3, (char**)a1, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "mutually exclusive") != NULL);
const char* a2[] = {"s", "--daemon", "--destination-root", "/x"};
EXPECT_EQ_INT(server_cli_parse(4, (char**)a2, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "module paths") != NULL);
const char* a3[] = {"s", "--config", "/x.conf"};
EXPECT_EQ_INT(server_cli_parse(3, (char**)a3, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "require --daemon") != NULL);
const char* a4[] = {"s", "--no-detach"};
EXPECT_EQ_INT(server_cli_parse(2, (char**)a4, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "require --daemon") != NULL);
server_cli_options_free(&opts);
}
static void test_server_cli_invalid() {
char err[256];
ServerCliOptions opts;
const char* a1[] = {"s", "-p", "notaport"};
EXPECT_EQ_INT(server_cli_parse(3, (char**)a1, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "invalid port") != NULL);
const char* a2[] = {"s", "-4", "-6"};
EXPECT_EQ_INT(server_cli_parse(3, (char**)a2, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "mutually exclusive") != NULL);
const char* a3[] = {"s", "--nope"};
EXPECT_EQ_INT(server_cli_parse(2, (char**)a3, &opts, err, sizeof(err)), -1);
EXPECT_TRUE(strstr(err, "unknown option") != NULL);
const char* a4[] = {"s", "--cert"};
EXPECT_EQ_INT(server_cli_parse(2, (char**)a4, &opts, err, sizeof(err)), -1);
server_cli_options_free(&opts);
}
static void test_server_cli_help() {
char err[256];
const char* a1[] = {"s", "--help"};
ServerCliOptions opts;
EXPECT_EQ_INT(server_cli_parse(2, (char**)a1, &opts, err, sizeof(err)), 1);
EXPECT_TRUE(opts.show_help);
server_cli_options_free(&opts);
}
void test_server_cli() {
test_server_cli_defaults();
test_server_cli_daemon_flags();
test_server_cli_config_and_dparam_forms();
test_server_cli_preserves_existing_flags();
test_server_cli_conflicts();
test_server_cli_invalid();
test_server_cli_help();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_SERVER_CLI_H
#define TEST_SERVER_CLI_H
void test_server_cli();
#endif