use super::*;
use sha2::Digest;
fn direct_loopback_request() -> axum::http::request::Builder {
Request::builder().extension(axum::extract::ConnectInfo(
"127.0.0.1:39731"
.parse::<std::net::SocketAddr>()
.expect("loopback socket address"),
))
}
fn verified_channel_actor(
tenant_context: tandem_types::TenantContext,
actor_id: &str,
deployment_admin: bool,
) -> tandem_types::VerifiedTenantContext {
let now = crate::now_ms();
tandem_types::VerifiedTenantContext {
tenant_context,
human_actor: tandem_types::HumanActor::tandem_user(actor_id),
authority_chain: tandem_types::AuthorityChain::from_request(
tandem_types::RequestPrincipal::authenticated_user(actor_id, "channel-test"),
),
roles: Vec::new(),
org_units: Vec::new(),
capabilities: deployment_admin
.then(|| vec!["deployment.admin".to_string()])
.unwrap_or_default(),
policy_version: None,
strict_projection: None,
issuer: "channel-test".to_string(),
audience: "tandem".to_string(),
issued_at_ms: now,
expires_at_ms: now.saturating_add(60_000),
assertion_id: format!("channel-test-{actor_id}-{now}"),
assertion_key_id: None,
}
}
pub(super) fn channel_tenant_request(
method: &str,
uri: &str,
org_id: &str,
workspace_id: &str,
actor_id: &str,
deployment_admin: bool,
body: Value,
) -> Request<Body> {
let tenant =
tandem_types::TenantContext::explicit_user_workspace(org_id, workspace_id, None, actor_id);
Request::builder()
.extension(axum::extract::ConnectInfo(
"198.51.100.44:443"
.parse::<std::net::SocketAddr>()
.expect("remote socket address"),
))
.extension(verified_channel_actor(tenant, actor_id, deployment_admin))
.method(method)
.uri(uri)
.header("content-type", "application/json")
.header("x-tandem-org-id", org_id)
.header("x-tandem-workspace-id", workspace_id)
.header("x-tandem-actor-id", actor_id)
.body(Body::from(body.to_string()))
.expect("channel tenant request")
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_config_returns_non_secret_shape() {
let state = test_state().await;
let _ = state
.config
.patch_project(json!({
"channels": {
"telegram": {
"bot_token": "tg-secret",
"allowed_users": ["@alice", "@bob"],
"mention_only": true,
"strict_kb_grounding": true
},
"discord": {
"bot_token": "dc-secret",
"allowed_users": ["*"],
"mention_only": false,
"guild_id": "1234",
"strict_kb_grounding": true,
"model_provider_id": "openai",
"model_id": "gpt-4.1-mini"
},
"slack": {
"bot_token": "sl-secret",
"channel_id": "C123",
"allowed_users": ["U1"],
"mention_only": true,
"strict_kb_grounding": false
}
}
}))
.await
.expect("patch project");
let app = app_router(state);
let req = direct_loopback_request()
.method("GET")
.uri("/channels/config")
.body(Body::empty())
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let body = to_bytes(resp.into_body(), usize::MAX)
.await
.expect("response body");
let payload: Value = serde_json::from_slice(&body).expect("json body");
assert_eq!(
payload
.get("telegram")
.and_then(|v| v.get("has_token"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
payload
.get("telegram")
.and_then(|v| v.get("token_masked"))
.and_then(Value::as_str),
Some("****")
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("token_masked"))
.and_then(Value::as_str),
Some("****")
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("strict_kb_grounding"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
payload
.get("telegram")
.and_then(|v| v.get("strict_kb_grounding"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("model_provider_id"))
.and_then(Value::as_str),
Some("openai")
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("model_id"))
.and_then(Value::as_str),
Some("gpt-4.1-mini")
);
assert_eq!(
payload
.get("slack")
.and_then(|v| v.get("token_masked"))
.and_then(Value::as_str),
Some("****")
);
assert!(payload
.get("telegram")
.and_then(Value::as_object)
.is_some_and(|obj| !obj.contains_key("bot_token")));
assert!(payload
.get("discord")
.and_then(Value::as_object)
.is_some_and(|obj| !obj.contains_key("bot_token")));
assert!(payload
.get("slack")
.and_then(Value::as_object)
.is_some_and(|obj| !obj.contains_key("bot_token")));
assert_eq!(
payload
.get("slack")
.and_then(|v| v.get("mention_only"))
.and_then(Value::as_bool),
Some(true)
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_roundtrips_model_override() {
let state = test_state().await;
let app = app_router(state.clone());
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/discord")
.header("content-type", "application/json")
.body(Body::from(
json!({
"bot_token": "dc-secret",
"allowed_users": ["*"],
"mention_only": true,
"guild_id": "1234",
"strict_kb_grounding": true,
"model_provider_id": "openai",
"model_id": "gpt-4.1-mini"
})
.to_string(),
))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let config_req = direct_loopback_request()
.method("GET")
.uri("/channels/config")
.body(Body::empty())
.expect("request");
let config_resp = app.clone().oneshot(config_req).await.expect("response");
assert_eq!(config_resp.status(), StatusCode::OK);
let body = to_bytes(config_resp.into_body(), usize::MAX)
.await
.expect("response body");
let payload: Value = serde_json::from_slice(&body).expect("json body");
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("strict_kb_grounding"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("model_provider_id"))
.and_then(Value::as_str),
Some("openai")
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("model_id"))
.and_then(Value::as_str),
Some("gpt-4.1-mini")
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_preserves_existing_token_when_only_model_changes() {
let state = test_state().await;
let _ = state
.config
.patch_project(json!({
"channels": {
"discord": {
"bot_token": "dc-secret",
"allowed_users": ["*"],
"mention_only": true,
"guild_id": "1234"
}
}
}))
.await
.expect("patch project");
let app = app_router(state.clone());
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/discord")
.header("content-type", "application/json")
.body(Body::from(
json!({
"allowed_users": ["*"],
"mention_only": true,
"guild_id": "1234",
"strict_kb_grounding": true,
"model_provider_id": "openai",
"model_id": "gpt-4.1-mini"
})
.to_string(),
))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let config_req = direct_loopback_request()
.method("GET")
.uri("/channels/config")
.body(Body::empty())
.expect("request");
let config_resp = app.clone().oneshot(config_req).await.expect("response");
assert_eq!(config_resp.status(), StatusCode::OK);
let body = to_bytes(config_resp.into_body(), usize::MAX)
.await
.expect("response body");
let payload: Value = serde_json::from_slice(&body).expect("json body");
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("strict_kb_grounding"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("has_token"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("model_provider_id"))
.and_then(Value::as_str),
Some("openai")
);
assert_eq!(
payload
.get("discord")
.and_then(|v| v.get("model_id"))
.and_then(Value::as_str),
Some("gpt-4.1-mini")
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_echoing_sanitized_config_preserves_slack_connections() {
let state = test_state().await;
let _ = state
.config
.patch_project(json!({
"channels": {
"slack": {
"bot_token": "xoxb-secret",
"channel_id": "C_MAIN",
"signing_secret": "shhh",
"events_enabled": true,
"team_id": "T1",
"app_id": "A1",
"tenant": { "org_id": "acme", "workspace_id": "hq" },
"connections": [
{ "channel_id": "C_SALES", "org_units": ["sales"] }
]
}
}
}))
.await
.expect("patch project");
let app = app_router(state.clone());
let config_req = direct_loopback_request()
.method("GET")
.uri("/channels/config")
.body(Body::empty())
.expect("request");
let config_resp = app.clone().oneshot(config_req).await.expect("response");
let body = to_bytes(config_resp.into_body(), usize::MAX)
.await
.expect("response body");
let payload: Value = serde_json::from_slice(&body).expect("json body");
let slack = payload.get("slack").expect("slack entry");
assert!(
slack.get("connections").is_none(),
"the sanitized snapshot must not expose a `connections` config key"
);
assert_eq!(
slack.get("allowed_users"),
Some(&json!([])),
"a missing Slack allowlist is deny-all on signed ingress and must \
not be reported as a '*' wildcard"
);
let summary = slack
.get("connections_summary")
.and_then(Value::as_array)
.expect("connections_summary rows");
assert_eq!(summary.len(), 2, "top-level + per-connection entries");
let put_req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"channel_id": "C_MAIN",
"allowed_users": slack.get("allowed_users"),
"mention_only": false,
"connections_summary": summary,
})
.to_string(),
))
.expect("request");
let put_resp = app.clone().oneshot(put_req).await.expect("response");
assert_eq!(put_resp.status(), StatusCode::OK);
let stored = state.config.get_project_value().await;
let slack = stored
.pointer("/channels/slack")
.expect("stored slack config");
assert_eq!(
slack.pointer("/signing_secret").and_then(Value::as_str),
Some("shhh"),
"echoed sanitized config must not wipe the signing secret"
);
assert_eq!(
slack.pointer("/team_id").and_then(Value::as_str),
Some("T1")
);
assert_eq!(
slack.pointer("/events_enabled").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
slack.pointer("/tenant/org_id").and_then(Value::as_str),
Some("acme")
);
assert_eq!(
slack
.pointer("/connections/0/channel_id")
.and_then(Value::as_str),
Some("C_SALES"),
"echoed sanitized config must not wipe per-connection entries"
);
assert!(
slack
.get("allowed_users")
.and_then(Value::as_array)
.is_none_or(Vec::is_empty),
"echoing the snapshot must not widen a deny-all allowlist to '*'"
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_verify_discord_without_token_returns_setup_hint() {
let state = test_state().await;
let app = app_router(state);
let req = direct_loopback_request()
.method("POST")
.uri("/channels/discord/verify")
.body(Body::empty())
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let body = to_bytes(resp.into_body(), usize::MAX)
.await
.expect("response body");
let payload: Value = serde_json::from_slice(&body).expect("json body");
assert_eq!(
payload.get("ok").and_then(Value::as_bool),
Some(false),
"verify should fail without token"
);
assert_eq!(
payload.get("channel").and_then(Value::as_str),
Some("discord")
);
assert!(
payload
.get("hints")
.and_then(Value::as_array)
.is_some_and(|arr| !arr.is_empty()),
"verify should include setup hints"
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_normalizes_empty_allowed_users_to_wildcard() {
let state = test_state().await;
let app = app_router(state.clone());
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/telegram")
.header("content-type", "application/json")
.body(Body::from(
json!({
"bot_token": "tg-secret",
"allowed_users": [],
"mention_only": false
})
.to_string(),
))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let effective = state.config.get_effective_value().await;
assert_eq!(
effective
.get("channels")
.and_then(|v| v.get("telegram"))
.and_then(|v| v.get("allowed_users"))
.and_then(Value::as_array)
.cloned(),
Some(vec![Value::String("*".to_string())])
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_fresh_slack_save_without_allowlist_stays_deny_all() {
let state = test_state().await;
let app = app_router(state.clone());
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"bot_token": "xoxb-fresh",
"channel_id": "C_FRESH"
})
.to_string(),
))
.expect("request");
let resp = app.oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let stored = state.config.get_project_value().await;
assert_eq!(
stored.pointer("/channels/slack/allowed_users"),
Some(&json!([])),
"a fresh save without an allowlist must not persist the '*' wildcard"
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_migrating_installation_does_not_carry_secrets() {
let state = test_state().await;
let _ = state
.config
.patch_project(json!({
"channels": {
"slack": {
"bot_token": "xoxb-top",
"channel_id": "C_MAIN",
"team_id": "T1",
"app_id": "A_OLD",
"signing_secret": "shh-old",
"events_enabled": true,
"connections": [
{ "channel_id": "C_SALES", "bot_token": "xoxb-sales" }
]
}
}
}))
.await
.expect("patch project");
let app = app_router(state.clone());
let effective = state.config.get_effective_value().await;
let old_signing_secret_id = tandem_core::slack_signing_secret_store_id(
effective
.pointer("/channels/slack")
.and_then(Value::as_object)
.expect("stored Slack config"),
);
assert!(
tandem_core::load_provider_auth().contains_key(&old_signing_secret_id),
"sanity: the old installation signing secret is stored"
);
let connections = crate::config::channels::slack_connections_from_effective_config(&effective);
assert!(connections
.iter()
.any(|connection| connection.channel_id == "C_SALES"
&& connection.bot_token.as_deref() == Some("xoxb-sales")));
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"channel_id": "C_MAIN",
"team_id": "T1",
"app_id": "A_NEW",
"allowed_users": []
})
.to_string(),
))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"a migrating save without new credentials must be rejected"
);
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"channel_id": "C_MAIN",
"team_id": "T1",
"app_id": "A_NEW",
"bot_token": "xoxb-new",
"allowed_users": []
})
.to_string(),
))
.expect("request");
let resp = app.oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let effective = state.config.get_effective_value().await;
assert_eq!(
effective
.pointer("/channels/slack/bot_token")
.and_then(Value::as_str),
Some("xoxb-new"),
"the migrated installation must run on the newly supplied token"
);
assert!(
effective
.pointer("/channels/slack/signing_secret")
.and_then(Value::as_str)
.is_none_or(|secret| secret != "shh-old"),
"the old app's signing secret must not follow the new identity"
);
let connections = crate::config::channels::slack_connections_from_effective_config(&effective);
let sales = connections
.iter()
.find(|connection| connection.channel_id == "C_SALES")
.expect("connection structure preserved");
assert!(
sales.bot_token.as_deref() != Some("xoxb-sales"),
"an inheriting connection must not carry the old installation's token"
);
assert!(
sales.signing_secret.is_none(),
"the migrated installation must fail closed until its own secret is set"
);
assert!(
!tandem_core::load_provider_auth().contains_key(&old_signing_secret_id),
"the old installation signing secret must be purged after config persistence"
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_protected_audit_records_shape_without_raw_credentials() {
let state = test_state().await;
let app = app_router(state.clone());
let token = "tg-channel-audit-secret-canary";
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/telegram")
.header("content-type", "application/json")
.body(Body::from(
json!({
"bot_token": token,
"allowed_users": ["@alice"],
"mention_only": true
})
.to_string(),
))
.expect("request");
let resp = app.oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let audit = tokio::fs::read_to_string(&state.protected_audit_path)
.await
.expect("protected audit file");
assert!(audit.contains("channel_config_update"));
assert!(
!audit.contains(token),
"channel administrative audit must contain field names, never raw credentials"
);
let _ = tandem_core::delete_provider_auth("channel::telegram::bot_token");
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_migrating_installation_revokes_stored_top_level_token() {
let state = test_state().await;
let _ = state
.config
.patch_project(json!({
"channels": {
"slack": {
"bot_token": "xoxb-top",
"channel_id": "C_MAIN",
"team_id": "T1",
"app_id": "A_OLD",
"connections": [
{
"channel_id": "C_SALES",
"team_id": "T2",
"app_id": "A2",
"bot_token": "xoxb-sales"
}
]
}
}
}))
.await
.expect("patch project");
let app = app_router(state.clone());
let effective = state.config.get_effective_value().await;
assert_eq!(
effective
.pointer("/channels/slack/bot_token")
.and_then(Value::as_str),
Some("xoxb-top")
);
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"channel_id": "C_MAIN",
"team_id": "T1",
"app_id": "A_NEW",
"allowed_users": []
})
.to_string(),
))
.expect("request");
let resp = app.oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let effective = state.config.get_effective_value().await;
assert!(
effective
.pointer("/channels/slack/bot_token")
.and_then(Value::as_str)
.is_none_or(|token| token.trim().is_empty()),
"the old installation's stored token must not resurface under the new identity"
);
let connections = crate::config::channels::slack_connections_from_effective_config(&effective);
assert!(
connections
.iter()
.any(|connection| connection.channel_id == "C_SALES"
&& connection.bot_token.as_deref() == Some("xoxb-sales")),
"the self-declared connection keeps its own credentials"
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_accepts_connection_only_slack_configs() {
let state = test_state().await;
let app = app_router(state.clone());
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"team_id": "T1",
"app_id": "A1",
"signing_secret": "shh",
"events_enabled": true,
"connections": [
{ "channel_id": "C_SALES", "bot_token": "xoxb-sales" }
]
})
.to_string(),
))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(
resp.status(),
StatusCode::OK,
"connection-only Slack configs must be accepted"
);
let effective = state.config.get_effective_value().await;
let connections = crate::config::channels::slack_connections_from_effective_config(&effective);
assert!(
connections
.iter()
.any(|connection| connection.channel_id == "C_SALES"
&& connection.bot_token.as_deref() == Some("xoxb-sales")),
"the resolved connection must be usable after the save"
);
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"channel_id": null,
"events_enabled": true,
"mention_only": false,
"allowed_users": []
})
.to_string(),
))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(
resp.status(),
StatusCode::OK,
"echoing the sanitized snapshot's channel_id: null must not 400"
);
let effective = state.config.get_effective_value().await;
let connections = crate::config::channels::slack_connections_from_effective_config(&effective);
assert!(
connections
.iter()
.any(|connection| connection.channel_id == "C_SALES"
&& connection.bot_token.as_deref() == Some("xoxb-sales")),
"the preserved connection must survive a null-channel_id echo"
);
let state = test_state().await;
let app = app_router(state.clone());
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/slack")
.header("content-type", "application/json")
.body(Body::from(
json!({
"team_id": "T1",
"connections": [ { "channel_id": "C_TOKENLESS" } ]
})
.to_string(),
))
.expect("request");
let resp = app.oneshot(req).await.expect("response");
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"a config with no usable connection must still be rejected"
);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_put_unknown_channel_returns_not_found() {
let state = test_state().await;
let app = app_router(state);
let req = direct_loopback_request()
.method("PUT")
.uri("/channels/unknown")
.header("content-type", "application/json")
.body(Body::from(json!({ "bot_token": "x" }).to_string()))
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_delete_unknown_channel_returns_not_found() {
let state = test_state().await;
let app = app_router(state);
let req = direct_loopback_request()
.method("DELETE")
.uri("/channels/unknown")
.body(Body::empty())
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn channel_identity_authority_is_admin_only_tenant_exact_and_locally_compatible() {
let state = test_state().await;
let app = app_router(state.clone());
let low_privilege = channel_tenant_request(
"POST",
"/channels/step-up",
"org-a",
"workspace-a",
"member-a",
false,
json!({
"channel": "slack",
"user_id": "U-EXACT",
"tenant_org_id": "org-a",
"tenant_workspace_id": "workspace-a"
}),
);
let response = app.clone().oneshot(low_privilege).await.expect("response");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let cross_tenant = channel_tenant_request(
"POST",
"/channels/step-up",
"org-a",
"workspace-a",
"admin-a",
true,
json!({
"channel": "slack",
"user_id": "U-EXACT",
"tenant_org_id": "org-b",
"tenant_workspace_id": "workspace-b"
}),
);
let response = app.clone().oneshot(cross_tenant).await.expect("response");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert!(
!state
.channel_step_up_active("slack", "U-EXACT", Some(("org-b", "workspace-b")))
.await
);
let exact_tenant = channel_tenant_request(
"POST",
"/channels/step-up",
"org-a",
"workspace-a",
"admin-a",
true,
json!({
"channel": "slack",
"user_id": "U-EXACT",
"tenant_org_id": "org-a",
"tenant_workspace_id": "workspace-a"
}),
);
let response = app.clone().oneshot(exact_tenant).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert!(
state
.channel_step_up_active("slack", "U-EXACT", Some(("org-a", "workspace-a")))
.await
);
let issue = channel_tenant_request(
"POST",
"/channels/enroll",
"org-a",
"workspace-a",
"admin-a",
true,
json!({
"action": "issue",
"channel": "telegram",
"user_id": "4242",
"tier": "approve",
"issued_by": "spoofed-actor",
"tenant_org_id": "org-a",
"tenant_workspace_id": "workspace-a"
}),
);
let response = app.clone().oneshot(issue).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("response body");
let issued: Value = serde_json::from_slice(&body).expect("issue response");
let pairing_code = issued
.get("pairing_code")
.and_then(Value::as_str)
.expect("pairing code")
.to_string();
let pending = state
.pending_channel_enrollment_code(&pairing_code)
.await
.expect("pending enrollment");
assert_eq!(pending.issued_by.as_deref(), Some("admin-a"));
assert_eq!(pending.tenant_org_id.as_deref(), Some("org-a"));
for candidate in ["NOT-A-REAL-CODE", pairing_code.as_str()] {
let ordinary_confirm = channel_tenant_request(
"POST",
"/channels/enroll",
"org-a",
"workspace-a",
"member-a",
false,
json!({
"action": "confirm",
"pairing_code": candidate,
"enrolled_by": "member-a"
}),
);
let response = app
.clone()
.oneshot(ordinary_confirm)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
assert!(state
.pending_channel_enrollment_code(&pairing_code)
.await
.is_some());
let wrong_tenant_confirm = channel_tenant_request(
"POST",
"/channels/enroll",
"org-b",
"workspace-b",
"admin-b",
true,
json!({
"action": "confirm",
"pairing_code": pairing_code,
"enrolled_by": "admin-b"
}),
);
let response = app
.clone()
.oneshot(wrong_tenant_confirm)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert!(state
.pending_channel_enrollment_code(&pairing_code)
.await
.is_some());
let exact_confirm = channel_tenant_request(
"POST",
"/channels/enroll",
"org-a",
"workspace-a",
"admin-a",
true,
json!({
"action": "confirm",
"pairing_code": pairing_code,
"enrolled_by": "spoofed-actor"
}),
);
let response = app.clone().oneshot(exact_confirm).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert!(state
.pending_channel_enrollment_code(&pairing_code)
.await
.is_none());
let audit = tokio::fs::read_to_string(&state.protected_audit_path)
.await
.expect("protected audit file");
let code_resource = format!(
"code-sha256:{:x}",
sha2::Sha256::digest(pairing_code.trim().to_ascii_uppercase().as_bytes())
);
assert!(audit.contains(&code_resource));
assert!(
!audit.contains(&pairing_code),
"channel enrollment audit must identify the code by digest, never reveal it"
);
let local_state = test_state().await;
let local_app = app_router(local_state.clone());
let local_step_up = direct_loopback_request()
.method("POST")
.uri("/channels/step-up")
.header("content-type", "application/json")
.body(Body::from(
json!({"channel": "slack", "user_id": "U-LOCAL"}).to_string(),
))
.expect("local request");
let response = local_app.oneshot(local_step_up).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert!(
local_state
.channel_step_up_active("slack", "U-LOCAL", None)
.await
);
}
#[tokio::test]
async fn slack_sender_inventory_requires_deployment_admin() {
let state = test_state().await;
let app = app_router(state);
let member = channel_tenant_request(
"GET",
"/channels/slack/senders",
"org-a",
"workspace-a",
"member-a",
false,
json!({}),
);
let response = app.clone().oneshot(member).await.expect("response");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let admin = channel_tenant_request(
"GET",
"/channels/slack/senders",
"org-a",
"workspace-a",
"admin-a",
true,
json!({}),
);
let response = app.oneshot(admin).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
#[serial_test::serial(channel_provider_auth)]
async fn channels_verify_unknown_channel_returns_not_found() {
let state = test_state().await;
let app = app_router(state);
let req = direct_loopback_request()
.method("POST")
.uri("/channels/unknown/verify")
.body(Body::empty())
.expect("request");
let resp = app.clone().oneshot(req).await.expect("response");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}