Service unit tests improvements (#103)

- Simpler API for TestService
- More granular tests
- Tests for authentication requirements
- Better error handling (and HTTP response codes) for various bad inputs
This commit is contained in:
Antoine Gersant 2020-11-30 01:26:55 -08:00 committed by GitHub
parent 1ffea255df
commit 847c26ddfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1414 additions and 593 deletions

38
Cargo.lock generated
View file

@ -982,6 +982,31 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "headers"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed18eb2459bf1a09ad2d6b1547840c3e5e62882fa09b9a6a20b1de8e3228848f"
dependencies = [
"base64 0.12.3",
"bitflags",
"bytes 0.5.6",
"headers-core",
"http 0.2.1",
"mime 0.3.16",
"sha-1",
"time 0.1.44",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http 0.2.1",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.17" version = "0.1.17"
@ -1819,6 +1844,7 @@ dependencies = [
"flame", "flame",
"flamer", "flamer",
"getopts", "getopts",
"headers",
"http 0.2.1", "http 0.2.1",
"id3", "id3",
"image", "image",
@ -2464,6 +2490,18 @@ dependencies = [
"url 1.7.2", "url 1.7.2",
] ]
[[package]]
name = "sha-1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
"block-buffer 0.7.3",
"digest 0.8.1",
"fake-simd",
"opaque-debug 0.2.3",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.6.0" version = "0.6.0"

View file

@ -64,6 +64,7 @@ unix-daemonize = "0.1.2"
percent-encoding = "2.1" percent-encoding = "2.1"
cookie = "0.14.0" cookie = "0.14.0"
http = "0.2.1" http = "0.2.1"
headers = "0.3"
[profile.release.build-override] [profile.release.build-override]
opt-level = 0 opt-level = 0

View file

@ -11,6 +11,20 @@ use crate::db::{directories, songs, DB};
use crate::index::*; use crate::index::*;
use crate::vfs::VFSSource; use crate::vfs::VFSSource;
#[derive(thiserror::Error, Debug)]
pub enum QueryError {
#[error("VFS path not found")]
VFSPathNotFound,
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for QueryError {
fn from(_: anyhow::Error) -> Self {
QueryError::Unspecified
}
}
no_arg_sql_function!( no_arg_sql_function!(
random, random,
sql_types::Integer, sql_types::Integer,
@ -47,7 +61,7 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option<Directory
Some(directory) Some(directory)
} }
pub fn browse<P>(db: &DB, virtual_path: P) -> Result<Vec<CollectionFile>> pub fn browse<P>(db: &DB, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -59,20 +73,24 @@ where
// Browse top-level // Browse top-level
let real_directories: Vec<Directory> = directories::table let real_directories: Vec<Directory> = directories::table
.filter(directories::parent.is_null()) .filter(directories::parent.is_null())
.load(&connection)?; .load(&connection)
.map_err(anyhow::Error::new)?;
let virtual_directories = real_directories let virtual_directories = real_directories
.into_iter() .into_iter()
.filter_map(|s| virtualize_directory(&vfs, s)); .filter_map(|s| virtualize_directory(&vfs, s));
output.extend(virtual_directories.map(CollectionFile::Directory)); output.extend(virtual_directories.map(CollectionFile::Directory));
} else { } else {
// Browse sub-directory // Browse sub-directory
let real_path = vfs.virtual_to_real(virtual_path)?; let real_path = vfs
.virtual_to_real(virtual_path)
.map_err(|_| QueryError::VFSPathNotFound)?;
let real_path_string = real_path.as_path().to_string_lossy().into_owned(); let real_path_string = real_path.as_path().to_string_lossy().into_owned();
let real_directories: Vec<Directory> = directories::table let real_directories: Vec<Directory> = directories::table
.filter(directories::parent.eq(&real_path_string)) .filter(directories::parent.eq(&real_path_string))
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC")) .order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
.load(&connection)?; .load(&connection)
.map_err(anyhow::Error::new)?;
let virtual_directories = real_directories let virtual_directories = real_directories
.into_iter() .into_iter()
.filter_map(|s| virtualize_directory(&vfs, s)); .filter_map(|s| virtualize_directory(&vfs, s));
@ -81,7 +99,8 @@ where
let real_songs: Vec<Song> = songs::table let real_songs: Vec<Song> = songs::table
.filter(songs::parent.eq(&real_path_string)) .filter(songs::parent.eq(&real_path_string))
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC")) .order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
.load(&connection)?; .load(&connection)
.map_err(anyhow::Error::new)?;
let virtual_songs = real_songs let virtual_songs = real_songs
.into_iter() .into_iter()
.filter_map(|s| virtualize_song(&vfs, s)); .filter_map(|s| virtualize_song(&vfs, s));
@ -91,7 +110,7 @@ where
Ok(output) Ok(output)
} }
pub fn flatten<P>(db: &DB, virtual_path: P) -> Result<Vec<Song>> pub fn flatten<P>(db: &DB, virtual_path: P) -> Result<Vec<Song>, QueryError>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -100,7 +119,9 @@ where
let connection = db.connect()?; let connection = db.connect()?;
let real_songs: Vec<Song> = if virtual_path.as_ref().parent() != None { let real_songs: Vec<Song> = if virtual_path.as_ref().parent() != None {
let real_path = vfs.virtual_to_real(virtual_path)?; let real_path = vfs
.virtual_to_real(virtual_path)
.map_err(|_| QueryError::VFSPathNotFound)?;
let song_path_filter = { let song_path_filter = {
let mut path_buf = real_path.clone(); let mut path_buf = real_path.clone();
path_buf.push("%"); path_buf.push("%");
@ -109,9 +130,13 @@ where
songs songs
.filter(path.like(&song_path_filter)) .filter(path.like(&song_path_filter))
.order(path) .order(path)
.load(&connection)? .load(&connection)
.map_err(anyhow::Error::new)?
} else { } else {
songs.order(path).load(&connection)? songs
.order(path)
.load(&connection)
.map_err(anyhow::Error::new)?
}; };
let virtual_songs = real_songs let virtual_songs = real_songs

View file

@ -5,6 +5,9 @@ use diesel::prelude::*;
use diesel::sql_types; use diesel::sql_types;
use diesel::BelongingToDsl; use diesel::BelongingToDsl;
use std::path::Path; use std::path::Path;
#[cfg(test)]
use std::path::PathBuf;
use thiserror::Error;
#[cfg(test)] #[cfg(test)]
use crate::db; use crate::db;
@ -13,6 +16,22 @@ use crate::db::{playlist_songs, playlists, users};
use crate::index::{self, Song}; use crate::index::{self, Song};
use crate::vfs::VFSSource; use crate::vfs::VFSSource;
#[derive(Error, Debug)]
pub enum PlaylistError {
#[error("User not found")]
UserNotFound,
#[error("Playlist not found")]
PlaylistNotFound,
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for PlaylistError {
fn from(_: anyhow::Error) -> Self {
PlaylistError::Unspecified
}
}
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "playlists"] #[table_name = "playlists"]
struct NewPlaylist { struct NewPlaylist {
@ -47,29 +66,36 @@ pub struct NewPlaylistSong {
ordering: i32, ordering: i32,
} }
pub fn list_playlists(owner: &str, db: &DB) -> Result<Vec<String>> { pub fn list_playlists(owner: &str, db: &DB) -> Result<Vec<String>, PlaylistError> {
let connection = db.connect()?; let connection = db.connect()?;
let user: User; let user: User = {
{
use self::users::dsl::*; use self::users::dsl::*;
user = users users
.filter(name.eq(owner)) .filter(name.eq(owner))
.select((id,)) .select((id,))
.first(&connection)?; .first(&connection)
} .optional()
.map_err(anyhow::Error::new)?
.ok_or(PlaylistError::UserNotFound)?
};
{ {
use self::playlists::dsl::*; use self::playlists::dsl::*;
let found_playlists: Vec<String> = Playlist::belonging_to(&user) let found_playlists: Vec<String> = Playlist::belonging_to(&user)
.select(name) .select(name)
.load(&connection)?; .load(&connection)
.map_err(anyhow::Error::new)?;
Ok(found_playlists) Ok(found_playlists)
} }
} }
pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &DB) -> Result<()> { pub fn save_playlist(
let user: User; playlist_name: &str,
owner: &str,
content: &[String],
db: &DB,
) -> Result<(), PlaylistError> {
let new_playlist: NewPlaylist; let new_playlist: NewPlaylist;
let playlist: Playlist; let playlist: Playlist;
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
@ -78,13 +104,16 @@ pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &
let connection = db.connect()?; let connection = db.connect()?;
// Find owner // Find owner
{ let user: User = {
use self::users::dsl::*; use self::users::dsl::*;
user = users users
.filter(name.eq(owner)) .filter(name.eq(owner))
.select((id,)) .select((id,))
.get_result(&connection)?; .first(&connection)
} .optional()
.map_err(anyhow::Error::new)?
.ok_or(PlaylistError::UserNotFound)?
};
// Create playlist // Create playlist
new_playlist = NewPlaylist { new_playlist = NewPlaylist {
@ -94,14 +123,16 @@ pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &
diesel::insert_into(playlists::table) diesel::insert_into(playlists::table)
.values(&new_playlist) .values(&new_playlist)
.execute(&connection)?; .execute(&connection)
.map_err(anyhow::Error::new)?;
{ playlist = {
use self::playlists::dsl::*; use self::playlists::dsl::*;
playlist = playlists playlists
.select((id, owner)) .select((id, owner))
.filter(name.eq(playlist_name).and(owner.eq(user.id))) .filter(name.eq(playlist_name).and(owner.eq(user.id)))
.get_result(&connection)?; .get_result(&connection)
.map_err(anyhow::Error::new)?
} }
} }
@ -125,48 +156,58 @@ pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &
{ {
let connection = db.connect()?; let connection = db.connect()?;
connection.transaction::<_, diesel::result::Error, _>(|| { connection
// Delete old content (if any) .transaction::<_, diesel::result::Error, _>(|| {
let old_songs = PlaylistSong::belonging_to(&playlist); // Delete old content (if any)
diesel::delete(old_songs).execute(&connection)?; let old_songs = PlaylistSong::belonging_to(&playlist);
diesel::delete(old_songs).execute(&connection)?;
// Insert content // Insert content
diesel::insert_into(playlist_songs::table) diesel::insert_into(playlist_songs::table)
.values(&new_songs) .values(&new_songs)
.execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 .execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
Ok(()) Ok(())
})?; })
.map_err(anyhow::Error::new)?;
} }
Ok(()) Ok(())
} }
pub fn read_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<Vec<Song>> { pub fn read_playlist(
playlist_name: &str,
owner: &str,
db: &DB,
) -> Result<Vec<Song>, PlaylistError> {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
let songs: Vec<Song>; let songs: Vec<Song>;
{ {
let connection = db.connect()?; let connection = db.connect()?;
let user: User;
let playlist: Playlist;
// Find owner // Find owner
{ let user: User = {
use self::users::dsl::*; use self::users::dsl::*;
user = users users
.filter(name.eq(owner)) .filter(name.eq(owner))
.select((id,)) .select((id,))
.get_result(&connection)?; .first(&connection)
} .optional()
.map_err(anyhow::Error::new)?
.ok_or(PlaylistError::UserNotFound)?
};
// Find playlist // Find playlist
{ let playlist: Playlist = {
use self::playlists::dsl::*; use self::playlists::dsl::*;
playlist = playlists playlists
.select((id, owner)) .select((id, owner))
.filter(name.eq(playlist_name).and(owner.eq(user.id))) .filter(name.eq(playlist_name).and(owner.eq(user.id)))
.get_result(&connection)?; .get_result(&connection)
} .optional()
.map_err(anyhow::Error::new)?
.ok_or(PlaylistError::PlaylistNotFound)?
};
// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column // Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
let query = diesel::sql_query( let query = diesel::sql_query(
@ -179,7 +220,7 @@ pub fn read_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<Vec<So
"#, "#,
); );
let query = query.clone().bind::<sql_types::Integer, _>(playlist.id); let query = query.clone().bind::<sql_types::Integer, _>(playlist.id);
songs = query.get_results(&connection)?; songs = query.get_results(&connection).map_err(anyhow::Error::new)?;
} }
// Map real path to virtual paths // Map real path to virtual paths
@ -191,25 +232,31 @@ pub fn read_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<Vec<So
Ok(virtual_songs) Ok(virtual_songs)
} }
pub fn delete_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<()> { pub fn delete_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<(), PlaylistError> {
let connection = db.connect()?; let connection = db.connect()?;
let user: User; let user: User = {
{
use self::users::dsl::*; use self::users::dsl::*;
user = users users
.filter(name.eq(owner)) .filter(name.eq(owner))
.select((id,)) .select((id,))
.first(&connection)?; .first(&connection)
} .optional()
.map_err(anyhow::Error::new)?
.ok_or(PlaylistError::UserNotFound)?
};
{ {
use self::playlists::dsl::*; use self::playlists::dsl::*;
let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name)); let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name));
diesel::delete(q).execute(&connection)?; match diesel::delete(q)
.execute(&connection)
.map_err(anyhow::Error::new)?
{
0 => Err(PlaylistError::PlaylistNotFound),
_ => Ok(()),
}
} }
Ok(())
} }
#[test] #[test]
@ -272,12 +319,9 @@ fn test_fill_playlist() {
assert_eq!(songs[0].title, Some("Above The Water".to_owned())); assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
assert_eq!(songs[13].title, Some("Above The Water".to_owned())); assert_eq!(songs[13].title, Some("Above The Water".to_owned()));
use std::path::PathBuf; let first_song_path: PathBuf = ["root", "Khemmis", "Hunted", "01 - Above The Water.mp3"]
let mut first_song_path = PathBuf::new(); .iter()
first_song_path.push("root"); .collect();
first_song_path.push("Khemmis");
first_song_path.push("Hunted");
first_song_path.push("01 - Above The Water.mp3");
assert_eq!(songs[0].path, first_song_path.to_str().unwrap()); assert_eq!(songs[0].path, first_song_path.to_str().unwrap());
// Save again to verify that we don't dupe the content // Save again to verify that we don't dupe the content

View file

@ -6,6 +6,12 @@ pub enum APIError {
IncorrectCredentials, IncorrectCredentials,
#[error("Cannot remove own admin privilege")] #[error("Cannot remove own admin privilege")]
OwnAdminPrivilegeRemoval, OwnAdminPrivilegeRemoval,
#[error("Path not found in virtual filesystem")]
VFSPathNotFound,
#[error("User not found")]
UserNotFound,
#[error("Playlist not found")]
PlaylistNotFound,
#[error("Unspecified")] #[error("Unspecified")]
Unspecified, Unspecified,
} }

View file

@ -15,10 +15,9 @@ use time::Duration;
use super::serve; use super::serve;
use crate::config::{self, Config, Preferences}; use crate::config::{self, Config, Preferences};
use crate::db::DB; use crate::db::DB;
use crate::index; use crate::index::{self, Index, QueryError};
use crate::index::Index;
use crate::lastfm; use crate::lastfm;
use crate::playlist; use crate::playlist::{self, PlaylistError};
use crate::service::constants::*; use crate::service::constants::*;
use crate::service::dto; use crate::service::dto;
use crate::service::error::APIError; use crate::service::error::APIError;
@ -62,12 +61,34 @@ impl<'r> rocket::response::Responder<'r> for APIError {
let status = match self { let status = match self {
APIError::IncorrectCredentials => rocket::http::Status::Unauthorized, APIError::IncorrectCredentials => rocket::http::Status::Unauthorized,
APIError::OwnAdminPrivilegeRemoval => rocket::http::Status::Conflict, APIError::OwnAdminPrivilegeRemoval => rocket::http::Status::Conflict,
APIError::VFSPathNotFound => rocket::http::Status::NotFound,
APIError::UserNotFound => rocket::http::Status::NotFound,
APIError::PlaylistNotFound => rocket::http::Status::NotFound,
APIError::Unspecified => rocket::http::Status::InternalServerError, APIError::Unspecified => rocket::http::Status::InternalServerError,
}; };
rocket::response::Response::build().status(status).ok() rocket::response::Response::build().status(status).ok()
} }
} }
impl From<PlaylistError> for APIError {
fn from(error: PlaylistError) -> APIError {
match error {
PlaylistError::PlaylistNotFound => APIError::PlaylistNotFound,
PlaylistError::UserNotFound => APIError::UserNotFound,
PlaylistError::Unspecified => APIError::Unspecified,
}
}
}
impl From<QueryError> for APIError {
fn from(error: QueryError) -> APIError {
match error {
QueryError::VFSPathNotFound => APIError::VFSPathNotFound,
QueryError::Unspecified => APIError::Unspecified,
}
}
}
struct Auth { struct Auth {
username: String, username: String,
} }
@ -280,7 +301,7 @@ fn browse(
db: State<'_, DB>, db: State<'_, DB>,
_auth: Auth, _auth: Auth,
path: VFSPathBuf, path: VFSPathBuf,
) -> Result<Json<Vec<index::CollectionFile>>> { ) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
let result = index::browse(db.deref().deref(), &path.into() as &PathBuf)?; let result = index::browse(db.deref().deref(), &path.into() as &PathBuf)?;
Ok(Json(result)) Ok(Json(result))
} }
@ -292,7 +313,11 @@ fn flatten_root(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::Song>>
} }
#[get("/flatten/<path>")] #[get("/flatten/<path>")]
fn flatten(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result<Json<Vec<index::Song>>> { fn flatten(
db: State<'_, DB>,
_auth: Auth,
path: VFSPathBuf,
) -> Result<Json<Vec<index::Song>>, APIError> {
let result = index::flatten(db.deref().deref(), &path.into() as &PathBuf)?; let result = index::flatten(db.deref().deref(), &path.into() as &PathBuf)?;
Ok(Json(result)) Ok(Json(result))
} }
@ -326,10 +351,16 @@ fn search(
} }
#[get("/audio/<path>")] #[get("/audio/<path>")]
fn audio(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result<serve::RangeResponder<File>> { fn audio(
db: State<'_, DB>,
_auth: Auth,
path: VFSPathBuf,
) -> Result<serve::RangeResponder<File>, APIError> {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
let real_path = vfs.virtual_to_real(&path.into() as &PathBuf)?; let real_path = vfs
let file = File::open(&real_path)?; .virtual_to_real(&path.into() as &PathBuf)
.map_err(|_| APIError::VFSPathNotFound)?;
let file = File::open(&real_path).map_err(|_| APIError::Unspecified)?;
Ok(serve::RangeResponder::new(file)) Ok(serve::RangeResponder::new(file))
} }
@ -340,13 +371,15 @@ fn thumbnail(
_auth: Auth, _auth: Auth,
path: VFSPathBuf, path: VFSPathBuf,
pad: Option<bool>, pad: Option<bool>,
) -> Result<File> { ) -> Result<File, APIError> {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
let image_path = vfs.virtual_to_real(&path.into() as &PathBuf)?; let image_path = vfs
.virtual_to_real(&path.into() as &PathBuf)
.map_err(|_| APIError::VFSPathNotFound)?;
let mut options = ThumbnailOptions::default(); let mut options = ThumbnailOptions::default();
options.pad_to_square = pad.unwrap_or(options.pad_to_square); options.pad_to_square = pad.unwrap_or(options.pad_to_square);
let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?; let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?;
let file = File::open(thumbnail_path)?; let file = File::open(thumbnail_path).map_err(|_| APIError::Unspecified)?;
Ok(file) Ok(file)
} }
@ -373,13 +406,17 @@ fn save_playlist(
} }
#[get("/playlist/<name>")] #[get("/playlist/<name>")]
fn read_playlist(db: State<'_, DB>, auth: Auth, name: String) -> Result<Json<Vec<index::Song>>> { fn read_playlist(
db: State<'_, DB>,
auth: Auth,
name: String,
) -> Result<Json<Vec<index::Song>>, APIError> {
let songs = playlist::read_playlist(&name, &auth.username, db.deref().deref())?; let songs = playlist::read_playlist(&name, &auth.username, db.deref().deref())?;
Ok(Json(songs)) Ok(Json(songs))
} }
#[delete("/playlist/<name>")] #[delete("/playlist/<name>")]
fn delete_playlist(db: State<'_, DB>, auth: Auth, name: String) -> Result<()> { fn delete_playlist(db: State<'_, DB>, auth: Auth, name: String) -> Result<(), APIError> {
playlist::delete_playlist(&name, &auth.username, db.deref().deref())?; playlist::delete_playlist(&name, &auth.username, db.deref().deref())?;
Ok(()) Ok(())
} }

View file

@ -1,65 +1,73 @@
use http::response::{Builder, Response}; use http::{header::HeaderName, method::Method, response::Builder, HeaderValue, Request, Response};
use http::{HeaderMap, HeaderValue};
use rocket; use rocket;
use rocket::local::Client; use rocket::local::{Client, LocalResponse};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::fs; use std::fs;
use std::ops::DerefMut;
use std::path::PathBuf; use std::path::PathBuf;
use super::server; use super::server;
use crate::db::DB; use crate::db::DB;
use crate::index; use crate::index;
use crate::service::test::TestService; use crate::service::test::{protocol, TestService};
use crate::thumbnails::ThumbnailsManager; use crate::thumbnails::ThumbnailsManager;
pub struct RocketResponse<'r, 's> {
response: &'s mut rocket::Response<'r>,
}
impl<'r, 's> RocketResponse<'r, 's> {
fn builder(&self) -> Builder {
let mut builder = Response::builder().status(self.response.status().code);
for header in self.response.headers().iter() {
builder = builder.header(header.name(), header.value());
}
builder
}
fn to_void(&self) -> Response<()> {
let builder = self.builder();
builder.body(()).unwrap()
}
fn to_bytes(&mut self) -> Response<Vec<u8>> {
let body = self.response.body().unwrap();
let body = body.into_bytes().unwrap();
let builder = self.builder();
builder.body(body).unwrap()
}
fn to_object<T: DeserializeOwned>(&mut self) -> Response<T> {
let body = self.response.body_string().unwrap();
let body = serde_json::from_str(&body).unwrap();
let builder = self.builder();
builder.body(body).unwrap()
}
}
pub struct RocketTestService { pub struct RocketTestService {
client: Client, client: Client,
request_builder: protocol::RequestBuilder,
} }
pub type ServiceType = RocketTestService; pub type ServiceType = RocketTestService;
impl RocketTestService {
fn process_internal<T: Serialize>(&mut self, request: &Request<T>) -> (LocalResponse, Builder) {
let rocket_response = {
let url = request.uri().to_string();
let mut rocket_request = match *request.method() {
Method::GET => self.client.get(url),
Method::POST => self.client.post(url),
Method::PUT => self.client.put(url),
Method::DELETE => self.client.delete(url),
_ => unimplemented!(),
};
for (name, value) in request.headers() {
rocket_request.add_header(rocket::http::Header::new(
name.as_str().to_owned(),
value.to_str().unwrap().to_owned(),
));
}
let payload = request.body();
let body = serde_json::to_string(payload).unwrap();
rocket_request.set_body(body);
let content_type = rocket::http::ContentType::JSON;
rocket_request.add_header(content_type);
rocket_request.dispatch()
};
let mut builder = Response::builder().status(rocket_response.status().code);
let headers = builder.headers_mut().unwrap();
for header in rocket_response.headers().iter() {
headers.append(
HeaderName::from_bytes(header.name.as_str().as_bytes()).unwrap(),
HeaderValue::from_str(header.value.as_ref()).unwrap(),
);
}
(rocket_response, builder)
}
}
impl TestService for RocketTestService { impl TestService for RocketTestService {
fn new(db_name: &str) -> Self { fn new(unique_db_name: &str) -> Self {
let mut db_path = PathBuf::new(); let mut db_path = PathBuf::new();
db_path.push("test-output"); db_path.push("test-output");
fs::create_dir_all(&db_path).unwrap(); fs::create_dir_all(&db_path).unwrap();
db_path.push(format!("{}.sqlite", db_name)); db_path.push(format!("{}.sqlite", unique_db_name));
if db_path.exists() { if db_path.exists() {
fs::remove_file(&db_path).unwrap(); fs::remove_file(&db_path).unwrap();
} }
@ -74,7 +82,7 @@ impl TestService for RocketTestService {
let mut thumbnails_path = PathBuf::new(); let mut thumbnails_path = PathBuf::new();
thumbnails_path.push("test-output"); thumbnails_path.push("test-output");
thumbnails_path.push("thumbnails"); thumbnails_path.push("thumbnails");
thumbnails_path.push(db_name); thumbnails_path.push(unique_db_name);
let thumbnails_manager = ThumbnailsManager::new(thumbnails_path.as_path()); let thumbnails_manager = ThumbnailsManager::new(thumbnails_path.as_path());
let auth_secret: [u8; 32] = [0; 32]; let auth_secret: [u8; 32] = [0; 32];
@ -93,72 +101,35 @@ impl TestService for RocketTestService {
) )
.unwrap(); .unwrap();
let client = Client::new(server).unwrap(); let client = Client::new(server).unwrap();
RocketTestService { client } let request_builder = protocol::RequestBuilder::new();
RocketTestService {
request_builder,
client,
}
} }
fn get(&mut self, url: &str) -> Response<()> { fn request_builder(&self) -> &protocol::RequestBuilder {
let mut response = self.client.get(url).dispatch(); &self.request_builder
RocketResponse {
response: response.deref_mut(),
}
.to_void()
} }
fn get_bytes(&mut self, url: &str, headers: &HeaderMap<HeaderValue>) -> Response<Vec<u8>> { fn fetch<T: Serialize>(&mut self, request: &Request<T>) -> Response<()> {
let mut request = self.client.get(url); let (_, builder) = self.process_internal(request);
for (name, value) in headers.iter() { builder.body(()).unwrap()
request.add_header(rocket::http::Header::new(
name.as_str().to_owned(),
value.to_str().unwrap().to_owned(),
))
}
let mut response = request.dispatch();
RocketResponse {
response: response.deref_mut(),
}
.to_bytes()
} }
fn post(&mut self, url: &str) -> Response<()> { fn fetch_bytes<T: Serialize>(&mut self, request: &Request<T>) -> Response<Vec<u8>> {
let mut response = self.client.post(url).dispatch(); let (mut rocket_response, builder) = self.process_internal(request);
RocketResponse { let body = rocket_response.body().unwrap().into_bytes().unwrap();
response: response.deref_mut(), builder.body(body).unwrap()
}
.to_void()
} }
fn delete(&mut self, url: &str) -> Response<()> { fn fetch_json<T: Serialize, U: DeserializeOwned>(
let mut response = self.client.delete(url).dispatch(); &mut self,
RocketResponse { request: &Request<T>,
response: response.deref_mut(), ) -> Response<U> {
} let (mut rocket_response, builder) = self.process_internal(request);
.to_void() let body = rocket_response.body_string().unwrap();
} let body = serde_json::from_str(&body).unwrap();
builder.body(body).unwrap()
fn get_json<T: DeserializeOwned>(&mut self, url: &str) -> Response<T> {
let mut response = self.client.get(url).dispatch();
RocketResponse {
response: response.deref_mut(),
}
.to_object()
}
fn put_json<T: Serialize>(&mut self, url: &str, payload: &T) -> Response<()> {
let client = &self.client;
let body = serde_json::to_string(payload).unwrap();
let mut response = client.put(url).body(&body).dispatch();
RocketResponse {
response: response.deref_mut(),
}
.to_void()
}
fn post_json<T: Serialize>(&mut self, url: &str, payload: &T) -> Response<()> {
let body = serde_json::to_string(payload).unwrap();
let mut response = self.client.post(url).body(&body).dispatch();
RocketResponse {
response: response.deref_mut(),
}
.to_void()
} }
} }

View file

@ -1,417 +0,0 @@
use cookie::Cookie;
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, index, vfs};
#[cfg(feature = "service-rocket")]
pub use crate::service::rocket::test::ServiceType;
const TEST_DB_PREFIX: &str = "service-test-";
const TEST_USERNAME: &str = "test_user";
const TEST_PASSWORD: &str = "test_password";
const TEST_MOUNT_NAME: &str = "collection";
const TEST_MOUNT_SOURCE: &str = "test-data/small-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<HeaderValue>) -> Response<Vec<u8>>;
fn post(&mut self, url: &str) -> Response<()>;
fn delete(&mut self, url: &str) -> Response<()>;
fn get_json<T: DeserializeOwned>(&mut self, url: &str) -> Response<T>;
fn put_json<T: Serialize>(&mut self, url: &str, payload: &T) -> Response<()>;
fn post_json<T: Serialize>(&mut self, url: &str, payload: &T) -> Response<()>;
fn complete_initial_setup(&mut self) {
let configuration = config::Config {
album_art_pattern: 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::<Vec<index::CollectionFile>>("/api/browse");
let entries = response.body();
if entries.len() > 0 {
break;
}
std::thread::sleep(Duration::from_secs(1));
}
loop {
let response = self.get_json::<Vec<index::Song>>("/api/flatten");
let entries = response.body();
if entries.len() > 0 {
break;
}
std::thread::sleep(Duration::from_secs(1));
}
}
}
#[test]
fn test_service_index() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.get("/");
}
#[test]
fn test_service_swagger_index() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
assert_eq!(
service.get("/swagger").status(),
StatusCode::PERMANENT_REDIRECT
);
}
#[test]
fn test_service_swagger_index_with_trailing_slash() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
assert_eq!(service.get("/swagger/").status(), StatusCode::OK);
}
#[test]
fn test_service_version() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
let response = service.get_json::<dto::Version>("/api/version");
let version = response.body();
assert_eq!(version, &dto::Version { major: 5, minor: 0 });
}
#[test]
fn test_service_initial_setup() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
{
let response = service.get_json::<dto::InitialSetup>("/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::<dto::InitialSetup>("/api/initial_setup");
let initial_setup = response.body();
assert_eq!(
initial_setup,
&dto::InitialSetup {
has_any_users: true
}
);
}
}
#[test]
fn test_service_settings() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
assert!(service.get("/api/settings").status() == StatusCode::UNAUTHORIZED);
service.login();
let response = service.get_json::<config::Config>("/api/settings");
assert_eq!(response.status(), StatusCode::OK);
let configuration = config::Config::default();
let response = service.put_json("/api/settings", &configuration);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_service_settings_cannot_unadmin_self() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
let mut configuration = config::Config::default();
configuration.users = Some(vec![config::ConfigUser {
name: TEST_USERNAME.into(),
password: "".into(),
admin: false,
}]);
let response = service.put_json("/api/settings", &configuration);
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[test]
fn test_service_preferences() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
let response = service.get_json::<config::Preferences>("/api/preferences");
assert_eq!(response.status(), StatusCode::OK);
let preferences = config::Preferences::default();
let response = service.put_json("/api/preferences", &preferences);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_service_trigger_index() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
let response = service.get_json::<Vec<index::Directory>>("/api/random");
let entries = response.body();
assert_eq!(entries.len(), 0);
service.index();
let response = service.get_json::<Vec<index::Directory>>("/api/random");
let entries = response.body();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_service_auth() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
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<Cookie> = 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));
}
}
#[test]
fn test_service_browse() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<index::CollectionFile>>("/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::<Vec<index::CollectionFile>>(&uri);
let entries = response.body();
assert_eq!(entries.len(), 5);
}
#[test]
fn test_service_flatten() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<index::Song>>("/api/flatten");
let entries = response.body();
assert_eq!(entries.len(), 13);
let response = service.get_json::<Vec<index::Song>>("/api/flatten/collection");
let entries = response.body();
assert_eq!(entries.len(), 13);
}
#[test]
fn test_service_random() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<index::Directory>>("/api/random");
let entries = response.body();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_service_recent() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<index::Directory>>("/api/recent");
let entries = response.body();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_service_search_root() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<index::CollectionFile>>("/api/search");
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_service_search() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<index::CollectionFile>>("/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!(),
}
}
#[test]
fn test_service_serve() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
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/audio/{}",
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");
}
}
#[test]
fn test_service_playlists() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let response = service.get_json::<Vec<dto::ListPlaylistsEntry>>("/api/playlists");
let playlists = response.body();
assert_eq!(playlists.len(), 0);
let response = service.get_json::<Vec<index::Song>>("/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::<Vec<dto::ListPlaylistsEntry>>("/api/playlists");
let playlists = response.body();
assert_eq!(
playlists,
&vec![dto::ListPlaylistsEntry {
name: "my_playlist".into()
}]
);
let response = service.get_json::<Vec<index::Song>>("/api/playlist/my_playlist");
let songs = response.body();
assert_eq!(songs, &my_songs);
service.delete("/api/playlist/my_playlist");
let response = service.get_json::<Vec<dto::ListPlaylistsEntry>>("/api/playlists");
let playlists = response.body();
assert_eq!(playlists.len(), 0);
}
#[test]
fn test_service_thumbnail() {
let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!()));
service.complete_initial_setup();
service.login();
service.index();
let mut path = PathBuf::new();
path.push("collection");
path.push("Khemmis");
path.push("Hunted");
path.push("Folder.jpg");
let uri = format!(
"/api/thumbnail/{}",
percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC)
);
let response = service.get(&uri);
assert_eq!(response.status(), StatusCode::OK);
}

81
src/service/test/admin.rs Normal file
View file

@ -0,0 +1,81 @@
use http::StatusCode;
use crate::index;
use crate::service::dto;
use crate::service::test::{ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_returns_api_version() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().version();
let response = service.fetch_json::<_, dto::Version>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_initial_setup_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().initial_setup();
{
let response = service.fetch_json::<_, dto::InitialSetup>(&request);
assert_eq!(response.status(), StatusCode::OK);
let initial_setup = response.body();
assert_eq!(
initial_setup,
&dto::InitialSetup {
has_any_users: false
}
);
}
service.complete_initial_setup();
{
let response = service.fetch_json::<_, dto::InitialSetup>(&request);
assert_eq!(response.status(), StatusCode::OK);
let initial_setup = response.body();
assert_eq!(
initial_setup,
&dto::InitialSetup {
has_any_users: true
}
);
}
}
#[test]
fn test_trigger_index_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
let request = service.request_builder().random();
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
let entries = response.body();
assert_eq!(entries.len(), 0);
service.index();
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
let entries = response.body();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_trigger_index_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let request = service.request_builder().trigger_index();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_trigger_index_requires_admin() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service.request_builder().trigger_index();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}

94
src/service/test/auth.rs Normal file
View file

@ -0,0 +1,94 @@
use cookie::Cookie;
use headers::{self, HeaderMapExt};
use http::{Response, StatusCode};
use crate::service::constants::*;
use crate::service::test::{constants::*, ServiceType, TestService};
use crate::unique_db_name;
fn validate_cookies<T>(response: &Response<T>) {
let cookies: Vec<Cookie> = response
.headers()
.get_all(http::header::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));
}
#[test]
fn test_login_rejects_bad_username() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let request = service.request_builder().login("garbage", TEST_PASSWORD);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_login_rejects_bad_password() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let request = service.request_builder().login(TEST_USERNAME, "garbage");
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_login_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let request = service
.request_builder()
.login(TEST_USERNAME, TEST_PASSWORD);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_cookies(&response);
}
#[test]
fn test_authentication_via_http_header_rejects_bad_username() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let mut request = service.request_builder().random();
let basic = headers::Authorization::basic("garbage", TEST_PASSWORD);
request.headers_mut().typed_insert(basic);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_authentication_via_http_header_rejects_bad_password() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let mut request = service.request_builder().random();
let basic = headers::Authorization::basic(TEST_PASSWORD, "garbage");
request.headers_mut().typed_insert(basic);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_authentication_via_http_header_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let mut request = service.request_builder().random();
let basic = headers::Authorization::basic(TEST_USERNAME, TEST_PASSWORD);
request.headers_mut().typed_insert(basic);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
validate_cookies(&response);
}

View file

@ -0,0 +1,194 @@
use http::StatusCode;
use std::path::{Path, PathBuf};
use crate::index;
use crate::service::test::{constants::*, ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_browse_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().browse(&PathBuf::new());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_browse_root() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let request = service.request_builder().browse(&PathBuf::new());
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
assert_eq!(response.status(), StatusCode::OK);
let entries = response.body();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_browse_directory() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
let request = service.request_builder().browse(&path);
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
assert_eq!(response.status(), StatusCode::OK);
let entries = response.body();
assert_eq!(entries.len(), 5);
}
#[test]
fn test_browse_bad_directory() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let path: PathBuf = ["not_my_collection"].iter().collect();
let request = service.request_builder().browse(&path);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn test_flatten_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().flatten(&PathBuf::new());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_flatten_root() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let request = service.request_builder().flatten(&PathBuf::new());
let response = service.fetch_json::<_, Vec<index::Song>>(&request);
assert_eq!(response.status(), StatusCode::OK);
let entries = response.body();
assert_eq!(entries.len(), 13);
}
#[test]
fn test_flatten_directory() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let request = service
.request_builder()
.flatten(Path::new(TEST_MOUNT_NAME));
let response = service.fetch_json::<_, Vec<index::Song>>(&request);
assert_eq!(response.status(), StatusCode::OK);
let entries = response.body();
assert_eq!(entries.len(), 13);
}
#[test]
fn test_flatten_bad_directory() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let path: PathBuf = ["not_my_collection"].iter().collect();
let request = service.request_builder().flatten(&path);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn test_random_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().random();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_random() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let request = service.request_builder().random();
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
assert_eq!(response.status(), StatusCode::OK);
let entries = response.body();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_recent_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().recent();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_recent() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let request = service.request_builder().recent();
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
assert_eq!(response.status(), StatusCode::OK);
let entries = response.body();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_search_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().search("");
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_search_without_query() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service.request_builder().search("");
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_search_with_query() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let request = service.request_builder().search("door");
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
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!(),
}
}

View file

@ -0,0 +1,7 @@
pub const TEST_USERNAME: &str = "test_user";
pub const TEST_PASSWORD: &str = "test_password";
pub const TEST_USERNAME_ADMIN: &str = "test_admin";
pub const TEST_PASSWORD_ADMIN: &str = "test_password_admin";
pub const TEST_MOUNT_NAME: &str = "collection";
pub const TEST_MOUNT_SOURCE: &str = "test-data/small-collection";
pub const TEST_PLAYLIST_NAME: &str = "my_playlist";

123
src/service/test/media.rs Normal file
View file

@ -0,0 +1,123 @@
use http::{header, HeaderValue, StatusCode};
use std::path::PathBuf;
use crate::service::test::{constants::*, ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_audio_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
.iter()
.collect();
let request = service.request_builder().audio(&path);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_audio_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
.iter()
.collect();
let request = service.request_builder().audio(&path);
let response = service.fetch_bytes(&request);
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.body().len(), 24_142);
}
#[test]
fn test_audio_partial_content() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
.iter()
.collect();
let mut request = service.request_builder().audio(&path);
let headers = request.headers_mut();
headers.append(
header::RANGE,
HeaderValue::from_str("bytes=100-299").unwrap(),
);
let response = service.fetch_bytes(&request);
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(response.body().len(), 200);
assert_eq!(
response.headers().get(header::CONTENT_LENGTH).unwrap(),
"200"
);
}
#[test]
fn test_audio_bad_path_returns_not_found() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let path: PathBuf = ["not_my_collection"].iter().collect();
let request = service.request_builder().audio(&path);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn test_thumbnail_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
.iter()
.collect();
let pad = None;
let request = service.request_builder().thumbnail(&path, pad);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_thumbnail_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
service.index();
service.login();
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
.iter()
.collect();
let pad = None;
let request = service.request_builder().thumbnail(&path, pad);
let response = service.fetch_bytes(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_thumbnail_bad_path_returns_not_found() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let path: PathBuf = ["not_my_collection"].iter().collect();
let pad = None;
let request = service.request_builder().thumbnail(&path, pad);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}

112
src/service/test/mod.rs Normal file
View file

@ -0,0 +1,112 @@
use http::{Request, Response, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::path::Path;
use std::time::Duration;
pub mod constants;
pub mod protocol;
mod admin;
mod auth;
mod collection;
mod media;
mod playlist;
mod preferences;
mod settings;
mod swagger;
mod web;
use crate::service::test::constants::*;
use crate::{config, index, vfs};
#[cfg(feature = "service-rocket")]
pub use crate::service::rocket::test::ServiceType;
#[macro_export]
macro_rules! unique_db_name {
() => {{
let file_name = file!();
let file_name = file_name.replace("/", "-");
let file_name = file_name.replace("\\", "-");
format!("{}-line-{}", file_name, line!())
}};
}
pub trait TestService {
fn new(unique_db_name: &str) -> Self;
fn request_builder(&self) -> &protocol::RequestBuilder;
fn fetch<T: Serialize>(&mut self, request: &Request<T>) -> Response<()>;
fn fetch_bytes<T: Serialize>(&mut self, request: &Request<T>) -> Response<Vec<u8>>;
fn fetch_json<T: Serialize, U: DeserializeOwned>(
&mut self,
request: &Request<T>,
) -> Response<U>;
fn complete_initial_setup(&mut self) {
let configuration = config::Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
ydns: None,
users: Some(vec![
config::ConfigUser {
name: TEST_USERNAME_ADMIN.into(),
password: TEST_PASSWORD_ADMIN.into(),
admin: true,
},
config::ConfigUser {
name: TEST_USERNAME.into(),
password: TEST_PASSWORD.into(),
admin: false,
},
]),
mount_dirs: Some(vec![vfs::MountPoint {
name: TEST_MOUNT_NAME.into(),
source: TEST_MOUNT_SOURCE.into(),
}]),
};
let request = self.request_builder().put_settings(configuration);
let response = self.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
fn login_admin(&mut self) {
let request = self
.request_builder()
.login(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN);
let response = self.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
fn login(&mut self) {
let request = self.request_builder().login(TEST_USERNAME, TEST_PASSWORD);
let response = self.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
fn index(&mut self) {
let request = self.request_builder().trigger_index();
let response = self.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
loop {
let browse_request = self.request_builder().browse(Path::new(""));
let response = self.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request);
let entries = response.body();
if entries.len() > 0 {
break;
}
std::thread::sleep(Duration::from_secs(1));
}
loop {
let flatten_request = self.request_builder().flatten(Path::new(""));
let response = self.fetch_json::<_, Vec<index::Song>>(&flatten_request);
let entries = response.body();
if entries.len() > 0 {
break;
}
std::thread::sleep(Duration::from_secs(1));
}
}
}

View file

@ -0,0 +1,133 @@
use http::StatusCode;
use crate::index;
use crate::service::dto;
use crate::service::test::{constants::*, ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_list_playlists_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().playlists();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_list_playlists_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service.request_builder().playlists();
let response = service.fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_save_playlist_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
let request = service
.request_builder()
.save_playlist(TEST_PLAYLIST_NAME, my_playlist);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_save_playlist_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
let request = service
.request_builder()
.save_playlist(TEST_PLAYLIST_NAME, my_playlist);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_get_playlist_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().read_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_get_playlist_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
{
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
let request = service
.request_builder()
.save_playlist(TEST_PLAYLIST_NAME, my_playlist);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
let request = service.request_builder().read_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch_json::<_, Vec<index::Song>>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_get_playlist_bad_name_returns_not_found() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service.request_builder().read_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn test_delete_playlist_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service
.request_builder()
.delete_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_delete_playlist_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
{
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
let request = service
.request_builder()
.save_playlist(TEST_PLAYLIST_NAME, my_playlist);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
let request = service
.request_builder()
.delete_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_delete_playlist_bad_name_returns_not_found() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service
.request_builder()
.delete_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}

View file

@ -0,0 +1,47 @@
use http::StatusCode;
use crate::config;
use crate::service::test::{ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_get_preferences_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().get_preferences();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_get_preferences_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service.request_builder().get_preferences();
let response = service.fetch_json::<_, config::Preferences>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_put_preferences_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service
.request_builder()
.put_preferences(config::Preferences::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_put_preferences_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service
.request_builder()
.put_preferences(config::Preferences::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}

View file

@ -0,0 +1,214 @@
use http::{method::Method, Request};
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use std::path::Path;
use crate::config;
use crate::service::dto;
pub struct RequestBuilder {}
impl RequestBuilder {
pub fn new() -> Self {
Self {}
}
pub fn web_index(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/")
.body(())
.unwrap()
}
pub fn swagger_index(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/swagger/")
.body(())
.unwrap()
}
pub fn version(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/version")
.body(())
.unwrap()
}
pub fn initial_setup(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/initial_setup")
.body(())
.unwrap()
}
pub fn login(&self, username: &str, password: &str) -> Request<dto::AuthCredentials> {
let credentials = dto::AuthCredentials {
username: username.into(),
password: password.into(),
};
Request::builder()
.method(Method::POST)
.uri("/api/auth")
.body(credentials)
.unwrap()
}
pub fn get_settings(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/settings")
.body(())
.unwrap()
}
pub fn put_settings(&self, configuration: config::Config) -> Request<config::Config> {
Request::builder()
.method(Method::PUT)
.uri("/api/settings")
.body(configuration)
.unwrap()
}
pub fn get_preferences(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/preferences")
.body(())
.unwrap()
}
pub fn put_preferences(
&self,
preferences: config::Preferences,
) -> Request<config::Preferences> {
Request::builder()
.method(Method::PUT)
.uri("/api/preferences")
.body(preferences)
.unwrap()
}
pub fn trigger_index(&self) -> Request<()> {
Request::builder()
.method(Method::POST)
.uri("/api/trigger_index")
.body(())
.unwrap()
}
pub fn browse(&self, path: &Path) -> Request<()> {
let path = path.to_string_lossy();
let uri = format!("/api/browse/{}", url_encode(path.as_ref()));
Request::builder()
.method(Method::GET)
.uri(uri)
.body(())
.unwrap()
}
pub fn flatten(&self, path: &Path) -> Request<()> {
let path = path.to_string_lossy();
let uri = format!("/api/flatten/{}", url_encode(path.as_ref()));
Request::builder()
.method(Method::GET)
.uri(uri)
.body(())
.unwrap()
}
pub fn random(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/random")
.body(())
.unwrap()
}
pub fn recent(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/recent")
.body(())
.unwrap()
}
pub fn search(&self, query: &str) -> Request<()> {
let uri = format!("/api/search/{}", url_encode(query));
Request::builder()
.method(Method::GET)
.uri(uri)
.body(())
.unwrap()
}
pub fn audio(&self, path: &Path) -> Request<()> {
let path = path.to_string_lossy();
let uri = format!("/api/audio/{}", url_encode(path.as_ref()));
Request::builder()
.method(Method::GET)
.uri(uri)
.body(())
.unwrap()
}
pub fn thumbnail(&self, path: &Path, pad: Option<bool>) -> Request<()> {
let path = path.to_string_lossy();
let mut uri = format!("/api/thumbnail/{}", url_encode(path.as_ref()));
match pad {
Some(true) => uri.push_str("?pad=true"),
Some(false) => uri.push_str("?pad=false"),
None => (),
};
Request::builder()
.method(Method::GET)
.uri(uri)
.body(())
.unwrap()
}
pub fn playlists(&self) -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/playlists")
.body(())
.unwrap()
}
pub fn save_playlist(
&self,
name: &str,
playlist: dto::SavePlaylistInput,
) -> Request<dto::SavePlaylistInput> {
let uri = format!("/api/playlist/{}", url_encode(name));
Request::builder()
.method(Method::PUT)
.uri(uri)
.body(playlist)
.unwrap()
}
pub fn read_playlist(&self, name: &str) -> Request<()> {
let uri = format!("/api/playlist/{}", url_encode(name));
Request::builder()
.method(Method::GET)
.uri(uri)
.body(())
.unwrap()
}
pub fn delete_playlist(&self, name: &str) -> Request<()> {
let uri = format!("/api/playlist/{}", url_encode(name));
Request::builder()
.method(Method::DELETE)
.uri(uri)
.body(())
.unwrap()
}
}
fn url_encode(input: &str) -> String {
percent_encode(input.as_bytes(), NON_ALPHANUMERIC).to_string()
}

View file

@ -0,0 +1,90 @@
use http::StatusCode;
use crate::config;
use crate::service::test::{constants::*, ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_get_settings_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let request = service.request_builder().get_settings();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_get_settings_requires_admin() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service.request_builder().get_settings();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_get_settings_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
let request = service.request_builder().get_settings();
let response = service.fetch_json::<_, config::Config>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_put_settings_requires_auth() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
let request = service
.request_builder()
.put_settings(config::Config::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_put_settings_requires_admin() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login();
let request = service
.request_builder()
.put_settings(config::Config::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_put_settings_golden_path() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
let request = service
.request_builder()
.put_settings(config::Config::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_put_settings_cannot_unadmin_self() {
let mut service = ServiceType::new(&unique_db_name!());
service.complete_initial_setup();
service.login_admin();
let mut configuration = config::Config::default();
configuration.users = Some(vec![config::ConfigUser {
name: TEST_USERNAME_ADMIN.into(),
password: "".into(),
admin: false,
}]);
let request = service.request_builder().put_settings(configuration);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::CONFLICT);
}

View file

@ -0,0 +1,12 @@
use http::StatusCode;
use crate::service::test::{ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_swagger_can_get_index() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().swagger_index();
let response = service.fetch_bytes(&request);
assert_eq!(response.status(), StatusCode::OK);
}

9
src/service/test/web.rs Normal file
View file

@ -0,0 +1,9 @@
use crate::service::test::{ServiceType, TestService};
use crate::unique_db_name;
#[test]
fn test_web_can_get_index() {
let mut service = ServiceType::new(&unique_db_name!());
let request = service.request_builder().web_index();
let _response = service.fetch_bytes(&request);
}