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
        "symbol": "USD",
24
1
        "name": "US Dollar"
25
    });
26

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

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

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

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

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

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

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

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

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

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

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

            
134
#[tokio::test]
135
1
async fn test_commodity_table_without_auth() {
136
1
    let app_state = create_test_app_state().await;
137
1
    let app = Router::new()
138
1
        .route(
139
1
            "/commodity/list",
140
1
            axum::routing::get(web::pages::commodity::list::commodity_table),
141
        )
142
1
        .with_state(app_state);
143

            
144
1
    let response = app
145
1
        .oneshot(
146
1
            Request::builder()
147
1
                .method("GET")
148
1
                .uri("/commodity/list")
149
1
                .body(Body::empty())
150
1
                .unwrap(),
151
1
        )
152
1
        .await
153
1
        .unwrap();
154

            
155
    // Should fail without authentication - expecting 401 or 500
156
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
157
1
}
158

            
159
#[tokio::test]
160
1
async fn test_commodity_table_with_mock_auth() {
161
1
    let app_state = create_test_app_state().await;
162
1
    let mock_user = create_mock_user();
163
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
164

            
165
1
    let app = Router::new()
166
1
        .route(
167
1
            "/commodity/list",
168
1
            axum::routing::get(web::pages::commodity::list::commodity_table),
169
        )
170
1
        .layer(axum::middleware::from_fn_with_state(
171
1
            app_state.clone(),
172
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
173
1
                let jwt_auth = jwt_auth.clone();
174
1
                async move {
175
1
                    req.extensions_mut().insert(jwt_auth);
176
1
                    next.run(req).await
177
1
                }
178
1
            },
179
        ))
180
1
        .with_state(app_state.clone());
181

            
182
1
    let response = app
183
1
        .oneshot(
184
1
            Request::builder()
185
1
                .method("GET")
186
1
                .uri("/commodity/list")
187
1
                .body(Body::empty())
188
1
                .unwrap(),
189
1
        )
190
1
        .await
191
1
        .unwrap();
192

            
193
    // This test verifies our ListCommodities macro integration works
194
    // Even if it fails due to DB issues, it should not be a parsing error
195
    // We expect either success (200) or a server error (500) due to missing DB
196
    // but NOT a client error (400) which would indicate API issues
197
1
    assert!(
198
1
        response.status().is_success() || response.status().is_server_error(),
199
        "Expected success or server error, got: {}",
200
        response.status()
201
    );
202

            
203
    // If we get a response, verify it has proper content type (HTML)
204
1
    if let Some(content_type) = response.headers().get("content-type") {
205
1
        let content_type_str = content_type.to_str().unwrap_or("");
206
1
        // Should be HTML for the table template
207
1
        assert!(
208
1
            content_type_str.contains("text/html") || content_type_str.contains("text/"),
209
1
            "Unexpected content type: {content_type_str}"
210
1
        );
211
1
    }
212
1
}
213

            
214
#[tokio::test]
215
1
async fn test_commodity_table_listing_integration() {
216
1
    let app_state = create_test_app_state().await;
217
1
    let mock_user = create_mock_user();
218
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
219

            
220
    // Create a router with both commodity creation and listing endpoints
221
1
    let app = Router::new()
222
1
        .route(
223
1
            "/commodity/create/submit",
224
1
            axum::routing::post(web::pages::commodity::create::submit::commodity_submit),
225
        )
226
1
        .route(
227
1
            "/commodity/list",
228
1
            axum::routing::get(web::pages::commodity::list::commodity_table),
229
        )
230
1
        .layer(axum::middleware::from_fn_with_state(
231
1
            app_state.clone(),
232
3
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
233
3
                let jwt_auth = jwt_auth.clone();
234
3
                async move {
235
3
                    req.extensions_mut().insert(jwt_auth);
236
3
                    next.run(req).await
237
3
                }
238
3
            },
239
        ))
240
1
        .with_state(app_state.clone());
241

            
242
    // First, create some test commodities
243
1
    let usd_commodity = serde_json::json!({
244
1
        "symbol": "USD",
245
1
        "name": "US Dollar"
246
    });
247

            
248
1
    let eur_commodity = serde_json::json!({
249
1
        "symbol": "EUR",
250
1
        "name": "Euro"
251
    });
252

            
253
    // Create USD commodity
254
1
    let create_response = app
255
1
        .clone()
256
1
        .oneshot(
257
1
            Request::builder()
258
1
                .method("POST")
259
1
                .uri("/commodity/create/submit")
260
1
                .header("content-type", "application/json")
261
1
                .body(Body::from(usd_commodity.to_string()))
262
1
                .unwrap(),
263
1
        )
264
1
        .await
265
1
        .unwrap();
266

            
267
    // Should succeed or fail gracefully (DB might not be available in test environment)
268
1
    assert!(
269
1
        create_response.status().is_success() || create_response.status().is_server_error(),
270
        "USD commodity creation failed with unexpected status: {}",
271
        create_response.status()
272
    );
273

            
274
    // Create EUR commodity
275
1
    let create_response = app
276
1
        .clone()
277
1
        .oneshot(
278
1
            Request::builder()
279
1
                .method("POST")
280
1
                .uri("/commodity/create/submit")
281
1
                .header("content-type", "application/json")
282
1
                .body(Body::from(eur_commodity.to_string()))
283
1
                .unwrap(),
284
1
        )
285
1
        .await
286
1
        .unwrap();
287

            
288
    // Should succeed or fail gracefully
289
1
    assert!(
290
1
        create_response.status().is_success() || create_response.status().is_server_error(),
291
        "EUR commodity creation failed with unexpected status: {}",
292
        create_response.status()
293
    );
294

            
295
    // Now test the commodity listing
296
1
    let list_response = app
297
1
        .oneshot(
298
1
            Request::builder()
299
1
                .method("GET")
300
1
                .uri("/commodity/list")
301
1
                .body(Body::empty())
302
1
                .unwrap(),
303
1
        )
304
1
        .await
305
1
        .unwrap();
306

            
307
    // This test verifies the complete ListCommodities integration in the web layer
308
    // It tests that the commodity_table handler correctly calls the ListCommodities command
309
    // and renders the HTML table template with the results
310
1
    assert!(
311
1
        list_response.status().is_success() || list_response.status().is_server_error(),
312
        "Expected success or server error for commodity list, got: {}",
313
        list_response.status()
314
    );
315

            
316
    // Verify response has correct content type for HTML table
317
1
    if let Some(content_type) = list_response.headers().get("content-type") {
318
1
        let content_type_str = content_type.to_str().unwrap_or("");
319
1
        assert!(
320
1
            content_type_str.contains("text/html") || content_type_str.contains("text/"),
321
1
            "Expected HTML content type for commodity table, got: {content_type_str}"
322
1
        );
323
1
    }
324
1

            
325
1
    // If we get a successful response, check that it's actually HTML table content
326
1
    if list_response.status().is_success() {
327
1
        let body = axum::body::to_bytes(list_response.into_body(), usize::MAX)
328
            .await
329
1
            .unwrap();
330
1
        let body_str = String::from_utf8(body.to_vec()).unwrap();
331
1

            
332
1
        // The response should contain HTML table elements for the commodity list
333
1
        // This verifies that the ListCommodities command result is properly rendered
334
1
        assert!(
335
1
            body_str.contains("<table") || body_str.contains("commodity") || !body_str.is_empty(),
336
1
            "Expected HTML table content in commodity list response, got empty or non-HTML content"
337
1
        );
338
1
    }
339
1
}
340

            
341
#[tokio::test]
342
1
async fn test_commodity_search_without_auth() {
343
1
    let app_state = create_test_app_state().await;
344
1
    let app = Router::new()
345
1
        .route(
346
1
            "/commodity/search",
347
1
            axum::routing::post(web::pages::commodity::search::search_commodities),
348
        )
349
1
        .with_state(app_state);
350

            
351
1
    let search_data = serde_json::json!({
352
1
        "commodity-search": "USD"
353
    });
354

            
355
1
    let response = app
356
1
        .oneshot(
357
1
            Request::builder()
358
1
                .method("POST")
359
1
                .uri("/commodity/search")
360
1
                .header("content-type", "application/json")
361
1
                .body(Body::from(search_data.to_string()))
362
1
                .unwrap(),
363
1
        )
364
1
        .await
365
1
        .unwrap();
366

            
367
    // Should fail without authentication - expecting 401 or 500
368
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
369
1
}
370

            
371
#[tokio::test]
372
1
async fn test_commodity_search_with_mock_auth() {
373
1
    let app_state = create_test_app_state().await;
374
1
    let mock_user = create_mock_user();
375
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
376

            
377
1
    let app = Router::new()
378
1
        .route(
379
1
            "/commodity/search",
380
1
            axum::routing::post(web::pages::commodity::search::search_commodities),
381
        )
382
1
        .layer(axum::middleware::from_fn_with_state(
383
1
            app_state.clone(),
384
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
385
1
                let jwt_auth = jwt_auth.clone();
386
1
                async move {
387
1
                    req.extensions_mut().insert(jwt_auth);
388
1
                    next.run(req).await
389
1
                }
390
1
            },
391
        ))
392
1
        .with_state(app_state.clone());
393

            
394
1
    let search_data = serde_json::json!({
395
1
        "commodity-search": "TST"
396
    });
397

            
398
1
    let response = app
399
1
        .oneshot(
400
1
            Request::builder()
401
1
                .method("POST")
402
1
                .uri("/commodity/search")
403
1
                .header("content-type", "application/json")
404
1
                .body(Body::from(search_data.to_string()))
405
1
                .unwrap(),
406
1
        )
407
1
        .await
408
1
        .unwrap();
409

            
410
    // This test verifies our ListCommodities macro integration works in search
411
    // Even if it fails due to DB issues, it should not be a JSON parsing error
412
    // We expect either success (200) or a server error (500) due to missing DB
413
    // but NOT a client error (400) which would indicate API issues
414
1
    assert!(
415
1
        response.status().is_success() || response.status().is_server_error(),
416
        "Expected success or server error, got: {}",
417
        response.status()
418
    );
419

            
420
    // If we get a response, verify it has proper content type (JSON)
421
1
    if let Some(content_type) = response.headers().get("content-type") {
422
1
        let content_type_str = content_type.to_str().unwrap_or("");
423
1
        // Should be JSON for the search results
424
1
        assert!(
425
1
            content_type_str.contains("application/json"),
426
1
            "Unexpected content type: {content_type_str}"
427
1
        );
428
1
    }
429
1
}
430

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

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

            
454
1
    let response = app
455
1
        .oneshot(
456
1
            Request::builder()
457
1
                .method("POST")
458
1
                .uri("/commodity/search")
459
1
                .header("content-type", "application/json")
460
1
                .body(Body::from("invalid json"))
461
1
                .unwrap(),
462
1
        )
463
1
        .await
464
1
        .unwrap();
465

            
466
    // Should return 400 for invalid JSON or other error
467
1
    assert!(
468
1
        response.status() == StatusCode::BAD_REQUEST || response.status().is_server_error(),
469
1
        "Expected 400 or server error, got: {}",
470
1
        response.status()
471
1
    );
472
1
}
473

            
474
#[tokio::test]
475
1
async fn test_get_commodity_account_balance_integration() {
476
1
    let app_state = create_test_app_state().await;
477
1
    let mock_user = create_mock_user();
478
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
479

            
480
1
    let app = Router::new()
481
1
        .route(
482
1
            "/account/list",
483
1
            axum::routing::get(web::pages::account::list::account_table),
484
        )
485
1
        .layer(axum::middleware::from_fn_with_state(
486
1
            app_state.clone(),
487
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
488
1
                let jwt_auth = jwt_auth.clone();
489
1
                async move {
490
1
                    req.extensions_mut().insert(jwt_auth);
491
1
                    next.run(req).await
492
1
                }
493
1
            },
494
        ))
495
1
        .with_state(app_state.clone());
496

            
497
1
    let response = app
498
1
        .oneshot(
499
1
            Request::builder()
500
1
                .method("GET")
501
1
                .uri("/account/list")
502
1
                .body(Body::empty())
503
1
                .unwrap(),
504
1
        )
505
1
        .await
506
1
        .unwrap();
507

            
508
    // This test verifies GetCommodity is working correctly for account balance currency display
509
    // Even with DB connection issues, it should handle the GetCommodity calls gracefully
510
1
    assert!(
511
1
        response.status().is_success() || response.status().is_server_error(),
512
1
        "Expected success or server error, got: {}",
513
1
        response.status()
514
1
    );
515
1
}
516

            
517
#[tokio::test]
518
1
async fn test_get_commodity_transaction_currency_integration() {
519
1
    let app_state = create_test_app_state().await;
520
1
    let mock_user = create_mock_user();
521
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
522

            
523
1
    let app = Router::new()
524
1
        .route(
525
1
            "/transaction/list",
526
1
            axum::routing::get(web::pages::transaction::list::transaction_table),
527
        )
528
1
        .layer(axum::middleware::from_fn_with_state(
529
1
            app_state.clone(),
530
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
531
1
                let jwt_auth = jwt_auth.clone();
532
1
                async move {
533
1
                    req.extensions_mut().insert(jwt_auth);
534
1
                    next.run(req).await
535
1
                }
536
1
            },
537
        ))
538
1
        .with_state(app_state.clone());
539

            
540
1
    let response = app
541
1
        .oneshot(
542
1
            Request::builder()
543
1
                .method("GET")
544
1
                .uri("/transaction/list")
545
1
                .body(Body::empty())
546
1
                .unwrap(),
547
1
        )
548
1
        .await
549
1
        .unwrap();
550

            
551
    // This test verifies GetCommodity integration in transaction currency display
552
    // The macro-based GetCommodity should work correctly for currency symbol lookup
553
1
    assert!(
554
1
        response.status().is_success() || response.status().is_server_error(),
555
1
        "Expected success or server error, got: {}",
556
1
        response.status()
557
1
    );
558
1
}
559

            
560
#[tokio::test]
561
1
async fn test_get_commodity_transaction_create_integration() {
562
1
    let app_state = create_test_app_state().await;
563
1
    let mock_user = create_mock_user();
564
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
565

            
566
1
    let app = Router::new()
567
1
        .route(
568
1
            "/transaction/create/submit",
569
1
            axum::routing::post(web::pages::transaction::create::submit::transaction_submit),
570
        )
571
1
        .layer(axum::middleware::from_fn_with_state(
572
1
            app_state.clone(),
573
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
574
1
                let jwt_auth = jwt_auth.clone();
575
1
                async move {
576
1
                    req.extensions_mut().insert(jwt_auth);
577
1
                    next.run(req).await
578
1
                }
579
1
            },
580
        ))
581
1
        .with_state(app_state.clone());
582

            
583
1
    let transaction_data = serde_json::json!({
584
1
        "splits": [{
585
1
            "amount": "100.00",
586
1
            "amount_converted": "100.00",
587
1
            "from_account": "00000000-0000-0000-0000-000000000001",
588
1
            "to_account": "00000000-0000-0000-0000-000000000002",
589
1
            "from_commodity": "00000000-0000-0000-0000-000000000003",
590
1
            "to_commodity": "00000000-0000-0000-0000-000000000003"
591
        }],
592
1
        "note": "Test transaction",
593
1
        "date": "2024-01-01T00:00:00Z"
594
    });
595

            
596
1
    let response = app
597
1
        .oneshot(
598
1
            Request::builder()
599
1
                .method("POST")
600
1
                .uri("/transaction/create/submit")
601
1
                .header("content-type", "application/json")
602
1
                .body(Body::from(transaction_data.to_string()))
603
1
                .unwrap(),
604
1
        )
605
1
        .await
606
1
        .unwrap();
607

            
608
    // This test verifies transaction creation integration
609
    // The macro-based commands should be called during transaction processing
610
1
    assert!(
611
1
        response.status().is_success() || response.status().is_server_error(),
612
1
        "Expected success or server error, got: {}",
613
1
        response.status()
614
1
    );
615
1
}