diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_core.c | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/tests/test_core.c b/tests/test_core.c index 47b6acf..29985e2 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -343,6 +343,134 @@ static void test_transport(void) } } +/* Minimal SMTP server for invoice.send: 220, EHLO (multiline), MAIL/RCPT, + 354, DATA capture until ".", 250 and 221. The transcript goes to logpath + as "C:" (client), "S:" (server) and "D:" (data) lines. */ +static int smtp_child_line(int fd, char *buf, size_t n) +{ + size_t o = 0; + for (;;) { + char ch; + ssize_t r = read(fd, &ch, 1); + if (r <= 0) + return -1; + if (ch == '\n') { + while (o > 0 && buf[o - 1] == '\r') + o--; + buf[o] = '\0'; + return (int)o; + } + if (o + 1 < n) + buf[o++] = ch; + } +} + +static void smtp_child_say(int fd, FILE *log, const char *line) +{ + fprintf(log, "S: %s\n", line); + fflush(log); + char out[512]; + int n = snprintf(out, sizeof out, "%s\r\n", line); + if (n > 0) { + ssize_t wr = write(fd, out, (size_t)n); + (void)wr; + } +} + +static void smtp_child(int wfd, const char *logpath, int reject_rcpt) +{ + signal(SIGPIPE, SIG_IGN); + alarm(20); + int sfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sfd < 0) + _exit(1); + struct sockaddr_in sa; + memset(&sa, 0, sizeof sa); + sa.sin_family = AF_INET; + sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sa.sin_port = 0; + if (bind(sfd, (struct sockaddr *)&sa, sizeof sa) != 0 || + listen(sfd, 1) != 0) + _exit(1); + socklen_t slen = sizeof sa; + if (getsockname(sfd, (struct sockaddr *)&sa, &slen) != 0) + _exit(1); + int port = ntohs(sa.sin_port); + if (write(wfd, &port, sizeof port) != (ssize_t)sizeof port) + _exit(1); + close(wfd); + int cfd = accept(sfd, NULL, NULL); + if (cfd < 0) + _exit(1); + FILE *log = fopen(logpath, "w"); + if (!log) + _exit(1); + smtp_child_say(cfd, log, "220 test.invalid ESMTP"); + char line[2048]; + while (smtp_child_line(cfd, line, sizeof line) >= 0) { + fprintf(log, "C: %s\n", line); + fflush(log); + if (strncmp(line, "EHLO", 4) == 0 || + strncmp(line, "HELO", 4) == 0) { + smtp_child_say(cfd, log, "250-test.invalid"); + smtp_child_say(cfd, log, "250 AUTH PLAIN LOGIN"); + } else if (strncmp(line, "AUTH", 4) == 0) { + smtp_child_say(cfd, log, "235 2.7.0 authenticated"); + } else if (strncmp(line, "MAIL FROM:", 10) == 0) { + smtp_child_say(cfd, log, "250 2.1.0 ok"); + } else if (strncmp(line, "RCPT TO:", 8) == 0) { + if (reject_rcpt) { + smtp_child_say(cfd, log, "550 5.1.1 no such user"); + break; + } + smtp_child_say(cfd, log, "250 2.1.5 ok"); + } else if (strcmp(line, "DATA") == 0) { + smtp_child_say(cfd, log, "354 end with ."); + for (;;) { + if (smtp_child_line(cfd, line, sizeof line) < 0) + break; + fprintf(log, "D: %s\n", line); + fflush(log); + if (strcmp(line, ".") == 0) + break; + } + smtp_child_say(cfd, log, "250 2.0.0 queued"); + } else if (strcmp(line, "QUIT") == 0) { + smtp_child_say(cfd, log, "221 2.0.0 bye"); + break; + } else { + smtp_child_say(cfd, log, "250 2.0.0 ok"); + } + } + fclose(log); + close(cfd); + close(sfd); + _exit(0); +} + +static pid_t fake_smtp_start(int *out_port, const char *logpath, int reject_rcpt) +{ + int pfd[2]; + if (pipe(pfd) != 0) + return -1; + pid_t pid = fork(); + if (pid == 0) { + close(pfd[0]); + smtp_child(pfd[1], logpath, reject_rcpt); + } + close(pfd[1]); + int port = 0; + ssize_t got = read(pfd[0], &port, sizeof port); + close(pfd[0]); + if (got != (ssize_t)sizeof port || port <= 0) { + if (pid > 0) + waitpid(pid, NULL, 0); + return -1; + } + *out_port = port; + return pid; +} + static int count_snapshots(const char *dir) { DIR *d = opendir(dir); @@ -3023,6 +3151,234 @@ int main(void) free(preview_b64); + /* ---------------- invoice.send over a fake SMTP server ----------- */ + + static const char smtp_key[] = + "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f"; + char smtp_log[600]; + snprintf(smtp_log, sizeof smtp_log, "%s/smtp.log", tmpdir); + unlink(smtp_log); + setenv("BOKFD_SECRET_KEY", smtp_key, 1); + + d = call(reqf("{\"v\":1,\"id\":\"smtp1\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_host\",\"value\":\"127.0.0.1\"}}", + g_session, (int)org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp2\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_from\",\"value\":\"fakturor@example.se\"}}", + g_session, (int)org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + /* user set: the password must decrypt and AUTH PLAIN must run */ + d = call(reqf("{\"v\":1,\"id\":\"smtp2b\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_user\",\"value\":\"faktura@example.se\"}}", + g_session, (int)org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp3\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_security\",\"value\":\"plain\"}}", + g_session, (int)org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp4\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_password\",\"value\":\"hemlig\"}}", + g_session, (int)org_id)); + CHECK_OK(d); + CHECK_STR(d, "result.value", "[redacted]"); + yyjson_doc_free(d); + + /* dry_run before any server exists: valid config, no send, no update */ + d = call(reqf("{\"v\":1,\"id\":\"smtp5\",\"cmd\":\"invoice.send\"," + "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" + "{\"id\":%lld,\"to\":\"test@example.se\"}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_OK(d); + CHECK(jbool(d, "result.dry_run")); + CHECK(jint(d, "result.id") == inv1); + CHECK_STR(d, "result.to", "test@example.se"); + CHECK_STR(d, "result.subject", "Faktura 17761"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp6\",\"cmd\":\"invoice.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_OK(d); + CHECK(yyjson_is_null(jget(d, "result.last_sent_at"))); + yyjson_doc_free(d); + + int smtp_port = 0; + pid_t smtp_pid = fake_smtp_start(&smtp_port, smtp_log, 0); + CHECK(smtp_pid > 0); + CHECK(smtp_port > 0); + + d = call(reqf("{\"v\":1,\"id\":\"smtp7\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_port\",\"value\":\"%d\"}}", + g_session, (int)org_id, smtp_port)); + CHECK_OK(d); + yyjson_doc_free(d); + + char sent_at[64] = ""; + d = call(reqf("{\"v\":1,\"id\":\"smtp8\",\"cmd\":\"invoice.send\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_OK(d); + CHECK(jint(d, "result.id") == inv1); + CHECK_STR(d, "result.sent_to", "faktura@andra.se"); + if (jstr(d, "result.at")) + snprintf(sent_at, sizeof sent_at, "%s", jstr(d, "result.at")); + CHECK(sent_at[0] != '\0'); + yyjson_doc_free(d); + if (smtp_pid > 0) + waitpid(smtp_pid, NULL, 0); + + { + FILE *lf = fopen(smtp_log, "rb"); + CHECK(lf != NULL); + if (lf) { + char lbuf[65536]; + size_t ln = fread(lbuf, 1, sizeof lbuf - 1, lf); + lbuf[ln] = '\0'; + fclose(lf); + CHECK(strstr(lbuf, "Faktura 17761") != NULL); + CHECK(strstr(lbuf, "application/pdf") != NULL); + CHECK(strstr(lbuf, "To: <faktura@andra.se>") != NULL); + CHECK(strstr(lbuf, "hemlig") == NULL); + } + } + + d = call(reqf("{\"v\":1,\"id\":\"smtp9\",\"cmd\":\"invoice.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_OK(d); + CHECK_STR(d, "result.last_sent_at", sent_at); + CHECK_STR(d, "result.last_sent_to", "faktura@andra.se"); + yyjson_doc_free(d); + + char *send_audit = db_text( + g_db, "SELECT request_json FROM audit_log" + " WHERE action='invoice.send' ORDER BY seq DESC LIMIT 1"); + CHECK(send_audit != NULL); + CHECK(send_audit && strstr(send_audit, "\"subject\":\"Faktura 17761\"")); + CHECK(send_audit && strstr(send_audit, "hemlig") == NULL); + free(send_audit); + + /* with a user set the stored password must decrypt */ + unsetenv("BOKFD_SECRET_KEY"); + d = call(reqf("{\"v\":1,\"id\":\"smtp9b\",\"cmd\":\"invoice.send\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_STR(d, "error.code", "SMTP_NOT_CONFIGURED"); + yyjson_doc_free(d); + setenv("BOKFD_SECRET_KEY", smtp_key, 1); + + /* a refused recipient maps to SMTP_FAILED and leaves the stamp alone */ + unlink(smtp_log); + smtp_pid = fake_smtp_start(&smtp_port, smtp_log, 1); + CHECK(smtp_pid > 0); + + d = call(reqf("{\"v\":1,\"id\":\"smtp10\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_port\",\"value\":\"%d\"}}", + g_session, (int)org_id, smtp_port)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp11\",\"cmd\":\"invoice.send\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_STR(d, "error.code", "SMTP_FAILED"); + CHECK(jstr(d, "error.message") && strstr(jstr(d, "error.message"), "550")); + yyjson_doc_free(d); + if (smtp_pid > 0) + waitpid(smtp_pid, NULL, 0); + + d = call(reqf("{\"v\":1,\"id\":\"smtp12\",\"cmd\":\"invoice.get\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)org_id, (long long)inv1)); + CHECK_OK(d); + CHECK_STR(d, "result.last_sent_at", sent_at); + CHECK_STR(d, "result.last_sent_to", "faktura@andra.se"); + yyjson_doc_free(d); + + /* missing SMTP settings on a fresh org; recipient is checked first */ + d = call(reqf("{\"v\":1,\"id\":\"smtp13\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"SMTP Saknas AB\"}}", + g_session)); + CHECK_OK(d); + int64_t smtp_org = jint(d, "result.id"); + CHECK(smtp_org > 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp14\",\"cmd\":\"customer.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":" + "\"Med Epost AB\",\"email\":\"mottagare@example.se\"}}", + g_session, (int)smtp_org)); + CHECK_OK(d); + int64_t smtp_cust = jint(d, "result.id"); + CHECK(smtp_cust > 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp15\",\"cmd\":\"customer.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":" + "\"Utan Epost AB\"}}", + g_session, (int)smtp_org)); + CHECK_OK(d); + int64_t no_mail_cust = jint(d, "result.id"); + CHECK(no_mail_cust > 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp16\",\"cmd\":\"invoice.issue\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" + "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":" + "\"2026-10-20\",\"rows\":[{\"description\":\"Tjänst\"," + "\"quantity\":\"1\",\"unit_price_ore\":10000," + "\"vat_code\":\"25\"}]}}", + g_session, (int)smtp_org, (long long)smtp_cust)); + CHECK_OK(d); + int64_t smtp_inv = jint(d, "result.id"); + CHECK(smtp_inv > 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp17\",\"cmd\":\"invoice.send\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)smtp_org, (long long)smtp_inv)); + CHECK_STR(d, "error.code", "SMTP_NOT_CONFIGURED"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp18\",\"cmd\":\"invoice.issue\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" + "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":" + "\"2026-10-20\",\"rows\":[{\"description\":\"Tjänst\"," + "\"quantity\":\"1\",\"unit_price_ore\":10000," + "\"vat_code\":\"25\"}]}}", + g_session, (int)smtp_org, (long long)no_mail_cust)); + CHECK_OK(d); + int64_t no_mail_inv = jint(d, "result.id"); + CHECK(no_mail_inv > 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"smtp19\",\"cmd\":\"invoice.send\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", + g_session, (int)smtp_org, (long long)no_mail_inv)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + CHECK(jstr(d, "error.message") && + strstr(jstr(d, "error.message"), "no e-mail") != NULL); + yyjson_doc_free(d); + + unsetenv("BOKFD_SECRET_KEY"); + /* ---------------- moms rules: ranges merge per box ------------- */ d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"org.create\"," "\"session\":\"%s\",\"args\":{\"name\":\"Moms AB\"}}", |
