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)]
172#[allow(clippy::unwrap_used)]
173mod tests {
174 use super::*;
175 use crate::fixtures::balanced_journal_entry;
176 use datasynth_core::models::AccountType;
177
178 #[test]
179 fn test_mock_balance_tracker() {
180 let tracker = MockBalanceTracker::new();
181
182 assert_eq!(tracker.get_balance("100000"), Decimal::ZERO);
183
184 tracker.update_balance("100000", Decimal::new(1000, 0));
185 assert_eq!(tracker.get_balance("100000"), Decimal::new(1000, 0));
186
187 tracker.update_balance("100000", Decimal::new(-500, 0));
188 assert_eq!(tracker.get_balance("100000"), Decimal::new(500, 0));
189 }
190
191 #[test]
192 fn test_mock_balance_tracker_apply_entry() {
193 let tracker = MockBalanceTracker::new();
194 let entry = balanced_journal_entry(Decimal::new(10000, 2));
195
196 tracker.apply_entry(&entry);
197
198 assert_eq!(tracker.get_balance("100000"), Decimal::new(10000, 2));
200 assert_eq!(tracker.get_balance("200000"), Decimal::new(-10000, 2));
202 }
203
204 #[test]
205 fn test_mock_chart_of_accounts() {
206 let coa = MockChartOfAccounts::default();
207
208 assert!(coa.has_account("100000"));
209 assert!(!coa.has_account("999999"));
210
211 let account = coa.get_account("100000").unwrap();
212 assert_eq!(account.account_type, AccountType::Asset);
213
214 let assets = coa.get_accounts_by_type(AccountType::Asset);
215 assert!(!assets.is_empty());
216 }
217
218 #[test]
219 fn test_mock_rng() {
220 let mut rng = MockRng::new(vec![10, 20, 30]);
221
222 assert_eq!(rng.next(), 10);
223 assert_eq!(rng.next(), 20);
224 assert_eq!(rng.next(), 30);
225 assert_eq!(rng.next(), 10); rng.reset();
228 assert_eq!(rng.next(), 10);
229 }
230
231 #[test]
232 fn test_mock_rng_range() {
233 let mut rng = MockRng::new(vec![0, 5, 10, 15, 20]);
234
235 let value = rng.next_in_range(1, 10);
236 assert!((1..=10).contains(&value));
237 }
238}