heldar_kernel/routes/
registry.rs1use axum::extract::State;
9use axum::routing::{get, post};
10use axum::{Json, Router};
11use serde_json::json;
12
13use crate::auth::{self, Principal};
14use crate::error::AppResult;
15use crate::registry::RegistryView;
16use crate::services;
17use crate::state::AppState;
18
19pub fn router() -> Router<AppState> {
20 Router::new()
21 .route("/api/v1/registry", get(list))
22 .route("/api/v1/registry/refresh", post(refresh))
23}
24
25async fn list(State(st): State<AppState>, principal: Principal) -> AppResult<Json<RegistryView>> {
27 principal.require(principal.can_view(), "view the plugin store")?;
28 let registrations = services::modules::list_registered(&st.pool).await?;
29 Ok(Json(st.catalog.view(&st.modules, ®istrations)))
30}
31
32async fn refresh(
34 State(st): State<AppState>,
35 principal: Principal,
36) -> AppResult<Json<RegistryView>> {
37 principal.require(principal.can_admin(), "refresh the plugin registry")?;
38 st.catalog.refresh().await;
39 auth::audit(
40 &st.pool,
41 &principal,
42 "refresh_registry",
43 "registry",
44 "remote",
45 json!({}),
46 )
47 .await;
48 let registrations = services::modules::list_registered(&st.pool).await?;
49 Ok(Json(st.catalog.view(&st.modules, ®istrations)))
50}
51
52#[cfg(test)]
53mod tests {
54 use crate::config::Config;
55 use crate::modules::{ModuleKind, ModuleManifest, NavEntry};
56 use crate::services::recorder::RecorderManager;
57 use crate::services::registry::CatalogService;
58 use crate::services::sampler::SamplerManager;
59 use crate::state::AppState;
60 use axum::body::Body;
61 use axum::http::Request;
62 use std::sync::Arc;
63 use tower::Service;
64
65 async fn state_with(modules: Vec<ModuleManifest>) -> AppState {
66 let pool = sqlx::sqlite::SqlitePoolOptions::new()
67 .max_connections(1)
68 .connect("sqlite::memory:")
69 .await
70 .unwrap();
71 crate::db::run_migrations(&pool).await.unwrap();
72 let mut cfg = Config::from_env();
73 cfg.auth_enabled = false;
74 let cfg = Arc::new(cfg);
75 AppState {
76 recorder: RecorderManager::new(pool.clone(), cfg.clone()),
77 sampler: SamplerManager::new(pool.clone(), cfg.clone()),
78 mirror: None,
79 consumers: Arc::new(Vec::new()),
80 modules: Arc::new(modules),
81 catalog: Arc::new(CatalogService::new(&cfg)),
82 http: reqwest::Client::new(),
83 started_at: chrono::Utc::now(),
84 pool,
85 cfg,
86 }
87 }
88
89 #[tokio::test]
92 async fn serves_bundled_catalog_cross_referenced() {
93 let entry = ModuleManifest::new(
94 "entry",
95 "Access Control",
96 "0.0.0",
97 "Heldar",
98 ModuleKind::Core,
99 "d",
100 vec![NavEntry::new("/entry", "Entry", "entry")],
101 );
102 let st = state_with(vec![entry]).await;
103 let mut app = super::router().with_state(st);
104 let res = app
105 .call(
106 Request::builder()
107 .uri("/api/v1/registry")
108 .body(Body::empty())
109 .unwrap(),
110 )
111 .await
112 .unwrap();
113 assert_eq!(res.status(), 200);
114 let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
115 .await
116 .unwrap();
117 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
118
119 let entries = json["entries"].as_array().unwrap();
120 let find = |id: &str| entries.iter().find(|e| e["id"] == id).cloned().unwrap();
121
122 let entry_v = find("entry");
123 assert_eq!(entry_v["shelf"], "core");
124 assert_eq!(entry_v["state"], "included"); assert_eq!(entry_v["verified"], true); assert_eq!(entry_v["source"], "bundled");
127
128 assert_eq!(find("movement")["state"], "not_in_build");
130
131 let hello = find("hello-module");
133 assert_eq!(hello["shelf"], "community");
134 assert_eq!(hello["state"], "available");
135
136 let bundled = json["sources"]
138 .as_array()
139 .unwrap()
140 .iter()
141 .find(|s| s["source"] == "bundled")
142 .unwrap();
143 assert_eq!(bundled["verified"], true);
144 assert_eq!(bundled["first_party"], true);
145 }
146}