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