gotify 0.4.0

Idiomatic client for the Gotify API
Documentation
use std::sync::{Arc, OnceLock};

pub use macro_rules_attribute::apply;

use super::*;

pub const GOTIFY_URL: &str = "http://localhost:30080";
pub const GOTIFY_APP_TOKEN: &str = "AGo8b9paHo5wPkI";
pub const GOTIFY_CLIENT_TOKEN: &str = "C4er8DTiNk08mtt";

pub fn unauthenticated_client() -> Arc<UnauthenticatedClient> {
    static CLIENT: OnceLock<Arc<UnauthenticatedClient>> = OnceLock::new();

    CLIENT
        .get_or_init(|| Arc::new(UnauthenticatedClient::new_unauthenticated(GOTIFY_URL).unwrap()))
        .clone()
}

pub fn app_client() -> Arc<AppClient> {
    static CLIENT: OnceLock<Arc<AppClient>> = OnceLock::new();

    CLIENT
        .get_or_init(|| Arc::new(AppClient::new(GOTIFY_URL, GOTIFY_APP_TOKEN).unwrap()))
        .clone()
}

pub fn client_client() -> Arc<ClientClient> {
    static CLIENT: OnceLock<Arc<ClientClient>> = OnceLock::new();

    CLIENT
        .get_or_init(|| Arc::new(ClientClient::new(GOTIFY_URL, GOTIFY_CLIENT_TOKEN).unwrap()))
        .clone()
}

macro_rules! run_test_server {
    (
        #[test]
        async fn $fn_name:ident() -> $return_type:ty $body:block
    ) => {
        #[test]
        fn $fn_name() -> $return_type {
            crate::testsuite::start_test_server_with(async { $body })
        }
    };
}

pub(crate) use run_test_server;

pub fn start_test_server_with(
    fut: impl std::future::Future<Output = eyre::Result<()>> + Send + 'static,
) -> eyre::Result<()> {
    static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();

    RUNTIME
        .get_or_init(|| tokio::runtime::Runtime::new().unwrap())
        .block_on(async {
            use futures_util::FutureExt;

            let mut server = start_server().await?;

            let result = std::panic::AssertUnwindSafe(fut).catch_unwind().await;

            server.kill()?;
            server.wait()?;

            match result {
                Ok(res) => res,
                Err(e) => std::panic::resume_unwind(e),
            }
        })
}

async fn start_server() -> eyre::Result<std::process::Child> {
    let test_server_dir =
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/test-server");

    #[cfg(target_arch = "x86")]
    let arch = "386";
    #[cfg(target_arch = "x86_64")]
    let arch = "amd64";
    #[cfg(target_arch = "arm")]
    let arch = "arm-7";
    #[cfg(target_arch = "aarch64")]
    let arch = "arm64";
    #[cfg(not(any(
        target_arch = "x86",
        target_arch = "x86_64",
        target_arch = "arm",
        target_arch = "aarch64"
    )))]
    compile_error!("Your architecture seems to be unsupported by gotify-server. If this assumption is incorrect, please create an issue on github.");

    #[cfg(target_os = "linux")]
    let gotify_binary = format!("gotify-linux-{arch}");
    #[cfg(target_os = "windows")]
    let gotify_binary = format!("gotify-windows-{arch}.exe");
    #[cfg(not(any(target_os = "linux", target_os = "windows")))]
    compile_error!("Your operating system seems to be unsupported by gotify-server. If this assumption is incorrect, please create an issue on github.");

    let gotify_binary_path = test_server_dir.join(&gotify_binary);

    if !gotify_binary_path.try_exists()? {
        let client = reqwest::Client::new();
        let download_url = client
            .get("https://api.github.com/repos/gotify/server/releases/latest")
            .header("User-Agent", "github.com/d-k-bo/gotify-rs testsuite")
            .header("Accept", "application/vnd.github+json")
            .header("X-GitHub-Api-Version", "2022-11-28")
            .send()
            .await?
            .json::<serde_json::Value>()
            .await?
            .get("assets")
            .and_then(serde_json::Value::as_array)
            .and_then(|assets| {
                assets.iter().find(|asset| {
                    asset
                        .get("name")
                        .and_then(serde_json::Value::as_str)
                        .and_then(|name| name.strip_suffix(".zip"))
                        .is_some_and(|name| name == gotify_binary)
                })
            })
            .and_then(|asset| {
                asset
                    .get("browser_download_url")
                    .and_then(serde_json::Value::as_str)
            })
            .ok_or_else(|| eyre::eyre!("failed to find latest gotify binary"))?
            .to_owned();

        zip::ZipArchive::new(std::io::Cursor::new(
            client.get(download_url).send().await?.bytes().await?,
        ))?
        .extract(&test_server_dir)?;
    }

    let data_dir = test_server_dir.join("data");
    std::fs::create_dir_all(&data_dir)?;

    std::fs::copy(
        test_server_dir.join("gotify.db"),
        data_dir.join("gotify.db"),
    )?;

    let server = std::process::Command::new(gotify_binary_path.canonicalize()?)
        .env("GOTIFY_SERVER_PORT", "30080")
        .stdout(std::fs::File::create(test_server_dir.join("gotify.log"))?)
        .current_dir(test_server_dir)
        .spawn()?;

    loop {
        match reqwest::get("http://localhost:30080").await {
            Ok(_) => return Ok(server),
            Err(e) if e.is_connect() => {
                tokio::time::sleep(std::time::Duration::from_millis(10)).await
            }
            Err(e) => return Err(e.into()),
        }
    }
}

#[test]
#[should_panic]
fn self_test() {
    start_test_server_with(async {
        assert_eq!(true, false);

        Ok(())
    })
    .unwrap()
}