use std::{collections::HashMap, env, fs}; use axum::{ http::{header, StatusCode}, routing, Router, }; use axum_test::{TestResponse, TestServer}; use laboratory::{describe, expect, SpecContext, Suite}; use url::Url; use sylvia_iot_auth::{ libs::config::{self, Config}, models::{self, ConnOptions, SqliteOptions}, routes, }; use sylvia_iot_corelib::constants::DbEngine; use crate::TestState; mod libs; pub mod oauth2; pub mod v1; use libs::new_state; pub const STATE: &'static str = "routes"; pub fn suite() -> Suite { describe("routes", |context| { context.it("new_state", fn_new_state); context.it("new_service", fn_new_service); context.it("new_service with API scopes", fn_api_scopes); context.it("GET /version", api_get_version); context.before_all(|state| { state.insert(STATE, new_state(None)); }); context.after_all(|_state| { remove_sqlite(config::DEF_SQLITE_PATH); let mut path = std::env::temp_dir(); path.push(config::DEF_SQLITE_PATH); remove_sqlite(path.to_str().unwrap()); let mut path = std::env::temp_dir(); path.push(crate::TEST_SQLITE_PATH); remove_sqlite(path.to_str().unwrap()); }); }) } fn read_location(resp: &TestResponse) -> Result { let location = match resp.headers().get(header::LOCATION) { None => return Err("no location header".to_string()), Some(location) => match location.to_str() { Err(e) => return Err(format!("location to_str() error: {}", e)), Ok(location) => location, }, }; match Url::parse(location) { Err(e) => match e { url::ParseError::RelativeUrlWithoutBase => { let url_with_base = format!("http://localhost{}", location); match Url::parse(url_with_base.as_str()) { Err(e) => return Err(format!("parse url with base error: {}", e)), Ok(url) => return Ok(url), } } _ => return Err(format!("parse url error: {}", e)), }, Ok(url) => return Ok(url), } } fn remove_sqlite(path: &str) { if let Err(e) = std::fs::remove_file(path) { println!("remove file {} error: {}", path, e); } let file = format!("{}-shm", path); if let Err(e) = std::fs::remove_file(file.as_str()) { println!("remove file {} error: {}", file.as_str(), e); } let file = format!("{}-wal", path); if let Err(e) = std::fs::remove_file(file.as_str()) { println!("remove file {} error: {}", file.as_str(), e); } } fn fn_new_state(context: &mut SpecContext) -> Result<(), String> { let state = context.state.borrow(); let state = state.get(STATE).unwrap(); let runtime = state.runtime.as_ref().unwrap(); let conf = Config { ..Default::default() }; let state = match runtime.block_on(async { routes::new_state("scope", &conf).await }) { Err(e) => return Err(format!("default config error: {}", e)), Ok(state) => match runtime.block_on(async { state.model.close().await }) { Err(e) => return Err(format!("disconnect default model error: {}", e)), Ok(_) => state, }, }; expect(state.scope_path).to_equal("scope")?; let conf = Config { db: Some(config::Db { engine: Some(DbEngine::MONGODB.to_string()), ..Default::default() }), ..Default::default() }; let state = match runtime.block_on(async { routes::new_state("scope", &conf).await }) { Err(e) => return Err(format!("mongodb config error: {}", e)), Ok(state) => match runtime.block_on(async { state.model.close().await }) { Err(e) => return Err(format!("disconnect mongodb model error: {}", e)), Ok(_) => state, }, }; expect(state.scope_path).to_equal("scope")?; let conf = Config { db: Some(config::Db { engine: Some(DbEngine::SQLITE.to_string()), ..Default::default() }), ..Default::default() }; let state = match runtime.block_on(async { routes::new_state("scope", &conf).await }) { Err(e) => return Err(format!("sqlite config error: {}", e)), Ok(state) => match runtime.block_on(async { state.model.close().await }) { Err(e) => return Err(format!("disconnect sqlite model error: {}", e)), Ok(_) => state, }, }; expect(state.scope_path).to_equal("scope")?; let conf = Config { db: Some(config::Db { engine: Some("test".to_string()), ..Default::default() }), ..Default::default() }; let state = match runtime.block_on(async { routes::new_state("scope", &conf).await }) { Err(e) => return Err(format!("test config error: {}", e)), Ok(state) => match runtime.block_on(async { state.model.close().await }) { Err(e) => return Err(format!("disconnect test model error: {}", e)), Ok(_) => state, }, }; expect(state.scope_path).to_equal("scope") } fn fn_new_service(context: &mut SpecContext) -> Result<(), String> { let state = context.state.borrow(); let state = state.get(STATE).unwrap(); let runtime = state.runtime.as_ref().unwrap(); let model = match runtime.block_on(async { let mut path = std::env::temp_dir(); path.push(crate::TEST_SQLITE_PATH); let opts = ConnOptions::Sqlite(SqliteOptions { path: path.to_str().unwrap().to_string(), }); models::new(&opts).await }) { Err(e) => return Err(format!("new model error: {}", e)), Ok(model) => model, }; let _ = routes::new_service(&routes::State { scope_path: "/test", api_scopes: HashMap::new(), templates: HashMap::new(), model: model.clone(), }); if let Err(e) = runtime.block_on(async { model.close().await }) { return Err(format!("close model error: {}", e)); } Ok(()) } fn fn_api_scopes(context: &mut SpecContext) -> Result<(), String> { let state = context.state.borrow(); let state = state.get(STATE).unwrap(); let runtime = state.runtime.as_ref().unwrap(); let model = match runtime.block_on(async { let mut path = std::env::temp_dir(); path.push(crate::TEST_SQLITE_PATH); let opts = ConnOptions::Sqlite(SqliteOptions { path: path.to_str().unwrap().to_string(), }); models::new(&opts).await }) { Err(e) => return Err(format!("new model error: {}", e)), Ok(model) => model, }; let mut api_scopes: HashMap> = HashMap::new(); api_scopes.insert("auth.tokeninfo.get".to_string(), vec![]); api_scopes.insert("auth.logout.post".to_string(), vec![]); api_scopes.insert("user.get".to_string(), vec![]); api_scopes.insert("user.patch".to_string(), vec![]); api_scopes.insert("user.post.admin".to_string(), vec![]); api_scopes.insert("user.get.admin".to_string(), vec![]); api_scopes.insert("user.patch.admin".to_string(), vec![]); api_scopes.insert("user.delete.admin".to_string(), vec![]); api_scopes.insert("client.post".to_string(), vec![]); api_scopes.insert("client.get".to_string(), vec![]); api_scopes.insert("client.patch".to_string(), vec![]); api_scopes.insert("client.delete".to_string(), vec![]); api_scopes.insert("client.delete.user".to_string(), vec![]); let template_path = env::temp_dir().join("path"); if let Err(_) = fs::write(template_path.as_path(), "") { return Err("cannot write template".to_string()); } let mut templates: HashMap = HashMap::new(); templates.insert( "login".to_string(), template_path.to_str().unwrap().to_string(), ); templates.insert( "grant".to_string(), template_path.to_str().unwrap().to_string(), ); let _ = routes::new_service(&routes::State { scope_path: "/test", api_scopes, templates, model: model.clone(), }); if let Err(e) = runtime.block_on(async { model.close().await }) { return Err(format!("close model error: {}", e)); } Ok(()) } fn api_get_version(context: &mut SpecContext) -> Result<(), String> { let state = context.state.borrow(); let state = state.get(STATE).unwrap(); let runtime = state.runtime.as_ref().unwrap(); const SERV_NAME: &'static str = env!("CARGO_PKG_NAME"); const SERV_VER: &'static str = env!("CARGO_PKG_VERSION"); let app = Router::new().route("/version", routing::get(routes::get_version)); let server = match TestServer::new(app) { Err(e) => return Err(format!("new server error: {}", e)), Ok(server) => server, }; // Default. let req = server.get("/version"); let resp = runtime.block_on(async { req.await }); expect(resp.status_code()).to_equal(StatusCode::OK)?; let body = resp.text(); let expect_body = format!( "{{\"data\":{{\"name\":\"{}\",\"version\":\"{}\"}}}}", SERV_NAME, SERV_VER ); expect(body.as_ref()).to_equal(expect_body.as_str().as_bytes())?; // Invalid query. let req = server.get("/version").add_query_param("q", "test"); let resp = runtime.block_on(async { req.await }); expect(resp.status_code()).to_equal(StatusCode::OK)?; let body = resp.text(); let expect_body = format!( "{{\"data\":{{\"name\":\"{}\",\"version\":\"{}\"}}}}", SERV_NAME, SERV_VER ); expect(body.as_ref()).to_equal(expect_body.as_str().as_bytes())?; // Query service name. let req = server.get("/version").add_query_param("q", "name"); let resp = runtime.block_on(async { req.await }); expect(resp.status_code()).to_equal(StatusCode::OK)?; let body = resp.text(); expect(body.as_ref()).to_equal(SERV_NAME.as_bytes())?; // Query service version. let req = server.get("/version").add_query_param("q", "version"); let resp = runtime.block_on(async { req.await }); expect(resp.status_code()).to_equal(StatusCode::OK)?; let body = resp.text(); expect(body.as_ref()).to_equal(SERV_VER.as_bytes()) }