feat(d5-daemon-core): daemon lifecycle, module config, ::dest, PROTOCOL 2.15.0
This commit is contained in:
@@ -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()
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_DAEMON_CONF_H
|
||||
#define TEST_DAEMON_CONF_H
|
||||
|
||||
void test_daemon_conf();
|
||||
|
||||
#endif
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_SERVER_CLI_H
|
||||
#define TEST_SERVER_CLI_H
|
||||
|
||||
void test_server_cli();
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user