diff --git a/src/common.c b/src/common.c index d5acb211..52aa0ee6 100644 --- a/src/common.c +++ b/src/common.c @@ -471,11 +471,15 @@ release_get_latest(void) } gboolean -release_is_new(char* found_version) +release_is_new(const char* const curr_version, const char* const found_version) { + if (!curr_version || !found_version) { + return FALSE; + } + int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch; - int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min, + int parse_curr = sscanf(curr_version, "%d.%d.%d", &curr_maj, &curr_min, &curr_patch); int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min, &found_patch); @@ -578,36 +582,48 @@ is_notify_enabled(void) GSList* prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result) { - if (needle == NULL || haystack == NULL) { + if (needle == NULL || haystack == NULL || *needle == '\0') { return *result; } - do { - gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset); - if (g_str_has_prefix(haystack_curr, needle)) { + size_t needle_len = strlen(needle); + gchar* p = g_utf8_offset_to_pointer(haystack, offset); + GSList* matches = NULL; + + while (p) { + if (g_str_has_prefix(p, needle)) { if (whole_word) { gunichar before = 0; - gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr); + gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, p); if (haystack_before_ch) { before = g_utf8_get_char(haystack_before_ch); } gunichar after = 0; - gchar* haystack_after_ch = haystack_curr + strlen(needle); - if (haystack_after_ch[0] != '\0') { + gchar* haystack_after_ch = p + needle_len; + if (*haystack_after_ch != '\0') { after = g_utf8_get_char(haystack_after_ch); } if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) { - *result = g_slist_append(*result, GINT_TO_POINTER(offset)); + matches = g_slist_prepend(matches, GINT_TO_POINTER(offset)); } } else { - *result = g_slist_append(*result, GINT_TO_POINTER(offset)); + matches = g_slist_prepend(matches, GINT_TO_POINTER(offset)); } } + if (*p == '\0') { + break; + } + + p = g_utf8_next_char(p); offset++; - } while (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0); + } + + if (matches) { + *result = g_slist_concat(*result, g_slist_reverse(matches)); + } return *result; } diff --git a/src/common.h b/src/common.h index d7c7baa9..6d4c0007 100644 --- a/src/common.h +++ b/src/common.h @@ -193,7 +193,7 @@ gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can gboolean valid_tls_policy_option(const char* is); char* release_get_latest(void); -gboolean release_is_new(char* found_version); +gboolean release_is_new(const char* const curr_version, const char* const found_version); char* strip_arg_quotes(const char* const input); gboolean is_notify_enabled(void); diff --git a/src/ui/console.c b/src/ui/console.c index 9302b9d3..bea715ac 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -395,7 +395,7 @@ cons_check_version(gboolean not_available_msg) gboolean relase_valid = g_regex_match_simple("^\\d+\\.\\d+\\.\\d+$", latest_release, 0, 0); if (relase_valid) { - if (release_is_new(latest_release)) { + if (release_is_new(PACKAGE_VERSION, latest_release)) { win_println(console, THEME_DEFAULT, "-", "A new version of Profanity is available: %s", latest_release); win_println(console, THEME_DEFAULT, "-", "Check for details."); win_println(console, THEME_DEFAULT, "-", ""); diff --git a/tests/unittests/test_autocomplete.c b/tests/unittests/test_autocomplete.c index 3988aaa4..0377fe87 100644 --- a/tests/unittests/test_autocomplete.c +++ b/tests/unittests/test_autocomplete.c @@ -9,7 +9,10 @@ void clear_empty(void** state) { Autocomplete ac = autocomplete_new(); + char* result = autocomplete_complete(ac, "test", TRUE, FALSE); + assert_null(result); autocomplete_free(ac); + free(result); } void @@ -25,6 +28,7 @@ find_after_create(void** state) { Autocomplete ac = autocomplete_new(); char* result = autocomplete_complete(ac, "hello", TRUE, FALSE); + assert_null(result); autocomplete_free(ac); free(result); } diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 889b2202..8926e57f 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -2,6 +2,7 @@ #include "common.h" #include "prof_cmocka.h" #include +#include "tests/unittests/ui/stub_ui.h" // Include for mocking cons_show void replace_one_substr(void** state) @@ -327,6 +328,225 @@ strip_quotes_strips_both(void** state) free(result); } +void test_valid_tls_policy_option(void** state) { + // Valid inputs + assert_true(valid_tls_policy_option("force")); + assert_true(valid_tls_policy_option("allow")); + assert_true(valid_tls_policy_option("trust")); + assert_true(valid_tls_policy_option("disable")); + assert_true(valid_tls_policy_option("legacy")); + assert_true(valid_tls_policy_option("direct")); + + // Invalid inputs + // Not an option + expect_any_cons_show(); // For "Invalid TLS policy: 'profanity'" + expect_any_cons_show(); // For "TLS policy must be one of: 'force', 'allow', 'trust', 'disable', 'legacy', or 'direct'." + assert_false(valid_tls_policy_option("profanity")); + + // Empty + expect_any_cons_show(); // For "Invalid TLS policy: ''" + expect_any_cons_show(); // For "TLS policy must be one of: 'force', 'allow', 'trust', 'disable', 'legacy', or 'direct'." + assert_false(valid_tls_policy_option("")); + + // NULL + assert_true(valid_tls_policy_option(NULL)); +} + +void test_get_expanded_path(void** state) { + gchar* expanded_path; + + // `file://` prefix + expanded_path = get_expanded_path("file:///tmp/test.txt"); + assert_string_equal("/tmp/test.txt", expanded_path); + g_free(expanded_path); + + // `~/"` prefix + setenv("HOME", "/home/test", 1); + expanded_path = get_expanded_path("~/folder/file.txt"); + assert_string_equal("/home/test/folder/file.txt", expanded_path); + g_free(expanded_path); + unsetenv("HOME"); + + // regular path + expanded_path = get_expanded_path("/home/test/file.pdf"); + assert_string_equal("/home/test/file.pdf", expanded_path); + g_free(expanded_path); + + // empty path + expanded_path = get_expanded_path(""); + assert_string_equal("", expanded_path); + g_free(expanded_path); +} + +void test_strtoi_range_valid_input(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + result = strtoi_range("10", &value, 0, 20, &err_msg); + assert_true(result); + assert_int_equal(10, value); + assert_null(err_msg); + + result = strtoi_range("0", &value, 0, 20, &err_msg); + assert_true(result); + assert_int_equal(0, value); + assert_null(err_msg); + + result = strtoi_range("20", &value, 0, 20, &err_msg); + assert_true(result); + assert_int_equal(20, value); + assert_null(err_msg); + + result = strtoi_range("-5", &value, -10, 0, &err_msg); + assert_true(result); + assert_int_equal(-5, value); + assert_null(err_msg); +} + +void test_strtoi_range_out_of_range(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + // too low, negative + result = strtoi_range("-11", &value, -10, 0, &err_msg); + assert_false(result); + assert_string_equal("Value -11 out of range. Must be in -10..0.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // too low + result = strtoi_range("-1", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Value -1 out of range. Must be in 0..10.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // too high + result = strtoi_range("11", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Value 11 out of range. Must be in 0..10.", err_msg); + g_free(err_msg); + err_msg = NULL; +} + +void test_strtoi_range_invalid_input(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + // not a number + result = strtoi_range("profanity", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"profanity\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // not a number, start with digits + result = strtoi_range("23kk", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"23kk\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; +} + +void test_strtoi_range_null_empty_input(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + // NULL input string + result = strtoi_range(NULL, &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("'str' input pointer can not be NULL", err_msg); + g_free(err_msg); + err_msg = NULL; + + // Empty input string + result = strtoi_range("", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; +} + +void test_strtoi_range_null_err_msg(void** state) { + int value; + gboolean result; + + // valid conversion, err_msg is NULL + result = strtoi_range("5", &value, 0, 10, NULL); + assert_true(result); + assert_int_equal(5, value); + + // invalid conversion, err_msg is NULL + result = strtoi_range("profanity", &value, 0, 10, NULL); + assert_false(result); +} + +void test_string_to_verbosity(void** state) { + int verbosity; + gchar* err_msg = NULL; + gboolean result; + + // Valid input string (0) + result = string_to_verbosity("0", &verbosity, &err_msg); + assert_true(result); + assert_int_equal(0, verbosity); + assert_null(err_msg); + g_free(err_msg); err_msg = NULL; // Clear for next test + + // Valid input string (1) + result = string_to_verbosity("1", &verbosity, &err_msg); + assert_true(result); + assert_int_equal(1, verbosity); + assert_null(err_msg); + g_free(err_msg); err_msg = NULL; + + // Valid input string (3) + result = string_to_verbosity("3", &verbosity, &err_msg); + assert_true(result); + assert_int_equal(3, verbosity); + assert_null(err_msg); + g_free(err_msg); err_msg = NULL; + + // Invalid input string (not a number) + result = string_to_verbosity("profanity", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"profanity\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // Valid input string, out of range (too low) + result = string_to_verbosity("-1", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Value -1 out of range. Must be in 0..3.", err_msg); + g_free(err_msg); err_msg = NULL; + + // Valid input string, out of range (too high) + result = string_to_verbosity("4", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Value 4 out of range. Must be in 0..3.", err_msg); + g_free(err_msg); err_msg = NULL; + + // NULL input string + result = string_to_verbosity(NULL, &verbosity, &err_msg); + assert_false(result); + assert_string_equal("'str' input pointer can not be NULL", err_msg); + g_free(err_msg); err_msg = NULL; + + // Empty input string + result = string_to_verbosity("", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"\" to a number.", err_msg); + g_free(err_msg); err_msg = NULL; + + // err_msg is NULL + result = string_to_verbosity("abc", &verbosity, NULL); + assert_false(result); +} + typedef struct { char* template; @@ -644,6 +864,114 @@ prof_partial_occurrences_tests(void** state) expected = NULL; } +void +get_mentions_tests(void** state) +{ + GSList* actual = NULL; + GSList* expected = NULL; + + // Basic match, case sensitive + expected = g_slist_append(expected, GINT_TO_POINTER(6)); + actual = get_mentions(FALSE, TRUE, "hello boothj5", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Case insensitive match + expected = g_slist_append(expected, GINT_TO_POINTER(6)); + actual = get_mentions(FALSE, FALSE, "hello BOOTHJ5", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Whole word match + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + actual = get_mentions(TRUE, TRUE, "boothj5 hello", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Whole word no match (partial) + actual = get_mentions(TRUE, TRUE, "boothj5hello", "boothj5"); + assert_true(_lists_equal(actual, expected)); // expected is NULL + g_slist_free(actual); actual = NULL; + + // Multiple matches + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + expected = g_slist_append(expected, GINT_TO_POINTER(14)); + actual = get_mentions(FALSE, TRUE, "boothj5 hello boothj5", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Nick with punctuation (whole word) + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + actual = get_mentions(TRUE, TRUE, "boothj5: hi", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Nick surrounded by punctuation + expected = g_slist_append(expected, GINT_TO_POINTER(1)); + actual = get_mentions(TRUE, TRUE, "(boothj5)", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Empty message + actual = get_mentions(FALSE, TRUE, "", "boothj5"); + assert_true(_lists_equal(actual, expected)); // expected is NULL + g_slist_free(actual); actual = NULL; + + // Empty nick + actual = get_mentions(FALSE, TRUE, "hello", ""); + assert_true(_lists_equal(actual, expected)); // expected is NULL + g_slist_free(actual); actual = NULL; + + // UTF-8 characters + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + actual = get_mentions(TRUE, TRUE, "我能 hello", "我能"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; +} + +void +release_is_new_tests(void** state) +{ + // Higher major version + assert_true(release_is_new("0.16.0", "1.0.0")); + // Higher minor version + assert_true(release_is_new("0.16.0", "0.17.0")); + // Higher patch version + assert_true(release_is_new("0.16.0", "0.16.1")); + + // Same version + assert_false(release_is_new("0.16.0", "0.16.0")); + + // Lower major version + assert_false(release_is_new("0.16.0", "0.15.9")); + // Lower minor version + assert_false(release_is_new("0.16.0", "0.15.0")); + // Lower patch version + assert_false(release_is_new("0.16.1", "0.16.0")); + + // Higher version but with different current versions + assert_true(release_is_new("1.2.3", "1.2.4")); + assert_true(release_is_new("1.2.3", "1.3.0")); + assert_true(release_is_new("1.2.3", "2.0.0")); + + // Malformed version strings + assert_false(release_is_new("0.16.0", "0.16")); // Missing patch in found + assert_false(release_is_new("0.16", "0.16.0")); // Missing patch in curr + assert_false(release_is_new("0.16.0", "0.16.0.1")); // Extra part + + assert_false(release_is_new("0.16.0", "abc.def.ghi")); + assert_false(release_is_new("0.16.0", "")); + assert_false(release_is_new(NULL, "1.0.0")); + assert_false(release_is_new("1.0.0", NULL)); +} + void prof_whole_occurrences_tests(void** state) { @@ -846,3 +1174,60 @@ prof_whole_occurrences_tests(void** state) g_slist_free(expected); expected = NULL; } + +void test_string_matches_one_of_edge_cases(void** state) { + // is is NULL, is_can_be_null is TRUE -> should return TRUE + assert_true(string_matches_one_of(NULL, NULL, TRUE, "option1", "option2", NULL)); + + // is is NULL, is_can_be_null is FALSE -> should return FALSE + assert_false(string_matches_one_of(NULL, NULL, FALSE, "option1", "option2", NULL)); + + // is matches one of the options + assert_true(string_matches_one_of("Test", "option1", FALSE, "option1", "option2", NULL)); + assert_true(string_matches_one_of("Test", "option2", FALSE, "option1", "option2", NULL)); + + // is does not match any of the options + expect_any_cons_show(); // For "Invalid Test: 'option3'" + expect_any_cons_show(); // For "Test must be one of: 'option1', or 'option2'." + assert_false(string_matches_one_of("Test", "option3", FALSE, "option1", "option2", NULL)); + + // what is NULL (no error message printed) + assert_false(string_matches_one_of(NULL, "option3", FALSE, "option1", "option2", NULL)); + + // Empty options list (first is NULL) + expect_any_cons_show(); // For "Invalid Test: 'option1'" + expect_any_cons_show(); // For "Test must be one of: ." (empty options list error message) + assert_false(string_matches_one_of("Test", "option1", FALSE, NULL, NULL)); + assert_false(string_matches_one_of(NULL, NULL, FALSE, NULL, NULL)); + assert_true(string_matches_one_of(NULL, NULL, TRUE, NULL, NULL)); + + // Single option, matches + assert_true(string_matches_one_of("Test", "single", FALSE, "single", NULL)); + + // Single option, no match + expect_any_cons_show(); // For "Invalid Test: 'nomatch'" + expect_any_cons_show(); // For "Test must be one of: 'single'." + assert_false(string_matches_one_of("Test", "nomatch", FALSE, "single", NULL)); + + // Multiple options, first matches + assert_true(string_matches_one_of("Test", "first", FALSE, "first", "second", "third", NULL)); + + // Multiple options, middle matches + assert_true(string_matches_one_of("Test", "second", FALSE, "first", "second", "third", NULL)); + + // Multiple options, last matches + assert_true(string_matches_one_of("Test", "third", FALSE, "first", "second", "third", NULL)); + + // Multiple options, no match + expect_any_cons_show(); // For "Invalid Test: 'none'" + expect_any_cons_show(); // For "Test must be one of: 'first', 'second', or 'third'." + assert_false(string_matches_one_of("Test", "none", FALSE, "first", "second", "third", NULL)); + + // is is an empty string, options are not + expect_any_cons_show(); // For "Invalid Test: ''" + expect_any_cons_show(); // For "Test must be one of: 'option1', or 'option2'." + assert_false(string_matches_one_of("Test", "", FALSE, "option1", "option2", NULL)); + + // is is an empty string, one of the options is an empty string + assert_true(string_matches_one_of("Test", "", FALSE, "option1", "", "option2", NULL)); +} diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index ffcc6c9d..6229d54a 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -34,3 +34,14 @@ void prof_whole_occurrences_tests(void** state); void prof_occurrences_of_large_message_tests(void** state); void unique_filename_from_url_td(void** state); void format_call_external_argv_td(void** state); +void test_get_expanded_path(void** state); +void test_string_to_verbosity(void** state); +void test_strtoi_range_valid_input(void** state); +void test_strtoi_range_out_of_range(void** state); +void test_strtoi_range_invalid_input(void** state); +void test_strtoi_range_null_empty_input(void** state); +void test_strtoi_range_null_err_msg(void** state); +void test_string_matches_one_of_edge_cases(void** state); +void test_valid_tls_policy_option(void** state); +void get_mentions_tests(void** state); +void release_is_new_tests(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 5e6833fe..8bf25b9b 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -94,7 +94,15 @@ main(int argc, char* argv[]) cmocka_unit_test(strip_quotes_strips_both), cmocka_unit_test(format_call_external_argv_td), cmocka_unit_test(unique_filename_from_url_td), - + cmocka_unit_test(test_string_to_verbosity), + cmocka_unit_test(test_get_expanded_path), + cmocka_unit_test(test_strtoi_range_valid_input), + cmocka_unit_test(test_strtoi_range_out_of_range), + cmocka_unit_test(test_strtoi_range_invalid_input), + cmocka_unit_test(test_strtoi_range_null_empty_input), + cmocka_unit_test(test_strtoi_range_null_err_msg), + cmocka_unit_test(test_string_matches_one_of_edge_cases), + cmocka_unit_test(test_valid_tls_policy_option), cmocka_unit_test(clear_empty), cmocka_unit_test(reset_after_create), cmocka_unit_test(find_after_create), @@ -626,6 +634,8 @@ main(int argc, char* argv[]) cmocka_unit_test(prof_partial_occurrences_tests), cmocka_unit_test(prof_whole_occurrences_tests), cmocka_unit_test(prof_occurrences_of_large_message_tests), + cmocka_unit_test(get_mentions_tests), + cmocka_unit_test(release_is_new_tests), cmocka_unit_test_setup_teardown(returns_no_commands, load_preferences,