Axum initial setup

This commit is contained in:
Antoine Gersant 2024-07-13 12:30:02 -07:00
parent 138886e55c
commit 08353a717f
26 changed files with 636 additions and 863 deletions

883
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,15 +9,12 @@ build = "build.rs"
ui = ["native-windows-gui", "native-windows-derive"]
[dependencies]
actix-files = { version = "0.6" }
actix-web-httpauth = { version = "0.8" }
ape = "0.5"
base64 = "0.21"
branca = "0.10.1"
crossbeam-channel = "0.5"
futures-util = { version = "0.3" }
getopts = "0.2.21"
http = "0.2.8"
id3 = "1.7.0"
lewton = "0.10.2"
log = "0.4.17"
@ -42,10 +39,10 @@ toml = "0.7"
ureq = "2.7"
url = "2.3"
[dependencies.actix-web]
version = "4"
[dependencies.axum]
version = "0.7.5"
default-features = false
features = ["macros", "compress-brotli", "compress-gzip"]
features = ["http1", "json", "tokio", "tower-log", "tracing", "query"]
[dependencies.image]
version = "0.24.4"
@ -76,8 +73,10 @@ embed-resource = "1.8"
winres = "0.1"
[dev-dependencies]
actix-test = "0.1.0"
headers = "0.3"
axum-test = "15.3"
bytes = "1.6.1"
headers = "0.4"
http = "1.1.0"
fs_extra = "1.2.0"
[profile.dev.package.sqlx-macros]

View file

@ -13,7 +13,7 @@ mod app;
mod db;
mod options;
mod paths;
mod service;
mod server;
#[cfg(test)]
mod test;
mod ui;
@ -149,7 +149,7 @@ async fn async_main(cli_options: CLIOptions, paths: paths::Paths) -> Result<(),
// Start server
info!("Starting up server");
if let Err(e) = service::launch(app) {
if let Err(e) = server::launch(app).await {
return Err(Error::ServiceStartup(e));
}

View file

@ -4,5 +4,5 @@ mod error;
#[cfg(test)]
mod test;
mod actix;
pub use actix::*;
mod axum;
pub use axum::*;

View file

@ -26,7 +26,7 @@ use crate::app::{
lastfm, playlist, settings, thumbnail, user,
vfs::{self, MountDir},
};
use crate::service::{dto, error::*};
use crate::server::{dto, error::*};
pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
move |cfg: &mut ServiceConfig| {

48
src/server/axum.rs Normal file
View file

@ -0,0 +1,48 @@
use axum::Router;
use crate::app::App;
mod api;
#[cfg(test)]
pub mod test;
pub fn make_router(app: App) -> Router {
Router::new()
// move |cfg: &mut ServiceConfig| {
// cfg.app_data(web::Data::new(app.index))
// .app_data(web::Data::new(app.config_manager))
// .app_data(web::Data::new(app.ddns_manager))
// .app_data(web::Data::new(app.lastfm_manager))
// .app_data(web::Data::new(app.playlist_manager))
// .app_data(web::Data::new(app.settings_manager))
// .app_data(web::Data::new(app.thumbnail_manager))
// .app_data(web::Data::new(app.user_manager))
// .app_data(web::Data::new(app.vfs_manager))
// .service(
// web::scope("/api")
// .configure(api::make_config())
// .wrap(NormalizePath::trim()),
// )
// .service(
// actix_files::Files::new("/swagger", app.swagger_dir_path)
// .redirect_to_slash_directory()
// .index_file("index.html"),
// )
// .service(
// actix_files::Files::new("/", app.web_dir_path)
// .redirect_to_slash_directory()
// .index_file("index.html"),
// );
// }
}
pub async fn launch(app: App) -> Result<(), std::io::Error> {
let port = app.port;
let router = make_router(app);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
axum::serve(listener, router).await?;
Ok(())
}

0
src/server/axum/api.rs Normal file
View file

89
src/server/axum/test.rs Normal file
View file

@ -0,0 +1,89 @@
use axum::body::Bytes;
use axum_test::TestServer;
use http::{response::Builder, Method, Request, Response};
use serde::Serialize;
use crate::app::App;
use crate::paths::Paths;
use crate::server::axum::*;
use crate::server::dto;
use crate::server::test::TestService;
use crate::test::*;
pub struct AxumTestService {
authorization: Option<dto::Authorization>,
server: TestServer,
}
pub type ServiceType = AxumTestService;
impl TestService for AxumTestService {
async fn new(test_name: &str) -> Self {
let output_dir = prepare_test_directory(test_name);
let paths = Paths {
cache_dir_path: ["test-output", test_name].iter().collect(),
config_file_path: None,
db_file_path: output_dir.join("db.sqlite"),
#[cfg(unix)]
pid_file_path: output_dir.join("polaris.pid"),
log_file_path: None,
swagger_dir_path: ["docs", "swagger"].iter().collect(),
web_dir_path: ["test-data", "web"].iter().collect(),
};
let app = App::new(5050, paths).await.unwrap();
let router = make_router(app);
let server = TestServer::new(router).unwrap();
AxumTestService {
authorization: None,
server,
}
}
async fn execute_request<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> (Builder, Option<Bytes>) {
let url = request.uri().to_string();
let body = request.body().clone();
let mut axum_request = match *request.method() {
Method::GET => self.server.get(&url),
Method::POST => self.server.post(&url),
Method::PUT => self.server.put(&url),
Method::DELETE => self.server.delete(&url),
_ => unimplemented!(),
};
for (name, value) in request.headers() {
axum_request = axum_request.add_header(name.clone(), value.clone());
}
if let Some(ref authorization) = self.authorization {
axum_request = axum_request.authorization_bearer(authorization.token.clone());
}
let axum_response = axum_request.json(&body).await;
let mut response_builder = Response::builder().status(axum_response.status_code());
let headers = response_builder.headers_mut().unwrap();
for (name, value) in axum_response.headers().iter() {
headers.append(name, value.clone());
}
let is_success = axum_response.status_code().is_success();
let body = if is_success {
Some(axum_response.into_bytes())
} else {
None
};
(response_builder, body)
}
fn set_authorization(&mut self, authorization: Option<dto::Authorization>) {
self.authorization = authorization;
}
}

View file

@ -1,6 +1,9 @@
use bytes::Bytes;
use http::response::Builder;
use http::{Request, Response, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::ops::Deref;
use std::path::Path;
use std::time::Duration;
@ -20,23 +23,45 @@ mod user;
mod web;
use crate::app::index;
use crate::service::dto;
use crate::service::test::constants::*;
use crate::server::dto;
use crate::server::test::constants::*;
pub use crate::service::actix::test::ServiceType;
pub use crate::server::axum::test::ServiceType;
pub trait TestService {
async fn new(test_name: &str) -> Self;
async fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>)
-> Response<()>;
async fn execute_request<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> (Builder, Option<Bytes>);
async fn fetch<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> Response<()> {
let (response_builder, _body) = self.execute_request(request).await;
response_builder.body(()).unwrap()
}
async fn fetch_bytes<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> Response<Vec<u8>>;
) -> Response<Vec<u8>> {
let (response_builder, body) = self.execute_request(request).await;
response_builder
.body(body.unwrap().deref().to_owned())
.unwrap()
}
async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
&mut self,
request: &Request<T>,
) -> Response<U>;
) -> Response<U> {
let (response_builder, body) = self.execute_request(request).await;
let body = serde_json::from_slice(&body.unwrap()).unwrap();
response_builder.body(body).unwrap()
}
async fn complete_initial_setup(&mut self) {
let configuration = dto::Config {

View file

@ -1,11 +1,11 @@
use http::StatusCode;
use crate::app::index;
use crate::service::dto;
use crate::service::test::{protocol, ServiceType, TestService};
use crate::server::dto;
use crate::server::test::{protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn returns_api_version() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::version();
@ -13,7 +13,7 @@ async fn returns_api_version() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn initial_setup_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::initial_setup();
@ -42,7 +42,7 @@ async fn initial_setup_golden_path() {
}
}
#[actix_web::test]
#[tokio::test]
async fn trigger_index_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -65,7 +65,7 @@ async fn trigger_index_golden_path() {
assert_eq!(entries.len(), 3);
}
#[actix_web::test]
#[tokio::test]
async fn trigger_index_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -74,7 +74,7 @@ async fn trigger_index_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn trigger_index_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -1,11 +1,11 @@
use headers::{self, HeaderMapExt};
use http::StatusCode;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::server::dto;
use crate::server::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn login_rejects_bad_username() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -15,7 +15,7 @@ async fn login_rejects_bad_username() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn login_rejects_bad_password() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -25,7 +25,7 @@ async fn login_rejects_bad_password() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn login_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -40,7 +40,7 @@ async fn login_golden_path() {
assert!(!authorization.token.is_empty());
}
#[actix_web::test]
#[tokio::test]
async fn authentication_via_bearer_http_header_rejects_bad_token() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -53,7 +53,7 @@ async fn authentication_via_bearer_http_header_rejects_bad_token() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn authentication_via_bearer_http_header_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -74,7 +74,7 @@ async fn authentication_via_bearer_http_header_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn authentication_via_query_param_rejects_bad_token() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -88,7 +88,7 @@ async fn authentication_via_query_param_rejects_bad_token() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn authentication_via_query_param_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -2,10 +2,10 @@ use http::StatusCode;
use std::path::{Path, PathBuf};
use crate::app::index;
use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService};
use crate::server::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn browse_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::browse(&PathBuf::new());
@ -13,7 +13,7 @@ async fn browse_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn browse_root() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -30,7 +30,7 @@ async fn browse_root() {
assert_eq!(entries.len(), 1);
}
#[actix_web::test]
#[tokio::test]
async fn browse_directory() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -48,7 +48,7 @@ async fn browse_directory() {
assert_eq!(entries.len(), 5);
}
#[actix_web::test]
#[tokio::test]
async fn browse_bad_directory() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -60,7 +60,7 @@ async fn browse_bad_directory() {
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
#[tokio::test]
async fn flatten_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::flatten(&PathBuf::new());
@ -68,7 +68,7 @@ async fn flatten_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn flatten_root() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -83,7 +83,7 @@ async fn flatten_root() {
assert_eq!(entries.len(), 13);
}
#[actix_web::test]
#[tokio::test]
async fn flatten_directory() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -98,7 +98,7 @@ async fn flatten_directory() {
assert_eq!(entries.len(), 13);
}
#[actix_web::test]
#[tokio::test]
async fn flatten_bad_directory() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -110,7 +110,7 @@ async fn flatten_bad_directory() {
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
#[tokio::test]
async fn random_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::random();
@ -118,7 +118,7 @@ async fn random_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn random_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -135,7 +135,7 @@ async fn random_golden_path() {
assert_eq!(entries.len(), 3);
}
#[actix_web::test]
#[tokio::test]
async fn random_with_trailing_slash() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -153,7 +153,7 @@ async fn random_with_trailing_slash() {
assert_eq!(entries.len(), 3);
}
#[actix_web::test]
#[tokio::test]
async fn recent_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::recent();
@ -161,7 +161,7 @@ async fn recent_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn recent_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -178,7 +178,7 @@ async fn recent_golden_path() {
assert_eq!(entries.len(), 3);
}
#[actix_web::test]
#[tokio::test]
async fn recent_with_trailing_slash() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -196,7 +196,7 @@ async fn recent_with_trailing_slash() {
assert_eq!(entries.len(), 3);
}
#[actix_web::test]
#[tokio::test]
async fn search_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::search("");
@ -204,7 +204,7 @@ async fn search_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn search_without_query() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -217,7 +217,7 @@ async fn search_without_query() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn search_with_query() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -1,10 +1,10 @@
use http::StatusCode;
use crate::service::dto;
use crate::service::test::{protocol, ServiceType, TestService};
use crate::server::dto;
use crate::server::test::{protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn get_ddns_config_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::get_ddns_config();
@ -18,7 +18,7 @@ async fn get_ddns_config_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn get_ddns_config_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -29,7 +29,7 @@ async fn get_ddns_config_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn put_ddns_config_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::put_ddns_config(dto::DDNSConfig {
@ -47,7 +47,7 @@ async fn put_ddns_config_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn put_ddns_config_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -1,11 +1,11 @@
use http::StatusCode;
use std::path::PathBuf;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::server::dto;
use crate::server::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn lastfm_scrobble_ignores_unlinked_user() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -22,7 +22,7 @@ async fn lastfm_scrobble_ignores_unlinked_user() {
assert_eq!(response.status(), StatusCode::NO_CONTENT);
}
#[actix_web::test]
#[tokio::test]
async fn lastfm_now_playing_ignores_unlinked_user() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -39,7 +39,7 @@ async fn lastfm_now_playing_ignores_unlinked_user() {
assert_eq!(response.status(), StatusCode::NO_CONTENT);
}
#[actix_web::test]
#[tokio::test]
async fn lastfm_link_token_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::lastfm_link_token();
@ -47,7 +47,7 @@ async fn lastfm_link_token_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn lastfm_link_token_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -1,11 +1,11 @@
use http::{header, HeaderValue, StatusCode};
use std::path::PathBuf;
use crate::service::dto::ThumbnailSize;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::server::dto::ThumbnailSize;
use crate::server::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn audio_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
@ -18,7 +18,7 @@ async fn audio_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn audio_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -40,7 +40,7 @@ async fn audio_golden_path() {
);
}
#[actix_web::test]
#[tokio::test]
async fn audio_does_not_encode_content() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -69,7 +69,7 @@ async fn audio_does_not_encode_content() {
);
}
#[actix_web::test]
#[tokio::test]
async fn audio_partial_content() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -97,7 +97,7 @@ async fn audio_partial_content() {
);
}
#[actix_web::test]
#[tokio::test]
async fn audio_bad_path_returns_not_found() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -110,7 +110,7 @@ async fn audio_bad_path_returns_not_found() {
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
#[tokio::test]
async fn thumbnail_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
@ -125,7 +125,7 @@ async fn thumbnail_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn thumbnail_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -144,7 +144,7 @@ async fn thumbnail_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn thumbnail_bad_path_returns_not_found() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -159,23 +159,23 @@ async fn thumbnail_bad_path_returns_not_found() {
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
#[tokio::test]
async fn thumbnail_size_default() {
thumbnail_size(&test_name!(), None, None, 400).await;
}
#[actix_web::test]
#[tokio::test]
async fn thumbnail_size_small() {
thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400).await;
}
#[actix_web::test]
#[tokio::test]
#[cfg(not(tarpaulin))]
async fn thumbnail_size_large() {
thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200).await;
}
#[actix_web::test]
#[tokio::test]
#[cfg(not(tarpaulin))]
async fn thumbnail_size_native() {
thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423).await;

View file

@ -1,11 +1,11 @@
use http::StatusCode;
use crate::app::index;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::server::dto;
use crate::server::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn list_playlists_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::playlists();
@ -13,7 +13,7 @@ async fn list_playlists_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn list_playlists_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -25,7 +25,7 @@ async fn list_playlists_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn save_playlist_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
@ -34,7 +34,7 @@ async fn save_playlist_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn save_playlist_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -46,7 +46,7 @@ async fn save_playlist_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn save_playlist_large() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -61,7 +61,7 @@ async fn save_playlist_large() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn get_playlist_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
@ -69,7 +69,7 @@ async fn get_playlist_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn get_playlist_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -87,7 +87,7 @@ async fn get_playlist_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn get_playlist_bad_name_returns_not_found() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -98,7 +98,7 @@ async fn get_playlist_bad_name_returns_not_found() {
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
#[tokio::test]
async fn delete_playlist_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
@ -106,7 +106,7 @@ async fn delete_playlist_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn delete_playlist_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -124,7 +124,7 @@ async fn delete_playlist_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn delete_playlist_bad_name_returns_not_found() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -2,8 +2,8 @@ use http::{Method, Request};
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use std::path::Path;
use crate::service::dto;
use crate::{app::user, service::dto::ThumbnailSize};
use crate::server::dto;
use crate::{app::user, server::dto::ThumbnailSize};
pub fn web_index() -> Request<()> {
Request::builder()

View file

@ -1,10 +1,10 @@
use http::StatusCode;
use crate::service::dto::{self, Settings};
use crate::service::test::{protocol, ServiceType, TestService};
use crate::server::dto::{self, Settings};
use crate::server::test::{protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn get_settings_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -14,7 +14,7 @@ async fn get_settings_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn get_settings_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -25,7 +25,7 @@ async fn get_settings_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn get_settings_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -36,7 +36,7 @@ async fn get_settings_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn put_settings_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -45,7 +45,7 @@ async fn put_settings_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn put_settings_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -55,7 +55,7 @@ async fn put_settings_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn put_settings_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -1,9 +1,9 @@
use http::StatusCode;
use crate::service::test::{add_trailing_slash, protocol, ServiceType, TestService};
use crate::server::test::{add_trailing_slash, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn can_get_swagger_index() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::swagger_index();
@ -11,7 +11,7 @@ async fn can_get_swagger_index() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn can_get_swagger_index_with_trailing_slash() {
let mut service = ServiceType::new(&test_name!()).await;
let mut request = protocol::swagger_index();

View file

@ -2,11 +2,11 @@ use http::StatusCode;
use std::default::Default;
use crate::app::user;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::server::dto;
use crate::server::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn list_users_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -20,7 +20,7 @@ async fn list_users_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn list_users_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -30,7 +30,7 @@ async fn list_users_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn create_user_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -48,7 +48,7 @@ async fn create_user_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn create_user_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -64,7 +64,7 @@ async fn create_user_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn update_user_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -78,7 +78,7 @@ async fn update_user_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn update_user_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -89,7 +89,7 @@ async fn update_user_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn update_user_cannot_unadmin_self() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -106,7 +106,7 @@ async fn update_user_cannot_unadmin_self() {
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[actix_web::test]
#[tokio::test]
async fn delete_user_requires_admin() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -120,7 +120,7 @@ async fn delete_user_requires_admin() {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[actix_web::test]
#[tokio::test]
async fn delete_user_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -131,7 +131,7 @@ async fn delete_user_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn delete_user_cannot_delete_self() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -141,7 +141,7 @@ async fn delete_user_cannot_delete_self() {
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[actix_web::test]
#[tokio::test]
async fn get_preferences_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::get_preferences();
@ -149,7 +149,7 @@ async fn get_preferences_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn get_preferences_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;
@ -160,7 +160,7 @@ async fn get_preferences_golden_path() {
assert_eq!(response.status(), StatusCode::OK);
}
#[actix_web::test]
#[tokio::test]
async fn put_preferences_requires_auth() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::put_preferences(user::Preferences::default());
@ -168,7 +168,7 @@ async fn put_preferences_requires_auth() {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
#[tokio::test]
async fn put_preferences_golden_path() {
let mut service = ServiceType::new(&test_name!()).await;
service.complete_initial_setup().await;

View file

@ -1,9 +1,9 @@
use http::StatusCode;
use crate::service::test::{protocol, ServiceType, TestService};
use crate::server::test::{protocol, ServiceType, TestService};
use crate::test_name;
#[actix_web::test]
#[tokio::test]
async fn serves_web_client() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::web_index();

View file

@ -1,76 +0,0 @@
use actix_web::{
dev::Service,
middleware::{Compress, Logger, NormalizePath},
web::{self, ServiceConfig},
App as ActixApp, HttpServer,
};
use log::error;
use crate::app::App;
mod api;
#[cfg(test)]
pub mod test;
pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
move |cfg: &mut ServiceConfig| {
cfg.app_data(web::Data::new(app.index))
.app_data(web::Data::new(app.config_manager))
.app_data(web::Data::new(app.ddns_manager))
.app_data(web::Data::new(app.lastfm_manager))
.app_data(web::Data::new(app.playlist_manager))
.app_data(web::Data::new(app.settings_manager))
.app_data(web::Data::new(app.thumbnail_manager))
.app_data(web::Data::new(app.user_manager))
.app_data(web::Data::new(app.vfs_manager))
.service(
web::scope("/api")
.configure(api::make_config())
.wrap(NormalizePath::trim()),
)
.service(
actix_files::Files::new("/swagger", app.swagger_dir_path)
.redirect_to_slash_directory()
.index_file("index.html"),
)
.service(
actix_files::Files::new("/", app.web_dir_path)
.redirect_to_slash_directory()
.index_file("index.html"),
);
}
}
pub fn launch(app: App) -> Result<(), std::io::Error> {
let address = ("0.0.0.0", app.port);
tokio::spawn(
HttpServer::new(move || {
ActixApp::new()
.wrap(Logger::default())
.wrap_fn(|req, srv| {
// For some reason, actix logs error as DEBUG level.
// This logs them as ERROR level
// See https://github.com/actix/actix-web/issues/2637
let response_future = srv.call(req);
async {
let response = response_future.await?;
if let Some(error) = response.response().error() {
error!("{}", error);
}
Ok(response)
}
})
.wrap(Compress::default())
.configure(make_config(app.clone()))
})
.disable_signals()
.bind(address)
.map_err(|e| {
error!("Error starting HTTP server: {:?}", e);
e
})?
.run(),
);
Ok(())
}

View file

@ -1,131 +0,0 @@
use actix_test::TestServer;
use actix_web::{
middleware::{Compress, Logger},
web::Bytes,
App as ActixApp,
};
use http::{response::Builder, Method, Request, Response};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::ops::Deref;
use crate::app::App;
use crate::paths::Paths;
use crate::service::actix::*;
use crate::service::dto;
use crate::service::test::TestService;
use crate::test::*;
pub struct ActixTestService {
authorization: Option<dto::Authorization>,
server: TestServer,
}
pub type ServiceType = ActixTestService;
impl ActixTestService {
async fn process_internal<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> (Builder, Option<Bytes>) {
let url = request.uri().to_string();
let body = request.body().clone();
let mut actix_request = match *request.method() {
Method::GET => self.server.get(url),
Method::POST => self.server.post(url),
Method::PUT => self.server.put(url),
Method::DELETE => self.server.delete(url),
_ => unimplemented!(),
}
.timeout(std::time::Duration::from_secs(30));
for (name, value) in request.headers() {
actix_request = actix_request.insert_header((name, value.clone()));
}
if let Some(ref authorization) = self.authorization {
actix_request = actix_request.bearer_auth(&authorization.token);
}
let mut actix_response = actix_request.send_json(&body).await.unwrap();
let mut response_builder = Response::builder().status(actix_response.status());
let headers = response_builder.headers_mut().unwrap();
for (name, value) in actix_response.headers().iter() {
headers.append(name, value.clone());
}
let is_success = actix_response.status().is_success();
let body = if is_success {
Some(actix_response.body().await.unwrap())
} else {
None
};
(response_builder, body)
}
}
impl TestService for ActixTestService {
async fn new(test_name: &str) -> Self {
let output_dir = prepare_test_directory(test_name);
let paths = Paths {
cache_dir_path: ["test-output", test_name].iter().collect(),
config_file_path: None,
db_file_path: output_dir.join("db.sqlite"),
#[cfg(unix)]
pid_file_path: output_dir.join("polaris.pid"),
log_file_path: None,
swagger_dir_path: ["docs", "swagger"].iter().collect(),
web_dir_path: ["test-data", "web"].iter().collect(),
};
let app = App::new(5050, paths).await.unwrap();
let server = actix_test::start(move || {
let config = make_config(app.clone());
ActixApp::new()
.wrap(Logger::default())
.wrap(Compress::default())
.configure(config)
});
ActixTestService {
authorization: None,
server,
}
}
async fn fetch<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> Response<()> {
let (response_builder, _body) = self.process_internal(request).await;
response_builder.body(()).unwrap()
}
async fn fetch_bytes<T: Serialize + Clone + 'static>(
&mut self,
request: &Request<T>,
) -> Response<Vec<u8>> {
let (response_builder, body) = self.process_internal(request).await;
response_builder
.body(body.unwrap().deref().to_owned())
.unwrap()
}
async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
&mut self,
request: &Request<T>,
) -> Response<U> {
let (response_builder, body) = self.process_internal(request).await;
let body = serde_json::from_slice(&body.unwrap()).unwrap();
response_builder.body(body).unwrap()
}
fn set_authorization(&mut self, authorization: Option<dto::Authorization>) {
self.authorization = authorization;
}
}