add SPA config UI, wire media/rss adapters, event-driven layout push

- React SPA: dashboard, data sources CRUD, widgets CRUD, layout builder,
  presets. TanStack Router + Query, shadcn/ui, Vite proxy to :3000
- wire media + rss adapters into polling loop, remove xtb source type
- media adapter: read username/password from headers, proper subsonic auth
- event handler: subscribe to LayoutChanged, push screen update to clients
- fix clippy warnings across workspace (Default impls, collapsible ifs,
  redundant closures, is_none_or, unused imports)
This commit is contained in:
2026-06-19 00:12:42 +02:00
parent 21c08911df
commit 26ebfad3a2
175 changed files with 12338 additions and 801 deletions

View File

@@ -1,9 +1,9 @@
mod routes;
use std::sync::Arc;
use axum::Router;
use tower_http::cors::CorsLayer;
use domain::{ConfigRepository, EventPublisher};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
pub struct AppState<C, E> {
pub config: Arc<C>,

View File

@@ -1,54 +1,107 @@
use crate::AppState;
use api_types::DataSourceDto;
use application::ConfigService;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use domain::{ConfigRepository, EventPublisher};
use application::ConfigService;
use crate::AppState;
use api_types::DataSourceDto;
type S<C, E> = State<AppState<C, E>>;
pub async fn list_data_sources<C, E>(State(state): S<C, E>) -> Result<Json<Vec<DataSourceDto>>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn list_data_sources<C, E>(
State(state): S<C, E>,
) -> Result<Json<Vec<DataSourceDto>>, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let sources = state.config.list_data_sources().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let sources = state
.config
.list_data_sources()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(sources.iter().map(DataSourceDto::from).collect()))
}
pub async fn get_data_source<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<Json<DataSourceDto>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn get_data_source<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<Json<DataSourceDto>, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let source = state.config.get_data_source(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let source = state
.config
.get_data_source(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match source {
Some(s) => Ok(Json(DataSourceDto::from(&s))),
None => Err(StatusCode::NOT_FOUND),
}
}
pub async fn create_data_source<C, E>(State(state): S<C, E>, Json(body): Json<DataSourceDto>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn create_data_source<C, E>(
State(state): S<C, E>,
Json(body): Json<DataSourceDto>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let source = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let source = body
.into_domain()
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.create_data_source(source).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.create_data_source(source)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::CREATED)
}
pub async fn update_data_source<C, E>(State(state): S<C, E>, Path(_id): Path<u16>, Json(body): Json<DataSourceDto>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn update_data_source<C, E>(
State(state): S<C, E>,
Path(_id): Path<u16>,
Json(body): Json<DataSourceDto>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let source = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let source = body
.into_domain()
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.update_data_source(source).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.update_data_source(source)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::OK)
}
pub async fn delete_data_source<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<StatusCode, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn delete_data_source<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<StatusCode, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.delete_data_source(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
svc.delete_data_source(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::NO_CONTENT)
}

View File

@@ -1,27 +1,42 @@
use axum::{
extract::State,
http::StatusCode,
response::Json,
};
use domain::{ConfigRepository, EventPublisher};
use application::ConfigService;
use crate::AppState;
use api_types::LayoutDto;
use application::ConfigService;
use axum::{extract::State, http::StatusCode, response::Json};
use domain::{ConfigRepository, EventPublisher};
type S<C, E> = State<AppState<C, E>>;
pub async fn get_layout<C, E>(State(state): S<C, E>) -> Result<Json<Option<LayoutDto>>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let layout = state.config.get_layout().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let layout = state
.config
.get_layout()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(layout.as_ref().map(LayoutDto::from)))
}
pub async fn update_layout<C, E>(State(state): S<C, E>, Json(body): Json<LayoutDto>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn update_layout<C, E>(
State(state): S<C, E>,
Json(body): Json<LayoutDto>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let layout = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let layout = body
.into_domain()
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.update_layout(layout).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.update_layout(layout)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::OK)
}

View File

@@ -1,12 +1,12 @@
mod widgets;
mod data_sources;
mod layout;
mod presets;
mod widgets;
use axum::Router;
use axum::routing::{get, post, put, delete};
use domain::{ConfigRepository, EventPublisher};
use crate::AppState;
use axum::Router;
use axum::routing::{get, post};
use domain::{ConfigRepository, EventPublisher};
pub fn api_routes<C, E>() -> Router<AppState<C, E>>
where
@@ -16,12 +16,38 @@ where
E::Error: std::fmt::Debug + Send,
{
Router::new()
.route("/widgets", get(widgets::list_widgets::<C, E>).post(widgets::create_widget::<C, E>))
.route("/widgets/{id}", get(widgets::get_widget::<C, E>).put(widgets::update_widget::<C, E>).delete(widgets::delete_widget::<C, E>))
.route("/data-sources", get(data_sources::list_data_sources::<C, E>).post(data_sources::create_data_source::<C, E>))
.route("/data-sources/{id}", get(data_sources::get_data_source::<C, E>).put(data_sources::update_data_source::<C, E>).delete(data_sources::delete_data_source::<C, E>))
.route("/layout", get(layout::get_layout::<C, E>).put(layout::update_layout::<C, E>))
.route("/presets", get(presets::list_presets::<C, E>).post(presets::create_preset::<C, E>))
.route("/presets/{id}", get(presets::get_preset::<C, E>).delete(presets::delete_preset::<C, E>))
.route(
"/widgets",
get(widgets::list_widgets::<C, E>).post(widgets::create_widget::<C, E>),
)
.route(
"/widgets/{id}",
get(widgets::get_widget::<C, E>)
.put(widgets::update_widget::<C, E>)
.delete(widgets::delete_widget::<C, E>),
)
.route(
"/data-sources",
get(data_sources::list_data_sources::<C, E>)
.post(data_sources::create_data_source::<C, E>),
)
.route(
"/data-sources/{id}",
get(data_sources::get_data_source::<C, E>)
.put(data_sources::update_data_source::<C, E>)
.delete(data_sources::delete_data_source::<C, E>),
)
.route(
"/layout",
get(layout::get_layout::<C, E>).put(layout::update_layout::<C, E>),
)
.route(
"/presets",
get(presets::list_presets::<C, E>).post(presets::create_preset::<C, E>),
)
.route(
"/presets/{id}",
get(presets::get_preset::<C, E>).delete(presets::delete_preset::<C, E>),
)
.route("/presets/{id}/load", post(presets::load_preset::<C, E>))
}

View File

@@ -1,53 +1,101 @@
use crate::AppState;
use api_types::{CreatePresetDto, PresetDto};
use application::ConfigService;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use domain::{ConfigRepository, EventPublisher};
use application::ConfigService;
use crate::AppState;
use api_types::{PresetDto, CreatePresetDto};
type S<C, E> = State<AppState<C, E>>;
pub async fn list_presets<C, E>(State(state): S<C, E>) -> Result<Json<Vec<PresetDto>>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let presets = state.config.list_presets().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let presets = state
.config
.list_presets()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(presets.iter().map(PresetDto::from).collect()))
}
pub async fn get_preset<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<Json<PresetDto>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn get_preset<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<Json<PresetDto>, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let preset = state.config.get_preset(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let preset = state
.config
.get_preset(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match preset {
Some(p) => Ok(Json(PresetDto::from(&p))),
None => Err(StatusCode::NOT_FOUND),
}
}
pub async fn create_preset<C, E>(State(state): S<C, E>, Json(body): Json<CreatePresetDto>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn create_preset<C, E>(
State(state): S<C, E>,
Json(body): Json<CreatePresetDto>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let preset = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let preset = body
.into_domain()
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.save_preset(preset).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.save_preset(preset)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::CREATED)
}
pub async fn delete_preset<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<StatusCode, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn delete_preset<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<StatusCode, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.delete_preset(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
svc.delete_preset(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn load_preset<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn load_preset<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.load_preset(id).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.load_preset(id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::OK)
}

View File

@@ -1,54 +1,105 @@
use crate::AppState;
use api_types::{CreateWidgetDto, WidgetDto};
use application::ConfigService;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use domain::{ConfigRepository, EventPublisher};
use application::ConfigService;
use crate::AppState;
use api_types::{WidgetDto, CreateWidgetDto};
type S<C, E> = State<AppState<C, E>>;
pub async fn list_widgets<C, E>(State(state): S<C, E>) -> Result<Json<Vec<WidgetDto>>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let widgets = state.config.list_widgets().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let widgets = state
.config
.list_widgets()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(widgets.iter().map(WidgetDto::from).collect()))
}
pub async fn get_widget<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<Json<WidgetDto>, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn get_widget<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<Json<WidgetDto>, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let widget = state.config.get_widget(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let widget = state
.config
.get_widget(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match widget {
Some(w) => Ok(Json(WidgetDto::from(&w))),
None => Err(StatusCode::NOT_FOUND),
}
}
pub async fn create_widget<C, E>(State(state): S<C, E>, Json(body): Json<CreateWidgetDto>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn create_widget<C, E>(
State(state): S<C, E>,
Json(body): Json<CreateWidgetDto>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let widget = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let widget = body
.into_domain()
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.create_widget(widget).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.create_widget(widget)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::CREATED)
}
pub async fn update_widget<C, E>(State(state): S<C, E>, Path(_id): Path<u16>, Json(body): Json<CreateWidgetDto>) -> Result<StatusCode, (StatusCode, String)>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn update_widget<C, E>(
State(state): S<C, E>,
Path(_id): Path<u16>,
Json(body): Json<CreateWidgetDto>,
) -> Result<StatusCode, (StatusCode, String)>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let widget = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let widget = body
.into_domain()
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.update_widget(widget).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
svc.update_widget(widget)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
Ok(StatusCode::OK)
}
pub async fn delete_widget<C, E>(State(state): S<C, E>, Path(id): Path<u16>) -> Result<StatusCode, StatusCode>
where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug,
pub async fn delete_widget<C, E>(
State(state): S<C, E>,
Path(id): Path<u16>,
) -> Result<StatusCode, StatusCode>
where
C: ConfigRepository,
C::Error: std::fmt::Debug,
E: EventPublisher,
E::Error: std::fmt::Debug,
{
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
svc.delete_widget(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
svc.delete_widget(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::NO_CONTENT)
}

View File

@@ -1,10 +1,10 @@
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use config_memory::MemoryConfigStore;
use tcp_server::TcpEventBus;
use http_api::{AppState, router};
use std::sync::Arc;
use tcp_server::TcpEventBus;
use tower::ServiceExt;
fn test_app() -> axum::Router {
let config = Arc::new(MemoryConfigStore::new());
@@ -38,13 +38,22 @@ async fn create_and_get_widget() {
"mappings": [{"source_path": "$.temp", "target_key": "temperature"}]
}"#;
let resp = app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap();
let resp = app
.clone()
.oneshot(json_request("POST", "/api/widgets", Some(body)))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap();
let resp = app
.oneshot(json_request("GET", "/api/widgets/1", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["name"], "weather");
assert_eq!(json["display_hint"], "icon_value");
@@ -58,11 +67,22 @@ async fn list_widgets() {
let w1 = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#;
let w2 = r#"{"id":2,"name":"b","display_hint":"key_value","data_source_id":2,"mappings":[]}"#;
app.clone().oneshot(json_request("POST", "/api/widgets", Some(w1))).await.unwrap();
app.clone().oneshot(json_request("POST", "/api/widgets", Some(w2))).await.unwrap();
app.clone()
.oneshot(json_request("POST", "/api/widgets", Some(w1)))
.await
.unwrap();
app.clone()
.oneshot(json_request("POST", "/api/widgets", Some(w2)))
.await
.unwrap();
let resp = app.oneshot(json_request("GET", "/api/widgets", None)).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
let resp = app
.oneshot(json_request("GET", "/api/widgets", None))
.await
.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
assert_eq!(json.len(), 2);
}
@@ -71,13 +91,24 @@ async fn list_widgets() {
async fn delete_widget() {
let app = test_app();
let body = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#;
app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap();
let body =
r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#;
app.clone()
.oneshot(json_request("POST", "/api/widgets", Some(body)))
.await
.unwrap();
let resp = app.clone().oneshot(json_request("DELETE", "/api/widgets/1", None)).await.unwrap();
let resp = app
.clone()
.oneshot(json_request("DELETE", "/api/widgets/1", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap();
let resp = app
.oneshot(json_request("GET", "/api/widgets/1", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
@@ -95,13 +126,22 @@ async fn create_and_get_data_source() {
"headers": []
}"#;
let resp = app.clone().oneshot(json_request("POST", "/api/data-sources", Some(body))).await.unwrap();
let resp = app
.clone()
.oneshot(json_request("POST", "/api/data-sources", Some(body)))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let resp = app.oneshot(json_request("GET", "/api/data-sources/10", None)).await.unwrap();
let resp = app
.oneshot(json_request("GET", "/api/data-sources/10", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["name"], "weather_api");
assert_eq!(json["poll_interval_secs"], 300);
@@ -124,13 +164,22 @@ async fn update_and_get_layout() {
}
}"#;
let resp = app.clone().oneshot(json_request("PUT", "/api/layout", Some(body))).await.unwrap();
let resp = app
.clone()
.oneshot(json_request("PUT", "/api/layout", Some(body)))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app.oneshot(json_request("GET", "/api/layout", None)).await.unwrap();
let resp = app
.oneshot(json_request("GET", "/api/layout", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["root"]["type"], "container");
assert_eq!(json["root"]["direction"], "row");
@@ -141,9 +190,16 @@ async fn update_and_get_layout() {
async fn get_nonexistent_returns_404() {
let app = test_app();
let resp = app.clone().oneshot(json_request("GET", "/api/widgets/99", None)).await.unwrap();
let resp = app
.clone()
.oneshot(json_request("GET", "/api/widgets/99", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let resp = app.oneshot(json_request("GET", "/api/data-sources/99", None)).await.unwrap();
let resp = app
.oneshot(json_request("GET", "/api/data-sources/99", None))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}