2 changed files with 152 additions and 0 deletions
@ -0,0 +1,147 @@
|
||||
/* Кодеки и пользовательский 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; |
||||
n = chat_reactions_encode(ids, 2, wire, sizeof(wire)); assert(n == 3); |
||||
assert(chat_reactions_decode(wire, n, read, &count) == 0 && count == 2 && read[0] == 0 && read[1] == 5); |
||||
wire[2] = 0; 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\":\"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; |
||||
} |
||||
Loading…
Reference in new issue