tests/http_tests.rs 7.4 K raw
1
use std::collections::HashMap;
2
3
use cielago::http::{fetch_token, send_request};
4
use cielago::model::{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
    };
70
    let client = reqwest::Client::new();
71
    let token = fetch_token(&client, &cfg).await.unwrap();
72
    assert_eq!(token.access_token, "tok-abc");
73
    assert!(cielago::http::token_valid(&token));
74
75
    let received = server.received_requests().await.unwrap();
76
    assert_eq!(received.len(), 1);
77
    let r = &received[0];
78
    let auth = r
79
        .headers
80
        .get("authorization")
81
        .unwrap()
82
        .to_str()
83
        .unwrap()
84
        .to_string();
85
    assert!(auth.starts_with("Basic "), "got {auth}");
86
    let body = String::from_utf8_lossy(&r.body).into_owned();
87
    assert!(
88
        body.contains("grant_type=client_credentials"),
89
        "body: {body}"
90
    );
91
    assert!(body.contains("scope="), "body: {body}");
92
}
93
94
#[tokio::test]
95
async fn oauth_post_style_sends_creds_in_body() {
96
    let server = MockServer::start().await;
97
    Mock::given(method("POST"))
98
        .and(path("/token"))
99
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
100
            "access_token": "tok"
101
        })))
102
        .mount(&server)
103
        .await;
104
105
    let cfg = OAuthConfig {
106
        token_url: format!("{}/token", server.uri()),
107
        client_id: "id2".into(),
108
        client_secret: "secret2".into(),
109
        scopes: vec![],
110
        auth_style: AuthStyle::Post,
111
    };
112
    let client = reqwest::Client::new();
113
    fetch_token(&client, &cfg).await.unwrap();
114
115
    let received = server.received_requests().await.unwrap();
116
    let r = &received[0];
117
    let body = String::from_utf8_lossy(&r.body).into_owned();
118
    assert!(body.contains("client_id=id2"), "body: {body}");
119
    assert!(body.contains("client_secret=secret2"), "body: {body}");
120
    assert!(r.headers.get("authorization").is_none());
121
}
122
123
#[tokio::test]
124
async fn bearer_token_injected_unless_header_present() {
125
    let server = MockServer::start().await;
126
    Mock::given(method("GET"))
127
        .and(path("/x"))
128
        .respond_with(ResponseTemplate::new(200))
129
        .mount(&server)
130
        .await;
131
132
    let client = reqwest::Client::new();
133
    let req = SavedRequest::blank("x");
134
    send_request(&client, &server.uri(), &req, &HashMap::new(), Some("tok-1"))
135
        .await
136
        .unwrap();
137
    let received = server.received_requests().await.unwrap();
138
    assert_eq!(
139
        received[0]
140
            .headers
141
            .get("authorization")
142
            .unwrap()
143
            .to_str()
144
            .unwrap(),
145
        "Bearer tok-1"
146
    );
147
148
    // Explicit Authorization header wins over the injected bearer.
149
    let mut req2 = SavedRequest::blank("x2");
150
    req2.headers
151
        .push(KeyValueRow::new("Authorization", "Bearer manual", true));
152
    send_request(
153
        &client,
154
        &server.uri(),
155
        &req2,
156
        &HashMap::new(),
157
        Some("tok-2"),
158
    )
159
    .await
160
    .unwrap();
161
    let received = server.received_requests().await.unwrap();
162
    assert_eq!(
163
        received[1]
164
            .headers
165
            .get("authorization")
166
            .unwrap()
167
            .to_str()
168
            .unwrap(),
169
        "Bearer manual"
170
    );
171
}
172
173
/// The compose/decompose contract: what `split_url_input` pulls apart,
174
/// `build_url` must put back together.
175
#[test]
176
fn pasted_url_round_trips_through_build_url() {
177
    let pasted = "https://api.example.com/orgs/{orgId}/pets?limit=10&sort=name";
178
    let parts = cielago::http::split_url_input(pasted);
179
180
    let mut req = SavedRequest::blank("round trip");
181
    req.path = parts.path;
182
    req.query = parts.query.unwrap();
183
    req.sync_path_params();
184
    // Path params come out of the paste blank; fill the one placeholder.
185
    assert_eq!(req.path_params.len(), 1);
186
    req.path_params[0].value = "acme".into();
187
188
    let base = parts.origin.unwrap();
189
    let url = cielago::http::client::build_url(&base, &req, &HashMap::new());
190
    assert_eq!(url, "https://api.example.com/orgs/acme/pets");
191
    // The query is applied by reqwest rather than `build_url`, so check the rows.
192
    let query: Vec<(&str, &str)> = req
193
        .query
194
        .iter()
195
        .map(|r| (r.key.as_str(), r.value.as_str()))
196
        .collect();
197
    assert_eq!(query, vec![("limit", "10"), ("sort", "name")]);
198
}
199
200
/// End to end: a pasted URL becomes a request that actually reaches the server
201
/// it named, with the query it carried.
202
#[tokio::test]
203
async fn a_pasted_url_sends_to_the_pasted_server() {
204
    let server = MockServer::start().await;
205
    Mock::given(method("GET"))
206
        .and(path("/v1/pets"))
207
        .and(query_param("limit", "5"))
208
        .respond_with(ResponseTemplate::new(200).set_body_string("ok"))
209
        .mount(&server)
210
        .await;
211
212
    let parts = cielago::http::split_url_input(&format!("{}/v1/pets?limit=5", server.uri()));
213
    let mut req = SavedRequest::blank("pasted");
214
    req.path = parts.path;
215
    req.query = parts.query.unwrap();
216
    req.sync_path_params();
217
218
    let client = reqwest::Client::new();
219
    let resp = send_request(&client, &parts.origin.unwrap(), &req, &HashMap::new(), None)
220
        .await
221
        .unwrap();
222
    assert_eq!(resp.status, 200);
223
}