use std::time::Duration;
use navi_notifier_core::model::{Actor, Event, EventKind, PullRequest, Repo, ViewerRelationship};
use navi_notifier_core::traits::Destination;
use navi_notifier_slack::{SlackDestination, SlackDestinationConfig};
use serde_json::Value;
use time::OffsetDateTime;
#[path = "../e2e_common.rs"]
mod e2e_common;
use e2e_common::{env, env_or, MemState};
#[tokio::main]
async fn main() {
match run().await {
Ok(()) => println!("e2e-slack: PASSED"),
Err(e) => {
eprintln!("e2e-slack: FAILED: {e}");
std::process::exit(1);
}
}
}
async fn run() -> Result<(), String> {
let token = env("E2E_SLACK_TOKEN")?;
let dm_to = env_or("E2E_SLACK_DM_TO", "self");
let api = env_or("E2E_SLACK_API", "https://slack.com/api");
let http = reqwest::Client::new();
let marker = format!("navi-e2e-slack-{}", std::process::id());
let channel = resolve_channel(&http, &api, &token, &dm_to).await?;
println!("e2e-slack: posting to channel {channel} with marker {marker}");
let destination = SlackDestination::new(SlackDestinationConfig {
token: token.clone(),
dm_to,
api_base: Some(api.clone()),
broadcast: Vec::new(),
})
.map_err(|e| format!("build slack destination: {e}"))?;
let oldest = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
destination
.send(&sample_event(&marker), &MemState::default())
.await
.map_err(|e| format!("slack send failed: {e}"))?;
for attempt in 1..=10 {
if history_contains(&http, &api, &token, &channel, &marker, oldest).await? {
println!("e2e-slack: read back the delivered message (marker {marker})");
return Ok(());
}
if attempt % 3 == 0 {
println!("e2e-slack: still waiting for read-back (attempt {attempt})…");
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(format!(
"posted, but never read back a message containing {marker} from {channel}"
))
}
async fn resolve_channel(
http: &reqwest::Client,
api: &str,
token: &str,
dm_to: &str,
) -> Result<String, String> {
if dm_to.starts_with('C') || dm_to.starts_with('#') {
return Ok(dm_to.to_string());
}
let user_id = if dm_to == "self" {
slack_get(http, api, token, "auth.test").await?["user_id"]
.as_str()
.ok_or("auth.test returned no user_id")?
.to_string()
} else {
dm_to.to_string()
};
let opened = slack_post(
http,
api,
token,
"conversations.open",
&[("users", &user_id)],
)
.await?;
opened["channel"]["id"]
.as_str()
.map(str::to_string)
.ok_or_else(|| "conversations.open returned no channel id".into())
}
async fn history_contains(
http: &reqwest::Client,
api: &str,
token: &str,
channel: &str,
marker: &str,
oldest: u64,
) -> Result<bool, String> {
let oldest = oldest.to_string();
let value = slack_post(
http,
api,
token,
"conversations.history",
&[("channel", channel), ("limit", "30"), ("oldest", &oldest)],
)
.await?;
let hit = value["messages"]
.as_array()
.into_iter()
.flatten()
.any(|m| m.to_string().contains(marker));
Ok(hit)
}
fn sample_event(marker: &str) -> Event {
Event {
source_id: "github".into(),
kind: EventKind::ReviewRequested,
pull_request: PullRequest {
repo: Repo::new("navi", "e2e"),
number: 1,
title: format!("navi e2e slack read-back {marker}"),
url: "https://github.com/navi/e2e/pull/1".into(),
author: Actor::new("navi-e2e"),
draft: false,
},
viewer: ViewerRelationship {
is_author: false,
is_reviewer: true,
actor_is_viewer: false,
},
actor: Actor::new("navi-e2e"),
occurred_at: OffsetDateTime::now_utc(),
target_url: Some("https://github.com/navi/e2e/pull/1".into()),
excerpt: Some(format!("read-back probe {marker}")),
dedup_key: format!("navi:e2e:slack:{marker}"),
}
}
async fn slack_get(
http: &reqwest::Client,
api: &str,
token: &str,
method: &str,
) -> Result<Value, String> {
let resp = http
.get(format!("{api}/{method}"))
.bearer_auth(token)
.send()
.await
.map_err(|e| format!("{method}: {e}"))?;
slack_ok(resp, method).await
}
async fn slack_post(
http: &reqwest::Client,
api: &str,
token: &str,
method: &str,
form: &[(&str, &str)],
) -> Result<Value, String> {
let resp = http
.post(format!("{api}/{method}"))
.bearer_auth(token)
.form(form)
.send()
.await
.map_err(|e| format!("{method}: {e}"))?;
slack_ok(resp, method).await
}
async fn slack_ok(resp: reqwest::Response, method: &str) -> Result<Value, String> {
let value: Value = resp
.json()
.await
.map_err(|e| format!("{method}: decode: {e}"))?;
if value["ok"].as_bool() == Some(true) {
Ok(value)
} else {
Err(format!(
"{method}: {}",
value["error"].as_str().unwrap_or("unknown_error")
))
}
}