diff --git a/Makefile.am b/Makefile.am index 60ff0f33..a7648fcb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -186,6 +186,8 @@ functionaltest_sources = \ tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \ tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.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 \ tests/functionaltests/functionaltests.c main_source = src/main.c diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index 46d1a7d2..f26d1dc1 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, Rooms, Software + * Group 1: Connect, Ping, Autoping (fast), Rooms, Software, Last Activity * Group 2: Message, Receipts, Roster, Chat Session - * Group 3: Presence, Disconnect + * Group 3: Presence, Disconnect, Autoping (slow) * Group 4: MUC, Carbons * * Parallel execution: @@ -53,6 +53,8 @@ #include "test_muc.h" #include "test_disconnect.h" #include "test_lastactivity.h" +#include "test_autoping.h" +#include "test_disco.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) @@ -109,6 +111,16 @@ main(int argc, char* argv[]) /* 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), }; /* ============================================================ @@ -143,7 +155,7 @@ main(int argc, char* argv[]) }; /* ============================================================ - * GROUP 3: Presence, Disconnect + * GROUP 3: Presence, Disconnect, Disco * Online/away/xa/dnd/chat status management * ============================================================ */ const struct CMUnitTest group3_tests[] = { @@ -165,6 +177,22 @@ main(int argc, char* argv[]) /* Disconnect - clean session termination */ PROF_FUNC_TEST(disconnect_ends_session), + + /* 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), }; /* ============================================================ @@ -212,9 +240,9 @@ main(int argc, char* argv[]) const struct CMUnitTest* tests; size_t count; } groups[] = { - { "Group 1: Connect/Ping/Rooms/Software", group1_tests, ARRAY_SIZE(group1_tests) }, + { "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", group3_tests, ARRAY_SIZE(group3_tests) }, + { "Group 3: 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/proftest.c b/tests/functionaltests/proftest.c index b67954f7..37813327 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -362,7 +362,6 @@ close_prof_test(void **state) fd = 0; child_pid = 0; } - _cleanup_dirs(); if (config_orig) { setenv("XDG_CONFIG_HOME", config_orig, 1); diff --git a/tests/functionaltests/test_autoping.c b/tests/functionaltests/test_autoping.c new file mode 100644 index 00000000..1e1a1332 --- /dev/null +++ b/tests/functionaltests/test_autoping.c @@ -0,0 +1,114 @@ +#include +#include "prof_cmocka.h" +#include +#include +#include + +#include + +#include "proftest.h" + +void +autoping_set_interval(void** state) +{ + prof_connect(); + + prof_input("/autoping set 60"); + assert_true(prof_output_exact("Autoping interval set to 60 seconds.")); +} + +void +autoping_set_zero_disables(void** state) +{ + prof_connect(); + + prof_input("/autoping set 0"); + assert_true(prof_output_exact("Autoping disabled.")); +} + +void +autoping_timeout_set(void** state) +{ + prof_connect(); + + prof_input("/autoping timeout 30"); + assert_true(prof_output_exact("Autoping timeout set to 30 seconds.")); +} + +void +autoping_timeout_zero_disables(void** state) +{ + prof_connect(); + + prof_input("/autoping timeout 0"); + assert_true(prof_output_exact("Autoping timeout disabled.")); +} + +void +autoping_sends_ping_after_interval(void** state) +{ + /* + * This test verifies that autoping sends a ping IQ after the configured + * interval. We set a short interval (1 second) and verify the ping is sent. + */ + + // Register disco#info response with ping support + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + // Register ping response + stbbr_for_query("urn:xmpp:ping", + "" + ); + + prof_connect(); + + // Set short autoping interval + prof_input("/autoping set 1"); + assert_true(prof_output_exact("Autoping interval set to 1 seconds.")); + + // Wait for autoping to trigger (interval + some buffer) + sleep(2); + + // Verify ping was sent (no 'to' attribute means server ping) + assert_true(stbbr_received( + "" + "" + "" + )); +} + +void +autoping_server_not_supporting_ping(void** state) +{ + /* + * When server doesn't support ping, autoping should show error. + */ + + // Register disco#info response WITHOUT ping support + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + + prof_connect(); + + // Set short autoping interval + prof_input("/autoping set 1"); + assert_true(prof_output_exact("Autoping interval set to 1 seconds.")); + + // Wait for autoping to trigger + sleep(2); + + // Should show error about ping not being supported + assert_true(prof_output_regex("Server ping not supported")); +} diff --git a/tests/functionaltests/test_autoping.h b/tests/functionaltests/test_autoping.h new file mode 100644 index 00000000..9456a50d --- /dev/null +++ b/tests/functionaltests/test_autoping.h @@ -0,0 +1,6 @@ +void autoping_set_interval(void** state); +void autoping_set_zero_disables(void** state); +void autoping_timeout_set(void** state); +void autoping_timeout_zero_disables(void** state); +void autoping_sends_ping_after_interval(void** state); +void autoping_server_not_supporting_ping(void** state); diff --git a/tests/functionaltests/test_disco.c b/tests/functionaltests/test_disco.c new file mode 100644 index 00000000..1d565b59 --- /dev/null +++ b/tests/functionaltests/test_disco.c @@ -0,0 +1,420 @@ +/* + * test_disco.c + * + * Functional tests for /disco command (XEP-0030 Service Discovery). + * Tests cover: + * - /disco info [jid] - query entity capabilities and features + * - /disco items [jid] - query entity items/services + * + * XEP-0030: https://xmpp.org/extensions/xep-0030.html + */ + +#include +#include "prof_cmocka.h" +#include +#include +#include + +#include + +#include "proftest.h" + +void +disco_info_shows_identity(void **state) +{ + /* + * Test that /disco info displays identity information correctly. + * Identity includes: name, type, category + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + + prof_connect(); + + + prof_input("/disco info"); + + prof_timeout(10); + assert_true(prof_output_exact("Service discovery info for localhost")); + assert_true(prof_output_exact("Identities")); + assert_true(prof_output_regex("Prosody.*im.*server")); + prof_timeout_reset(); +} + +void +disco_info_shows_features(void **state) +{ + /* + * Test that /disco info displays feature list. + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + + prof_input("/disco info"); + + prof_timeout(10); + assert_true(prof_output_exact("Features:")); + assert_true(prof_output_exact("urn:xmpp:ping")); + assert_true(prof_output_exact("http://jabber.org/protocol/disco#info")); + prof_timeout_reset(); +} + +void +disco_info_to_server(void **state) +{ + /* + * Test that /disco info without arguments queries the server (domainpart). + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + + prof_connect(); + + + prof_input("/disco info"); + + /* Verify request was sent to server (localhost) */ + prof_timeout(10); + assert_true(stbbr_received( + "" + "" + "" + )); + prof_timeout_reset(); +} + +void +disco_info_to_jid(void **state) +{ + /* + * Test that /disco info queries the specified JID. + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + + prof_input("/disco info conference.localhost"); + + prof_timeout(10); + /* Verify request was sent to specified JID */ + assert_true(stbbr_received( + "" + "" + "" + )); + + assert_true(prof_output_exact("Service discovery info for conference.localhost")); + prof_timeout_reset(); +} + +void +disco_info_not_found(void **state) +{ + /* + * Test error handling when disco info returns item-not-found. + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + + prof_connect(); + + + prof_input("/disco info unknown.localhost"); + + prof_timeout(10); + assert_true(prof_output_regex("Service discovery failed.*item-not-found")); + prof_timeout_reset(); +} + +void +disco_items_shows_items(void **state) +{ + /* + * Test that /disco items displays items list with JID and name. + */ + stbbr_for_query("http://jabber.org/protocol/disco#items", + "" + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco items"); + + prof_timeout(10); + assert_true(prof_output_exact("Service discovery items for localhost:")); + assert_true(prof_output_regex("conference.localhost.*Chat Rooms")); + assert_true(prof_output_regex("pubsub.localhost.*Publish-Subscribe")); + assert_true(prof_output_regex("proxy.localhost.*SOCKS5 Bytestreams")); + + /* Verify IQ was sent with correct id */ + assert_true(stbbr_received( + "" + "" + "" + )); + prof_timeout_reset(); +} + +void +disco_items_empty_result(void **state) +{ + /* + * Test that /disco items handles empty result gracefully. + * Per XEP-0030: "if an entity has no associated items, it MUST return + * an empty element (rather than an error)" + */ + stbbr_for_query("http://jabber.org/protocol/disco#items", + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco items"); + + prof_timeout(10); + assert_true(prof_output_exact("No service discovery items for localhost")); + prof_timeout_reset(); +} + +void +disco_requires_connection(void **state) +{ + /* + * Test that /disco info and /disco items require an active connection. + * First test without any connection, then after connect/disconnect. + */ + + /* Without connection */ + prof_input("/disconnect"); + assert_true(prof_output_exact("You are not currently connected.")); + + prof_input("/disco info"); + assert_true(prof_output_exact("You are not currently connected.")); + + prof_input("/disco items"); + assert_true(prof_output_exact("You are not currently connected.")); + + /* After connect and disconnect */ + prof_connect(); + + prof_input("/disconnect"); + assert_true(prof_output_exact("stabber@localhost logged out successfully.")); + + prof_input("/disco info"); + assert_true(prof_output_exact("You are not currently connected.")); + + prof_input("/disco items"); + assert_true(prof_output_exact("You are not currently connected.")); +} + +void +disco_items_to_jid(void **state) +{ + /* + * Test that /disco items queries the specified JID. + */ + stbbr_for_query("http://jabber.org/protocol/disco#items", + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco items conference.localhost"); + + prof_timeout(10); + /* Verify request was sent to specified JID */ + assert_true(stbbr_received( + "" + "" + "" + )); + + assert_true(prof_output_exact("Service discovery items for conference.localhost:")); + assert_true(prof_output_regex("room1@conference.localhost.*General Chat")); + prof_timeout_reset(); +} + +void +disco_info_empty_result(void **state) +{ + /* + * Test that /disco info handles empty result (no identities/features). + * This can happen with minimal server configurations. + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco info minimal.localhost"); + + prof_timeout(10); + /* Verify request was sent */ + assert_true(stbbr_received( + "" + "" + "" + )); + /* Empty result should not crash and should not show "Service discovery info" */ + prof_timeout_reset(); +} + +void +disco_info_multiple_identities(void **state) +{ + /* + * Test that /disco info displays multiple identities correctly. + * Entities can have multiple identities (e.g., server + gateway). + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco info gateway.localhost"); + + prof_timeout(10); + assert_true(prof_output_exact("Service discovery info for gateway.localhost")); + assert_true(prof_output_exact("Identities")); + assert_true(prof_output_regex("IRC Gateway.*irc.*gateway")); + assert_true(prof_output_regex("Room Directory.*chatroom.*directory")); + assert_true(prof_output_regex("Ad-Hoc Commands.*command-node.*automation")); + prof_timeout_reset(); +} + +void +disco_info_without_name(void **state) +{ + /* + * Test that /disco info handles identity without name attribute. + * Per XEP-0030: name is OPTIONAL, only category and type are REQUIRED. + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco info"); + + prof_timeout(10); + assert_true(prof_output_exact("Service discovery info for localhost")); + assert_true(prof_output_exact("Identities")); + /* Should show type and category even without name */ + assert_true(prof_output_regex("im.*server")); + prof_timeout_reset(); +} + +void +disco_items_without_name(void **state) +{ + /* + * Test that /disco items handles items without name attribute. + * Per XEP-0030: name is OPTIONAL for items, only jid is REQUIRED. + */ + stbbr_for_query("http://jabber.org/protocol/disco#items", + "" + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco items"); + + prof_timeout(10); + assert_true(prof_output_exact("Service discovery items for localhost:")); + /* Items without name should still display their JID */ + assert_true(prof_output_exact("conference.localhost")); + assert_true(prof_output_regex("pubsub.localhost.*PubSub Service")); + assert_true(prof_output_exact("upload.localhost")); + prof_timeout_reset(); +} + +void +disco_info_service_unavailable(void **state) +{ + /* + * Test error handling when disco info returns service-unavailable. + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + + prof_connect(); + + prof_input("/disco info offline.localhost"); + + prof_timeout(10); + assert_true(prof_output_regex("Service discovery failed.*service-unavailable")); + prof_timeout_reset(); +} diff --git a/tests/functionaltests/test_disco.h b/tests/functionaltests/test_disco.h new file mode 100644 index 00000000..89a45c33 --- /dev/null +++ b/tests/functionaltests/test_disco.h @@ -0,0 +1,19 @@ +/* test_disco.h + * + * Functional tests for /disco command (XEP-0030 Service Discovery) + */ + +void disco_info_shows_identity(void **state); +void disco_info_shows_features(void **state); +void disco_info_to_server(void **state); +void disco_info_to_jid(void **state); +void disco_info_not_found(void **state); +void disco_items_shows_items(void **state); +void disco_items_empty_result(void **state); +void disco_requires_connection(void **state); +void disco_items_to_jid(void **state); +void disco_info_empty_result(void **state); +void disco_info_multiple_identities(void **state); +void disco_info_without_name(void **state); +void disco_items_without_name(void **state); +void disco_info_service_unavailable(void **state);