Skip to content

Commit dc9e1bd

Browse files
andinuxclaude
andcommitted
test: cover the real error path in cloudsync_dbversion_rebuild
Install an sqlite3 authorizer that denies reads of sqlite_master, so sqlite3_prepare_v2 of SQL_DBVERSION_BUILD_QUERY fails with SQLITE_AUTH. On a context with a non-empty cloudsync_table_settings, this must now return a non-OK rc and set cloudsync_errcode/errmsg — before the review fix it was silently collapsed into DBRES_OK, leaving db_version_stmt unset. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 00bff49 commit dc9e1bd

1 file changed

Lines changed: 78 additions & 0 deletions

File tree

test/unit.c

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ sqlite3 *do_create_database (void);
4646

4747
int cloudsync_table_sanity_check (cloudsync_context *data, const char *name, CLOUDSYNC_INIT_FLAG init_flags);
4848
bool database_system_exists (cloudsync_context *data, const char *name, const char *type);
49+
int cloudsync_dbversion_rebuild (cloudsync_context *data);
4950

5051
static int stdout_backup = -1; // Backup file descriptor for stdout
5152
static int dev_null_fd = -1; // File descriptor for /dev/null
@@ -2479,6 +2480,82 @@ bool do_test_stale_table_settings_dropped_meta(bool cleanup_databases) {
24792480
return result;
24802481
}
24812482

2483+
// Authorizer that denies SELECT reads of sqlite_master. Used to force
2484+
// sqlite3_prepare_v2 of SQL_DBVERSION_BUILD_QUERY (which scans sqlite_master)
2485+
// to fail with SQLITE_AUTH, exercising the real error path in
2486+
// cloudsync_dbversion_rebuild introduced after 1.0.14.
2487+
static int deny_sqlite_master_authorizer(void *pUserData, int action, const char *zArg1,
2488+
const char *zArg2, const char *zDbName, const char *zTrigger) {
2489+
(void)pUserData; (void)zArg2; (void)zDbName; (void)zTrigger;
2490+
if (action == SQLITE_READ && zArg1 && strcmp(zArg1, "sqlite_master") == 0) {
2491+
return SQLITE_DENY;
2492+
}
2493+
return SQLITE_OK;
2494+
}
2495+
2496+
// Verify that cloudsync_dbversion_rebuild surfaces a real failure from
2497+
// database_select_text(SQL_DBVERSION_BUILD_QUERY, ...) instead of silently
2498+
// treating it as "no *_cloudsync meta-tables present" — which would leave
2499+
// db_version_stmt unset and cause writes to fall back to CLOUDSYNC_MIN_DB_VERSION.
2500+
bool do_test_dbversion_rebuild_error(void) {
2501+
sqlite3 *db = NULL;
2502+
cloudsync_context *ctx = NULL;
2503+
bool result = false;
2504+
2505+
int rc = sqlite3_open(":memory:", &db);
2506+
if (rc != SQLITE_OK) return false;
2507+
rc = sqlite3_cloudsync_init(db, NULL, NULL);
2508+
if (rc != SQLITE_OK) goto cleanup;
2509+
2510+
// Create a real cloudsync table so cloudsync_table_settings has a row
2511+
// (count_tables > 0 — the early-return-OK path is not taken).
2512+
rc = sqlite3_exec(db, "CREATE TABLE t (id TEXT PRIMARY KEY NOT NULL, v TEXT);", NULL, NULL, NULL);
2513+
if (rc != SQLITE_OK) goto cleanup;
2514+
rc = sqlite3_exec(db, "SELECT cloudsync_init('t');", NULL, NULL, NULL);
2515+
if (rc != SQLITE_OK) goto cleanup;
2516+
2517+
// Create a secondary context on the same db and initialize it. This
2518+
// context is independent from the one registered by sqlite3_cloudsync_init,
2519+
// so we can call cloudsync_dbversion_rebuild on it directly without
2520+
// disturbing the registered functions.
2521+
ctx = cloudsync_context_create(db);
2522+
if (!ctx) goto cleanup;
2523+
if (cloudsync_context_init(ctx) == NULL) goto cleanup;
2524+
2525+
// Install an authorizer that denies reads of sqlite_master. New prepares
2526+
// (including the one SQL_DBVERSION_BUILD_QUERY triggers inside
2527+
// database_select_text) will fail with SQLITE_AUTH. Already-prepared
2528+
// statements are unaffected, so the registered cloudsync_* functions
2529+
// still work for cleanup.
2530+
sqlite3_set_authorizer(db, deny_sqlite_master_authorizer, NULL);
2531+
2532+
// Expect a non-OK result now that the build query cannot be prepared.
2533+
// Before the review fix this would incorrectly return DBRES_OK and leave
2534+
// db_version_stmt == NULL, silently masking the failure.
2535+
int rebuild_rc = cloudsync_dbversion_rebuild(ctx);
2536+
2537+
// Remove authorizer before any further work so cleanup can run normally.
2538+
sqlite3_set_authorizer(db, NULL, NULL);
2539+
2540+
if (rebuild_rc == DBRES_OK) goto cleanup;
2541+
2542+
// The error must have been recorded on the context via cloudsync_set_dberror.
2543+
if (cloudsync_errcode(ctx) == DBRES_OK) goto cleanup;
2544+
const char *msg = cloudsync_errmsg(ctx);
2545+
if (!msg || msg[0] == 0) goto cleanup;
2546+
2547+
result = true;
2548+
2549+
cleanup:
2550+
sqlite3_set_authorizer(db, NULL, NULL);
2551+
if (ctx) cloudsync_context_free(ctx);
2552+
if (db) {
2553+
sqlite3_exec(db, "SELECT cloudsync_terminate();", NULL, NULL, NULL);
2554+
sqlite3_close(db);
2555+
}
2556+
return result;
2557+
}
2558+
24822559
// Authorizer state for do_test_context_cb_error_cleanup.
24832560
// Denies INSERT on a specific table after allowing a set number of INSERTs.
24842561
static const char *g_deny_insert_table = NULL;
@@ -12347,6 +12424,7 @@ int main (int argc, const char * argv[]) {
1234712424
result += test_report("Schema Hash Mismatch:", do_test_schema_hash_mismatch(2, print_result, cleanup_databases));
1234812425
result += test_report("Stale Table Settings:", do_test_stale_table_settings(cleanup_databases));
1234912426
result += test_report("Stale Table Settings Dropped Meta:", do_test_stale_table_settings_dropped_meta(cleanup_databases));
12427+
result += test_report("DBVersion Rebuild Error:", do_test_dbversion_rebuild_error());
1235012428
result += test_report("Block LWW Existing Data:", do_test_block_lww_existing_data(cleanup_databases));
1235112429
result += test_report("Block Column Reload:", do_test_block_column_reload(cleanup_databases));
1235212430
result += test_report("CB Error Cleanup:", do_test_context_cb_error_cleanup());

0 commit comments

Comments
 (0)