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:
parent
1ffea255df
commit
847c26ddfe
20 changed files with 1414 additions and 593 deletions
38
Cargo.lock
generated
38
Cargo.lock
generated
|
@ -982,6 +982,31 @@ version = "0.9.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.17"
|
||||
|
@ -1819,6 +1844,7 @@ dependencies = [
|
|||
"flame",
|
||||
"flamer",
|
||||
"getopts",
|
||||
"headers",
|
||||
"http 0.2.1",
|
||||
"id3",
|
||||
"image",
|
||||
|
@ -2464,6 +2490,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "sha1"
|
||||
version = "0.6.0"
|
||||
|
|
|
@ -64,6 +64,7 @@ unix-daemonize = "0.1.2"
|
|||
percent-encoding = "2.1"
|
||||
cookie = "0.14.0"
|
||||
http = "0.2.1"
|
||||
headers = "0.3"
|
||||
|
||||
[profile.release.build-override]
|
||||
opt-level = 0
|
||||
|
|
|
@ -11,6 +11,20 @@ use crate::db::{directories, songs, DB};
|
|||
use crate::index::*;
|
||||
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!(
|
||||
random,
|
||||
sql_types::Integer,
|
||||
|
@ -47,7 +61,7 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option<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
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
|
@ -59,20 +73,24 @@ where
|
|||
// Browse top-level
|
||||
let real_directories: Vec<Directory> = directories::table
|
||||
.filter(directories::parent.is_null())
|
||||
.load(&connection)?;
|
||||
.load(&connection)
|
||||
.map_err(anyhow::Error::new)?;
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
.filter_map(|s| virtualize_directory(&vfs, s));
|
||||
output.extend(virtual_directories.map(CollectionFile::Directory));
|
||||
} else {
|
||||
// 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_directories: Vec<Directory> = directories::table
|
||||
.filter(directories::parent.eq(&real_path_string))
|
||||
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
|
||||
.load(&connection)?;
|
||||
.load(&connection)
|
||||
.map_err(anyhow::Error::new)?;
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
.filter_map(|s| virtualize_directory(&vfs, s));
|
||||
|
@ -81,7 +99,8 @@ where
|
|||
let real_songs: Vec<Song> = songs::table
|
||||
.filter(songs::parent.eq(&real_path_string))
|
||||
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
|
||||
.load(&connection)?;
|
||||
.load(&connection)
|
||||
.map_err(anyhow::Error::new)?;
|
||||
let virtual_songs = real_songs
|
||||
.into_iter()
|
||||
.filter_map(|s| virtualize_song(&vfs, s));
|
||||
|
@ -91,7 +110,7 @@ where
|
|||
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
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
|
@ -100,7 +119,9 @@ where
|
|||
let connection = db.connect()?;
|
||||
|
||||
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 mut path_buf = real_path.clone();
|
||||
path_buf.push("%");
|
||||
|
@ -109,9 +130,13 @@ where
|
|||
songs
|
||||
.filter(path.like(&song_path_filter))
|
||||
.order(path)
|
||||
.load(&connection)?
|
||||
.load(&connection)
|
||||
.map_err(anyhow::Error::new)?
|
||||
} else {
|
||||
songs.order(path).load(&connection)?
|
||||
songs
|
||||
.order(path)
|
||||
.load(&connection)
|
||||
.map_err(anyhow::Error::new)?
|
||||
};
|
||||
|
||||
let virtual_songs = real_songs
|
||||
|
|
136
src/playlist.rs
136
src/playlist.rs
|
@ -5,6 +5,9 @@ use diesel::prelude::*;
|
|||
use diesel::sql_types;
|
||||
use diesel::BelongingToDsl;
|
||||
use std::path::Path;
|
||||
#[cfg(test)]
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::db;
|
||||
|
@ -13,6 +16,22 @@ use crate::db::{playlist_songs, playlists, users};
|
|||
use crate::index::{self, Song};
|
||||
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)]
|
||||
#[table_name = "playlists"]
|
||||
struct NewPlaylist {
|
||||
|
@ -47,29 +66,36 @@ pub struct NewPlaylistSong {
|
|||
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 user: User;
|
||||
{
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
user = users
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.first(&connection)?;
|
||||
}
|
||||
.first(&connection)
|
||||
.optional()
|
||||
.map_err(anyhow::Error::new)?
|
||||
.ok_or(PlaylistError::UserNotFound)?
|
||||
};
|
||||
|
||||
{
|
||||
use self::playlists::dsl::*;
|
||||
let found_playlists: Vec<String> = Playlist::belonging_to(&user)
|
||||
.select(name)
|
||||
.load(&connection)?;
|
||||
.load(&connection)
|
||||
.map_err(anyhow::Error::new)?;
|
||||
Ok(found_playlists)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &DB) -> Result<()> {
|
||||
let user: User;
|
||||
pub fn save_playlist(
|
||||
playlist_name: &str,
|
||||
owner: &str,
|
||||
content: &[String],
|
||||
db: &DB,
|
||||
) -> Result<(), PlaylistError> {
|
||||
let new_playlist: NewPlaylist;
|
||||
let playlist: Playlist;
|
||||
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()?;
|
||||
|
||||
// Find owner
|
||||
{
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
user = users
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.get_result(&connection)?;
|
||||
}
|
||||
.first(&connection)
|
||||
.optional()
|
||||
.map_err(anyhow::Error::new)?
|
||||
.ok_or(PlaylistError::UserNotFound)?
|
||||
};
|
||||
|
||||
// Create playlist
|
||||
new_playlist = NewPlaylist {
|
||||
|
@ -94,14 +123,16 @@ pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &
|
|||
|
||||
diesel::insert_into(playlists::table)
|
||||
.values(&new_playlist)
|
||||
.execute(&connection)?;
|
||||
.execute(&connection)
|
||||
.map_err(anyhow::Error::new)?;
|
||||
|
||||
{
|
||||
playlist = {
|
||||
use self::playlists::dsl::*;
|
||||
playlist = playlists
|
||||
playlists
|
||||
.select((id, owner))
|
||||
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
||||
.get_result(&connection)?;
|
||||
.get_result(&connection)
|
||||
.map_err(anyhow::Error::new)?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +156,8 @@ pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &
|
|||
|
||||
{
|
||||
let connection = db.connect()?;
|
||||
connection.transaction::<_, diesel::result::Error, _>(|| {
|
||||
connection
|
||||
.transaction::<_, diesel::result::Error, _>(|| {
|
||||
// Delete old content (if any)
|
||||
let old_songs = PlaylistSong::belonging_to(&playlist);
|
||||
diesel::delete(old_songs).execute(&connection)?;
|
||||
|
@ -135,38 +167,47 @@ pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &
|
|||
.values(&new_songs)
|
||||
.execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||
Ok(())
|
||||
})?;
|
||||
})
|
||||
.map_err(anyhow::Error::new)?;
|
||||
}
|
||||
|
||||
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 songs: Vec<Song>;
|
||||
|
||||
{
|
||||
let connection = db.connect()?;
|
||||
let user: User;
|
||||
let playlist: Playlist;
|
||||
|
||||
// Find owner
|
||||
{
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
user = users
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.get_result(&connection)?;
|
||||
}
|
||||
.first(&connection)
|
||||
.optional()
|
||||
.map_err(anyhow::Error::new)?
|
||||
.ok_or(PlaylistError::UserNotFound)?
|
||||
};
|
||||
|
||||
// Find playlist
|
||||
{
|
||||
let playlist: Playlist = {
|
||||
use self::playlists::dsl::*;
|
||||
playlist = playlists
|
||||
playlists
|
||||
.select((id, owner))
|
||||
.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
|
||||
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);
|
||||
songs = query.get_results(&connection)?;
|
||||
songs = query.get_results(&connection).map_err(anyhow::Error::new)?;
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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 user: User;
|
||||
{
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
user = users
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.first(&connection)?;
|
||||
}
|
||||
.first(&connection)
|
||||
.optional()
|
||||
.map_err(anyhow::Error::new)?
|
||||
.ok_or(PlaylistError::UserNotFound)?
|
||||
};
|
||||
|
||||
{
|
||||
use self::playlists::dsl::*;
|
||||
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]
|
||||
|
@ -272,12 +319,9 @@ fn test_fill_playlist() {
|
|||
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
|
||||
assert_eq!(songs[13].title, Some("Above The Water".to_owned()));
|
||||
|
||||
use std::path::PathBuf;
|
||||
let mut first_song_path = PathBuf::new();
|
||||
first_song_path.push("root");
|
||||
first_song_path.push("Khemmis");
|
||||
first_song_path.push("Hunted");
|
||||
first_song_path.push("01 - Above The Water.mp3");
|
||||
let first_song_path: PathBuf = ["root", "Khemmis", "Hunted", "01 - Above The Water.mp3"]
|
||||
.iter()
|
||||
.collect();
|
||||
assert_eq!(songs[0].path, first_song_path.to_str().unwrap());
|
||||
|
||||
// Save again to verify that we don't dupe the content
|
||||
|
|
|
@ -6,6 +6,12 @@ pub enum APIError {
|
|||
IncorrectCredentials,
|
||||
#[error("Cannot remove own admin privilege")]
|
||||
OwnAdminPrivilegeRemoval,
|
||||
#[error("Path not found in virtual filesystem")]
|
||||
VFSPathNotFound,
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
#[error("Playlist not found")]
|
||||
PlaylistNotFound,
|
||||
#[error("Unspecified")]
|
||||
Unspecified,
|
||||
}
|
||||
|
|
|
@ -15,10 +15,9 @@ use time::Duration;
|
|||
use super::serve;
|
||||
use crate::config::{self, Config, Preferences};
|
||||
use crate::db::DB;
|
||||
use crate::index;
|
||||
use crate::index::Index;
|
||||
use crate::index::{self, Index, QueryError};
|
||||
use crate::lastfm;
|
||||
use crate::playlist;
|
||||
use crate::playlist::{self, PlaylistError};
|
||||
use crate::service::constants::*;
|
||||
use crate::service::dto;
|
||||
use crate::service::error::APIError;
|
||||
|
@ -62,12 +61,34 @@ impl<'r> rocket::response::Responder<'r> for APIError {
|
|||
let status = match self {
|
||||
APIError::IncorrectCredentials => rocket::http::Status::Unauthorized,
|
||||
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,
|
||||
};
|
||||
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 {
|
||||
username: String,
|
||||
}
|
||||
|
@ -280,7 +301,7 @@ fn browse(
|
|||
db: State<'_, DB>,
|
||||
_auth: Auth,
|
||||
path: VFSPathBuf,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
let result = index::browse(db.deref().deref(), &path.into() as &PathBuf)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
@ -292,7 +313,11 @@ fn flatten_root(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::Song>>
|
|||
}
|
||||
|
||||
#[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)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
@ -326,10 +351,16 @@ fn search(
|
|||
}
|
||||
|
||||
#[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 real_path = vfs.virtual_to_real(&path.into() as &PathBuf)?;
|
||||
let file = File::open(&real_path)?;
|
||||
let real_path = vfs
|
||||
.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))
|
||||
}
|
||||
|
||||
|
@ -340,13 +371,15 @@ fn thumbnail(
|
|||
_auth: Auth,
|
||||
path: VFSPathBuf,
|
||||
pad: Option<bool>,
|
||||
) -> Result<File> {
|
||||
) -> Result<File, APIError> {
|
||||
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();
|
||||
options.pad_to_square = pad.unwrap_or(options.pad_to_square);
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -373,13 +406,17 @@ fn save_playlist(
|
|||
}
|
||||
|
||||
#[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())?;
|
||||
Ok(Json(songs))
|
||||
}
|
||||
|
||||
#[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())?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,65 +1,73 @@
|
|||
use http::response::{Builder, Response};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use http::{header::HeaderName, method::Method, response::Builder, HeaderValue, Request, Response};
|
||||
use rocket;
|
||||
use rocket::local::Client;
|
||||
use rocket::local::{Client, LocalResponse};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::server;
|
||||
use crate::db::DB;
|
||||
use crate::index;
|
||||
use crate::service::test::TestService;
|
||||
use crate::service::test::{protocol, TestService};
|
||||
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 {
|
||||
client: Client,
|
||||
request_builder: protocol::RequestBuilder,
|
||||
}
|
||||
|
||||
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 {
|
||||
fn new(db_name: &str) -> Self {
|
||||
fn new(unique_db_name: &str) -> Self {
|
||||
let mut db_path = PathBuf::new();
|
||||
db_path.push("test-output");
|
||||
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() {
|
||||
fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
|
@ -74,7 +82,7 @@ impl TestService for RocketTestService {
|
|||
let mut thumbnails_path = PathBuf::new();
|
||||
thumbnails_path.push("test-output");
|
||||
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 auth_secret: [u8; 32] = [0; 32];
|
||||
|
@ -93,72 +101,35 @@ impl TestService for RocketTestService {
|
|||
)
|
||||
.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<()> {
|
||||
let mut response = self.client.get(url).dispatch();
|
||||
RocketResponse {
|
||||
response: response.deref_mut(),
|
||||
}
|
||||
.to_void()
|
||||
fn request_builder(&self) -> &protocol::RequestBuilder {
|
||||
&self.request_builder
|
||||
}
|
||||
|
||||
fn get_bytes(&mut self, url: &str, headers: &HeaderMap<HeaderValue>) -> Response<Vec<u8>> {
|
||||
let mut request = self.client.get(url);
|
||||
for (name, value) in headers.iter() {
|
||||
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 fetch<T: Serialize>(&mut self, request: &Request<T>) -> Response<()> {
|
||||
let (_, builder) = self.process_internal(request);
|
||||
builder.body(()).unwrap()
|
||||
}
|
||||
|
||||
fn post(&mut self, url: &str) -> Response<()> {
|
||||
let mut response = self.client.post(url).dispatch();
|
||||
RocketResponse {
|
||||
response: response.deref_mut(),
|
||||
}
|
||||
.to_void()
|
||||
fn fetch_bytes<T: Serialize>(&mut self, request: &Request<T>) -> Response<Vec<u8>> {
|
||||
let (mut rocket_response, builder) = self.process_internal(request);
|
||||
let body = rocket_response.body().unwrap().into_bytes().unwrap();
|
||||
builder.body(body).unwrap()
|
||||
}
|
||||
|
||||
fn delete(&mut self, url: &str) -> Response<()> {
|
||||
let mut response = self.client.delete(url).dispatch();
|
||||
RocketResponse {
|
||||
response: response.deref_mut(),
|
||||
}
|
||||
.to_void()
|
||||
}
|
||||
|
||||
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()
|
||||
fn fetch_json<T: Serialize, U: DeserializeOwned>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> Response<U> {
|
||||
let (mut rocket_response, builder) = self.process_internal(request);
|
||||
let body = rocket_response.body_string().unwrap();
|
||||
let body = serde_json::from_str(&body).unwrap();
|
||||
builder.body(body).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
81
src/service/test/admin.rs
Normal 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
94
src/service/test/auth.rs
Normal 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);
|
||||
}
|
194
src/service/test/collection.rs
Normal file
194
src/service/test/collection.rs
Normal 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!(),
|
||||
}
|
||||
}
|
7
src/service/test/constants.rs
Normal file
7
src/service/test/constants.rs
Normal 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
123
src/service/test/media.rs
Normal 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
112
src/service/test/mod.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
133
src/service/test/playlist.rs
Normal file
133
src/service/test/playlist.rs
Normal 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);
|
||||
}
|
47
src/service/test/preferences.rs
Normal file
47
src/service/test/preferences.rs
Normal 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);
|
||||
}
|
214
src/service/test/protocol.rs
Normal file
214
src/service/test/protocol.rs
Normal 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()
|
||||
}
|
90
src/service/test/settings.rs
Normal file
90
src/service/test/settings.rs
Normal 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);
|
||||
}
|
12
src/service/test/swagger.rs
Normal file
12
src/service/test/swagger.rs
Normal 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
9
src/service/test/web.rs
Normal 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);
|
||||
}
|
Loading…
Add table
Reference in a new issue