310 lines
23 KiB
C
310 lines
23 KiB
C
/* Кодеки и пользовательский API поверх настоящего ядра, подписей и SQLite. */
|
||
#include "../src/chat/chat_feedback.c"
|
||
#include "../src/chat/chat_poll.h"
|
||
#include "../src/chat/chat_core.h"
|
||
#include "../src/config_parser.h"
|
||
#include <assert.h>
|
||
|
||
enum poll_fault { P_NONE, P_PREPARE, P_BIND, P_STEP, P_FINALIZE };
|
||
enum poll_query { P_VALID, P_FIELD, P_TARGET, P_SNAPSHOT, P_CHOICES, P_OPTIONS };
|
||
static enum poll_fault fault;
|
||
static enum poll_query query;
|
||
static unsigned query_number, matching_queries, fault_bind, changed_events;
|
||
static sqlite3_stmt* fault_statement;
|
||
static int fail_ack_allocation;
|
||
|
||
extern void* __real_u_malloc_impl(uint32_t, const char*);
|
||
void* __wrap_u_malloc_impl(uint32_t size, const char* location) {
|
||
if (fail_ack_allocation && strstr(location, "chat_poll.c")) {
|
||
fail_ack_allocation = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "poll test: committed ack allocation failed bytes=%u", size); return NULL;
|
||
}
|
||
return __real_u_malloc_impl(size, location);
|
||
}
|
||
|
||
static int query_matches(const char* sql) {
|
||
const char* prefix[] = {"SELECT json_valid(?1)", "SELECT json_type(?1,?2),json_extract(?1,?2)",
|
||
"SELECT CASE WHEN json_valid", "SELECT node_id,kind,ver,data FROM feedback_",
|
||
"SELECT type,value FROM json_each(?1,?2)", "SELECT type,value FROM json_each(?1,'$.options')"};
|
||
return !strncmp(sql, prefix[query], strlen(prefix[query]));
|
||
}
|
||
|
||
static int take_fault(enum poll_fault point, sqlite3_stmt* st, unsigned index) {
|
||
if (fault != point || st != fault_statement || (point == P_BIND && index != fault_bind)) return 0;
|
||
fault = P_NONE;
|
||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "poll test: injected query=%d occurrence=%u stage=%d bind=%u", query, query_number, point, index);
|
||
return 1;
|
||
}
|
||
|
||
extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**);
|
||
int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int length, sqlite3_stmt** st, const char** tail) {
|
||
int target = fault != P_NONE && query_matches(sql) && ++matching_queries == query_number;
|
||
if (target && fault == P_PREPARE) {
|
||
fault = P_NONE; *st = NULL; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "poll test: prepare query=%d occurrence=%u", query, query_number);
|
||
return SQLITE_NOMEM;
|
||
}
|
||
int rc = __real_sqlite3_prepare_v2(db, sql, length, st, tail);
|
||
if (target && rc == SQLITE_OK) fault_statement = *st;
|
||
return rc;
|
||
}
|
||
extern int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*));
|
||
int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* bytes, int length, void (*destroy)(void*)) {
|
||
if (take_fault(P_BIND, st, index)) return SQLITE_NOMEM;
|
||
return __real_sqlite3_bind_blob(st, index, bytes, length, destroy);
|
||
}
|
||
extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*));
|
||
int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* bytes, int length, void (*destroy)(void*)) {
|
||
if (take_fault(P_BIND, st, index)) return SQLITE_NOMEM;
|
||
return __real_sqlite3_bind_text(st, index, bytes, length, destroy);
|
||
}
|
||
extern int __real_sqlite3_step(sqlite3_stmt*);
|
||
int __wrap_sqlite3_step(sqlite3_stmt* st) {
|
||
if (take_fault(P_STEP, st, 0)) return SQLITE_IOERR;
|
||
return __real_sqlite3_step(st);
|
||
}
|
||
extern int __real_sqlite3_finalize(sqlite3_stmt*);
|
||
int __wrap_sqlite3_finalize(sqlite3_stmt* st) {
|
||
int fail = st && take_fault(P_FINALIZE, st, 0);
|
||
int rc = __real_sqlite3_finalize(st); if (st == fault_statement) fault_statement = NULL;
|
||
return fail ? SQLITE_IOERR : rc;
|
||
}
|
||
|
||
static void poll_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
|
||
(void)data; (void)len;
|
||
assert(sqlite3_get_autocommit(inst->topo_sqlite_db));
|
||
if (type == CHAT_EVT_FEEDBACK_CHANGED || type == CHAT_EVT_MSG_RECEIVED) changed_events++;
|
||
}
|
||
|
||
static sqlite3_int64 json_number(sqlite3* db, const char* json, const char* path) {
|
||
sqlite3_stmt* st = NULL;
|
||
assert(sqlite3_prepare_v2(db, "SELECT json_extract(?1,?2)", -1, &st, NULL) == SQLITE_OK);
|
||
sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC);
|
||
assert(sqlite3_step(st) == SQLITE_ROW); sqlite3_int64 value = sqlite3_column_int64(st, 0); sqlite3_finalize(st); return value;
|
||
}
|
||
|
||
static void json_string(sqlite3* db, const char* json, const char* path, char* out, size_t size) {
|
||
sqlite3_stmt* st = NULL;
|
||
assert(sqlite3_prepare_v2(db, "SELECT json_extract(?1,?2)", -1, &st, NULL) == SQLITE_OK);
|
||
sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC);
|
||
assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_type(st, 0) == SQLITE_TEXT);
|
||
const char* text = (const char*)sqlite3_column_text(st, 0); assert(strlen(text) < size);
|
||
memcpy(out, text, strlen(text) + 1); sqlite3_finalize(st);
|
||
}
|
||
|
||
static char* command(struct UTUN_INSTANCE* inst, const char* text, int expected) {
|
||
char* result = NULL; assert(chat_feedback_command(inst, text, &result) == expected && result);
|
||
assert(json_number(inst->topo_sqlite_db, "{\"valid\":1}", "$.valid") == 1); return result;
|
||
}
|
||
|
||
static int message_count(struct UTUN_INSTANCE* inst, const char* channel) {
|
||
char sql[128]; snprintf(sql, sizeof(sql), "SELECT count(*) FROM msg_%s", channel); sqlite3_stmt* st = NULL;
|
||
assert(sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
||
int count = sqlite3_column_int(st, 0); assert(sqlite3_finalize(st) == SQLITE_OK); return count;
|
||
}
|
||
|
||
/* Ошибка SQL не становится отсутствующим полем/пустой сводкой и не публикует команду. */
|
||
static void sql_faults(struct UTUN_INSTANCE* inst, const char* channel, const char* hex, const uint8_t message[32]) {
|
||
char react[512], vote[512], create[512]; uint8_t before[32], after[32]; unsigned cases = 0;
|
||
snprintf(react, sizeof(react), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":0}", channel, hex);
|
||
snprintf(vote, sizeof(vote), "{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\",\"choices\":[0]}", channel, hex);
|
||
snprintf(create, sizeof(create), "{\"cmd\":\"poll_create\",\"ch\":\"%s\",\"question\":\"SQL retry\","
|
||
"\"mode\":\"single\",\"options\":[\"one\",\"two\"]}", channel);
|
||
chat_event_set_handler(inst, poll_event);
|
||
for (enum poll_query operation = P_VALID; operation <= P_OPTIONS; operation++) {
|
||
unsigned occurrences = operation == P_FIELD ? 6 : 1;
|
||
for (unsigned occurrence = 1; occurrence <= occurrences; occurrence++) {
|
||
for (enum poll_fault point = P_PREPARE; point <= P_FINALIZE; point++) {
|
||
unsigned bindings = point == P_BIND && (operation == P_FIELD || operation == P_CHOICES) ? 2 : 1;
|
||
for (unsigned bind = 1; bind <= bindings; bind++) {
|
||
struct chat_feedback_record saved, actual;
|
||
assert(chat_feedback_get(inst, channel, message, inst->node_id, CHAT_FEEDBACK_REACTION, &saved) == 1);
|
||
assert(!merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, channel, 0, 0, before));
|
||
unsigned events = changed_events; int messages = message_count(inst, channel);
|
||
query = operation; query_number = occurrence; matching_queries = 0; fault_statement = NULL;
|
||
fault_bind = bind; fault = point; char* result = NULL;
|
||
const char* request = operation == P_CHOICES ? vote : operation == P_OPTIONS ? create : react;
|
||
if (operation == P_SNAPSHOT) {
|
||
assert(chat_feedback_snapshot(inst, channel, message, &result) < 0 && !result);
|
||
} else { result = command(inst, request, -1); u_free(result); }
|
||
assert(fault == P_NONE && !fault_statement && changed_events == events && message_count(inst, channel) == messages);
|
||
assert(!merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, channel, 0, 0, after) && !memcmp(before, after, 32));
|
||
assert(chat_feedback_get(inst, channel, message, inst->node_id, CHAT_FEEDBACK_REACTION, &actual) == 1);
|
||
assert(actual.ver == saved.ver && actual.data_len == saved.data_len && !memcmp(actual.data, saved.data, saved.data_len));
|
||
assert(!memcmp(actual.signature, saved.signature, 64));
|
||
if (operation == P_SNAPSHOT) assert(!chat_feedback_snapshot(inst, channel, message, &result) && result);
|
||
else result = command(inst, request, 0);
|
||
u_free(result); cases++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
struct chat_feedback_record saved, actual;
|
||
assert(chat_feedback_get(inst, channel, message, inst->node_id, CHAT_FEEDBACK_REACTION, &saved) == 1);
|
||
snprintf(react, sizeof(react), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":\"👍\\u0000tail\"}", channel, hex);
|
||
char* result = command(inst, react, -1); u_free(result);
|
||
assert(chat_feedback_get(inst, channel, message, inst->node_id, CHAT_FEEDBACK_REACTION, &actual) == 1 && actual.ver == saved.ver);
|
||
assert(actual.data_len == saved.data_len && !memcmp(actual.data, saved.data, saved.data_len));
|
||
/* Snapshot после commit может отказать: ответ остаётся успехом, повторяем только чтение. */
|
||
snprintf(react, sizeof(react), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":0}", channel, hex);
|
||
for (unsigned publish = 0; publish < 2; publish++) {
|
||
const char* request = publish ? create : react;
|
||
for (enum poll_fault point = P_PREPARE; point <= P_FINALIZE; point++) {
|
||
int messages = message_count(inst, channel);
|
||
query = P_SNAPSHOT; query_number = 1; matching_queries = 0; fault_statement = NULL; fault_bind = 1; fault = point;
|
||
result = command(inst, request, 0); assert(fault == P_NONE && !fault_statement);
|
||
assert(json_number(inst->topo_sqlite_db, result, "$.committed") == 1 &&
|
||
json_number(inst->topo_sqlite_db, result, "$.snapshot_pending") == 1);
|
||
char acknowledged[65], read[256]; json_string(inst->topo_sqlite_db, result, "$.message_id", acknowledged, sizeof(acknowledged));
|
||
u_free(result);
|
||
assert(message_count(inst, channel) == messages + (int)publish);
|
||
snprintf(read, sizeof(read), "{\"cmd\":\"feedback\",\"ch\":\"%s\",\"message_id\":\"%s\"}", channel, acknowledged);
|
||
assert(!merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, channel, 0, 0, before)); unsigned events = changed_events;
|
||
result = command(inst, read, 0); u_free(result);
|
||
assert(message_count(inst, channel) == messages + (int)publish && changed_events == events);
|
||
assert(!merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, channel, 0, 0, after) && !memcmp(before, after, 32));
|
||
}
|
||
int messages = message_count(inst, channel); unsigned events = changed_events;
|
||
assert(!merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, channel, 0, 0, before));
|
||
fail_ack_allocation = 1; result = command(inst, request, -1); u_free(result);
|
||
assert(!fail_ack_allocation && changed_events == events && message_count(inst, channel) == messages);
|
||
assert(!merkle_sync_read_hash(inst, MT_DATA_FEEDBACK, channel, 0, 0, after) && !memcmp(before, after, 32));
|
||
}
|
||
chat_event_set_handler(inst, NULL);
|
||
printf("PASS: %u poll SQL faults,8 post-commit read failures,2 pre-commit allocation failures; state/events preserved, read-only retry, complete emoji text\n", cases);
|
||
}
|
||
|
||
/* Проверяем границы, каноничность и невозможность интерпретации битого UTF-8. */
|
||
static void codecs(void) {
|
||
struct chat_poll poll = { .mode = CHAT_POLL_SINGLE, .allow_custom_answer = 1, .option_count = 2 }, decoded;
|
||
strcpy(poll.question, "Вопрос \"с кавычками\"\nи переносом"); strcpy(poll.options[0], "Да"); strcpy(poll.options[1], "Нет");
|
||
uint8_t wire[CHAT_POLL_WIRE_MAX]; int n = chat_poll_encode(&poll, wire, sizeof(wire)); assert(n > 0);
|
||
assert(chat_poll_decode(wire, n, &decoded) == 0 && !memcmp(&poll, &decoded, sizeof(poll)));
|
||
for (int i = 0; i < n; i++) assert(chat_poll_decode(wire, i, &decoded) < 0);
|
||
uint8_t saved = wire[2]; wire[2] = 2; assert(chat_poll_decode(wire, n, &decoded) < 0); wire[2] = saved;
|
||
wire[n] = 0; assert(chat_poll_decode(wire, n + 1, &decoded) < 0);
|
||
strcpy(poll.options[1], poll.options[0]); assert(chat_poll_encode(&poll, wire, sizeof(wire)) < 0);
|
||
poll.options[1][0] = (char)0xc0; poll.options[1][1] = (char)0x80; poll.options[1][2] = 0;
|
||
assert(chat_poll_encode(&poll, wire, sizeof(wire)) < 0);
|
||
struct chat_poll_answer answer = { .count = 2, .choices = {3, 1} }, parsed;
|
||
strcpy(answer.custom, "Свой ответ");
|
||
n = chat_answer_encode(&answer, wire, sizeof(wire)); assert(n > 0);
|
||
assert(chat_answer_decode(wire, n, &parsed) == 0 && parsed.choices[0] == 1 && parsed.choices[1] == 3);
|
||
wire[1] = 3; wire[2] = 1; assert(chat_answer_decode(wire, n, &parsed) < 0);
|
||
answer.choices[1] = 3; assert(chat_answer_encode(&answer, wire, sizeof(wire)) < 0);
|
||
memset(&answer, 0, sizeof(answer)); assert(chat_answer_encode(&answer, wire, sizeof(wire)) == 0);
|
||
assert(chat_answer_decode(NULL, 0, &parsed) == 0 && !parsed.count);
|
||
uint8_t empty[] = {0, 0, 0}; assert(chat_answer_decode(empty, sizeof(empty), &parsed) < 0);
|
||
uint8_t ids[] = {5, 0}, read[CHAT_REACTION_SELECTED_MAX]; size_t count;
|
||
assert(chat_reactions_encode(ids, 2, wire, sizeof(wire)) < 0);
|
||
uint8_t multi[] = {2, 0, 5}; assert(chat_reactions_decode(multi, sizeof(multi), read, &count) < 0);
|
||
n = chat_reactions_encode(ids, 1, wire, sizeof(wire)); assert(n == 2);
|
||
assert(chat_reactions_decode(wire, n, read, &count) == 0 && count == 1 && read[0] == 5);
|
||
wire[1] = CHAT_REACTION_COUNT; assert(chat_reactions_decode(wire, n, read, &count) < 0);
|
||
assert(chat_reactions_encode(NULL, 0, wire, sizeof(wire)) == 0);
|
||
puts("PASS: canonical codecs, truncation, duplicate choices and UTF-8 rejection");
|
||
}
|
||
|
||
int main(void) {
|
||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); codecs();
|
||
struct UASYNC* ua = uasync_create(); assert(ua);
|
||
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua,
|
||
"[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
|
||
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\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");
|
||
assert(inst && utun_core_start(inst) == 0 && chat_service_start(inst) == 0);
|
||
chat_core_create_channel_auto(inst, "Poll tests");
|
||
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);
|
||
assert(sqlite3_step(st) == SQLITE_ROW); char channel[21]; snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0));
|
||
sqlite3_finalize(st);
|
||
char request[4096], id[65]; uint8_t message_id[32];
|
||
snprintf(request, sizeof(request),
|
||
"{\"cmd\":\"poll_create\",\"ch\":\"%s\",\"question\":\"Куда? \\\"Да\\\"\\nНовая строка\","
|
||
"\"mode\":\"single\",\"options\":[\"Домой\",\"В парк\"],\"allow_custom_answer\":false}", channel);
|
||
char* result = command(inst, request, 0); json_string(db, result, "$.message_id", id, sizeof(id));
|
||
assert(sc_hex_to_binary(id, message_id, 32) == 0);
|
||
assert(json_number(db, result, "$.poll.voters") == 0);
|
||
char question[1024]; json_string(db, result, "$.poll.question", question, sizeof(question));
|
||
assert(!strcmp(question, "Куда? \"Да\"\nНовая строка")); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\",\"choices\":[0]}", channel, id);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.poll.voters") == 1);
|
||
assert(json_number(db, result, "$.poll.options[0].count") == 1 && json_number(db, result, "$.poll.ver") == 1); u_free(result);
|
||
snprintf(request, sizeof(request),
|
||
"{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\",\"choices\":[0,1]}", channel, id);
|
||
result = command(inst, request, -1); u_free(result);
|
||
snprintf(request, sizeof(request),
|
||
"{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\",\"custom\":\"Свой вариант\"}", channel, id);
|
||
result = command(inst, request, -1); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\",\"choices\":[1]}", channel, id);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.poll.options[0].count") == 0);
|
||
assert(json_number(db, result, "$.poll.options[1].count") == 1 && json_number(db, result, "$.poll.ver") == 2); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\"}", channel, id);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.poll.voters") == 0);
|
||
assert(json_number(db, result, "$.poll.ver") == 3); u_free(result);
|
||
puts("PASS: single-choice rules, disabled custom answer, replacement and withdrawal");
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":0}", channel, id);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.reactions[0].count") == 1); u_free(result);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.reaction_ver") == 2); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":0}", channel, id);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.reactions[0].id") == 0); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":4}", channel, id);
|
||
result = command(inst, request, 0);
|
||
assert(json_number(db, result, "$.reactions[0].id") == 4 && json_number(db, result, "$.reaction_ver") == 4);
|
||
assert(json_number(db, result, "$.reactions[1].count") == 0); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":0,\"on\":false}", channel, id);
|
||
result = command(inst, request, 0);
|
||
assert(json_number(db, result, "$.reactions[0].id") == 4 && json_number(db, result, "$.reaction_ver") == 4); u_free(result);
|
||
snprintf(request, sizeof(request), "{\"cmd\":\"react\",\"ch\":\"%s\",\"message_id\":\"%s\",\"emoji\":4}", channel, id);
|
||
result = command(inst, request, 0); assert(json_number(db, result, "$.reactions[0].count") == 0);
|
||
assert(json_number(db, result, "$.reaction_ver") == 5); u_free(result);
|
||
puts("PASS: selecting another emoji replaces the previous one; unselected withdrawal is inert; repeat click cancels");
|
||
sql_faults(inst, channel, id, message_id);
|
||
snprintf(request, sizeof(request),
|
||
"{\"cmd\":\"poll_create\",\"ch\":\"%s\",\"question\":\"Выбор\",\"mode\":\"multiple\","
|
||
"\"options\":[\"Первый\",\"Второй\"],\"allow_custom_answer\":true}", channel);
|
||
result = command(inst, request, 0); json_string(db, result, "$.message_id", id, sizeof(id)); u_free(result);
|
||
assert(sc_hex_to_binary(id, message_id, 32) == 0);
|
||
snprintf(request, sizeof(request),
|
||
"{\"cmd\":\"vote\",\"ch\":\"%s\",\"message_id\":\"%s\",\"choices\":[1,0],\"custom\":\"Мой \\\"ответ\\\"\"}", channel, id);
|
||
result = command(inst, request, 0);
|
||
assert(json_number(db, result, "$.poll.voters") == 1 && json_number(db, result, "$.poll.custom_count") == 1);
|
||
assert(json_number(db, result, "$.poll.options[0].percent") == 100 && json_number(db, result, "$.poll.options[1].percent") == 100);
|
||
u_free(result);
|
||
/* Исходное сообщение временно отсутствует: ответ остаётся в Merkle, но не считается. */
|
||
char sql[512]; snprintf(sql, sizeof(sql), "CREATE TEMP TABLE poll_backup AS SELECT * FROM msg_%s", channel);
|
||
assert(sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK);
|
||
snprintf(sql, sizeof(sql), "DELETE FROM msg_%s WHERE message_id=?", channel);
|
||
assert(sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK);
|
||
sqlite3_bind_blob(st, 1, message_id, 32, SQLITE_STATIC); assert(sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st);
|
||
struct chat_feedback_record answer;
|
||
assert(chat_feedback_get(inst, channel, message_id, inst->node_id, CHAT_FEEDBACK_ANSWER, &answer) == 1);
|
||
answer.ver++; uint8_t body[FB_WIRE_MAX + 32]; int bytes = fb_signed_bytes(strtoull(channel, NULL, 10), &answer, body);
|
||
assert(bytes > 0 && sc_ed25519_sign(inst->my_ed25519_privkey, body, bytes, answer.signature) == SC_OK);
|
||
assert(chat_feedback_apply(inst, channel, &answer) == 1);
|
||
assert(chat_feedback_snapshot(inst, channel, message_id, &result) == 0 && json_number(db, result, "$.pending") == 1);
|
||
u_free(result);
|
||
snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO msg_%s SELECT * FROM poll_backup", channel);
|
||
assert(sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK); chat_feedback_check_message(inst, channel, message_id);
|
||
assert(chat_feedback_snapshot(inst, channel, message_id, &result) == 0 && json_number(db, result, "$.pending") == 0);
|
||
assert(json_number(db, result, "$.poll.voters") == 1 && json_number(db, result, "$.poll.ver") == 2); u_free(result);
|
||
/* Неверный подписанный ответ синхронизируется, но исключается из результатов. */
|
||
struct chat_poll_answer wrong = { .count = 1, .choices = {7} };
|
||
answer.data_len = chat_answer_encode(&wrong, answer.data, sizeof(answer.data)); answer.ver++;
|
||
bytes = fb_signed_bytes(strtoull(channel, NULL, 10), &answer, body);
|
||
assert(sc_ed25519_sign(inst->my_ed25519_privkey, body, bytes, answer.signature) == SC_OK);
|
||
assert(chat_feedback_apply(inst, channel, &answer) == 1);
|
||
assert(chat_feedback_snapshot(inst, channel, message_id, &result) == 0 && json_number(db, result, "$.poll.voters") == 0);
|
||
u_free(result);
|
||
char listing[16384]; size_t len;
|
||
assert(chat_core_get_messages_json(inst, channel, 20, 0, listing, sizeof(listing), &len) == 0 && strstr(listing, id));
|
||
char tiny[8]; assert(chat_core_get_messages_json(inst, channel, 20, 0, tiny, sizeof(tiny), &len) < 0 && !len);
|
||
result = command(inst, "{\"cmd\":\"reaction_catalog\"}", 0);
|
||
assert(json_number(db, result, "$.catalog[0].id") == 0); u_free(result);
|
||
puts("PASS: multi-choice custom answers, delayed source, invalid signed answer and complete message listing");
|
||
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: poll API and codecs");
|
||
return 0;
|
||
}
|