//! Mattermost's REST calls against a scripted server: the requests on the wire, and what a 401, //! 403, 429, 5xx or 404 does (M4a spec, sections 5 and 9). Do not edit. #[path = "support/http_server.rs"] mod http_server; #[path = "support/tls_server.rs"] mod tls_server; use std::net::SocketAddr; use std::sync::mpsc::Receiver; use std::time::{Duration, Instant}; use gatewayd::config::ServerUrl; use gatewayd::mm::MmError; use gatewayd::mm::rest::Client; use gatewayd::net::Connector; use gatewayd::secrets::Secret; use http_server::{Request, reply, serve_http}; use serde_json::json; use tls_server::{fixture, server_config}; const BOT: &str = "b0000000000000000000000000"; const KYLE: &str = "k0000000000000000000000000"; const DM: &str = "d0000000000000000000000000"; const POST: &str = "p0000000000000000000000001"; fn client(addr: SocketAddr, tls: bool) -> Client { let url = ServerUrl { tls, host: "localhost".to_string(), port: addr.port(), }; let ca = tls.then(|| fixture("test-ca.pem")); let connector = Connector::new(url, ca.as_deref()).unwrap(); Client::new( connector, Secret::new("TOKEN".to_string()), Duration::from_secs(5), ) } fn me_body() -> String { json!({"id": BOT, "username": "boxmaker-straylight", "roles": "system_user", "is_bot": true}) .to_string() } fn requests(rx: &Receiver) -> Vec { rx.try_iter().collect() } #[test] fn me_plain_and_over_tls() { for tls in [false, true] { let config = tls.then(|| server_config("server")); let (addr, rx) = serve_http(config, |_, _| reply(200, "", &me_body())); let me = client(addr, tls).me().unwrap(); assert_eq!( (me.id.as_str(), me.username.as_str()), (BOT, "boxmaker-straylight") ); let r = rx.recv_timeout(Duration::from_secs(5)).unwrap(); assert_eq!( (r.method.as_str(), r.path.as_str()), ("GET", "/api/v4/users/me") ); assert!( r.head.contains("Authorization: Bearer TOKEN\r\n"), "{}", r.head ); assert!( r.head .contains(&format!("Host: localhost:{}\r\n", addr.port())), "{}", r.head ); } } #[test] fn create_post_sends_the_thread_and_reads_the_post_back() { let (addr, rx) = serve_http(None, |_, r| { let mut p = r.json(); p["id"] = json!(POST); p["user_id"] = json!(BOT); p["create_at"] = json!(5); reply(201, "", &p.to_string()) }); let post = client(addr, false) .create_post(DM, KYLE, "an answer\nin two lines") .unwrap(); assert_eq!((post.id.as_str(), post.root_id.as_str()), (POST, KYLE)); let r = rx.recv_timeout(Duration::from_secs(5)).unwrap(); assert_eq!( (r.method.as_str(), r.path.as_str()), ("POST", "/api/v4/posts") ); assert!( r.head.contains("Content-Type: application/json\r\n"), "{}", r.head ); assert_eq!( r.json(), json!({"channel_id": DM, "root_id": KYLE, "message": "an answer\nin two lines"}) ); } #[test] fn posts_since_and_the_direct_channel() { let (addr, rx) = serve_http(None, |_, r| { if r.path.contains("/posts?since=") { reply(200, "", r#"{"order":[],"posts":{}}"#) } else { reply(201, "", &json!({"id": DM, "type": "D"}).to_string()) } }); let c = client(addr, false); assert!( c.posts_since(DM, 1_758_650_000_000) .unwrap() .posts .is_empty() ); assert_eq!(c.direct_channel(BOT, KYLE).unwrap(), DM); let rs = requests(&rx); assert_eq!( rs[0].path, format!("/api/v4/channels/{DM}/posts?since=1758650000000") ); assert_eq!( (rs[1].method.as_str(), rs[1].path.as_str()), ("POST", "/api/v4/channels/direct") ); assert_eq!(rs[1].json(), json!([BOT, KYLE])); } #[test] fn a_channel_that_is_not_an_id_is_never_sent() { let (addr, rx) = serve_http(None, |_, _| reply(200, "", r#"{"order":[],"posts":{}}"#)); let err = client(addr, false) .posts_since("../users/me?x=", 0) .unwrap_err(); assert!(matches!(err, MmError::Json(_)), "{err}"); std::thread::sleep(Duration::from_millis(100)); assert!(requests(&rx).is_empty()); } #[test] fn a_refused_token_stops_at_once() { for status in [401, 403] { let (addr, rx) = serve_http(None, move |_, _| { reply( status, "", r#"{"id":"api.context.session_expired.app_error"}"#, ) }); let err = client(addr, false).me().unwrap_err(); assert!(matches!(err, MmError::Auth(s) if s == status), "{err}"); std::thread::sleep(Duration::from_millis(100)); assert_eq!(requests(&rx).len(), 1, "no retry on {status}"); } } #[test] fn a_server_error_is_tried_twice_more() { let (addr, rx) = serve_http(None, |n, _| { if n < 2 { reply(503, "", "{}") } else { reply(200, "", &me_body()) } }); assert!(client(addr, false).me().is_ok()); assert_eq!(requests(&rx).len(), 3); let (addr, rx) = serve_http(None, |_, _| reply(502, "", "{}")); let err = client(addr, false).me().unwrap_err(); assert!(matches!(err, MmError::Status(502, _)), "{err}"); std::thread::sleep(Duration::from_millis(100)); assert_eq!(requests(&rx).len(), 3); } #[test] fn a_rate_limit_is_waited_out() { let (addr, rx) = serve_http(None, |n, _| { if n == 0 { reply(429, "X-Ratelimit-Reset: 1\r\n", "{}") } else { reply(200, "", &me_body()) } }); let started = Instant::now(); assert!(client(addr, false).me().is_ok()); assert!( started.elapsed() >= Duration::from_millis(900), "{:?}", started.elapsed() ); assert_eq!(requests(&rx).len(), 2); let (addr, _rx) = serve_http(None, |_, _| reply(429, "X-Ratelimit-Reset: 1\r\n", "{}")); assert!(matches!( client(addr, false).me(), Err(MmError::RateLimited(_)) )); } #[test] fn other_statuses_keep_the_start_of_the_body() { let long = format!("{{\"message\":\"{}\"}}", "x".repeat(1000)); let (addr, _rx) = serve_http(None, move |_, _| reply(404, "", &long)); let Err(MmError::Status(404, body)) = client(addr, false).me() else { panic!("not a 404") }; assert_eq!(body.chars().count(), 200); } #[test] fn answers_that_are_not_what_we_expect() { for body in [ "not json", r#"{"id":"short","username":"x"}"#, r#"{"id":"b0000000000000000000000000","username":""}"#, ] { let (addr, _rx) = serve_http(None, move |_, _| reply(200, "", body)); assert!( matches!(client(addr, false).me(), Err(MmError::Json(_))), "{body}" ); } } #[test] fn nobody_listening_is_a_network_error() { let port = std::net::TcpListener::bind("127.0.0.1:0") .unwrap() .local_addr() .unwrap(); assert!(matches!(client(port, false).me(), Err(MmError::Net(_)))); }