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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -436,6 +436,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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
114
tests/functionaltests/test_export_import.c
Normal file
114
tests/functionaltests/test_export_import.c
Normal file
@@ -0,0 +1,114 @@
|
||||
#include <glib.h>
|
||||
#include "prof_cmocka.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <stabber.h>
|
||||
|
||||
#include "proftest.h"
|
||||
|
||||
/*
|
||||
* Test: SQLite -> flat-file export preserves messages.
|
||||
*
|
||||
* Uses <delay> 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(
|
||||
"<message id='exp1' to='stabber@localhost' from='buddy1@localhost/res' type='chat'>"
|
||||
"<body>SQLite reply at 1030</body>"
|
||||
"<delay xmlns='urn:xmpp:delay' stamp='2025-06-15T10:30:00Z'/>"
|
||||
"</message>");
|
||||
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(
|
||||
"<message id='imp1' to='stabber@localhost' from='buddy1@localhost/res' type='chat'>"
|
||||
"<body>Flatfile reply at 1445</body>"
|
||||
"<delay xmlns='urn:xmpp:delay' stamp='2025-06-15T14:45:00Z'/>"
|
||||
"</message>");
|
||||
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"));
|
||||
}
|
||||
2
tests/functionaltests/test_export_import.h
Normal file
2
tests/functionaltests/test_export_import.h
Normal file
@@ -0,0 +1,2 @@
|
||||
void export_sqlite_to_flatfile(void** state);
|
||||
void import_flatfile_to_sqlite(void** state);
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
444
tests/unittests/test_database_export.c
Normal file
444
tests/unittests/test_database_export.c
Normal file
@@ -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 <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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)));
|
||||
}
|
||||
39
tests/unittests/test_database_export.h
Normal file
39
tests/unittests/test_database_export.h
Normal file
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user