ordinary-api 0.6.0-pre.9

API server for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use ordinary_api::{client::OrdinaryApiClient, server::OrdinaryApiServer};
use ordinary_utils::shutdown_signal;
use reqwest::StatusCode;
use std::{error::Error, net::SocketAddr, path::Path};
use tokio::net::TcpListener;
use totp_rs::{Secret, TOTP};

static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

async fn server() -> anyhow::Result<(SocketAddr, Vec<u8>)> {
    if tokio_rustls::rustls::crypto::ring::default_provider()
        .install_default()
        .is_err()
    {
        tracing::error!("failed to get rustls default provider");
    }

    let listener = TcpListener::bind("0.0.0.0:0").await?;
    let addr = listener.local_addr()?;

    // !! environment clean

    let environment_dir = Path::new(".ordinary").join("environments").join("test");

    if std::fs::read_dir(&environment_dir).is_ok() {
        std::fs::remove_dir_all(&environment_dir)?;
    }

    std::fs::create_dir_all(&environment_dir)?;

    let storage_size = 16384 * 64 * 10;

    let server_span = tracing::info_span!("test");

    let (_, mfa_secret) = OrdinaryApiServer::init(
        "test",
        "api.example.com",
        "root_password",
        &environment_dir,
        storage_size,
        &["example@api.example.com".into()],
        &["example.com".into()],
        &None,
        &environment_dir.join("logs"),
    )?;

    let api_server = OrdinaryApiServer::new(
        "test",
        &environment_dir,
        storage_size,
        false,
        0,
        0,
        0,
        &environment_dir.join("logs"),
        false,
        &tracing::info_span!("server"),
    )?;

    tokio::spawn(async move {
        api_server
            .start::<&str, _>(
                server_span,
                ordinary_api::server::SecurityMode::Insecure,
                listener,
                false,
                false,
                false,
                false,
                None,
                true,
                false,
                None,
                false,
                shutdown_signal,
            )
            .await
            .expect("server crashed");
    });

    Ok((addr, mfa_secret))
}

#[tokio::test]
async fn admin() -> Result<(), Box<dyn Error>> {
    let project_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("project");
    let project = project_path.to_str().expect("project path not a string");

    ordinary_build::build(project, true)?;

    let (addr, mfa_secret) = server().await?;

    let addr = format!("http://{addr}");

    let totp = TOTP::new(
        totp_rs::Algorithm::SHA1,
        6,
        1,
        30,
        Secret::Raw(mfa_secret).to_bytes()?,
        Some("test".into()),
        "root".into(),
    )?;

    let mfa_code = totp.generate_current()?;

    // !! client clean

    let client_dir = Path::new(".ordinary")
        .join("clients")
        .join("api.example.com");

    if std::fs::read_dir(&client_dir).is_ok() {
        std::fs::remove_dir_all(&client_dir)?;
    }

    std::fs::create_dir_all(&client_dir)?;

    // !! `api.example.com` API root login

    let root_client =
        OrdinaryApiClient::new(&addr, "root", Some("api.example.com"), false, USER_AGENT)?;

    root_client.login("root_password", &mfa_code).await?;

    // !! `example.com` API account invite

    let invite_token = root_client
        .invite_api_account("test.example.com", "admin", vec![0])
        .await?;

    // `example.com` API registration

    let api_client =
        OrdinaryApiClient::new(&addr, "admin", Some("api.example.com"), false, USER_AGENT)?;

    let (totp, recovery_codes_str) = api_client.register("api_password", &invite_token).await?;

    let mut recovery_code1 = String::new();
    let mut recovery_code2 = String::new();

    for (i, c) in recovery_codes_str.chars().enumerate() {
        if i < 11 {
            recovery_code1 = format!("{recovery_code1}{c}");
        } else if i < 22 {
            recovery_code2 = format!("{recovery_code2}{c}");
        } else {
            break;
        }
    }

    // `example.com` API login

    let mfa_code = totp.generate_current()?;

    api_client.login("api_password", &mfa_code).await?;

    // !! deploy

    api_client.deploy(project).await?;

    // !! patch

    api_client.patch(project).await?;

    // !! kill

    api_client.kill(project).await?;

    // !! restart

    api_client.restart(project).await?;

    // !! reset password

    let mfa_code = totp.generate_current()?;

    api_client
        .reset_password("api_password", &mfa_code, "api_password1")
        .await?;

    // !! log in with new password

    let mfa_code = totp.generate_current()?;

    api_client.login("api_password1", &mfa_code).await?;

    // !! forgot password

    api_client
        .forgot_password("api_password2", &recovery_code1)
        .await?;

    // !! log in with new password

    let mfa_code = totp.generate_current()?;

    api_client.login("api_password2", &mfa_code).await?;

    // !! MFA TOTP reset

    let mfa_code = totp.generate_current()?;

    let totp = api_client
        .mfa_totp_reset("api_password2", &mfa_code)
        .await?;

    // !! log in with new MFA TOTP

    let mfa_code = totp.generate_current()?;

    api_client.login("api_password2", &mfa_code).await?;

    // !! MFA TOTP lost

    let totp = api_client
        .mfa_totp_lost("api_password2", &recovery_code2)
        .await?;

    // !! log in with new MFA TOTP

    let mfa_code = totp.generate_current()?;

    api_client.login("api_password2", &mfa_code).await?;

    // !! recovery codes reset

    let mfa_code = totp.generate_current()?;

    let recovery_codes_str = api_client
        .recovery_codes_reset("api_password2", &mfa_code)
        .await?;

    let mut recovery_code3 = String::new();

    for (i, c) in recovery_codes_str.chars().enumerate() {
        if i < 11 {
            recovery_code3 = format!("{recovery_code3}{c}");
        } else {
            break;
        }
    }

    // !! forgot password

    api_client
        .forgot_password("api_password3", &recovery_code3)
        .await?;

    // !! log in with new password

    let mfa_code = totp.generate_current()?;

    api_client.login("api_password3", &mfa_code).await?;

    // !! store a secret

    api_client
        .store(project, "TEST_SECRET", &[1, 2, 3, 4])
        .await?;

    // !! restart

    let app_port = api_client.restart(project).await?;

    // !! 503 for undeployed template

    let status = reqwest::get(format!("http://localhost:{app_port}"))
        .await?
        .status();

    assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);

    // !! 503 for undeployed action

    let client = reqwest::Client::new();
    let status = client
        .post(format!("http://localhost:{app_port}/test"))
        .header("content-type", "application/json")
        .body("[]")
        .send()
        .await?
        .status();

    assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);

    // todo: more tests

    Ok(())
}