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

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

            
11
3
fn make_convert_app(
12
3
    app_state: std::sync::Arc<web::AppState>,
13
3
    jwt_auth: web::jwt_auth::JWTAuthMiddleware,
14
3
) -> Router {
15
3
    Router::new()
16
3
        .route(
17
3
            "/commodity/convert/submit",
18
3
            post(web::pages::commodity::convert::submit::commodity_convert_submit),
19
        )
20
3
        .layer(axum::middleware::from_fn_with_state(
21
3
            app_state.clone(),
22
3
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
23
3
                let jwt_auth = jwt_auth.clone();
24
3
                async move {
25
3
                    req.extensions_mut().insert(jwt_auth);
26
3
                    next.run(req).await
27
3
                }
28
3
            },
29
        ))
30
3
        .with_state(app_state)
31
3
}
32

            
33
#[tokio::test]
34
1
async fn test_commodity_convert_without_auth() {
35
1
    let app_state = create_test_app_state().await;
36
1
    let app = Router::new()
37
1
        .route(
38
1
            "/commodity/convert/submit",
39
1
            post(web::pages::commodity::convert::submit::commodity_convert_submit),
40
        )
41
1
        .with_state(app_state);
42

            
43
1
    let response = app
44
1
        .oneshot(
45
1
            Request::builder()
46
1
                .method("POST")
47
1
                .uri("/commodity/convert/submit")
48
1
                .header("content-type", "application/x-www-form-urlencoded")
49
1
                .body(Body::from(
50
1
                    "amount=100&source_commodity_id=00000000-0000-0000-0000-000000000001\
51
1
                     &target_commodity_id=00000000-0000-0000-0000-000000000002",
52
1
                ))
53
1
                .unwrap(),
54
1
        )
55
1
        .await
56
1
        .unwrap();
57

            
58
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
59
1
}
60

            
61
#[tokio::test]
62
1
async fn test_commodity_convert_invalid_amount() {
63
1
    let app_state = create_test_app_state().await;
64
1
    let mock_user = create_mock_user();
65
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
66
1
    let app = make_convert_app(app_state, jwt_auth);
67

            
68
1
    let response = app
69
1
        .oneshot(
70
1
            Request::builder()
71
1
                .method("POST")
72
1
                .uri("/commodity/convert/submit")
73
1
                .header("content-type", "application/x-www-form-urlencoded")
74
1
                .body(Body::from(
75
1
                    "amount=not-a-number\
76
1
                     &source_commodity_id=00000000-0000-0000-0000-000000000001\
77
1
                     &target_commodity_id=00000000-0000-0000-0000-000000000002",
78
1
                ))
79
1
                .unwrap(),
80
1
        )
81
1
        .await
82
1
        .unwrap();
83

            
84
    // Invalid amount → error fragment returned with 200 (HTMX pattern)
85
1
    assert!(
86
1
        response.status().is_success() || response.status() == StatusCode::BAD_REQUEST,
87
        "Expected success or 400 for invalid amount, got: {}",
88
        response.status()
89
    );
90

            
91
1
    if response.status().is_success() {
92
1
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
93
1
            .await
94
1
            .unwrap();
95
1
        let body_str = String::from_utf8(body.to_vec()).unwrap();
96
1
        assert!(
97
1
            body_str.contains("error") || body_str.contains("Invalid"),
98
1
            "Expected error content in response for invalid amount, got: {body_str}"
99
1
        );
100
1
    }
101
1
}
102

            
103
#[tokio::test]
104
1
async fn test_commodity_convert_invalid_uuid() {
105
1
    let app_state = create_test_app_state().await;
106
1
    let mock_user = create_mock_user();
107
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
108
1
    let app = make_convert_app(app_state, jwt_auth);
109

            
110
1
    let response = app
111
1
        .oneshot(
112
1
            Request::builder()
113
1
                .method("POST")
114
1
                .uri("/commodity/convert/submit")
115
1
                .header("content-type", "application/x-www-form-urlencoded")
116
1
                .body(Body::from(
117
1
                    "amount=100&source_commodity_id=not-a-uuid\
118
1
                     &target_commodity_id=00000000-0000-0000-0000-000000000002",
119
1
                ))
120
1
                .unwrap(),
121
1
        )
122
1
        .await
123
1
        .unwrap();
124

            
125
    // Invalid UUID → error fragment returned with 200 (HTMX pattern)
126
1
    assert!(
127
1
        response.status().is_success(),
128
        "Expected success (HTMX error fragment) for invalid UUID, got: {}",
129
        response.status()
130
    );
131

            
132
1
    let body = axum::body::to_bytes(response.into_body(), usize::MAX)
133
1
        .await
134
1
        .unwrap();
135
1
    let body_str = String::from_utf8(body.to_vec()).unwrap();
136
1
    assert!(
137
1
        body_str.contains("error") || body_str.contains("Invalid"),
138
1
        "Expected error content for invalid UUID, got: {body_str}"
139
1
    );
140
1
}
141

            
142
#[tokio::test]
143
1
async fn test_commodity_convert_no_price_row() {
144
1
    let app_state = create_test_app_state().await;
145
1
    let mock_user = create_mock_user();
146
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
147
1
    let app = make_convert_app(app_state, jwt_auth);
148

            
149
    // Valid UUIDs but no price row exists between them in test DB
150
1
    let response = app
151
1
        .oneshot(
152
1
            Request::builder()
153
1
                .method("POST")
154
1
                .uri("/commodity/convert/submit")
155
1
                .header("content-type", "application/x-www-form-urlencoded")
156
1
                .body(Body::from(
157
1
                    "amount=100\
158
1
                     &source_commodity_id=00000000-0000-0000-0000-000000000001\
159
1
                     &target_commodity_id=00000000-0000-0000-0000-000000000002",
160
1
                ))
161
1
                .unwrap(),
162
1
        )
163
1
        .await
164
1
        .unwrap();
165

            
166
    // Either an HTMX error fragment (200) or a server error is acceptable
167
1
    assert!(
168
1
        response.status().is_success() || response.status().is_server_error(),
169
        "Expected success or server error, got: {}",
170
        response.status()
171
    );
172

            
173
1
    if response.status().is_success() {
174
1
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
175
1
            .await
176
1
            .unwrap();
177
1
        let body_str = String::from_utf8(body.to_vec()).unwrap();
178
1
        // Should contain an error message about missing price row
179
1
        assert!(
180
1
            body_str.contains("error") || body_str.contains("exchange") || body_str.contains("No"),
181
1
            "Expected error fragment for missing price row, got: {body_str}"
182
1
        );
183
1
    }
184
1
}
185

            
186
#[tokio::test]
187
1
async fn test_commodity_convert_page_with_auth() {
188
1
    let app_state = create_test_app_state().await;
189
1
    let mock_user = create_mock_user();
190
1
    let jwt_auth = create_mock_jwt_auth(mock_user);
191

            
192
1
    let app = Router::new()
193
1
        .route(
194
1
            "/commodity/convert",
195
1
            get(web::pages::commodity::convert::submit::commodity_convert_page),
196
        )
197
1
        .layer(axum::middleware::from_fn_with_state(
198
1
            app_state.clone(),
199
1
            move |mut req: axum::http::Request<Body>, next: axum::middleware::Next| {
200
1
                let jwt_auth = jwt_auth.clone();
201
1
                async move {
202
1
                    req.extensions_mut().insert(jwt_auth);
203
1
                    next.run(req).await
204
1
                }
205
1
            },
206
        ))
207
1
        .with_state(app_state);
208

            
209
1
    let response = app
210
1
        .oneshot(
211
1
            Request::builder()
212
1
                .method("GET")
213
1
                .uri("/commodity/convert")
214
1
                .body(Body::empty())
215
1
                .unwrap(),
216
1
        )
217
1
        .await
218
1
        .unwrap();
219

            
220
1
    assert!(
221
1
        response.status().is_success() || response.status().is_server_error(),
222
        "Expected success or server error for convert page, got: {}",
223
        response.status()
224
    );
225

            
226
1
    if response.status().is_success() {
227
1
        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
228
            .await
229
1
            .unwrap();
230
1
        let body_str = String::from_utf8(body.to_vec()).unwrap();
231
1
        assert!(
232
1
            body_str.contains("convert") || body_str.contains("Convert"),
233
1
            "Expected convert page content, got: {body_str}"
234
1
        );
235
1
    }
236
1
}