summaryrefslogtreecommitdiff
path: root/tests/test_core.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_core.c')
-rw-r--r--tests/test_core.c1005
1 files changed, 661 insertions, 344 deletions
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 <a@b.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\":\"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");