#include #include "prof_cmocka.h" #include #include #include #include #include #include "proftest.h" /* * Test: SQLite -> flat-file export preserves messages. * * Uses stamped messages to verify round-trip through export. * Timestamp preservation is verified by unit tests (test_database_export.c); * the history-load path returns NULL GDateTime for timestamps, so * functional tests verify content survival only. * * Flow: * 1. Connect (SQLite backend) * 2. Receive message from buddy1 with delay stamp * 3. Send outgoing message * 4. /close, /history export, /history switch flatfile * 5. Re-open chat -- history loaded from flat-file * 6. Verify message content survives cross-backend migration */ void export_sqlite_to_flatfile(void** state) { /* Phase 1: create message history in SQLite */ prof_connect(); /* Send outgoing message (timestamp = now) */ prof_input("/msg buddy1@localhost Hello from SQLite export test"); assert_true(prof_output_regex("me: .+Hello from SQLite export test")); /* Receive incoming message with a fixed delay timestamp (10:30 UTC) */ stbbr_send( "" "SQLite reply at 1030" "" ""); assert_true(prof_output_regex("Buddy1/res: .+SQLite reply at 1030")); /* Close chat window, return to console */ prof_input("/close"); /* Export SQLite history to flat-file format */ prof_input("/history export buddy1@localhost"); assert_true(prof_output_regex("Export complete: [0-9]+ message\\(s\\) exported\\.")); /* Phase 2: switch to flat-file backend and verify content survived */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); /* Open chat window -- history loads from flat-file backend */ prof_input("/msg buddy1@localhost"); /* Verify messages survived cross-backend migration */ assert_true(prof_output_regex("SQLite reply at 1030")); assert_true(prof_output_regex("Hello from SQLite export test")); } /* * Test: flat-file -> SQLite import preserves messages. * * Flow: * 1. Connect, switch to flat-file backend * 2. Receive message with delay stamp -- stored in flat-file * 3. Send outgoing message * 4. /close, switch to SQLite, /history import * 5. Open chat -- history loaded from SQLite * 6. Verify message content survives cross-backend migration */ void import_flatfile_to_sqlite(void** state) { /* Phase 1: connect and switch to flat-file backend */ prof_connect(); prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); /* Send outgoing message */ prof_input("/msg buddy1@localhost Hello from flatfile import test"); assert_true(prof_output_regex("me: .+Hello from flatfile import test")); /* Receive incoming message with a fixed delay timestamp (14:45 UTC) */ stbbr_send( "" "Flatfile reply at 1445" "" ""); assert_true(prof_output_regex("Buddy1/res: .+Flatfile reply at 1445")); /* Close chat window */ prof_input("/close"); /* Phase 2: switch back to SQLite and import */ prof_input("/history switch sqlite"); assert_true(prof_output_regex("Database backend switched to 'sqlite'\\.")); /* Import flat-file history into SQLite */ prof_input("/history import buddy1@localhost"); assert_true(prof_output_regex("Import complete: [0-9]+ message\\(s\\) imported\\.")); /* Open chat window -- history loads from SQLite with imported data */ prof_input("/msg buddy1@localhost"); /* Verify messages survived cross-backend migration */ assert_true(prof_output_regex("Flatfile reply at 1445")); assert_true(prof_output_regex("Hello from flatfile import test")); } /* * Test: re-export idempotency — export + import + re-export does not * duplicate messages. * * Flow: * 1. Connect (SQLite), send 2 messages, export to flatfile * 2. Switch to flatfile, verify 2 messages * 3. Switch back to SQLite, export again * 4. Switch to flatfile, reopen — still exactly 2 messages * (the dedup logic in export must prevent duplication) */ void export_idempotent_no_duplicates(void** state) { prof_connect(); /* Create 2 messages in SQLite */ prof_input("/msg buddy1@localhost idemp-msg-alpha"); assert_true(prof_output_regex("me: .+idemp-msg-alpha")); stbbr_send( "" "idemp-msg-beta" ""); assert_true(prof_output_regex("Buddy1/res: .+idemp-msg-beta")); prof_input("/close"); /* Export #1 */ prof_input("/history export buddy1@localhost"); assert_true(prof_output_regex("Export complete: 2 message\\(s\\) exported\\.")); /* Verify flatfile has the messages */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("idemp-msg-alpha")); assert_true(prof_output_exact("idemp-msg-beta")); prof_input("/close"); /* Switch back to SQLite and export again (re-export) */ prof_input("/history switch sqlite"); assert_true(prof_output_regex("Database backend switched to 'sqlite'\\.")); prof_input("/history export buddy1@localhost"); /* Dedup: 0 new messages exported (all already present in flatfile) */ assert_true(prof_output_regex("Export complete: 0 message\\(s\\) exported\\.")); /* Verify flatfile still has exactly 2, not 4 */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("idemp-msg-alpha")); assert_true(prof_output_exact("idemp-msg-beta")); } /* * Test: LMC (Last Message Correction, XEP-0308) survives export. * * A corrected message in SQLite should appear as corrected in the * flat-file after export — the original body must be absent and the * corrected body must be present. */ void export_lmc_correction_survives(void** state) { prof_connect(); prof_input("/history on"); assert_true(prof_output_regex("Chat history enabled\\.")); /* Original message */ stbbr_send( "" "lmc-export-before-fix" ""); assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); /* Correction replacing the original */ stbbr_send( "" "lmc-export-after-fix" "" ""); /* Sync barrier */ stbbr_send( "" "lmc-export-sync" ""); assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)")); prof_input("/close all"); assert_true(prof_output_exact("Closed 2 windows.")); /* Export to flatfile */ prof_input("/history export buddy1@localhost"); assert_true(prof_output_regex("Export complete: [0-9]+ message\\(s\\) exported\\.")); /* Switch to flatfile and verify correction survived */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("lmc-export-after-fix")); /* Original should be gone (correction was applied in SQLite) */ prof_timeout(3); assert_false(prof_output_exact("lmc-export-before-fix")); prof_timeout_reset(); } /* * Test: /history switch preserves old backend data. * * Switching from SQLite to flatfile should not destroy SQLite data. * Switching back should recover the original messages. * * Uses incoming messages received on the console so the body is never * rendered to the terminal. The negative check (flatfile is empty) * is performed BEFORE the positive check (SQLite has the message) * so that the body text has not yet entered the cumulative buffer. */ void switch_preserves_old_backend_data(void** state) { prof_connect(); prof_input("/history on"); assert_true(prof_output_regex("Chat history enabled\\.")); /* Receive incoming on console — body NOT rendered to terminal */ stbbr_send( "" "switch-preserve-sqlite-42" ""); assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); /* Close window (destroy in-memory buffer) */ prof_input("/close 2"); assert_true(prof_output_exact("Closed window 2")); /* Switch to flatfile BEFORE verifying SQLite — body is still * absent from the cumulative terminal buffer at this point */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); /* Flatfile is empty — body must NOT appear (and it has never * been rendered yet, so the cumulative buffer is clean) */ prof_input("/msg buddy1@localhost"); prof_timeout(3); assert_false(prof_output_exact("switch-preserve-sqlite-42")); prof_timeout_reset(); prof_input("/close"); /* Switch back to SQLite — original data should be intact */ prof_input("/history switch sqlite"); assert_true(prof_output_regex("Database backend switched to 'sqlite'\\.")); /* Now the body is rendered for the first time from DB history */ prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("switch-preserve-sqlite-42")); } /* * Test: global export (no JID argument) exports all contacts. * * Creates messages for buddy1 and buddy2 in SQLite, then exports all. * Both contacts' messages must appear in flatfile after switch. */ void export_all_contacts(void** state) { prof_connect(); /* Messages to buddy1 */ prof_input("/msg buddy1@localhost global-export-buddy1-aaa"); assert_true(prof_output_regex("me: .+global-export-buddy1-aaa")); prof_input("/close"); /* Messages to buddy2 */ prof_input("/msg buddy2@localhost global-export-buddy2-bbb"); assert_true(prof_output_regex("me: .+global-export-buddy2-bbb")); prof_input("/close"); /* Export all (no JID) */ prof_input("/history export"); assert_true(prof_output_regex("Exporting all SQLite history to flat-file\\.\\.\\.")); assert_true(prof_output_regex("Export complete: [0-9]+ message\\(s\\) exported\\.")); /* Switch to flatfile */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); /* Verify buddy1 */ prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("global-export-buddy1-aaa")); prof_input("/close"); /* Verify buddy2 */ prof_input("/msg buddy2@localhost"); assert_true(prof_output_exact("global-export-buddy2-bbb")); } /* * Test: double import deduplication. * * Importing the same flatfile twice should not create duplicate messages * in SQLite. * * Flow: * 1. Create messages in flatfile * 2. Switch to SQLite, import, verify count * 3. Import again — count should be 0 (all deduplicated) * 4. Verify only one copy of each message in history */ void import_double_dedup(void** state) { prof_connect(); /* Create messages in flatfile */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); prof_input("/msg buddy1@localhost dedup-import-msg-one"); assert_true(prof_output_regex("me: .+dedup-import-msg-one")); stbbr_send( "" "dedup-import-msg-two" ""); assert_true(prof_output_regex("Buddy1/res: .+dedup-import-msg-two")); prof_input("/close"); /* Switch to SQLite and import #1 */ prof_input("/history switch sqlite"); assert_true(prof_output_regex("Database backend switched to 'sqlite'\\.")); prof_input("/history import buddy1@localhost"); assert_true(prof_output_regex("Import complete: 2 message\\(s\\) imported\\.")); /* Import #2 — should be all duplicates */ prof_input("/history import buddy1@localhost"); assert_true(prof_output_regex("Import complete: 0 message\\(s\\) imported\\.")); /* Verify messages exist and are not doubled */ prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("dedup-import-msg-one")); assert_true(prof_output_exact("dedup-import-msg-two")); } /* * Test: /history verify on data that went through export/import. * * After migrating SQLite → flatfile via export, /history verify should * complete without errors on the resulting flat-files. */ void verify_after_export(void** state) { prof_connect(); /* Create some messages in SQLite */ prof_input("/msg buddy1@localhost verify-after-export-msg"); assert_true(prof_output_regex("me: .+verify-after-export-msg")); stbbr_send( "" "verify-export-reply" ""); assert_true(prof_output_regex("Buddy1/res: .+verify-export-reply")); prof_input("/close"); /* Export to flatfile */ prof_input("/history export buddy1@localhost"); assert_true(prof_output_regex("Export complete: [0-9]+ message\\(s\\) exported\\.")); /* Switch to flatfile and verify integrity */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); prof_input("/history verify buddy1@localhost"); /* Accept "no issues found" or "0 error(s)" — minor info notes are OK */ assert_true(prof_output_regex("Verification complete")); } /* * Test: each backend maintains its own independent messages. * * After switching backends, new messages go to the new backend only. * Switching back reveals the original backend's messages unchanged. * * Uses incoming messages on the console (body never rendered) and two * different contacts (buddy1 for SQLite, buddy2 for flatfile). * * All negative (assert_false) checks are done BEFORE any positive * check renders a body, because prof_output_exact searches the * cumulative terminal buffer. */ void switch_backends_independent_messages(void** state) { prof_connect(); prof_input("/history on"); assert_true(prof_output_regex("Chat history enabled\\.")); /* Phase 1: receive buddy1 msg while on SQLite — body NOT rendered */ stbbr_send( "" "indep-sqlite-msg-7k" ""); assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)")); prof_input("/close 2"); assert_true(prof_output_exact("Closed window 2")); /* Phase 2: switch to flatfile, receive buddy2 msg */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); stbbr_send( "" "indep-flatfile-msg-9m" ""); assert_true(prof_output_exact("<< chat message: Buddy2/laptop (win 2)")); prof_input("/close 2"); assert_true(prof_output_exact("Closed window 2")); /* --- Negative checks first (neither body has been rendered yet) --- */ /* buddy1 on flatfile — should be empty (received on SQLite) */ prof_input("/msg buddy1@localhost"); prof_timeout(3); assert_false(prof_output_exact("indep-sqlite-msg-7k")); prof_timeout_reset(); prof_input("/close"); /* Switch to SQLite for buddy2 negative check */ prof_input("/history switch sqlite"); assert_true(prof_output_regex("Database backend switched to 'sqlite'\\.")); /* buddy2 on SQLite — should be empty (received on flatfile) */ prof_input("/msg buddy2@localhost"); prof_timeout(3); assert_false(prof_output_exact("indep-flatfile-msg-9m")); prof_timeout_reset(); prof_input("/close"); /* --- Positive checks (bodies rendered for the first time) --- */ /* buddy1 on SQLite — should have the sqlite msg */ prof_input("/msg buddy1@localhost"); assert_true(prof_output_exact("indep-sqlite-msg-7k")); prof_input("/close"); /* Switch to flatfile for buddy2 positive check */ prof_input("/history switch flatfile"); assert_true(prof_output_regex("Database backend switched to 'flatfile'\\.")); /* buddy2 on flatfile — should have the flatfile msg */ prof_input("/msg buddy2@localhost"); assert_true(prof_output_exact("indep-flatfile-msg-9m")); } /* * Test: export of a contact with no messages doesn't crash and reports 0. * * buddy2 has no message history — export should succeed gracefully * with 0 messages exported. */ void export_empty_contact(void** state) { prof_connect(); /* buddy2 has never exchanged messages */ prof_input("/history export buddy2@localhost"); assert_true(prof_output_regex("Export complete: 0 message\\(s\\) exported\\.")); }