Compare commits

...

3 Commits

Author SHA1 Message Date
be6e2a9540 Add end to end tests 2022-10-20 01:32:33 +02:00
4f38526dfb Better errors 2022-10-20 01:28:11 +02:00
cbe7bc51a6 Load settings from -c/--config flag and print errors
Ignore hidden files
Add add subcommand
2022-10-16 15:59:50 +02:00
18 changed files with 683 additions and 59 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
.*

View File

@ -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)
}
}

View File

@ -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(())

View File

@ -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(())
}

View File

@ -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(())

View File

@ -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");
}
}
}
}

View File

@ -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,
}

View File

@ -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 {

View File

@ -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(),
}
}
}

View File

@ -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);
}
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}