/* * 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")); } /* * 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.")); } /* * 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(); } /* * Test: messages from the SAME bare JID but with DIFFERENT resources are * stored together in one contact's history AND remain distinguishable on * reopen — the resource part of the sender JID must be preserved per row, * not collapsed. * * Flow: * 1. /history on * 2. buddy1@localhost/phone sends msg A * 3. buddy1@localhost/laptop sends msg B * 4. buddy1@localhost/tablet sends msg C * 5. Close the auto-opened chat window so its in-memory buffer is gone. * 6. /msg buddy1@localhost — chatwin_new() rehydrates from DB. * 7. All three bodies must reappear, AND the resource markers * "/phone", "/laptop", "/tablet" must each be visible on at least one * history line (PREF_RESOURCE_MESSAGE is on by default in the test * profile, so the renderer prefixes each line with "Buddy1/"). */ void message_db_history_multi_resource(void** state) { prof_connect(); prof_input("/history on"); assert_true(prof_output_regex("Chat history enabled\\.")); stbbr_send( "" "multi-resource-A-from-phone" ""); assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); stbbr_send( "" "multi-resource-B-from-laptop" ""); assert_true(prof_output_exact("<< chat message: Buddy1/laptop (win 2)")); stbbr_send( "" "multi-resource-C-from-tablet" ""); assert_true(prof_output_exact("<< chat message: Buddy1/tablet (win 2)")); prof_input("/close 2"); assert_true(prof_output_exact("Closed window 2")); prof_input("/msg buddy1@localhost"); /* All three bodies must be present in history (single-contact aggregation) */ assert_true(prof_output_exact("multi-resource-A-from-phone")); assert_true(prof_output_exact("multi-resource-B-from-laptop")); assert_true(prof_output_exact("multi-resource-C-from-tablet")); /* Each resource must remain identifiable on rehydrate — proves we don't * lose per-row resource info when loading the same JID's chat back. */ assert_true(prof_output_regex("Buddy1/phone")); assert_true(prof_output_regex("Buddy1/laptop")); assert_true(prof_output_regex("Buddy1/tablet")); }