use cookie::Cookie; use function_name::named; use http::header::*; use http::{HeaderMap, HeaderValue, Response, StatusCode}; use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use serde::de::DeserializeOwned; use serde::Serialize; use std::path::PathBuf; use std::time::Duration; use crate::service::constants::*; use crate::service::dto; use crate::{config, ddns, index, vfs}; #[cfg(feature = "service-rocket")] pub use crate::service::rocket::test::ServiceType; const TEST_USERNAME: &str = "test_user"; const TEST_PASSWORD: &str = "test_password"; const TEST_MOUNT_NAME: &str = "collection"; const TEST_MOUNT_SOURCE: &str = "test/collection"; pub trait TestService { fn new(db_name: &str) -> Self; fn get(&mut self, url: &str) -> Response<()>; fn get_bytes(&mut self, url: &str, headers: &HeaderMap) -> Response>; fn post(&mut self, url: &str) -> Response<()>; fn delete(&mut self, url: &str) -> Response<()>; fn get_json(&mut self, url: &str) -> Response; fn put_json(&mut self, url: &str, payload: &T) -> Response<()>; fn post_json(&mut self, url: &str, payload: &T) -> Response<()>; fn complete_initial_setup(&mut self) { let configuration = config::Config { album_art_pattern: None, prefix_url: None, reindex_every_n_seconds: None, ydns: None, users: Some(vec![config::ConfigUser { name: TEST_USERNAME.into(), password: TEST_PASSWORD.into(), admin: true, }]), mount_dirs: Some(vec![vfs::MountPoint { name: TEST_MOUNT_NAME.into(), source: TEST_MOUNT_SOURCE.into(), }]), }; self.put_json("/api/settings", &configuration); } fn login(&mut self) { let credentials = dto::AuthCredentials { username: TEST_USERNAME.into(), password: TEST_PASSWORD.into(), }; self.post_json("/api/auth", &credentials); } fn index(&mut self) { assert!(self.post("/api/trigger_index").status() == StatusCode::OK); loop { let response = self.get_json::>("/api/browse"); let entries = response.body(); if entries.len() > 0 { break; } std::thread::sleep(Duration::from_secs(1)); } loop { let response = self.get_json::>("/api/flatten"); let entries = response.body(); if entries.len() > 0 { break; } std::thread::sleep(Duration::from_secs(1)); } } } #[named] #[test] fn test_service_index() { let mut service = ServiceType::new(function_name!()); service.get("/"); } #[named] #[test] fn test_service_swagger_index() { let mut service = ServiceType::new(function_name!()); assert!(service.get("/swagger").status() == StatusCode::OK); } #[named] #[test] fn test_service_swagger_index_with_trailing_slash() { let mut service = ServiceType::new(function_name!()); assert!(service.get("/swagger/").status() == StatusCode::OK); } #[named] #[test] fn test_service_version() { let mut service = ServiceType::new(function_name!()); let response = service.get_json::("/api/version"); let version = response.body(); assert_eq!(version, &dto::Version { major: 4, minor: 0 }); } #[named] #[test] fn test_service_initial_setup() { let mut service = ServiceType::new(function_name!()); { let response = service.get_json::("/api/initial_setup"); let initial_setup = response.body(); assert_eq!( initial_setup, &dto::InitialSetup { has_any_users: false } ); } service.complete_initial_setup(); { let response = service.get_json::("/api/initial_setup"); let initial_setup = response.body(); assert_eq!( initial_setup, &dto::InitialSetup { has_any_users: true } ); } } #[named] #[test] fn test_service_settings() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); assert!(service.get("/api/settings").status() == StatusCode::UNAUTHORIZED); service.login(); { let response = service.get_json::("/api/settings"); let configuration = response.body(); assert_eq!( configuration, &config::Config { album_art_pattern: Some("Folder.(jpg|png)".to_string()), reindex_every_n_seconds: Some(1800), mount_dirs: Some(vec![vfs::MountPoint { name: TEST_MOUNT_NAME.into(), source: TEST_MOUNT_SOURCE.into() }]), prefix_url: None, users: Some(vec![config::ConfigUser { name: TEST_USERNAME.into(), password: "".into(), admin: true }]), ydns: Some(ddns::DDNSConfig { host: "".into(), username: "".into(), password: "".into() }), } ); } let mut configuration = config::Config { album_art_pattern: Some("my_pattern".to_owned()), reindex_every_n_seconds: Some(3600), mount_dirs: Some(vec![ vfs::MountPoint { name: TEST_MOUNT_NAME.into(), source: TEST_MOUNT_SOURCE.into(), }, vfs::MountPoint { name: "more_music".into(), source: "test/collection".into(), }, ]), prefix_url: Some("my_prefix".to_owned()), users: Some(vec![ config::ConfigUser { name: "test_user".into(), password: "some_password".into(), admin: true, }, config::ConfigUser { name: "other_user".into(), password: "some_other_password".into(), admin: false, }, ]), ydns: Some(ddns::DDNSConfig { host: "my_host".into(), username: "my_username".into(), password: "my_password".into(), }), }; service.put_json("/api/settings", &configuration); configuration.users = Some(vec![ config::ConfigUser { name: "test_user".into(), password: "".into(), admin: true, }, config::ConfigUser { name: "other_user".into(), password: "".into(), admin: false, }, ]); let response = service.get_json::("/api/settings"); let received = response.body(); assert_eq!(received, &configuration); } #[named] #[test] fn test_service_preferences() { // TODO } #[named] #[test] fn test_service_trigger_index() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); let response = service.get_json::>("/api/random"); let entries = response.body(); assert_eq!(entries.len(), 0); service.index(); let response = service.get_json::>("/api/random"); let entries = response.body(); assert_eq!(entries.len(), 2); } #[named] #[test] fn test_service_auth() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); { let credentials = dto::AuthCredentials { username: "garbage".into(), password: "garbage".into(), }; assert!(service.post_json("/api/auth", &credentials).status() == StatusCode::UNAUTHORIZED); } { let credentials = dto::AuthCredentials { username: TEST_USERNAME.into(), password: "garbage".into(), }; assert!(service.post_json("/api/auth", &credentials).status() == StatusCode::UNAUTHORIZED); } { let credentials = dto::AuthCredentials { username: TEST_USERNAME.into(), password: TEST_PASSWORD.into(), }; let response = service.post_json("/api/auth", &credentials); assert!(response.status() == StatusCode::OK); let cookies: Vec = response .headers() .get_all(SET_COOKIE) .iter() .map(|c| Cookie::parse(c.to_str().unwrap()).unwrap()) .collect(); assert!(cookies.iter().any(|c| c.name() == COOKIE_SESSION)); assert!(cookies.iter().any(|c| c.name() == COOKIE_USERNAME)); assert!(cookies.iter().any(|c| c.name() == COOKIE_ADMIN)); } } #[named] #[test] fn test_service_browse() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let response = service.get_json::>("/api/browse"); let entries = response.body(); assert_eq!(entries.len(), 1); let mut path = PathBuf::new(); path.push("collection"); path.push("Khemmis"); path.push("Hunted"); let uri = format!( "/api/browse/{}", percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC) ); let response = service.get_json::>(&uri); let entries = response.body(); assert_eq!(entries.len(), 5); } #[named] #[test] fn test_service_flatten() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let response = service.get_json::>("/api/flatten"); let entries = response.body(); assert_eq!(entries.len(), 12); let response = service.get_json::>("/api/flatten/collection"); let entries = response.body(); assert_eq!(entries.len(), 12); } #[named] #[test] fn test_service_random() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let response = service.get_json::>("/api/random"); let entries = response.body(); assert_eq!(entries.len(), 2); } #[named] #[test] fn test_service_recent() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let response = service.get_json::>("/api/recent"); let entries = response.body(); assert_eq!(entries.len(), 2); } #[named] #[test] fn test_service_search() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let response = service.get_json::>("/api/search/door"); let results = response.body(); assert_eq!(results.len(), 1); match results[0] { index::CollectionFile::Song(ref s) => assert_eq!(s.title, Some("Beyond The Door".into())), _ => panic!(), } } #[named] #[test] fn test_service_serve() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let mut path = PathBuf::new(); path.push("collection"); path.push("Khemmis"); path.push("Hunted"); path.push("02 - Candlelight.mp3"); let uri = format!( "/api/serve/{}", percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC) ); let response = service.get_bytes(&uri, &HeaderMap::new()); assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().len(), 24_142); { let mut headers = HeaderMap::new(); headers.append(RANGE, HeaderValue::from_str("bytes=100-299").unwrap()); let response = service.get_bytes(&uri, &headers); assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); assert_eq!(response.body().len(), 200); assert_eq!(response.headers().get(CONTENT_LENGTH).unwrap(), "200"); } } #[named] #[test] fn test_service_playlists() { let mut service = ServiceType::new(function_name!()); service.complete_initial_setup(); service.login(); service.index(); let response = service.get_json::>("/api/playlists"); let playlists = response.body(); assert_eq!(playlists.len(), 0); let response = service.get_json::>("/api/flatten"); let mut my_songs = response.into_body(); my_songs.pop(); my_songs.pop(); let my_playlist = dto::SavePlaylistInput { tracks: my_songs.iter().map(|s| s.path.clone()).collect(), }; service.put_json("/api/playlist/my_playlist", &my_playlist); let response = service.get_json::>("/api/playlists"); let playlists = response.body(); assert_eq!( playlists, &vec![dto::ListPlaylistsEntry { name: "my_playlist".into() }] ); let response = service.get_json::>("/api/playlist/my_playlist"); let songs = response.body(); assert_eq!(songs, &my_songs); service.delete("/api/playlist/my_playlist"); let response = service.get_json::>("/api/playlists"); let playlists = response.body(); assert_eq!(playlists.len(), 0); }