1
use finance::account::Account;
2
use finance::tag::Tag;
3
use server::command::{CmdResult, CommodityInfo, FinanceEntity};
4
use std::collections::HashMap;
5
use uuid::Uuid;
6

            
7
use super::balance::{format_rational, run_get_balance_single, run_get_balances};
8
use super::create::run_create_account;
9
use super::create::run_set_account_tag;
10
use super::list::{
11
    format_commodity_info_list, format_manage_tree, format_tagged_entities, list_account_entries,
12
    parse_account_commodities_id, parse_get_account_for_manage_id, validate_lookup_key,
13
};
14
use super::quote_string;
15

            
16
3
fn account_entity(id: Uuid) -> FinanceEntity {
17
3
    FinanceEntity::Account(Account::builder().id(id).build().expect("account builder"))
18
3
}
19

            
20
#[test]
21
1
fn format_empty_list() {
22
1
    assert_eq!(format_tagged_entities(&[]), "(:accounts ())");
23
1
}
24

            
25
#[test]
26
1
fn format_single_account_no_tags() {
27
1
    let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
28
1
    let out = format_tagged_entities(&[(account_entity(id), HashMap::new())]);
29
1
    assert_eq!(
30
        out,
31
        "(:accounts ((:id \"550e8400-e29b-41d4-a716-446655440000\")))"
32
    );
33
1
}
34

            
35
#[test]
36
1
fn format_account_with_name_tag() {
37
1
    let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
38
1
    let mut tags = HashMap::new();
39
1
    tags.insert(
40
1
        "name".to_string(),
41
1
        FinanceEntity::Tag(Tag {
42
1
            id: Uuid::nil(),
43
1
            tag_name: "name".to_string(),
44
1
            tag_value: "Checking".to_string(),
45
1
            description: None,
46
1
        }),
47
    );
48
1
    let out = format_tagged_entities(&[(account_entity(id), tags)]);
49
1
    assert!(out.contains(":name \"Checking\""));
50
1
    assert!(out.contains(":id \"550e8400-e29b-41d4-a716-446655440000\""));
51
1
}
52

            
53
#[test]
54
1
fn format_escapes_quotes_in_name() {
55
1
    let id = Uuid::nil();
56
1
    let mut tags = HashMap::new();
57
1
    tags.insert(
58
1
        "name".to_string(),
59
1
        FinanceEntity::Tag(Tag {
60
1
            id: Uuid::nil(),
61
1
            tag_name: "name".to_string(),
62
1
            tag_value: "He said \"hi\"".to_string(),
63
1
            description: None,
64
1
        }),
65
    );
66
1
    let out = format_tagged_entities(&[(account_entity(id), tags)]);
67
1
    assert!(out.contains("\"He said \\\"hi\\\"\""));
68
1
}
69

            
70
#[test]
71
1
fn quote_string_round_trip_safe() {
72
1
    assert_eq!(quote_string("simple"), "\"simple\"");
73
1
    assert_eq!(quote_string("a\"b"), "\"a\\\"b\"");
74
1
    assert_eq!(quote_string("a\\b"), "\"a\\\\b\"");
75
1
}
76

            
77
#[test]
78
1
fn validate_lookup_key_rejects_missing_arg() {
79
1
    let err = validate_lookup_key("get-account", None).unwrap_err();
80
1
    assert!(err.to_string().contains("missing or empty"), "got: {err}");
81
1
}
82

            
83
#[test]
84
1
fn validate_lookup_key_rejects_empty_string() {
85
1
    let err = validate_lookup_key("get-account", Some(String::new())).unwrap_err();
86
1
    assert!(err.to_string().contains("missing or empty"), "got: {err}");
87
1
}
88

            
89
#[test]
90
1
fn format_rational_integer_drops_denom() {
91
    use num_rational::Rational64;
92
1
    assert_eq!(format_rational(&Rational64::new(42, 1)), "42");
93
1
    assert_eq!(format_rational(&Rational64::new(0, 1)), "0");
94
1
}
95

            
96
#[test]
97
1
fn format_rational_fraction_preserves_denom() {
98
    use num_rational::Rational64;
99
1
    assert_eq!(format_rational(&Rational64::new(5000, 100)), "50");
100
1
    assert_eq!(format_rational(&Rational64::new(1, 3)), "1/3");
101
1
    assert_eq!(format_rational(&Rational64::new(-7, 2)), "-7/2");
102
1
}
103

            
104
#[tokio::test]
105
1
async fn run_get_balance_single_with_no_arg_emits_error() {
106
1
    let err = run_get_balance_single(Uuid::nil(), None).await.unwrap_err();
107
1
    assert!(err.to_string().contains("missing or empty"), "got: {err}");
108
1
}
109

            
110
#[tokio::test]
111
1
async fn run_get_balance_single_with_invalid_uuid_emits_error() {
112
1
    let err = run_get_balance_single(Uuid::nil(), Some("not-uuid".into()))
113
1
        .await
114
1
        .unwrap_err();
115
1
    assert!(err.to_string().contains("invalid uuid"), "got: {err}");
116
1
}
117

            
118
#[tokio::test]
119
1
async fn run_get_balances_missing_arg_emits_error() {
120
1
    let err = run_get_balances(Uuid::nil(), None).await.unwrap_err();
121
1
    assert!(err.to_string().contains("missing or empty"), "got: {err}");
122
1
}
123

            
124
#[tokio::test]
125
1
async fn run_get_balances_invalid_uuid_emits_error() {
126
1
    let err = run_get_balances(Uuid::nil(), Some("not-uuid".into()))
127
1
        .await
128
1
        .unwrap_err();
129
1
    assert!(err.to_string().contains("invalid uuid"), "got: {err}");
130
1
}
131

            
132
#[tokio::test]
133
1
async fn run_create_account_empty_parent_falls_through_to_name_check() {
134
1
    let err = run_create_account(Uuid::nil(), None, Some(String::new()))
135
1
        .await
136
1
        .unwrap_err();
137
1
    assert!(err.to_string().contains(":name"), "got: {err}");
138
1
}
139

            
140
#[tokio::test]
141
1
async fn run_create_account_invalid_parent_uuid_emits_error() {
142
1
    let err = run_create_account(
143
1
        Uuid::nil(),
144
1
        Some("MyAccount".into()),
145
1
        Some("not-a-uuid".into()),
146
1
    )
147
1
    .await
148
1
    .unwrap_err();
149
1
    assert!(
150
1
        err.to_string().contains("invalid parent uuid"),
151
1
        "got: {err}"
152
1
    );
153
1
}
154

            
155
#[test]
156
1
fn format_account_commodities_empty() {
157
1
    assert_eq!(format_commodity_info_list(&[]), "(:account-commodities ())");
158
1
}
159

            
160
#[test]
161
1
fn format_account_commodities_multi() {
162
1
    let id1 = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
163
1
    let id2 = Uuid::parse_str("71ddfbdb-1f00-4403-9548-dc973b43e443").unwrap();
164
1
    let items = vec![
165
1
        CommodityInfo {
166
1
            commodity_id: id1,
167
1
            symbol: "USD".into(),
168
1
            name: "US Dollar".into(),
169
1
        },
170
1
        CommodityInfo {
171
1
            commodity_id: id2,
172
1
            symbol: "JPY".into(),
173
1
            name: "Japanese Yen".into(),
174
1
        },
175
    ];
176
1
    let out = format_commodity_info_list(&items);
177
1
    assert!(out.contains(":commodity-id \"550e8400-e29b-41d4-a716-446655440000\""));
178
1
    assert!(out.contains(":symbol \"USD\""));
179
1
    assert!(out.contains(":name \"US Dollar\""));
180
1
    assert!(out.contains(":commodity-id \"71ddfbdb-1f00-4403-9548-dc973b43e443\""));
181
1
    assert!(out.contains(":symbol \"JPY\""));
182
1
}
183

            
184
#[test]
185
1
fn parse_account_commodities_rejects_missing() {
186
1
    let err = parse_account_commodities_id(None).unwrap_err();
187
1
    assert!(err.to_string().contains("missing or empty"), "got: {err}");
188
1
}
189

            
190
#[test]
191
1
fn parse_account_commodities_rejects_invalid_uuid() {
192
1
    let err = parse_account_commodities_id(Some("nope".into())).unwrap_err();
193
1
    assert!(err.to_string().contains("invalid uuid"), "got: {err}");
194
1
}
195

            
196
#[tokio::test]
197
1
async fn run_set_account_tag_missing_id_emits_error() {
198
1
    let err = run_set_account_tag(Uuid::nil(), None, Some("k".into()), Some("v".into()))
199
1
        .await
200
1
        .unwrap_err();
201
1
    assert!(err.to_string().contains(":account-id"), "got: {err}");
202
1
}
203

            
204
#[tokio::test]
205
1
async fn run_set_account_tag_invalid_uuid_emits_error() {
206
1
    let err = run_set_account_tag(
207
1
        Uuid::nil(),
208
1
        Some("not-uuid".into()),
209
1
        Some("k".into()),
210
1
        Some("v".into()),
211
1
    )
212
1
    .await
213
1
    .unwrap_err();
214
1
    assert!(err.to_string().contains("invalid uuid"), "got: {err}");
215
1
}
216

            
217
#[tokio::test]
218
1
async fn run_set_account_tag_missing_name_emits_error() {
219
1
    let id = "11111111-1111-1111-1111-111111111111";
220
1
    let err = run_set_account_tag(Uuid::nil(), Some(id.into()), None, Some("v".into()))
221
1
        .await
222
1
        .unwrap_err();
223
1
    assert!(err.to_string().contains(":tag-name"), "got: {err}");
224
1
}
225

            
226
#[tokio::test]
227
1
async fn run_set_account_tag_missing_value_emits_error() {
228
1
    let id = "11111111-1111-1111-1111-111111111111";
229
1
    let err = run_set_account_tag(Uuid::nil(), Some(id.into()), Some("k".into()), None)
230
1
        .await
231
1
        .unwrap_err();
232
1
    assert!(err.to_string().contains(":tag-value"), "got: {err}");
233
1
}
234

            
235
#[tokio::test]
236
1
async fn run_create_account_missing_name_emits_error() {
237
1
    let err = run_create_account(Uuid::nil(), None, None)
238
1
        .await
239
1
        .unwrap_err();
240
1
    assert!(err.to_string().contains(":name"), "got: {err}");
241
1
}
242

            
243
#[tokio::test]
244
1
async fn run_create_account_empty_name_emits_error() {
245
1
    let err = run_create_account(Uuid::nil(), Some(String::new()), None)
246
1
        .await
247
1
        .unwrap_err();
248
1
    assert!(err.to_string().contains(":name"), "got: {err}");
249
1
}
250

            
251
#[test]
252
1
fn format_manage_tree_empty() {
253
1
    assert_eq!(format_manage_tree(&[]), "(:accounts-tree ())");
254
1
}
255

            
256
#[test]
257
1
fn format_manage_tree_root_emits_nil_parent() {
258
1
    let id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
259
1
    let root = FinanceEntity::Account(Account::builder().id(id).build().expect("account builder"));
260
1
    let out = format_manage_tree(&[(root, HashMap::new())]);
261
1
    assert!(out.contains(":id \"11111111-1111-1111-1111-111111111111\""));
262
1
    assert!(out.contains(":parent-id nil"));
263
1
}
264

            
265
#[test]
266
1
fn parse_get_account_for_manage_id_rejects_missing() {
267
1
    let err = parse_get_account_for_manage_id(None).unwrap_err();
268
1
    assert!(err.to_string().contains(":account-id"), "got: {err}");
269
1
}
270

            
271
#[test]
272
1
fn parse_get_account_for_manage_id_rejects_invalid_uuid() {
273
1
    let err = parse_get_account_for_manage_id(Some("not-uuid".into())).unwrap_err();
274
1
    assert!(err.to_string().contains("invalid uuid"), "got: {err}");
275
1
}
276

            
277
#[test]
278
1
fn format_manage_tree_child_surfaces_parent_uuid_and_name() {
279
1
    let id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap();
280
1
    let parent = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
281
1
    let child = FinanceEntity::Account(
282
1
        Account::builder()
283
1
            .id(id)
284
1
            .parent(parent)
285
1
            .build()
286
1
            .expect("account builder"),
287
1
    );
288
1
    let mut tags = HashMap::new();
289
1
    tags.insert(
290
1
        "name".to_string(),
291
1
        FinanceEntity::Tag(Tag {
292
1
            id: Uuid::nil(),
293
1
            tag_name: "name".to_string(),
294
1
            tag_value: "Sub".to_string(),
295
1
            description: None,
296
1
        }),
297
    );
298
1
    let out = format_manage_tree(&[(child, tags)]);
299
1
    assert!(out.contains(":parent-id \"11111111-1111-1111-1111-111111111111\""));
300
1
    assert!(out.contains(":name \"Sub\""));
301
1
}
302

            
303
#[test]
304
1
fn list_account_entries_extracts_type_tag() {
305
1
    let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
306
1
    let account =
307
1
        FinanceEntity::Account(Account::builder().id(id).build().expect("account builder"));
308
1
    let mut tags = HashMap::new();
309
1
    tags.insert(
310
1
        "type".to_string(),
311
1
        FinanceEntity::Tag(Tag {
312
1
            id: Uuid::nil(),
313
1
            tag_name: "type".to_string(),
314
1
            tag_value: "asset".to_string(),
315
1
            description: None,
316
1
        }),
317
    );
318
1
    let result = Ok(Some(CmdResult::TaggedEntities {
319
1
        entities: vec![(account, tags)],
320
1
        pagination: None,
321
1
    }));
322
1
    let entries = list_account_entries("test", result).unwrap();
323
1
    assert_eq!(entries.len(), 1);
324
1
    let (_, _, _, type_val) = &entries[0];
325
1
    assert_eq!(type_val.as_deref(), Some("asset"));
326
1
}