From 3a5a6d3b04dc4e17a42225afb243eecb3502ead6 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 21:47:22 +0200 Subject: tests: named test_core sections with --only --- tests/test_core.c | 1005 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 661 insertions(+), 344 deletions(-) (limited to 'tests') diff --git a/tests/test_core.c b/tests/test_core.c index 29985e2..96361cb 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -26,13 +26,37 @@ static sqlite3 *g_db; static char g_session[128]; static int g_failures; +static const char *g_test = ""; +static const char *g_failed[64]; +static int g_failed_n; + +static void mark_failed(void) +{ + for (int i = 0; i < g_failed_n; i++) + if (strcmp(g_failed[i], g_test) == 0) + return; + if (g_failed_n < (int)(sizeof g_failed / sizeof g_failed[0])) + g_failed[g_failed_n++] = g_test; +} + +struct tctx { + char tmpdir[512]; + char backupdir[512]; + int64_t org_id; + int64_t fy1; + int64_t sale_id; + char *sie_b64; + int64_t inv1; + int64_t vat_org; +}; #define CHECK(cond) \ do { \ if (!(cond)) { \ - fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ + fprintf(stderr, "FAIL %s:%d: %s\n", g_test, __LINE__, \ #cond); \ g_failures++; \ + mark_failed(); \ } \ } while (0) @@ -111,9 +135,10 @@ static void check_str(yyjson_doc *d, const char *path, const char *expect, { const char *got = jstr(d, path); if (!got || strcmp(got, expect) != 0) { - fprintf(stderr, "FAIL %s:%d: %s = %s, expected %s\n", __FILE__, line, + fprintf(stderr, "FAIL %s:%d: %s = %s, expected %s\n", g_test, line, path, got ? got : "(null)", expect); g_failures++; + mark_failed(); } } @@ -123,9 +148,10 @@ static void check_ok(yyjson_doc *d, int line) { if (!jbool(d, "ok")) { const char *code = jstr(d, "error.code"); - fprintf(stderr, "FAIL %s:%d: not ok (%s)\n", __FILE__, line, + fprintf(stderr, "FAIL %s:%d: not ok (%s)\n", g_test, line, code ? code : "no code"); g_failures++; + mark_failed(); } } @@ -293,8 +319,9 @@ static void echo_child(int wfd, int tls) /* variant 0: plaintext tcp; 1: TLS trusting the test CA; 2: TLS without the CA, which must fail certificate verification. */ -static void test_transport(void) +static void test_transport(struct tctx *t) { + (void)t; signal(SIGPIPE, SIG_IGN); for (int variant = 0; variant < 3; variant++) { int tls = variant > 0; @@ -532,18 +559,17 @@ static int find_snapshot(const char *dir, char *out, size_t n) return found; } -static void test_pre_migration_snapshot(const char *tmpdir, - const char *backupdir) +static void test_pre_migration_snapshot(struct tctx *ctx) { char dbpath[600]; - snprintf(dbpath, sizeof dbpath, "%s/migrate.db", tmpdir); + snprintf(dbpath, sizeof dbpath, "%s/migrate.db", ctx->tmpdir); char *err = NULL; sqlite3 *db = NULL; CHECK(db_open(dbpath, &db, &err) == 0); free(err); err = NULL; - CHECK(count_snapshots(backupdir) == 0); + CHECK(count_snapshots(ctx->backupdir) == 0); if (!db) return; @@ -568,7 +594,7 @@ static void test_pre_migration_snapshot(const char *tmpdir, /* a failed snapshot must abort before the first migration statement */ char blocker[600], baddir[700]; - snprintf(blocker, sizeof blocker, "%s/blocker", tmpdir); + snprintf(blocker, sizeof blocker, "%s/blocker", ctx->tmpdir); snprintf(baddir, sizeof baddir, "%s/backup", blocker); FILE *f = fopen(blocker, "wb"); if (f) @@ -582,7 +608,7 @@ static void test_pre_migration_snapshot(const char *tmpdir, free(err); err = NULL; free(g_cfg.backup_dir); - g_cfg.backup_dir = xstrdup(backupdir); + g_cfg.backup_dir = xstrdup(ctx->backupdir); sqlite3 *raw = NULL; CHECK(sqlite3_open_v2(dbpath, &raw, SQLITE_OPEN_READONLY, NULL) == @@ -603,8 +629,8 @@ static void test_pre_migration_snapshot(const char *tmpdir, char stamp[32]; strftime(stamp, sizeof stamp, "%Y%m%dT%H%M%SZ", &tm); char occupied[1400]; - snprintf(occupied, sizeof occupied, "%s/pre-migration-v3-%s.db", backupdir, - stamp); + snprintf(occupied, sizeof occupied, "%s/pre-migration-v3-%s.db", + ctx->backupdir, stamp); f = fopen(occupied, "wb"); if (f) { fputs("occupied", f); @@ -620,7 +646,7 @@ static void test_pre_migration_snapshot(const char *tmpdir, unlink(dbpath); return; } - CHECK(count_snapshots(backupdir) == 2); + CHECK(count_snapshots(ctx->backupdir) == 2); time_t t2 = (time_t)util_now(); gmtime_r(&t2, &tm); @@ -629,7 +655,7 @@ static void test_pre_migration_snapshot(const char *tmpdir, if (strcmp(stamp, stamp2) == 0) { char expect[1500]; snprintf(expect, sizeof expect, "%s/pre-migration-v3-%s-1.db", - backupdir, stamp); + ctx->backupdir, stamp); CHECK(access(expect, F_OK) == 0); } @@ -644,7 +670,7 @@ static void test_pre_migration_snapshot(const char *tmpdir, /* the snapshot is a valid database holding the pre-migration state */ char snap[1500] = ""; - CHECK(find_snapshot(backupdir, snap, sizeof snap) == 0); + CHECK(find_snapshot(ctx->backupdir, snap, sizeof snap) == 0); sqlite3 *sdb = NULL; CHECK(sqlite3_open_v2(snap, &sdb, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); v = db_text(sdb, "SELECT value FROM meta WHERE key='schema_version'"); @@ -690,15 +716,16 @@ static void test_pre_migration_snapshot(const char *tmpdir, if (snap[0]) unlink(snap); unlink(dbpath); - snprintf(dbpath, sizeof dbpath, "%s/migrate.db-wal", tmpdir); + snprintf(dbpath, sizeof dbpath, "%s/migrate.db-wal", ctx->tmpdir); unlink(dbpath); - snprintf(dbpath, sizeof dbpath, "%s/migrate.db-shm", tmpdir); + snprintf(dbpath, sizeof dbpath, "%s/migrate.db-shm", ctx->tmpdir); unlink(dbpath); unlink(blocker); } -static void test_secret(void) +static void test_secret(struct tctx *t) { + (void)t; static const char hexkey[] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; static const char otherkey[] = @@ -796,45 +823,9 @@ static void test_secret(void) CHECK(!secret_available()); free(b64key); } - -int main(void) +static void test_session_auth(struct tctx *t) { - char tmpdir[] = "/tmp/bokf-test-XXXXXX"; - if (!mkdtemp(tmpdir)) { - perror("mkdtemp"); - return 1; - } - char dbpath[512], backupdir[512]; - snprintf(dbpath, sizeof dbpath, "%s/bokfd.db", tmpdir); - snprintf(backupdir, sizeof backupdir, "%s/backup", tmpdir); - mkdir(backupdir, 0700); - - config_defaults(); - g_cfg.log_level = 0; - free(g_cfg.backup_dir); - g_cfg.backup_dir = xstrdup(backupdir); - free(g_cfg.export_dir); - g_cfg.export_dir = xstrdup(tmpdir); - free(g_cfg.chart_k2_file); - g_cfg.chart_k2_file = xstrdup("data/bas_k2.csv"); - free(g_cfg.chart_k3_file); - g_cfg.chart_k3_file = xstrdup("data/bas_k3.csv"); - - char *err = NULL; - if (db_open(dbpath, &g_db, &err) != 0) { - fprintf(stderr, "db_open: %s\n", err ? err : "?"); - return 1; - } - int64_t admin_id = 0; - if (db_create_user(g_db, "admin", "Admin", "secret123", 1, &admin_id, - &err) != 0) { - fprintf(stderr, "create admin: %s\n", err ? err : "?"); - return 1; - } - sessions_init(3600); - - test_secret(); - + (void)t; yyjson_doc *d; d = call("{\"v\":1,\"id\":\"1\",\"cmd\":\"health\"}"); @@ -876,14 +867,19 @@ int main(void) CHECK_STR(d, "result.user.username", "admin"); CHECK(jbool(d, "result.user.is_admin")); yyjson_doc_free(d); +} + +static void test_org_members(struct tctx *t) +{ + yyjson_doc *d; d = call(reqf("{\"v\":1,\"id\":\"7\",\"cmd\":\"org.create\"," "\"session\":\"%s\",\"args\":" "{\"name\":\"AB Ett\",\"org_nr\":\"5560123456\"}}", g_session)); CHECK_OK(d); - int64_t org_id = jint(d, "result.id"); - CHECK(org_id > 0); + t->org_id = jint(d, "result.id"); + CHECK(t->org_id > 0); CHECK_STR(d, "result.name", "AB Ett"); CHECK_STR(d, "result.org_nr", "5560123456"); CHECK(jint(d, "result.fiscal_year_id") > 0); @@ -905,7 +901,7 @@ int main(void) "\"phone\":\"+46700000000\"," "\"description\":\"Konsultverksamhet inom it.\"," "\"shares\":25000}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.vat_nr", "SE559331212601"); CHECK_STR(d, "result.org_nr", "559331-2126"); @@ -915,7 +911,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"8c\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.address", "Lingonvägen 9"); CHECK_STR(d, "result.postal_code", "192 48"); @@ -925,20 +921,20 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"8d\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"moms_period\":\"week\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8e\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"city\":\"Nyköping\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8f\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.city", "Sollentuna"); CHECK_STR(d, "result.description", "Konsultverksamhet inom it."); @@ -948,7 +944,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"8g\",\"cmd\":\"board.add\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Anders Bergsten\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t bm1 = jint(d, "result.id"); CHECK(bm1 > 0); @@ -959,7 +955,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Saphira Bergsten\"," "\"title\":\"Styrelseledamot\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t bm2 = jint(d, "result.id"); CHECK(bm2 > 0); @@ -967,7 +963,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"8i\",\"cmd\":\"board.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 2); CHECK_STR(d, "result.items.0.name", "Anders Bergsten"); @@ -977,40 +973,40 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"8j\",\"cmd\":\"board.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"title\":\"Suppleant\"}}", - g_session, (int)org_id, (long long)bm2)); + g_session, (int)t->org_id, (long long)bm2)); CHECK_OK(d); CHECK_STR(d, "result.title", "Suppleant"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8k\",\"cmd\":\"board.add\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8l\",\"cmd\":\"board.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999," "\"name\":\"Ingen\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8m\",\"cmd\":\"board.remove\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)bm2)); + g_session, (int)t->org_id, (long long)bm2)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8n\",\"cmd\":\"board.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"9\",\"cmd\":\"session.use_org\"," "\"session\":\"%s\",\"args\":{\"org\":%d}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.role", "owner"); yyjson_doc_free(d); @@ -1023,15 +1019,21 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"11\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.moms_period", "month"); yyjson_doc_free(d); +} + +static void test_tokens(struct tctx *t) +{ + char *err = NULL; + yyjson_doc *d; d = call(reqf("{\"v\":1,\"id\":\"12\",\"cmd\":\"token.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"label\":\"test\",\"scopes\":[\"read\",\"write\"]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); char token_copy[128] = ""; snprintf(token_copy, sizeof token_copy, "%s", jstr(d, "result.token")); @@ -1040,14 +1042,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"13\",\"cmd\":\"token.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"14\",\"cmd\":\"token.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"label\":\"ro\",\"scopes\":[\"read\"]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); char ro_token[128] = ""; snprintf(ro_token, sizeof ro_token, "%s", jstr(d, "result.token")); @@ -1057,7 +1059,7 @@ int main(void) "{\"method\":\"token\",\"token\":\"%s\"}}", token_copy)); CHECK_OK(d); - CHECK(jint(d, "result.active_org") == org_id); + CHECK(jint(d, "result.active_org") == t->org_id); snprintf(g_session, sizeof g_session, "%s", jstr(d, "result.session")); yyjson_doc_free(d); @@ -1078,7 +1080,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"18\",\"cmd\":\"org.member_add\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\",\"role\":\"viewer\"}}", - ro_session, (int)org_id)); + ro_session, (int)t->org_id)); CHECK_STR(d, "error.code", "FORBIDDEN"); yyjson_doc_free(d); @@ -1102,34 +1104,34 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"21\",\"cmd\":\"org.member_add\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\",\"role\":\"viewer\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"22\",\"cmd\":\"org.member_list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK(yyjson_arr_size(jget(d, "result.items")) == 2); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"23\",\"cmd\":\"org.member_set_role\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\",\"role\":\"bookkeeper\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"24\",\"cmd\":\"org.member_remove\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"admin\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"25\",\"cmd\":\"org.member_remove\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); @@ -1146,7 +1148,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"27\",\"cmd\":\"audit.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) > 0); yyjson_doc_free(d); @@ -1175,7 +1177,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"28b\",\"cmd\":\"token.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"label\":\"backup\",\"scopes\":[\"admin\"]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); char backup_token[128] = ""; snprintf(backup_token, sizeof backup_token, "%s", jstr(d, "result.token")); @@ -1192,7 +1194,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"28d\",\"cmd\":\"backup.snapshot\"," "\"session\":\"%s\",\"args\":{\"dest\":\"%s/snap-token.db\"}}", - backup_session, tmpdir)); + backup_session, t->tmpdir)); CHECK_OK(d); yyjson_doc_free(d); @@ -1243,21 +1245,21 @@ int main(void) "{\"description\":\"Saknar datum\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"30d\",\"cmd\":\"account.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"number\":1930," "\"name\":\"Fel typ\",\"type\":\"expense\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"30e\",\"cmd\":\"org.member_set_role\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"admin\",\"role\":\"king\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -1268,13 +1270,18 @@ int main(void) CHECK(jstr(d, "result.content") && strstr(jstr(d, "result.content"), "dry_run") != NULL); yyjson_doc_free(d); +} + +static void test_accounts(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- M2: kontoplan, vouchers, reports, SIE ---------- */ d = call(reqf("{\"v\":1,\"id\":\"40\",\"cmd\":\"account.list\"," "\"session\":\"%s\",\"org\":%d," "\"args\":{\"active_only\":true}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) > 1000); yyjson_doc_free(d); @@ -1282,7 +1289,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"41\",\"cmd\":\"account.get\"," "\"session\":\"%s\",\"org\":%d," "\"args\":{\"number\":\"1930\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.type", "asset"); yyjson_doc_free(d); @@ -1291,7 +1298,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"number\":\"0099\",\"name\":\"Testkonto\"," "\"type\":\"expense\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t acc2999 = jint(d, "result.id"); CHECK(acc2999 > 0); @@ -1301,43 +1308,48 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"number\":\"0099\",\"name\":\"Dup\"," "\"type\":\"expense\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"44\",\"cmd\":\"account.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"id\":%lld,\"name\":\"Testkonto 2\"}}", - g_session, (int)org_id, (long long)acc2999)); + g_session, (int)t->org_id, (long long)acc2999)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"45\",\"cmd\":\"account.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)acc2999)); + g_session, (int)t->org_id, (long long)acc2999)); CHECK_STR(d, "result.name", "Testkonto 2"); yyjson_doc_free(d); +} + +static void test_fiscal_years(struct tctx *t) +{ + yyjson_doc *d; d = call(reqf("{\"v\":1,\"id\":\"46\",\"cmd\":\"fiscal_year.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); - int64_t fy1 = jint(d, "result.items.0.id"); - CHECK(fy1 > 0); + t->fy1 = jint(d, "result.items.0.id"); + CHECK(t->fy1 > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"46a\",\"cmd\":\"fiscal_year.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"dividend_ore\":123456}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(jint(d, "result.dividend_ore") == 123456); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"46b\",\"cmd\":\"fiscal_year.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(jint(d, "result.dividend_ore") == 123456); yyjson_doc_free(d); @@ -1345,7 +1357,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"46c\",\"cmd\":\"fiscal_year.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"dividend_ore\":-1}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -1355,7 +1367,7 @@ int main(void) "\"agm_date\":\"2026-06-04\"," "\"dividend_date\":\"2026-06-05\"," "\"employees\":\"1\",\"notes\":\"Revisor saknas.\"}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK_STR(d, "result.events", "Inga väsentliga händelser."); CHECK_STR(d, "result.agm_date", "2026-06-04"); @@ -1363,7 +1375,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"46e\",\"cmd\":\"fiscal_year.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(jint(d, "result.dividend_ore") == 123456); CHECK_STR(d, "result.events", "Inga väsentliga händelser."); @@ -1376,7 +1388,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"46f\",\"cmd\":\"fiscal_year.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"agm_date\":\"2026-6-4\"}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -1423,6 +1435,11 @@ int main(void) CHECK(jint(d, "result.dividend_ore") == 0); yyjson_doc_free(d); } +} + +static void test_vouchers(struct tctx *t) +{ + yyjson_doc *d; /* sale: 1250.00 kr incl 25% moms */ d = call(reqf("{\"v\":1,\"id\":\"47\",\"cmd\":\"voucher.post\"," @@ -1431,11 +1448,11 @@ int main(void) "{\"account\":\"1930\",\"debit_ore\":125000}," "{\"account\":\"3001\",\"credit_ore\":100000}," "{\"account\":\"2611\",\"credit_ore\":25000}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.number") == 1); - int64_t sale_id = jint(d, "result.id"); - CHECK(sale_id > 0); + t->sale_id = jint(d, "result.id"); + CHECK(t->sale_id > 0); CHECK(jstr(d, "result.hash") && strlen(jstr(d, "result.hash")) == 64); CHECK(yyjson_arr_size(jget(d, "result.rows")) == 3); yyjson_doc_free(d); @@ -1447,17 +1464,22 @@ int main(void) "\"Torrkörning\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"49\",\"cmd\":\"voucher.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); +} + +static void test_voucher_errors(struct tctx *t) +{ + yyjson_doc *d; /* invalid vouchers */ d = call(reqf("{\"v\":1,\"id\":\"50\",\"cmd\":\"voucher.post\"," @@ -1465,7 +1487,7 @@ int main(void) "\"2026-01-17\",\"description\":\"Obalans\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":90}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "UNBALANCED"); CHECK(jint(d, "error.details.difference_ore") == 10); yyjson_doc_free(d); @@ -1475,14 +1497,19 @@ int main(void) "\"2026-01-17\",\"description\":\"Fel konto\",\"rows\":[" "{\"account\":\"9999\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND"); yyjson_doc_free(d); +} + +static void test_reports(struct tctx *t) +{ + yyjson_doc *d; /* reports based on the single sale */ d = call(reqf("{\"v\":1,\"id\":\"52\",\"cmd\":\"report.income_statement\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.result_ore") == 100000); yyjson_doc_free(d); @@ -1490,17 +1517,22 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"53\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(vat_box(d, "05") == 100000); CHECK(vat_box(d, "10") == 25000); yyjson_doc_free(d); +} + +static void test_general_ledger(struct tctx *t) +{ + yyjson_doc *d; /* huvudbok: one account block per account with activity or IB */ d = call(reqf("{\"v\":1,\"id\":\"54a\",\"cmd\":\"report.general_ledger\"" ",\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.accounts")) == 3); CHECK(find_amount(d, "result.accounts", "account", "1930", "debit_ore") == @@ -1518,16 +1550,21 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"54b\",\"cmd\":\"report.general_ledger\"" ",\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld,\"accounts\":[\"1930\"]}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.accounts")) == 1); yyjson_doc_free(d); +} + +static void test_voucher_list(struct tctx *t) +{ + yyjson_doc *d; /* verifikationslista: vouchers with their rows and totals */ d = call(reqf("{\"v\":1,\"id\":\"54c\",\"cmd\":\"report.voucher_list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 1); CHECK_STR(d, "result.vouchers.0.series", "A"); @@ -1540,17 +1577,22 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"54d\",\"cmd\":\"report.voucher_list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld,\"series\":\"ZZ\"}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 0); yyjson_doc_free(d); +} + +static void test_vat_eskd(struct tctx *t) +{ + yyjson_doc *d; /* eSKD: ISO-8859-1 XML for the momsdeklaration */ d = call(reqf("{\"v\":1,\"id\":\"54e\",\"cmd\":\"report.vat_eskd\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"," "\"upplysning\":\"Blå\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.org_nr", "559331-2126"); CHECK_STR(d, "result.period", "202601"); @@ -1573,16 +1615,26 @@ int main(void) NULL); free(xml); yyjson_doc_free(d); +} + +static void test_trial_balance(struct tctx *t) +{ + yyjson_doc *d; d = call(reqf("{\"v\":1,\"id\":\"54\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(find_amount(d, "result.accounts", "account", "1930", "ub_ore") == 125000); CHECK(jint(d, "result.totals.debit_ore") == jint(d, "result.totals.credit_ore")); yyjson_doc_free(d); +} + +static void test_idempotency(struct tctx *t) +{ + yyjson_doc *d; /* idempotency */ d = call(reqf("{\"v\":1,\"id\":\"55\",\"cmd\":\"voucher.post\"," @@ -1591,7 +1643,7 @@ int main(void) "\"client_ref\":\"ref-1\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":50000}," "{\"account\":\"3001\",\"credit_ore\":50000}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t v2 = jint(d, "result.id"); CHECK(!jbool(d, "result.replayed")); @@ -1603,18 +1655,23 @@ int main(void) "\"client_ref\":\"ref-1\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":50000}," "{\"account\":\"3001\",\"credit_ore\":50000}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jbool(d, "result.replayed")); CHECK(jint(d, "result.id") == v2); yyjson_doc_free(d); +} + +static void test_attachments(struct tctx *t) +{ + yyjson_doc *d; /* attachments */ d = call(reqf("{\"v\":1,\"id\":\"57\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"kvitto.txt\",\"mime\":\"text/plain\"," "\"content_base64\":\"aGVsbG8gd29ybGQ=\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t att_id = jint(d, "result.id"); CHECK(att_id > 0); @@ -1623,14 +1680,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"58\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"unlinked\":true}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"59\",\"cmd\":\"attachment.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)att_id)); + g_session, (int)t->org_id, (long long)att_id)); CHECK_OK(d); CHECK_STR(d, "result.content_base64", "aGVsbG8gd29ybGQ="); yyjson_doc_free(d); @@ -1641,7 +1698,7 @@ int main(void) "\"attachment_ids\":[%lld],\"rows\":[" "{\"account\":\"5010\",\"debit_ore\":10000}," "{\"account\":\"1930\",\"credit_ore\":10000}]}}", - g_session, (int)org_id, (long long)att_id)); + g_session, (int)t->org_id, (long long)att_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.attachment_ids")) == 1); int64_t rent_id = jint(d, "result.id"); @@ -1651,7 +1708,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"61\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"unlinked\":true}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK(yyjson_arr_size(jget(d, "result.items")) == 0); yyjson_doc_free(d); @@ -1660,7 +1717,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"kvitto.txt\",\"mime\":\"text/plain\"," "\"content_base64\":\"aGVsbG8gd29ybGQ=\",\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)sale_id)); + g_session, (int)t->org_id, (long long)t->sale_id)); CHECK_OK(d); CHECK(jint(d, "result.id") == att_id); yyjson_doc_free(d); @@ -1669,7 +1726,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"kvitto.txt\",\"mime\":\"text/plain\"," "\"content_base64\":\"aGVsbG8gd29ybGQ=\",\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)sale_id)); + g_session, (int)t->org_id, (long long)t->sale_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); @@ -1677,7 +1734,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"61c\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"extra.txt\",\"content_base64\":\"ZXh0cmE=\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t orphan = jint(d, "result.id"); CHECK(orphan > 0); @@ -1686,47 +1743,47 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"61d\",\"cmd\":\"attachment.link\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)orphan, - (long long)sale_id)); + g_session, (int)t->org_id, (long long)orphan, + (long long)t->sale_id)); CHECK_OK(d); - CHECK(jint(d, "result.voucher_id") == sale_id); + CHECK(jint(d, "result.voucher_id") == t->sale_id); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61e\",\"cmd\":\"attachment.link\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)orphan, - (long long)sale_id)); + g_session, (int)t->org_id, (long long)orphan, + (long long)t->sale_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61f\",\"cmd\":\"attachment.unlink\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)orphan, - (long long)sale_id)); + g_session, (int)t->org_id, (long long)orphan, + (long long)t->sale_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61g\",\"cmd\":\"attachment.unlink\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)orphan, - (long long)sale_id)); + g_session, (int)t->org_id, (long long)orphan, + (long long)t->sale_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61h\",\"cmd\":\"attachment.link\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":999999}}", - g_session, (int)org_id, (long long)orphan)); + g_session, (int)t->org_id, (long long)orphan)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); /* listing by voucher returns every linked attachment, with that voucher */ d = call(reqf("{\"v\":1,\"id\":\"61j\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)rent_id)); + g_session, (int)t->org_id, (long long)rent_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK(jint(d, "result.items.0.id") == att_id); @@ -1735,17 +1792,17 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"61k\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)sale_id)); + g_session, (int)t->org_id, (long long)t->sale_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK(jint(d, "result.items.0.id") == att_id); - CHECK(jint(d, "result.items.0.voucher_id") == sale_id); + CHECK(jint(d, "result.items.0.voucher_id") == t->sale_id); yyjson_doc_free(d); /* voucher.list reports the attachment count */ d = call(reqf("{\"v\":1,\"id\":\"61i\",\"cmd\":\"voucher.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"limit\":5}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.items.0.attachment_count") >= 1); CHECK(jint(d, "result.items.1.attachment_count") == 0); @@ -1757,16 +1814,16 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":{\"voucher\":%lld," "\"description\":\"Rättelse felaktig försäljning\"," "\"client_ref\":\"ref-3\"}}", - g_session, (int)org_id, (long long)sale_id)); + g_session, (int)t->org_id, (long long)t->sale_id)); CHECK_OK(d); - CHECK(jint(d, "result.corrects_voucher_id") == sale_id); + CHECK(jint(d, "result.corrects_voucher_id") == t->sale_id); int64_t corr_id = jint(d, "result.id"); - CHECK(corr_id > sale_id); + CHECK(corr_id > t->sale_id); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"63\",\"cmd\":\"voucher.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)sale_id)); + g_session, (int)t->org_id, (long long)t->sale_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.rows")) == 3); yyjson_doc_free(d); @@ -1774,7 +1831,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"64\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(vat_box(d, "05") == 50000); CHECK(vat_box(d, "10") == 0); @@ -1782,14 +1839,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"65\",\"cmd\":\"report.income_statement\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.result_ore") == 40000); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"66\",\"cmd\":\"report.balance_sheet\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(find_amount(d, "result.assets.accounts", "account", "1930", "amount_ore") == 40000); @@ -1807,11 +1864,16 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"filename\":\"big.bin\",\"mime\":\"application/octet-stream\"," "\"content_base64\":\"%s\"}}", - g_session, (int)org_id, big)); + g_session, (int)t->org_id, big)); CHECK_STR(d, "error.code", "TOO_LARGE"); yyjson_doc_free(d); g_cfg.max_attachment_bytes = 10 * 1024 * 1024; } +} + +static void test_templates(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- templates (konteringsmallar) ------------------- */ @@ -1821,7 +1883,7 @@ int main(void) "{\"account\":\"1930\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x/1.25\"}," "{\"account\":\"2611\",\"formula\":\"-x/5\"}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t tpl_id = jint(d, "result.id"); CHECK(tpl_id > 0); @@ -1830,7 +1892,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"81\",\"cmd\":\"template.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"active_only\":true}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) >= 1); yyjson_doc_free(d); @@ -1838,7 +1900,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"82\",\"cmd\":\"template.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Testmall\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.rows")) == 3); CHECK_STR(d, "result.rows.0.formula", "x"); @@ -1849,7 +1911,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"template\":\"Testmall\",\"x\":\"1250\"," "\"date\":\"2026-03-05\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.rows.0.debit_ore") == 125000); CHECK(jint(d, "result.rows.1.credit_ore") == 100000); @@ -1861,7 +1923,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"template\":\"Testmall\",\"x\":\"1250\"," "\"date\":\"2026-03-05\",\"client_ref\":\"tplref\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t tpl_voucher = jint(d, "result.id"); CHECK(tpl_voucher > 0); @@ -1871,7 +1933,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"template\":\"Testmall\",\"x\":\"1250\"," "\"date\":\"2026-03-05\",\"client_ref\":\"tplref\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jbool(d, "result.replayed")); yyjson_doc_free(d); @@ -1885,7 +1947,7 @@ int main(void) "{\"account\":\"2611\",\"formula\":\"-x/4\"}," "{\"account\":\"2641\",\"formula\":\"-x/4\"}," "{\"account\":\"5011\",\"formula\":\"-x/4\"}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); @@ -1893,7 +1955,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"template\":\"Rundning\",\"x\":\"0.03\"," "\"date\":\"2026-03-06\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.rows.0.debit_ore") == 4); CHECK(jint(d, "result.rows.1.credit_ore") == 1); @@ -1905,7 +1967,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"Fel1\"," "\"rows\":[{\"account\":\"1930\",\"formula\":\"x**\"}," "{\"account\":\"3001\",\"formula\":\"-x\"}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -1913,14 +1975,14 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"Fel2\"," "\"rows\":[{\"account\":\"9999\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x\"}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"90\",\"cmd\":\"template.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"name\":\"Testmall 2\"}}", - g_session, (int)org_id, (long long)tpl_id)); + g_session, (int)t->org_id, (long long)tpl_id)); CHECK_OK(d); CHECK_STR(d, "result.name", "Testmall 2"); yyjson_doc_free(d); @@ -1928,7 +1990,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"91\",\"cmd\":\"template.archive\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Testmall 2\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); @@ -1936,9 +1998,14 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"template\":\"Testmall 2\",\"x\":\"100\"," "\"date\":\"2026-03-05\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); +} + +static void test_ib(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- ingående balans (series IB) -------------------- */ @@ -1947,26 +2014,31 @@ int main(void) "\"description\":\"Ingående balans\",\"series\":\"IB\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":50000}," "{\"account\":\"2010\",\"credit_ore\":50000}]}}", - g_session, (int)org_id, "2026-01-01")); + g_session, (int)t->org_id, "2026-01-01")); CHECK_OK(d); CHECK_STR(d, "result.series", "IB"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"94\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(find_amount(d, "result.accounts", "account", "1930", "ib_ore") == 50000); CHECK(find_amount(d, "result.accounts", "account", "2010", "ib_ore") == -50000); yyjson_doc_free(d); +} + +static void test_period_lock(struct tctx *t) +{ + yyjson_doc *d; /* period lock */ d = call(reqf("{\"v\":1,\"id\":\"67\",\"cmd\":\"period.lock\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" "%lld,\"until\":\"2026-01-31\",\"reason\":\"bokslut\"}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); yyjson_doc_free(d); @@ -1975,28 +2047,33 @@ int main(void) "\"args\":{\"date\":\"2026-01-15\",\"description\":\"Låst\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "PERIOD_LOCKED"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"69\",\"cmd\":\"period.unlock\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" "%lld,\"reason\":\"test\"}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); yyjson_doc_free(d); +} + +static void test_sie_export(struct tctx *t) +{ + yyjson_doc *d; /* SIE export */ d = call(reqf("{\"v\":1,\"id\":\"70\",\"cmd\":\"sie.export\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" "%lld,\"inline\":true}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(jint(d, "result.size") > 0); CHECK(jstr(d, "result.sha256") && strlen(jstr(d, "result.sha256")) == 64); char sie_path[512] = ""; snprintf(sie_path, sizeof sie_path, "%s", jstr(d, "result.path")); - char *sie_b64 = xstrdup(jstr(d, "result.content_base64")); + t->sie_b64 = xstrdup(jstr(d, "result.content_base64")); yyjson_doc_free(d); { @@ -2012,6 +2089,11 @@ int main(void) CHECK(strstr(sbuf, "#TRANS 1930") != NULL); } } +} + +static void test_sie_import(struct tctx *t) +{ + yyjson_doc *d; /* SIE import into a second org */ d = call(reqf("{\"v\":1,\"id\":\"71\",\"cmd\":\"org.create\"," @@ -2022,17 +2104,17 @@ int main(void) CHECK(jint(d, "result.fiscal_year_id") > 0); yyjson_doc_free(d); - d = call_sie_import(g_session, org2, sie_b64, 1); + d = call_sie_import(g_session, org2, t->sie_b64, 1); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK(jint(d, "result.vouchers") >= 4); yyjson_doc_free(d); - d = call_sie_import(g_session, org2, sie_b64, 0); + d = call_sie_import(g_session, org2, t->sie_b64, 0); CHECK_OK(d); CHECK(jint(d, "result.vouchers") >= 4); yyjson_doc_free(d); - free(sie_b64); + free(t->sie_b64); d = call(reqf("{\"v\":1,\"id\":\"72\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", @@ -2047,7 +2129,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"72b\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t ub_org1 = find_amount(d, "result.accounts", "account", "1930", "ub_ore"); @@ -2128,12 +2210,17 @@ int main(void) "ub_ore") == 105000); yyjson_doc_free(d); } +} + +static void test_settings(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- settings: default series ----------------------- */ d = call(reqf("{\"v\":1,\"id\":\"95\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.default_series", "A"); CHECK_STR(d, "result.bank_account", "1930"); @@ -2142,14 +2229,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"96\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"default_series\",\"value\":\"V-\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "V-"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"96b\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.default_series", "V-"); yyjson_doc_free(d); @@ -2159,7 +2246,7 @@ int main(void) "\"description\":\"Serietest\",\"client_ref\":\"sertest\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id, "2026-04-01")); + g_session, (int)t->org_id, "2026-04-01")); CHECK_OK(d); CHECK_STR(d, "result.series", "V-"); CHECK(jint(d, "result.number") == 1); @@ -2170,7 +2257,7 @@ int main(void) "{\"name\":\"Seriemall\",\"rows\":[" "{\"account\":\"1930\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x\"}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.series", "V-"); yyjson_doc_free(d); @@ -2178,37 +2265,42 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"99\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"default_series\",\"value\":\"123456789\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"100\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"nope\",\"value\":\"x\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "UNSUPPORTED"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"99b\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"attachment_dir\",\"value\":\"/tmp/bilagor\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "/tmp/bilagor"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"101\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.attachment_dir", "/tmp/bilagor"); yyjson_doc_free(d); +} + +static void test_smtp_settings(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- settings: SMTP and encrypted password ----------- */ d = call(reqf("{\"v\":1,\"id\":\"102\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.smtp_security", "starttls"); CHECK(jget(d, "result.smtp_password") == NULL); @@ -2218,7 +2310,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"103\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_host\",\"value\":\"smtp.example.se\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "smtp.example.se"); yyjson_doc_free(d); @@ -2226,14 +2318,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"104\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_host\",\"value\":\"bad\\u0001host\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"105\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_port\",\"value\":\"587\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "587"); yyjson_doc_free(d); @@ -2241,56 +2333,56 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"106\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_port\",\"value\":\"0\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"107\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_port\",\"value\":\"70000\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"108\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_security\",\"value\":\"ssl\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"109\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_user\",\"value\":\"faktura@example.se\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_from\",\"value\":\"Bokf AB \"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"111\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_reply_to\",\"value\":\"svar@example.se\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"112\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_security\",\"value\":\"tls\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "tls"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"113\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.smtp_host", "smtp.example.se"); CHECK_STR(d, "result.smtp_port", "587"); @@ -2304,7 +2396,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"114\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_password\",\"value\":\"hunter2\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INTERNAL"); CHECK(jstr(d, "error.message") && strstr(jstr(d, "error.message"), "BOKFD_SECRET_KEY") != NULL); @@ -2312,7 +2404,7 @@ int main(void) char *secret_stored = db_text(g_db, reqf("SELECT value FROM settings WHERE" " org_id=%d AND key='smtp_password'", - (int)org_id)); + (int)t->org_id)); CHECK(secret_stored == NULL); free(secret_stored); @@ -2320,7 +2412,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"115\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_password\",\"value\":\"hunter2\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INTERNAL"); yyjson_doc_free(d); unsetenv("BOKFD_SECRET_KEY"); @@ -2332,14 +2424,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_password\",\"value\":\"hunter2\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "[redacted]"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"117\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jbool(d, "result.smtp_password_set")); CHECK(jget(d, "result.smtp_password") == NULL); @@ -2347,7 +2439,7 @@ int main(void) secret_stored = db_text(g_db, reqf("SELECT value FROM settings WHERE" " org_id=%d AND key='smtp_password'", - (int)org_id)); + (int)t->org_id)); CHECK(secret_stored != NULL); CHECK(secret_stored && strncmp(secret_stored, "enc:v1:", 7) == 0); CHECK(secret_stored && strstr(secret_stored, "hunter2") == NULL); @@ -2370,14 +2462,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"118\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_password\",\"value\":\"\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "[redacted]"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"119\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(!jbool(d, "result.smtp_password_set")); CHECK(jget(d, "result.smtp_password") == NULL); @@ -2385,7 +2477,7 @@ int main(void) secret_stored = db_text(g_db, reqf("SELECT value FROM settings WHERE" " org_id=%d AND key='smtp_password'", - (int)org_id)); + (int)t->org_id)); CHECK(secret_stored == NULL); free(secret_stored); @@ -2401,10 +2493,15 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"120\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"smtp_password\",\"value\":\"\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INTERNAL"); yyjson_doc_free(d); unsetenv("BOKFD_SECRET_KEY"); +} + +static void test_bank(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- bank reconciliation (schema v8) ----------------- */ @@ -2424,7 +2521,7 @@ int main(void) "Bokförd;Valutadatum;Text;Typ;Insättningar;Uttag;Bokfört saldo\n" "2026-06-01;2026-06-01;Fel;Insättning;12,abc;;10,00\n"; - d = call_bank_import(g_session, org_id, seb_csv, 1); + d = call_bank_import(g_session, t->org_id, seb_csv, 1); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK_STR(d, "result.format", "seb"); @@ -2438,21 +2535,21 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"200b\",\"cmd\":\"bank.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 0); CHECK(jint(d, "result.summary.unmatched") == 0); CHECK(jint(d, "result.summary.matched") == 0); yyjson_doc_free(d); - d = call_bank_import(g_session, org_id, seb_csv, 0); + d = call_bank_import(g_session, t->org_id, seb_csv, 0); CHECK_OK(d); CHECK(jint(d, "result.total") == 3); CHECK(jint(d, "result.imported") == 3); CHECK(jint(d, "result.duplicates") == 0); yyjson_doc_free(d); - d = call_bank_import(g_session, org_id, seb_csv, 0); + d = call_bank_import(g_session, t->org_id, seb_csv, 0); CHECK_OK(d); CHECK(jint(d, "result.imported") == 0); CHECK(jint(d, "result.duplicates") == 3); @@ -2460,7 +2557,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"200d\",\"cmd\":\"bank.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 3); CHECK(jint(d, "result.items.0.amount_ore") == 125000); @@ -2480,7 +2577,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"200e\",\"cmd\":\"bank.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-05-11\",\"to\":\"2026-05-19\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK_STR(d, "result.items.0.booked_at", "2026-05-11"); @@ -2493,7 +2590,7 @@ int main(void) "\"client_ref\":\"bankref1\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":125000}," "{\"account\":\"3001\",\"credit_ore\":125000}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t bank_voucher = jint(d, "result.id"); CHECK(bank_voucher > 0); @@ -2501,7 +2598,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"202\",\"cmd\":\"bank.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items.0.suggestions")) == 1); CHECK(jint(d, "result.items.0.suggestions.0.voucher_id") == bank_voucher); @@ -2512,7 +2609,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"203\",\"cmd\":\"bank.match\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"transaction_id\":%lld,\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)tx1, + g_session, (int)t->org_id, (long long)tx1, (long long)bank_voucher)); CHECK_OK(d); CHECK(jint(d, "result.transaction_id") == tx1); @@ -2523,7 +2620,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"204\",\"cmd\":\"bank.match\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"transaction_id\":%lld,\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)tx1, + g_session, (int)t->org_id, (long long)tx1, (long long)bank_voucher)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); @@ -2531,7 +2628,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"205\",\"cmd\":\"bank.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"status\":\"matched\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK(jint(d, "result.items.0.id") == tx1); @@ -2549,7 +2646,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"206\",\"cmd\":\"bank.unmatch\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"transaction_id\":%lld,\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)tx1, + g_session, (int)t->org_id, (long long)tx1, (long long)bank_voucher)); CHECK_OK(d); CHECK(jbool(d, "result.unmatched")); @@ -2558,7 +2655,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"207\",\"cmd\":\"bank.unmatch\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"transaction_id\":%lld,\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)tx1, + g_session, (int)t->org_id, (long long)tx1, (long long)bank_voucher)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); @@ -2566,19 +2663,19 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"208\",\"cmd\":\"bank.match\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"transaction_id\":%lld,\"voucher_id\":%lld}}", - g_session, (int)org_id, (long long)tx1, + g_session, (int)t->org_id, (long long)tx1, (long long)bank_voucher)); CHECK_OK(d); CHECK(jint(d, "result.difference_ore") == 0); yyjson_doc_free(d); - d = call_bank_import(g_session, org_id, seb_bad_header, 1); + d = call_bank_import(g_session, t->org_id, seb_bad_header, 1); CHECK_STR(d, "error.code", "INVALID_ARGS"); CHECK(jstr(d, "error.message") && strstr(jstr(d, "error.message"), "header mismatch") != NULL); yyjson_doc_free(d); - d = call_bank_import(g_session, org_id, seb_bad_amount, 1); + d = call_bank_import(g_session, t->org_id, seb_bad_amount, 1); CHECK_STR(d, "error.code", "INVALID_ARGS"); CHECK(jstr(d, "error.message") && strstr(jstr(d, "error.message"), "line 2") != NULL); @@ -2591,7 +2688,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"format\":\"seb\",\"content_base64\":\"%s\"," "\"account\":\"9999\"}}", - g_session, (int)org_id, bad_b64)); + g_session, (int)t->org_id, bad_b64)); CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND"); yyjson_doc_free(d); free(bad_b64); @@ -2600,26 +2697,26 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"210\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"bank_account\",\"value\":\"19a0\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"211\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"bank_account\",\"value\":\"1940\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "1940"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"212\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.bank_account", "1940"); yyjson_doc_free(d); - d = call_bank_import(g_session, org_id, seb_one, 0); + d = call_bank_import(g_session, t->org_id, seb_one, 0); CHECK_OK(d); CHECK_STR(d, "result.account", "1940"); CHECK(jint(d, "result.imported") == 1); @@ -2628,17 +2725,22 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"213\",\"cmd\":\"bank.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"account\":\"1940\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK_STR(d, "result.items.0.account", "1940"); yyjson_doc_free(d); +} + +static void test_invoices(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- invoicing: customers, sequence, issue ---------- */ d = call(reqf("{\"v\":1,\"id\":\"300\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.invoice_receivable_account", "1510"); CHECK_STR(d, "result.invoice_revenue_account", "3001"); @@ -2651,7 +2753,7 @@ int main(void) "\"vat_nr\":\"SE559232855201\"," "\"email\":\"faktura@andra.se\",\"your_ref\":\"Eric\"," "\"payment_days\":30}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t cust1 = jint(d, "result.id"); CHECK(cust1 > 0); @@ -2665,14 +2767,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"302\",\"cmd\":\"customer.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":" "\"Andra bygg AB\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"303\",\"cmd\":\"customer.create\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"name\":\"Tillfällig AB\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK_STR(d, "result.name", "Tillfällig AB"); @@ -2682,7 +2784,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"304\",\"cmd\":\"customer.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":" "\"NZ Bygg AB\",\"city\":\"Bromma\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); int64_t cust2 = jint(d, "result.id"); CHECK(cust2 > 0); @@ -2691,7 +2793,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"305\",\"cmd\":\"customer.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_OK(d); CHECK_STR(d, "result.name", "Andra bygg AB"); CHECK_STR(d, "result.address", "Storgatan 1"); @@ -2699,13 +2801,13 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"306\",\"cmd\":\"customer.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"307\",\"cmd\":\"customer.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 2); CHECK_STR(d, "result.items.0.name", "Andra bygg AB"); @@ -2715,7 +2817,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"308\",\"cmd\":\"customer.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"city\":\"Sundbyberg\",\"your_ref\":\"Lars\"}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_OK(d); CHECK_STR(d, "result.city", "Sundbyberg"); CHECK_STR(d, "result.your_ref", "Lars"); @@ -2726,21 +2828,21 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"309\",\"cmd\":\"customer.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"name\":\"Andra bygg AB\"}}", - g_session, (int)org_id, (long long)cust2)); + g_session, (int)t->org_id, (long long)cust2)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"310\",\"cmd\":\"customer.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999," "\"city\":\"X\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"311\",\"cmd\":\"customer.update\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"id\":%lld,\"city\":\"Nacka\"}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK_STR(d, "result.city", "Nacka"); @@ -2748,7 +2850,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"312\",\"cmd\":\"customer.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_OK(d); CHECK_STR(d, "result.city", "Sundbyberg"); yyjson_doc_free(d); @@ -2756,7 +2858,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"313\",\"cmd\":\"customer.archive\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"active\":false}}", - g_session, (int)org_id, (long long)cust2)); + g_session, (int)t->org_id, (long long)cust2)); CHECK_OK(d); CHECK(jint(d, "result.id") == cust2); CHECK(!jbool(d, "result.active")); @@ -2765,7 +2867,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"314\",\"cmd\":\"customer.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"active_only\":true}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK_STR(d, "result.items.0.name", "Andra bygg AB"); @@ -2774,13 +2876,13 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"315\",\"cmd\":\"customer.archive\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999," "\"active\":false}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"320\",\"cmd\":\"invoice.sequence_get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.next_number") == 1); yyjson_doc_free(d); @@ -2788,21 +2890,21 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"321\",\"cmd\":\"invoice.sequence_set\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"next_number\":" "0}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"322\",\"cmd\":\"invoice.sequence_set\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"next_number\":" "17761}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.next_number") == 17761); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"323\",\"cmd\":\"invoice.sequence_get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.next_number") == 17761); yyjson_doc_free(d); @@ -2820,7 +2922,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"330\",\"cmd\":\"invoice.preview\"," "\"session\":\"%s\",\"org\":%d,\"args\":%s}", - g_session, (int)org_id, draft_args)); + g_session, (int)t->org_id, draft_args)); CHECK_OK(d); CHECK(jint(d, "result.number") == 17761); char number_str[32]; @@ -2840,7 +2942,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"331\",\"cmd\":\"invoice.preview\"," "\"session\":\"%s\",\"org\":%d,\"args\":%s}", - g_session, (int)org_id, draft_args)); + g_session, (int)t->org_id, draft_args)); CHECK_OK(d); CHECK(preview_b64 && jstr(d, "result.content_base64") && strcmp(jstr(d, "result.content_base64"), preview_b64) == 0); @@ -2848,7 +2950,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"332\",\"cmd\":\"invoice.sequence_get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.next_number") == 17761); yyjson_doc_free(d); @@ -2859,7 +2961,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"1\",\"unit_price_ore\":100," "\"vat_code\":\"99\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -2869,7 +2971,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"1,2345\",\"unit_price_ore\":100," "\"vat_code\":\"25\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -2879,7 +2981,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"0\",\"unit_price_ore\":100," "\"vat_code\":\"25\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -2889,7 +2991,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"1\",\"unit_price_ore\":100," "\"vat_code\":\"25\"}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); @@ -2899,7 +3001,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"1\",\"unit_price_ore\":100," "\"vat_code\":\"25\"}]}}", - g_session, (int)org_id, (long long)cust2)); + g_session, (int)t->org_id, (long long)cust2)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); @@ -2909,7 +3011,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"1000\",\"unit_price_ore\":" "9223372036854775807,\"vat_code\":\"25\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -2919,7 +3021,7 @@ int main(void) "\"2026-10-20\",\"rows\":[{\"description\":\"X\"," "\"quantity\":\"1\",\"unit_price_ore\":-1," "\"vat_code\":\"25\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -2938,7 +3040,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":" "\"2026-10-20\",\"rows\":%s}}", - g_session, (int)org_id, (long long)cust1, rows_json)); + g_session, (int)t->org_id, (long long)cust1, rows_json)); CHECK_STR(d, "error.code", "TOO_LARGE"); yyjson_doc_free(d); } @@ -2949,7 +3051,7 @@ int main(void) "\"due_date\":\"2026-10-20\",\"rows\":[{\"description\":" "\"X\",\"quantity\":\"1\",\"unit_price_ore\":100," "\"vat_code\":\"25\",\"account\":\"9999\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND"); yyjson_doc_free(d); @@ -2959,7 +3061,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"340\",\"cmd\":\"invoice.issue\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":%s}", - g_session, (int)org_id, draft_args)); + g_session, (int)t->org_id, draft_args)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK(jint(d, "result.number") == 17761); @@ -2976,19 +3078,19 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"341\",\"cmd\":\"invoice.sequence_get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.next_number") == 17761); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"350\",\"cmd\":\"invoice.issue\"," "\"session\":\"%s\",\"org\":%d,\"args\":%s}", - g_session, (int)org_id, draft_args)); + g_session, (int)t->org_id, draft_args)); CHECK_OK(d); - int64_t inv1 = jint(d, "result.id"); + t->inv1 = jint(d, "result.id"); int64_t doc1 = jint(d, "result.document_id"); int64_t vou1 = jint(d, "result.voucher_id"); - CHECK(inv1 > 0); + CHECK(t->inv1 > 0); CHECK(doc1 > 0); CHECK(vou1 > 0); CHECK(jint(d, "result.number") == 17761); @@ -3003,7 +3105,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"351\",\"cmd\":\"voucher.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)vou1)); + g_session, (int)t->org_id, (long long)vou1)); CHECK_OK(d); CHECK_STR(d, "result.source", "invoice"); CHECK_STR(d, "result.date", "2026-09-20"); @@ -3018,7 +3120,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"352\",\"cmd\":\"invoice.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)inv1)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); CHECK_STR(d, "result.status", "issued"); CHECK(jint(d, "result.customer_id") == cust1); @@ -3036,7 +3138,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"353\",\"cmd\":\"invoice.pdf\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)inv1)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); const char *stored = jstr(d, "result.content_base64"); CHECK(stored && preview_b64 && strcmp(stored, preview_b64) == 0); @@ -3049,12 +3151,12 @@ int main(void) "\"quantity\":\"2,5\",\"unit\":\"tim\"," "\"unit_price_ore\":100000,\"vat_code\":\"12\"," "\"account\":\"3002\"}]}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_OK(d); int64_t inv2 = jint(d, "result.id"); int64_t doc2 = jint(d, "result.document_id"); int64_t vou2 = jint(d, "result.voucher_id"); - CHECK(inv2 > inv1); + CHECK(inv2 > t->inv1); CHECK(doc2 > doc1); CHECK(vou2 > vou1); CHECK(jint(d, "result.number") == 17762); @@ -3068,7 +3170,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"356\",\"cmd\":\"voucher.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)vou2)); + g_session, (int)t->org_id, (long long)vou2)); CHECK_OK(d); CHECK_STR(d, "result.source", "invoice"); CHECK(find_amount(d, "result.rows", "account", "1510", "debit_ore") == @@ -3082,7 +3184,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"357\",\"cmd\":\"invoice.list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 2); CHECK(jint(d, "result.items.0.number") == 17762); @@ -3094,7 +3196,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"358\",\"cmd\":\"invoice.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" "%lld}}", - g_session, (int)org_id, (long long)cust1)); + g_session, (int)t->org_id, (long long)cust1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 2); yyjson_doc_free(d); @@ -3102,7 +3204,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"359\",\"cmd\":\"invoice.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":" "%lld}}", - g_session, (int)org_id, (long long)cust2)); + g_session, (int)t->org_id, (long long)cust2)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 0); yyjson_doc_free(d); @@ -3110,14 +3212,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"360\",\"cmd\":\"invoice.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"status\":" "\"credited\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"361\",\"cmd\":\"invoice.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"limit\":1}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK(jint(d, "result.items.0.number") == 17762); @@ -3126,30 +3228,35 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"362\",\"cmd\":\"invoice.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"status\":" "\"nope\"}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"363\",\"cmd\":\"invoice.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"364\",\"cmd\":\"invoice.pdf\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"365\",\"cmd\":\"invoice.sequence_get\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK(jint(d, "result.next_number") == 17763); yyjson_doc_free(d); free(preview_b64); +} + +static void test_invoice_send(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- invoice.send over a fake SMTP server ----------- */ @@ -3157,21 +3264,21 @@ int main(void) "000102030405060708090a0b0c0d0e0f" "101112131415161718191a1b1c1d1e1f"; char smtp_log[600]; - snprintf(smtp_log, sizeof smtp_log, "%s/smtp.log", tmpdir); + snprintf(smtp_log, sizeof smtp_log, "%s/smtp.log", t->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)); + g_session, (int)t->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)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); @@ -3179,21 +3286,21 @@ int main(void) 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)); + g_session, (int)t->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)); + g_session, (int)t->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)); + g_session, (int)t->org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "[redacted]"); yyjson_doc_free(d); @@ -3202,17 +3309,17 @@ int main(void) 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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); - CHECK(jint(d, "result.id") == inv1); + CHECK(jint(d, "result.id") == t->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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); CHECK(yyjson_is_null(jget(d, "result.last_sent_at"))); yyjson_doc_free(d); @@ -3225,16 +3332,16 @@ int main(void) 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)); + g_session, (int)t->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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); - CHECK(jint(d, "result.id") == inv1); + CHECK(jint(d, "result.id") == t->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")); @@ -3260,7 +3367,7 @@ int main(void) 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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); CHECK_STR(d, "result.last_sent_at", sent_at); CHECK_STR(d, "result.last_sent_to", "faktura@andra.se"); @@ -3278,7 +3385,7 @@ int main(void) 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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_STR(d, "error.code", "SMTP_NOT_CONFIGURED"); yyjson_doc_free(d); setenv("BOKFD_SECRET_KEY", smtp_key, 1); @@ -3291,13 +3398,13 @@ int main(void) 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)); + g_session, (int)t->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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_STR(d, "error.code", "SMTP_FAILED"); CHECK(jstr(d, "error.message") && strstr(jstr(d, "error.message"), "550")); yyjson_doc_free(d); @@ -3306,7 +3413,7 @@ int main(void) 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)); + g_session, (int)t->org_id, (long long)t->inv1)); CHECK_OK(d); CHECK_STR(d, "result.last_sent_at", sent_at); CHECK_STR(d, "result.last_sent_to", "faktura@andra.se"); @@ -3378,14 +3485,19 @@ int main(void) yyjson_doc_free(d); unsetenv("BOKFD_SECRET_KEY"); +} + +static void test_moms_rules(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- moms rules: ranges merge per box ------------- */ d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"org.create\"," "\"session\":\"%s\",\"args\":{\"name\":\"Moms AB\"}}", g_session)); CHECK_OK(d); - int64_t vat_org = jint(d, "result.id"); - CHECK(vat_org > 0); + t->vat_org = jint(d, "result.id"); + CHECK(t->vat_org > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"111\",\"cmd\":\"voucher.post\"," @@ -3394,7 +3506,7 @@ int main(void) "{\"account\":\"1930\",\"debit_ore\":125000}," "{\"account\":\"3001\",\"credit_ore\":100000}," "{\"account\":\"2611\",\"credit_ore\":25000}]}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); yyjson_doc_free(d); @@ -3404,7 +3516,7 @@ int main(void) "{\"account\":\"1930\",\"debit_ore\":62500}," "{\"account\":\"3105\",\"credit_ore\":50000}," "{\"account\":\"2611\",\"credit_ore\":12500}]}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); yyjson_doc_free(d); @@ -3415,7 +3527,7 @@ int main(void) "{\"account\":\"2645\",\"debit_ore\":2500}," "{\"account\":\"2614\",\"credit_ore\":2500}," "{\"account\":\"2410\",\"credit_ore\":10000}]}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); yyjson_doc_free(d); @@ -3425,14 +3537,14 @@ int main(void) "{\"account\":\"6540\",\"debit_ore\":8000}," "{\"account\":\"2641\",\"debit_ore\":2000}," "{\"account\":\"2410\",\"credit_ore\":10000}]}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"115\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-02-01\",\"to\":\"2026-02-28\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(vat_box(d, "05") == 150000); CHECK(vat_box(d, "10") == 37500); @@ -3449,11 +3561,16 @@ int main(void) } CHECK(n05 == 1); yyjson_doc_free(d); +} + +static void test_rules_editor(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- moms rules: per-org editor -------------------- */ d = call(reqf("{\"v\":1,\"id\":\"116a\",\"cmd\":\"report.rule_list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); int nrules = (int)yyjson_arr_size(jget(d, "result.items")); CHECK(nrules >= 15); @@ -3471,7 +3588,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116b\",\"cmd\":\"report.rule_list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"report\":\"vat\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == (size_t)nrules); yyjson_doc_free(d); @@ -3481,7 +3598,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"report\":\"vat\",\"box\":\"05\",\"match_type\":\"type\"," "\"pattern\":\"revenue\",\"sign\":-1,\"sort_order\":1}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK(jint(d, "result.id") == 0); @@ -3492,7 +3609,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116d\",\"cmd\":\"report.rule_list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == (size_t)nrules); yyjson_doc_free(d); @@ -3501,7 +3618,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"report\":\"vat\",\"box\":\"05\",\"match_type\":\"type\"," "\"pattern\":\"revenue\",\"sign\":-1,\"sort_order\":1}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); int64_t rule_id = jint(d, "result.id"); CHECK(rule_id > 0); @@ -3513,7 +3630,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116f\",\"cmd\":\"report.rule_list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == (size_t)nrules + 1); { @@ -3532,7 +3649,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116g\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-02-01\",\"to\":\"2026-02-28\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(vat_box(d, "05") == 300000); CHECK(vat_box(d, "49") == 35500); @@ -3541,7 +3658,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116h\",\"cmd\":\"report.rule_update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"id\":%lld,\"sign\":1,\"sort_order\":2}}", - g_session, (int)vat_org, (long long)rule_id)); + g_session, (int)t->vat_org, (long long)rule_id)); CHECK_OK(d); CHECK_STR(d, "result.match_type", "type"); CHECK_STR(d, "result.pattern", "revenue"); @@ -3552,7 +3669,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116i\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-02-01\",\"to\":\"2026-02-28\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(vat_box(d, "05") == 0); yyjson_doc_free(d); @@ -3560,13 +3677,13 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116j\",\"cmd\":\"report.rule_update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"match_type\":\"range\",\"pattern\":\"3100-3000\"}}", - g_session, (int)vat_org, (long long)rule_id)); + g_session, (int)t->vat_org, (long long)rule_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"116k\",\"cmd\":\"report.rule_update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%d}}", - g_session, (int)vat_org, 999999)); + g_session, (int)t->vat_org, 999999)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); @@ -3574,7 +3691,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116l\",\"cmd\":\"report.rule_delete\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"id\":%lld}}", - g_session, (int)vat_org, (long long)rule_id)); + g_session, (int)t->vat_org, (long long)rule_id)); CHECK_OK(d); CHECK(jbool(d, "result.deleted")); CHECK(jbool(d, "result.dry_run")); @@ -3582,7 +3699,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116m\",\"cmd\":\"report.rule_delete\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)vat_org, (long long)rule_id)); + g_session, (int)t->vat_org, (long long)rule_id)); CHECK_OK(d); CHECK(jint(d, "result.id") == rule_id); CHECK(jbool(d, "result.deleted")); @@ -3590,7 +3707,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116n\",\"cmd\":\"report.rule_list\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == (size_t)nrules); yyjson_doc_free(d); @@ -3598,14 +3715,14 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"116o\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-02-01\",\"to\":\"2026-02-28\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(vat_box(d, "05") == 150000); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"116p\",\"cmd\":\"report.rule_delete\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)vat_org, (long long)rule_id)); + g_session, (int)t->vat_org, (long long)rule_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); @@ -3614,7 +3731,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"report\":\"vat\",\"box\":\"05\"," "\"match_type\":\"bogus\",\"pattern\":\"3001\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -3622,7 +3739,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"report\":\"vat\",\"box\":\"05\"," "\"match_type\":\"range\",\"pattern\":\"3100-3000\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -3630,7 +3747,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"report\":\"vat\",\"box\":\"05\"," "\"match_type\":\"account\",\"pattern\":\"30a1\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); @@ -3638,15 +3755,20 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"report\":\"vat\",\"box\":\"05\"," "\"match_type\":\"type\",\"pattern\":\"revenue\",\"sign\":0}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); +} + +static void test_bokslut(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- bokslut: dispositions, tax, transfer ---------- */ d = call(reqf("{\"v\":1,\"id\":\"120\",\"cmd\":\"bokslut.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"tax_rate\":20.6}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(jint(d, "result.result_before_ore") == 132000); CHECK(jint(d, "result.taxable_ore") == 132000); @@ -3658,7 +3780,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"121\",\"cmd\":\"bokslut.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"tax_rate\":20.6,\"periodiseringsfond_ore\":50000}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(jint(d, "result.entries_ore") == -50000); CHECK(jint(d, "result.taxable_ore") == 82000); @@ -3669,7 +3791,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"122\",\"cmd\":\"report.balance_sheet\"," "\"session\":\"%s\",\"org\":%d}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK(find_amount(d, "result.equity.accounts", "account", "2110", "amount_ore") == 50000); @@ -3683,16 +3805,21 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"entries\":[{\"debit_account\":\"8910\"," "\"amount_ore\":100}]}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); +} + +static void test_sru(struct tctx *t) +{ + yyjson_doc *d; /* ---------------- SRU (INK2/INK2R/INK2S) ----------------------- */ d = call(reqf("{\"v\":1,\"id\":\"124\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"org_nr\":\"559331-2126\",\"postal_code\":\"192 48\"," "\"city\":\"Sollentuna\"}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); yyjson_doc_free(d); @@ -3700,7 +3827,7 @@ int main(void) "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"adjustments\":[{\"code\":\"7653\"," "\"amount_ore\":10000}]}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); CHECK_STR(d, "result.period", "2026P4"); CHECK_STR(d, "result.blankett", "INK2-2026P4"); @@ -3739,7 +3866,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"126\",\"cmd\":\"sru.export\"," "\"session\":\"%s\",\"org\":%d,\"args\":{}}", - g_session, (int)vat_org)); + g_session, (int)t->vat_org)); CHECK_OK(d); sb64 = jstr(d, "result.info.content_base64"); srub = NULL; @@ -3754,12 +3881,17 @@ int main(void) CHECK(strstr(sru, "#FILNAMN BLANKETTER.SRU") != NULL); free(sru); yyjson_doc_free(d); +} + +static void test_year_close(struct tctx *t) +{ + yyjson_doc *d; /* close fiscal year and verify hard stop */ d = call(reqf("{\"v\":1,\"id\":\"73\",\"cmd\":\"fiscal_year.close\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"confirm\":true}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); yyjson_doc_free(d); @@ -3768,7 +3900,7 @@ int main(void) "\"args\":{\"date\":\"2026-03-01\",\"description\":\"Stängt\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_STR(d, "error.code", "FISCAL_YEAR_CLOSED"); yyjson_doc_free(d); @@ -3776,7 +3908,7 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"76\",\"cmd\":\"fiscal_year.reopen\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"id\":%lld,\"confirm\":true}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); yyjson_doc_free(d); @@ -3784,13 +3916,13 @@ int main(void) d = call(reqf("{\"v\":1,\"id\":\"77\",\"cmd\":\"fiscal_year.reopen\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"confirm\":true}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"78\",\"cmd\":\"fiscal_year.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_OK(d); CHECK_STR(d, "result.status", "open"); yyjson_doc_free(d); @@ -3800,16 +3932,22 @@ int main(void) "\"args\":{\"date\":\"2026-03-01\",\"description\":\"Öppet\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", - g_session, (int)org_id)); + g_session, (int)t->org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"79b\",\"cmd\":\"fiscal_year.reopen\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"confirm\":true}}", - g_session, (int)org_id, (long long)fy1)); + g_session, (int)t->org_id, (long long)t->fy1)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); +} + +static void test_audit_verify(struct tctx *t) +{ + (void)t; + yyjson_doc *d; d = call(reqf("{\"v\":1,\"id\":\"75\",\"cmd\":\"audit.verify\"," "\"session\":\"%s\"}", @@ -3832,6 +3970,12 @@ int main(void) CHECK(jint(d, "result.attachments_checked") >= 1); CHECK(jint(d, "result.unbalanced_vouchers") == 0); yyjson_doc_free(d); +} + +static void test_audit_tamper(struct tctx *t) +{ + char *err = NULL; + yyjson_doc *d; int64_t voucher_id = 0; char voucher_desc[256] = ""; @@ -3955,7 +4099,7 @@ int main(void) "(SELECT id FROM accounts WHERE org_id=%d" " AND number='3001'),1,0,NULL" " FROM vouchers WHERE id=%lld", - (int)org_id, (long long)voucher_id), + (int)t->org_id, (long long)voucher_id), &err) == 0); free(err); err = NULL; @@ -3969,10 +4113,12 @@ int main(void) CHECK(jint(d, "result.first_unbalanced_voucher_id") == voucher_id); CHECK(jint(d, "result.unbalanced_vouchers") == 1); yyjson_doc_free(d); +} - test_transport(); - - test_pre_migration_snapshot(tmpdir, backupdir); +static void test_rate_limit(struct tctx *t) +{ + (void)t; + yyjson_doc *d; /* rate limiting must stay last: it blocks the login key */ for (int i = 0; i < 5; i++) @@ -3982,6 +4128,173 @@ int main(void) "\"password\":\"secret123\"}}"); CHECK_STR(d, "error.code", "RATE_LIMITED"); yyjson_doc_free(d); +} + +struct ttest { + const char *name; + void (*fn)(struct tctx *); + const char *deps; +}; + +static const struct ttest TESTS[] = { + { "secret", test_secret, "" }, + { "session_auth", test_session_auth, "" }, + { "org_members", test_org_members, "session_auth" }, + { "tokens", test_tokens, "org_members" }, + { "accounts", test_accounts, "org_members" }, + { "fiscal_years", test_fiscal_years, "org_members" }, + { "vouchers", test_vouchers, "org_members" }, + { "voucher_errors", test_voucher_errors, "org_members" }, + { "reports", test_reports, "vouchers" }, + { "general_ledger", test_general_ledger, "vouchers" }, + { "voucher_list", test_voucher_list, "vouchers" }, + { "vat_eskd", test_vat_eskd, "vouchers" }, + { "trial_balance", test_trial_balance, "vouchers" }, + { "idempotency", test_idempotency, "org_members" }, + { "attachments", test_attachments, "vouchers idempotency" }, + { "templates", test_templates, "org_members" }, + { "ib", test_ib, "org_members" }, + { "period_lock", test_period_lock, "fiscal_years" }, + { "sie_export", test_sie_export, "ib" }, + { "sie_import", test_sie_import, "attachments sie_export" }, + { "settings", test_settings, "org_members" }, + { "smtp_settings", test_smtp_settings, "org_members" }, + { "bank", test_bank, "settings" }, + { "invoices", test_invoices, "org_members" }, + { "invoice_send", test_invoice_send, "invoices" }, + { "moms_rules", test_moms_rules, "org_members" }, + { "rules_editor", test_rules_editor, "moms_rules" }, + { "bokslut", test_bokslut, "moms_rules" }, + { "sru", test_sru, "bokslut" }, + { "year_close", test_year_close, "fiscal_years" }, + { "audit_verify", test_audit_verify, "sie_import" }, + { "audit_tamper", test_audit_tamper, "audit_verify" }, + { "transport", test_transport, "" }, + { "pre_migration_snapshot", test_pre_migration_snapshot, "" }, + { "rate_limit", test_rate_limit, "" }, +}; + +static int test_index(const char *name) +{ + for (size_t i = 0; i < sizeof TESTS / sizeof TESTS[0]; i++) + if (strcmp(TESTS[i].name, name) == 0) + return (int)i; + return -1; +} + +static void list_tests(FILE *out) +{ + for (size_t i = 0; i < sizeof TESTS / sizeof TESTS[0]; i++) + fprintf(out, "%s\n", TESTS[i].name); +} + +static int dep_selected(const char *deps, const char *name) +{ + char buf[256]; + snprintf(buf, sizeof buf, "%s", deps); + char *save = NULL; + for (char *tok = strtok_r(buf, " ", &save); tok; + tok = strtok_r(NULL, " ", &save)) + if (strcmp(tok, name) == 0) + return 1; + return 0; +} + +static void run_tests(struct tctx *t, const char *only) +{ + size_t n = sizeof TESTS / sizeof TESTS[0]; + int sel[sizeof TESTS / sizeof TESTS[0]]; + memset(sel, 0, sizeof sel); + if (only) { + sel[test_index(only)] = 1; + int changed = 1; + while (changed) { + changed = 0; + for (size_t i = 0; i < n; i++) { + if (!sel[i]) + continue; + for (size_t j = 0; j < n; j++) { + if (!sel[j] && + dep_selected(TESTS[i].deps, TESTS[j].name)) { + sel[j] = 1; + changed = 1; + } + } + } + } + } else { + for (size_t i = 0; i < n; i++) + sel[i] = 1; + } + for (size_t i = 0; i < n; i++) { + if (!sel[i]) + continue; + if (only) + fprintf(stderr, "== %s\n", TESTS[i].name); + g_test = TESTS[i].name; + TESTS[i].fn(t); + } +} + +int main(int argc, char **argv) +{ + const char *only = NULL; + if (argc > 1) { + if (strcmp(argv[1], "--list") == 0 && argc == 2) { + list_tests(stdout); + return 0; + } + if (strcmp(argv[1], "--only") == 0 && argc == 3) { + if (test_index(argv[2]) < 0) { + fprintf(stderr, "unknown test: %s\n", argv[2]); + list_tests(stderr); + return 2; + } + only = argv[2]; + } else { + fprintf(stderr, "usage: %s [--list | --only NAME]\n", argv[0]); + return 2; + } + } + + char tmpdir[] = "/tmp/bokf-test-XXXXXX"; + if (!mkdtemp(tmpdir)) { + perror("mkdtemp"); + return 1; + } + struct tctx t; + memset(&t, 0, sizeof t); + snprintf(t.tmpdir, sizeof t.tmpdir, "%s", tmpdir); + snprintf(t.backupdir, sizeof t.backupdir, "%s/backup", tmpdir); + char dbpath[512]; + snprintf(dbpath, sizeof dbpath, "%s/bokfd.db", tmpdir); + mkdir(t.backupdir, 0700); + + config_defaults(); + g_cfg.log_level = 0; + free(g_cfg.backup_dir); + g_cfg.backup_dir = xstrdup(t.backupdir); + free(g_cfg.export_dir); + g_cfg.export_dir = xstrdup(t.tmpdir); + free(g_cfg.chart_k2_file); + g_cfg.chart_k2_file = xstrdup("data/bas_k2.csv"); + free(g_cfg.chart_k3_file); + g_cfg.chart_k3_file = xstrdup("data/bas_k3.csv"); + + char *err = NULL; + if (db_open(dbpath, &g_db, &err) != 0) { + fprintf(stderr, "db_open: %s\n", err ? err : "?"); + return 1; + } + int64_t admin_id = 0; + if (db_create_user(g_db, "admin", "Admin", "secret123", 1, &admin_id, + &err) != 0) { + fprintf(stderr, "create admin: %s\n", err ? err : "?"); + return 1; + } + sessions_init(3600); + + run_tests(&t, only); sqlite3_close(g_db); char path[600]; @@ -3990,12 +4303,16 @@ int main(void) snprintf(path, sizeof path, "%s-shm", dbpath); unlink(path); unlink(dbpath); - rmdir(backupdir); - rmdir(tmpdir); + rmdir(t.backupdir); + rmdir(t.tmpdir); config_free(); if (g_failures) { fprintf(stderr, "%d check(s) failed\n", g_failures); + fprintf(stderr, "failed tests:"); + for (int i = 0; i < g_failed_n; i++) + fprintf(stderr, " %s", g_failed[i]); + fprintf(stderr, "\n"); return 1; } printf("all tests passed\n"); -- cgit v1.3