| 1 | //! End-to-end: TUI action → async send task → response/token state update. |
| 2 | |
| 3 | use std::path::PathBuf; |
| 4 | |
| 5 | use cielago::app::App; |
| 6 | use cielago::model::{Collection, KeyValueRow, Method, OAuthConfig, SavedRequest}; |
| 7 | use cielago::store::AppConfig; |
| 8 | use wiremock::matchers::{method, path}; |
| 9 | use wiremock::{Mock, MockServer, ResponseTemplate}; |
| 10 | |
| 11 | fn app_with(base_url: String) -> App { |
| 12 | let mut c = Collection::new("test"); |
| 13 | c.servers = vec![base_url]; |
| 14 | let mut req = SavedRequest::blank("get thing"); |
| 15 | req.method = Method::Get; |
| 16 | req.path = "/things/1".into(); |
| 17 | req.headers |
| 18 | .push(KeyValueRow::new("X-Request-Id", "{{uuid}}", true)); |
| 19 | c.requests = vec![req]; |
| 20 | App::new( |
| 21 | c, |
| 22 | PathBuf::from("/tmp/cielago-test.json"), |
| 23 | AppConfig::default(), |
| 24 | ) |
| 25 | } |
| 26 | |
| 27 | #[tokio::test] |
| 28 | async fn send_from_app_updates_response_pane() { |
| 29 | let server = MockServer::start().await; |
| 30 | Mock::given(method("GET")) |
| 31 | .and(path("/things/1")) |
| 32 | .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 1}))) |
| 33 | .mount(&server) |
| 34 | .await; |
| 35 | |
| 36 | let mut app = app_with(server.uri()); |
| 37 | assert!(app.response.is_none()); |
| 38 | |
| 39 | app.send_selected(); |
| 40 | assert!(app.sending); |
| 41 | |
| 42 | let outcome = app.rx.recv().await.expect("send outcome"); |
| 43 | app.handle_outcome(outcome); |
| 44 | |
| 45 | assert!(!app.sending); |
| 46 | let resp = app.response.as_ref().expect("response recorded"); |
| 47 | assert_eq!(resp.status, 200); |
| 48 | assert!(resp.body.contains("\"id\": 1")); |
| 49 | assert!(app.status.contains("200")); |
| 50 | |
| 51 | // {{uuid}} was substituted in the outgoing header. |
| 52 | let received = server.received_requests().await.unwrap(); |
| 53 | let id = received[0] |
| 54 | .headers |
| 55 | .get("x-request-id") |
| 56 | .unwrap() |
| 57 | .to_str() |
| 58 | .unwrap(); |
| 59 | assert!(uuid::Uuid::parse_str(id).is_ok(), "got {id}"); |
| 60 | } |
| 61 | |
| 62 | #[tokio::test] |
| 63 | async fn send_with_oauth_fetches_and_caches_token() { |
| 64 | let server = MockServer::start().await; |
| 65 | Mock::given(method("POST")) |
| 66 | .and(path("/token")) |
| 67 | .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ |
| 68 | "access_token": "cached-tok", |
| 69 | "expires_in": 3600 |
| 70 | }))) |
| 71 | .expect(1) // fetched once, then cached |
| 72 | .mount(&server) |
| 73 | .await; |
| 74 | Mock::given(method("GET")) |
| 75 | .and(path("/things/1")) |
| 76 | .respond_with(ResponseTemplate::new(200)) |
| 77 | .expect(2) |
| 78 | .mount(&server) |
| 79 | .await; |
| 80 | |
| 81 | let mut app = app_with(server.uri()); |
| 82 | app.collection.auth = Some(OAuthConfig { |
| 83 | token_url: format!("{}/token", server.uri()), |
| 84 | client_id: "id".into(), |
| 85 | client_secret: "secret".into(), |
| 86 | scopes: vec![], |
| 87 | auth_style: cielago::model::AuthStyle::Basic, |
| 88 | ..Default::default() |
| 89 | }); |
| 90 | |
| 91 | // First send: fetches a token. |
| 92 | app.send_selected(); |
| 93 | let outcome = app.rx.recv().await.unwrap(); |
| 94 | app.handle_outcome(outcome); |
| 95 | assert_eq!(app.response.as_ref().unwrap().status, 200); |
| 96 | assert!(app.token.is_some()); |
| 97 | |
| 98 | // Second send: reuses the cached token (token endpoint expect(1)). |
| 99 | app.send_selected(); |
| 100 | let outcome = app.rx.recv().await.unwrap(); |
| 101 | app.handle_outcome(outcome); |
| 102 | assert_eq!(app.response.as_ref().unwrap().status, 200); |
| 103 | |
| 104 | // Both API calls carried the bearer token. |
| 105 | let received = server.received_requests().await.unwrap(); |
| 106 | let api_calls: Vec<_> = received |
| 107 | .iter() |
| 108 | .filter(|r| r.url.path() == "/things/1") |
| 109 | .collect(); |
| 110 | assert_eq!(api_calls.len(), 2); |
| 111 | for r in api_calls { |
| 112 | assert_eq!( |
| 113 | r.headers.get("authorization").unwrap().to_str().unwrap(), |
| 114 | "Bearer cached-tok" |
| 115 | ); |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | #[tokio::test] |
| 120 | async fn send_without_server_shows_status_error() { |
| 121 | let mut app = app_with("".into()); |
| 122 | app.collection.servers.clear(); |
| 123 | app.send_selected(); |
| 124 | assert!(!app.sending); |
| 125 | assert!(app.status.contains("No server configured")); |
| 126 | } |