stmo-cli 0.6.1

CLI for version controlling Redash queries and dashboards
Documentation
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]

mod common;

use common::*;
use std::env;
use std::sync::OnceLock;
use stmo_cli::api::RedashClient;
use tempfile::TempDir;
use tokio::sync::Mutex;

static TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();

fn get_test_lock() -> &'static Mutex<()> {
    TEST_MUTEX.get_or_init(|| Mutex::new(()))
}

struct TempWorkDir {
    _temp_dir: TempDir,
    original_dir: std::path::PathBuf,
}

impl TempWorkDir {
    fn new() -> Self {
        let temp_dir = TempDir::new().unwrap();
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(temp_dir.path()).unwrap();
        Self {
            _temp_dir: temp_dir,
            original_dir,
        }
    }
}

impl Drop for TempWorkDir {
    fn drop(&mut self) {
        env::set_current_dir(&self.original_dir).ok();
    }
}

#[tokio::test]
async fn test_deploy_new_query_with_id_zero() {
    let _guard = get_test_lock().lock().await;
    let _temp_dir = TempWorkDir::new();
    let mock_server = wiremock::MockServer::start().await;

    mock_create_query(42, "Test Query")
        .mount(&mock_server)
        .await;

    mock_get_query_with_table_viz(42, "Test Query")
        .mount(&mock_server)
        .await;

    let client = RedashClient::new(mock_server.uri(), "test-key").unwrap();

    std::fs::create_dir_all("queries").unwrap();
    std::fs::write("queries/0-test-query.sql", "SELECT 1").unwrap();
    std::fs::write(
        "queries/0-test-query.yaml",
        "id: 0\nname: Test Query\ndescription: null\ndata_source_id: 63\nschedule: null\noptions:\n  parameters: []\nvisualizations: []\ntags: null\n",
    )
    .unwrap();

    let result = stmo_cli::commands::deploy::deploy(&client, vec![0], false).await;

    assert!(result.is_ok(), "Deploy failed: {:?}", result.err());

    assert!(
        !std::path::Path::new("queries/0-test-query.sql").exists(),
        "Old 0-*.sql file should be removed after creation"
    );
    assert!(
        !std::path::Path::new("queries/0-test-query.yaml").exists(),
        "Old 0-*.yaml file should be removed after creation"
    );

    assert!(
        std::path::Path::new("queries/42-test-query.sql").exists(),
        "New SQL file with server ID should be created"
    );
    assert!(
        std::path::Path::new("queries/42-test-query.yaml").exists(),
        "New YAML file with server ID should be created"
    );

    let yaml_content = std::fs::read_to_string("queries/42-test-query.yaml").unwrap();
    assert!(
        yaml_content.contains("id: 42"),
        "YAML should contain the new ID"
    );
}

#[tokio::test]
async fn test_deploy_new_query_does_not_duplicate_auto_created_table() {
    let _guard = get_test_lock().lock().await;
    let _temp_dir = TempWorkDir::new();
    let mock_server = wiremock::MockServer::start().await;

    mock_create_query(42, "Test Query")
        .mount(&mock_server)
        .await;

    mock_get_query_with_table_viz(42, "Test Query")
        .mount(&mock_server)
        .await;

    mock_update_visualization(99999)
        .expect(1)
        .mount(&mock_server)
        .await;

    let client = RedashClient::new(mock_server.uri(), "test-key").unwrap();

    std::fs::create_dir_all("queries").unwrap();
    std::fs::write("queries/0-test-query.sql", "SELECT 1").unwrap();
    std::fs::write(
        "queries/0-test-query.yaml",
        "id: 0\nname: Test Query\ndescription: null\ndata_source_id: 63\nschedule: null\noptions:\n  parameters: []\nvisualizations:\n  - id: 0\n    name: Table\n    type: TABLE\n    options: {}\n    description: null\ntags: null\n",
    )
    .unwrap();

    let result = stmo_cli::commands::deploy::deploy(&client, vec![0], false).await;

    assert!(result.is_ok(), "Deploy failed: {:?}", result.err());

    mock_server.verify().await;
}

#[tokio::test]
async fn test_deploy_new_viz_does_not_overwrite_existing() {
    let _guard = get_test_lock().lock().await;
    let _temp_dir = TempWorkDir::new();
    let mock_server = wiremock::MockServer::start().await;

    let vizs = serde_json::json!([
        {"id": 200, "name": "Existing Chart", "type": "CHART", "options": {}, "description": null}
    ]);

    mock_update_query_with_vizs(42, "Test Query", &vizs)
        .mount(&mock_server)
        .await;

    mock_get_query_with_vizs(42, "Test Query", &vizs)
        .mount(&mock_server)
        .await;

    mock_update_visualization(200)
        .expect(1)
        .mount(&mock_server)
        .await;

    mock_create_visualization(300, "New Chart")
        .expect(1)
        .mount(&mock_server)
        .await;

    let client = RedashClient::new(mock_server.uri(), "test-key").unwrap();

    std::fs::create_dir_all("queries").unwrap();
    std::fs::write("queries/42-test-query.sql", "SELECT 1").unwrap();
    std::fs::write(
        "queries/42-test-query.yaml",
        "id: 42\nname: Test Query\ndescription: null\ndata_source_id: 63\nschedule: null\noptions:\n  parameters: []\nvisualizations:\n  - id: 200\n    name: Existing Chart\n    type: CHART\n    options: {}\n    description: null\n  - name: New Chart\n    type: CHART\n    options: {}\n    description: null\ntags: null\n",
    )
    .unwrap();

    let result = stmo_cli::commands::deploy::deploy(&client, vec![42], false).await;
    assert!(result.is_ok(), "Deploy failed: {:?}", result.err());

    mock_server.verify().await;
}