Skip to main content

datasynth_test_utils/
mocks.rs

1//! Mock implementations for testing.
2
3use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use datasynth_core::models::{GLAccount, JournalEntry};
7use rust_decimal::Decimal;
8
9/// Mock balance tracker for testing.
10pub struct MockBalanceTracker {
11    balances: Arc<RwLock<HashMap<String, Decimal>>>,
12}
13
14impl MockBalanceTracker {
15    pub fn new() -> Self {
16        Self {
17            balances: Arc::new(RwLock::new(HashMap::new())),
18        }
19    }
20
21    /// Get balance for an account.
22    pub fn get_balance(&self, account: &str) -> Decimal {
23        self.balances
24            .read()
25            .unwrap()
26            .get(account)
27            .copied()
28            .unwrap_or(Decimal::ZERO)
29    }
30
31    /// Update balance for an account.
32    pub fn update_balance(&self, account: &str, amount: Decimal) {
33        let mut balances = self.balances.write().unwrap();
34        *balances.entry(account.to_string()).or_insert(Decimal::ZERO) += amount;
35    }
36
37    /// Apply a journal entry to the balances.
38    pub fn apply_entry(&self, entry: &JournalEntry) {
39        for line in &entry.lines {
40            let net_amount = line.debit_amount - line.credit_amount;
41            self.update_balance(&line.gl_account, net_amount);
42        }
43    }
44
45    /// Get total debits across all accounts.
46    pub fn total_debits(&self) -> Decimal {
47        self.balances
48            .read()
49            .unwrap()
50            .values()
51            .filter(|v| **v > Decimal::ZERO)
52            .copied()
53            .sum()
54    }
55
56    /// Get total credits across all accounts (as positive number).
57    pub fn total_credits(&self) -> Decimal {
58        self.balances
59            .read()
60            .unwrap()
61            .values()
62            .filter(|v| **v < Decimal::ZERO)
63            .map(|v| v.abs())
64            .sum()
65    }
66
67    /// Clear all balances.
68    pub fn clear(&self) {
69        self.balances.write().unwrap().clear();
70    }
71}
72
73impl Default for MockBalanceTracker {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79/// Mock chart of accounts for testing.
80pub struct MockChartOfAccounts {
81    accounts: Vec<GLAccount>,
82}
83
84impl MockChartOfAccounts {
85    pub fn new(accounts: Vec<GLAccount>) -> Self {
86        Self { accounts }
87    }
88
89    /// Get an account by number.
90    pub fn get_account(&self, number: &str) -> Option<&GLAccount> {
91        self.accounts.iter().find(|a| a.account_number == number)
92    }
93
94    /// Get all accounts.
95    pub fn all_accounts(&self) -> &[GLAccount] {
96        &self.accounts
97    }
98
99    /// Check if an account exists.
100    pub fn has_account(&self, number: &str) -> bool {
101        self.accounts.iter().any(|a| a.account_number == number)
102    }
103
104    /// Get accounts by type.
105    pub fn get_accounts_by_type(
106        &self,
107        account_type: datasynth_core::models::AccountType,
108    ) -> Vec<&GLAccount> {
109        self.accounts
110            .iter()
111            .filter(|a| a.account_type == account_type)
112            .collect()
113    }
114}
115
116impl Default for MockChartOfAccounts {
117    fn default() -> Self {
118        Self::new(crate::fixtures::standard_test_accounts())
119    }
120}
121
122/// Mock random number generator for deterministic testing.
123pub struct MockRng {
124    sequence: Vec<u64>,
125    index: usize,
126}
127
128impl MockRng {
129    pub fn new(sequence: Vec<u64>) -> Self {
130        Self { sequence, index: 0 }
131    }
132
133    /// Get the next value in the sequence.
134    #[allow(clippy::should_implement_trait)]
135    pub fn next(&mut self) -> u64 {
136        let value = self.sequence[self.index % self.sequence.len()];
137        self.index += 1;
138        value
139    }
140
141    /// Get a value in a range.
142    pub fn next_in_range(&mut self, min: u64, max: u64) -> u64 {
143        let value = self.next();
144        min + (value % (max - min + 1))
145    }
146
147    /// Get a float in [0, 1).
148    pub fn next_float(&mut self) -> f64 {
149        (self.next() as f64) / (u64::MAX as f64)
150    }
151
152    /// Reset the sequence.
153    pub fn reset(&mut self) {
154        self.index = 0;
155    }
156}
157
158impl Default for MockRng {
159    fn default() -> Self {
160        // Predictable sequence for tests
161        Self::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::fixtures::balanced_journal_entry;
169    use datasynth_core::models::AccountType;
170
171    #[test]
172    fn test_mock_balance_tracker() {
173        let tracker = MockBalanceTracker::new();
174
175        assert_eq!(tracker.get_balance("100000"), Decimal::ZERO);
176
177        tracker.update_balance("100000", Decimal::new(1000, 0));
178        assert_eq!(tracker.get_balance("100000"), Decimal::new(1000, 0));
179
180        tracker.update_balance("100000", Decimal::new(-500, 0));
181        assert_eq!(tracker.get_balance("100000"), Decimal::new(500, 0));
182    }
183
184    #[test]
185    fn test_mock_balance_tracker_apply_entry() {
186        let tracker = MockBalanceTracker::new();
187        let entry = balanced_journal_entry(Decimal::new(10000, 2));
188
189        tracker.apply_entry(&entry);
190
191        // Debit account should have positive balance
192        assert_eq!(tracker.get_balance("100000"), Decimal::new(10000, 2));
193        // Credit account should have negative balance
194        assert_eq!(tracker.get_balance("200000"), Decimal::new(-10000, 2));
195    }
196
197    #[test]
198    fn test_mock_chart_of_accounts() {
199        let coa = MockChartOfAccounts::default();
200
201        assert!(coa.has_account("100000"));
202        assert!(!coa.has_account("999999"));
203
204        let account = coa.get_account("100000").unwrap();
205        assert_eq!(account.account_type, AccountType::Asset);
206
207        let assets = coa.get_accounts_by_type(AccountType::Asset);
208        assert!(!assets.is_empty());
209    }
210
211    #[test]
212    fn test_mock_rng() {
213        let mut rng = MockRng::new(vec![10, 20, 30]);
214
215        assert_eq!(rng.next(), 10);
216        assert_eq!(rng.next(), 20);
217        assert_eq!(rng.next(), 30);
218        assert_eq!(rng.next(), 10); // Wraps around
219
220        rng.reset();
221        assert_eq!(rng.next(), 10);
222    }
223
224    #[test]
225    fn test_mock_rng_range() {
226        let mut rng = MockRng::new(vec![0, 5, 10, 15, 20]);
227
228        let value = rng.next_in_range(1, 10);
229        assert!((1..=10).contains(&value));
230    }
231}