From 21fae2c134c84e295e563a81720924143e1709cf Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Thu, 19 Feb 2026 16:19:47 +0300 Subject: [PATCH] tests: add functional tests for DB message persistence, rebalance groups Add backend-agnostic functional tests for database message persistence. All tests work with both SQLite and flat-file backends. New tests in test_history.c: - message_db_history_on_reopen: basic incoming write+read round-trip - message_db_history_multiple: 3 messages from different resources - message_db_history_contact_isolation: buddy1 msg absent from buddy2 - message_db_history_special_chars: XML entities survive decode+DB - message_db_history_outgoing: sent message persists across reopen - message_db_history_dialog: outgoing + incoming both in history - message_db_history_empty: no crash on contact with no history - message_db_history_long_message: 1000+ char body not truncated - message_db_history_newline: embedded LF stored correctly - message_db_history_service_chars: backslash pipe percent braces etc - message_db_history_verify: /history verify (ifdef HAVE_HISTORY_VERIFY) - message_db_history_lmc: XEP-0308 correction replaces original in DB Rebalance test groups for parallel execution (19/22/22/21): - Group 1: Connect, Ping, Rooms, Software, LastActivity - Group 2: Message, Receipts, Roster, DB History - Group 3: Chat Session, Presence, Disconnect - Group 4: MUC, Carbons --- Makefile.am | 1 + tests/functionaltests/functionaltests.c | 50 ++- tests/functionaltests/test_history.c | 475 ++++++++++++++++++++++++ tests/functionaltests/test_history.h | 14 + 4 files changed, 526 insertions(+), 14 deletions(-) create mode 100644 tests/functionaltests/test_history.c create mode 100644 tests/functionaltests/test_history.h diff --git a/Makefile.am b/Makefile.am index 38abbbea..23506d29 100644 --- a/Makefile.am +++ b/Makefile.am @@ -189,6 +189,7 @@ functionaltest_sources = \ tests/functionaltests/test_software.c tests/functionaltests/test_software.h \ tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \ tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \ + tests/functionaltests/test_history.c tests/functionaltests/test_history.h \ tests/functionaltests/test_lastactivity.c tests/functionaltests/test_lastactivity.h \ tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \ tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \ diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index f26d1dc1..b9d297d2 100644 --- a/tests/functionaltests/functionaltests.c +++ b/tests/functionaltests/functionaltests.c @@ -15,9 +15,9 @@ * functional tests run less frequently than unit tests. * * Tests are organized into groups for better maintainability and parallel execution: - * Group 1: Connect, Ping, Autoping (fast), Rooms, Software, Last Activity - * Group 2: Message, Receipts, Roster, Chat Session - * Group 3: Presence, Disconnect, Autoping (slow) + * Group 1: Connect, Ping, Rooms, Software, Last Activity, Autoping + * Group 2: Message, Receipts, Roster, DB History + * Group 3: Chat Session, Presence, Disconnect, Disco * Group 4: MUC, Carbons * * Parallel execution: @@ -55,6 +55,7 @@ #include "test_lastactivity.h" #include "test_autoping.h" #include "test_disco.h" +#include "test_history.h" /* Macro to wrap each test with setup/teardown functions */ #define PROF_FUNC_TEST(test) cmocka_unit_test_setup_teardown(test, init_prof_test, close_prof_test) @@ -79,8 +80,9 @@ main(int argc, char* argv[]) fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group); /* ============================================================ - * GROUP 1: Connect, Ping, Rooms, Software + * GROUP 1: Connect, Ping, Rooms, Software, Last Activity * Basic XMPP session establishment and server queries + * (19 tests) * ============================================================ */ const struct CMUnitTest group1_tests[] = { /* Connection tests - verify login, roster, bookmarks */ @@ -124,8 +126,9 @@ main(int argc, char* argv[]) }; /* ============================================================ - * GROUP 2: Message, Receipts, Roster, Chat Session + * GROUP 2: Message, Receipts, Roster, DB History * Core messaging and contact management + * (23 tests) * ============================================================ */ const struct CMUnitTest group2_tests[] = { /* Basic message send/receive */ @@ -145,6 +148,29 @@ main(int argc, char* argv[]) PROF_FUNC_TEST(sends_remove_item_nick), PROF_FUNC_TEST(sends_nick_change), + /* Database persistence - message write+read round-trip */ + PROF_FUNC_TEST(message_db_history_on_reopen), + PROF_FUNC_TEST(message_db_history_multiple), + PROF_FUNC_TEST(message_db_history_contact_isolation), + PROF_FUNC_TEST(message_db_history_special_chars), + PROF_FUNC_TEST(message_db_history_outgoing), + PROF_FUNC_TEST(message_db_history_dialog), + PROF_FUNC_TEST(message_db_history_empty), + PROF_FUNC_TEST(message_db_history_long_message), + PROF_FUNC_TEST(message_db_history_newline), + PROF_FUNC_TEST(message_db_history_service_chars), +#ifdef HAVE_HISTORY_VERIFY + PROF_FUNC_TEST(message_db_history_verify), +#endif + PROF_FUNC_TEST(message_db_history_lmc), + }; + + /* ============================================================ + * GROUP 3: Chat Session, Presence, Disconnect + * Session routing, status management, clean teardown + * (22 tests) + * ============================================================ */ + const struct CMUnitTest group3_tests[] = { /* Chat session management - bare/full JID routing */ PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline), PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online), @@ -152,13 +178,8 @@ main(int argc, char* argv[]) PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid), PROF_FUNC_TEST(resets_to_barejid_after_presence_received), PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid), - }; - /* ============================================================ - * GROUP 3: Presence, Disconnect, Disco - * Online/away/xa/dnd/chat status management - * ============================================================ */ - const struct CMUnitTest group3_tests[] = { + /* Presence - online/away/xa/dnd/chat status management */ PROF_FUNC_TEST(presence_online), PROF_FUNC_TEST(presence_online_with_message), PROF_FUNC_TEST(presence_away), @@ -198,6 +219,7 @@ main(int argc, char* argv[]) /* ============================================================ * GROUP 4: MUC, Carbons * Multi-user chat and message synchronization + * (21 tests) * ============================================================ */ const struct CMUnitTest group4_tests[] = { /* MUC room join with various options - XEP-0045 */ @@ -240,9 +262,9 @@ main(int argc, char* argv[]) const struct CMUnitTest* tests; size_t count; } groups[] = { - { "Group 1: Connect/Ping/Rooms/Software/Autoping", group1_tests, ARRAY_SIZE(group1_tests) }, - { "Group 2: Message/Receipts/Roster/Session", group2_tests, ARRAY_SIZE(group2_tests) }, - { "Group 3: Presence/Disconnect/Disco", group3_tests, ARRAY_SIZE(group3_tests) }, + { "Group 1: Connect/Ping/Rooms/Software/LastActivity/Autoping", group1_tests, ARRAY_SIZE(group1_tests) }, + { "Group 2: Message/Receipts/Roster/DBHistory", group2_tests, ARRAY_SIZE(group2_tests) }, + { "Group 3: Session/Presence/Disconnect/Disco", group3_tests, ARRAY_SIZE(group3_tests) }, { "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) }, }; const int num_groups = ARRAY_SIZE(groups); diff --git a/tests/functionaltests/test_history.c b/tests/functionaltests/test_history.c new file mode 100644 index 00000000..86fa0155 --- /dev/null +++ b/tests/functionaltests/test_history.c @@ -0,0 +1,475 @@ +/* + * test_history.c + * + * Functional test for database message persistence. + * Verifies that a received chat message is written to the database + * and loaded back as history when the chat window is reopened. + * + * These tests only exercise the public log_database API through normal + * profanity UI operations, so they work with any storage backend. + */ + +#include +#include +#include +#include "prof_cmocka.h" +#include +#include + +#include + +#include "proftest.h" + +/* + * Test: message written to DB is loaded as history on chat window reopen. + * + * Flow: + * 1. Connect and enable /history on (enables PREF_CHLOG + PREF_HISTORY) + * 2. Receive a chat message while on the console window — the message + * body is NOT rendered to the terminal (only a console notification + * "<< chat message: ... (win N)" appears), but the message IS + * written to the database via chat_log_msg_in() + * 3. Close the auto-created chat window (destroying its in-memory buffer) + * 4. Reopen the chat window with /msg — chatwin_new() calls + * _chatwin_history() which reads from log_database_get_previous_chat() + * 5. Assert the message body appears in the terminal output, proving + * it was persisted to and read back from the database + */ +void +message_db_history_on_reopen(void **state) +{ + prof_connect(); + + /* Enable chat logging and history display */ + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Receive a message while on the console window. + * The body text is NOT printed to the visible terminal — only the + * console notification appears. The message is logged to the DB. */ + stbbr_send( + "" + "persistence-roundtrip-check-42" + ""); + + /* Wait for the console notification (proves message was received). + * buddy1@localhost is in the roster as "Buddy1", and PREF_RESOURCE_MESSAGE + * is on by default, so the display name is "Buddy1/phone". */ + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + /* Close the chat window that was auto-created for the incoming message. + * This destroys its in-memory buffer so the text is gone from UI. */ + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + /* Reopen the chat window — chatwin_new() will load history from DB. + * The message body should now appear on screen for the first time. */ + prof_input("/msg buddy1@localhost"); + + /* The history-loaded message must contain the body we sent earlier. + * Since the body was never rendered to the terminal before (it was + * only in the hidden window's ncurses buffer which was destroyed), + * finding it now proves the database write+read round-trip works. */ + assert_true(prof_output_exact("persistence-roundtrip-check-42")); +} + +/* + * Test: multiple messages from the same contact are all preserved. + * + * Uses different resources so each console notification is unique and + * we can reliably synchronise on each delivery before proceeding. + */ +void +message_db_history_multiple(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Three messages from different resources → distinct notifications */ + stbbr_send( + "" + "db-multi-first-aaa" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + stbbr_send( + "" + "db-multi-second-bbb" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/laptop (win 2)")); + + stbbr_send( + "" + "db-multi-third-ccc" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/tablet (win 2)")); + + /* Close and reopen */ + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + prof_input("/msg buddy1@localhost"); + + /* All three must be present in history */ + assert_true(prof_output_exact("db-multi-first-aaa")); + assert_true(prof_output_exact("db-multi-second-bbb")); + assert_true(prof_output_exact("db-multi-third-ccc")); +} + +/* + * Test: messages are stored per-contact — buddy1's history does not + * leak into buddy2's window. + * + * 1. Receive from buddy1 and buddy2 while on console (bodies hidden) + * 2. Close all chat windows + * 3. Open buddy1 → verify buddy1's body present, buddy2's body absent + */ +void +message_db_history_contact_isolation(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Receive from buddy1 */ + stbbr_send( + "" + "isolation-buddy1-only-xyzzy" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + /* Receive from buddy2 */ + stbbr_send( + "" + "isolation-buddy2-only-plugh" + ""); + assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)")); + + /* Close all chat windows at once */ + prof_input("/close all"); + assert_true(prof_output_exact("Closed 2 windows.")); + + /* Open buddy1 — history should contain ONLY buddy1's message */ + prof_input("/msg buddy1@localhost"); + assert_true(prof_output_exact("isolation-buddy1-only-xyzzy")); + + /* buddy2's body was never printed to the terminal (received on + * console), so finding it now would mean cross-contact leakage */ + prof_timeout(3); + assert_false(prof_output_exact("isolation-buddy2-only-plugh")); + prof_timeout_reset(); +} + +/* + * Test: special characters survive the DB write+read round-trip. + * + * The XMPP body uses XML entities (& < > ") which the + * XML parser decodes before storage. The DB must preserve the decoded + * characters and return them unchanged. + */ +void +message_db_history_special_chars(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + stbbr_send( + "" + "chars: & <tag> "quoted"" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + prof_input("/msg buddy1@localhost"); + + /* The decoded text must appear exactly as-is */ + assert_true(prof_output_exact("chars: & \"quoted\"")); +} + +/* + * Test: outgoing message (sent via /msg) persists in DB. + * + * NOTE: The outgoing body IS rendered to the terminal when sent. Since + * prof_output_exact searches the cumulative buffer, the assertion alone + * does not conclusively prove DB persistence. However, combined with + * the incoming tests, this verifies the end-to-end outgoing write path + * and catches crashes in the outgoing DB code. + */ +void +message_db_history_outgoing(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Send outgoing — opens and focuses chat window */ + prof_input("/msg buddy1@localhost db-outgoing-sent-42"); + assert_true(prof_output_regex("me: .+db-outgoing-sent-42")); + + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + /* Reopen — history should load the outgoing message */ + prof_input("/msg buddy1@localhost"); + assert_true(prof_output_exact("db-outgoing-sent-42")); +} + +/* + * Test: a dialog (outgoing + incoming) is fully preserved in history. + * + * Sends an outgoing message, then closes the window. A reply arrives + * while on the console (body never rendered). After reopen, both sides + * of the conversation appear — proving the incoming read from DB works + * and the outgoing write path completed without error. + */ +void +message_db_history_dialog(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Phase 1: send outgoing (renders to terminal) */ + prof_input("/msg buddy1@localhost dialog-out-msg-42"); + assert_true(prof_output_regex("me: .+dialog-out-msg-42")); + + /* Return to console */ + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + /* Phase 2: receive reply on console (body NOT in terminal buffer) */ + stbbr_send( + "" + "dialog-in-reply-77" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + /* Close the auto-created window */ + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + /* Phase 3: reopen — history should have both directions */ + prof_input("/msg buddy1@localhost"); + + /* Incoming body conclusively proves DB round-trip */ + assert_true(prof_output_exact("dialog-in-reply-77")); + /* Outgoing body is consistent with DB persistence */ + assert_true(prof_output_exact("dialog-out-msg-42")); +} + +/* + * Test: opening a chat window for a contact with no prior messages + * does not crash and the window is usable. + */ +void +message_db_history_empty(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* buddy2 has never exchanged messages — history is empty */ + prof_input("/msg buddy2@localhost"); + assert_true(prof_output_exact("buddy2@localhost")); + + /* Verify the window is functional by sending a message */ + prof_input("empty-history-smoke-test"); + assert_true(prof_output_regex("me: .+empty-history-smoke-test")); +} + +/* + * Test: a long message (1000+ characters) is not truncated by the DB. + */ +void +message_db_history_long_message(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Build a 1020-character body: 1000 x 'A' + unique suffix */ + char body[1100]; + memset(body, 'A', 1000); + strcpy(body + 1000, "-long-db-end-MARKER"); + + char xml[1500]; + snprintf(xml, sizeof(xml), + "" + "%s" + "", body); + stbbr_send(xml); + + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + prof_input("/msg buddy1@localhost"); + assert_true(prof_output_exact("-long-db-end-MARKER")); +} + +/* + * Test: message body containing embedded newlines (LF) is stored and + * loaded correctly. + * + * Uses XML character reference for literal newline in body. + */ +void +message_db_history_newline(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + stbbr_send( + "" + "nl-first-line-7k nl-second-line-9m nl-third-line-2p" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + prof_input("/msg buddy1@localhost"); + /* Each fragment must survive the round-trip */ + assert_true(prof_output_exact("nl-first-line-7k")); + assert_true(prof_output_exact("nl-second-line-9m")); + assert_true(prof_output_exact("nl-third-line-2p")); +} + +/* + * Test: characters that could break storage format (backslash, pipe, + * percent, braces, brackets, equals) survive DB round-trip. + */ +void +message_db_history_service_chars(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + stbbr_send( + "" + "svc: a\\b c|d e%f {g} [h] i=j" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + prof_input("/close 2"); + assert_true(prof_output_exact("Closed window 2")); + + prof_input("/msg buddy1@localhost"); + assert_true(prof_output_exact("svc: a\\b c|d e%f {g} [h] i=j")); +} + +#ifdef HAVE_HISTORY_VERIFY +/* + * Test: /history verify reports no issues after normal message writes. + */ +void +message_db_history_verify(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Write a few messages to create non-trivial DB state */ + stbbr_send( + "" + "verify-msg-one" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + stbbr_send( + "" + "verify-msg-two" + ""); + assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)")); + + /* Run verify — should find no issues */ + prof_input("/history verify"); + assert_true(prof_output_exact("Verification complete: no issues found.")); +} +#endif /* HAVE_HISTORY_VERIFY */ + +/* + * Test: LMC (Last Message Correction, XEP-0308) — incoming correction + * replaces the original in DB history. + * + * Both messages arrive while focused on the console so their bodies + * are never rendered to the terminal. After reopen, only the corrected + * text should appear — the original should be absent. + */ +void +message_db_history_lmc(void **state) +{ + prof_connect(); + + prof_input("/history on"); + assert_true(prof_output_regex("Chat history enabled\\.")); + + /* Original message (body never displayed — received on console) */ + stbbr_send( + "" + "lmc-before-correction-aaa" + ""); + assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); + + /* Correction replacing the original (XEP-0308). + * Use buddy2 message as sync barrier to ensure correction is processed. */ + stbbr_send( + "" + "lmc-after-correction-zzz" + "" + ""); + + /* Sync barrier: send from buddy2, wait for its notification */ + stbbr_send( + "" + "lmc-sync-barrier" + ""); + assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)")); + + /* Close buddy1 and buddy2 windows */ + prof_input("/close all"); + assert_true(prof_output_exact("Closed 2 windows.")); + + /* Reopen buddy1 — should have corrected text only */ + prof_input("/msg buddy1@localhost"); + assert_true(prof_output_exact("lmc-after-correction-zzz")); + + /* Original body was never in the terminal buffer (received on console). + * Its absence in history proves the correction was applied in the DB. */ + prof_timeout(3); + assert_false(prof_output_exact("lmc-before-correction-aaa")); + prof_timeout_reset(); +} diff --git a/tests/functionaltests/test_history.h b/tests/functionaltests/test_history.h new file mode 100644 index 00000000..c2d0f040 --- /dev/null +++ b/tests/functionaltests/test_history.h @@ -0,0 +1,14 @@ +void message_db_history_on_reopen(void **state); +void message_db_history_multiple(void **state); +void message_db_history_contact_isolation(void **state); +void message_db_history_special_chars(void **state); +void message_db_history_outgoing(void **state); +void message_db_history_dialog(void **state); +void message_db_history_empty(void **state); +void message_db_history_long_message(void **state); +void message_db_history_newline(void **state); +void message_db_history_service_chars(void **state); +#ifdef HAVE_HISTORY_VERIFY +void message_db_history_verify(void **state); +#endif +void message_db_history_lmc(void **state);