1
use axum::{
2
    Router,
3
    body::Body,
4
    http::{Request, StatusCode},
5
    routing::post,
6
};
7
use serde_json::json;
8
use tower::ServiceExt;
9

            
10
use crate::common::{create_mock_jwt_auth, create_mock_user, create_test_app_state};
11

            
12
#[tokio::test]
13
1
async fn test_commodity_submit_without_auth() {
14
1
    let app_state = create_test_app_state().await;
15
1
    let app = Router::new()
16
1
        .route(
17
1
            "/commodity/create/submit",
18
1
            post(web::pages::commodity::create::submit::commodity_submit),
19
        )
20
1
        .with_state(app_state);
21

            
22
1
    let commodity_data = json!({
23
1
        "fraction": "100",
24
1
        "symbol": "USD",
25
1
        "name": "US Dollar"
26
    });
27

            
28
1
    let response = app
29
1
        .oneshot(
30
1
            Request::builder()
31
1
                .method("POST")
32
1
                .uri("/commodity/create/submit")
33
1
                .header("content-type", "application/json")
34
1
                .body(Body::from(commodity_data.to_string()))
35
1
                .unwrap(),
36
1
        )
37
1
        .await
38
1
        .unwrap();
39

            
40
    // Should fail without authentication - expecting 401 or 500
41
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
42
1
}
43

            
44
#[tokio::test]
45
1
async fn test_commodity_submit_with_invalid_json() {
46
1
    let app_state = create_test_app_state().await;
47
1
    let app = Router::new()
48
1
        .route(
49
1
            "/commodity/create/submit",
50
1
            post(web::pages::commodity::create::submit::commodity_submit),
51
        )
52
1
        .with_state(app_state);
53

            
54
1
    let response = app
55
1
        .oneshot(
56
1
            Request::builder()
57
1
                .method("POST")
58
1
                .uri("/commodity/create/submit")
59
1
                .header("content-type", "application/json")
60
1
                .body(Body::from("invalid json"))
61
1
                .unwrap(),
62
1
        )
63
1
        .await
64
1
        .unwrap();
65

            
66
    // Should return 400 for invalid JSON or 500 if auth fails first
67
1
    assert!(
68
1
        response.status() == StatusCode::BAD_REQUEST || response.status().is_server_error(),
69
1
        "Expected 400 or 5xx error, got: {}",
70
1
        response.status()
71
1
    );
72
1
}
73

            
74
#[tokio::test]
75
1
async fn test_commodity_submit_with_mock_auth() {
76
1
    let app_state = create_test_app_state().await;
77
1
    let mock_user = create_mock_user();
78
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
79

            
80
1
    let app = Router::new()
81
1
        .route(
82
1
            "/commodity/create/submit",
83
1
            post(web::pages::commodity::create::submit::commodity_submit),
84
        )
85
1
        .layer(axum::middleware::from_fn_with_state(
86
1
            app_state.clone(),
87
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
88
1
                let jwt_auth = jwt_auth.clone();
89
1
                async move {
90
1
                    req.extensions_mut().insert(jwt_auth);
91
1
                    next.run(req).await
92
1
                }
93
1
            },
94
        ))
95
1
        .with_state(app_state.clone());
96

            
97
1
    let commodity_data = json!({
98
1
        "fraction": "100",
99
1
        "symbol": "TST",
100
1
        "name": "Test Commodity"
101
    });
102

            
103
1
    let response = app
104
1
        .oneshot(
105
1
            Request::builder()
106
1
                .method("POST")
107
1
                .uri("/commodity/create/submit")
108
1
                .header("content-type", "application/json")
109
1
                .body(Body::from(commodity_data.to_string()))
110
1
                .unwrap(),
111
1
        )
112
1
        .await
113
1
        .unwrap();
114

            
115
    // This test verifies our new CreateCommodity macro integration works
116
    // Even if it fails due to DB issues, it should not be a JSON parsing error
117
    // We expect either success (200) or a server error (500) due to missing DB
118
    // but NOT a client error (400) which would indicate API issues
119
1
    assert!(
120
1
        response.status().is_success() || response.status().is_server_error(),
121
        "Expected success or server error, got: {}",
122
        response.status()
123
    );
124

            
125
    // If we get a response, verify it has proper content type
126
1
    if let Some(content_type) = response.headers().get("content-type") {
127
1
        let content_type_str = content_type.to_str().unwrap_or("");
128
1
        // Should be either JSON (for success/error response) or text (for success message)
129
1
        assert!(
130
1
            content_type_str.contains("application/json") || content_type_str.contains("text/"),
131
1
            "Unexpected content type: {content_type_str}"
132
1
        );
133
1
    }
134
1
}
135

            
136
#[tokio::test]
137
1
async fn test_commodity_submit_invalid_fraction() {
138
1
    let app_state = create_test_app_state().await;
139
1
    let mock_user = create_mock_user();
140
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
141

            
142
1
    let app = Router::new()
143
1
        .route(
144
1
            "/commodity/create/submit",
145
1
            post(web::pages::commodity::create::submit::commodity_submit),
146
        )
147
1
        .layer(axum::middleware::from_fn_with_state(
148
1
            app_state.clone(),
149
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
150
1
                let jwt_auth = jwt_auth.clone();
151
1
                async move {
152
1
                    req.extensions_mut().insert(jwt_auth);
153
1
                    next.run(req).await
154
1
                }
155
1
            },
156
        ))
157
1
        .with_state(app_state.clone());
158

            
159
1
    let commodity_data = json!({
160
1
        "fraction": "invalid_number",
161
1
        "symbol": "TST",
162
1
        "name": "Test Commodity"
163
    });
164

            
165
1
    let response = app
166
1
        .oneshot(
167
1
            Request::builder()
168
1
                .method("POST")
169
1
                .uri("/commodity/create/submit")
170
1
                .header("content-type", "application/json")
171
1
                .body(Body::from(commodity_data.to_string()))
172
1
                .unwrap(),
173
1
        )
174
1
        .await
175
1
        .unwrap();
176

            
177
    // Should return 400 for invalid fraction format
178
1
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
179

            
180
    // Verify it returns JSON error response
181
1
    let body = axum::body::to_bytes(response.into_body(), usize::MAX)
182
1
        .await
183
1
        .unwrap();
184
1
    let body_str = String::from_utf8(body.to_vec()).unwrap();
185
1
    let json_response: serde_json::Value =
186
1
        serde_json::from_str(&body_str).expect("Response should be valid JSON");
187

            
188
    // Verify error structure
189
1
    assert_eq!(json_response["status"], "fail");
190
1
    assert!(json_response["message"].is_string());
191
1
}
192

            
193
#[tokio::test]
194
1
async fn test_commodity_table_without_auth() {
195
1
    let app_state = create_test_app_state().await;
196
1
    let app = Router::new()
197
1
        .route(
198
1
            "/commodity/list",
199
1
            axum::routing::get(web::pages::commodity::list::commodity_table),
200
        )
201
1
        .with_state(app_state);
202

            
203
1
    let response = app
204
1
        .oneshot(
205
1
            Request::builder()
206
1
                .method("GET")
207
1
                .uri("/commodity/list")
208
1
                .body(Body::empty())
209
1
                .unwrap(),
210
1
        )
211
1
        .await
212
1
        .unwrap();
213

            
214
    // Should fail without authentication - expecting 401 or 500
215
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
216
1
}
217

            
218
#[tokio::test]
219
1
async fn test_commodity_table_with_mock_auth() {
220
1
    let app_state = create_test_app_state().await;
221
1
    let mock_user = create_mock_user();
222
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
223

            
224
1
    let app = Router::new()
225
1
        .route(
226
1
            "/commodity/list",
227
1
            axum::routing::get(web::pages::commodity::list::commodity_table),
228
        )
229
1
        .layer(axum::middleware::from_fn_with_state(
230
1
            app_state.clone(),
231
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
232
1
                let jwt_auth = jwt_auth.clone();
233
1
                async move {
234
1
                    req.extensions_mut().insert(jwt_auth);
235
1
                    next.run(req).await
236
1
                }
237
1
            },
238
        ))
239
1
        .with_state(app_state.clone());
240

            
241
1
    let response = app
242
1
        .oneshot(
243
1
            Request::builder()
244
1
                .method("GET")
245
1
                .uri("/commodity/list")
246
1
                .body(Body::empty())
247
1
                .unwrap(),
248
1
        )
249
1
        .await
250
1
        .unwrap();
251

            
252
    // This test verifies our ListCommodities macro integration works
253
    // Even if it fails due to DB issues, it should not be a parsing error
254
    // We expect either success (200) or a server error (500) due to missing DB
255
    // but NOT a client error (400) which would indicate API issues
256
1
    assert!(
257
1
        response.status().is_success() || response.status().is_server_error(),
258
        "Expected success or server error, got: {}",
259
        response.status()
260
    );
261

            
262
    // If we get a response, verify it has proper content type (HTML)
263
1
    if let Some(content_type) = response.headers().get("content-type") {
264
1
        let content_type_str = content_type.to_str().unwrap_or("");
265
1
        // Should be HTML for the table template
266
1
        assert!(
267
1
            content_type_str.contains("text/html") || content_type_str.contains("text/"),
268
1
            "Unexpected content type: {content_type_str}"
269
1
        );
270
1
    }
271
1
}
272

            
273
#[tokio::test]
274
1
async fn test_commodity_table_listing_integration() {
275
1
    let app_state = create_test_app_state().await;
276
1
    let mock_user = create_mock_user();
277
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
278

            
279
    // Create a router with both commodity creation and listing endpoints
280
1
    let app = Router::new()
281
1
        .route(
282
1
            "/commodity/create/submit",
283
1
            axum::routing::post(web::pages::commodity::create::submit::commodity_submit),
284
        )
285
1
        .route(
286
1
            "/commodity/list",
287
1
            axum::routing::get(web::pages::commodity::list::commodity_table),
288
        )
289
1
        .layer(axum::middleware::from_fn_with_state(
290
1
            app_state.clone(),
291
3
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
292
3
                let jwt_auth = jwt_auth.clone();
293
3
                async move {
294
3
                    req.extensions_mut().insert(jwt_auth);
295
3
                    next.run(req).await
296
3
                }
297
3
            },
298
        ))
299
1
        .with_state(app_state.clone());
300

            
301
    // First, create some test commodities
302
1
    let usd_commodity = serde_json::json!({
303
1
        "fraction": "100",
304
1
        "symbol": "USD",
305
1
        "name": "US Dollar"
306
    });
307

            
308
1
    let eur_commodity = serde_json::json!({
309
1
        "fraction": "100",
310
1
        "symbol": "EUR",
311
1
        "name": "Euro"
312
    });
313

            
314
    // Create USD commodity
315
1
    let create_response = app
316
1
        .clone()
317
1
        .oneshot(
318
1
            Request::builder()
319
1
                .method("POST")
320
1
                .uri("/commodity/create/submit")
321
1
                .header("content-type", "application/json")
322
1
                .body(Body::from(usd_commodity.to_string()))
323
1
                .unwrap(),
324
1
        )
325
1
        .await
326
1
        .unwrap();
327

            
328
    // Should succeed or fail gracefully (DB might not be available in test environment)
329
1
    assert!(
330
1
        create_response.status().is_success() || create_response.status().is_server_error(),
331
        "USD commodity creation failed with unexpected status: {}",
332
        create_response.status()
333
    );
334

            
335
    // Create EUR commodity
336
1
    let create_response = app
337
1
        .clone()
338
1
        .oneshot(
339
1
            Request::builder()
340
1
                .method("POST")
341
1
                .uri("/commodity/create/submit")
342
1
                .header("content-type", "application/json")
343
1
                .body(Body::from(eur_commodity.to_string()))
344
1
                .unwrap(),
345
1
        )
346
1
        .await
347
1
        .unwrap();
348

            
349
    // Should succeed or fail gracefully
350
1
    assert!(
351
1
        create_response.status().is_success() || create_response.status().is_server_error(),
352
        "EUR commodity creation failed with unexpected status: {}",
353
        create_response.status()
354
    );
355

            
356
    // Now test the commodity listing
357
1
    let list_response = app
358
1
        .oneshot(
359
1
            Request::builder()
360
1
                .method("GET")
361
1
                .uri("/commodity/list")
362
1
                .body(Body::empty())
363
1
                .unwrap(),
364
1
        )
365
1
        .await
366
1
        .unwrap();
367

            
368
    // This test verifies the complete ListCommodities integration in the web layer
369
    // It tests that the commodity_table handler correctly calls the ListCommodities command
370
    // and renders the HTML table template with the results
371
1
    assert!(
372
1
        list_response.status().is_success() || list_response.status().is_server_error(),
373
        "Expected success or server error for commodity list, got: {}",
374
        list_response.status()
375
    );
376

            
377
    // Verify response has correct content type for HTML table
378
1
    if let Some(content_type) = list_response.headers().get("content-type") {
379
1
        let content_type_str = content_type.to_str().unwrap_or("");
380
1
        assert!(
381
1
            content_type_str.contains("text/html") || content_type_str.contains("text/"),
382
1
            "Expected HTML content type for commodity table, got: {content_type_str}"
383
1
        );
384
1
    }
385
1

            
386
1
    // If we get a successful response, check that it's actually HTML table content
387
1
    if list_response.status().is_success() {
388
1
        let body = axum::body::to_bytes(list_response.into_body(), usize::MAX)
389
            .await
390
1
            .unwrap();
391
1
        let body_str = String::from_utf8(body.to_vec()).unwrap();
392
1

            
393
1
        // The response should contain HTML table elements for the commodity list
394
1
        // This verifies that the ListCommodities command result is properly rendered
395
1
        assert!(
396
1
            body_str.contains("<table") || body_str.contains("commodity") || !body_str.is_empty(),
397
1
            "Expected HTML table content in commodity list response, got empty or non-HTML content"
398
1
        );
399
1
    }
400
1
}
401

            
402
#[tokio::test]
403
1
async fn test_commodity_search_without_auth() {
404
1
    let app_state = create_test_app_state().await;
405
1
    let app = Router::new()
406
1
        .route(
407
1
            "/commodity/search",
408
1
            axum::routing::post(web::pages::commodity::search::search_commodities),
409
        )
410
1
        .with_state(app_state);
411

            
412
1
    let search_data = serde_json::json!({
413
1
        "commodity-search": "USD"
414
    });
415

            
416
1
    let response = app
417
1
        .oneshot(
418
1
            Request::builder()
419
1
                .method("POST")
420
1
                .uri("/commodity/search")
421
1
                .header("content-type", "application/json")
422
1
                .body(Body::from(search_data.to_string()))
423
1
                .unwrap(),
424
1
        )
425
1
        .await
426
1
        .unwrap();
427

            
428
    // Should fail without authentication - expecting 401 or 500
429
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
430
1
}
431

            
432
#[tokio::test]
433
1
async fn test_commodity_search_with_mock_auth() {
434
1
    let app_state = create_test_app_state().await;
435
1
    let mock_user = create_mock_user();
436
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
437

            
438
1
    let app = Router::new()
439
1
        .route(
440
1
            "/commodity/search",
441
1
            axum::routing::post(web::pages::commodity::search::search_commodities),
442
        )
443
1
        .layer(axum::middleware::from_fn_with_state(
444
1
            app_state.clone(),
445
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
446
1
                let jwt_auth = jwt_auth.clone();
447
1
                async move {
448
1
                    req.extensions_mut().insert(jwt_auth);
449
1
                    next.run(req).await
450
1
                }
451
1
            },
452
        ))
453
1
        .with_state(app_state.clone());
454

            
455
1
    let search_data = serde_json::json!({
456
1
        "commodity-search": "TST"
457
    });
458

            
459
1
    let response = app
460
1
        .oneshot(
461
1
            Request::builder()
462
1
                .method("POST")
463
1
                .uri("/commodity/search")
464
1
                .header("content-type", "application/json")
465
1
                .body(Body::from(search_data.to_string()))
466
1
                .unwrap(),
467
1
        )
468
1
        .await
469
1
        .unwrap();
470

            
471
    // This test verifies our ListCommodities macro integration works in search
472
    // Even if it fails due to DB issues, it should not be a JSON parsing error
473
    // We expect either success (200) or a server error (500) due to missing DB
474
    // but NOT a client error (400) which would indicate API issues
475
1
    assert!(
476
1
        response.status().is_success() || response.status().is_server_error(),
477
        "Expected success or server error, got: {}",
478
        response.status()
479
    );
480

            
481
    // If we get a response, verify it has proper content type (JSON)
482
1
    if let Some(content_type) = response.headers().get("content-type") {
483
1
        let content_type_str = content_type.to_str().unwrap_or("");
484
1
        // Should be JSON for the search results
485
1
        assert!(
486
1
            content_type_str.contains("application/json"),
487
1
            "Unexpected content type: {content_type_str}"
488
1
        );
489
1
    }
490
1
}
491

            
492
#[tokio::test]
493
1
async fn test_commodity_search_with_invalid_json() {
494
1
    let app_state = create_test_app_state().await;
495
1
    let mock_user = create_mock_user();
496
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
497

            
498
1
    let app = Router::new()
499
1
        .route(
500
1
            "/commodity/search",
501
1
            axum::routing::post(web::pages::commodity::search::search_commodities),
502
        )
503
1
        .layer(axum::middleware::from_fn_with_state(
504
1
            app_state.clone(),
505
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
506
1
                let jwt_auth = jwt_auth.clone();
507
1
                async move {
508
1
                    req.extensions_mut().insert(jwt_auth);
509
1
                    next.run(req).await
510
1
                }
511
1
            },
512
        ))
513
1
        .with_state(app_state.clone());
514

            
515
1
    let response = app
516
1
        .oneshot(
517
1
            Request::builder()
518
1
                .method("POST")
519
1
                .uri("/commodity/search")
520
1
                .header("content-type", "application/json")
521
1
                .body(Body::from("invalid json"))
522
1
                .unwrap(),
523
1
        )
524
1
        .await
525
1
        .unwrap();
526

            
527
    // Should return 400 for invalid JSON or other error
528
1
    assert!(
529
1
        response.status() == StatusCode::BAD_REQUEST || response.status().is_server_error(),
530
1
        "Expected 400 or server error, got: {}",
531
1
        response.status()
532
1
    );
533
1
}
534

            
535
#[tokio::test]
536
1
async fn test_get_commodity_account_balance_integration() {
537
1
    let app_state = create_test_app_state().await;
538
1
    let mock_user = create_mock_user();
539
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
540

            
541
1
    let app = Router::new()
542
1
        .route(
543
1
            "/account/list",
544
1
            axum::routing::get(web::pages::account::list::account_table),
545
        )
546
1
        .layer(axum::middleware::from_fn_with_state(
547
1
            app_state.clone(),
548
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
549
1
                let jwt_auth = jwt_auth.clone();
550
1
                async move {
551
1
                    req.extensions_mut().insert(jwt_auth);
552
1
                    next.run(req).await
553
1
                }
554
1
            },
555
        ))
556
1
        .with_state(app_state.clone());
557

            
558
1
    let response = app
559
1
        .oneshot(
560
1
            Request::builder()
561
1
                .method("GET")
562
1
                .uri("/account/list")
563
1
                .body(Body::empty())
564
1
                .unwrap(),
565
1
        )
566
1
        .await
567
1
        .unwrap();
568

            
569
    // This test verifies GetCommodity is working correctly for account balance currency display
570
    // Even with DB connection issues, it should handle the GetCommodity calls gracefully
571
1
    assert!(
572
1
        response.status().is_success() || response.status().is_server_error(),
573
1
        "Expected success or server error, got: {}",
574
1
        response.status()
575
1
    );
576
1
}
577

            
578
#[tokio::test]
579
1
async fn test_get_commodity_transaction_currency_integration() {
580
1
    let app_state = create_test_app_state().await;
581
1
    let mock_user = create_mock_user();
582
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
583

            
584
1
    let app = Router::new()
585
1
        .route(
586
1
            "/transaction/list",
587
1
            axum::routing::get(web::pages::transaction::list::transaction_table),
588
        )
589
1
        .layer(axum::middleware::from_fn_with_state(
590
1
            app_state.clone(),
591
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
592
1
                let jwt_auth = jwt_auth.clone();
593
1
                async move {
594
1
                    req.extensions_mut().insert(jwt_auth);
595
1
                    next.run(req).await
596
1
                }
597
1
            },
598
        ))
599
1
        .with_state(app_state.clone());
600

            
601
1
    let response = app
602
1
        .oneshot(
603
1
            Request::builder()
604
1
                .method("GET")
605
1
                .uri("/transaction/list")
606
1
                .body(Body::empty())
607
1
                .unwrap(),
608
1
        )
609
1
        .await
610
1
        .unwrap();
611

            
612
    // This test verifies GetCommodity integration in transaction currency display
613
    // The macro-based GetCommodity should work correctly for currency symbol lookup
614
1
    assert!(
615
1
        response.status().is_success() || response.status().is_server_error(),
616
1
        "Expected success or server error, got: {}",
617
1
        response.status()
618
1
    );
619
1
}
620

            
621
#[tokio::test]
622
1
async fn test_get_commodity_transaction_create_integration() {
623
1
    let app_state = create_test_app_state().await;
624
1
    let mock_user = create_mock_user();
625
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
626

            
627
1
    let app = Router::new()
628
1
        .route(
629
1
            "/transaction/create/submit",
630
1
            axum::routing::post(web::pages::transaction::create::submit::transaction_submit),
631
        )
632
1
        .layer(axum::middleware::from_fn_with_state(
633
1
            app_state.clone(),
634
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
635
1
                let jwt_auth = jwt_auth.clone();
636
1
                async move {
637
1
                    req.extensions_mut().insert(jwt_auth);
638
1
                    next.run(req).await
639
1
                }
640
1
            },
641
        ))
642
1
        .with_state(app_state.clone());
643

            
644
1
    let transaction_data = serde_json::json!({
645
1
        "splits": [{
646
1
            "amount": "100.00",
647
1
            "amount_converted": "100.00",
648
1
            "from_account": "00000000-0000-0000-0000-000000000001",
649
1
            "to_account": "00000000-0000-0000-0000-000000000002",
650
1
            "from_commodity": "00000000-0000-0000-0000-000000000003",
651
1
            "to_commodity": "00000000-0000-0000-0000-000000000003"
652
        }],
653
1
        "note": "Test transaction",
654
1
        "date": "2024-01-01T00:00:00Z"
655
    });
656

            
657
1
    let response = app
658
1
        .oneshot(
659
1
            Request::builder()
660
1
                .method("POST")
661
1
                .uri("/transaction/create/submit")
662
1
                .header("content-type", "application/json")
663
1
                .body(Body::from(transaction_data.to_string()))
664
1
                .unwrap(),
665
1
        )
666
1
        .await
667
1
        .unwrap();
668

            
669
    // This test verifies GetCommodity integration in transaction creation for commodity fraction validation
670
    // The macro-based GetCommodity should be called during transaction processing
671
1
    assert!(
672
1
        response.status().is_success() || response.status().is_server_error(),
673
1
        "Expected success or server error, got: {}",
674
1
        response.status()
675
1
    );
676
1
}