webfingerd/tests/test_links.rs

362 lines
12 KiB
Rust

mod common;
use axum_test::TestServer;
use serde_json::json;
use webfingerd::handler;
/// Helper: create verified domain + service token, return (domain_id, owner_token, service_token).
/// Uses MockChallengeVerifier -- no manual DB manipulation needed.
async fn setup_domain_and_token(
server: &TestServer,
_state: &webfingerd::state::AppState,
domain_name: &str,
) -> (String, String, String) {
// Register domain
let create_resp = server
.post("/api/v1/domains")
.json(&json!({"domain": domain_name, "challenge_type": "dns-01"}))
.await;
let body: serde_json::Value = create_resp.json();
let id = body["id"].as_str().unwrap().to_string();
let reg_secret = body["registration_secret"].as_str().unwrap().to_string();
// MockChallengeVerifier always succeeds
let verify_resp = server
.post(&format!("/api/v1/domains/{id}/verify"))
.json(&json!({"registration_secret": reg_secret}))
.await;
let owner_token = verify_resp.json::<serde_json::Value>()["owner_token"]
.as_str()
.unwrap()
.to_string();
// Create service token
let token_resp = server
.post(&format!("/api/v1/domains/{id}/tokens"))
.add_header("Authorization", format!("Bearer {owner_token}"))
.json(&json!({
"name": "oxifed",
"allowed_rels": ["self", "http://webfinger.net/rel/profile-page"],
"resource_pattern": "acct:*@example.com"
}))
.await;
let service_token = token_resp.json::<serde_json::Value>()["token"]
.as_str()
.unwrap()
.to_string();
(id, owner_token, service_token)
}
#[tokio::test]
async fn test_register_link() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
let response = server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice",
"type": "application/activity+json"
}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let body: serde_json::Value = response.json();
assert!(body["id"].is_string());
// Should now be in cache and queryable
let wf = server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await;
wf.assert_status_ok();
let jrd: serde_json::Value = wf.json();
assert_eq!(jrd["subject"], "acct:alice@example.com");
assert_eq!(jrd["links"][0]["rel"], "self");
}
#[tokio::test]
async fn test_register_link_rejected_for_forbidden_rel() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
let response = server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://evil.com"
}))
.await;
response.assert_status(axum::http::StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_register_link_rejected_for_wrong_domain() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
let response = server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@evil.com",
"rel": "self",
"href": "https://evil.com/users/alice"
}))
.await;
response.assert_status(axum::http::StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_upsert_link() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
// First insert
server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice",
"type": "application/activity+json"
}))
.await
.assert_status(axum::http::StatusCode::CREATED);
// Upsert with same (resource, rel, href) but different type
server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice",
"type": "application/ld+json"
}))
.await
.assert_status(axum::http::StatusCode::CREATED);
// Should only have one link
let wf = server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await;
let jrd: serde_json::Value = wf.json();
let links = jrd["links"].as_array().unwrap();
assert_eq!(links.len(), 1);
assert_eq!(links[0]["type"], "application/ld+json");
}
#[tokio::test]
async fn test_batch_link_registration() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
let response = server
.post("/api/v1/links/batch")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"links": [
{
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice",
"type": "application/activity+json"
},
{
"resource_uri": "acct:bob@example.com",
"rel": "self",
"href": "https://example.com/users/bob",
"type": "application/activity+json"
}
]
}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
// Both should be queryable
server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await
.assert_status_ok();
server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:bob@example.com")
.await
.assert_status_ok();
}
#[tokio::test]
async fn test_batch_all_or_nothing() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
// Second link has forbidden rel -- entire batch should fail
let response = server
.post("/api/v1/links/batch")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"links": [
{
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice"
},
{
"resource_uri": "acct:bob@example.com",
"rel": "forbidden-rel",
"href": "https://example.com/users/bob"
}
]
}))
.await;
// Batch should fail
response.assert_status(axum::http::StatusCode::FORBIDDEN);
// alice should NOT be registered (all-or-nothing)
server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await
.assert_status_not_found();
}
#[tokio::test]
async fn test_delete_link() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
let create_resp = server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice"
}))
.await;
let link_id = create_resp.json::<serde_json::Value>()["id"]
.as_str()
.unwrap()
.to_string();
// Delete it
server
.delete(&format!("/api/v1/links/{link_id}"))
.add_header("Authorization", format!("Bearer {service_token}"))
.await
.assert_status(axum::http::StatusCode::NO_CONTENT);
// Should be gone
server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await
.assert_status_not_found();
}
#[tokio::test]
async fn test_link_with_ttl() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (_, _, service_token) = setup_domain_and_token(&server, &state, "example.com").await;
let response = server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice",
"ttl_seconds": 300
}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let body: serde_json::Value = response.json();
assert!(body["expires_at"].is_string());
}
#[tokio::test]
async fn test_revoke_service_token_deletes_links() {
let state = common::test_state().await;
let app = handler::router(state.clone());
let server = TestServer::new(app);
let (id, owner_token, service_token) =
setup_domain_and_token(&server, &state, "example.com").await;
// Register a link
server
.post("/api/v1/links")
.add_header("Authorization", format!("Bearer {service_token}"))
.json(&json!({
"resource_uri": "acct:alice@example.com",
"rel": "self",
"href": "https://example.com/users/alice",
"type": "application/activity+json"
}))
.await
.assert_status(axum::http::StatusCode::CREATED);
// Verify it exists
server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await
.assert_status_ok();
// Extract the token ID from the service token (format: {id}.{secret})
let token_id = service_token.split('.').next().unwrap();
// Revoke the service token via owner API
server
.delete(&format!("/api/v1/domains/{id}/tokens/{token_id}"))
.add_header("Authorization", format!("Bearer {owner_token}"))
.await
.assert_status(axum::http::StatusCode::NO_CONTENT);
// WebFinger should no longer find the link (cascade delete + cache eviction)
server
.get("/.well-known/webfinger")
.add_query_param("resource", "acct:alice@example.com")
.await
.assert_status_not_found();
}