From c4c2023ced94246cb94173b9e62f2c71f8baebd6 Mon Sep 17 00:00:00 2001 From: Jeremy Fievet Date: Tue, 2 Jun 2026 22:59:59 +0200 Subject: [PATCH] test: add backend coverage for imports and migrations --- src-tauri/src/commands.rs | 30 +++++++++ src-tauri/src/db.rs | 55 ++++++++++++++++ src-tauri/src/import.rs | 130 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 85f5402..c0cc340 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1396,6 +1396,12 @@ mod tests { assert_ne!(a, b); } + #[test] + fn test_deobfuscate_rejects_invalid_hex() { + let result = deobfuscate("not-hex", OBFUSCATION_KEY); + assert!(result.is_err()); + } + #[test] fn test_days_in_month() { assert_eq!(days_in_month(2024, 1), 31); @@ -1447,6 +1453,30 @@ mod tests { assert!(result.is_none()); } + #[test] + fn test_compute_next_expected_weekly_and_biweekly_future_dates() { + assert_eq!( + compute_next_expected("weekly", 15, "2030-01-01"), + Some("2030-01-08".to_string()) + ); + assert_eq!( + compute_next_expected("biweekly", 15, "2030-01-01"), + Some("2030-01-15".to_string()) + ); + } + + #[test] + fn test_compute_next_expected_clamps_invalid_day_and_defaults_unknown_frequency() { + assert_eq!( + compute_next_expected("monthly", 0, "2030-01-15"), + Some("2030-02-01".to_string()) + ); + assert_eq!( + compute_next_expected("unknown", 10, "2030-01-15"), + Some("2030-02-10".to_string()) + ); + } + #[test] fn test_anonymize_label() { assert_eq!(anonymize_label("CARTE 17/03 CARREFOUR CB*1234 5678"), "CARTE CARREFOUR CB*1234"); diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 57fd958..a48c450 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -82,3 +82,58 @@ pub fn get_migrations() -> Vec { }, ] } + +#[cfg(test)] +mod tests { + use super::*; + use tauri_plugin_sql::MigrationKind; + + #[test] + fn migrations_are_ordered_and_non_empty() { + let migrations = get_migrations(); + + assert_eq!(migrations.len(), 13); + for (index, migration) in migrations.iter().enumerate() { + assert_eq!(migration.version, (index + 1) as i64); + assert!(!migration.description.trim().is_empty()); + assert!(!migration.sql.trim().is_empty()); + assert!(matches!(migration.kind, MigrationKind::Up)); + } + } + + #[test] + fn migrations_cover_current_schema_features() { + let sql = get_migrations() + .into_iter() + .map(|migration| migration.sql.to_string()) + .collect::>() + .join("\n"); + + for required in [ + "CREATE TABLE IF NOT EXISTS accounts", + "CREATE TABLE IF NOT EXISTS transactions", + "CREATE TABLE IF NOT EXISTS budget_series", + "CREATE TABLE IF NOT EXISTS transaction_splits", + "CREATE TABLE IF NOT EXISTS recurring_transactions", + "CREATE TABLE IF NOT EXISTS budget_carry_over", + "CREATE TABLE IF NOT EXISTS app_settings", + "CREATE TABLE IF NOT EXISTS ai_settings", + "ALTER TABLE transactions ADD COLUMN is_reconciled", + "ALTER TABLE accounts ADD COLUMN low_balance_threshold", + ] { + assert!(sql.contains(required), "missing migration fragment: {required}"); + } + } + + #[test] + fn migration_versions_are_unique() { + let mut versions = get_migrations() + .into_iter() + .map(|migration| migration.version) + .collect::>(); + versions.sort_unstable(); + versions.dedup(); + + assert_eq!(versions.len(), 13); + } +} diff --git a/src-tauri/src/import.rs b/src-tauri/src/import.rs index a53cb8e..736725d 100644 --- a/src-tauri/src/import.rs +++ b/src-tauri/src/import.rs @@ -656,4 +656,134 @@ mod tests { assert_eq!(config.skip_lines, 0); assert_eq!(config.date_format, "%d/%m/%Y"); } + + #[test] + fn test_detect_format() { + assert_eq!(detect_format(""), "ofx"); + assert_eq!(detect_format("!Type:Bank\nD15/03/2026\nT-10\n^"), "qif"); + assert_eq!(detect_format("date;label;amount\n15/03/2026;EDF;-42,00\n"), "csv"); + } + + #[test] + fn test_parse_ofx_sgml_without_closing_transaction_tag() { + let ofx = r#" + + +20260315120000 +-42,35 +PRLV EDF +Facture mars +FIT-1 + +"#; + + let (txs, _, _) = parse_ofx(ofx).unwrap(); + + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].date, "2026-03-15"); + assert_eq!(txs[0].label, "PRLV EDF"); + assert_eq!(txs[0].note, Some("Facture mars".to_string())); + assert_eq!(txs[0].fitid, Some("FIT-1".to_string())); + assert!((txs[0].amount - (-42.35)).abs() < 0.01); + } + + #[test] + fn test_parse_qif_without_trailing_separator_and_two_digit_year() { + let qif = "!Type:Bank\nD15/03/26\nT-12,34\nPBOULANGERIE\nMCarte bancaire"; + + let txs = parse_qif(qif).unwrap(); + + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].date, "2026-03-15"); + assert_eq!(txs[0].label, "BOULANGERIE"); + assert_eq!(txs[0].note, Some("Carte bancaire".to_string())); + assert!((txs[0].amount - (-12.34)).abs() < 0.01); + } + + #[test] + fn test_detect_csv_config_for_french_debit_credit_export() { + let csv = "Date operation;Libelle;Debit;Credit\n15/03/2026;CARREFOUR;42,35;\n"; + + let config = detect_csv_config(csv); + + assert_eq!(config.delimiter, ';'); + assert_eq!(config.decimal_separator, ','); + assert_eq!(config.date_column, 0); + assert_eq!(config.label_column, 1); + assert_eq!(config.debit_column, Some(2)); + assert_eq!(config.credit_column, Some(3)); + assert_eq!(config.date_format, "%d/%m/%Y"); + } + + #[test] + fn test_detect_csv_config_for_comma_decimal_dot_export() { + let csv = "date,label,amount\n2026-03-15,CARREFOUR,-42.35\n"; + + let config = detect_csv_config(csv); + + assert_eq!(config.delimiter, ','); + assert_eq!(config.decimal_separator, '.'); + assert_eq!(config.date_format, "%Y-%m-%d"); + } + + #[test] + fn test_parse_csv_skips_rows_without_required_fields_and_handles_thousands() { + let csv = "date;label;amount\n15/03/2026;LOYER;-1 234,56\n16/03/2026;;-10,00\n;EDF;-42,00\n"; + let config = CsvConfig { + delimiter: ';', + date_column: 0, + label_column: 1, + amount_column: 2, + debit_column: None, + credit_column: None, + date_format: "%d/%m/%Y".to_string(), + skip_lines: 1, + decimal_separator: ',', + }; + + let txs = parse_csv(csv, &config).unwrap(); + + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].date, "2026-03-15"); + assert_eq!(txs[0].label, "LOYER"); + assert!((txs[0].amount - (-1234.56)).abs() < 0.01); + } + + #[test] + fn test_find_duplicate_indices_uses_fitid_then_transaction_hash() { + let new_txs = vec![ + RawTransaction { + date: "2026-03-15".to_string(), + label: "CARREFOUR".to_string(), + original_label: "CARREFOUR".to_string(), + amount: -42.35, + note: None, + fitid: Some("FIT-1".to_string()), + }, + RawTransaction { + date: "2026-03-16".to_string(), + label: "EDF".to_string(), + original_label: "EDF".to_string(), + amount: -100.0, + note: None, + fitid: None, + }, + RawTransaction { + date: "2026-03-17".to_string(), + label: "NOUVEAU".to_string(), + original_label: "NOUVEAU".to_string(), + amount: -5.0, + note: None, + fitid: None, + }, + ]; + + let duplicates = find_duplicate_indices( + &new_txs, + &["FIT-1".to_string()], + &[("2026-03-16".to_string(), -10000, "edf".to_string())], + ); + + assert_eq!(duplicates, vec![0, 1]); + } }