Files
utun2/tests/test_chat_poll.c
T

163 lines
13 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>
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;
}
/* Проверяем границы, каноничность и невозможность интерпретации битого 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");
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;
}