sylphx-cli 0.1.3

Sylphx Platform CLI — dogfoods the Rust Management SDK
//! Real CLI auth path: `sylphx login` writes credentials; subsequent whoami works
//! without SYLPHX_TOKEN / --token (reads credentials file).

use assert_cmd::cargo::cargo_bin;
use serde_json::Value;
use sylphx_wire_mock::WireMockServer;
use tokio::process::Command;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn login_token_then_whoami_twice_without_env_token() {
    let mock = WireMockServer::start().await;
    let base = mock.management_base_url();
    let bin = cargo_bin!("sylphx");
    let dir = tempfile::tempdir().expect("tmpdir");

    for run in 1..=2 {
        // Isolate credential store per run.
        let cfg = dir.path().join(format!("cfg{run}"));
        std::fs::create_dir_all(&cfg).unwrap();

        // login --token stores credential and verifies whoami
        let out = Command::new(&bin)
            .env("SYLPHX_CONFIG_DIR", &cfg)
            .env_remove("SYLPHX_TOKEN")
            .args([
                "login",
                "--token",
                "svc_test",
                "--api-url",
                &base,
                "--json",
            ])
            .output()
            .await
            .expect("spawn login");
        assert!(
            out.status.success(),
            "login run={run} stderr={}",
            String::from_utf8_lossy(&out.stderr)
        );
        let body: Value = serde_json::from_slice(&out.stdout).expect("login json");
        assert_eq!(body["ok"], true, "run={run} body={body}");
        assert_eq!(body["source"], "token");
        assert!(
            body["whoami"]["user"]["id"].as_str().unwrap_or("").len() > 0
                || body["whoami"]["user"].is_object(),
            "whoami fields run={run} {body}"
        );

        // whoami without --token / SYLPHX_TOKEN uses stored credentials
        let out2 = Command::new(&bin)
            .env("SYLPHX_CONFIG_DIR", &cfg)
            .env_remove("SYLPHX_TOKEN")
            .args(["--api-url", &base, "whoami", "--json"])
            .output()
            .await
            .expect("spawn whoami");
        assert!(
            out2.status.success(),
            "whoami run={run} stderr={}",
            String::from_utf8_lossy(&out2.stderr)
        );
        let who: Value = serde_json::from_slice(&out2.stdout).expect("whoami json");
        assert!(
            who.get("user").is_some() || who.pointer("/user/id").is_some(),
            "whoami primary fields run={run} {who}"
        );

        // logout clears store
        let out3 = Command::new(&bin)
            .env("SYLPHX_CONFIG_DIR", &cfg)
            .env_remove("SYLPHX_TOKEN")
            .args(["logout", "--json"])
            .output()
            .await
            .expect("spawn logout");
        assert!(out3.status.success());
        let lo: Value = serde_json::from_slice(&out3.stdout).unwrap();
        assert_eq!(lo["removed"], true);

        // whoami fails closed without token
        let out4 = Command::new(&bin)
            .env("SYLPHX_CONFIG_DIR", &cfg)
            .env_remove("SYLPHX_TOKEN")
            .args(["--api-url", &base, "whoami", "--json"])
            .output()
            .await
            .expect("spawn whoami after logout");
        assert!(
            !out4.status.success(),
            "whoami should fail after logout run={run}"
        );
        eprintln!("OK cli_auth token path run={run}");
    }
    eprintln!("CLI_AUTH_OK token");
    mock.shutdown().await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn login_device_flow_twice() {
    let mock = WireMockServer::start().await;
    let base = mock.management_base_url();
    let bin = cargo_bin!("sylphx");
    let dir = tempfile::tempdir().expect("tmpdir");

    for run in 1..=2 {
        let cfg = dir.path().join(format!("dev{run}"));
        std::fs::create_dir_all(&cfg).unwrap();
        let out = Command::new(&bin)
            .env("SYLPHX_CONFIG_DIR", &cfg)
            .env_remove("SYLPHX_TOKEN")
            .args([
                "login",
                "--device",
                "--auth-url",
                &base,
                "--api-url",
                &base,
                "--timeout-secs",
                "15",
                "--json",
            ])
            .output()
            .await
            .expect("spawn device login");
        assert!(
            out.status.success(),
            "device login run={run} stdout={} stderr={}",
            String::from_utf8_lossy(&out.stdout),
            String::from_utf8_lossy(&out.stderr)
        );
        let body: Value = serde_json::from_slice(&out.stdout).expect("json");
        assert_eq!(body["ok"], true);
        assert_eq!(body["source"], "device");
        assert!(
            body["whoami"]["user"].is_object() || body["whoami"].get("user").is_some(),
            "device whoami run={run} {body}"
        );

        let out2 = Command::new(&bin)
            .env("SYLPHX_CONFIG_DIR", &cfg)
            .env_remove("SYLPHX_TOKEN")
            .args(["--api-url", &base, "whoami", "--json"])
            .output()
            .await
            .expect("whoami");
        assert!(out2.status.success(), "whoami after device login run={run}");
        eprintln!("OK cli_auth device path run={run}");
    }
    eprintln!("CLI_AUTH_OK device");
    mock.shutdown().await;
}