| 109 | 109 | | impl Reporter { |
| 110 | 110 | | fn send(&self, step: &str, mut body: Value) -> Result<Value> { |
| 111 | 111 | | body["token"] = self.token.clone().into(); |
| 112 | | − | let response = ureq::post(&format!("{}/{step}", self.base)) |
| 113 | | − | .send_json(body) |
| 114 | | − | .with_context(|| format!("could not report `{step}` to g1t"))?; |
| 115 | | − | Ok(response.into_json().unwrap_or(Value::Null)) |
| 112 | + | match ureq::post(&format!("{}/{step}", self.base)).send_json(body) { |
| 113 | + | Ok(response) => Ok(response.into_json().unwrap_or(Value::Null)), |
| 114 | + | // A refusal (409 `not_enabled`, say) still says why in its |
| 115 | + | // body, which the step reads: ureq makes any status over 399 |
| 116 | + | // an error. |
| 117 | + | Err(ureq::Error::Status(status, response)) => match response.into_json::<Value>() { |
| 118 | + | Ok(answer) if answer.get("ok").is_some() => Ok(answer), |
| 119 | + | _ => bail!("could not report `{step}` to g1t: status code {status}"), |
| 120 | + | }, |
| 121 | + | Err(error) => Err(error).with_context(|| format!("could not report `{step}` to g1t")), |
| 122 | + | } |
| 116 | 123 | | } |
| 117 | 124 | | } |
| 118 | 125 | | |
| ⋯ |
| 1535 | 1542 | | mod tests { |
| 1536 | 1543 | | use super::*; |
| 1537 | 1544 | | |
| 1545 | + | /// A server that answers one request with `status` and `body`. |
| 1546 | + | fn answering(status: &str, body: &'static str) -> String { |
| 1547 | + | use std::io::{Read, Write}; |
| 1548 | + | let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); |
| 1549 | + | let base = format!("http://{}", listener.local_addr().unwrap()); |
| 1550 | + | let status = status.to_owned(); |
| 1551 | + | std::thread::spawn(move || { |
| 1552 | + | let (mut stream, _) = listener.accept().unwrap(); |
| 1553 | + | // The whole request first (its headers, then its body by |
| 1554 | + | // length), or the client may still be writing when the answer |
| 1555 | + | // closes the connection. |
| 1556 | + | let mut seen = Vec::new(); |
| 1557 | + | let mut buf = [0; 4096]; |
| 1558 | + | loop { |
| 1559 | + | let n = stream.read(&mut buf).unwrap_or(0); |
| 1560 | + | if n == 0 { |
| 1561 | + | break; |
| 1562 | + | } |
| 1563 | + | seen.extend_from_slice(&buf[..n]); |
| 1564 | + | let text = String::from_utf8_lossy(&seen); |
| 1565 | + | if let Some(end) = text.find("\r\n\r\n") { |
| 1566 | + | let length = text[..end] |
| 1567 | + | .lines() |
| 1568 | + | .find_map(|l| l.to_ascii_lowercase().strip_prefix("content-length:").map(|v| v.trim().parse::<usize>().unwrap_or(0))) |
| 1569 | + | .unwrap_or(0); |
| 1570 | + | if seen.len() >= end + 4 + length { |
| 1571 | + | break; |
| 1572 | + | } |
| 1573 | + | } |
| 1574 | + | } |
| 1575 | + | let reply = format!("HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", body.len()); |
| 1576 | + | stream.write_all(reply.as_bytes()).unwrap(); |
| 1577 | + | }); |
| 1578 | + | base |
| 1579 | + | } |
| 1580 | + | |
| 1581 | + | #[test] |
| 1582 | + | fn a_refusal_reaches_the_step_with_its_reason() { |
| 1583 | + | let base = answering("409 Conflict", r#"{"ok":false,"error":{"code":"not_enabled","message":"Docker deployments are not switched on for g1t.page yet."}}"#); |
| 1584 | + | let answer = Reporter { base, token: "t".into() }.send("registry", json!({})).unwrap(); |
| 1585 | + | assert_eq!(answer["ok"], false); |
| 1586 | + | assert_eq!(answer["error"]["code"], "not_enabled"); |
| 1587 | + | } |
| 1588 | + | |
| 1589 | + | #[test] |
| 1590 | + | fn a_failure_without_an_answer_says_its_status() { |
| 1591 | + | let base = answering("502 Bad Gateway", "upstream down"); |
| 1592 | + | let error = Reporter { base, token: "t".into() }.send("registry", json!({})).unwrap_err(); |
| 1593 | + | assert!(format!("{error:#}").contains("status code 502"), "{error:#}"); |
| 1594 | + | } |
| 1595 | + | |
| 1538 | 1596 | | #[test] |
| 1539 | 1597 | | fn the_upload_session_is_read_in_either_spelling() { |
| 1540 | 1598 | | for answer in [ |