summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_core.c140
1 files changed, 140 insertions, 0 deletions
diff --git a/tests/test_core.c b/tests/test_core.c
index 6cd7b18..885523c 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -635,6 +635,9 @@ int main(void)
CHECK_OK(d);
CHECK(jbool(d, "result.ok"));
CHECK(jint(d, "result.checked") > 5);
+ CHECK(jint(d, "result.vouchers_checked") == 0);
+ CHECK(jint(d, "result.unbalanced_vouchers") == 0);
+ CHECK(jint(d, "result.first_bad_voucher_id") == -1);
yyjson_doc_free(d);
d = call(reqf("{\"v\":1,\"id\":\"27\",\"cmd\":\"audit.list\","
@@ -1890,6 +1893,143 @@ int main(void)
CHECK_OK(d);
CHECK(jbool(d, "result.ok"));
CHECK(jint(d, "result.checked") > 20);
+ CHECK(jint(d, "result.vouchers_checked") > 10);
+ CHECK(jint(d, "result.unbalanced_vouchers") == 0);
+ yyjson_doc_free(d);
+
+ /* ------------------- audit.verify tamper detection ---------------- */
+
+ d = call(reqf("{\"v\":1,\"id\":\"80\",\"cmd\":\"audit.verify\","
+ "\"session\":\"%s\",\"args\":{\"full\":true}}",
+ g_session));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.ok"));
+ CHECK(jint(d, "result.vouchers_checked") > 10);
+ CHECK(jint(d, "result.attachments_checked") >= 1);
+ CHECK(jint(d, "result.unbalanced_vouchers") == 0);
+ yyjson_doc_free(d);
+
+ int64_t voucher_id = 0;
+ char voucher_desc[256] = "";
+ sqlite3_stmt *vst = NULL;
+ CHECK(sqlite3_prepare_v2(g_db,
+ "SELECT id,description FROM vouchers"
+ " ORDER BY id LIMIT 1",
+ -1, &vst, NULL) == SQLITE_OK);
+ if (sqlite3_step(vst) == SQLITE_ROW) {
+ voucher_id = sqlite3_column_int64(vst, 0);
+ snprintf(voucher_desc, sizeof voucher_desc, "%s",
+ (const char *)sqlite3_column_text(vst, 1));
+ }
+ sqlite3_finalize(vst);
+ CHECK(voucher_id > 0);
+
+ int64_t attachment_id = 0;
+ sqlite3_stmt *ast = NULL;
+ CHECK(sqlite3_prepare_v2(g_db,
+ "SELECT id FROM attachments ORDER BY id LIMIT 1",
+ -1, &ast, NULL) == SQLITE_OK);
+ if (sqlite3_step(ast) == SQLITE_ROW)
+ attachment_id = sqlite3_column_int64(ast, 0);
+ sqlite3_finalize(ast);
+ CHECK(attachment_id > 0);
+
+ /* attachments are re-hashed only with full:true */
+ err = NULL;
+ CHECK(db_exec(g_db, reqf("UPDATE attachments SET content=x'00'"
+ " WHERE id=%lld", (long long)attachment_id),
+ &err) == 0);
+ free(err);
+ err = NULL;
+
+ d = call(reqf("{\"v\":1,\"id\":\"81\",\"cmd\":\"audit.verify\","
+ "\"session\":\"%s\"}",
+ g_session));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.ok"));
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"82\",\"cmd\":\"audit.verify\","
+ "\"session\":\"%s\",\"args\":{\"full\":true}}",
+ g_session));
+ CHECK_OK(d);
+ CHECK(!jbool(d, "result.ok"));
+ CHECK(jint(d, "result.first_bad_attachment_id") == attachment_id);
+ CHECK(jint(d, "result.vouchers_checked") > 10);
+ yyjson_doc_free(d);
+
+ /* the append-only trigger blocks the tamper; after dropping it the
+ modified description must break the chain at that voucher */
+ err = NULL;
+ CHECK(db_exec(g_db, reqf("UPDATE vouchers SET description='x'"
+ " WHERE id=%lld", (long long)voucher_id),
+ &err) != 0);
+ free(err);
+ err = NULL;
+ CHECK(db_exec(g_db, "DROP TRIGGER vouchers_no_update", &err) == 0);
+ free(err);
+ err = NULL;
+ CHECK(db_exec(g_db, reqf("UPDATE vouchers SET description='kapad'"
+ " WHERE id=%lld", (long long)voucher_id),
+ &err) == 0);
+ free(err);
+ err = NULL;
+
+ d = call(reqf("{\"v\":1,\"id\":\"83\",\"cmd\":\"audit.verify\","
+ "\"session\":\"%s\"}",
+ g_session));
+ CHECK_OK(d);
+ CHECK(!jbool(d, "result.ok"));
+ CHECK(jint(d, "result.first_bad_voucher_id") == voucher_id);
+ CHECK(jint(d, "result.first_bad_seq") == -1);
+ yyjson_doc_free(d);
+
+ char *fix = sqlite3_mprintf("UPDATE vouchers SET description=%Q"
+ " WHERE id=%lld", voucher_desc,
+ (long long)voucher_id);
+ CHECK(fix != NULL);
+ err = NULL;
+ CHECK(db_exec(g_db, fix, &err) == 0);
+ sqlite3_free(fix);
+ free(err);
+ err = NULL;
+ CHECK(db_exec(g_db, "CREATE TRIGGER vouchers_no_update BEFORE UPDATE ON"
+ " vouchers BEGIN SELECT RAISE(ABORT,"
+ " 'vouchers are append-only'); END",
+ &err) == 0);
+ free(err);
+ err = NULL;
+
+ d = call(reqf("{\"v\":1,\"id\":\"84\",\"cmd\":\"audit.verify\","
+ "\"session\":\"%s\"}",
+ g_session));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.ok"));
+ CHECK(jint(d, "result.first_bad_voucher_id") == -1);
+ yyjson_doc_free(d);
+
+ /* the balance backstop catches a row appended outside the ledger path */
+ err = NULL;
+ CHECK(db_exec(g_db,
+ reqf("INSERT INTO voucher_rows(org_id,voucher_id,line_no,"
+ "account_id,debit_ore,credit_ore,description)"
+ " SELECT org_id,id,99,"
+ "(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),
+ &err) == 0);
+ free(err);
+ err = NULL;
+
+ d = call(reqf("{\"v\":1,\"id\":\"85\",\"cmd\":\"audit.verify\","
+ "\"session\":\"%s\"}",
+ g_session));
+ CHECK_OK(d);
+ CHECK(!jbool(d, "result.ok"));
+ CHECK(jint(d, "result.first_bad_voucher_id") == voucher_id);
+ CHECK(jint(d, "result.first_unbalanced_voucher_id") == voucher_id);
+ CHECK(jint(d, "result.unbalanced_vouchers") == 1);
yyjson_doc_free(d);
test_transport();