98 lines
7.3 KiB
C
98 lines
7.3 KiB
C
/* Новый dev-формат истории: атомарный сброс старой схемы, сохранность PM и версии после restart. */
|
|
#include "../src/utun_instance.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 "../lib/debug_config.h"
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
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 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); assert(sqlite3_finalize(st) == SQLITE_OK); return value;
|
|
}
|
|
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, const uint8_t* reply) {
|
|
struct chat_msg_submit req = {.inst = inst, .data = (uint8_t*)text, .data_len = strlen(text)};
|
|
snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel); strcpy(req.content_type, "text/plain");
|
|
if (reply) memcpy(req.reply_to, reply, 32);
|
|
assert(chat_core_submit_message(inst, &req) == 0); return req;
|
|
}
|
|
|
|
int main(void) {
|
|
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);
|
|
const char* config =
|
|
"[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
|
|
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"
|
|
"[server: udp]\naddr=127.0.0.1:0\ntype=public\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n";
|
|
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && utun_core_start(inst) == 0);
|
|
assert(chat_service_start(inst) == 0); chat_core_create_channel_auto(inst, "Versioned 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[2048]; snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st);
|
|
snprintf(table, sizeof(table), "msg_%s", channel);
|
|
struct chat_msg_submit first = send_text(inst, channel, "До правки", NULL);
|
|
struct chat_msg_submit reply = send_text(inst, channel, "Ответ", first.message_id);
|
|
uint8_t reaction[] = {1, 0};
|
|
assert(chat_feedback_set(inst, channel, first.message_id, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), NULL) == 1);
|
|
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET local_attrs='saved',flags=1,delivered_peers=2,delivery_chain='peer' WHERE id=1", table);
|
|
execute(db, sql);
|
|
assert(chat_core_edit_message(inst, channel, first.message_id, 1, "После правки 👍", strlen("После правки 👍")) == 0);
|
|
assert(chat_core_count(inst, channel) == 2);
|
|
snprintf(sql, sizeof(sql), "SELECT ver FROM \"%s\" WHERE id=1", table); assert(number(db, sql) == 2);
|
|
snprintf(sql, sizeof(sql), "SELECT timestamp FROM \"%s\" WHERE id=1", table); assert(number(db, sql) == (sqlite3_int64)first.timestamp);
|
|
snprintf(sql, sizeof(sql), "SELECT count(*) FROM \"%s\" WHERE id=1 AND local_attrs='saved' AND delivered_peers=0 AND delivery_chain=''", table);
|
|
assert(number(db, sql) == 1);
|
|
snprintf(sql, sizeof(sql), "SELECT count(*) FROM \"%s\" r JOIN \"%s\" o ON lower(hex(o.message_id))=json_extract(r.data,'$.reply_to')", table, table);
|
|
assert(number(db, sql) == 1);
|
|
struct chat_feedback_record feedback;
|
|
assert(chat_feedback_get(inst, channel, first.message_id, inst->node_id, CHAT_FEEDBACK_REACTION, &feedback) == 1);
|
|
assert(chat_core_edit_message(inst, channel, first.message_id, 1, "Stale", 5) == -2);
|
|
assert(chat_core_edit_message(inst, channel, first.message_id, 2, "После правки 👍", strlen("После правки 👍")) == 0);
|
|
snprintf(sql, sizeof(sql), "SELECT ver FROM \"%s\" WHERE id=1", table); assert(number(db, sql) == 2);
|
|
char json[8192]; size_t bytes;
|
|
assert(chat_core_get_messages_json(inst, channel, 10, 0, json, sizeof(json), &bytes) == 0);
|
|
assert(strstr(json, "\"ver\":2") && strstr(json, "\"edited_at\":") && strstr(json, "После правки"));
|
|
chat_service_stop(inst); assert(chat_service_start(inst) == 0);
|
|
assert(chat_core_count(inst, channel) == 2);
|
|
assert(chat_core_edit_message(inst, channel, first.message_id, 2, "После перезапуска", strlen("После перезапуска")) == 0);
|
|
snprintf(sql, sizeof(sql), "SELECT ver FROM \"%s\" WHERE id=1", table); assert(number(db, sql) == 3);
|
|
assert(chat_core_edit_message(inst, channel, reply.message_id, 1, "", 0) == -1);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS edit preserves IDs, timestamp, local attrs, replies, reactions; rejects stale editor and survives restart");
|
|
|
|
/* Старая история не переносится; при отказе DROP её строки остаются до успешного повторения. */
|
|
assert(topo_groups_create_group(inst->topo_groups, 9001, TOPO_GROUP_TYPE_CHAT, "9001"));
|
|
execute(db, "CREATE TABLE msg_9001(timestamp INTEGER,author_signature BLOB,data BLOB);"
|
|
"INSERT INTO msg_9001 VALUES(1,zeroblob(64),'old');"
|
|
"CREATE TABLE format_pm_sentinel(data TEXT);INSERT INTO format_pm_sentinel VALUES('private history');");
|
|
assert(chat_feedback_ensure_channel(inst, "9001") == 0);
|
|
execute(db, "INSERT INTO feedback_9001 VALUES(zeroblob(32),0,zeroblob(32),1,1,1,x'0100',zeroblob(64));");
|
|
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") == 1);
|
|
assert(number(db, "SELECT count(*) FROM feedback_9001") == 1);
|
|
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, "msg_9001", 9001);
|
|
assert(si && db_sync_count(si) == 0 && number(db, "SELECT count(*) FROM feedback_9001") == 0);
|
|
assert(number(db, "SELECT count(*) FROM format_pm_sentinel WHERE data='private history'") == 1);
|
|
assert(number(db, "SELECT count(*) FROM pragma_table_info('msg_9001') WHERE name IN ('nonce','ver','edited_at')") == 3);
|
|
db_sync_instance_remove(si);
|
|
si = db_sync_instance_add(inst, "msg_9001", 9001); assert(si && db_sync_count(si) == 0);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS old group format reset is atomic, scoped, and idempotent");
|
|
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: group message revisions and history format"); return 0;
|
|
}
|