v0.0.26 - Add mnemonic-stdin and mnemonic-fd startup input modes with tests/docs
This commit is contained in:
@@ -172,5 +172,13 @@ else
|
||||
echo "[warn] no @nsigner entries found"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "[extra] mnemonic startup input tests"
|
||||
if [[ -x ./build/test_mnemonic_input ]]; then
|
||||
./build/test_mnemonic_input || true
|
||||
else
|
||||
echo "[warn] build/test_mnemonic_input not found (run: make test-mnemonic-input)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Smoke test complete."
|
||||
532
tests/test_mnemonic_input.c
Normal file
532
tests/test_mnemonic_input.c
Normal file
@@ -0,0 +1,532 @@
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define VALID_MNEMONIC "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
#define LONG_INPUT_LEN 1024
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
static void check_condition(const char *name, int condition) {
|
||||
if (condition) {
|
||||
printf("PASS: %s\n", name);
|
||||
} else {
|
||||
printf("FAIL: %s\n", name);
|
||||
g_failures++;
|
||||
}
|
||||
}
|
||||
|
||||
static int sleep_ms(int ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
|
||||
return nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
static int write_full(int fd, const void *buf, size_t len) {
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, p + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int connect_socket_retry(const char *name, int timeout_ms) {
|
||||
int elapsed = 0;
|
||||
|
||||
while (elapsed < timeout_ms) {
|
||||
int fd;
|
||||
struct sockaddr_un addr;
|
||||
socklen_t addr_len;
|
||||
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
addr.sun_path[0] = '\0';
|
||||
strncpy(&addr.sun_path[1], name, sizeof(addr.sun_path) - 2);
|
||||
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
|
||||
addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(name));
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
sleep_ms(100);
|
||||
elapsed += 100;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static pid_t spawn_with_stdin_and_fd(const char *const argv[],
|
||||
const char *stdin_data,
|
||||
int close_stdin_after_write,
|
||||
const char *fd_data,
|
||||
int fd_num,
|
||||
int close_fd_after_write,
|
||||
int *stdin_write_end,
|
||||
int *fd_write_end,
|
||||
int *capture_read_end) {
|
||||
int stdin_pipe[2] = {-1, -1};
|
||||
int fd_pipe[2] = {-1, -1};
|
||||
int capture_pipe[2] = {-1, -1};
|
||||
pid_t child;
|
||||
|
||||
if (pipe(stdin_pipe) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (pipe(capture_pipe) != 0) {
|
||||
close(stdin_pipe[0]);
|
||||
close(stdin_pipe[1]);
|
||||
return -1;
|
||||
}
|
||||
if (fd_data != NULL) {
|
||||
if (pipe(fd_pipe) != 0) {
|
||||
close(stdin_pipe[0]);
|
||||
close(stdin_pipe[1]);
|
||||
close(capture_pipe[0]);
|
||||
close(capture_pipe[1]);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
child = fork();
|
||||
if (child < 0) {
|
||||
close(stdin_pipe[0]);
|
||||
close(stdin_pipe[1]);
|
||||
close(capture_pipe[0]);
|
||||
close(capture_pipe[1]);
|
||||
if (fd_pipe[0] >= 0) close(fd_pipe[0]);
|
||||
if (fd_pipe[1] >= 0) close(fd_pipe[1]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (child == 0) {
|
||||
dup2(stdin_pipe[0], STDIN_FILENO);
|
||||
dup2(capture_pipe[1], STDOUT_FILENO);
|
||||
dup2(capture_pipe[1], STDERR_FILENO);
|
||||
|
||||
close(stdin_pipe[0]);
|
||||
close(stdin_pipe[1]);
|
||||
close(capture_pipe[0]);
|
||||
close(capture_pipe[1]);
|
||||
|
||||
if (fd_data != NULL) {
|
||||
dup2(fd_pipe[0], fd_num);
|
||||
close(fd_pipe[0]);
|
||||
close(fd_pipe[1]);
|
||||
}
|
||||
|
||||
execv("./build/nsigner", (char *const *)argv);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
close(stdin_pipe[0]);
|
||||
close(capture_pipe[1]);
|
||||
|
||||
if (fd_data != NULL) {
|
||||
close(fd_pipe[0]);
|
||||
}
|
||||
|
||||
if (stdin_data != NULL) {
|
||||
(void)write_full(stdin_pipe[1], stdin_data, strlen(stdin_data));
|
||||
if (close_stdin_after_write) {
|
||||
close(stdin_pipe[1]);
|
||||
stdin_pipe[1] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (fd_data != NULL) {
|
||||
(void)write_full(fd_pipe[1], fd_data, strlen(fd_data));
|
||||
if (close_fd_after_write) {
|
||||
close(fd_pipe[1]);
|
||||
fd_pipe[1] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (stdin_write_end != NULL) {
|
||||
*stdin_write_end = stdin_pipe[1];
|
||||
} else if (stdin_pipe[1] >= 0) {
|
||||
close(stdin_pipe[1]);
|
||||
}
|
||||
|
||||
if (fd_write_end != NULL) {
|
||||
*fd_write_end = fd_pipe[1];
|
||||
} else if (fd_pipe[1] >= 0) {
|
||||
close(fd_pipe[1]);
|
||||
}
|
||||
|
||||
if (capture_read_end != NULL) {
|
||||
*capture_read_end = capture_pipe[0];
|
||||
} else {
|
||||
close(capture_pipe[0]);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
static int read_capture_nonblocking(int fd, char *buf, size_t buf_sz) {
|
||||
ssize_t n;
|
||||
if (buf == NULL || buf_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
n = read(fd, buf, buf_sz - 1);
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
buf[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wait_exit_timeout(pid_t pid, int timeout_ms, int *status) {
|
||||
int elapsed = 0;
|
||||
while (elapsed < timeout_ms) {
|
||||
pid_t rc = waitpid(pid, status, WNOHANG);
|
||||
if (rc == pid) {
|
||||
return 0;
|
||||
}
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
sleep_ms(50);
|
||||
elapsed += 50;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void test_stdin_happy_path(void) {
|
||||
const char *const argv[] = {
|
||||
"./build/nsigner", "--socket-name", "nsigner_test_mi_stdin_ok", "--mnemonic-stdin", NULL
|
||||
};
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
int fd;
|
||||
pid_t child = spawn_with_stdin_and_fd(argv,
|
||||
VALID_MNEMONIC "\n",
|
||||
1,
|
||||
NULL,
|
||||
-1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition("spawn --mnemonic-stdin happy-path child", child > 0);
|
||||
if (child <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fd = connect_socket_retry("nsigner_test_mi_stdin_ok", 5000);
|
||||
check_condition("--mnemonic-stdin reaches running server", fd >= 0);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
(void)kill(child, SIGTERM);
|
||||
(void)waitpid(child, &status, 0);
|
||||
if (cap_fd >= 0) {
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_stdin_eof_without_newline(void) {
|
||||
const char *const argv[] = {
|
||||
"./build/nsigner", "--socket-name", "nsigner_test_mi_stdin_eof", "--mnemonic-stdin", NULL
|
||||
};
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
int fd;
|
||||
pid_t child = spawn_with_stdin_and_fd(argv,
|
||||
VALID_MNEMONIC,
|
||||
1,
|
||||
NULL,
|
||||
-1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition("spawn --mnemonic-stdin EOF-without-newline child", child > 0);
|
||||
if (child <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fd = connect_socket_retry("nsigner_test_mi_stdin_eof", 5000);
|
||||
check_condition("--mnemonic-stdin EOF without newline accepted", fd >= 0);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
(void)kill(child, SIGTERM);
|
||||
(void)waitpid(child, &status, 0);
|
||||
if (cap_fd >= 0) {
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_stdin_invalid_mnemonic(void) {
|
||||
const char *const argv[] = {
|
||||
"./build/nsigner", "--mnemonic-stdin", NULL
|
||||
};
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
char cap[512];
|
||||
pid_t child = spawn_with_stdin_and_fd(argv,
|
||||
"not a valid mnemonic\n",
|
||||
1,
|
||||
NULL,
|
||||
-1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition("spawn --mnemonic-stdin invalid child", child > 0);
|
||||
if (child <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_condition("--mnemonic-stdin invalid exits", wait_exit_timeout(child, 2000, &status) == 0);
|
||||
check_condition("--mnemonic-stdin invalid exits non-zero", WIFEXITED(status) && WEXITSTATUS(status) != 0);
|
||||
|
||||
if (cap_fd >= 0) {
|
||||
(void)read_capture_nonblocking(cap_fd, cap, sizeof(cap));
|
||||
check_condition("--mnemonic-stdin invalid emits validation failure", strstr(cap, "mnemonic validation failed") != NULL);
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_stdin_overflow(void) {
|
||||
const char *const argv[] = {
|
||||
"./build/nsigner", "--mnemonic-stdin", NULL
|
||||
};
|
||||
char long_input[LONG_INPUT_LEN + 2];
|
||||
int i;
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
char cap[512];
|
||||
pid_t child;
|
||||
|
||||
for (i = 0; i < LONG_INPUT_LEN; ++i) {
|
||||
long_input[i] = 'a';
|
||||
}
|
||||
long_input[LONG_INPUT_LEN] = '\n';
|
||||
long_input[LONG_INPUT_LEN + 1] = '\0';
|
||||
|
||||
child = spawn_with_stdin_and_fd(argv,
|
||||
long_input,
|
||||
1,
|
||||
NULL,
|
||||
-1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition("spawn --mnemonic-stdin overflow child", child > 0);
|
||||
if (child <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_condition("--mnemonic-stdin overflow exits", wait_exit_timeout(child, 2000, &status) == 0);
|
||||
check_condition("--mnemonic-stdin overflow exits non-zero", WIFEXITED(status) && WEXITSTATUS(status) != 0);
|
||||
|
||||
if (cap_fd >= 0) {
|
||||
(void)read_capture_nonblocking(cap_fd, cap, sizeof(cap));
|
||||
check_condition("--mnemonic-stdin overflow emits length error", strstr(cap, "exceeded maximum length") != NULL);
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_fd_happy_path(void) {
|
||||
const char *const argv[] = {
|
||||
"./build/nsigner", "--socket-name", "nsigner_test_mi_fd_ok", "--mnemonic-fd", "3", NULL
|
||||
};
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
int fd;
|
||||
pid_t child = spawn_with_stdin_and_fd(argv,
|
||||
"",
|
||||
0,
|
||||
VALID_MNEMONIC "\n",
|
||||
3,
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition("spawn --mnemonic-fd happy-path child", child > 0);
|
||||
if (child <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fd = connect_socket_retry("nsigner_test_mi_fd_ok", 5000);
|
||||
check_condition("--mnemonic-fd reaches running server", fd >= 0);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
(void)kill(child, SIGTERM);
|
||||
(void)waitpid(child, &status, 0);
|
||||
if (cap_fd >= 0) {
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_mutual_exclusion_and_validation_errors(void) {
|
||||
struct {
|
||||
const char *name;
|
||||
const char *const *argv;
|
||||
const char *expect_substr;
|
||||
} cases[] = {
|
||||
{
|
||||
"mutual exclusion stdin+fd",
|
||||
(const char *const[]){"./build/nsigner", "--mnemonic-stdin", "--mnemonic-fd", "3", NULL},
|
||||
"mutually exclusive"
|
||||
},
|
||||
{
|
||||
"stdin with stdio listener",
|
||||
(const char *const[]){"./build/nsigner", "--listen", "stdio", "--mnemonic-stdin", NULL},
|
||||
"requires --listen unix or --listen tcp"
|
||||
},
|
||||
{
|
||||
"mnemonic-fd 1 rejected",
|
||||
(const char *const[]){"./build/nsigner", "--mnemonic-fd", "1", NULL},
|
||||
"--mnemonic-fd 1 is not allowed"
|
||||
},
|
||||
{
|
||||
"mnemonic-fd 0 rejected with stdio",
|
||||
(const char *const[]){"./build/nsigner", "--listen", "stdio", "--mnemonic-fd", "0", NULL},
|
||||
"cannot be used with --listen stdio"
|
||||
}
|
||||
};
|
||||
|
||||
size_t i;
|
||||
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
char cap[512];
|
||||
pid_t child = spawn_with_stdin_and_fd(cases[i].argv,
|
||||
"",
|
||||
1,
|
||||
NULL,
|
||||
-1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition(cases[i].name, child > 0);
|
||||
if (child <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
check_condition("expected parse-time failure exits", wait_exit_timeout(child, 2000, &status) == 0);
|
||||
check_condition("expected parse-time failure non-zero", WIFEXITED(status) && WEXITSTATUS(status) != 0);
|
||||
|
||||
if (cap_fd >= 0) {
|
||||
(void)read_capture_nonblocking(cap_fd, cap, sizeof(cap));
|
||||
check_condition("expected parse-time error message", strstr(cap, cases[i].expect_substr) != NULL);
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void test_lock_reunlock_falls_back_to_tui_after_fd_startup(void) {
|
||||
const char *const argv[] = {
|
||||
"./build/nsigner", "--socket-name", "nsigner_test_mi_lock", "--mnemonic-fd", "3", NULL
|
||||
};
|
||||
int stdin_write = -1;
|
||||
int cap_fd = -1;
|
||||
int status = 0;
|
||||
int fd;
|
||||
pid_t child = spawn_with_stdin_and_fd(argv,
|
||||
"",
|
||||
0,
|
||||
VALID_MNEMONIC "\n",
|
||||
3,
|
||||
1,
|
||||
&stdin_write,
|
||||
NULL,
|
||||
&cap_fd);
|
||||
|
||||
check_condition("spawn --mnemonic-fd lock test child", child > 0);
|
||||
if (child <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fd = connect_socket_retry("nsigner_test_mi_lock", 5000);
|
||||
check_condition("--mnemonic-fd lock test initial running server", fd >= 0);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
(void)write_full(stdin_write, "l\n" VALID_MNEMONIC "\n", strlen("l\n" VALID_MNEMONIC "\n"));
|
||||
sleep_ms(800);
|
||||
|
||||
fd = connect_socket_retry("nsigner_test_mi_lock", 2000);
|
||||
check_condition("lock+reunlock via TUI keeps server alive", fd >= 0);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
if (stdin_write >= 0) {
|
||||
close(stdin_write);
|
||||
}
|
||||
|
||||
(void)kill(child, SIGTERM);
|
||||
(void)waitpid(child, &status, 0);
|
||||
if (cap_fd >= 0) {
|
||||
close(cap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
test_stdin_happy_path();
|
||||
test_stdin_eof_without_newline();
|
||||
test_stdin_invalid_mnemonic();
|
||||
test_stdin_overflow();
|
||||
test_fd_happy_path();
|
||||
test_mutual_exclusion_and_validation_errors();
|
||||
test_lock_reunlock_falls_back_to_tui_after_fd_startup();
|
||||
|
||||
if (g_failures == 0) {
|
||||
printf("ALL TESTS PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("TESTS FAILED: %d\n", g_failures);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user