Unified search box (#30) (#75)
ci / rust (push) Failing after 1m26s
ci / web (push) Successful in 29s
e2e / e2e (push) Successful in 1m55s

This commit was merged in pull request #75.
This commit is contained in:
2026-08-22 22:08:32 +01:00
parent ac6ed8b798
commit bf526b9198
8 changed files with 1113 additions and 1 deletions
+79
View File
@@ -0,0 +1,79 @@
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<AppState>) -> Result<Json<Vec<Root>>, 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<serde_json::Value> = reqwest::get(format!("http://{address}/api/roots"))
.await
.expect("roots")
.json()
.await
.expect("json");
assert_eq!(roots.len(), 2, "the two seeded movie 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")));
}
}