#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use autumn_plugin_slack::{SlackClient, SlackPlugin, testing};
const SECRET: &str = "boot-test-secret";
const OLD_SECRET: &str = "boot-test-old-secret";
const CHILD_MARK: &str = "SLACK_BOOT_TEST_CHILD";
#[tokio::test(flavor = "multi_thread")]
#[ignore = "runs only as the child of slack_only_app_boots_and_serves_signed_requests"]
async fn boot_child_server() {
if std::env::var(CHILD_MARK).is_err() {
return;
}
Box::pin(
autumn_web::app()
.plugin(SlackPlugin::new())
.state_initializer(|state| {
if SlackClient::from_state(state).is_none() {
eprintln!("no SlackClient before job workers start");
std::process::exit(3);
}
})
.run(),
)
.await;
}
struct KillOnDrop(Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[tokio::test(flavor = "multi_thread")]
async fn slack_only_app_boots_and_serves_signed_requests() {
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
};
let dir = std::env::temp_dir().join(format!("slack-boot-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut child = KillOnDrop(
Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"boot_child_server",
"--include-ignored",
"--nocapture",
])
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.env("AUTUMN_MANIFEST_DIR", &dir)
.current_dir(&dir)
.env(CHILD_MARK, "1")
.env("AUTUMN_SERVER__PORT", port.to_string())
.env("AUTUMN_SERVER__HOST", "127.0.0.1")
.env("SLACK_SIGNING_SECRET", SECRET)
.env(
"AUTUMN_SLACK__PREVIOUS_SIGNING_SECRET_ENVS",
"SLACK_BOOT_OLD_SECRET",
)
.env("SLACK_BOOT_OLD_SECRET", OLD_SECRET)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.unwrap(),
);
let body = r#"{"type":"url_verification","challenge":"boot"}"#;
let client = reqwest::Client::builder().no_proxy().build().unwrap();
let url = format!("http://127.0.0.1:{port}/slack/events");
let signed_post = |secret: &'static str| {
let ts = u64::try_from(autumn_web::reexports::chrono::Utc::now().timestamp()).unwrap();
let [(h1, v1), (h2, v2)] = testing::signed_headers(secret, ts, body.as_bytes());
client
.post(&url)
.header(h1, v1)
.header(h2, v2)
.header("content-type", "application/json")
.body(body)
.send()
};
let start = Instant::now();
let first = loop {
if let Some(status) = child.0.try_wait().unwrap() {
let mut err = String::new();
if let Some(mut e) = child.0.stderr.take() {
std::io::Read::read_to_string(&mut e, &mut err).unwrap();
}
panic!("the app stopped at boot ({status}):\n{err}");
}
assert!(
start.elapsed() < Duration::from_secs(60),
"the app did not answer"
);
if let Ok(res) = signed_post(SECRET).await {
break res;
}
tokio::time::sleep(Duration::from_millis(100)).await;
};
assert_eq!(first.status().as_u16(), 200);
assert!(first.text().await.unwrap().contains("boot"));
assert_eq!(
signed_post(OLD_SECRET).await.unwrap().status().as_u16(),
200
);
assert_eq!(
signed_post("not-a-secret").await.unwrap().status().as_u16(),
401
);
}