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