Compare commits
	
		
			3 Commits
		
	
	
		
			9097a18926
			...
			be6e2a9540
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| be6e2a9540 | |||
| 4f38526dfb | |||
| cbe7bc51a6 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1 +1,2 @@
 | 
			
		||||
/target
 | 
			
		||||
.*
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,32 @@
 | 
			
		||||
use argh::FromArgs;
 | 
			
		||||
use tokio::task;
 | 
			
		||||
use qbittorrent_web_api::Api;
 | 
			
		||||
 | 
			
		||||
use crate::api::ApiClient;
 | 
			
		||||
use crate::Error;
 | 
			
		||||
use crate::action::ActionExec;
 | 
			
		||||
use crate::config::Config;
 | 
			
		||||
use crate::utils::torrent_to_magnet;
 | 
			
		||||
 | 
			
		||||
#[derive(FromArgs, PartialEq, Debug)]
 | 
			
		||||
#[argh(subcommand, name = "add")]
 | 
			
		||||
/// add a torrent to qBittorrent (only magnet for the moment)
 | 
			
		||||
/// add a torrent to qBittorrent (magnet or torrent file)
 | 
			
		||||
pub struct AddAction {
 | 
			
		||||
    #[argh(switch, short = 'p')]
 | 
			
		||||
    /// pause the torrent instead of starting immediately
 | 
			
		||||
    paused: bool,
 | 
			
		||||
 | 
			
		||||
    #[argh(positional)]
 | 
			
		||||
    /// the magnet link to add
 | 
			
		||||
    magnet: String,
 | 
			
		||||
    /// the torrent to add
 | 
			
		||||
    torrent: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ActionExec for AddAction {
 | 
			
		||||
    fn exec(&self, _config: &Config) -> Result<(), Error> {
 | 
			
		||||
        unimplemented!();
 | 
			
		||||
    fn exec(&self, config: &Config) -> Result<(), Error> {
 | 
			
		||||
        let magnet = if self.torrent.starts_with("magnet:") {
 | 
			
		||||
            self.torrent.clone()
 | 
			
		||||
        } else {
 | 
			
		||||
            torrent_to_magnet(&self.torrent)?
 | 
			
		||||
        };
 | 
			
		||||
        let api = ApiClient::from_config(&config)?;
 | 
			
		||||
        api.add(&magnet, self.paused)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,9 +18,9 @@ impl ActionExec for HashAction {
 | 
			
		||||
    fn exec(&self, _config: &Config) -> Result<(), Error> {
 | 
			
		||||
        // TODO errors
 | 
			
		||||
        let hash = if self.torrent.starts_with("magnet:") {
 | 
			
		||||
            magnet_hash(&self.torrent)
 | 
			
		||||
            magnet_hash(&self.torrent)?
 | 
			
		||||
        } else {
 | 
			
		||||
            torrent_hash(&self.torrent)
 | 
			
		||||
            torrent_hash(&self.torrent)?
 | 
			
		||||
        };
 | 
			
		||||
        println!("{}", hash);
 | 
			
		||||
        Ok(())
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ impl ActionExec for MagnetAction {
 | 
			
		||||
        if self.torrent.starts_with("magnet:") {
 | 
			
		||||
            println!("{}", &self.torrent);
 | 
			
		||||
        } else {
 | 
			
		||||
            println!("{}", torrent_to_magnet(&self.torrent));
 | 
			
		||||
            println!("{}", torrent_to_magnet(&self.torrent)?);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
use argh::FromArgs;
 | 
			
		||||
use snafu::prelude::*;
 | 
			
		||||
 | 
			
		||||
use crate::Error;
 | 
			
		||||
use crate::action::ActionExec;
 | 
			
		||||
use crate::config::Config;
 | 
			
		||||
use crate::utils::{magnet_name, torrent_name};
 | 
			
		||||
use crate::error::EmptyNameMagnetSnafu as EmptyNameMagnet;
 | 
			
		||||
 | 
			
		||||
#[derive(FromArgs, PartialEq, Debug)]
 | 
			
		||||
#[argh(subcommand, name = "name")]
 | 
			
		||||
@ -17,9 +19,10 @@ pub struct NameAction {
 | 
			
		||||
impl ActionExec for NameAction {
 | 
			
		||||
    fn exec(&self, _config: &Config) -> Result<(), Error> {
 | 
			
		||||
        let name = if self.torrent.starts_with("magnet:") {
 | 
			
		||||
            magnet_name(&self.torrent)
 | 
			
		||||
            magnet_name(&self.torrent)?
 | 
			
		||||
                .context(EmptyNameMagnet { magnet: &self.torrent })?
 | 
			
		||||
        } else {
 | 
			
		||||
            torrent_name(&self.torrent)
 | 
			
		||||
            torrent_name(&self.torrent)?
 | 
			
		||||
        };
 | 
			
		||||
        println!("{}", name);
 | 
			
		||||
        Ok(())
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										145
									
								
								src/api.rs
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								src/api.rs
									
									
									
									
									
								
							@ -1,34 +1,109 @@
 | 
			
		||||
use snafu::ResultExt;
 | 
			
		||||
use qbittorrent_web_api::Api;
 | 
			
		||||
use qbittorrent_web_api::api_impl::Error as QBittorrentError;
 | 
			
		||||
use tokio::runtime::Builder;
 | 
			
		||||
 | 
			
		||||
use crate::config::Config;
 | 
			
		||||
use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError};
 | 
			
		||||
 | 
			
		||||
pub struct ApiClient {
 | 
			
		||||
pub fn blocking_runtime() -> std::io::Result<tokio::runtime::Runtime> {
 | 
			
		||||
    Builder::new_current_thread()
 | 
			
		||||
        .enable_all()
 | 
			
		||||
        .build()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct UnauthenticatedRawApiClient {
 | 
			
		||||
    pub rt: tokio::runtime::Runtime,
 | 
			
		||||
    pub api: qbittorrent_web_api::api_impl::Authenticated
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UnauthenticatedRawApiClient {
 | 
			
		||||
    /// Initialize a blocking runtime for the API client
 | 
			
		||||
    pub fn new() -> Result<UnauthenticatedRawApiClient, std::io::Error> {
 | 
			
		||||
        Ok(UnauthenticatedRawApiClient {
 | 
			
		||||
            rt: blocking_runtime()?,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Login into a qBittorrent backend and return a proper ApiClient instance
 | 
			
		||||
    pub fn login(self, host: &str, login: &str, password: &str) -> Result<RawApiClient, QBittorrentError> {
 | 
			
		||||
        let api = self.rt.block_on(Api::login(host, login, password))?;
 | 
			
		||||
        Ok(RawApiClient {
 | 
			
		||||
            rt: self.rt,
 | 
			
		||||
            api,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct RawApiClient {
 | 
			
		||||
    rt: tokio::runtime::Runtime,
 | 
			
		||||
    api: qbittorrent_web_api::api_impl::Authenticated,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RawApiClient {
 | 
			
		||||
    pub fn add(&self, magnet: &str, paused: bool) -> Result<String, QBittorrentError> {
 | 
			
		||||
        let base_call = self.api.torrent_management();
 | 
			
		||||
        let call = if paused {
 | 
			
		||||
            base_call.add(magnet).paused("true")
 | 
			
		||||
        } else {
 | 
			
		||||
            base_call.add(magnet)
 | 
			
		||||
        };
 | 
			
		||||
        self.rt.block_on(call.send_raw())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self, hash: &str) -> Result<String, QBittorrentError> {
 | 
			
		||||
        self.rt.block_on(self.api.torrent_management().properties_raw(hash))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn list(&self) -> Result<String, QBittorrentError> {
 | 
			
		||||
        self.rt.block_on(self.api.torrent_management().info().send_raw())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
/// ApiClient is a convenience struct around qbittorrent_web_api for use in async programs, using qbt::Error error types.
 | 
			
		||||
pub struct ApiClient {
 | 
			
		||||
    pub raw_api: RawApiClient,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ApiClient {
 | 
			
		||||
    /// Login to a qBittorrent backend and return a handle to the ApiClient
 | 
			
		||||
    pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
 | 
			
		||||
        let unauthenticated = UnauthenticatedRawApiClient::new().context(IOError)?;
 | 
			
		||||
        let authenticated = unauthenticated.login(host, login, password).map_err(|e| {
 | 
			
		||||
            match e {
 | 
			
		||||
                QBittorrentError::HttpError(_) => {
 | 
			
		||||
                    Error::FailedToReachAPI { source: e }
 | 
			
		||||
                }, QBittorrentError::InvalidUsernameOrPassword => {
 | 
			
		||||
                    Error::FailedLogin { user: login.to_string() }
 | 
			
		||||
                } _ => {
 | 
			
		||||
                    panic!("Cookie error");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })?;
 | 
			
		||||
        Ok(ApiClient {
 | 
			
		||||
            raw_api: authenticated,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_config(config: &Config) -> Result<ApiClient, Error> {
 | 
			
		||||
        Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
 | 
			
		||||
        let rt = Builder::new_current_thread()
 | 
			
		||||
            .enable_all()
 | 
			
		||||
            .build().context(IOError)?;
 | 
			
		||||
        // Call the asynchronous connect method using the runtime.
 | 
			
		||||
        let api = rt.block_on(Api::login(host, &login, &password)).context(ApiError)?;
 | 
			
		||||
        Ok(ApiClient {
 | 
			
		||||
            rt,
 | 
			
		||||
            api,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
 | 
			
		||||
        let res = self.raw_api.add(magnet, paused).context(ApiError)?;
 | 
			
		||||
        if res == "Ok." {
 | 
			
		||||
            Ok(())
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(Error::message(res.clone()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self, hash: &str) -> Result<Option<String>, Error> {
 | 
			
		||||
        let res = self.rt.block_on(self.api.torrent_management().properties_raw(hash)).context(ApiError)?;
 | 
			
		||||
        let res = self.raw_api.get(hash).context(ApiError)?;
 | 
			
		||||
        if res == "" {
 | 
			
		||||
            Ok(None)
 | 
			
		||||
        } else {
 | 
			
		||||
@ -37,12 +112,50 @@ impl ApiClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn list(&self) -> Result<String, Error> {
 | 
			
		||||
        Ok(
 | 
			
		||||
            self.rt.block_on(self.api.torrent_management().info().send_raw()).context(ApiError)?
 | 
			
		||||
        )
 | 
			
		||||
        self.raw_api.list().context(ApiError)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: typestate builder https://www.greyblake.com/blog/builder-with-typestate-in-rust/
 | 
			
		||||
//struct ApiClientBuilder {}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use crate::utils::*;
 | 
			
		||||
    use crate::api::{ApiClient, UnauthenticatedRawApiClient, QBittorrentError};
 | 
			
		||||
    use crate::error::Error;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn raw_wrong_server() {
 | 
			
		||||
        let port = find_free_port();
 | 
			
		||||
        let api = UnauthenticatedRawApiClient::new().expect("IOERROR");
 | 
			
		||||
        let login = api.login(&format!("http://localhost:{}", &port), "admin", "adminadmin");
 | 
			
		||||
        assert!(login.is_err());
 | 
			
		||||
        let err = login.unwrap_err();
 | 
			
		||||
        match &err {
 | 
			
		||||
            QBittorrentError::HttpError(_) => {
 | 
			
		||||
                return;
 | 
			
		||||
            }, _ => {
 | 
			
		||||
                println!("{:?}", err);
 | 
			
		||||
                panic!("API CHANGE!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn wrong_server() {
 | 
			
		||||
        let port = find_free_port();
 | 
			
		||||
        let api = ApiClient::new(&format!("http://localhost:{}", &port), "admin", "adminadmin");
 | 
			
		||||
        assert!(api.is_err());
 | 
			
		||||
        let err = api.unwrap_err();
 | 
			
		||||
        match &err {
 | 
			
		||||
            Error::FailedToReachAPI { source: _ }  => {
 | 
			
		||||
                return;
 | 
			
		||||
            }, _ => {
 | 
			
		||||
                println!("{:?}", err);
 | 
			
		||||
                panic!("ERROR CONVERSION PROBLEM");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,16 @@
 | 
			
		||||
use argh::FromArgs;
 | 
			
		||||
 | 
			
		||||
use std::path::{PathBuf};
 | 
			
		||||
 | 
			
		||||
use crate::action::Action;
 | 
			
		||||
 | 
			
		||||
#[derive(FromArgs, Debug)]
 | 
			
		||||
/// interact with your qBittorrent instance from the command line
 | 
			
		||||
pub struct Cli {
 | 
			
		||||
    #[argh(option, short = 'c')]
 | 
			
		||||
    /// path to config file (defaults to ~/.config/qbt/.qbt.toml)
 | 
			
		||||
    pub config: Option<PathBuf>,
 | 
			
		||||
 | 
			
		||||
    #[argh(subcommand)]
 | 
			
		||||
    pub command: Action,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ use xdg::{BaseDirectories, BaseDirectoriesError};
 | 
			
		||||
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
 | 
			
		||||
use crate::cli::Cli;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug)]
 | 
			
		||||
pub struct Config {
 | 
			
		||||
    pub qbittorrent: ClientConfig,
 | 
			
		||||
@ -46,7 +48,15 @@ impl Config {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_default_path() -> Result<Config, ConfigError> {
 | 
			
		||||
        Ok(Self::from_path(&Self::default_path()?)?)
 | 
			
		||||
        Self::from_path(&Self::default_path()?)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_cli(cli: &Cli) -> Result<Config, ConfigError> {
 | 
			
		||||
        if let Some(cfg_path) = &cli.config {
 | 
			
		||||
            Self::from_path(cfg_path)
 | 
			
		||||
        } else {
 | 
			
		||||
            Self::from_default_path()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn format_host(&self) -> String {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								src/error.rs
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								src/error.rs
									
									
									
									
									
								
							@ -1,19 +1,45 @@
 | 
			
		||||
use snafu::prelude::*;
 | 
			
		||||
//use snafu::*;
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use crate::config::ConfigError;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Snafu)]
 | 
			
		||||
#[snafu(visibility(pub))]
 | 
			
		||||
/// The possible error cases
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    #[snafu(display("Configuration file reading or parsing error"))]
 | 
			
		||||
    #[snafu(display("{}\nConfiguration file reading or parsing error (see above)", source))]
 | 
			
		||||
    Config { source: ConfigError },
 | 
			
		||||
    #[snafu(display("Torrent/magnet reading or parsing error"))]
 | 
			
		||||
    #[snafu(display("{}\nTorrent/magnet reading or parsing error", source))]
 | 
			
		||||
    Imdl { source: imdl::error::Error },
 | 
			
		||||
    #[snafu(display("qBittorrent API communication error"))]
 | 
			
		||||
    #[snafu(display("{}\nqBittorrent API communication error", source))]
 | 
			
		||||
    Api { source: std::io::Error },
 | 
			
		||||
    #[snafu(display("Internal qBittorrent API error"))]
 | 
			
		||||
    #[snafu(display("{}\nInternal qBittorrent API error", source))]
 | 
			
		||||
    InternalApi { source: qbittorrent_web_api::api_impl::Error },
 | 
			
		||||
    //GenericIOError(std::io::Error),
 | 
			
		||||
    #[snafu(display("Other error:\n{}", message))]
 | 
			
		||||
    Message { message: String },
 | 
			
		||||
    // New and better error messages
 | 
			
		||||
    #[snafu(display("Failed to reach qBittorrent API:\n{}", source))]
 | 
			
		||||
    FailedToReachAPI { source: qbittorrent_web_api::api_impl::Error },
 | 
			
		||||
    #[snafu(display("Failed to login to qBittorrent API with user: {}", user))]
 | 
			
		||||
    FailedLogin { user: String },
 | 
			
		||||
    #[snafu(display("Invalid torrent: {}", torrent.display()))]
 | 
			
		||||
    InvalidTorrent { torrent: PathBuf, source: imdl::error::Error },
 | 
			
		||||
    #[snafu(display("Invalid magnet: {}", magnet))]
 | 
			
		||||
    InvalidMagnet { magnet: String, source: imdl::magnet_link_parse_error::MagnetLinkParseError },
 | 
			
		||||
    #[snafu(display("Other torrent error inside imdl library with file {}:\n{}", torrent.display(), source))]
 | 
			
		||||
    OtherTorrent { torrent: PathBuf, source: imdl::error::Error },
 | 
			
		||||
    #[snafu(display("The magnet link contains no name: {}", magnet))]
 | 
			
		||||
    EmptyNameMagnet { magnet: String },
 | 
			
		||||
    #[snafu(display("Failed to read file {} because of underlying IO error:\n{}", path.display(), source))]
 | 
			
		||||
    FailedToReadFile { path: PathBuf, source: std::io::Error },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Error {
 | 
			
		||||
  pub fn message(message: impl Into<String>) -> Error {
 | 
			
		||||
    Self::Message {
 | 
			
		||||
      message: message.into(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							@ -10,11 +10,20 @@ mod error;
 | 
			
		||||
use crate::error::{Error, ConfigSnafu as ConfigError};
 | 
			
		||||
mod utils;
 | 
			
		||||
 | 
			
		||||
fn main() -> Result<(), Error> {
 | 
			
		||||
    let config = Config::from_default_path().context(ConfigError)?;
 | 
			
		||||
fn fallible_main() -> Result<(), Error> {
 | 
			
		||||
    let cli = Cli::from_args();
 | 
			
		||||
    let config = Config::from_cli(&cli).context(ConfigError)?;
 | 
			
		||||
    // TODO: Make ActionExec type return Option<String>? where None means no data was found and so we abort with a proper exit code
 | 
			
		||||
    cli.command.exec(&config)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    let res = fallible_main();
 | 
			
		||||
    if res.is_err() {
 | 
			
		||||
        let err = res.unwrap_err();
 | 
			
		||||
        eprintln!("{}", err);
 | 
			
		||||
        std::process::exit(1);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								src/utils.rs
									
									
									
									
									
								
							@ -1,3 +1,5 @@
 | 
			
		||||
use snafu::prelude::*;
 | 
			
		||||
 | 
			
		||||
use imdl::infohash::Infohash;
 | 
			
		||||
use imdl::input::Input;
 | 
			
		||||
use imdl::input_target::InputTarget;
 | 
			
		||||
@ -6,12 +8,15 @@ use imdl::metainfo::Metainfo;
 | 
			
		||||
use imdl::torrent_summary::TorrentSummary;
 | 
			
		||||
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
use std::net::TcpListener;
 | 
			
		||||
 | 
			
		||||
use crate::error::{Error, InvalidMagnetSnafu as InvalidMagnet, InvalidTorrentSnafu as InvalidTorrent, FailedToReadFileSnafu as FailedToReadFile, OtherTorrentSnafu as OtherTorrentError};
 | 
			
		||||
 | 
			
		||||
/// Helper method for imdl functions which expect an imdl::Input type
 | 
			
		||||
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
 | 
			
		||||
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, Error> {
 | 
			
		||||
    let path = path.as_ref();
 | 
			
		||||
    let absolute = path.canonicalize()?;
 | 
			
		||||
    let data = std::fs::read(absolute)?;
 | 
			
		||||
    let absolute = path.canonicalize().context(FailedToReadFile { path: path.to_path_buf() })?;
 | 
			
		||||
    let data = std::fs::read(absolute).context(FailedToReadFile { path: path.to_path_buf() })?;
 | 
			
		||||
    Ok(Input::new(
 | 
			
		||||
        InputTarget::Path(path.to_path_buf()),
 | 
			
		||||
        data
 | 
			
		||||
@ -19,50 +24,62 @@ pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Extracts the infohash of a magnet link
 | 
			
		||||
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> String {
 | 
			
		||||
    let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
 | 
			
		||||
    //Ok(magnet.name.expect("Magnet link has no name!"))
 | 
			
		||||
    //Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string())
 | 
			
		||||
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> Result<String, Error> {
 | 
			
		||||
    let magnet = magnet.as_ref();
 | 
			
		||||
    let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet: magnet.to_string() })?;
 | 
			
		||||
    let bytes = magnet.infohash.inner.bytes.clone();
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
    for byte in bytes {
 | 
			
		||||
        s.push_str(&format!("{:02x}", byte));
 | 
			
		||||
    }
 | 
			
		||||
    s
 | 
			
		||||
    Ok(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Extracts the name of a magnet link
 | 
			
		||||
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> String {
 | 
			
		||||
    let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
 | 
			
		||||
    magnet.name.expect("Magnet link has no name!")
 | 
			
		||||
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> Result<Option<String>, Error> {
 | 
			
		||||
    let magnet = magnet.as_ref();
 | 
			
		||||
    let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet })?;
 | 
			
		||||
    Ok(magnet.name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Extracts the infohash of a torrent file
 | 
			
		||||
pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> String {
 | 
			
		||||
    let input = input_path(torrent).unwrap();
 | 
			
		||||
    TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash
 | 
			
		||||
pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
 | 
			
		||||
    let torrent = torrent.as_ref();
 | 
			
		||||
    let input = input_path(torrent)?;
 | 
			
		||||
    let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?;
 | 
			
		||||
    Ok(summary.torrent_summary_data().info_hash)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Extracts the name of a torrent file
 | 
			
		||||
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> String {
 | 
			
		||||
    let input = input_path(torrent).unwrap();
 | 
			
		||||
    TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().name
 | 
			
		||||
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
 | 
			
		||||
    let torrent = torrent.as_ref();
 | 
			
		||||
    let input = input_path(torrent)?;
 | 
			
		||||
    let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?;
 | 
			
		||||
    Ok(summary.torrent_summary_data().name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Turns a torrent file into a magnet link
 | 
			
		||||
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> String {
 | 
			
		||||
    let input = input_path(torrent).expect("Failed to read torrent file");
 | 
			
		||||
    let infohash = Infohash::from_input(&input).expect("Failed to parse infohash");
 | 
			
		||||
    let metainfo = Metainfo::from_input(&input).expect("Failed to parse meta info");
 | 
			
		||||
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
 | 
			
		||||
    let torrent = torrent.as_ref();
 | 
			
		||||
    let input = input_path(torrent)?;
 | 
			
		||||
    let infohash = Infohash::from_input(&input).context(InvalidTorrent { torrent })?;
 | 
			
		||||
    let metainfo = Metainfo::from_input(&input).context(InvalidTorrent { torrent })?;
 | 
			
		||||
 | 
			
		||||
    let mut link = MagnetLink::with_infohash(infohash);
 | 
			
		||||
 | 
			
		||||
    link.set_name(&metainfo.info.name);
 | 
			
		||||
 | 
			
		||||
    for result in metainfo.trackers() {
 | 
			
		||||
      link.add_tracker(result.expect("failed tracker in metainfo"));
 | 
			
		||||
        let result = result.context(OtherTorrentError { torrent })?;
 | 
			
		||||
      link.add_tracker(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    link.to_url().to_string()
 | 
			
		||||
    Ok(link.to_url().to_string())
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Fallible: only used in tests
 | 
			
		||||
pub fn find_free_port() -> u16 {
 | 
			
		||||
    let bind = TcpListener::bind("127.0.0.1:0").unwrap();
 | 
			
		||||
    bind.local_addr().unwrap().port()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								tests/default.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/default.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
[qbittorrent]
 | 
			
		||||
host     = "127.0.0.1" # qbittorrent webui-api hostname/ip
 | 
			
		||||
port     = $FREEPORT        # qbittorrent webui-api port
 | 
			
		||||
login    = "admin"      # qbittorrent webui-api user
 | 
			
		||||
password = "adminadmin"  # qbittorrent webui-api password
 | 
			
		||||
							
								
								
									
										85
									
								
								tests/qbittorrent-nox.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										85
									
								
								tests/qbittorrent-nox.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
#! /usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
# qbittorrent-nox.sh start|stop DIR PORT
 | 
			
		||||
#   DIR will contain the qbittorrent-nox profile, as well as "pid" and "qBottorrent.log"
 | 
			
		||||
#   PORT configures port for qbittorrent-nox web API
 | 
			
		||||
 | 
			
		||||
# Wait for qBittorrent WEB API to come online at address $1
 | 
			
		||||
# waitforqBittorrentStart "http://localhost:1312"
 | 
			
		||||
function waitforqBittorrentStart() {
 | 
			
		||||
    #set +x
 | 
			
		||||
    TIMESTAMP=$(date +%s)
 | 
			
		||||
    END=$((TIMESTAMP+10))
 | 
			
		||||
    ERR=0
 | 
			
		||||
    while true; do
 | 
			
		||||
        NEWTIMESTAMP=$(date +%s)
 | 
			
		||||
        if [ $NEWTIMESTAMP -gt $END ]; then
 | 
			
		||||
            ERR=1
 | 
			
		||||
            break
 | 
			
		||||
        fi
 | 
			
		||||
        if curl --silent "$1" 2>&1 > /dev/null; then
 | 
			
		||||
            break
 | 
			
		||||
        else
 | 
			
		||||
            sleep 0.1
 | 
			
		||||
        fi
 | 
			
		||||
    done
 | 
			
		||||
    return $ERR
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Wait for qBittorrent to be done cleanly exiting
 | 
			
		||||
# Necessary because otherwise it will leave temporary files behind!
 | 
			
		||||
# waitforQbittorrentStop 1234
 | 
			
		||||
function waitforqBittorrentStop() {
 | 
			
		||||
    TIMESTAMP=$(date +%s)
 | 
			
		||||
    END=$((TIMESTAMP+15))
 | 
			
		||||
    ERR=0
 | 
			
		||||
    while true; do
 | 
			
		||||
        NEWTIMESTAMP=$(date +%s)
 | 
			
		||||
        if [ $NEWTIMESTAMP -gt $END ]; then
 | 
			
		||||
            ERR=1
 | 
			
		||||
            break
 | 
			
		||||
        fi
 | 
			
		||||
        if ! ps x | grep -P "^\s+$PID\s+" 2>&1 > /dev/null; then
 | 
			
		||||
            # process died successfully
 | 
			
		||||
            break
 | 
			
		||||
        else
 | 
			
		||||
            sleep 0.1
 | 
			
		||||
        fi
 | 
			
		||||
    done
 | 
			
		||||
    return $ERR
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
start() {
 | 
			
		||||
    echo "y" | qbittorrent-nox --profile="$1" --webui-port="$2" 2>&1 > "$1"/qBittorrent.log &
 | 
			
		||||
    echo $! > "$1"/pid
 | 
			
		||||
    if waitforqBittorrentStart "http://localhost:$2"; then
 | 
			
		||||
        return 0
 | 
			
		||||
    else
 | 
			
		||||
        return 1
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stop() {
 | 
			
		||||
    PID="$(cat "$1"/pid)"
 | 
			
		||||
    kill $PID
 | 
			
		||||
    if ! waitforqBittorrentStop $PID; then
 | 
			
		||||
        echo "qBittorrent does not quit. Using SIGKILL"
 | 
			
		||||
        kill -9 $PID
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
case "$1" in
 | 
			
		||||
    "start")
 | 
			
		||||
        QBTNOX_DIR="$2"
 | 
			
		||||
        QBTNOX_PORT="$3"
 | 
			
		||||
        start "$QBTNOX_DIR" "$QBTNOX_PORT"
 | 
			
		||||
        STATUS=$?
 | 
			
		||||
        ;;
 | 
			
		||||
    "stop")
 | 
			
		||||
        QBTNOX_DIR="$2"
 | 
			
		||||
        stop "$QBTNOX_DIR"
 | 
			
		||||
        STATUS=$?
 | 
			
		||||
        ;;
 | 
			
		||||
esac
 | 
			
		||||
 | 
			
		||||
exit $STATUS
 | 
			
		||||
							
								
								
									
										44
									
								
								tests/runners/local.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										44
									
								
								tests/runners/local.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
#! /usr/bin/bash
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES=("qbittorrent-nox" "bats" "curl" "grep" "cat" "sed")
 | 
			
		||||
 | 
			
		||||
if [[ "${QBTNOX_DIR:-plz}" = "plz" ]]; then
 | 
			
		||||
    export QBTNOX_DIR="$(mktemp -d)"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
ORIGDIR="$(pwd)"
 | 
			
		||||
cd "$(dirname "$0")"
 | 
			
		||||
 | 
			
		||||
source ../utils.sh
 | 
			
		||||
 | 
			
		||||
SUBCOMMAND="${1:-test}"
 | 
			
		||||
if [[ "$SUBCOMMAND" = "test" ]] && [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
 | 
			
		||||
    depsCheck
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
export FREEPORT="$(findFreePort)"
 | 
			
		||||
API="http://localhost:$FREEPORT"
 | 
			
		||||
 | 
			
		||||
# Subcommand is test and checks succeeded
 | 
			
		||||
subcommand "$SUBCOMMAND"
 | 
			
		||||
STATUS=$?
 | 
			
		||||
if [ $STATUS -eq 0 ]; then
 | 
			
		||||
    if [[ "$SUBCOMMAND" = "test" ]]; then
 | 
			
		||||
        cat ../default.toml | envsubst > "$QBTNOX_DIR"/config.toml
 | 
			
		||||
        export CFGFILE="$QBTNOX_DIR"/config.toml
 | 
			
		||||
        if ../qbittorrent-nox.sh start "$QBTNOX_DIR" "$FREEPORT"; then
 | 
			
		||||
            runTests "$API"
 | 
			
		||||
            STATUS=$?
 | 
			
		||||
            ../qbittorrent-nox.sh stop "$QBTNOX_DIR"
 | 
			
		||||
        else
 | 
			
		||||
            echo "qBittorrent did not start"
 | 
			
		||||
            STATUS=1
 | 
			
		||||
        fi
 | 
			
		||||
    fi
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
 | 
			
		||||
    rm -R "$QBTNOX_DIR"
 | 
			
		||||
fi
 | 
			
		||||
cd "$ORIGDIR"
 | 
			
		||||
exit $STATUS
 | 
			
		||||
							
								
								
									
										56
									
								
								tests/runners/nix.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										56
									
								
								tests/runners/nix.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
#! /usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES=("nix" "nix-shell")
 | 
			
		||||
# TODO: find grep package name for --pure environment
 | 
			
		||||
NIX_DEPENDENCIES=("bats" "curl" "qbittorrent-nox")
 | 
			
		||||
 | 
			
		||||
if [[ "${QBTNOX_DIR:-plz}" = "plz" ]]; then
 | 
			
		||||
    export QBTNOX_DIR="$(mktemp -d)"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
ORIGDIR="$(pwd)"
 | 
			
		||||
cd "$(dirname "$0")"
 | 
			
		||||
 | 
			
		||||
source ../utils.sh
 | 
			
		||||
 | 
			
		||||
SUBCOMMAND="${1:-test}"
 | 
			
		||||
 | 
			
		||||
# Ensure nix shell with bats and curl... if we are running tests
 | 
			
		||||
if [[ "$SUBCOMMAND" = "test" ]] && [[ "${IN_NIX_SHELL:-plz}" = "plz" ]]; then
 | 
			
		||||
    # Did test.sh perform depsCheck already?
 | 
			
		||||
    if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
 | 
			
		||||
        depsCheck
 | 
			
		||||
    fi
 | 
			
		||||
    # Are we in nix-shell already?
 | 
			
		||||
    # TODO: redo --pure when grep dep is found
 | 
			
		||||
    IN_NIX_SHELL=1 nix-shell -p ${NIX_DEPENDENCIES[@]} --run "\"$ORIGDIR\"/\"$0\" "$@""
 | 
			
		||||
    exit $?
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export FREEPORT="$(findFreePort)"
 | 
			
		||||
API="http://localhost:$FREEPORT"
 | 
			
		||||
 | 
			
		||||
# Subcommand is test and checks succeeded
 | 
			
		||||
subcommand "$SUBCOMMAND"
 | 
			
		||||
STATUS=$?
 | 
			
		||||
if [ $STATUS -eq 0 ]; then
 | 
			
		||||
    if [[ "$SUBCOMMAND" = "test" ]]; then
 | 
			
		||||
        cat ../default.toml | envsubst > "$QBTNOX_DIR"/config.toml
 | 
			
		||||
        export CFGFILE="$QBTNOX_DIR"/config.toml
 | 
			
		||||
        if ../qbittorrent-nox.sh start "$QBTNOX_DIR" "$FREEPORT"; then
 | 
			
		||||
            runTests "$API"
 | 
			
		||||
            STATUS=$?
 | 
			
		||||
            ../qbittorrent-nox.sh stop "$QBTNOX_DIR"
 | 
			
		||||
        else
 | 
			
		||||
            echo "qBittorrent did not start"
 | 
			
		||||
            STATUS=1
 | 
			
		||||
        fi
 | 
			
		||||
    fi
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
 | 
			
		||||
    rm -R "$QBTNOX_DIR"
 | 
			
		||||
fi 
 | 
			
		||||
cd "$ORIGDIR"
 | 
			
		||||
exit $STATUS
 | 
			
		||||
							
								
								
									
										94
									
								
								tests/test.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										94
									
								
								tests/test.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
#! /usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
# LET TEST RUNNERS KNOW WE ALREADY PERFORM CHECKS
 | 
			
		||||
export TESTRUNNER="YEAH"
 | 
			
		||||
 | 
			
		||||
export QBTNOX_DIR="$(mktemp -d)"
 | 
			
		||||
 | 
			
		||||
ORIGDIR="$(pwd)"
 | 
			
		||||
cd "$(dirname "$0")"
 | 
			
		||||
 | 
			
		||||
help() {
 | 
			
		||||
    echo "test.sh RUNNER"
 | 
			
		||||
    echo "  Run the test suite. Requires bats and qBittorrent as dependencies. Dependencies are found by one of the following runners:"
 | 
			
		||||
    echo "  - local: the local system's \$PATH"
 | 
			
		||||
    echo "  - nix: nixos.org package repositories (requires working nix setup)"
 | 
			
		||||
    echo "  - docker: Debian stable base image + APT packages (requires working docker setup/daemon)"
 | 
			
		||||
    echo "  (default) if no explicit runner is requested, they are tried in order."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# FIND THE QBT BINARY
 | 
			
		||||
cd ..
 | 
			
		||||
BIN_NAME="$(grep -Po '^name = (.*)' Cargo.toml | sed 's/[" ]//g' | sed 's/name=//')"
 | 
			
		||||
if [ -f target/release/$BIN_NAME ]; then
 | 
			
		||||
    cargo build --release
 | 
			
		||||
    export BIN_PATH="$(realpath target/release/"$BIN_NAME")"
 | 
			
		||||
else
 | 
			
		||||
    cargo build
 | 
			
		||||
    export BIN_PATH="$(realpath target/debug/"$BIN_NAME")"
 | 
			
		||||
fi
 | 
			
		||||
echo "Using build $BIN_PATH"
 | 
			
		||||
 | 
			
		||||
cd tests
 | 
			
		||||
SUCCESS=0
 | 
			
		||||
RUNNER="${1:-default}"
 | 
			
		||||
shift
 | 
			
		||||
 | 
			
		||||
case "$RUNNER" in
 | 
			
		||||
    "--help"|"-h"|"help")
 | 
			
		||||
        help
 | 
			
		||||
        ;;
 | 
			
		||||
#    "docker")
 | 
			
		||||
#        echo "Running tests under: docker ("$QBTNOX_DIR")"
 | 
			
		||||
#        if ./runners/docker.sh deps-check; then
 | 
			
		||||
#            ./runners/docker.sh test
 | 
			
		||||
#            SUCCESS=$?
 | 
			
		||||
#        else
 | 
			
		||||
#            SUCCESS=1
 | 
			
		||||
#        fi
 | 
			
		||||
#        ;;
 | 
			
		||||
    "nix")
 | 
			
		||||
        echo "Running tests under: nix ("$QBTNOX_DIR")"
 | 
			
		||||
        if ./runners/nix.sh deps-check; then
 | 
			
		||||
            ./runners/nix.sh test
 | 
			
		||||
            SUCCESS=$?
 | 
			
		||||
        else
 | 
			
		||||
            SUCCESS=1
 | 
			
		||||
        fi
 | 
			
		||||
        ;;
 | 
			
		||||
    "local")
 | 
			
		||||
        echo "Running tests under: local ("$QBTNOX_DIR")"
 | 
			
		||||
        if ./runners/.local.sh deps-check; then
 | 
			
		||||
            ./runners/local.sh test
 | 
			
		||||
            SUCCESS=$?
 | 
			
		||||
        else
 | 
			
		||||
            SUCCESS=1
 | 
			
		||||
        fi
 | 
			
		||||
        ;;
 | 
			
		||||
    *)
 | 
			
		||||
        echo "Autodiscovery for runner"
 | 
			
		||||
        if ./runners/local.sh deps-check; then
 | 
			
		||||
            echo "Running tests under: local ("$QBTNOX_DIR")"
 | 
			
		||||
            ./runners/local.sh test
 | 
			
		||||
            SUCCESS=$?
 | 
			
		||||
        elif ./runners/nix.sh deps-check; then
 | 
			
		||||
            echo "Running tests under: nix ("$QBTNOX_DIR")"
 | 
			
		||||
            ./runners/nix.sh test
 | 
			
		||||
            SUCCESS=$?
 | 
			
		||||
#        elif ./runners/docker.sh deps-check; then
 | 
			
		||||
#            echo "Running tests under: docker"
 | 
			
		||||
#            ./runners/docker.sh test
 | 
			
		||||
#            SUCCESS=$?
 | 
			
		||||
        else
 | 
			
		||||
            echo "Failed to find a test runner! Please setup qbittorrent-nox and bats on your system, or alternatively setup the nix package manager or the docker container manager to install them dynamically."
 | 
			
		||||
            SUCCESS=1
 | 
			
		||||
        fi
 | 
			
		||||
        ;;
 | 
			
		||||
esac
 | 
			
		||||
 | 
			
		||||
if [ -d "$QBTNOX_DIR" ]; then
 | 
			
		||||
    rm -R "$QBTNOX_DIR"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
cd "$ORIGDIR"
 | 
			
		||||
exit $SUCCESS
 | 
			
		||||
							
								
								
									
										51
									
								
								tests/units/main.bats
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/units/main.bats
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
#! /usr/bin/env bats
 | 
			
		||||
 | 
			
		||||
setup() {
 | 
			
		||||
    bats_require_minimum_version 1.5.0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "wrong server" {
 | 
			
		||||
    cat "$CFGFILE" | sed "s/$FREEPORT/22/" > "$CFGFILE".wrongserver.toml
 | 
			
		||||
    cat "$CFGFILE".wrongserver.toml
 | 
			
		||||
    run -1 "$BIN_PATH" -c "$CFGFILE".wrongserver.toml list
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    echo "$output" | grep "request error: error sending request"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "wrong login" {
 | 
			
		||||
    cat "$CFGFILE" | sed 's/admin/user/g' > "$CFGFILE".wronguser.toml
 | 
			
		||||
    cat "$CFGFILE".wronguser.toml
 | 
			
		||||
    run -1 "$BIN_PATH" -c "$CFGFILE".wronguser.toml list
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    echo "$output" | grep "Failed to login.*user"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "empty list" {
 | 
			
		||||
    run "$BIN_PATH" -c "$CFGFILE" list
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    [[ "$output" = "[]" ]]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "add ISO" {
 | 
			
		||||
    run "$BIN_PATH" -c "$CFGFILE" add --paused "magnet:?xt=urn:btih:a982743fdb1115a0e501cabb75cca85c828f9445&dn=tails-amd64-4.29-img&tr=udp%3a%2f%2ftracker.torrent.eu.org%3a451&tr=udp%3a%2f%2ftracker.coppersurfer.tk%3a6969"
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    [[ "$output" = "" ]]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "add broken magnet" {
 | 
			
		||||
    run -1 "$BIN_PATH" -c "$CFGFILE" add --paused "magnet:?qsdjksqdjsqdsqdsqldkqs"
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    echo "$output" | grep "Other error:"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "get info about ISO" {
 | 
			
		||||
    run "$BIN_PATH" -c "$CFGFILE" get "a982743fdb1115a0e501cabb75cca85c828f9445"
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    echo "$output" | grep 'a982743fdb1115a0e501cabb75cca85c828f9445'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "non-empty list" {
 | 
			
		||||
    run "$BIN_PATH" -c "$CFGFILE" list
 | 
			
		||||
    echo "$output"
 | 
			
		||||
    echo "$output" | grep "a982743fdb1115a0e501cabb75cca85c828f9445"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								tests/utils.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										94
									
								
								tests/utils.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
#! /usr/bin/bash
 | 
			
		||||
 | 
			
		||||
#echo "-- " "$0" "$@" "(utils)"
 | 
			
		||||
 | 
			
		||||
function help() {
 | 
			
		||||
    echo "$(basename "$0") SUBCOMMAND"
 | 
			
		||||
    echo "  test: run the test suite using this runner"
 | 
			
		||||
    echo "  deps-check: check for this runner's dependencies"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function depsCheck() {
 | 
			
		||||
    # Explicit arguments, or $DEPENDENCIES
 | 
			
		||||
    echo "-- Dependency checks for "$(basename "$0")""
 | 
			
		||||
    if [ ! $# -eq 0 ]; then
 | 
			
		||||
        DEPS=("$@")
 | 
			
		||||
    else
 | 
			
		||||
        DEPS=("${DEPENDENCIES[@]}")
 | 
			
		||||
    fi
 | 
			
		||||
    MISSING=()
 | 
			
		||||
    for i in "${DEPS[@]}"; do
 | 
			
		||||
        if ! command -v "$i" >/dev/null 2>&1; then
 | 
			
		||||
            MISSING+=("$i")
 | 
			
		||||
        fi
 | 
			
		||||
    done
 | 
			
		||||
    if [ ${#MISSING[@]} -gt 0 ]; then
 | 
			
		||||
        echo "ERROR: missing dependencies: "${MISSING[@]}""
 | 
			
		||||
        return 1
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function subcommand() {
 | 
			
		||||
    SUBCOMMAND="${1:-test}"
 | 
			
		||||
    #echo "  Subcommand "$1" evaluated to "$SUBCOMMAND""
 | 
			
		||||
    case "$SUBCOMMAND" in
 | 
			
		||||
        "help"|"--help"|"-h")
 | 
			
		||||
            help
 | 
			
		||||
            return 1
 | 
			
		||||
            ;;
 | 
			
		||||
        "deps-check")
 | 
			
		||||
            depsCheck
 | 
			
		||||
            return $?
 | 
			
		||||
            ;;
 | 
			
		||||
        "test")
 | 
			
		||||
            echo "Running tests from "$QBTNOX_DIR""
 | 
			
		||||
            return $?
 | 
			
		||||
            ;;
 | 
			
		||||
        *)
 | 
			
		||||
            echo "$(basename "$0") ERROR: wrong subcommand "$SUBCOMMAND""
 | 
			
		||||
            return 2
 | 
			
		||||
            ;;
 | 
			
		||||
    esac
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# $1 exit code
 | 
			
		||||
# $2 reason
 | 
			
		||||
# $3 orig dir to return to
 | 
			
		||||
function abortIfError() {
 | 
			
		||||
    if [ ! $1 -eq 0 ]; then
 | 
			
		||||
        cd "$ORIGDIR"
 | 
			
		||||
        echo "$2"
 | 
			
		||||
        #set +x
 | 
			
		||||
        if [ $# -eq 3 ]; then
 | 
			
		||||
            cd "$3"
 | 
			
		||||
        fi
 | 
			
		||||
        exit $1
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# finds a free TCP port
 | 
			
		||||
# https://stackoverflow.com/questions/28989069/how-to-find-a-free-tcp-port/45539101#45539101
 | 
			
		||||
function findFreePort() {
 | 
			
		||||
    BASE_PORT=16998
 | 
			
		||||
    INCREMENT=1
 | 
			
		||||
 | 
			
		||||
    port=$BASE_PORT
 | 
			
		||||
    isfree=$(netstat -taln | grep $port)
 | 
			
		||||
 | 
			
		||||
    while [[ -n "$isfree" ]]; do
 | 
			
		||||
        port=$[port+INCREMENT]
 | 
			
		||||
        isfree=$(netstat -taln | grep $port)
 | 
			
		||||
    done
 | 
			
		||||
 | 
			
		||||
    echo "$port"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# runTests APIADDR
 | 
			
		||||
function runTests {
 | 
			
		||||
    echo "Running tests at "$1""
 | 
			
		||||
    if bats ../units/; then
 | 
			
		||||
        echo "OK"
 | 
			
		||||
    else
 | 
			
		||||
        echo "FAIL"
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user