use std::fs::File; use std::io::{self, Read, Write}; use std::str; use std::time::Duration; use async_trait::async_trait; use regex::Regex; use tokio::time::timeout; use tokio_test::io::Builder; use instant_epp::client::{Connector, EppClient}; use instant_epp::domain::{DomainCheck, DomainContact, DomainCreate, Period, PeriodLength}; use instant_epp::login::Login; use instant_epp::response::ResultCode; use instant_epp::Error; const CLTRID: &str = "cltrid:1626454866"; struct TestWriter; impl Write for TestWriter { fn write(&mut self, buf: &[u8]) -> io::Result { print!( "{}", str::from_utf8(buf).expect("tried to log invalid UTF-8") ); Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { io::stdout().flush() } } fn log_to_stdout() -> tracing::subscriber::DefaultGuard { let sub = tracing_subscriber::FmtSubscriber::builder() .with_max_level(tracing::Level::TRACE) .with_writer(|| TestWriter) .finish(); tracing::subscriber::set_default(sub) } fn len_bytes(bytes: &str) -> [u8; 4] { ((bytes.len() as u32) + 4).to_be_bytes() } fn xml(path: &str) -> String { let ws_regex = Regex::new(r"[\s]{2,}").unwrap(); let end_regex = Regex::new(r"\?>").unwrap(); let mut f = File::open(format!("tests/resources/{path}")).unwrap(); let mut buf = String::new(); f.read_to_string(&mut buf).unwrap(); if !buf.is_empty() { let mat = end_regex.find(buf.as_str()).unwrap(); let start = mat.end(); buf = format!( "{}\r\n{}", &buf[..start], ws_regex.replace_all(&buf[start..], "") ); } buf } fn build_stream(units: &[&str]) -> Builder { let mut builder = Builder::new(); for (i, path) in units.iter().enumerate() { let buf = xml(path); match i % 2 { 0 => builder.read(&len_bytes(&buf)).read(buf.as_bytes()), 1 => builder.write(&len_bytes(&buf)).write(buf.as_bytes()), _ => unreachable!(), }; } builder } #[tokio::test] async fn client() { let _guard = log_to_stdout(); struct FakeConnector; #[async_trait] impl Connector for FakeConnector { type Connection = tokio_test::io::Mock; async fn connect(&self, _: Duration) -> Result { Ok(build_stream(&[ "response/greeting.xml", "request/login.xml", "response/login.xml", "request/domain/check.xml", "response/domain/check.xml", ]) .build()) } } let mut client = EppClient::new(FakeConnector, "test".into(), Duration::from_secs(5)) .await .unwrap(); assert_eq!(client.xml_greeting(), xml("response/greeting.xml")); let rsp = client .transact( &Login::new( "username", "password", Some("new-password"), Some(&["http://schema.ispapi.net/epp/xml/keyvalue-1.0"]), ), CLTRID, ) .await .unwrap(); assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully); let rsp = client .transact( &DomainCheck { domains: &["eppdev.com", "eppdev.net"], }, CLTRID, ) .await .unwrap(); assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully); let result = rsp.res_data().unwrap(); assert_eq!(result.list[0].inner.id, "eppdev.com"); } #[tokio::test] async fn dropped() { let _guard = log_to_stdout(); struct FakeConnector; #[async_trait] impl Connector for FakeConnector { type Connection = tokio_test::io::Mock; async fn connect(&self, _: Duration) -> Result { let mut builder = Builder::new(); let buf = xml("response/greeting.xml"); builder.read(&len_bytes(&buf)).read(buf.as_bytes()); let buf = xml("request/login.xml"); builder.write(&len_bytes(&buf)).write(buf.as_bytes()); let buf = xml("response/login.xml"); builder.read(&len_bytes(&buf)).read(buf.as_bytes()); let buf = xml("request/domain/check.xml"); builder.write(&len_bytes(&buf)).write(buf.as_bytes()); // We add a wait here. We're going to timeout below as a way of dropping the future. builder.wait(Duration::from_millis(100)); let buf = xml("response/domain/check.xml"); builder.read(&len_bytes(&buf)).read(buf.as_bytes()); let buf = xml("request/domain/create.xml"); builder.write(&len_bytes(&buf)).write(buf.as_bytes()); let buf = xml("response/domain/create.xml"); builder.read(&len_bytes(&buf)).read(buf.as_bytes()); Ok(builder.build()) } } let mut client = EppClient::new(FakeConnector, "test".into(), Duration::from_secs(5)) .await .unwrap(); assert_eq!(client.xml_greeting(), xml("response/greeting.xml")); let rsp = client .transact( &Login::new( "username", "password", Some("new-password"), Some(&["http://schema.ispapi.net/epp/xml/keyvalue-1.0"]), ), CLTRID, ) .await .unwrap(); assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully); // Here, we add a 10ms timeout on the entire transaction. The mock stream // specifies that the caller will have to wait for 100ms after sending // the request before the response is returned. When `timeout()` returns // `Err(Elapsed)`, the `RequestFuture` inside the `Timeout` future is dropped, // leaving a half-finished request in the `EppConnection`. timeout( Duration::from_millis(10), client.transact( &DomainCheck { domains: &["eppdev.com", "eppdev.net"], }, CLTRID, ), ) .await .unwrap_err(); let contacts = &[ DomainContact { contact_type: "admin".into(), id: "eppdev-contact-3".into(), }, DomainContact { contact_type: "tech".into(), id: "eppdev-contact-3".into(), }, DomainContact { contact_type: "billing".into(), id: "eppdev-contact-3".into(), }, ]; // Then, we start another request (of a different type). This should push through the // remainder of the in-flight request before starting the new one, and succeed. let create = DomainCreate::new( "eppdev-1.com", Period::Years(PeriodLength::new(1).unwrap()), None, Some("eppdev-contact-3"), "epP4uthd#v", Some(contacts), ); let rsp = client.transact(&create, CLTRID).await.unwrap(); assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully); }