link-assistant-router 0.115.0

Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs
Documentation
//! Unit tests for the one targeting rule ([`crate::remote_command`]).

use super::*;
use crate::cli::Cli;
use clap::Parser as _;

fn command_of(args: &[&str]) -> Command {
    Cli::try_parse_from(args)
        .expect("the command line must parse")
        .command
        .expect("a subcommand was given")
}

/// Every state-touching family takes the three target flags (issue #294).
///
/// Targeting was decided per family, so what "the router" meant depended on
/// which subcommand was typed — one predictable behaviour turned into a table
/// an operator had to memorise.
#[test]
fn every_state_touching_family_accepts_a_target() {
    for args in [
        &["router", "tokens", "list"][..],
        &["router", "accounts", "list"][..],
        &["router", "providers", "list"][..],
        &["router", "logs", "anomalies"][..],
        &["router", "doctor"][..],
    ] {
        assert!(
            target_of(&command_of(args)).is_some(),
            "{args:?} must carry a target"
        );
        // And the flags actually parse, which is what `--server` did not do.
        let with_server = [args, &["--server", "http://router.example:8080"]].concat();
        let command = command_of(&with_server);
        let target = target_of(&command).expect("a target");
        assert_eq!(
            target.server.as_deref(),
            Some("http://router.example:8080"),
            "{args:?} must accept --server"
        );
    }
}

/// `--local` and `--managed` short-circuit resolution; a bare command does not.
///
/// A bare invocation may still be remote, because a *persisted* selection also
/// names a target and only resolution can tell.
#[test]
fn only_an_explicit_local_target_skips_resolution() {
    for args in [
        &["router", "tokens", "list"][..],
        &["router", "logs", "anomalies"][..],
    ] {
        assert!(
            may_be_remote(&command_of(args)),
            "{args:?} must resolve: a persisted selection counts"
        );
        for flag in ["--local", "--managed"] {
            let explicit = [args, &[flag]].concat();
            assert!(
                !may_be_remote(&command_of(&explicit)),
                "{args:?} {flag} asks for this machine and must not contact a server"
            );
        }
    }
}

/// A command with no target keeps its own dispatch untouched.
#[test]
fn commands_without_a_target_are_left_alone() {
    for args in [
        &["router", "serve"][..],
        &["router", "auth", "status"][..],
        &["router", "clients", "list"][..],
    ] {
        let command = command_of(args);
        assert!(target_of(&command).is_none(), "{args:?}");
        assert!(!may_be_remote(&command), "{args:?}");
    }
}

/// A remote command runs without `TOKEN_SECRET` (issue #294).
///
/// The secret signs *this* machine's tokens; a command aimed at another
/// deployment neither issues nor validates them here. Requiring it refused to
/// start rather than acting on the wrong target, and pushed operators toward
/// copying the deployment's signing secret onto a workstation.
#[test]
fn a_remote_command_does_not_need_the_local_signing_secret() {
    let cli = Cli::try_parse_from(["router", "tokens", "list"]).expect("parses");
    assert!(
        cli.token_secret.as_deref().is_none_or(str::is_empty),
        "the fixture must start without a secret"
    );

    let relaxed = relax_token_secret_for_remote(cli);

    assert!(
        relaxed
            .token_secret
            .as_deref()
            .is_some_and(|secret| !secret.is_empty()),
        "a remote command must be able to start without one"
    );
}

/// A secret the operator supplied is never replaced.
#[test]
fn a_supplied_secret_survives_the_relaxation() {
    let cli = Cli::try_parse_from(["router", "tokens", "list", "--token-secret", "real-secret"])
        .expect("parses");

    let relaxed = relax_token_secret_for_remote(cli);

    assert_eq!(relaxed.token_secret.as_deref(), Some("real-secret"));
}

/// The local path still demands a real secret, because it signs with it.
#[test]
fn an_explicitly_local_command_still_needs_its_secret() {
    let cli = Cli::try_parse_from(["router", "tokens", "list", "--local"]).expect("parses");

    let relaxed = relax_token_secret_for_remote(cli);

    assert!(
        relaxed.token_secret.as_deref().is_none_or(str::is_empty),
        "signing happens here, so the secret is genuinely required"
    );
}

/// A refusal names the target and says what can be done instead (issue #294).
#[test]
fn a_refusal_names_the_target_and_an_alternative() {
    let server = ResolvedServer::at(
        "http://router.example:8080".to_string(),
        Some("token".to_string()),
        "test",
    );

    let lines = no_remote_form("doctor", &server, "run `router doctor` on that deployment");

    let all = lines.join("\n");
    assert!(
        all.contains("http://router.example:8080"),
        "describing local state as though it were the target is the bug: {all}"
    );
    assert!(
        all.contains("run `router doctor` on that deployment"),
        "{all}"
    );
    assert!(
        all.contains("--local"),
        "the local action stays reachable: {all}"
    );
    assert!(all.starts_with("error:"), "{all}");
}

/// `--local` resolves to the local path without contacting anything.
#[tokio::test]
async fn an_explicit_local_target_resolves_without_a_server() {
    let target = AuthTarget {
        local: true,
        server: None,
        managed: false,
    };

    // `Target` holds the admin credential and is deliberately not `Debug`, so
    // this matches rather than unwrapping.
    match resolve(&target).await {
        Ok(Target::Local) => {}
        Ok(Target::Remote(_)) => panic!("--local must not contact a server"),
        Err(code) => panic!("--local must always resolve, got {code:?}"),
    }
}

/// An unreachable named target is an error, not a quiet fall back to local.
///
/// Falling back is the surprise the whole targeting rule exists to remove: a
/// command that cannot reach the router it was told to use must say so.
#[tokio::test]
async fn an_unreachable_named_target_is_an_error() {
    let target = AuthTarget {
        local: false,
        server: Some("http://127.0.0.1:1".to_string()),
        managed: false,
    };

    // Matched rather than unwrapped: `Target` holds the admin credential and
    // is deliberately not `Debug`.
    match resolve(&target).await {
        Ok(_) => panic!("an unreachable target must not resolve to the local path"),
        Err(code) => assert_eq!(format!("{code:?}"), format!("{:?}", ExitCode::from(1))),
    }
}

/// A refusal exits non-zero after printing its reason.
#[test]
fn a_refusal_exits_non_zero() {
    let code = refuse(vec!["error: nope".to_string()]);

    assert_eq!(format!("{code:?}"), format!("{:?}", ExitCode::from(1)));
}

/// Every subcommand in a family answers the target question (issue #294).
///
/// The accessors are what make one rule possible: a variant that forgot to
/// report its target would silently fall back to the local path, which is the
/// failure the rule exists to remove.
#[test]
fn every_subcommand_in_every_family_reports_its_target() {
    for args in [
        // tokens: all six verbs
        &["router", "tokens", "issue"][..],
        &["router", "tokens", "rotate", "tok-1"][..],
        &["router", "tokens", "list"][..],
        &["router", "tokens", "revoke", "tok-1"][..],
        &["router", "tokens", "expire", "tok-1"][..],
        &["router", "tokens", "show", "tok-1"][..],
        // accounts
        &["router", "accounts", "list"][..],
        // providers: all five
        &["router", "providers", "list"][..],
        &[
            "router",
            "providers",
            "add",
            "--name",
            "d",
            "--base-url",
            "https://d/v1",
        ][..],
        &["router", "providers", "show", "d"][..],
        &["router", "providers", "remove", "d"][..],
        &["router", "providers", "import", "/tmp/m.lenv"][..],
        // logs: all three
        &["router", "logs", "summary"][..],
        &["router", "logs", "anomalies"][..],
        &["router", "logs", "show", "cid-1"][..],
    ] {
        let command = command_of(args);
        assert!(
            target_of(&command).is_some(),
            "{args:?} must report a target, or it silently stays local"
        );
        // And each honours `--local`, which is how the local path is asked for.
        let local = [args, &["--local"]].concat();
        assert!(
            !may_be_remote(&command_of(&local)),
            "{args:?} must honour --local"
        );
    }
}