Files
utun2/tests/test_db_sync_schema.c
T

171 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Сохранение настоящей истории при обновлении схемы, повторный запуск и атомарный отказ. */
#include "../src/utun_instance.h"
#include "../src/config_parser.h"
#include "../src/chat/chat_core.h"
#include "../src/chat/chat_feedback.h"
#include "../src/chat/db_sync.h"
#include "../src/routing_layer/topo_group.h"
#include "../src/transport_layer/secure_channel.h"
#include "../lib/debug_config.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SAVED_COLUMNS "timestamp,node_id,id,flags,data,author_signature,local_attrs,delivered_peers,delivery_chain"
static void execute(sqlite3* db, const char* sql) {
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "schema test SQL=%s: %s", sql, sqlite3_errmsg(db));
assert(rc == SQLITE_OK);
}
static sqlite3_int64 number(sqlite3* db, const char* sql) {
sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
sqlite3_int64 value = sqlite3_column_int64(st, 0); sqlite3_finalize(st); return value;
}
static void legacy_table(sqlite3* db, const char* name) {
char sql[1024];
snprintf(sql, sizeof(sql), "CREATE TABLE \"%s\" (timestamp INTEGER NOT NULL,node_id INTEGER NOT NULL,"
"id INTEGER NOT NULL,chain_hash BLOB NOT NULL,flags INTEGER NOT NULL DEFAULT 0,data BLOB,"
"author_signature BLOB NOT NULL,local_attrs TEXT DEFAULT '',delivered_peers INTEGER NOT NULL DEFAULT 0,"
"delivery_chain TEXT NOT NULL DEFAULT '',PRIMARY KEY(timestamp,author_signature));"
"CREATE INDEX \"idx_%s_ttl\" ON \"%s\" (node_id,timestamp)", name, name, name);
execute(db, sql);
}
static void check_history(sqlite3* db, const char* table, uint64_t group) {
char sql[768];
snprintf(sql, sizeof(sql), "SELECT count(*) FROM (SELECT " SAVED_COLUMNS " FROM \"%s\" EXCEPT SELECT * FROM expected)", table);
assert(number(db, sql) == 0);
snprintf(sql, sizeof(sql), "SELECT count(*) FROM (SELECT * FROM expected EXCEPT SELECT " SAVED_COLUMNS " FROM \"%s\")", table);
assert(number(db, sql) == 0);
snprintf(sql, sizeof(sql), "SELECT timestamp,author_signature,message_id FROM \"%s\"", table);
sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK);
int rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
uint8_t id[32]; assert(sqlite3_column_bytes(st, 1) == 64 && sqlite3_column_bytes(st, 2) == 32);
assert(chat_message_id(group, (uint64_t)sqlite3_column_int64(st, 0), sqlite3_column_blob(st, 1), id) == 0);
assert(!memcmp(id, sqlite3_column_blob(st, 2), 32));
}
assert(rc == SQLITE_DONE); sqlite3_finalize(st);
snprintf(sql, sizeof(sql), "SELECT count(*) FROM sqlite_master WHERE type='index' AND name='idx_%s_ttl'", table);
assert(number(db, sql) == 1 && number(db, "SELECT count(*) FROM sqlite_master WHERE name LIKE 'new_msg_%'") == 0);
assert(sqlite3_get_autocommit(db));
}
static int deny_drop(void* ctx, int action, const char* name, const char* arg, const char* db, const char* source) {
(void)ctx; (void)arg; (void)db; (void)source;
return action == SQLITE_DROP_TABLE && name && !strcmp(name, "msg_9001") ? SQLITE_DENY : SQLITE_OK;
}
static struct chat_msg_submit send_text(struct UTUN_INSTANCE* inst, const char* channel, const char* text) {
struct chat_msg_submit req = { .inst = inst, .data = (const uint8_t*)text, .data_len = strlen(text) };
snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel); strcpy(req.content_type, "text");
assert(chat_core_submit_message(inst, &req) == 0); return req;
}
/* Дополнительный режим проверяет копии пользовательских БД через тот же публичный API. */
static void check_copy(struct UTUN_INSTANCE* inst) {
sqlite3* db = inst->topo_sqlite_db; char** tables = NULL; int rows, columns;
assert(sqlite3_get_table(db, "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'msg_[0-9]*'",
&tables, &rows, &columns, NULL) == SQLITE_OK);
assert(db_sync_enable(inst) == 0);
for (int i = 0; i < rows; i++) {
const char* table = tables[i + 1]; uint64_t gid = strtoull(table + 4, NULL, 10); assert(gid);
assert(topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, table + 4));
char sql[768]; snprintf(sql, sizeof(sql), "CREATE TEMP TABLE expected AS SELECT " SAVED_COLUMNS " FROM \"%s\"", table);
execute(db, sql); struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, table, gid); assert(si);
check_history(db, table, gid); db_sync_instance_remove(si);
si = db_sync_instance_add(inst, table, gid); assert(si); check_history(db, table, gid); db_sync_instance_remove(si);
execute(db, "DROP TABLE expected");
}
sqlite3_free_table(tables); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS: user DB copy, tables=%d, all fields preserved", rows);
}
int main(int argc, char** argv) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_INFO);
struct UASYNC* ua = uasync_create(); assert(ua); char config[2048];
snprintf(config, sizeof(config),
"[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"
"db_path=%s\n[routing]\nmy_subnet=192.168.42.0/24\n[server: udp]\naddr=127.0.0.1:0\ntype=public\n"
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", argc == 2 ? argv[1] : "");
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && utun_core_start(inst) == 0);
if (argc == 2) { check_copy(inst); goto done; }
assert(chat_service_start(inst) == 0); chat_core_create_channel_auto(inst, "Stored history");
sqlite3* db = inst->topo_sqlite_db; sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(db, "SELECT channel_id FROM channels", -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
char channel[21], table[32], sql[1024]; snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st);
snprintf(table, sizeof(table), "msg_%s", channel); uint64_t gid = strtoull(channel, NULL, 10);
struct chat_msg_submit first = send_text(inst, channel, "История \"с кавычками\"\n👍");
send_text(inst, channel, "Второе сообщение");
/* Один timestamp и разные подписи должны получить разные стабильные ID. */
const char* json = "{\"ct\":\"text\",\"d\":\"same timestamp\"}"; uint8_t body[128], signature[64];
memcpy(body, &first.timestamp, 8); memcpy(body + 8, json, strlen(json));
assert(sc_ed25519_sign(inst->my_ed25519_privkey, body, 8 + strlen(json), signature) == SC_OK);
assert(db_sync_insert_signed(db_sync_instance_find(inst, gid), json, strlen(json), signature, 64, first.timestamp, NULL) == 0);
uasync_poll(ua, 0);
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET flags=1,local_attrs=NULL,delivered_peers=7,delivery_chain='saved chain';"
"CREATE TEMP TABLE expected AS SELECT " SAVED_COLUMNS " FROM \"%s\"", table, table); execute(db, sql);
chat_core_create_channel_auto(inst, "Empty history"); chat_service_stop(inst);
/* Воспроизводим старую схему всех каналов, чтобы тест покрывал DDL при загрузке списка каналов. */
char** tables = NULL; int rows, columns;
assert(sqlite3_get_table(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'msg_%'",
&tables, &rows, &columns, NULL) == SQLITE_OK);
for (int i = 0; i < rows; i++) {
const char* name = tables[i + 1];
snprintf(sql, sizeof(sql), "DROP INDEX \"idx_%s_ttl\";ALTER TABLE \"%s\" RENAME TO saved", name, name); execute(db, sql);
legacy_table(db, name); snprintf(sql, sizeof(sql), "INSERT INTO \"%s\" SELECT timestamp,node_id,id,zeroblob(32),flags,data,"
"author_signature,local_attrs,delivered_peers,delivery_chain FROM saved;DROP TABLE saved", name);
execute(db, sql);
}
sqlite3_free_table(tables);
assert(chat_service_start(inst) == 0); check_history(db, table, gid); assert(chat_core_count(inst, channel) == 3);
char listing[8192]; size_t len;
assert(chat_core_get_messages_json(inst, channel, 10, 0, listing, sizeof(listing), &len) == 0 && len > 0);
snprintf(sql, sizeof(sql), "INSERT INTO \"%s\" (" SAVED_COLUMNS ") SELECT * FROM expected", table);
assert(sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_CONSTRAINT);
assert(sqlite3_extended_errcode(db) == SQLITE_CONSTRAINT_NOTNULL);
snprintf(sql, sizeof(sql), "INSERT INTO \"%s\" (" SAVED_COLUMNS ",message_id,bucket) SELECT *,x'01',0 FROM expected", table);
assert(sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_CONSTRAINT);
assert(sqlite3_extended_errcode(db) == SQLITE_CONSTRAINT_CHECK);
snprintf(sql, sizeof(sql), "INSERT INTO \"%s\" SELECT timestamp+1000000,node_id,id,flags,data,"
"author_signature,message_id,bucket,local_attrs,delivered_peers,delivery_chain FROM \"%s\" LIMIT 1", table, table);
assert(sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_CONSTRAINT);
assert(sqlite3_extended_errcode(db) == SQLITE_CONSTRAINT_UNIQUE);
chat_service_stop(inst); assert(chat_service_start(inst) == 0); check_history(db, table, gid);
send_text(inst, channel, "Отправка после обновления 👍"); assert(chat_core_count(inst, channel) == 4);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS: restart, empty table, preserved fields, canonical IDs and new message submission");
/* Отказ DDL после копирования строк откатывает всю перестройку, не удаляя допустимую историю. */
assert(topo_groups_create_group(inst->topo_groups, 9001, TOPO_GROUP_TYPE_CHAT, "9001")); legacy_table(db, "msg_9001");
snprintf(sql, sizeof(sql), "INSERT INTO msg_9001 SELECT timestamp,node_id,id,zeroblob(32),flags,data,"
"author_signature,local_attrs,delivered_peers,delivery_chain FROM \"%s\" LIMIT 2", table); execute(db, sql);
assert(sqlite3_set_authorizer(db, deny_drop, NULL) == SQLITE_OK);
assert(!db_sync_instance_add(inst, "msg_9001", 9001));
assert(sqlite3_set_authorizer(db, NULL, NULL) == SQLITE_OK);
assert(sqlite3_get_autocommit(db) && number(db, "SELECT count(*) FROM msg_9001") == 2);
assert(number(db, "SELECT count(*) FROM pragma_table_info('msg_9001') WHERE name='message_id'") == 0);
assert(number(db, "SELECT count(*) FROM sqlite_master WHERE name='new_msg_9001'") == 0);
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, "msg_9001", 9001);
assert(si && db_sync_count(si) == 2 && db_sync_verify(si) == 0);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS schema DDL failure rolls back; valid records survive retry");
/* Доказанный битый формат удаляется при перестройке, неверная подпись — при preflight. */
assert(topo_groups_create_group(inst->topo_groups, 9002, TOPO_GROUP_TYPE_CHAT, "9002")); legacy_table(db, "msg_9002");
snprintf(sql, sizeof(sql), "INSERT INTO msg_9002 SELECT timestamp,node_id,id,zeroblob(32),flags,data,"
"author_signature,local_attrs,delivered_peers,delivery_chain FROM \"%s\" LIMIT 1;"
"INSERT INTO msg_9002 SELECT 2,node_id,200,zeroblob(32),0,x'01',zeroblob(3),'bad format',0,'' FROM \"%s\" LIMIT 1;"
"INSERT INTO msg_9002 SELECT 3,node_id,201,zeroblob(32),0,x'01',zeroblob(64),'bad signature',0,'' FROM \"%s\" LIMIT 1",
table, table, table); execute(db, sql);
si = db_sync_instance_add(inst, "msg_9002", 9002);
assert(si && db_sync_count(si) == 2 && db_sync_verify(si) == 0 && db_sync_count(si) == 1);
assert(number(db, "SELECT count(*) FROM pragma_table_info('msg_9002') WHERE name='chain_hash'") == 0);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS proven invalid legacy rows removed; valid signed data preserved");
done:
utun_instance_destroy(inst); uasync_poll(ua, 0);
assert(ua->timer_alloc_count == ua->timer_free_count && ua->socket_alloc_count == ua->socket_free_count);
uasync_destroy(ua, 0); puts("ALL PASS: chat history schema repair"); return 0;
}