use axum::extract::State; use axum::Json; use serde::Serialize; use utoipa::ToSchema; use crate::movies::{ApiError, ErrorBody}; use crate::state::AppState; /// A root folder joined with its policy, enough for the add flow to pre-fill /// root and policy (DESIGN.md ยง9.2) without a second request. #[derive(Debug, Clone, Serialize, ToSchema)] pub struct Root { pub id: i64, pub kind: String, pub audience: String, pub path: String, pub policy_id: i64, pub policy_name: String, } #[utoipa::path( get, path = "/api/roots", tag = "roots", responses( (status = 200, body = [Root]), (status = 500, body = ErrorBody), (status = 503, body = ErrorBody) ) )] pub async fn list(State(state): State) -> Result>, ApiError> { let database = state.database().ok_or(ApiError::Unavailable)?; let roots = sqlx::query_as!( Root, r#"SELECT r.id AS "id!: i64", r.kind AS "kind!: String", r.audience AS "audience!: String", r.path AS "path!: String", p.id AS "policy_id!: i64", p.name AS "policy_name!: String" FROM roots r JOIN policies p ON p.id = r.policy_id ORDER BY r.id"# ) .fetch_all(database.pool()) .await?; Ok(Json(roots)) } #[cfg(test)] mod tests { use crate::state::Upstreams; use crate::{router, AppState}; #[tokio::test] async fn roots_carry_their_policy_name() { let dir = tempfile::tempdir().expect("tempdir"); let database = arr_db::Db::connect(dir.path().join("arr.db")) .await .expect("database"); database.migrate().await.expect("migrate"); let state = AppState::new(Upstreams::new( "http://127.0.0.1:1".into(), "http://127.0.0.1:1".into(), )) .expect("state") .with_database(database); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("bind"); let address = listener.local_addr().expect("address"); let app = router(state); tokio::spawn(async move { axum::serve(listener, app).await.expect("serve") }); let roots: Vec = reqwest::get(format!("http://{address}/api/roots")) .await .expect("roots") .json() .await .expect("json"); assert_eq!(roots.len(), 4, "the seeded movie and TV roots: {roots:?}"); assert_eq!(roots[0]["audience"], "main"); assert_eq!(roots[0]["policy_name"], "Movies โ€” main"); assert_eq!(roots[1]["audience"], "kids"); assert!(roots[1]["path"] .as_str() .is_some_and(|p| p.contains("kids"))); assert_eq!(roots[2]["kind"], "tv"); assert_eq!(roots[2]["policy_name"], "TV โ€” main"); assert_eq!(roots[3]["kind"], "tv"); assert_eq!(roots[3]["policy_name"], "TV โ€” kids"); } }