use super::*;
use crate::cli::{AuthTarget, Cli};
use clap::Parser as _;
fn token_op(args: &[&str]) -> TokenOp {
let cli = Cli::try_parse_from(args).expect("the command line must parse");
match cli.command.expect("a subcommand") {
crate::cli::Command::Tokens { op } => op,
other => panic!("expected tokens, got {other:?}"),
}
}
#[test]
fn rotate_targets_the_named_token_not_the_callers_credential() {
let call = call_for(&token_op(&["router", "tokens", "rotate", "tok-1"]));
assert_eq!(call.path, "/api/management/tokens/rotate-client");
assert_eq!(call.method, "POST");
let body = call.body.expect("a rotate carries a body");
assert_eq!(
body["id"], "tok-1",
"the named token is the subject: {body}"
);
}
#[test]
fn an_admin_token_is_requested_only_when_asked_for() {
let plain = call_for(&token_op(&["router", "tokens", "issue"]));
let body = plain.body.expect("body");
assert!(
body["scope"].is_null(),
"an ordinary issue must not request admin scope: {body}"
);
let admin = call_for(&token_op(&["router", "tokens", "issue", "--admin"]));
let body = admin.body.expect("body");
assert_eq!(body["scope"], crate::token::ADMIN_SCOPE, "{body}");
}
#[test]
fn issue_forwards_every_control_it_was_given() {
let call = call_for(&token_op(&[
"router",
"tokens",
"issue",
"--label",
"ci",
"--max-requests",
"100",
"--max-tokens",
"50000",
"--rate-limit-per-minute",
"60",
"--account",
"team-a",
"--github-repo",
"owner/repo",
]));
let body = call.body.expect("body");
assert_eq!(body["label"], "ci", "{body}");
assert_eq!(body["max_requests"], 100, "{body}");
assert_eq!(body["max_tokens"], 50_000, "{body}");
assert_eq!(body["rate_limit_per_minute"], 60, "{body}");
assert_eq!(body["account"], "team-a", "{body}");
assert_eq!(body["github_repos"][0], "owner/repo", "{body}");
}
#[test]
fn an_unrestricted_token_sends_no_repository_list() {
let call = call_for(&token_op(&["router", "tokens", "issue"]));
let body = call.body.expect("body");
assert!(body["github_repos"].is_null(), "{body}");
}
#[test]
fn revoke_and_expire_are_one_operation() {
let revoke = call_for(&token_op(&["router", "tokens", "revoke", "tok-9"]));
let expire = call_for(&token_op(&["router", "tokens", "expire", "tok-9"]));
assert_eq!(revoke, expire);
assert_eq!(revoke.path, "/api/management/tokens/revoke");
assert_eq!(revoke.body.expect("body")["id"], "tok-9");
}
#[test]
fn show_reads_the_list_rather_than_a_route_of_its_own() {
let list = call_for(&token_op(&["router", "tokens", "list"]));
let show = call_for(&token_op(&["router", "tokens", "show", "tok-1"]));
assert_eq!(list.method, "GET");
assert_eq!(list.path, "/api/management/tokens");
assert_eq!(show, list, "show filters the list");
assert!(list.body.is_none(), "a GET carries no body");
}
#[test]
fn showing_an_unknown_token_exits_two() {
let records = vec![serde_json::json!({"id": "tok-1"})];
assert_eq!(show_one(&records, "tok-1"), std::process::ExitCode::SUCCESS);
assert_eq!(
format!("{:?}", show_one(&records, "absent")),
format!("{:?}", std::process::ExitCode::from(2)),
"an unknown id must not read as success"
);
}
#[test]
fn records_are_read_from_the_data_array() {
let answer = serde_json::json!({"data": [{"id": "a"}, {"id": "b"}]});
assert_eq!(records_in(&answer).len(), 2);
assert!(records_in(&serde_json::json!({})).is_empty());
assert!(records_in(&serde_json::json!({"data": "not an array"})).is_empty());
}
#[test]
fn the_target_is_not_sent_to_the_deployment() {
let call = call_for(&token_op(&[
"router",
"tokens",
"issue",
"--server",
"http://router.example:8080",
]));
let body = call.body.expect("body").to_string();
assert!(
!body.contains("router.example"),
"which router was targeted is not the deployment's business: {body}"
);
let _ = AuthTarget::default();
}
#[tokio::test]
async fn a_remote_issue_prints_the_token_the_deployment_minted() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let origin = format!("http://{}", listener.local_addr().unwrap());
let handle = tokio::spawn(async move {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
let (mut socket, _) = listener.accept().await.expect("a request");
let mut buffer = [0; 4096];
let read = socket.read(&mut buffer).await.unwrap_or(0);
let body = r#"{"token":"la_sk_minted_there"}"#;
let _ = socket
.write_all(
format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
body.len()
)
.as_bytes(),
)
.await;
String::from_utf8_lossy(&buffer[..read]).to_string()
});
let server = ResolvedServer::at(origin, Some("admin-token".to_string()), "test");
let code = run(
&server,
&token_op(&["router", "tokens", "issue", "--label", "ci"]),
)
.await;
assert_eq!(
format!("{code:?}"),
format!("{:?}", std::process::ExitCode::SUCCESS)
);
let request = handle.await.expect("the server task");
assert!(
request.starts_with("POST /api/management/tokens"),
"{request}"
);
assert!(request.contains(r#""label":"ci""#), "{request}");
}
#[tokio::test]
async fn a_refused_remote_call_is_not_reported_as_success() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let origin = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(async move {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
if let Ok((mut socket, _)) = listener.accept().await {
let mut buffer = [0; 1024];
let _ = socket.read(&mut buffer).await;
let _ = socket
.write_all(
b"HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
)
.await;
}
});
let server = ResolvedServer::at(origin, Some("admin-token".to_string()), "test");
let code = run(&server, &token_op(&["router", "tokens", "list"])).await;
assert_ne!(
format!("{code:?}"),
format!("{:?}", std::process::ExitCode::SUCCESS),
"a refused call must not read as an empty token list"
);
}