From 9183ec7c2a8c8b6cbfbe29f878fc22f4fa0e154f Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Mon, 2 Mar 2026 15:45:27 +0300 Subject: [PATCH] Add functional and unit tests for DB export/import - 2 functional tests: export_sqlite_to_flatfile, import_flatfile_to_sqlite with XEP-0203 delay timestamps for deterministic verification - 27 unit tests for database_export covering edge cases - Merge test/db-functional-tests: 12 DB persistence tests (test_history) - Group 2 rebalanced: 25 tests (23 base + 2 SQLite-only export/import) - #ifdef HAVE_SQLITE guards for export/import tests - prof_stop() helper in proftest.c - Makefile.am: source ordering cleanup, new test files added --- Makefile.am | 8 +- tests/functionaltests/functionaltests.c | 15 +- tests/functionaltests/proftest.c | 18 + tests/functionaltests/proftest.h | 1 + tests/functionaltests/test_export_import.c | 114 ++++++ tests/functionaltests/test_export_import.h | 2 + tests/functionaltests/test_history.c | 2 - tests/functionaltests/test_history.h | 2 - tests/unittests/test_database_export.c | 444 +++++++++++++++++++++ tests/unittests/test_database_export.h | 39 ++ tests/unittests/unittests.c | 38 ++ 11 files changed, 673 insertions(+), 10 deletions(-) create mode 100644 tests/functionaltests/test_export_import.c create mode 100644 tests/functionaltests/test_export_import.h create mode 100644 tests/unittests/test_database_export.c create mode 100644 tests/unittests/test_database_export.h diff --git a/Makefile.am b/Makefile.am index 23506d29..e3d09249 100644 --- a/Makefile.am +++ b/Makefile.am @@ -97,6 +97,8 @@ unittest_sources = \ src/omemo/crypto.h \ src/omemo/store.h \ src/xmpp/vcard.h src/xmpp/vcard_funcs.h \ + src/database_flatfile.h \ + src/database_flatfile_parser.c \ src/command/cmd_defs.h src/command/cmd_defs.c \ src/command/cmd_funcs.h src/command/cmd_funcs.c \ src/command/cmd_ac.h src/command/cmd_ac.c \ @@ -172,6 +174,7 @@ unittest_sources = \ tests/unittests/test_callbacks.c tests/unittests/test_callbacks.h \ tests/unittests/test_plugins_disco.c tests/unittests/test_plugins_disco.h \ tests/unittests/test_forced_encryption.c tests/unittests/test_forced_encryption.h \ + tests/unittests/test_database_export.c tests/unittests/test_database_export.h \ tests/unittests/unittests.c functionaltest_sources = \ @@ -191,8 +194,9 @@ functionaltest_sources = \ tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \ tests/functionaltests/test_history.c tests/functionaltests/test_history.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/test_autoping.c tests/functionaltests/test_autoping.h \ + tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \ + tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \ tests/functionaltests/functionaltests.c main_source = src/main.c diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index b9d297d2..14ea36ba 100644 --- a/tests/functionaltests/functionaltests.c +++ b/tests/functionaltests/functionaltests.c @@ -16,7 +16,7 @@ * * Tests are organized into groups for better maintainability and parallel execution: * Group 1: Connect, Ping, Rooms, Software, Last Activity, Autoping - * Group 2: Message, Receipts, Roster, DB History + * Group 2: Message, Receipts, Roster, DB History (+2 with SQLite) * Group 3: Chat Session, Presence, Disconnect, Disco * Group 4: MUC, Carbons * @@ -56,6 +56,9 @@ #include "test_autoping.h" #include "test_disco.h" #include "test_history.h" +#ifdef HAVE_SQLITE +#include "test_export_import.h" +#endif /* 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) @@ -128,9 +131,15 @@ main(int argc, char* argv[]) /* ============================================================ * GROUP 2: Message, Receipts, Roster, DB History * Core messaging and contact management - * (23 tests) + * (25 tests with SQLite, 23 without) * ============================================================ */ 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), +#endif + /* Basic message send/receive */ PROF_FUNC_TEST(message_send), PROF_FUNC_TEST(message_receive_console), @@ -159,9 +168,7 @@ main(int argc, char* argv[]) PROF_FUNC_TEST(message_db_history_long_message), PROF_FUNC_TEST(message_db_history_newline), PROF_FUNC_TEST(message_db_history_service_chars), -#ifdef HAVE_HISTORY_VERIFY PROF_FUNC_TEST(message_db_history_verify), -#endif PROF_FUNC_TEST(message_db_history_lmc), }; diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index 685246c3..84e818f8 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -395,6 +395,24 @@ close_prof_test(void **state) return 0; } +void +prof_stop(void) +{ + if (fd > 0 && child_pid > 0) { + prof_input("/quit"); + sleep(1); + waitpid(child_pid, NULL, 0); + close(fd); + fd = 0; + child_pid = 0; + } + /* Reset output buffer for clean restart */ + output_len = 0; + output_buffer[0] = '\0'; + /* Brief delay to let stabber release old connection state */ + usleep(200000); /* 200ms */ +} + void prof_input(const char *input) { diff --git a/tests/functionaltests/proftest.h b/tests/functionaltests/proftest.h index d8229fce..c1d9d218 100644 --- a/tests/functionaltests/proftest.h +++ b/tests/functionaltests/proftest.h @@ -15,6 +15,7 @@ int init_prof_test(void **state); int close_prof_test(void **state); void prof_start(void); +void prof_stop(void); void prof_connect(void); void prof_connect_with_roster(const char *roster); void prof_input(const char *input); diff --git a/tests/functionaltests/test_export_import.c b/tests/functionaltests/test_export_import.c new file mode 100644 index 00000000..9de71788 --- /dev/null +++ b/tests/functionaltests/test_export_import.c @@ -0,0 +1,114 @@ +#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")); +} diff --git a/tests/functionaltests/test_export_import.h b/tests/functionaltests/test_export_import.h new file mode 100644 index 00000000..c2ea2bbf --- /dev/null +++ b/tests/functionaltests/test_export_import.h @@ -0,0 +1,2 @@ +void export_sqlite_to_flatfile(void** state); +void import_flatfile_to_sqlite(void** state); diff --git a/tests/functionaltests/test_history.c b/tests/functionaltests/test_history.c index 86fa0155..5242e6d9 100644 --- a/tests/functionaltests/test_history.c +++ b/tests/functionaltests/test_history.c @@ -385,7 +385,6 @@ message_db_history_service_chars(void **state) assert_true(prof_output_exact("svc: a\\b c|d e%f {g} [h] i=j")); } -#ifdef HAVE_HISTORY_VERIFY /* * Test: /history verify reports no issues after normal message writes. */ @@ -416,7 +415,6 @@ message_db_history_verify(void **state) prof_input("/history verify"); assert_true(prof_output_exact("Verification complete: no issues found.")); } -#endif /* HAVE_HISTORY_VERIFY */ /* * Test: LMC (Last Message Correction, XEP-0308) — incoming correction diff --git a/tests/functionaltests/test_history.h b/tests/functionaltests/test_history.h index c2d0f040..551bfc8d 100644 --- a/tests/functionaltests/test_history.h +++ b/tests/functionaltests/test_history.h @@ -8,7 +8,5 @@ void message_db_history_empty(void **state); void message_db_history_long_message(void **state); void message_db_history_newline(void **state); void message_db_history_service_chars(void **state); -#ifdef HAVE_HISTORY_VERIFY void message_db_history_verify(void **state); -#endif void message_db_history_lmc(void **state); diff --git a/tests/unittests/test_database_export.c b/tests/unittests/test_database_export.c new file mode 100644 index 00000000..3ca20691 --- /dev/null +++ b/tests/unittests/test_database_export.c @@ -0,0 +1,444 @@ +/* + * test_database_export.c + * + * Unit tests for the flatfile write→parse round-trip, escape/unescape helpers, + * jid_to_dir path construction, and parser edge cases — these are the core + * functions exercised by database_export.c during export and import. + * + * Copyright (C) 2026 Profanity Contributors + * License: GPL-3.0-or-later (same as Profanity) + */ + +#include "prof_cmocka.h" + +#include +#include +#include +#include +#include + +#include "config.h" +#include "database_flatfile.h" + +/* ================================================================ + * Helper: write a single line to a temporary file, then read and + * parse it back. Returns the parsed result; caller must free with + * ff_parsed_line_free(). The temp file is removed automatically. + * ================================================================ */ +static ff_parsed_line_t* +_roundtrip(const char* timestamp, const char* type, const char* enc, + const char* stanza_id, const char* archive_id, const char* replace_id, + const char* from_jid, const char* from_resource, const char* message) +{ + char tmppath[] = "/tmp/proftest_rt_XXXXXX"; + int fd = mkstemp(tmppath); + assert_true(fd >= 0); + + FILE* fp = fdopen(fd, "w"); + assert_non_null(fp); + + ff_write_line(fp, timestamp, type, enc, + stanza_id, archive_id, replace_id, + from_jid, from_resource, message); + fclose(fp); + + /* Read & parse */ + fp = fopen(tmppath, "r"); + assert_non_null(fp); + + gboolean truncated = FALSE; + char* buf = ff_readline(fp, &truncated); + fclose(fp); + unlink(tmppath); + + assert_non_null(buf); + assert_false(truncated); + + ff_parsed_line_t* pl = ff_parse_line(buf); + free(buf); + + return pl; +} + +/* ================================================================ + * Write → parse round-trip tests + * ================================================================ */ + +void +test_ff_roundtrip_simple_chat(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-06-15T12:30:00+00:00", "chat", "none", + NULL, NULL, NULL, + "alice@example.com", NULL, "Hello, world!"); + + assert_non_null(pl); + assert_string_equal(pl->timestamp_str, "2025-06-15T12:30:00+00:00"); + assert_string_equal(pl->type, "chat"); + assert_string_equal(pl->enc, "none"); + assert_string_equal(pl->from_jid, "alice@example.com"); + assert_null(pl->stanza_id); + assert_null(pl->archive_id); + assert_null(pl->replace_id); + assert_string_equal(pl->message, "Hello, world!"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_with_all_metadata(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-06-15T12:30:00+00:00", "chat", "omemo", + "sid-abc-123", "aid-xyz-789", "corrects-old-id", + "bob@example.com", "phone", "Encrypted message."); + + assert_non_null(pl); + assert_string_equal(pl->stanza_id, "sid-abc-123"); + assert_string_equal(pl->archive_id, "aid-xyz-789"); + assert_string_equal(pl->replace_id, "corrects-old-id"); + assert_string_equal(pl->enc, "omemo"); + assert_string_equal(pl->from_jid, "bob@example.com"); + assert_string_equal(pl->from_resource, "phone"); + assert_string_equal(pl->message, "Encrypted message."); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_with_resource(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-01-01T00:00:00Z", "chat", "none", + NULL, NULL, NULL, + "user@jabber.org", "Profanity.abc123", "hi"); + + assert_non_null(pl); + assert_string_equal(pl->from_jid, "user@jabber.org"); + assert_string_equal(pl->from_resource, "Profanity.abc123"); + assert_string_equal(pl->message, "hi"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_newline_in_body(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + NULL, NULL, NULL, + "alice@x.com", NULL, "line1\nline2\nline3"); + + assert_non_null(pl); + assert_string_equal(pl->message, "line1\nline2\nline3"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_pipe_in_stanza_id(void** state) +{ + /* pipes in stanza_id must be escaped in metadata */ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + "id|with|pipes", "aid|also|has|pipes", NULL, + "a@b.com", NULL, "test"); + + assert_non_null(pl); + assert_string_equal(pl->stanza_id, "id|with|pipes"); + assert_string_equal(pl->archive_id, "aid|also|has|pipes"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_backslash_in_body(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + NULL, NULL, NULL, + "a@b.com", NULL, "path\\to\\file C:\\Users\\test"); + + assert_non_null(pl); + assert_string_equal(pl->message, "path\\to\\file C:\\Users\\test"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_unicode_body(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + NULL, NULL, NULL, + "a@b.com", NULL, "Привет мир 🌍 日本語テスト"); + + assert_non_null(pl); + assert_string_equal(pl->message, "Привет мир 🌍 日本語テスト"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_empty_body(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + NULL, NULL, NULL, + "a@b.com", NULL, ""); + + assert_non_null(pl); + assert_string_equal(pl->message, ""); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_colonspace_in_resource(void** state) +{ + /* ": " in resource must be escaped during write and unescaped on parse */ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + NULL, NULL, NULL, + "a@b.com", "res: with: colons", "msg"); + + assert_non_null(pl); + assert_string_equal(pl->from_jid, "a@b.com"); + assert_string_equal(pl->from_resource, "res: with: colons"); + assert_string_equal(pl->message, "msg"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_muc_type(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "muc", "none", + NULL, NULL, NULL, + "room@conference.x.com", "nick", "hello room"); + + assert_non_null(pl); + assert_string_equal(pl->type, "muc"); + assert_string_equal(pl->from_jid, "room@conference.x.com"); + assert_string_equal(pl->from_resource, "nick"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_omemo_enc(void** state) +{ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "omemo", + "sid-1", NULL, NULL, + "a@b.com", NULL, "secret"); + + assert_non_null(pl); + assert_string_equal(pl->enc, "omemo"); + ff_parsed_line_free(pl); +} + +void +test_ff_roundtrip_replace_id(void** state) +{ + /* LMC (Last Message Correction): corrects: field round-trip */ + ff_parsed_line_t* pl = _roundtrip( + "2025-03-01T10:00:00Z", "chat", "none", + "new-id", NULL, "old-id-to-correct", + "a@b.com", NULL, "corrected text"); + + assert_non_null(pl); + assert_string_equal(pl->stanza_id, "new-id"); + assert_string_equal(pl->replace_id, "old-id-to-correct"); + assert_string_equal(pl->message, "corrected text"); + ff_parsed_line_free(pl); +} + +/* ================================================================ + * Escape / unescape symmetry tests + * ================================================================ */ + +void +test_ff_escape_unescape_message_identity(void** state) +{ + const char* inputs[] = { + "simple text", + "has\\backslash", + "has\nnewline", + "has\rcarriage return", + "all\\together\nnow\r!", + "already escaped \\n \\r \\\\", + "", + }; + + for (size_t i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) { + char* escaped = ff_escape_message(inputs[i]); + assert_non_null(escaped); + /* escaped must not contain raw newlines */ + assert_null(strchr(escaped, '\n')); + assert_null(strchr(escaped, '\r')); + + char* unescaped = ff_unescape_message(escaped); + assert_string_equal(unescaped, inputs[i]); + + g_free(escaped); + g_free(unescaped); + } +} + +void +test_ff_escape_unescape_meta_identity(void** state) +{ + const char* inputs[] = { + "simple-id", + "has|pipe", + "has]bracket", + "has\\backslash", + "has\nnewline", + "all|to]ge\\th\ner", + }; + + for (size_t i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) { + char* escaped = ff_escape_meta_value(inputs[i]); + assert_non_null(escaped); + + char* unescaped = ff_unescape_meta_value(escaped); + assert_string_equal(unescaped, inputs[i]); + + g_free(escaped); + g_free(unescaped); + } +} + +void +test_ff_escape_meta_null_returns_null(void** state) +{ + assert_null(ff_escape_meta_value(NULL)); + assert_null(ff_escape_meta_value("")); + assert_null(ff_unescape_meta_value(NULL)); +} + +void +test_ff_escape_message_null_returns_empty(void** state) +{ + char* result = ff_escape_message(NULL); + assert_non_null(result); + assert_string_equal(result, ""); + g_free(result); + + result = ff_unescape_message(NULL); + assert_non_null(result); + assert_string_equal(result, ""); + g_free(result); +} + +/* ================================================================ + * jid_to_dir tests + * ================================================================ */ + +void +test_ff_jid_to_dir_simple(void** state) +{ + char* dir = ff_jid_to_dir("alice"); + assert_non_null(dir); + assert_string_equal(dir, "alice"); + g_free(dir); +} + +void +test_ff_jid_to_dir_with_at(void** state) +{ + char* dir = ff_jid_to_dir("alice@example.com"); + assert_non_null(dir); + assert_string_equal(dir, "alice_at_example.com"); + g_free(dir); +} + +void +test_ff_jid_to_dir_path_traversal_rejected(void** state) +{ + /* Slashes and '..' must be sanitized */ + char* dir = ff_jid_to_dir("../../etc/passwd"); + assert_non_null(dir); + /* No slashes or '..' sequences should remain */ + assert_null(strchr(dir, '/')); + assert_null(strstr(dir, "..")); + g_free(dir); +} + +void +test_ff_jid_to_dir_null(void** state) +{ + assert_null(ff_jid_to_dir(NULL)); + assert_null(ff_jid_to_dir("")); +} + +/* ================================================================ + * Parser edge-case tests + * ================================================================ */ + +void +test_ff_parse_line_empty(void** state) +{ + assert_null(ff_parse_line(NULL)); + assert_null(ff_parse_line("")); +} + +void +test_ff_parse_line_comment(void** state) +{ + assert_null(ff_parse_line("# this is a comment")); + assert_null(ff_parse_line("# profanity chat log — UTF-8, LF line endings")); +} + +void +test_ff_parse_line_legacy_format(void** state) +{ + /* Legacy chatlog.c format: {timestamp} - {sender}: {message} */ + ff_parsed_line_t* pl = ff_parse_line( + "2025-06-15T12:30:00+00:00 - alice@example.com: Hello legacy"); + + assert_non_null(pl); + assert_string_equal(pl->timestamp_str, "2025-06-15T12:30:00+00:00"); + assert_string_equal(pl->from_jid, "alice@example.com"); + assert_string_equal(pl->message, "Hello legacy"); + assert_string_equal(pl->type, "chat"); + assert_string_equal(pl->enc, "none"); + ff_parsed_line_free(pl); +} + +void +test_ff_parse_line_no_metadata(void** state) +{ + /* Simple format without [] brackets */ + ff_parsed_line_t* pl = ff_parse_line( + "2025-06-15T12:30:00Z alice@example.com: Simple message"); + + assert_non_null(pl); + assert_string_equal(pl->from_jid, "alice@example.com"); + assert_string_equal(pl->message, "Simple message"); + ff_parsed_line_free(pl); +} + +void +test_ff_parse_line_invalid_timestamp(void** state) +{ + /* Invalid timestamp should cause parse failure */ + ff_parsed_line_t* pl = ff_parse_line( + "not-a-timestamp [chat|none] a@b.com: msg"); + + assert_null(pl); +} + +/* ================================================================ + * Type / enc conversion round-trip + * ================================================================ */ + +void +test_ff_type_str_roundtrip(void** state) +{ + assert_int_equal(PROF_MSG_TYPE_CHAT, ff_get_message_type_type(ff_get_message_type_str(PROF_MSG_TYPE_CHAT))); + assert_int_equal(PROF_MSG_TYPE_MUC, ff_get_message_type_type(ff_get_message_type_str(PROF_MSG_TYPE_MUC))); + assert_int_equal(PROF_MSG_TYPE_MUCPM, ff_get_message_type_type(ff_get_message_type_str(PROF_MSG_TYPE_MUCPM))); +} + +void +test_ff_enc_str_roundtrip(void** state) +{ + assert_int_equal(PROF_MSG_ENC_NONE, ff_get_message_enc_type(ff_get_message_enc_str(PROF_MSG_ENC_NONE))); + assert_int_equal(PROF_MSG_ENC_OTR, ff_get_message_enc_type(ff_get_message_enc_str(PROF_MSG_ENC_OTR))); + assert_int_equal(PROF_MSG_ENC_PGP, ff_get_message_enc_type(ff_get_message_enc_str(PROF_MSG_ENC_PGP))); + assert_int_equal(PROF_MSG_ENC_OX, ff_get_message_enc_type(ff_get_message_enc_str(PROF_MSG_ENC_OX))); + assert_int_equal(PROF_MSG_ENC_OMEMO, ff_get_message_enc_type(ff_get_message_enc_str(PROF_MSG_ENC_OMEMO))); +} diff --git a/tests/unittests/test_database_export.h b/tests/unittests/test_database_export.h new file mode 100644 index 00000000..88f07bc9 --- /dev/null +++ b/tests/unittests/test_database_export.h @@ -0,0 +1,39 @@ +/* test_database_export.h — unit tests for flatfile write/parse round-trip, + * escape/unescape, and jid_to_dir helpers used by export/import. */ + +/* write → parse round-trip */ +void test_ff_roundtrip_simple_chat(void** state); +void test_ff_roundtrip_with_all_metadata(void** state); +void test_ff_roundtrip_with_resource(void** state); +void test_ff_roundtrip_newline_in_body(void** state); +void test_ff_roundtrip_pipe_in_stanza_id(void** state); +void test_ff_roundtrip_backslash_in_body(void** state); +void test_ff_roundtrip_unicode_body(void** state); +void test_ff_roundtrip_empty_body(void** state); +void test_ff_roundtrip_colonspace_in_resource(void** state); +void test_ff_roundtrip_muc_type(void** state); +void test_ff_roundtrip_omemo_enc(void** state); +void test_ff_roundtrip_replace_id(void** state); + +/* escape / unescape symmetry */ +void test_ff_escape_unescape_message_identity(void** state); +void test_ff_escape_unescape_meta_identity(void** state); +void test_ff_escape_meta_null_returns_null(void** state); +void test_ff_escape_message_null_returns_empty(void** state); + +/* jid_to_dir */ +void test_ff_jid_to_dir_simple(void** state); +void test_ff_jid_to_dir_with_at(void** state); +void test_ff_jid_to_dir_path_traversal_rejected(void** state); +void test_ff_jid_to_dir_null(void** state); + +/* parser edge cases */ +void test_ff_parse_line_empty(void** state); +void test_ff_parse_line_comment(void** state); +void test_ff_parse_line_legacy_format(void** state); +void test_ff_parse_line_no_metadata(void** state); +void test_ff_parse_line_invalid_timestamp(void** state); + +/* type/enc conversion round-trip */ +void test_ff_type_str_roundtrip(void** state); +void test_ff_enc_str_roundtrip(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 69fbd4bf..26cf6bde 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -36,6 +36,7 @@ #include "test_form.h" #include "test_callbacks.h" #include "test_plugins_disco.h" +#include "test_database_export.h" #define muc_unit_test(f) cmocka_unit_test_setup_teardown(f, muc_before_test, muc_after_test) @@ -656,6 +657,43 @@ main(int argc, char* argv[]) cmocka_unit_test_setup_teardown(test_allow_unencrypted_message_confirms_on_second_attempt, load_preferences, close_preferences), cmocka_unit_test_setup_teardown(test_cmd_force_encryption_invalid_policy, load_preferences, close_preferences), cmocka_unit_test_setup_teardown(test_allow_unencrypted_message_invalid_mode, load_preferences, close_preferences), + + // Flatfile export/import round-trip + cmocka_unit_test(test_ff_roundtrip_simple_chat), + cmocka_unit_test(test_ff_roundtrip_with_all_metadata), + cmocka_unit_test(test_ff_roundtrip_with_resource), + cmocka_unit_test(test_ff_roundtrip_newline_in_body), + cmocka_unit_test(test_ff_roundtrip_pipe_in_stanza_id), + cmocka_unit_test(test_ff_roundtrip_backslash_in_body), + cmocka_unit_test(test_ff_roundtrip_unicode_body), + cmocka_unit_test(test_ff_roundtrip_empty_body), + cmocka_unit_test(test_ff_roundtrip_colonspace_in_resource), + cmocka_unit_test(test_ff_roundtrip_muc_type), + cmocka_unit_test(test_ff_roundtrip_omemo_enc), + cmocka_unit_test(test_ff_roundtrip_replace_id), + + // Escape / unescape + cmocka_unit_test(test_ff_escape_unescape_message_identity), + cmocka_unit_test(test_ff_escape_unescape_meta_identity), + cmocka_unit_test(test_ff_escape_meta_null_returns_null), + cmocka_unit_test(test_ff_escape_message_null_returns_empty), + + // jid_to_dir + cmocka_unit_test(test_ff_jid_to_dir_simple), + cmocka_unit_test(test_ff_jid_to_dir_with_at), + cmocka_unit_test(test_ff_jid_to_dir_path_traversal_rejected), + cmocka_unit_test(test_ff_jid_to_dir_null), + + // Parser edge cases + cmocka_unit_test(test_ff_parse_line_empty), + cmocka_unit_test(test_ff_parse_line_comment), + cmocka_unit_test(test_ff_parse_line_legacy_format), + cmocka_unit_test(test_ff_parse_line_no_metadata), + cmocka_unit_test(test_ff_parse_line_invalid_timestamp), + + // Type/enc round-trip + cmocka_unit_test(test_ff_type_str_roundtrip), + cmocka_unit_test(test_ff_enc_str_roundtrip), }; return cmocka_run_group_tests(all_tests, NULL, NULL); }