mod support;
use support::{FakeGithub, Outcome, run, runner_manager_against};
const ONBOARDING_ACTIONS: usize = 3;
const CRITICAL_PERMISSION: &str = "Administration: Read and write";
const GRANT_TEXT: [&str; 7] = [
CRITICAL_PERMISSION,
"DELETING",
"RENAMING",
"TRANSFERRING",
"collaborators",
"Repository -> Metadata",
"Organization -> Self-hosted runners",
];
#[derive(Debug, Clone, PartialEq, Eq)]
struct Action {
index: usize,
total: usize,
text: String,
}
fn actions_in(transcript: &str) -> Vec<Action> {
let mut found = Vec::new();
for line in transcript.lines() {
let Some(rest) = line.trim_start().strip_prefix("Action ") else {
continue;
};
let Some((counts, text)) = rest.split_once(':') else {
continue;
};
let Some((index, total)) = counts.trim().split_once(" of ") else {
continue;
};
let total = total.split_whitespace().next().unwrap_or_default();
let (Ok(index), Ok(total)) = (index.trim().parse(), total.parse()) else {
continue;
};
found.push(Action {
index,
total,
text: text.trim().to_string(),
});
}
found
}
fn clean_machine_login() -> (tempfile::TempDir, FakeGithub, Outcome) {
let data_dir = tempfile::tempdir().expect("a temporary directory");
let github = FakeGithub::start();
github.with_device_code();
github.with_approval();
github.with_no_installations();
let outcome = run({
let mut command = runner_manager_against(data_dir.path(), &github);
command.args(["auth", "login"]);
command
});
(data_dir, github, outcome)
}
#[test]
fn a_clean_machine_reaches_an_authenticated_tool_in_three_actions() {
let (_data_dir, _github, outcome) = clean_machine_login();
assert_eq!(
outcome.code, 0,
"the login must succeed; stdout was:\n{}\nstderr:\n{}",
outcome.stdout, outcome.stderr
);
let actions = actions_in(&outcome.stdout);
assert!(
!actions.is_empty(),
"no actions were counted in the login transcript, so any count read from it would \
be vacuous. Transcript:\n{}",
outcome.stdout
);
assert_eq!(
actions.len(),
ONBOARDING_ACTIONS,
"D3's release gate is three user actions from a clean machine to an authenticated \
tool. Counted {} in:\n{}",
actions.len(),
outcome.stdout
);
for action in &actions {
assert_eq!(
action.total, ONBOARDING_ACTIONS,
"an action announcing a budget of {} would let a fourth step in under the same \
headline number",
action.total
);
}
for (position, action) in actions.iter().enumerate() {
assert_eq!(
action.index,
position + 1,
"the numbering must be dense and ascending: {actions:#?}"
);
}
assert!(
actions[0].text.contains("runner-manager auth login"),
"action 1 is the one command: {:?}",
actions[0].text
);
assert!(
actions[1].text.contains("/login/device"),
"action 2 is the code entry, on GitHub's device page: {:?}",
actions[1].text
);
assert!(
actions[2].text.contains("installations/new"),
"action 3 is the repository selection: {:?}",
actions[2].text
);
}
#[test]
fn exactly_one_of_the_three_actions_is_a_command_to_run() {
let (_data_dir, _github, outcome) = clean_machine_login();
let actions = actions_in(&outcome.stdout);
let commands = actions
.iter()
.filter(|action| action.text.contains("runner-manager "))
.count();
assert_eq!(
commands, 1,
"D3 budgets one command, one code entry, one repository selection. Actions naming \
a `runner-manager` command: {commands}. Actions:\n{actions:#?}"
);
}
#[test]
fn the_action_count_would_reject_a_fourth_step() {
let (_data_dir, _github, outcome) = clean_machine_login();
let mut inflated = outcome.stdout.clone();
inflated.push_str("Action 4 of 3: run `runner-manager auth confirm`.\n");
let actions = actions_in(&inflated);
assert_eq!(
actions.len(),
ONBOARDING_ACTIONS + 1,
"the parser must see a fourth action if one is printed, or the count above is a \
spelling check over three lines that happen to exist"
);
assert_eq!(
actions[3].index, 4,
"and the extra one must be read as the fourth action rather than as noise, or the \
parser could be hiding a step instead of counting it"
);
}
#[test]
fn the_login_screen_carries_no_permission_table() {
let (_data_dir, _github, outcome) = clean_machine_login();
let everything = outcome.both();
for needle in GRANT_TEXT {
assert!(
!everything.contains(needle),
"`auth login` must not print `{needle}`. The table is identical on every run and for every user, and it sat above the one code the operator came for. It lives in README.md and in `auth status --permissions` now. Transcript:
{everything}"
);
}
}
#[test]
fn the_code_is_reached_within_a_dozen_lines() {
let (_data_dir, _github, outcome) = clean_machine_login();
let transcript = &outcome.stdout;
let position = transcript
.lines()
.position(|line| line.contains("WDJB-MJHT"))
.unwrap_or_else(|| {
panic!(
"the login must print the user code somewhere:
{transcript}"
)
});
assert!(
position <= 12,
"the user code is the only thing on this screen the operator has to act on, and it appeared on line {position}. Everything above it is what they read first. Transcript:
{transcript}"
);
}
#[test]
fn a_login_that_never_reaches_github_still_says_where_it_was_signing_in() {
let data_dir = tempfile::tempdir().expect("a temporary directory");
let github = FakeGithub::start();
let outcome = run({
let mut command = runner_manager_against(data_dir.path(), &github);
command.args(["auth", "login"]);
command
});
assert_ne!(outcome.code, 0, "the login cannot have succeeded");
assert!(
outcome.stdout.contains("Credential store:"),
"the store this sign-in chose is the one thing the operator cannot see for themselves, so it is written before the first request:
{}
stderr:
{}",
outcome.stdout,
outcome.stderr
);
assert!(
!outcome.stdout.contains("Action 2 of"),
"and there must be no browser step, since no device code was ever obtained:
{}",
outcome.stdout
);
assert!(
outcome.stdout.contains("Action 1 of"),
"the command the operator already ran is still action 1 of 3:
{}",
outcome.stdout
);
}
#[test]
fn the_login_shows_the_user_code_and_never_the_device_code() {
let (_data_dir, _github, outcome) = clean_machine_login();
let everything = outcome.both();
assert!(
everything.contains("WDJB-MJHT"),
"the user code is displayed by design, and a login that hid it could not be \
completed:\n{everything}"
);
assert!(
!everything.contains(&support::fixture_device_code()),
"the device code must never be displayed:\n{everything}"
);
assert!(
!everything.contains(&support::fixture_token()),
"and neither must the token:\n{everything}"
);
}
#[test]
fn the_login_offers_one_place_to_type_the_code() {
let (_data_dir, _github, outcome) = clean_machine_login();
let actions = actions_in(&outcome.stdout);
let code_entry = &actions[1].text;
assert!(code_entry.contains("/login/device"), "got: {code_entry}");
let start = outcome
.stdout
.find("Action 2 of")
.expect("the browser step");
let end = outcome
.stdout
.find("Signed in.")
.unwrap_or(outcome.stdout.len());
let prompt = &outcome.stdout[start..end];
assert_eq!(
prompt.matches("http").count(),
1,
"the prompt must offer exactly one URL; a second is a second place somebody might \
type the code:\n{prompt}"
);
assert!(
prompt.contains("only on that page"),
"and it must say the code goes nowhere else:\n{prompt}"
);
}