use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpListener;
use std::path::Path;
use std::process::{Command, Output};
use std::sync::{Arc, Mutex};
struct Fake {
url: String,
seen: Arc<Mutex<Vec<String>>>,
}
impl Fake {
fn start(routes: Vec<(&'static str, String)>) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let seen = Arc::new(Mutex::new(Vec::new()));
let log = Arc::clone(&seen);
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut request = String::new();
reader.read_line(&mut request).unwrap_or_default();
let path = request.split_whitespace().nth(1).unwrap_or("").to_string();
let mut auth = String::new();
let mut length = 0;
loop {
let mut header = String::new();
if reader.read_line(&mut header).unwrap_or(0) == 0 || header.trim().is_empty() {
break;
}
let lower = header.to_ascii_lowercase();
if let Some(value) = lower.strip_prefix("authorization:") {
auth = header[header.len() - value.len()..].trim().to_string();
}
if let Some(value) = lower.strip_prefix("content-length:") {
length = value.trim().parse().unwrap_or(0);
}
}
let mut body = vec![0; length];
let _ = reader.read_exact(&mut body);
log.lock().unwrap().push(format!("{path} {auth}"));
let (status, answer) = routes
.iter()
.find(|(prefix, _)| path.starts_with(prefix))
.map(|(_, answer)| ("200 OK", answer.as_str()))
.unwrap_or(("404 Not Found", "{}"));
let _ = write!(
stream,
"HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{answer}",
answer.len()
);
}
});
Fake { url, seen }
}
fn seen(&self) -> Vec<String> {
self.seen.lock().unwrap().clone()
}
}
struct Home(tempfile::TempDir);
impl Home {
fn new() -> Self {
Home(tempfile::tempdir().unwrap())
}
fn credentials(&self, sessions: &[(&str, &str)]) {
let sessions: serde_json::Map<String, serde_json::Value> = sessions
.iter()
.map(|(origin, token)| {
(
origin.to_string(),
serde_json::json!({ "accessToken": token, "refreshToken": format!("r-{token}") }),
)
})
.collect();
std::fs::create_dir_all(self.0.path().join("portaki")).unwrap();
std::fs::write(
self.0.path().join("portaki/credentials.json"),
serde_json::json!({ "sessions": sessions }).to_string(),
)
.unwrap();
}
fn stored(&self) -> serde_json::Value {
serde_json::from_str(
&std::fs::read_to_string(self.0.path().join("portaki/credentials.json")).unwrap(),
)
.unwrap()
}
fn run(&self, cwd: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_portaki"))
.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", self.0.path())
.env("XDG_CACHE_HOME", self.0.path())
.env("PORTAKI_NO_UPDATE_CHECK", "1")
.env("PORTAKI_COSIGN", "/nonexistent/cosign")
.env("PORTAKI_GH", "/nonexistent/gh")
.env_remove("PORTAKI_CREDENTIALS_FILE")
.env_remove("PORTAKI_CREDENTIALS")
.env_remove("PORTAKI_API_URL")
.env_remove("PORTAKI_DEV_URL")
.env_remove("PORTAKI_DEV_TOKEN")
.env_remove("CI")
.env_remove("GITHUB_ACTIONS")
.stdin(std::process::Stdio::null())
.output()
.unwrap()
}
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).to_string()
}
fn document(output: &Output) -> serde_json::Value {
let stdout = text(&output.stdout);
assert_eq!(
stdout.trim().lines().count(),
1,
"stdout: {stdout}\nstderr: {}",
text(&output.stderr)
);
serde_json::from_str(stdout.trim()).unwrap_or_else(|failure| panic!("{failure}: {stdout}"))
}
fn module(dir: &Path, id: &str) {
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
format!(
"[package]\nname = \"{id}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
[dependencies]\nportaki-sdk = {{ version = \"8\", features = [\"kv\"] }}\n"
),
)
.unwrap();
std::fs::write(dir.join("src/lib.rs"), "").unwrap();
}
fn monorepo() -> tempfile::TempDir {
let repo = tempfile::tempdir().unwrap();
module(&repo.path().join("modules/nuki"), "nuki");
module(&repo.path().join("modules/wifi"), "wifi");
repo
}
#[test]
fn an_ambiguous_monorepo_is_a_usage_error_that_names_the_modules() {
let home = Home::new();
let repo = monorepo();
let output = home.run(repo.path(), &["--plain", "lint"]);
assert_eq!(output.status.code(), Some(2), "{}", text(&output.stderr));
let stderr = text(&output.stderr);
assert!(stderr.contains("--module <id> or --all"), "{stderr}");
assert!(stderr.contains("nuki, wifi"), "{stderr}");
let unknown = home.run(
repo.path(),
&["--plain", "ci", "info", "--module", "absent"],
);
assert_eq!(unknown.status.code(), Some(2));
let all = home.run(repo.path(), &["--plain", "dev", "--all"]);
assert_eq!(all.status.code(), Some(2));
assert!(text(&all.stderr).contains("one module at a time"));
}
#[test]
fn the_same_selection_works_on_ci_with_json() {
let home = Home::new();
let repo = monorepo();
let all = document(&home.run(repo.path(), &["--json", "ci", "info", "--all"]));
let ids: Vec<&str> = all["modules"]
.as_array()
.unwrap()
.iter()
.map(|module| module["id"].as_str().unwrap())
.collect();
assert_eq!(ids, vec!["nuki", "wifi"]);
let one = document(&home.run(repo.path(), &["--json", "ci", "info", "--module", "wifi"]));
assert_eq!(one["modules"][0]["version"], "0.1.0");
let root = repo.path().join("modules/nuki");
let legacy = document(&home.run(
repo.path(),
&["--json", "ci", "info", "--root", root.to_str().unwrap()],
));
assert_eq!(legacy["modules"][0]["id"], "nuki");
let modules = document(&home.run(
repo.path(),
&["--json", "ci", "modules", "--module", "nuki"],
));
assert_eq!(modules["modules"], serde_json::json!(["nuki"]));
assert_eq!(modules["any"], true);
}
#[test]
fn an_unknown_profile_is_a_usage_error() {
let home = Home::new();
let dir = tempfile::tempdir().unwrap();
let output = home.run(dir.path(), &["--plain", "--env", "stagging", "status"]);
assert_eq!(output.status.code(), Some(2));
assert!(text(&output.stderr).contains("prod, staging, local"));
}
#[test]
fn a_command_without_a_session_there_names_the_login_for_that_environment() {
let home = Home::new();
home.credentials(&[("https://api.portaki.app", "prod")]);
let dir = tempfile::tempdir().unwrap();
module(dir.path(), "nuki");
let status = document(&home.run(dir.path(), &["--json", "--env", "staging", "status"]));
assert_eq!(status["signedIn"], false);
assert_eq!(status["api"], "https://api-staging.portaki.app");
assert_eq!(status["next"]["command"], "portaki login --env staging");
std::fs::write(
home.0.path().join("portaki/config.toml"),
"[env.preprod]\napi = \"https://pre.example\"\n",
)
.unwrap();
let logs = home.run(dir.path(), &["--plain", "--env", "preprod", "logs"]);
assert_eq!(logs.status.code(), Some(1));
assert!(
text(&logs.stderr).contains("portaki login --env preprod"),
"{}",
text(&logs.stderr)
);
}
#[test]
fn the_session_sent_is_the_one_of_the_origin_addressed() {
let fake = Fake::start(vec![("/dev/v1/onboarding", "{}".to_string())]);
let home = Home::new();
home.credentials(&[("https://api.portaki.app", "prod"), (&fake.url, "local")]);
let dir = tempfile::tempdir().unwrap();
module(dir.path(), "nuki");
let output = home.run(dir.path(), &["--json", "--api", &fake.url, "status"]);
assert!(output.status.success(), "{}", text(&output.stderr));
let seen = fake.seen();
assert!(!seen.is_empty());
assert!(
seen.iter().all(|line| line.ends_with("Bearer local")),
"{seen:?}"
);
}
#[test]
fn signing_in_to_one_origin_keeps_the_others() {
let fake = Fake::start(vec![
(
"/api/v1/auth/device/code",
r#"{"success":true,"data":{"deviceCode":"d","userCode":"U","verificationUri":"http://127.0.0.1/x","expiresIn":30,"interval":1}}"#.to_string(),
),
(
"/api/v1/auth/device/token",
r#"{"success":true,"data":{"accessToken":"new","refreshToken":"r-new"}}"#.to_string(),
),
]);
let home = Home::new();
home.credentials(&[("https://api.portaki.app", "prod")]);
let dir = tempfile::tempdir().unwrap();
let output = home.run(
dir.path(),
&["--plain", "--api", &fake.url, "login", "--no-browser"],
);
assert!(output.status.success(), "{}", text(&output.stderr));
let stored = home.stored();
assert_eq!(
stored["sessions"]["https://api.portaki.app"]["accessToken"],
"prod"
);
assert_eq!(stored["sessions"][&fake.url]["accessToken"], "new");
let _ = home.run(dir.path(), &["--plain", "--api", &fake.url, "logout"]);
let stored = home.stored();
assert!(stored["sessions"].get(&fake.url).is_none());
assert_eq!(
stored["sessions"]["https://api.portaki.app"]["accessToken"],
"prod"
);
}
#[test]
fn status_reads_the_journey_and_names_the_next_command() {
let fake = Fake::start(vec![
(
"/dev/v1/onboarding",
r#"{"cliConnected":true,"deployed":true,"rendered":true,"conformant":true,"published":true}"#.to_string(),
),
(
"/dev/v1/nav-counts",
r#"{"modulesWithErrors":1,"reviewsToFix":0,"perModule":{"nuki":{"err24":4,"openReports":2,"failedChecks":0,"linked":true}}}"#.to_string(),
),
(
"/dev/v1/modules/nuki/link",
r#"{"moduleId":"nuki","repository":"acme/nuki"}"#.to_string(),
),
(
"/dev/v1/modules/nuki/status",
r#"{"watchConnected":false,"lastDeploy":{"version":"1.2.0","at":"2026-09-27T10:00:00Z"},"lastRun":{"status":"ok","at":"2026-09-27T10:01:00Z"}}"#.to_string(),
),
(
"/registry/v1/publications/mine",
r#"{"items":[{"digest":"sha256:b","moduleId":"nuki","version":"1.2.0","channel":"stable","status":"approved","releaseState":"available","publishedAt":"2026-09-26T09:00:00Z"}]}"#.to_string(),
),
(
"/registry/v1/publications/sha256:b/release",
r#"{"releaseState":"available","missing":[],"supplyChain":{"signature":"signed","signatureSource":"ci"}}"#.to_string(),
),
]);
let home = Home::new();
home.credentials(&[(&fake.url, "local")]);
let dir = tempfile::tempdir().unwrap();
module(dir.path(), "nuki");
let output = home.run(dir.path(), &["--json", "--api", &fake.url, "status"]);
assert!(output.status.success(), "{}", text(&output.stderr));
let status = document(&output);
assert_eq!(status["schemaVersion"], 1);
assert_eq!(status["journey"]["conformant"], true);
let nuki = &status["modules"][0];
assert_eq!(nuki["repository"], "acme/nuki");
assert_eq!(nuki["err24"], 4);
assert_eq!(nuki["openReports"], 2);
assert_eq!(nuki["latest"]["signature"], "signed");
assert_eq!(nuki["latest"]["signatureSource"], "ci");
assert_eq!(nuki["latest"]["review"], "approved");
assert_eq!(status["next"]["command"], "portaki logs");
let human = home.run(dir.path(), &["--plain", "--api", &fake.url, "status"]);
let stdout = text(&human.stdout);
assert!(stdout.contains("acme/nuki"), "{stdout}");
assert!(
stdout.contains("1.2.0 · stable · available · signed (ci) · review approved"),
"{stdout}"
);
}
#[test]
fn doctor_says_what_fails_and_how_to_fix_it() {
let fake = Fake::start(vec![
("/dev/v1/onboarding", "{}".to_string()),
("/dev/v1/modules/nuki/link", "{}".to_string()),
]);
let home = Home::new();
let dir = tempfile::tempdir().unwrap();
module(dir.path(), "nuki");
std::fs::create_dir_all(dir.path().join(".cargo")).unwrap();
std::fs::write(dir.path().join(".cargo/config.toml"), "").unwrap();
std::fs::create_dir_all(dir.path().join(".git")).unwrap();
let output = home.run(
dir.path(),
&["--json", "--api", &fake.url, "doctor", "--offline"],
);
assert_eq!(output.status.code(), Some(1));
let doctor = document(&output);
assert_eq!(doctor["ok"], false);
let check = |id: &str| {
doctor["checks"]
.as_array()
.unwrap()
.iter()
.find(|check| check["id"] == id)
.cloned()
.unwrap_or_else(|| panic!("no {id} in {doctor}"))
};
assert_eq!(check("session")["status"], "fail");
assert_eq!(
check("session")["fix"],
format!("portaki login --api {}", fake.url)
);
assert_eq!(check("cosign")["status"], "warn");
assert_eq!(check("overrides")["status"], "warn");
assert!(check("overrides")["summary"]
.as_str()
.unwrap()
.contains(".cargo/config.toml"));
assert_eq!(check("overrides")["module"], "nuki");
home.credentials(&[(&fake.url, "local")]);
let doctor = document(&home.run(
dir.path(),
&["--json", "--api", &fake.url, "doctor", "--offline"],
));
let session = doctor["checks"]
.as_array()
.unwrap()
.iter()
.find(|check| check["id"] == "session")
.unwrap()
.clone();
assert_eq!(session["status"], "ok");
let link = doctor["checks"]
.as_array()
.unwrap()
.iter()
.find(|check| check["id"] == "link")
.unwrap()
.clone();
assert_eq!(link["status"], "ok");
}
#[test]
fn nothing_to_do_exits_with_three() {
let home = Home::new();
let dir = tempfile::tempdir().unwrap();
module(dir.path(), "nuki");
let output = home.run(dir.path(), &["--plain", "permissions", "add", "kv"]);
assert_eq!(output.status.code(), Some(3), "{}", text(&output.stderr));
let added = home.run(dir.path(), &["--plain", "permissions", "add", "email"]);
assert_eq!(added.status.code(), Some(0), "{}", text(&added.stderr));
}
#[test]
fn the_old_dev_variable_is_a_deprecated_alias() {
let home = Home::new();
let dir = tempfile::tempdir().unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_portaki"))
.args(["--json", "status"])
.current_dir(dir.path())
.env("XDG_CONFIG_HOME", home.0.path())
.env("PORTAKI_NO_UPDATE_CHECK", "1")
.env("PORTAKI_DEV_URL", "https://dev.example")
.env_remove("PORTAKI_API_URL")
.env_remove("PORTAKI_DEV_TOKEN")
.env_remove("PORTAKI_CREDENTIALS_FILE")
.output()
.unwrap();
assert!(output.status.success(), "{}", text(&output.stderr));
assert_eq!(document(&output)["api"], "https://dev.example");
assert!(text(&output.stderr).contains("PORTAKI_DEV_URL is deprecated"));
}