diff --git a/smart-house-web/README.md b/smart-house-web/README.md index d6c8380..30ee50f 100644 --- a/smart-house-web/README.md +++ b/smart-house-web/README.md @@ -16,10 +16,10 @@ API backend сервиса предоставляет доступ ко всему базовому функционалу библиотеки умного дома: -- [ ] Добавление/удаление/перечисление комнат в доме и получение информации о конкретной комнате. -- [ ] Добавление/удаление/перечисление устройств в комнате и получение информации о конкретном устройстве. -- [ ] Получение отчёта о доме. -- [ ] Присутствуют функциональные тесты, которые общаются с backend-ом и проверяют его ответы. +- [x] Добавление/удаление/перечисление комнат в доме и получение информации о конкретной комнате. +- [x] Добавление/удаление/перечисление устройств в комнате и получение информации о конкретном устройстве. +- [x] Получение отчёта о доме. +- [x] Присутствуют функциональные тесты, которые общаются с backend-ом и проверяют его ответы. Frontend приложение: diff --git a/smart-house-web/backend/Cargo.toml b/smart-house-web/backend/Cargo.toml index 8eedad5..666690e 100644 --- a/smart-house-web/backend/Cargo.toml +++ b/smart-house-web/backend/Cargo.toml @@ -12,4 +12,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [dev-dependencies] -hyper = "1.9.0" +reqwest = { version = "0.13.3", features = ["json"] } diff --git a/smart-house-web/backend/src/lib.rs b/smart-house-web/backend/src/lib.rs index 8d90375..9bd7010 100644 --- a/smart-house-web/backend/src/lib.rs +++ b/smart-house-web/backend/src/lib.rs @@ -1,3 +1,6 @@ +use std::process::exit; +use tracing::{Level, error, trace}; + /// Ошибка инициализации логгера const CODE_LOGGER_INITIALIZATION_ERROR: i32 = 1; @@ -15,8 +18,6 @@ const CODE_CTRL_C_SIGNAL_INSTALL_ERROR: i32 = 5; /// Инициализация логирования pub fn init_logger() { - use std::process::exit; - use tracing::{Level, trace}; use tracing_subscriber::{ Layer, filter::Targets, fmt::layer, layer::SubscriberExt, registry, util::SubscriberInitExt, }; @@ -29,6 +30,8 @@ pub fn init_logger() { .with_filter( Targets::new() .with_target("axum::serve", Level::INFO) + .with_target("hyper_util::client::legacy", Level::INFO) + .with_target("reqwest::retry", Level::INFO) .with_default(Level::TRACE), ) .boxed(); @@ -40,8 +43,31 @@ pub fn init_logger() { } } +pub fn build_runtime() -> tokio::runtime::Runtime { + use std::sync::atomic::{AtomicUsize, Ordering}; + + match tokio::runtime::Builder::new_multi_thread() + .name("tokio") + .thread_name_fn(|| { + static LAST_ID: AtomicUsize = AtomicUsize::new(0); + let id = LAST_ID.fetch_add(1, Ordering::SeqCst); + format!("tkwr-{id}") + }) + .worker_threads(2) + .thread_stack_size(256 * 1024) + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(e) => { + error!("Failed to create Tokio runtime: {:?}", e); + exit(crate::CODE_TOKIO_RUNTIME_CREATION_ERROR); + } + } +} + mod server; -pub use server::run_server; +pub use server::server_main; mod house; pub use house::{Device, House, PowerSocket, Room, Thermometer}; diff --git a/smart-house-web/backend/src/main.rs b/smart-house-web/backend/src/main.rs index da4e57b..ce2b9a4 100644 --- a/smart-house-web/backend/src/main.rs +++ b/smart-house-web/backend/src/main.rs @@ -1,6 +1,7 @@ -use backend::{init_logger, run_server}; +use backend::{build_runtime, init_logger, server_main}; fn main() { init_logger(); - run_server(); + let runtime = build_runtime(); + runtime.block_on(server_main()); } diff --git a/smart-house-web/backend/src/server.rs b/smart-house-web/backend/src/server.rs index 213071f..cb31124 100644 --- a/smart-house-web/backend/src/server.rs +++ b/smart-house-web/backend/src/server.rs @@ -1,44 +1,15 @@ use axum::routing::{delete, get, post, put}; -use std::{ - process::exit, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, -}; +use std::{process::exit, sync::Arc}; use tokio::sync::RwLock; use tracing::{error, info}; use crate::House; -/// Запуск сервера -pub fn run_server() { - let runtime = match tokio::runtime::Builder::new_multi_thread() - .name("tokio") - .thread_name_fn(|| { - static LAST_ID: AtomicUsize = AtomicUsize::new(0); - let id = LAST_ID.fetch_add(1, Ordering::SeqCst); - format!("tkwr-{id}") - }) - .worker_threads(2) - .thread_stack_size(256 * 1024) - .enable_all() - .build() - { - Ok(runtime) => runtime, - Err(e) => { - error!("Failed to create Tokio runtime: {:?}", e); - exit(crate::CODE_TOKIO_RUNTIME_CREATION_ERROR); - } - }; - runtime.block_on(server_main()); -} - /// Тип состояния type ServerState = Arc>; /// Основная функция сервера -async fn server_main() { +pub async fn server_main() { let state: ServerState = Arc::new(RwLock::new(House::default())); let app = axum::Router::new() diff --git a/smart-house-web/backend/tests/api_smoke_test.rs b/smart-house-web/backend/tests/api_smoke_test.rs new file mode 100644 index 0000000..c71801b --- /dev/null +++ b/smart-house-web/backend/tests/api_smoke_test.rs @@ -0,0 +1,238 @@ +use std::collections::HashMap; + +use backend::{Device, Room, init_logger, server_main}; +use reqwest::{Client, Response, StatusCode}; +use serde_json::json; +use tokio::spawn; +use tracing::info; + +type RqResult = Result; + +#[tokio::test(flavor = "current_thread")] +async fn smoke() -> RqResult<()> { + init_logger(); + spawn(server_main()); + + let client = Client::new(); + + verify_rooms_state0(&client).await?; + init_rooms(&client).await?; + verify_rooms_state1(&client).await?; + delete_room0(&client).await?; + verify_rooms_state2(&client).await?; + put_room(&client).await?; + verify_rooms_state3(&client).await?; + verify_room1_state0(&client).await?; + verify_room1_devices_state0(&client).await?; + verify_room1_device_psock_state0(&client).await?; + delete_psock(&client).await?; + verify_room1_device_psock_state1(&client).await?; + put_device(&client).await?; + verify_new_device_in_place(&client).await?; + print_house_report(&client).await?; + + Ok(()) +} + +async fn print_resp(resp: Response) -> RqResult<()> { + info!( + "\n<<< StatusCode: {}\n<<< BODY: {}", + resp.status(), + resp.text().await? + ); + Ok(()) +} + +async fn verify_rooms_state0(client: &Client) -> RqResult<()> { + let resp = client.get("http://localhost:8080/rooms").send().await?; + assert_eq!(resp.status(), StatusCode::OK); + + let rooms = resp.json::>().await?; + assert!(rooms.is_empty()); + + Ok(()) +} + +async fn init_rooms(client: &Client) -> RqResult<()> { + let resp = client + .post("http://localhost:8080/rooms") + .json(&json!({ + "ROOM0": { "devices": {} }, + "ROOM1": { "devices": { + "therm": { "type": "Thermometer", "temperature": 22 }, + "psock": { "type": "PowerSocket", "power_rate": 11, "on": false } + } } + })) + .send() + .await?; + assert_eq!(resp.status(), StatusCode::CREATED); + + Ok(()) +} + +async fn verify_rooms_state1(client: &Client) -> RqResult<()> { + let resp = client.get("http://localhost:8080/rooms").send().await?; + assert_eq!(resp.status(), StatusCode::OK); + + let rooms = resp.json::>().await?; + assert_eq!(rooms.len(), 2); + assert_eq!(rooms.get("ROOM1").unwrap().get_devices().len(), 2); + + Ok(()) +} + +async fn delete_room0(client: &Client) -> RqResult<()> { + let resp = client + .delete("http://localhost:8080/room/ROOM0") + .send() + .await?; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + Ok(()) +} + +async fn verify_rooms_state2(client: &Client) -> RqResult<()> { + let resp = client.get("http://localhost:8080/rooms").send().await?; + assert_eq!(resp.status(), StatusCode::OK); + + let rooms = resp.json::>().await?; + assert_eq!(rooms.len(), 1); + assert_eq!(rooms.get("ROOM1").unwrap().get_devices().len(), 2); + + Ok(()) +} + +async fn put_room(client: &Client) -> RqResult<()> { + let resp = client + .put("http://localhost:8080/room/ROOM") + .json(&json!({ "devices": { + "therm": { "type": "Thermometer", "temperature": 20 }, + "psock": { "type": "PowerSocket", "power_rate": 10, "on": true } + } })) + .send() + .await?; + assert_eq!(resp.status(), StatusCode::CREATED); + + Ok(()) +} + +async fn verify_rooms_state3(client: &Client) -> RqResult<()> { + let resp = client.get("http://localhost:8080/rooms").send().await?; + assert_eq!(resp.status(), StatusCode::OK); + + let rooms = resp.json::>().await?; + assert_eq!(rooms.len(), 2); + assert_eq!(rooms.get("ROOM").unwrap().get_devices().len(), 2); + + Ok(()) +} + +async fn verify_room1_state0(client: &Client) -> RqResult<()> { + let resp = client + .get("http://localhost:8080/room/ROOM1") + .send() + .await?; + assert_eq!(resp.status(), StatusCode::OK); + + let room = resp.json::().await?; + assert_eq!(room.get_devices().len(), 2); + + Ok(()) +} + +async fn verify_room1_devices_state0(client: &Client) -> RqResult<()> { + let resp = client + .get("http://localhost:8080/room/ROOM1/devices") + .send() + .await?; + assert_eq!(resp.status(), StatusCode::OK); + + let device_map = resp.json::>().await?; + assert_eq!(device_map.len(), 2); + + Ok(()) +} + +async fn verify_room1_device_psock_state0(client: &Client) -> RqResult<()> { + let resp = client + .get("http://localhost:8080/room/ROOM1/device/psock") + .send() + .await?; + assert_eq!(resp.status(), StatusCode::OK); + + let device = resp.json::().await?; + + let Device::PowerSocket(power_socket) = device else { + panic!("PowerSocket expected"); + }; + assert_eq!(power_socket.get_power(), 0.0); + assert_eq!(power_socket.is_on(), false); + + Ok(()) +} + +async fn delete_psock(client: &Client) -> RqResult<()> { + let resp = client + .delete("http://localhost:8080/room/ROOM1/device/psock") + .send() + .await?; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + Ok(()) +} + +async fn verify_room1_device_psock_state1(client: &Client) -> RqResult<()> { + let resp = client + .get("http://localhost:8080/room/ROOM1/device/psock") + .send() + .await?; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + Ok(()) +} + +async fn put_device(client: &Client) -> RqResult<()> { + let resp = client + .put("http://localhost:8080/room/ROOM1/device/TEST") + .json(&json!({ + "type": "PowerSocket", + "power_rate": 5, + "on": true + })) + .send() + .await?; + assert_eq!(resp.status(), StatusCode::CREATED); + + Ok(()) +} + +async fn verify_new_device_in_place(client: &Client) -> RqResult<()> { + let resp = client.get("http://localhost:8080/rooms").send().await?; + assert_eq!(resp.status(), StatusCode::OK); + + let rooms = resp.json::>().await?; + assert_eq!(rooms.len(), 2); + assert_eq!(rooms.get("ROOM1").unwrap().get_devices().len(), 2); + let device = rooms + .get("ROOM1") + .unwrap() + .get_devices() + .get("TEST") + .unwrap(); + let Device::PowerSocket(power_socket) = device else { + panic!("TEST device must be a PowerSocket") + }; + assert!(power_socket.is_on()); + assert_eq!(power_socket.get_power(), 5.0); + + Ok(()) +} + +async fn print_house_report(client: &Client) -> RqResult<()> { + let resp = client.get("http://localhost:8080/rooms").send().await?; + assert_eq!(resp.status(), StatusCode::OK); + + print_resp(resp).await?; + + Ok(()) +} diff --git a/smart-house-web/backend/tests/api_tests.rs b/smart-house-web/backend/tests/api_tests.rs deleted file mode 100644 index 97bcadc..0000000 --- a/smart-house-web/backend/tests/api_tests.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[tokio::test(flavor = "current_thread")] -async fn smoke() { - println!("Hello test!") -}