diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_core.c | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/tests/test_core.c b/tests/test_core.c index 945c308..12d42e5 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -724,6 +724,10 @@ static void test_pre_migration_snapshot(struct tctx *ctx) " AND name='tax_table_meta'"); CHECK(v && strcmp(v, "tax_table_meta") == 0); free(v); + v = db_text(db, "SELECT name FROM pragma_table_info('employees')" + " WHERE name='email'"); + CHECK(v && strcmp(v, "email") == 0); + free(v); int64_t user_id = 0; CHECK(db_create_user(db, "migrated", "Migrated", "secret123", 0, &user_id, &err) == 0); @@ -4745,6 +4749,353 @@ static void test_payroll(struct tctx *t) yyjson_doc_free(d); } +static void test_payslip(struct tctx *t) +{ + static const char hexkey[] = + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; + setenv("BOKFD_SECRET_KEY", hexkey, 1); + yyjson_doc *d; + + int64_t anna_id = 0; + { + char sql[256]; + snprintf(sql, sizeof sql, + "SELECT id FROM employees WHERE org_id=%d" + " AND name='Anna Anställd'", + (int)t->org_id); + char *v = db_text(g_db, sql); + CHECK(v != NULL); + if (v) + anna_id = strtoll(v, NULL, 10); + free(v); + } + CHECK(anna_id > 0); + + d = call(reqf("{\"v\":1,\"id\":\"ps1\",\"cmd\":\"employee.update\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"email\":\"anna@example.se\"}}", + g_session, (int)t->org_id, (long long)anna_id)); + CHECK_OK(d); + CHECK_STR(d, "result.email", "anna@example.se"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ps2\",\"cmd\":\"employee.update\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"email\":\"bad\\nmail\"}}", + g_session, (int)t->org_id, (long long)anna_id)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ps3\",\"cmd\":\"payroll.run_post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"period\":\"2026-07\",\"pay_date\":\"2026-07-25\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + int64_t run = jint(d, "result.id"); + int64_t voucher = jint(d, "result.voucher_id"); + CHECK(run > 0); + CHECK(voucher > 0); + yyjson_doc_free(d); + + /* a run with two lines needs employee_id */ + d = call(reqf("{\"v\":1,\"id\":\"ps4\",\"cmd\":\"payroll.payslip\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld}}", + g_session, (int)t->org_id, (long long)run)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"ps5\",\"cmd\":\"payroll.payslip\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld," + "\"employee_id\":999999}}", + g_session, (int)t->org_id, (long long)run)); + CHECK_STR(d, "error.code", "NOT_FOUND"); + yyjson_doc_free(d); + + char req[512]; + snprintf(req, sizeof req, + "{\"v\":1,\"id\":\"ps6\",\"cmd\":\"payroll.payslip\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld," + "\"employee_id\":%lld}}", + g_session, (int)t->org_id, (long long)run, (long long)anna_id); + char *raw = protocol_handle_line(g_db, req, strlen(req)); + CHECK(raw != NULL); + CHECK(raw && strstr(raw, "19750505-1111") == NULL); + CHECK(raw && strstr(raw, "JVBERi0") != NULL); + yyjson_doc *rd = raw ? yyjson_read(raw, strlen(raw), 0) : NULL; + CHECK_OK(rd); + CHECK_STR(rd, "result.filename", "Lönebesked 2026-07 Anna Anställd.pdf"); + const char *b64 = jstr(rd, "result.content_base64"); + CHECK(b64 != NULL); + CHECK(b64 && strncmp(b64, "JVBERi0", 7) == 0); + if (b64) { + unsigned char *pdf = NULL; + size_t pdf_len = 0; + CHECK(util_b64_decode(b64, strlen(b64), &pdf, &pdf_len) == 0); + if (pdf) { + char *text = xmalloc(pdf_len + 1); + memcpy(text, pdf, pdf_len); + text[pdf_len] = '\0'; + CHECK(strstr(text, "19750505-1111") == NULL); + CHECK(strstr(text, "********-1111") != NULL); + CHECK(strstr(text, "NEBESKED") != NULL); + CHECK(strstr(text, "rning 2026-07") != NULL); + CHECK(strstr(text, "-4 208,00") != NULL); + CHECK(strstr(text, "Arbetsgivaravgifter 31,42 % betalas av" + " arbetsgivaren.") != NULL); + free(text); + } + free(pdf); + } + yyjson_doc_free(rd); + free(raw); +} + +static void test_payslip_mail(struct tctx *t) +{ + static const char hexkey[] = + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; + setenv("BOKFD_SECRET_KEY", hexkey, 1); + yyjson_doc *d; + char sql[512]; + + snprintf(sql, sizeof sql, + "SELECT id FROM employees WHERE org_id=%d" + " AND name='Anna Anställd'", + (int)t->org_id); + char *v = db_text(g_db, sql); + int64_t anna_id = v ? strtoll(v, NULL, 10) : 0; + free(v); + CHECK(anna_id > 0); + snprintf(sql, sizeof sql, + "SELECT id FROM employees WHERE org_id=%d" + " AND name='Bertil Bygg'", + (int)t->org_id); + v = db_text(g_db, sql); + int64_t bertil_id = v ? strtoll(v, NULL, 10) : 0; + free(v); + CHECK(bertil_id > 0); + + d = call(reqf("{\"v\":1,\"id\":\"pm1\",\"cmd\":\"employee.update\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," + "\"email\":\"anna@example.se\"}}", + g_session, (int)t->org_id, (long long)anna_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + /* dry run: config is checked, the PDF rendered, nothing stored/sent */ + d = call(reqf("{\"v\":1,\"id\":\"pm2\",\"cmd\":\"payroll.run_post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"period\":\"2026-06\",\"pay_date\":\"2026-06-25\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + int64_t dry_run = jint(d, "result.id"); + int64_t dry_voucher = jint(d, "result.voucher_id"); + CHECK(dry_run > 0 && dry_voucher > 0); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"pm3\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_host\",\"value\":\"127.0.0.1\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm4\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_from\",\"value\":\"loner@example.se\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm5\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_security\",\"value\":\"plain\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm6\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_user\",\"value\":\"loner@example.se\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm7\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_password\",\"value\":\"hemlig\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"pm8\",\"cmd\":\"payroll.payslip_mail\"," + "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" + "{\"run_id\":%lld,\"employee_id\":%lld}}", + g_session, (int)t->org_id, (long long)dry_run, + (long long)anna_id)); + CHECK_OK(d); + CHECK(jbool(d, "result.dry_run")); + CHECK_STR(d, "result.sent_to", "anna@example.se"); + CHECK_STR(d, "result.filename", + "Lönebesked 2026-06 Anna Anställd.pdf"); + yyjson_doc_free(d); + snprintf(sql, sizeof sql, + "SELECT count(*) FROM attachments WHERE org_id=%d" + " AND filename LIKE 'Lönebesked 2026-06%%'", + (int)t->org_id); + CHECK(db_count(g_db, sql) == 0); + snprintf(sql, sizeof sql, + "SELECT count(*) FROM voucher_attachments WHERE org_id=%d" + " AND voucher_id=%lld", + (int)t->org_id, (long long)dry_voucher); + CHECK(db_count(g_db, sql) == 0); + + /* no e-mail on the employee */ + d = call(reqf("{\"v\":1,\"id\":\"pm9\",\"cmd\":\"payroll.payslip_mail\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld," + "\"employee_id\":%lld}}", + g_session, (int)t->org_id, (long long)dry_run, + (long long)bertil_id)); + 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); + + /* a successful send stores and links the attachment */ + char smtp_log[600]; + snprintf(smtp_log, sizeof smtp_log, "%s/payslip-smtp.log", t->tmpdir); + unlink(smtp_log); + int smtp_port = 0; + pid_t smtp_pid = fake_smtp_start(&smtp_port, smtp_log, 0); + CHECK(smtp_pid > 0); + d = call(reqf("{\"v\":1,\"id\":\"pm10\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_port\",\"value\":\"%d\"}}", + g_session, (int)t->org_id, smtp_port)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"pm11\",\"cmd\":\"payroll.payslip_mail\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld," + "\"employee_id\":%lld}}", + g_session, (int)t->org_id, (long long)dry_run, + (long long)anna_id)); + CHECK_OK(d); + CHECK_STR(d, "result.sent_to", "anna@example.se"); + CHECK_STR(d, "result.filename", + "Lönebesked 2026-06 Anna Anställd.pdf"); + int64_t attachment_id = jint(d, "result.attachment_id"); + CHECK(attachment_id > 0); + CHECK(jstr(d, "result.at") && *jstr(d, "result.at")); + 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[131072]; + size_t ln = fread(lbuf, 1, sizeof lbuf - 1, lf); + lbuf[ln] = '\0'; + fclose(lf); + CHECK(strstr(lbuf, "Lönebesked") != NULL); + CHECK(strstr(lbuf, "application/pdf") != NULL); + CHECK(strstr(lbuf, "To: <anna@example.se>") != NULL); + CHECK(strstr(lbuf, "hemlig") == NULL); + } + } + + snprintf(sql, sizeof sql, + "SELECT count(*) FROM voucher_attachments va JOIN attachments a" + " ON a.org_id=va.org_id AND a.id=va.attachment_id" + " WHERE va.org_id=%d AND va.voucher_id=%lld" + " AND a.id=%lld AND a.mime='application/pdf'" + " AND a.filename='Lönebesked 2026-06 Anna Anställd.pdf'", + (int)t->org_id, (long long)dry_voucher, + (long long)attachment_id); + CHECK(db_count(g_db, sql) == 1); + char *audit = db_text(g_db, "SELECT request_json FROM audit_log" + " WHERE action='payroll.payslip_mail'" + " ORDER BY seq DESC LIMIT 1"); + CHECK(audit != NULL); + free(audit); + + /* a refused recipient is SMTP_FAILED and the attachment stays linked */ + d = call(reqf("{\"v\":1,\"id\":\"pm12\",\"cmd\":\"payroll.run_post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"period\":\"2026-05\",\"pay_date\":\"2026-05-25\"}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + int64_t reject_run = jint(d, "result.id"); + int64_t reject_voucher = jint(d, "result.voucher_id"); + CHECK(reject_run > 0 && reject_voucher > 0); + yyjson_doc_free(d); + + unlink(smtp_log); + smtp_pid = fake_smtp_start(&smtp_port, smtp_log, 1); + CHECK(smtp_pid > 0); + d = call(reqf("{\"v\":1,\"id\":\"pm13\",\"cmd\":\"settings.set\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"key\":\"smtp_port\",\"value\":\"%d\"}}", + g_session, (int)t->org_id, smtp_port)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"pm14\",\"cmd\":\"payroll.payslip_mail\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld," + "\"employee_id\":%lld}}", + g_session, (int)t->org_id, (long long)reject_run, + (long long)anna_id)); + 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); + snprintf(sql, sizeof sql, + "SELECT count(*) FROM voucher_attachments va JOIN attachments a" + " ON a.org_id=va.org_id AND a.id=va.attachment_id" + " WHERE va.org_id=%d AND va.voucher_id=%lld" + " AND a.filename='Lönebesked 2026-05 Anna Anställd.pdf'", + (int)t->org_id, (long long)reject_voucher); + CHECK(db_count(g_db, sql) == 1); + + /* a fresh org has no SMTP configuration */ + d = call(reqf("{\"v\":1,\"id\":\"pm15\",\"cmd\":\"org.create\"," + "\"session\":\"%s\",\"args\":{\"name\":\"Lön utan SMTP AB\"}}", + g_session)); + CHECK_OK(d); + int64_t nomail_org = jint(d, "result.id"); + CHECK(nomail_org > 0); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm16\",\"cmd\":\"employee.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"Eva\"," + "\"personal_no\":\"19850101-1234\",\"monthly_salary_ore\":" + "2500000,\"tax_table\":30,\"tax_column\":1," + "\"email\":\"eva@example.se\"}}", + g_session, (int)nomail_org)); + CHECK_OK(d); + int64_t eva = jint(d, "result.id"); + CHECK(eva > 0); + CHECK_STR(d, "result.email", "eva@example.se"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm17\",\"cmd\":\"payroll.run_post\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"period\":\"2026-08\",\"pay_date\":\"2026-08-25\"}}", + g_session, (int)nomail_org)); + CHECK_OK(d); + int64_t nomail_run = jint(d, "result.id"); + CHECK(nomail_run > 0); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"pm18\",\"cmd\":\"payroll.payslip_mail\"," + "\"session\":\"%s\",\"org\":%d,\"args\":{\"run_id\":%lld}}", + g_session, (int)nomail_org, (long long)nomail_run)); + CHECK_STR(d, "error.code", "SMTP_NOT_CONFIGURED"); + yyjson_doc_free(d); + snprintf(sql, sizeof sql, + "SELECT count(*) FROM attachments WHERE org_id=%d", + (int)nomail_org); + CHECK(db_count(g_db, sql) == 0); + + unsetenv("BOKFD_SECRET_KEY"); +} + struct ttest { const char *name; void (*fn)(struct tctx *); @@ -4787,6 +5138,8 @@ static const struct ttest TESTS[] = { { "employees", test_employees, "org_members" }, { "tax_tables", test_tax_tables, "org_members" }, { "payroll", test_payroll, "employees tax_tables" }, + { "payslip", test_payslip, "payroll" }, + { "payslip_mail", test_payslip_mail, "payroll" }, { "transport", test_transport, "" }, { "pre_migration_snapshot", test_pre_migration_snapshot, "" }, { "rate_limit", test_rate_limit, "" }, |
