Files
utun2/tests/test_db_sync_schema.c
T

152 lines
11 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 OLD_COLUMNS "timestamp,node_id,id,chain_hash,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 " OLD_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 " OLD_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 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 " OLD_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 " OLD_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 " OLD_COLUMNS " 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\" (" OLD_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\" (" OLD_COLUMNS ",message_id) SELECT *,x'01' 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,chain_hash,flags,data,"
"author_signature,message_id,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");
/* Ошибка после копирования первой строки должна откатить всю перестройку. */
assert(topo_groups_create_group(inst->topo_groups, 9001, TOPO_GROUP_TYPE_CHAT, "9001")); legacy_table(db, "msg_9001");
execute(db, "INSERT INTO msg_9001 VALUES(1,1,1,zeroblob(32),0,x'01',zeroblob(64),'kept',0,'');"
"INSERT INTO msg_9001 VALUES(2,1,2,zeroblob(32),0,x'02',zeroblob(3),'kept',0,'')");
assert(!db_sync_instance_add(inst, "msg_9001", 9001));
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);
execute(db, "UPDATE msg_9001 SET author_signature=zeroblob(64) WHERE timestamp=2");
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, "msg_9001", 9001); assert(si && db_sync_count(si) == 2);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS: partial-copy failure rolls back, registration can be retried");
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;
}