Files
utun2/tests/test_chat_poll.c
T

310 lines
23 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.
/* Кодеки и пользовательский 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;
}