//! Router-level tests for the GPG keys settings page: login, CSRF, deleting only your own keys //! and the unavailable notice. use forge_core::Store; use topcoat::router::request::Request; use topcoat::router::{Body, Router, to_bytes}; use crate::auth::SESSION_COOKIE; const CSRF: &str = "gpg-test-csrf"; const TOKEN: &str = "gpg-test-token"; const FINGERPRINT: &str = "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"; struct Site { router: Router, store: Store, _dir: tempfile::TempDir, } struct Reply { status: u16, location: Option, body: String, } /// A fresh site; the GPG availability is reported alongside it so tests can check the notice. async fn site() -> (Site, bool) { let dir = tempfile::tempdir().expect("tempdir"); let state = crate::test_support::test_state(dir.path()).await; let available = crate::signatures::gpg_available(&state).await; let store = state.store.clone(); let router = crate::test_support::app_router(state); ( Site { router, store, _dir: dir, }, available, ) } async fn sign_in(store: &Store, user_id: &str) -> String { crate::test_support::create_session(store, user_id, TOKEN, CSRF).await; format!("__Host-{SESSION_COOKIE}={TOKEN}") } async fn call( site: &Site, method: &str, uri: &str, cookie: Option<&str>, fields: Option<&[(&str, &str)]>, ) -> Reply { let mut builder = Request::builder().method(method).uri(uri); if let Some(cookie) = cookie { builder = builder.header("cookie", cookie); } let body = match fields { Some(fields) => { builder = builder.header("content-type", "application/x-www-form-urlencoded"); Body::from( form_urlencoded::Serializer::new(String::new()) .extend_pairs(fields) .finish(), ) } None => Body::empty(), }; let res = site .router .handle(builder.body(body).expect("request")) .await; let status = res.status().as_u16(); let location = res .headers() .get("location") .and_then(|v| v.to_str().ok()) .map(str::to_owned); let bytes = to_bytes(res.into_body(), usize::MAX).await.expect("body"); Reply { status, location, body: String::from_utf8_lossy(&bytes).into_owned(), } } #[tokio::test] async fn gpg_page_requires_login() { let (site, _) = site().await; let reply = call(&site, "GET", "/settings/gpg", None, None).await; assert_eq!(reply.status, 303); assert!( reply .location .as_deref() .is_some_and(|l| l.starts_with("/login")), "redirects to login, got {:?}", reply.location ); } #[tokio::test] async fn adding_a_key_requires_login() { let (site, _) = site().await; let fields = [("csrf", CSRF), ("armored", "junk")]; let reply = call(&site, "POST", "/settings/gpg", None, Some(&fields)).await; assert_eq!(reply.status, 303); } #[tokio::test] async fn adding_a_key_rejects_a_bad_csrf_token() { let (site, _) = site().await; let alice = site .store .create_user("alice", None, "h", false) .await .unwrap(); let cookie = sign_in(&site.store, &alice.id).await; let fields = [("csrf", "wrong"), ("armored", "junk")]; let reply = call(&site, "POST", "/settings/gpg", Some(&cookie), Some(&fields)).await; assert_eq!(reply.status, 403); } #[tokio::test] async fn deleting_a_key_requires_login_and_csrf() { let (site, _) = site().await; let alice = site .store .create_user("alice", None, "h", false) .await .unwrap(); let key = site .store .add_gpg_key( &alice.id, FINGERPRINT, "armored", &["alice@example.com".into()], ) .await .unwrap(); let fields = [("csrf", CSRF), ("id", key.id.as_str())]; let anon = call(&site, "POST", "/settings/gpg/delete", None, Some(&fields)).await; assert_eq!(anon.status, 303); let cookie = sign_in(&site.store, &alice.id).await; let bad = [("csrf", "wrong"), ("id", key.id.as_str())]; let reply = call( &site, "POST", "/settings/gpg/delete", Some(&cookie), Some(&bad), ) .await; assert_eq!(reply.status, 403); assert_eq!(site.store.list_gpg_keys(&alice.id).await.unwrap().len(), 1); } #[tokio::test] async fn cannot_delete_another_users_key() { let (site, _) = site().await; let alice = site .store .create_user("alice", None, "h", false) .await .unwrap(); let bob = site .store .create_user("bob", None, "h", false) .await .unwrap(); let key = site .store .add_gpg_key(&bob.id, FINGERPRINT, "armored", &["bob@example.com".into()]) .await .unwrap(); let cookie = sign_in(&site.store, &alice.id).await; let fields = [("csrf", CSRF), ("id", key.id.as_str())]; let reply = call( &site, "POST", "/settings/gpg/delete", Some(&cookie), Some(&fields), ) .await; assert_eq!(reply.status, 404); assert_eq!(site.store.list_gpg_keys(&bob.id).await.unwrap().len(), 1); } #[tokio::test] async fn owner_can_delete_their_own_key() { let (site, _) = site().await; let alice = site .store .create_user("alice", None, "h", false) .await .unwrap(); let key = site .store .add_gpg_key( &alice.id, FINGERPRINT, "armored", &["alice@example.com".into()], ) .await .unwrap(); let cookie = sign_in(&site.store, &alice.id).await; let fields = [("csrf", CSRF), ("id", key.id.as_str())]; let reply = call( &site, "POST", "/settings/gpg/delete", Some(&cookie), Some(&fields), ) .await; assert_eq!(reply.status, 303); assert!( site.store .list_gpg_keys(&alice.id) .await .unwrap() .is_empty() ); } #[tokio::test] async fn page_lists_keys_and_shows_the_notice_only_when_gpg_is_missing() { let (site, available) = site().await; let alice = site .store .create_user("alice", None, "h", false) .await .unwrap(); site.store .add_gpg_key( &alice.id, FINGERPRINT, "armored", &["alice@example.com".into()], ) .await .unwrap(); let cookie = sign_in(&site.store, &alice.id).await; let reply = call(&site, "GET", "/settings/gpg", Some(&cookie), None).await; assert_eq!(reply.status, 200); assert!(reply.body.contains(FINGERPRINT)); assert!(reply.body.contains("alice@example.com")); let notice = "GPG verification is unavailable on this server"; assert_eq!(reply.body.contains(notice), !available); }