tests/http_tests.rs 9.6 K raw
1
use std::collections::HashMap;
2
3
use cielago::http::{fetch_token, send_request, send_with_auth};
4
use cielago::model::{AuthKind, AuthStyle, KeyValueRow, Method, OAuthConfig, SavedRequest};
5
use wiremock::matchers::{method, path, query_param};
6
use wiremock::{Mock, MockServer, ResponseTemplate};
7
8
#[tokio::test]
9
async fn sends_request_with_params_and_uuid_header() {
10
    let server = MockServer::start().await;
11
    Mock::given(method("GET"))
12
        .and(path("/pets/123"))
13
        .and(query_param("limit", "10"))
14
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true})))
15
        .mount(&server)
16
        .await;
17
18
    let mut req = SavedRequest::blank("get pet");
19
    req.method = Method::Get;
20
    req.path = "/pets/{petId}".into();
21
    req.path_params.push(KeyValueRow::new("petId", "123", true));
22
    req.query.push(KeyValueRow::new("limit", "10", true));
23
    req.query.push(KeyValueRow::new("disabled", "x", false));
24
    req.headers
25
        .push(KeyValueRow::new("X-Request-Id", "{{uuid}}", true));
26
    req.headers
27
        .push(KeyValueRow::new("X-Tenant", "{{tenant}}", true));
28
29
    let vars = HashMap::from([("tenant".to_string(), "acme".to_string())]);
30
    let client = reqwest::Client::new();
31
    let resp = send_request(&client, &server.uri(), &req, &vars, None, &[])
32
        .await
33
        .unwrap();
34
35
    assert_eq!(resp.status, 200);
36
    assert!(resp.body.contains("\"ok\": true"));
37
38
    // Inspect the recorded request.
39
    let received = server.received_requests().await.unwrap();
40
    assert_eq!(received.len(), 1);
41
    let r = &received[0];
42
    // {{uuid}} became a real UUID.
43
    let id = r.headers.get("x-request-id").unwrap().to_str().unwrap();
44
    assert!(uuid::Uuid::parse_str(id).is_ok(), "got {id}");
45
    // {{tenant}} became acme.
46
    assert_eq!(r.headers.get("x-tenant").unwrap().to_str().unwrap(), "acme");
47
    // disabled query param was not sent.
48
    assert!(!r.url.query().unwrap_or_default().contains("disabled"));
49
}
50
51
#[tokio::test]
52
async fn oauth_client_credentials_basic_flow() {
53
    let server = MockServer::start().await;
54
    Mock::given(method("POST"))
55
        .and(path("/token"))
56
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
57
            "access_token": "tok-abc",
58
            "expires_in": 3600
59
        })))
60
        .mount(&server)
61
        .await;
62
63
    let cfg = OAuthConfig {
64
        token_url: format!("{}/token", server.uri()),
65
        client_id: "my-id".into(),
66
        client_secret: "my-secret".into(),
67
        scopes: vec!["read".into(), "write".into()],
68
        auth_style: AuthStyle::Basic,
69
        ..Default::default()
70
    };
71
    let client = reqwest::Client::new();
72
    let token = fetch_token(&client, &cfg).await.unwrap();
73
    assert_eq!(token.access_token, "tok-abc");
74
    assert!(cielago::http::token_valid(&token));
75
76
    let received = server.received_requests().await.unwrap();
77
    assert_eq!(received.len(), 1);
78
    let r = &received[0];
79
    let auth = r
80
        .headers
81
        .get("authorization")
82
        .unwrap()
83
        .to_str()
84
        .unwrap()
85
        .to_string();
86
    assert!(auth.starts_with("Basic "), "got {auth}");
87
    let body = String::from_utf8_lossy(&r.body).into_owned();
88
    assert!(
89
        body.contains("grant_type=client_credentials"),
90
        "body: {body}"
91
    );
92
    assert!(body.contains("scope="), "body: {body}");
93
}
94
95
#[tokio::test]
96
async fn oauth_post_style_sends_creds_in_body() {
97
    let server = MockServer::start().await;
98
    Mock::given(method("POST"))
99
        .and(path("/token"))
100
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
101
            "access_token": "tok"
102
        })))
103
        .mount(&server)
104
        .await;
105
106
    let cfg = OAuthConfig {
107
        token_url: format!("{}/token", server.uri()),
108
        client_id: "id2".into(),
109
        client_secret: "secret2".into(),
110
        scopes: vec![],
111
        auth_style: AuthStyle::Post,
112
        ..Default::default()
113
    };
114
    let client = reqwest::Client::new();
115
    fetch_token(&client, &cfg).await.unwrap();
116
117
    let received = server.received_requests().await.unwrap();
118
    let r = &received[0];
119
    let body = String::from_utf8_lossy(&r.body).into_owned();
120
    assert!(body.contains("client_id=id2"), "body: {body}");
121
    assert!(body.contains("client_secret=secret2"), "body: {body}");
122
    assert!(r.headers.get("authorization").is_none());
123
}
124
125
#[tokio::test]
126
async fn bearer_token_injected_unless_header_present() {
127
    let server = MockServer::start().await;
128
    Mock::given(method("GET"))
129
        .and(path("/x"))
130
        .respond_with(ResponseTemplate::new(200))
131
        .mount(&server)
132
        .await;
133
134
    let client = reqwest::Client::new();
135
    let req = SavedRequest::blank("x");
136
    send_request(
137
        &client,
138
        &server.uri(),
139
        &req,
140
        &HashMap::new(),
141
        Some("tok-1"),
142
        &[],
143
    )
144
    .await
145
    .unwrap();
146
    let received = server.received_requests().await.unwrap();
147
    assert_eq!(
148
        received[0]
149
            .headers
150
            .get("authorization")
151
            .unwrap()
152
            .to_str()
153
            .unwrap(),
154
        "Bearer tok-1"
155
    );
156
157
    // Explicit Authorization header wins over the injected bearer.
158
    let mut req2 = SavedRequest::blank("x2");
159
    req2.headers
160
        .push(KeyValueRow::new("Authorization", "Bearer manual", true));
161
    send_request(
162
        &client,
163
        &server.uri(),
164
        &req2,
165
        &HashMap::new(),
166
        Some("tok-2"),
167
        &[],
168
    )
169
    .await
170
    .unwrap();
171
    let received = server.received_requests().await.unwrap();
172
    assert_eq!(
173
        received[1]
174
            .headers
175
            .get("authorization")
176
            .unwrap()
177
            .to_str()
178
            .unwrap(),
179
        "Bearer manual"
180
    );
181
}
182
183
#[tokio::test]
184
async fn api_key_auth_resolves_shell_secret_into_header() {
185
    let server = MockServer::start().await;
186
    Mock::given(method("GET"))
187
        .and(path("/x"))
188
        .respond_with(ResponseTemplate::new(200))
189
        .mount(&server)
190
        .await;
191
192
    // Value is a `$(…)` command substitution, resolved at send time.
193
    let cfg = OAuthConfig {
194
        kind: AuthKind::ApiKey,
195
        token: "$(printf 'sk-secret')".into(),
196
        header: "X-Api-Key".into(),
197
        ..Default::default()
198
    };
199
    let req = SavedRequest::blank("x");
200
    let client = reqwest::Client::new();
201
    let outcome = send_with_auth(
202
        &client,
203
        &server.uri(),
204
        &req,
205
        &HashMap::new(),
206
        Some(&cfg),
207
        None,
208
    )
209
    .await;
210
    assert!(outcome.result.is_ok(), "{:?}", outcome.result.err());
211
    assert!(outcome.token.is_none());
212
213
    let received = server.received_requests().await.unwrap();
214
    assert_eq!(
215
        received[0]
216
            .headers
217
            .get("x-api-key")
218
            .unwrap()
219
            .to_str()
220
            .unwrap(),
221
        "sk-secret"
222
    );
223
}
224
225
#[tokio::test]
226
async fn bearer_auth_sends_resolved_token() {
227
    let server = MockServer::start().await;
228
    Mock::given(method("GET"))
229
        .and(path("/x"))
230
        .respond_with(ResponseTemplate::new(200))
231
        .mount(&server)
232
        .await;
233
234
    let cfg = OAuthConfig {
235
        kind: AuthKind::Bearer,
236
        token: "plain-tok".into(),
237
        ..Default::default()
238
    };
239
    let req = SavedRequest::blank("x");
240
    let client = reqwest::Client::new();
241
    let outcome = send_with_auth(
242
        &client,
243
        &server.uri(),
244
        &req,
245
        &HashMap::new(),
246
        Some(&cfg),
247
        None,
248
    )
249
    .await;
250
    assert!(outcome.result.is_ok(), "{:?}", outcome.result.err());
251
252
    let received = server.received_requests().await.unwrap();
253
    assert_eq!(
254
        received[0]
255
            .headers
256
            .get("authorization")
257
            .unwrap()
258
            .to_str()
259
            .unwrap(),
260
        "Bearer plain-tok"
261
    );
262
}
263
264
/// The compose/decompose contract: what `split_url_input` pulls apart,
265
/// `build_url` must put back together.
266
#[test]
267
fn pasted_url_round_trips_through_build_url() {
268
    let pasted = "https://api.example.com/orgs/{orgId}/pets?limit=10&sort=name";
269
    let parts = cielago::http::split_url_input(pasted);
270
271
    let mut req = SavedRequest::blank("round trip");
272
    req.path = parts.path;
273
    req.query = parts.query.unwrap();
274
    req.sync_path_params();
275
    // Path params come out of the paste blank; fill the one placeholder.
276
    assert_eq!(req.path_params.len(), 1);
277
    req.path_params[0].value = "acme".into();
278
279
    let base = parts.origin.unwrap();
280
    let url = cielago::http::client::build_url(&base, &req, &HashMap::new());
281
    assert_eq!(url, "https://api.example.com/orgs/acme/pets");
282
    // The query is applied by reqwest rather than `build_url`, so check the rows.
283
    let query: Vec<(&str, &str)> = req
284
        .query
285
        .iter()
286
        .map(|r| (r.key.as_str(), r.value.as_str()))
287
        .collect();
288
    assert_eq!(query, vec![("limit", "10"), ("sort", "name")]);
289
}
290
291
/// End to end: a pasted URL becomes a request that actually reaches the server
292
/// it named, with the query it carried.
293
#[tokio::test]
294
async fn a_pasted_url_sends_to_the_pasted_server() {
295
    let server = MockServer::start().await;
296
    Mock::given(method("GET"))
297
        .and(path("/v1/pets"))
298
        .and(query_param("limit", "5"))
299
        .respond_with(ResponseTemplate::new(200).set_body_string("ok"))
300
        .mount(&server)
301
        .await;
302
303
    let parts = cielago::http::split_url_input(&format!("{}/v1/pets?limit=5", server.uri()));
304
    let mut req = SavedRequest::blank("pasted");
305
    req.path = parts.path;
306
    req.query = parts.query.unwrap();
307
    req.sync_path_params();
308
309
    let client = reqwest::Client::new();
310
    let resp = send_request(
311
        &client,
312
        &parts.origin.unwrap(),
313
        &req,
314
        &HashMap::new(),
315
        None,
316
        &[],
317
    )
318
    .await
319
    .unwrap();
320
    assert_eq!(resp.status, 200);
321
}