From a341534c56dfb6e87c4b7fd34d036b91b7890570 Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Sun, 17 Sep 2023 17:38:29 +0200 Subject: [PATCH] Use reqwest in web API tests --- Cargo.lock | 147 ++++++++ ofdb-webserver/Cargo.toml | 2 + .../src/web/api/events/tests/create.rs | 18 +- .../src/web/api/events/tests/read.rs | 20 +- ofdb-webserver/src/web/api/tests.rs | 337 +++++++++++------- ofdb-webserver/src/web/frontend/login.rs | 2 +- ofdb-webserver/src/web/frontend/tests.rs | 2 +- ofdb-webserver/src/web/tests.rs | 76 +++- 8 files changed, 430 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31d6cbc3..5c4a4c45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -977,6 +987,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1368,6 +1393,19 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1842,6 +1880,24 @@ dependencies = [ "byteorder", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -2091,6 +2147,7 @@ dependencies = [ "parking_lot", "r2d2", "rand", + "reqwest", "rocket", "rocket_cors", "rust-embed", @@ -2098,6 +2155,7 @@ dependencies = [ "serde_json", "thiserror", "time", + "tokio", "uuid", "walkdir", ] @@ -2148,6 +2206,50 @@ dependencies = [ "toml", ] +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -2553,10 +2655,12 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2566,6 +2670,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", + "tokio-native-tls", "tokio-rustls", "tower-service", "url", @@ -2830,6 +2935,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -2861,6 +2975,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.188" @@ -3329,6 +3466,16 @@ dependencies = [ "syn 2.0.33", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" diff --git a/ofdb-webserver/Cargo.toml b/ofdb-webserver/Cargo.toml index 95a54863..6f1c0bd2 100644 --- a/ofdb-webserver/Cargo.toml +++ b/ofdb-webserver/Cargo.toml @@ -39,6 +39,8 @@ uuid = { version = "1.4.1", features = ["v4"] } [dev-dependencies] ofdb-entities = { version = "0.12.5", features = ["url", "builders"] } +reqwest = { version = "0.11.20", features = ["rustls", "json"] } +tokio = { version = "1.32.0", features = ["rt-multi-thread"] } [build-dependencies] walkdir = "2.4.0" diff --git a/ofdb-webserver/src/web/api/events/tests/create.rs b/ofdb-webserver/src/web/api/events/tests/create.rs index 2cf6b204..eec6ab96 100644 --- a/ofdb-webserver/src/web/api/events/tests/create.rs +++ b/ofdb-webserver/src/web/api/events/tests/create.rs @@ -18,7 +18,7 @@ fn without_creator_email() { // But in the future we might allow anonymous event creation: // // assert_eq!(response.status(), HttpStatus::Ok); - // test_json(&response); + // assert_rocket_response_has_json_content_type(&response); // let body_str = response.into_string().unwrap(); // let eid = db.get().unwrap().all_events_chronologically().unwrap()[0].id. // clone(); assert_eq!(body_str, format!("\"{}\"", eid)); @@ -39,7 +39,7 @@ fn without_api_token_but_with_creator_email() { // But in the future we might allow anonymous event creation: // // assert_eq!(response.status(), HttpStatus::Ok); - // test_json(&response); + // assert_rocket_response_has_json_content_type(&response); // let body_str = response.into_string().unwrap(); // let ev = db.get().unwrap().all_events_chronologically().unwrap()[0]. // clone(); let eid = ev.id.clone(); @@ -50,7 +50,7 @@ fn without_api_token_but_with_creator_email() { // .header(ContentType::JSON); // let response = req.dispatch(); // assert_eq!(response.status(), HttpStatus::Ok); - // test_json(&response); + // assert_rocket_response_has_json_content_type(&response); // let body_str = response.into_string().unwrap(); // assert_eq!( // body_str, @@ -83,7 +83,7 @@ mod with_api_token { .body(r#"{"title":"x","start":4132508400,"created_by":"foo@bar.com"}"#) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let ev = db.shared().unwrap().all_events_chronologically().unwrap()[0].clone(); let eid = ev.id.clone(); @@ -110,7 +110,7 @@ mod with_api_token { .body(r#"{"title":"x","start":4132508400,"created_by":"foo@bar.com"}"#) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let ev = db.shared().unwrap().all_events_chronologically().unwrap()[0].clone(); let eid = ev.id.clone(); @@ -163,7 +163,7 @@ mod with_api_token { .body(r#"{"title":"x","start":4132508400,"created_by":"foo@bar.com","email":"","homepage":"","description":"","registration":""}"#) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let ev = db.shared().unwrap().all_events_chronologically().unwrap()[0].clone(); assert!(ev.contact.is_none()); assert!(ev.homepage.is_none()); @@ -189,7 +189,7 @@ mod with_api_token { .body(r#"{"title":"title","description":"","start":-4132508400,"end":-4132508399,"created_by":"foo@bar.com"}"#) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let ev = db.shared().unwrap().all_events_chronologically().unwrap()[0].clone(); assert_eq!(-4132508400, ev.start.as_secs()); assert_eq!(Some(-4132508399), ev.end.map(Timestamp::as_secs)); @@ -214,7 +214,7 @@ mod with_api_token { .body(r#"{"title":"x","start":4132508400,"created_by":"foo@bar.com","registration":"telephone","telephone":"12345"}"#) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let ev = db.shared().unwrap().all_events_chronologically().unwrap()[0].clone(); assert_eq!(ev.registration.unwrap(), RegistrationType::Phone); } @@ -282,7 +282,7 @@ mod with_api_token { .body(r#"{"title":"x","start":4132508400,"created_by":"foo@bar.com","tags":["", " "," tag","tag ","two tags", "tag"]}"#) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let ev = db.shared().unwrap().all_events_chronologically().unwrap()[0].clone(); let mut actual_tags = ev.tags; actual_tags.sort_unstable(); diff --git a/ofdb-webserver/src/web/api/events/tests/read.rs b/ofdb-webserver/src/web/api/events/tests/read.rs index 5cf9fbc6..d48757de 100644 --- a/ofdb-webserver/src/web/api/events/tests/read.rs +++ b/ofdb-webserver/src/web/api/events/tests/read.rs @@ -19,7 +19,7 @@ fn by_id() { .header(ContentType::JSON); let response = req.dispatch(); assert_eq!(response.status(), HttpStatus::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert_eq!( body_str, @@ -55,7 +55,7 @@ fn all() { let req = client.get("/events").header(ContentType::JSON); let response = req.dispatch(); assert_eq!(response.status(), HttpStatus::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains("\"id\":\"a\"")); } @@ -77,7 +77,7 @@ fn sorted_by_start() { } let res = client.get("/events").header(ContentType::JSON).dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let objects: Vec<_> = body_str.split("},{").collect(); assert!(objects[0].contains(&format!("\"start\":{}", now))); @@ -105,7 +105,7 @@ fn filtered_by_tags() { let req = client.get("/events?tag=a").header(ContentType::JSON); let response = req.dispatch(); assert_eq!(response.status(), HttpStatus::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains("\"tags\":[\"a\"]")); assert!(!body_str.contains("\"tags\":[\"b\"]")); @@ -230,7 +230,7 @@ fn filtered_by_start_min() { .header(ContentType::JSON) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let objects: Vec<_> = body_str.split("},{").collect(); assert_eq!(objects.len(), 2); @@ -261,7 +261,7 @@ fn filtered_by_end_min() { .header(ContentType::JSON) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let objects: Vec<_> = body_str.split("},{").collect(); assert_eq!(objects.len(), 2); @@ -289,7 +289,7 @@ fn filtered_by_start_max() { .header(ContentType::JSON) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let objects: Vec<_> = body_str.split("},{").collect(); assert_eq!(objects.len(), 4); @@ -322,7 +322,7 @@ fn filtered_by_end_max() { .header(ContentType::JSON) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let objects: Vec<_> = body_str.split("},{").collect(); assert_eq!(objects.len(), 4); @@ -352,7 +352,7 @@ fn filtered_by_bounding_box() { .header(ContentType::JSON) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); assert!(body_str.contains("\"title\":\"-8-0\"")); assert!(body_str.contains("\"title\":\"7-7.9\"")); @@ -364,7 +364,7 @@ fn filtered_by_bounding_box() { .header(ContentType::JSON) .dispatch(); assert_eq!(res.status(), HttpStatus::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); assert!(!body_str.contains("\"title\":\"-8-0\"")); assert!(!body_str.contains("\"title\":\"7-7.9\"")); diff --git a/ofdb-webserver/src/web/api/tests.rs b/ofdb-webserver/src/web/api/tests.rs index 3f095352..1e45ae7e 100644 --- a/ofdb-webserver/src/web/api/tests.rs +++ b/ofdb-webserver/src/web/api/tests.rs @@ -1,5 +1,3 @@ -use std::fmt::Write as _; - use ofdb_core::rating::Rated; use super::*; @@ -11,6 +9,14 @@ pub mod prelude { use ofdb_core::gateways::notify::NotificationGateway; use std::collections::HashSet; + pub use reqwest::StatusCode; + pub use serde_json::json; + pub use std::net::SocketAddr; + + pub fn endpoint(address: SocketAddr, endpoint: &str) -> String { + format!("http://{address}/api{endpoint}") + } + pub use crate::{ core::db::*, web::{ @@ -29,7 +35,8 @@ pub mod prelude { } pub fn setup_with_cfg(cfg: Cfg) -> (Client, sqlite::Connections) { - let (client, conn, _) = web::tests::setup_with_cfg(vec![("/", api::routes())], cfg); + let (client, conn, _) = + web::tests::rocket_test_setup_with_cfg(vec![("/", api::routes())], cfg); (client, conn) } @@ -39,17 +46,25 @@ pub mod prelude { tantivy::SearchEngine, impl NotificationGateway, ) { - let (client, connections, search_engine) = web::tests::setup(vec![("/", api::routes())]); + let (client, connections, search_engine) = + web::tests::rocket_test_setup(vec![("/", api::routes())]); (client, connections, search_engine, DummyNotifyGW {}) } - pub fn test_json(r: &Response) { + pub fn assert_rocket_response_has_json_content_type(r: &Response) { assert_eq!( r.headers().get("Content-Type").collect::>()[0], "application/json" ); } + pub fn assert_reqwest_response_has_json_content_type(response: &reqwest::Response) { + assert_eq!( + response.headers().get("Content-Type").unwrap(), + "application/json" + ); + } + pub use super::cookie_from_response; pub fn default_accepted_licenses() -> std::collections::HashSet { @@ -69,16 +84,25 @@ pub mod prelude { use self::prelude::*; -#[test] -fn create_a_new_place() { - let (client, db) = setup(); - let req = client.post("/entries") - .header(ContentType::JSON) - .body(r#"{"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":[]}"#); - let response = req.dispatch(); - assert_eq!(response.status(), Status::Ok); - test_json(&response); - let body_str = response.into_string().unwrap(); +#[tokio::test] +async fn create_a_new_place() { + let (addr, db, _) = run_server(); + let client = reqwest::Client::new(); + let req = client.post(endpoint(addr, "/entries")).json(&json!( + { + "title":"foo", + "description":"blablabla", + "lat":0.0, + "lng":0.0, + "categories":["x"], + "license":"CC0-1.0", + "tags":[] + } + )); + let response = req.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_reqwest_response_has_json_content_type(&response); + let body_str = response.text().await.unwrap(); let eid = db.exclusive().unwrap().all_places().unwrap()[0] .0 .id @@ -107,16 +131,16 @@ fn create_place_with_reserved_tag() { assert_eq!(res.status(), Status::Forbidden); } -#[test] -fn create_place_with_tag_duplicates() { - let (client, db) = setup(); - let req = client.post("/entries") - .header(ContentType::JSON) - .body(r#"{"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":["foo","foo"]}"#); - let response = req.dispatch(); - assert_eq!(response.status(), Status::Ok); - test_json(&response); - let body_str = response.into_string().unwrap(); +#[tokio::test] +async fn create_place_with_tag_duplicates() { + let (addr, db, _) = run_server(); + let client = reqwest::Client::new(); + let req = client.post(endpoint(addr,"/entries")) + .json(&json!({"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":["foo","foo"]})); + let response = req.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_reqwest_response_has_json_content_type(&response); + let body_str = response.text().await.unwrap(); let eid = db.exclusive().unwrap().all_places().unwrap()[0] .0 .id @@ -124,17 +148,19 @@ fn create_place_with_tag_duplicates() { assert_eq!(body_str, format!("\"{}\"", eid)); } -#[test] -fn create_place_with_sharp_tag_and_custom_link() { - let (client, db) = setup(); - let json = r##"{"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":["foo","#bar","#foo&bar","foo#bar"],"links":[{"url":"example.com","title":"Auto-completed URL"}]}"##; +#[tokio::test] +async fn create_place_with_sharp_tag_and_custom_link() { + let (addr, db, _) = run_server(); + let client = reqwest::Client::new(); + let json = json!({"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":["foo","#bar","#foo&bar","foo#bar"],"links":[{"url":"example.com","title":"Auto-completed URL"}]}); let response = client - .post("/entries") - .header(ContentType::JSON) - .body(json) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - test_json(&response); + .post(endpoint(addr, "/entries")) + .json(&json) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_reqwest_response_has_json_content_type(&response); let (place, _) = db .shared() .unwrap() @@ -155,28 +181,31 @@ fn create_place_with_sharp_tag_and_custom_link() { ); } -#[test] -fn update_place_with_tag_duplicates() { - let (client, db) = setup(); - let req = client.post("/entries") - .header(ContentType::JSON) - .body(r#"{"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"ODbL-1.0","tags":["foo","foo"]}"#); - let _res = req.dispatch(); +#[tokio::test] +async fn update_place_with_tag_duplicates() { + let (addr, db, _) = run_server(); + let client = reqwest::Client::new(); + let req = client.post(endpoint(addr,"/entries")) + .json(&json!({"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"ODbL-1.0","tags":["foo","foo"]})); + let _res = req.send().await.unwrap(); let (place, _) = db.exclusive().unwrap().all_places().unwrap()[0].clone(); - let mut json = String::new(); - write!( - &mut json, - "{{\"version\":{},\"id\":\"{}\"", - u64::from(place.revision.next()), - place.id - ) - .unwrap(); - json.push_str(r#","title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":["bar","bar"]}"#); + let json = json!({ + "version": u64::from(place.revision.next()), + "id": place.id.to_string(), + "title":"foo", + "description":"blablabla", + "lat":0.0, + "lng":0.0, + "categories":["x"], + "license":"CC0-1.0", + "tags":["bar","bar"] + }); + let url = format!("/entries/{}", place.id); - let req = client.put(url).header(ContentType::JSON).body(json); - let response = req.dispatch(); - assert_eq!(response.status(), Status::Ok); - test_json(&response); + let req = client.put(endpoint(addr, &url)).json(&json); + let response = req.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_reqwest_response_has_json_content_type(&response); let (e, _) = db.exclusive().unwrap().all_places().unwrap()[0].clone(); assert_eq!(e.tags, vec!["bar"]); } @@ -212,7 +241,7 @@ fn get_one_entry() { let req = client.get("/entries/get_one_entry_test"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert_eq!(body_str.as_str().chars().next().unwrap(), '['); let entries: Vec = serde_json::from_str(&body_str).unwrap(); @@ -253,7 +282,7 @@ fn get_multiple_places() { let req = client.get("/entries/get_multiple_entry_test_one,get_multiple_entry_test_two"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert_eq!(body_str.as_str().chars().next().unwrap(), '['); let entries: Vec = serde_json::from_str(&body_str).unwrap(); @@ -333,7 +362,7 @@ fn search_with_categories_and_bbox() { )); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -345,7 +374,7 @@ fn search_with_categories_and_bbox() { )); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -430,7 +459,7 @@ fn search_with_text() { let req = client.get("/search?bbox=-10,-10,10,10&text=Foo&limit=2"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -440,7 +469,7 @@ fn search_with_text() { let req = client.get("/search?bbox=1.8,0.5,3.0,3.0&text=Foo"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -450,7 +479,7 @@ fn search_with_text() { let req = client.get("/search?bbox=-10,-10,10,10&text=blub%20foo"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -460,7 +489,7 @@ fn search_with_text() { let req = client.get("/search?bbox=0.9,0.5,2.5,2.0&text=blub%20foo"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -473,7 +502,7 @@ fn search_with_text() { let req = client.get("/search?bbox=-10,-10,10,10&text=blub,Foo"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -511,7 +540,7 @@ fn search_partial_text() { let req = client.get("/search?bbox=-10,-10,10,10&text=bar-baz"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -521,7 +550,7 @@ fn search_partial_text() { let req = client.get("/search?bbox=-10,-10,10,10&text=blub-"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -558,7 +587,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=+Foo"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -567,7 +596,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=+Foo%20-BAZ"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -576,7 +605,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=+Foo%20+BAZ&limit=1"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -585,7 +614,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=+Foo%20+BAZ%20-bar"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -594,7 +623,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=+bAz%20+BAr&limit=1"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -603,7 +632,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=-foo%20+bAz%20+BAr"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -612,7 +641,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=-foo%20+bar"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(!body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -621,7 +650,7 @@ fn search_with_text_terms_inclusive_exclusive() { let req = client.get("/search?bbox=-10,-10,10,10&text=+foo+bar"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -670,7 +699,7 @@ fn search_with_city() { let req = client.get("/search?bbox=-10,-10,10,10&text=stuttgart&limit=2"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!("\"{}\"", place_ids[0]))); assert!(!body_str.contains(&format!("\"{}\"", place_ids[1]))); @@ -738,7 +767,7 @@ fn search_with_tags() { let req = client.get("/search?bbox=-10,-10,10,10&tags=bla-blubb"); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert!(body_str.contains(&format!( "\"visible\":[{{\"id\":\"{}\",\"status\":\"created\",\"lat\":0.0,\"lng\":0.0,\"title\":\"\",\"description\":\"\",\"categories\":[\"{}\"],\"tags\":[\"bla-blubb\",\"foo-bar\"],\"ratings\":{{\"total\":0.0,\"diversity\":0.0,\"fairness\":0.0,\"humanity\":0.0,\"renewable\":0.0,\"solidarity\":0.0,\"transparency\":0.0}}}}]", @@ -1236,7 +1265,7 @@ fn create_new_user() { .unwrap(); assert_eq!(u.email.as_str(), "foo@bar.com"); assert!(u.password.verify("foo bar")); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); } #[test] @@ -1264,7 +1293,7 @@ fn create_rating() { .value, RatingValue::from(1) ); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); } #[test] @@ -1300,7 +1329,7 @@ fn get_one_rating() { let req = client.get(format!("/ratings/{}", rid)); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert_eq!(body_str.as_str().chars().next().unwrap(), '['); let ratings: Vec = serde_json::from_str(&body_str).unwrap(); @@ -1362,7 +1391,7 @@ fn ratings_with_and_without_source() { let req = client.get(format!("/ratings/{}", rid)); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); assert_eq!(body_str.as_str().chars().next().unwrap(), '['); let ratings: Vec = serde_json::from_str(&body_str).unwrap(); @@ -1492,7 +1521,7 @@ fn login_logout_succeeds_jwt() { .body(r#"{"email": "foo@bar", "password": "secret"}"#) .dispatch(); assert_eq!(res.status(), Status::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let jwt_token: ofdb_boundary::JwtToken = serde_json::from_str(&body_str).unwrap(); @@ -1761,43 +1790,66 @@ fn recently_changed_entries() { assert!(!body_since_until_str.contains("\"id\":\"new\"")); } -#[test] -fn count_most_popular_tags_on_empty_db_to_verify_sql() { +#[tokio::test] +async fn count_most_popular_tags_on_empty_db_to_verify_sql() { + let (addr, _, _) = run_server(); + let client = reqwest::Client::new(); + // Check that the requests succeeds on an empty database just // to verify that the literal SQL query that is not verified // at compile-time still matches the current database schema! - let (client, _) = setup(); + // All parameters let response = client - .get("/entries/most-popular-tags?offset=10&limit=1000&min_count=10&max_count=100&max_cache_age=0") - .dispatch(); - assert_eq!(response.status(), Status::Ok); - let body_str = response.into_string().unwrap(); + .get(endpoint(addr, "/entries/most-popular-tags?offset=10&limit=1000&min_count=10&max_count=100&max_cache_age=0")) + .send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body_str = response.text().await.unwrap(); assert_eq!(body_str, "[]"); // Only offset parameter let response = client - .get("/entries/most-popular-tags?offset=1&max_cache_age=0") - .dispatch(); - assert_eq!(response.status(), Status::Ok); + .get(endpoint( + addr, + "/entries/most-popular-tags?offset=1&max_cache_age=0", + )) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); // Only limit parameter let response = client - .get("/entries/most-popular-tags?limit=1&max_cache_age=0") - .dispatch(); - assert_eq!(response.status(), Status::Ok); + .get(endpoint( + addr, + "/entries/most-popular-tags?limit=1&max_cache_age=0", + )) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); // Only min_count parameter let response = client - .get("/entries/most-popular-tags?min_count=1&max_cache_age=0") - .dispatch(); - assert_eq!(response.status(), Status::Ok); + .get(endpoint( + addr, + "/entries/most-popular-tags?min_count=1&max_cache_age=0", + )) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); // Only max_count parameter let response = client - .get("/entries/most-popular-tags?max_count=1&max_cache_age=0") - .dispatch(); - assert_eq!(response.status(), Status::Ok); + .get(endpoint( + addr, + "/entries/most-popular-tags?max_count=1&max_cache_age=0", + )) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); } fn init_tags_cache_test_db(cnt: usize, db: &sqlite::Connections) { @@ -1812,51 +1864,59 @@ fn init_tags_cache_test_db(cnt: usize, db: &sqlite::Connections) { }); } -#[test] -fn update_most_popular_tags_on_outdated_cache() { - let (client, db) = setup(); +#[tokio::test] +async fn update_most_popular_tags_on_outdated_cache() { + let (addr, db, _) = run_server(); + let client = reqwest::Client::new(); // init init_tags_cache_test_db(10, &db); // get only popular tags let response = client - .get("/entries/most-popular-tags?min_count=8&max_cache_age=0") - .dispatch(); - assert_eq!(response.status(), Status::Ok); - let body_str = response.into_string().unwrap(); + .get(endpoint( + addr, + "/entries/most-popular-tags?min_count=8&max_cache_age=0", + )) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body_str = response.text().await.unwrap(); let tags: Vec = serde_json::from_str(&body_str).unwrap(); assert_eq!(tags.len(), 3); let response = client - .get("/entries/most-popular-tags?max_count=1") - .dispatch(); - assert_eq!(response.status(), Status::Ok); - let body_str = response.into_string().unwrap(); + .get(endpoint(addr, "/entries/most-popular-tags?max_count=1")) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body_str = response.text().await.unwrap(); let tags: Vec = serde_json::from_str(&body_str).unwrap(); assert_eq!(tags.len(), 1); // get cached popular tags again let response = client - .get("/entries/most-popular-tags?min_count=8") - .dispatch(); - assert_eq!(response.status(), Status::Ok); - let body_str = response.into_string().unwrap(); + .get(endpoint(addr, "/entries/most-popular-tags?min_count=8")) + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body_str = response.text().await.unwrap(); let tags: Vec = serde_json::from_str(&body_str).unwrap(); assert_eq!(tags.len(), 3); } -#[test] -fn openapi() { - let (client, _) = setup(); - let req = client.get("/server/openapi.yaml"); - let response = req.dispatch(); - assert_eq!(response.status(), Status::Ok); - assert_eq!( - response.headers().get("Content-Type").collect::>()[0], - "text/yaml" - ); - let body_str = response.into_string().unwrap(); +#[tokio::test] +async fn openapi() { + let (addr, _, _) = run_server(); + let client = reqwest::Client::new(); + let req = client.get(endpoint(addr, "/server/openapi.yaml")); + let response = req.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers().get("Content-Type").unwrap(), "text/yaml"); + let body_str = response.text().await.unwrap(); assert!(body_str.contains("openapi:")) } @@ -2094,7 +2154,7 @@ fn search_duplicates() { .body(r#"{"title":"foO","description":"bla","lat":0.0005,"lng":0.0005}"#) .dispatch(); assert_eq!(res.status(), Status::Ok); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); let body_str = res.into_string().unwrap(); let duplicate_places: Vec = serde_json::from_str(&body_str).unwrap(); @@ -2102,13 +2162,14 @@ fn search_duplicates() { assert_eq!(place.id.to_string(), duplicate_places.first().unwrap().id); } -#[test] -fn get_version() { - let (client, _) = setup(); - let req = client.get("/server/version"); - let response = req.dispatch(); - assert_eq!(response.status(), Status::Ok); - let body_str = response.into_string().unwrap(); +#[tokio::test] +async fn get_version() { + let (addr, _, _) = run_server(); + let client = reqwest::Client::new(); + let req = client.get(endpoint(addr, "/server/version")); + let response = req.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body_str = response.text().await.unwrap(); assert_eq!(body_str, DUMMY_VERSION); } @@ -2143,7 +2204,7 @@ mod with_captcha_protection_enabled { .body(r#"{"title":"foo","description":"blablabla","lat":0.0,"lng":0.0,"categories":["x"],"license":"CC0-1.0","tags":[]}"#); let response = req.dispatch(); assert_eq!(response.status(), Status::Ok); - test_json(&response); + assert_rocket_response_has_json_content_type(&response); let body_str = response.into_string().unwrap(); let eid = db.exclusive().unwrap().all_places().unwrap()[0] .0 @@ -2245,7 +2306,7 @@ fn review_place_with_token() { )) .dispatch(); // TODO: should be Status::Created - test_json(&res); + assert_rocket_response_has_json_content_type(&res); assert_eq!(res.status(), Status::Ok); } @@ -2277,7 +2338,7 @@ fn review_place_with_token_and_invalid_status() { "{{\"token\":\"{token}\",\"status\":\"doesnotexist\"}}", )) .dispatch(); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); assert_eq!(res.status(), Status::UnprocessableEntity); } @@ -2309,6 +2370,6 @@ fn review_place_with_token_and_invalid_revision() { "{{\"token\":\"{token}\",\"status\":\"confirmed\"}}", )) .dispatch(); - test_json(&res); + assert_rocket_response_has_json_content_type(&res); assert_eq!(res.status(), Status::BadRequest); } diff --git a/ofdb-webserver/src/web/frontend/login.rs b/ofdb-webserver/src/web/frontend/login.rs index 2dca2931..999b9e67 100644 --- a/ofdb-webserver/src/web/frontend/login.rs +++ b/ofdb-webserver/src/web/frontend/login.rs @@ -101,7 +101,7 @@ pub mod tests { }; fn setup() -> (Client, Connections) { - let (client, db, _) = web::tests::setup(vec![("/", super::super::routes())]); + let (client, db, _) = web::tests::rocket_test_setup(vec![("/", super::super::routes())]); (client, db) } diff --git a/ofdb-webserver/src/web/frontend/tests.rs b/ofdb-webserver/src/web/frontend/tests.rs index 46643164..3e117cf3 100644 --- a/ofdb-webserver/src/web/frontend/tests.rs +++ b/ofdb-webserver/src/web/frontend/tests.rs @@ -10,7 +10,7 @@ pub fn setup() -> ( sqlite::Connections, tantivy::SearchEngine, ) { - crate::web::tests::setup(vec![("/", super::routes())]) + crate::web::tests::rocket_test_setup(vec![("/", super::routes())]) } fn create_user(pool: &Connections, name: &str, role: Role) { diff --git a/ofdb-webserver/src/web/tests.rs b/ofdb-webserver/src/web/tests.rs index 29e626ee..9d43c8d0 100644 --- a/ofdb-webserver/src/web/tests.rs +++ b/ofdb-webserver/src/web/tests.rs @@ -1,8 +1,13 @@ +use std::{ + net::{SocketAddr, TcpListener}, + thread, +}; + use rocket::{config::Config as RocketCfg, local::blocking::Client, Route}; use crate::{ core::{prelude::*, usecases}, - web::{sqlite, tantivy, Cfg}, + web::{api::tests::prelude::default_accepted_licenses, sqlite, tantivy, Cfg}, }; pub mod prelude { @@ -15,27 +20,47 @@ pub mod prelude { response::Response, }; - pub use super::DummyNotifyGW; + pub use super::{run_server, DummyNotifyGW}; + pub use crate::core::db::*; } -pub fn setup( - mounts: Vec<(&'static str, Vec)>, -) -> (Client, sqlite::Connections, tantivy::SearchEngine) { - setup_with_cfg( - mounts, - Cfg { - accepted_licenses: crate::web::api::tests::prelude::default_accepted_licenses(), - protect_with_captcha: false, - }, - ) +pub fn run_server() -> (SocketAddr, sqlite::Connections, tantivy::SearchEngine) { + let cfg = Cfg { + accepted_licenses: default_accepted_licenses(), + protect_with_captcha: false, + }; + + let address = { + let listener = TcpListener::bind("127.0.0.1:0".parse::().unwrap()).unwrap(); + listener.local_addr().unwrap() + }; + + let mut rocket_cfg = RocketCfg::debug_default(); + rocket_cfg.port = address.port(); + rocket_cfg.address = address.ip(); + + let socket_address: SocketAddr = (rocket_cfg.address, rocket_cfg.port).into(); + + let (rocket, db, search_engine) = + rocket_test_instance_with_cfg(crate::web::mounts(), cfg, rocket_cfg); + + thread::spawn(move || { + rocket::execute(rocket.launch()).unwrap(); + }); + + (socket_address, db, search_engine) } -pub fn setup_with_cfg( +fn rocket_test_instance_with_cfg( mounts: Vec<(&'static str, Vec)>, cfg: Cfg, -) -> (Client, sqlite::Connections, tantivy::SearchEngine) { - let rocket_cfg = RocketCfg::debug_default(); + rocket_cfg: RocketCfg, +) -> ( + rocket::Rocket, + sqlite::Connections, + tantivy::SearchEngine, +) { let connections = ofdb_db_sqlite::Connections::init(":memory:", 1).unwrap(); ofdb_db_sqlite::run_embedded_database_migrations(connections.exclusive().unwrap()); let search_engine = tantivy::SearchEngine::init_in_ram().unwrap(); @@ -58,6 +83,27 @@ pub fn setup_with_cfg( notify: Box::new(notify_gw), }; let rocket = super::rocket_instance(options, connections, gateways); + (rocket, db, search_engine) +} + +pub fn rocket_test_setup( + mounts: Vec<(&'static str, Vec)>, +) -> (Client, sqlite::Connections, tantivy::SearchEngine) { + rocket_test_setup_with_cfg( + mounts, + Cfg { + accepted_licenses: default_accepted_licenses(), + protect_with_captcha: false, + }, + ) +} + +pub fn rocket_test_setup_with_cfg( + mounts: Vec<(&'static str, Vec)>, + cfg: Cfg, +) -> (Client, sqlite::Connections, tantivy::SearchEngine) { + let rocket_cfg = RocketCfg::debug_default(); + let (rocket, db, search_engine) = rocket_test_instance_with_cfg(mounts, cfg, rocket_cfg); let client = Client::tracked(rocket).unwrap(); (client, db, search_engine) }