/* * functionaltests.c * * Functional tests for CProof XMPP client. * Uses cmocka framework with stabber mock XMPP server. * * Each test is wrapped with PROF_FUNC_TEST macro which sets up * init_prof_test (starts stabber server and profanity client) * and close_prof_test (cleanup) as setup/teardown functions. * * NOTE: We restart client and server for each test to ensure complete * isolation. This prevents state leakage between tests (roster entries, * MUC rooms, presence subscriptions, etc.). While slower, it eliminates * flaky tests caused by leftover state. The overhead is acceptable since * functional tests run less frequently than unit tests. * * Tests are organized into groups balanced for parallel execution: * Group 1: Connect, Ping, Rooms, Software, Last Activity, Autoping, Disco, Roster * Group 2: Export/Import (core + bidirectional), Receipts * Group 3: Presence, Disconnect, DB History, Message, MUC Database * Group 4: Chat Session, MUC, Carbons, Migration Stress * * Parallel execution: * ./functionaltests - run all tests sequentially * ./functionaltests N - run group N only (N = 1..num_groups) * * For parallel execution, run multiple groups simultaneously: * ./functionaltests 1 & ./functionaltests 2 & ./functionaltests 3 & ./functionaltests 4 & wait */ #include #include #include #include #include #include "prof_cmocka.h" #include #include "config.h" #include "common.h" #include "proftest.h" #include "test_connect.h" #include "test_ping.h" #include "test_rooms.h" #include "test_presence.h" #include "test_message.h" #include "test_carbons.h" #include "test_chat_session.h" #include "test_receipts.h" #include "test_roster.h" #include "test_software.h" #include "test_muc.h" #include "test_disconnect.h" #include "test_lastactivity.h" #include "test_autoping.h" #include "test_disco.h" #include "test_history.h" #ifdef HAVE_SQLITE #include "test_export_import.h" #endif #include "test_ai.h" /* Macro to wrap each test with setup/teardown functions */ #define PROF_FUNC_TEST(test) \ { \ #test, test, init_prof_test, close_prof_test, #test \ } /* AI tests use a custom init that primes stabber via prof_connect so * stbbr_stop() in close_prof_test() doesn't hang on a never-connected * server thread. See ai_init_test() in test_ai.c. */ #define PROF_FUNC_TEST_AI(test) \ { \ #test, test, ai_init_test, close_prof_test, #test \ } #ifdef HAVE_SQLITE /* Custom init that sets a non-UTC timezone (POSIX TST-3 = UTC+3) */ static int init_tz_plus3_test(void** state) { prof_test_tz = "TST-3"; int ret = init_prof_test(state); prof_test_tz = "UTC"; return ret; } #endif int main(int argc, char* argv[]) { int group = 0; /* 0 = all groups */ if (argc > 1) { group = atoi(argv[1]); if (group < 1 || group > 5) { fprintf(stderr, "Usage: %s [group]\n", argv[0]); fprintf(stderr, " group: 1-5 to run specific group, or omit for all\n"); return 1; } } char group_env[16]; snprintf(group_env, sizeof(group_env), "%d", group); setenv("PROF_TEST_GROUP", group_env, 1); fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group); /* ============================================================ * GROUP 1: Connect, Ping, Rooms, Software, Last Activity, Disco, Roster * Basic XMPP session establishment, server queries, contact mgmt * ============================================================ */ const struct CMUnitTest group1_tests[] = { /* Connection tests - verify login, roster, bookmarks */ PROF_FUNC_TEST(connect_jid_requests_roster), PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster), PROF_FUNC_TEST(connect_jid_requests_bookmarks), PROF_FUNC_TEST(connect_bad_password), PROF_FUNC_TEST(connect_shows_presence_updates), /* Ping tests - XEP-0199 XMPP Ping */ PROF_FUNC_TEST(ping_server), PROF_FUNC_TEST(ping_server_not_supported), PROF_FUNC_TEST(ping_responds_to_server_request), PROF_FUNC_TEST(ping_jid), PROF_FUNC_TEST(ping_jid_not_supported), /* Room discovery - XEP-0045 */ PROF_FUNC_TEST(rooms_query), /* Software Version - XEP-0092 */ PROF_FUNC_TEST(send_software_version_request), PROF_FUNC_TEST(display_software_version_result), PROF_FUNC_TEST(shows_message_when_software_version_error), PROF_FUNC_TEST(display_software_version_result_when_from_domainpart), PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource), PROF_FUNC_TEST(display_software_version_result_in_chat), /* Last Activity - XEP-0012 */ PROF_FUNC_TEST(responds_to_last_activity_request), PROF_FUNC_TEST(last_activity_request_to_contact), /* Autoping command tests - fast, no waiting */ PROF_FUNC_TEST(autoping_set_interval), PROF_FUNC_TEST(autoping_set_zero_disables), PROF_FUNC_TEST(autoping_timeout_set), PROF_FUNC_TEST(autoping_timeout_zero_disables), /* Autoping slow tests - require sleep for timer triggers (~2s each) */ PROF_FUNC_TEST(autoping_sends_ping_after_interval), PROF_FUNC_TEST(autoping_server_not_supporting_ping), /* Service Discovery - XEP-0030 */ PROF_FUNC_TEST(disco_info_shows_identity), PROF_FUNC_TEST(disco_info_shows_features), PROF_FUNC_TEST(disco_info_to_server), PROF_FUNC_TEST(disco_info_to_jid), PROF_FUNC_TEST(disco_info_not_found), PROF_FUNC_TEST(disco_items_shows_items), PROF_FUNC_TEST(disco_items_empty_result), PROF_FUNC_TEST(disco_requires_connection), PROF_FUNC_TEST(disco_items_to_jid), PROF_FUNC_TEST(disco_info_empty_result), PROF_FUNC_TEST(disco_info_multiple_identities), PROF_FUNC_TEST(disco_info_without_name), PROF_FUNC_TEST(disco_items_without_name), PROF_FUNC_TEST(disco_info_service_unavailable), /* Roster management - add/remove/rename contacts */ PROF_FUNC_TEST(sends_new_item), PROF_FUNC_TEST(sends_new_item_nick), PROF_FUNC_TEST(sends_remove_item), PROF_FUNC_TEST(sends_remove_item_nick), PROF_FUNC_TEST(sends_nick_change), }; /* ============================================================ * GROUP 2: Export/Import (core + bidirectional), Receipts * Cross-backend migration, message timestamp round-trip * ============================================================ */ const struct CMUnitTest group2_tests[] = { #ifdef HAVE_SQLITE /* Export/Import — cross-backend migration (SQLite ↔ flat-file) */ PROF_FUNC_TEST(export_sqlite_to_flatfile), PROF_FUNC_TEST(import_flatfile_to_sqlite), PROF_FUNC_TEST(export_idempotent_no_duplicates), PROF_FUNC_TEST(export_lmc_correction_survives), PROF_FUNC_TEST(switch_preserves_old_backend_data), PROF_FUNC_TEST(export_all_contacts), PROF_FUNC_TEST(import_double_dedup), PROF_FUNC_TEST(verify_after_export), PROF_FUNC_TEST(switch_backends_independent_messages), PROF_FUNC_TEST(export_empty_contact), /* Bidirectional migration — split timelines merged across backends */ PROF_FUNC_TEST(bidirectional_merge_to_flatfile), PROF_FUNC_TEST(bidirectional_merge_to_sqlite), PROF_FUNC_TEST(migration_incremental_accumulation), PROF_FUNC_TEST(roundtrip_full_cycle), PROF_FUNC_TEST(export_timestamps_preserved), PROF_FUNC_TEST(import_timestamps_preserved), { "export_timestamps_non_utc", export_timestamps_non_utc, init_tz_plus3_test, close_prof_test, "export_timestamps_non_utc" }, #endif /* Message receipts - XEP-0184 */ PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid), PROF_FUNC_TEST(send_receipt_request), PROF_FUNC_TEST(send_receipt_on_request), }; /* ============================================================ * GROUP 3: Presence, Disconnect, DB History, Message, MUC Database * Status management, message persistence, MUC storage * ============================================================ */ 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), PROF_FUNC_TEST(presence_away_with_message), PROF_FUNC_TEST(presence_xa), PROF_FUNC_TEST(presence_xa_with_message), PROF_FUNC_TEST(presence_dnd), PROF_FUNC_TEST(presence_dnd_with_message), PROF_FUNC_TEST(presence_chat), PROF_FUNC_TEST(presence_chat_with_message), PROF_FUNC_TEST(presence_set_priority), PROF_FUNC_TEST(presence_includes_priority), PROF_FUNC_TEST(presence_keeps_status), PROF_FUNC_TEST(presence_received), PROF_FUNC_TEST(presence_missing_resource_defaults), /* Disconnect - clean session termination */ PROF_FUNC_TEST(disconnect_ends_session), /* 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), PROF_FUNC_TEST(message_db_history_verify), PROF_FUNC_TEST(message_db_history_lmc), PROF_FUNC_TEST(message_db_history_multi_resource), /* Basic message send/receive */ PROF_FUNC_TEST(message_send), PROF_FUNC_TEST(message_receive_console), PROF_FUNC_TEST(message_receive_chatwin), /* XEP-0359 disco gate for stanza-id trust */ PROF_FUNC_TEST(stanza_id_dedup_fires_when_server_announces_sid0), PROF_FUNC_TEST(stanza_id_not_trusted_when_server_does_not_announce_sid0), #ifdef HAVE_SQLITE /* MUC (groupchat) database — export/import of type="muc" messages */ PROF_FUNC_TEST(muc_export_sqlite_to_flatfile), PROF_FUNC_TEST(muc_import_flatfile_to_sqlite), PROF_FUNC_TEST(muc_export_timestamps_preserved), PROF_FUNC_TEST(muc_export_multiple_rooms), #endif }; /* ============================================================ * GROUP 4: Chat Session, MUC, Carbons, Migration Stress * Session routing, multi-user chat, message sync, stress tests * ============================================================ */ const struct CMUnitTest group4_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), PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid), 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), /* MUC room join with various options - XEP-0045 */ PROF_FUNC_TEST(sends_room_join), PROF_FUNC_TEST(sends_room_join_with_nick), PROF_FUNC_TEST(sends_room_join_with_password), PROF_FUNC_TEST(sends_room_join_with_nick_and_password), /* MUC room information display */ PROF_FUNC_TEST(shows_role_and_affiliation_on_join), PROF_FUNC_TEST(shows_subject_on_join), PROF_FUNC_TEST(shows_occupant_join), /* MUC messaging */ PROF_FUNC_TEST(shows_message), PROF_FUNC_TEST(shows_me_message_from_occupant), PROF_FUNC_TEST(shows_me_message_from_self), /* MUC console notification settings */ PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed), PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed), PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed), /* MUC moderation - XEP-0045 room admin */ PROF_FUNC_TEST(sends_affiliation_list_request), PROF_FUNC_TEST(sends_kick_request), /* Message Carbons - XEP-0280 (message sync across devices) */ PROF_FUNC_TEST(send_enable_carbons), PROF_FUNC_TEST(connect_with_carbons_enabled), PROF_FUNC_TEST(send_disable_carbons), PROF_FUNC_TEST(receive_carbon), PROF_FUNC_TEST(receive_self_carbon), PROF_FUNC_TEST(receive_private_carbon), #ifdef HAVE_SQLITE /* Migration stress — pool accumulation, LMC chains, large bodies */ PROF_FUNC_TEST(export_message_pool_dated), PROF_FUNC_TEST(export_lmc_pool_multiple), PROF_FUNC_TEST(export_large_body_survives), #endif }; /* ============================================================ * GROUP 5: AI command surface (console + HTTP stub) * Standalone because AI tests don't use stabber's XMPP path; mixing * them with XMPP-bound tests in the same group poisons stabber state * between fixture init/teardown cycles and hangs subsequent * prof_connect() calls. * ============================================================ */ const struct CMUnitTest group5_tests[] = { /* Console-only — no network calls */ PROF_FUNC_TEST_AI(ai_no_args_shows_help), PROF_FUNC_TEST_AI(ai_providers_lists_defaults), PROF_FUNC_TEST_AI(ai_providers_list_shows_key_status), PROF_FUNC_TEST_AI(ai_set_provider_adds_custom), PROF_FUNC_TEST_AI(ai_set_token_marks_key_set), PROF_FUNC_TEST_AI(ai_start_unknown_provider_errors), PROF_FUNC_TEST_AI(ai_start_without_key_errors), PROF_FUNC_TEST_AI(ai_start_with_key_opens_window), PROF_FUNC_TEST_AI(ai_clear_without_window_errors), PROF_FUNC_TEST_AI(ai_remove_provider_works), PROF_FUNC_TEST_AI(ai_remove_provider_unknown_errors), PROF_FUNC_TEST_AI(ai_set_default_model_updates_provider), PROF_FUNC_TEST_AI(ai_switch_without_window_errors), PROF_FUNC_TEST_AI(ai_bad_subcommand_shows_usage), }; /* Test group registry for easy extension */ struct { const char* name; const struct CMUnitTest* tests; size_t count; } groups[] = { { "Group 1: Connect/Ping/Disco/Roster", group1_tests, ARRAY_SIZE(group1_tests) }, { "Group 2: ExportImport/Receipts", group2_tests, ARRAY_SIZE(group2_tests) }, { "Group 3: Presence/Disconnect/DBHistory/Message/MUC-DB", group3_tests, ARRAY_SIZE(group3_tests) }, { "Group 4: Session/MUC/Carbons/MigrationStress", group4_tests, ARRAY_SIZE(group4_tests) }, { "Group 5: AI command surface (console only)", group5_tests, ARRAY_SIZE(group5_tests) }, }; const int num_groups = ARRAY_SIZE(groups); int result = 0; if (group > 0 && group <= num_groups) { /* Run specific group */ result = _cmocka_run_group_tests(groups[group - 1].name, groups[group - 1].tests, groups[group - 1].count, NULL, NULL); } else { /* Run all groups sequentially */ for (int i = 0; i < num_groups; i++) { result |= _cmocka_run_group_tests(groups[i].name, groups[i].tests, groups[i].count, NULL, NULL); } } return result; }