#include #include #include #include #include "utun_instance.h" #include "dm/dm_schema.h" static void sql(sqlite3* db, const char* text) { int rc = sqlite3_exec(db, text, NULL, NULL, NULL); if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_DM, "schema test: SQL failed rc=%d text=%s error=%s", rc, text, sqlite3_errmsg(db)); assert(rc == SQLITE_OK); } static sqlite3_int64 number(sqlite3* db, const char* text) { sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, text, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); sqlite3_int64 value = sqlite3_column_int64(st, 0); assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK); return value; } static int fail_drop(void* arg, int action, const char* name, const char* column, const char* db, const char* trigger) { (void)arg; (void)column; (void)db; (void)trigger; return action == SQLITE_DROP_TABLE && name && !strcmp(name, "dm_messages_previous") ? SQLITE_DENY : SQLITE_OK; } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); struct UTUN_INSTANCE inst = {.node_id = 55}; assert(sqlite3_open(":memory:", &inst.topo_sqlite_db) == SQLITE_OK); sqlite3* db = inst.topo_sqlite_db; /* Снимок прежней схемы, включая неотправленное тело и локально удалённую историю. */ sql(db, "CREATE TABLE dm_conversations(conv_id TEXT PRIMARY KEY,peer_node_id INTEGER NOT NULL,peer_x25519 BLOB NOT NULL," "peer_ed25519 BLOB NOT NULL,peer_name TEXT,group_id INTEGER,last_out_seq INTEGER,last_in_seq INTEGER,created_at INTEGER,last_ts INTEGER);" "INSERT INTO dm_conversations VALUES('1',22,zeroblob(32),zeroblob(32),'old peer',10,7,3,1,2);" "INSERT INTO dm_conversations VALUES('2',33,zeroblob(32),zeroblob(32),'another peer',10,19,0,1,2);" "CREATE TABLE dm_messages(conv_id TEXT NOT NULL,dir INTEGER NOT NULL,seq INTEGER NOT NULL,ts INTEGER NOT NULL," "author INTEGER NOT NULL,ct TEXT,data BLOB,sig BLOB,PRIMARY KEY(conv_id,dir,seq));" "INSERT INTO dm_messages VALUES('1',1,7,2,55,'text',x'0102',x'0304');" "INSERT INTO dm_messages VALUES('1',0,3,1,22,'text',x'0506',x'0708');" "CREATE TABLE dm_deleted_messages(conv_id TEXT NOT NULL,dir INTEGER NOT NULL,seq INTEGER NOT NULL," "body_hash BLOB NOT NULL,media_id BLOB,media_receipt BLOB,PRIMARY KEY(conv_id,dir,seq));" "INSERT INTO dm_deleted_messages VALUES('1',1,1,x'010203',NULL,NULL);" "INSERT INTO dm_deleted_messages VALUES('1',0,1,x'040506',x'07',x'08');" "CREATE TABLE dm_outbox(conv_id TEXT NOT NULL,seq INTEGER NOT NULL,body BLOB NOT NULL,retry_at INTEGER NOT NULL,PRIMARY KEY(conv_id,seq));" "INSERT INTO dm_outbox VALUES('1',7,x'1122334455',123);"); assert(sqlite3_set_authorizer(db, fail_drop, NULL) == SQLITE_OK); assert(dm_schema_init(&inst) < 0); assert(sqlite3_set_authorizer(db, NULL, NULL) == SQLITE_OK); assert(number(db, "SELECT COUNT(*) FROM pragma_table_info('dm_messages') WHERE name='body'") == 0); assert(number(db, "SELECT COUNT(*) FROM dm_messages") == 2 && sqlite3_get_autocommit(db)); assert(!dm_schema_init(&inst)); assert(number(db, "SELECT COUNT(*) FROM dm_messages WHERE body IS NULL") == 2); assert(number(db, "SELECT COUNT(*) FROM dm_messages WHERE author=55 AND hex(data)='0102' AND hex(sig)='0304'") == 1); assert(number(db, "SELECT COUNT(*) FROM dm_outbox WHERE hex(body)='1122334455' AND retry_at=123") == 1); assert(number(db, "SELECT COUNT(*) FROM dm_deleted_messages WHERE author=55 AND hex(body_hash)='010203'") == 1); assert(number(db, "SELECT COUNT(*) FROM dm_deleted_messages WHERE author=22 AND hex(media_id)='07' AND hex(media_receipt)='08'") == 1); assert(number(db, "SELECT COUNT(*) FROM dm_delivery WHERE author=55 AND recipient=22 AND seq=7 AND retry_at=123") == 1); assert(number(db, "SELECT COUNT(*) FROM dm_conversations WHERE mode=0 AND local_party IS NULL AND closed=0") == 2); sql(db, "INSERT INTO dm_messages VALUES('1',1,7,3,66,'text',x'09',x'0A',x'0B')"); assert(number(db, "SELECT COUNT(*) FROM dm_messages WHERE conv_id='1' AND dir=1 AND seq=7") == 2); uint64_t seq = 0; assert(!dm_sequence_next(&inst, &seq) && seq == 20); assert(dm_sequence_take(&inst, seq) < 0); sql(db, "BEGIN"); assert(!dm_sequence_take(&inst, seq)); sql(db, "ROLLBACK"); assert(!dm_sequence_next(&inst, &seq) && seq == 20); sql(db, "BEGIN"); assert(!dm_sequence_take(&inst, seq)); assert(dm_sequence_take(&inst, seq) < 0); sql(db, "COMMIT"); assert(!dm_schema_init(&inst) && !dm_sequence_next(&inst, &seq) && seq == 21); assert(number(db, "SELECT COUNT(*) FROM dm_delivery") == 1 && number(db, "SELECT COUNT(*) FROM dm_messages") == 3); sql(db, "UPDATE dm_sequence SET value=9223372036854775807"); assert(dm_sequence_next(&inst, &seq) < 0 && seq == 21); assert(sqlite3_close(db) == SQLITE_OK); /* Чистая база создаётся сразу с новой идентичностью сообщения. */ assert(sqlite3_open(":memory:", &inst.topo_sqlite_db) == SQLITE_OK && !dm_schema_init(&inst)); assert(!dm_sequence_next(&inst, &seq) && seq == 1); assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK); puts("PASS DM schema: atomic migration, immutable outbox, replay fingerprints, multiple authors and durable sequence"); return 0; }