pub(crate) mod claude;
pub(crate) mod codex;
pub(crate) mod desktop;
pub(crate) mod folder;
pub(crate) mod jwt;
pub mod names;
mod printed;
use crate::context::Context;
use crate::service::Permit;
use crate::usage;
use serde_json::Value;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ProviderId {
Claude,
Codex,
Desktop,
}
#[derive(Debug, Clone, Copy)]
pub struct Cli {
pub program_variable: &'static str,
pub install_places: fn(&std::path::Path) -> Vec<std::path::PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignsIn {
ByItsProgram,
InItsApp,
}
impl ProviderId {
pub const ALL: &'static [ProviderId] =
&[ProviderId::Claude, ProviderId::Codex, ProviderId::Desktop];
pub fn code(self) -> &'static str {
match self {
ProviderId::Claude => "claude",
ProviderId::Codex => "codex",
ProviderId::Desktop => "desktop",
}
}
pub fn service(self) -> &'static str {
match self {
ProviderId::Claude | ProviderId::Desktop => "Anthropic",
ProviderId::Codex => "OpenAI",
}
}
pub fn name(self) -> &'static str {
match self {
ProviderId::Claude => "Claude Code",
ProviderId::Codex => "Codex",
ProviderId::Desktop => "Claude Desktop",
}
}
pub fn program(self) -> &'static str {
match self {
ProviderId::Claude => "claude",
ProviderId::Codex => "codex",
ProviderId::Desktop => "Claude",
}
}
pub fn cli(self) -> Option<Cli> {
match self {
ProviderId::Claude => Some(Cli {
program_variable: "PITBOARD_CLAUDE",
install_places: claude::paths::install_places,
}),
ProviderId::Codex => Some(Cli {
program_variable: "PITBOARD_CODEX",
install_places: codex::paths::install_places,
}),
ProviderId::Desktop => None,
}
}
pub fn signs_in(self) -> SignsIn {
match of(self) {
LoginKind::Document(_) => SignsIn::ByItsProgram,
LoginKind::Folder(_) => SignsIn::InItsApp,
}
}
pub fn reads_usage(self) -> bool {
match of(self) {
LoginKind::Document(_) => true,
LoginKind::Folder(_) => false,
}
}
pub fn supported_on(self, os: crate::host::Os) -> bool {
use crate::host::Os;
match (self, os) {
(ProviderId::Claude | ProviderId::Codex, _) => true,
(ProviderId::Desktop, Os::MacOs) => true,
(ProviderId::Desktop, Os::Linux | Os::Windows) => false,
}
}
pub fn parse(code: &str) -> Option<ProviderId> {
match code {
"claude" => Some(ProviderId::Claude),
"codex" => Some(ProviderId::Codex),
"desktop" => Some(ProviderId::Desktop),
_ => None,
}
}
}
impl std::fmt::Display for ProviderId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Credential {
pub(crate) provider: ProviderId,
pub(crate) raw: Value,
}
impl Credential {
pub(crate) fn new(provider: ProviderId, raw: Value) -> Credential {
Credential { provider, raw }
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Owner {
pub account_uuid: String,
pub email: String,
pub organization_uuid: String,
}
impl Owner {
pub(crate) fn same_login(&self, other: &Owner) -> bool {
self.account_uuid == other.account_uuid && self.organization_uuid == other.organization_uuid
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ProviderError {
#[error("the session has expired")]
Unauthorized,
#[error("{service} is rate limiting this request")]
RateLimited {
service: &'static str,
retry_after: Option<i64>,
},
#[error("could not reach {service}: {detail}")]
Network {
service: &'static str,
detail: String,
},
#[error("{service} answered {status}")]
Unexpected { service: &'static str, status: u16 },
#[error("{service}'s answer was not understood: {detail}")]
Malformed {
service: &'static str,
detail: String,
},
#[error("{service} no longer accepts this login")]
InvalidGrant { service: &'static str },
#[error("the stored login is not the shape {provider} keeps: {detail}")]
ShapeUnexpected {
provider: ProviderId,
detail: String,
},
#[error("{reason}")]
Unsupported {
provider: ProviderId,
reason: String,
},
#[error("nothing is signed in")]
NoLogin { provider: ProviderId },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Adoption {
PollingWithin(u32),
AtRenewal {
file: std::path::PathBuf,
},
RestartRequired {
program: &'static str,
holders: &'static [crate::holder::Holder],
},
NextOpen {
app: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Behind {
pub path: std::path::PathBuf,
pub held: Held,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Held {
Login,
NoLogin,
Unreadable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ParkSemantics {
CopyWhileLive,
MoveOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Isolation {
Isolated,
NotIsolated { reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Expiry {
pub(crate) access_expires_at: Option<i64>,
pub(crate) refresh_expires_at: Option<i64>,
}
pub(crate) struct LiveStore {
pub(crate) chain: crate::store::Live,
pub(crate) service: String,
}
pub(crate) fn store_error(error: crate::store::Error) -> ProviderError {
const STORE: &str = "this machine's credential store";
match error {
crate::store::Error::Malformed(detail) => ProviderError::Malformed {
service: STORE,
detail,
},
other => ProviderError::Network {
service: STORE,
detail: other.to_string(),
},
}
}
pub(crate) trait Provider: Send + Sync + std::fmt::Debug {
fn id(&self) -> ProviderId;
fn live(&self, ctx: &Context) -> Result<LiveStore, ProviderError>;
fn read_live(&self, ctx: &Context) -> Result<Option<Credential>, ProviderError> {
let live = self.live(ctx)?;
crate::store::read(&live.chain, &live.service)
.map(|found| found.map(|raw| Credential::new(self.id(), raw)))
.map_err(store_error)
}
fn identify(&self, ctx: &Context, credential: &Credential) -> Result<Owner, ProviderError>;
fn identifies_by_itself(&self) -> bool;
fn verify(&self, ctx: &Context, credential: &Credential) -> Result<Owner, ProviderError> {
self.identify(ctx, credential)
}
fn usage(
&self,
ctx: &Context,
credential: &Credential,
) -> Result<usage::Snapshot, ProviderError>;
fn renew(
&self,
ctx: &Context,
permit: Permit,
credential: &Credential,
) -> Result<Credential, ProviderError>;
fn slot(&self, ctx: &Context) -> String;
fn write_lock(&self, ctx: &Context) -> Option<std::path::PathBuf>;
fn refresh_lock(&self, ctx: &Context) -> Option<(std::path::PathBuf, std::path::PathBuf)>;
fn own_record(&self, ctx: &Context) -> Option<Owner>;
fn after_switch(
&self,
ctx: &Context,
permit: Permit,
incoming: &crate::state::Account,
outgoing: &Owner,
) -> Result<(), crate::error::Error>;
fn program(&self, ctx: &Context) -> Option<std::path::PathBuf>;
fn sign_in(
&self,
ctx: &Context,
permit: Permit,
dir: &std::path::Path,
) -> std::process::Command;
fn read_signin(
&self,
ctx: &Context,
dir: &std::path::Path,
) -> Result<Option<String>, crate::store::Error>;
fn discard_signin(&self, ctx: &Context, permit: Permit, dir: &std::path::Path);
fn read_sign_in(&self, said: &str) -> SignInView;
fn refused_code(&self, since: &str) -> bool;
fn overridden_by(&self, ctx: &Context) -> Vec<String>;
fn behind(&self, ctx: &Context) -> Option<Behind>;
fn adoption(&self, behind: Option<&Behind>) -> Adoption;
fn park_semantics(&self) -> ParkSemantics;
fn private_signin_isolation(&self, ctx: &Context) -> Isolation;
fn slice(&self, live: &Value) -> Result<Value, ProviderError>;
fn plan(&self, document: &Value) -> Option<String>;
fn splice(&self, live: &Value, incoming: &Value) -> Result<Value, ProviderError>;
fn fingerprint(&self, slice: &Value) -> String;
fn expiry(&self, slice: &Value) -> Expiry;
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SignInView {
pub url: Option<String>,
pub wants_code: bool,
}
pub fn sign_in_view(provider: ProviderId, said: &str, pasted: bool) -> SignInView {
match of(provider) {
LoginKind::Document(tool) => {
let read = tool.read_sign_in(said);
SignInView {
wants_code: read.wants_code && !pasted,
..read
}
}
LoginKind::Folder(_) => SignInView::default(),
}
}
pub fn refused_code(provider: ProviderId, since: &str) -> bool {
match of(provider) {
LoginKind::Document(tool) => tool.refused_code(since),
LoginKind::Folder(_) => false,
}
}
pub(crate) fn https_address(said: &str) -> Option<String> {
printed::read(said).find_map(|piece| match piece {
printed::Printed::Text(text) => bare_https_address(text),
printed::Printed::Link(target) => target
.strip_prefix(HTTPS)
.is_some_and(|rest| !rest.is_empty() && !rest.contains(ends_an_address))
.then(|| target.to_owned()),
})
}
const HTTPS: &str = "https://";
fn bare_https_address(text: &str) -> Option<String> {
text.match_indices(HTTPS).find_map(|(at, _)| {
let rest = &text[at + HTTPS.len()..];
let length = rest.find(ends_an_address).unwrap_or(rest.len());
(length > 0).then(|| text[at..at + HTTPS.len() + length].to_owned())
})
}
fn ends_an_address(c: char) -> bool {
c.is_whitespace() || matches!(c, '"' | '\'' | '<' | '>')
}
pub(crate) fn program_of(ctx: &Context, tool: ProviderId) -> Option<std::path::PathBuf> {
crate::host::program::find(ctx.program_for(tool)?, &ctx.search_path())
}
pub(crate) fn command(ctx: &Context, _: Permit, tool: ProviderId) -> std::process::Command {
let search = ctx.search_path();
let Some(program) = program_of(ctx, tool) else {
let named = ctx
.program_for(tool)
.unwrap_or(std::path::Path::new(tool.program()));
let mut command = std::process::Command::new(named);
command.env("PATH", search);
return command;
};
let entries: Vec<std::path::PathBuf> = if search.is_empty() {
Vec::new()
} else {
std::env::split_paths(&search).collect()
};
let path = match program
.parent()
.filter(|dir| !entries.iter().any(|entry| entry == dir))
{
Some(dir) => std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(entries))
.unwrap_or(search),
None => search,
};
let mut command = std::process::Command::new(program);
command.env("PATH", path);
command
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum LoginKind {
Document(&'static dyn Provider),
Folder(&'static dyn folder::Folder),
}
pub(crate) fn of(tool: ProviderId) -> LoginKind {
match tool {
ProviderId::Claude => LoginKind::Document(&claude::engine::Claude),
ProviderId::Codex => LoginKind::Document(&codex::engine::Codex),
ProviderId::Desktop => LoginKind::Folder(&desktop::Desktop),
}
}
pub(crate) fn claude_code() -> &'static dyn Provider {
&claude::engine::Claude
}
pub(crate) fn codex() -> &'static dyn Provider {
&codex::engine::Codex
}
pub(crate) fn slot(ctx: &Context, tool: ProviderId) -> Option<String> {
match of(tool) {
LoginKind::Document(found) => Some(found.slot(ctx)),
LoginKind::Folder(app) => app
.root(ctx)
.ok()
.map(|root| root.to_string_lossy().into_owned()),
}
}
#[cfg(test)]
pub(crate) fn document(tool: ProviderId) -> &'static dyn Provider {
match of(tool) {
LoginKind::Document(found) => found,
LoginKind::Folder(_) => panic!("{tool} keeps no login document"),
}
}
#[cfg(test)]
pub(crate) fn documents() -> impl Iterator<Item = &'static dyn Provider> {
ProviderId::ALL.iter().filter_map(|&tool| match of(tool) {
LoginKind::Document(found) => Some(found),
LoginKind::Folder(_) => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_provider_code_parses_back_to_itself() {
for &id in ProviderId::ALL {
assert_eq!(ProviderId::parse(id.code()), Some(id), "{id}");
assert!(
id.code().chars().all(|c| c.is_ascii_lowercase()),
"{id} is not a plain lowercase code"
);
}
assert_eq!(ProviderId::parse("nothing"), None);
}
#[test]
fn every_provider_is_in_all() {
for &id in ProviderId::ALL {
match id {
ProviderId::Claude | ProviderId::Codex | ProviderId::Desktop => {}
}
}
assert_eq!(ProviderId::ALL.len(), 3, "add the new provider to ALL");
}
#[test]
fn desktop_parses_as_a_tool_and_keeps_no_command_line() {
assert_eq!(ProviderId::parse("desktop"), Some(ProviderId::Desktop));
assert_eq!(ProviderId::Desktop.name(), "Claude Desktop");
assert_eq!(ProviderId::Desktop.program(), "Claude");
assert!(ProviderId::Desktop.cli().is_none());
assert_eq!(ProviderId::Desktop.signs_in(), SignsIn::InItsApp);
assert!(!ProviderId::Desktop.reads_usage());
assert!(matches!(of(ProviderId::Desktop), LoginKind::Folder(_)));
for tool in [ProviderId::Claude, ProviderId::Codex] {
assert!(tool.cli().is_some(), "{tool}");
assert_eq!(tool.signs_in(), SignsIn::ByItsProgram, "{tool}");
assert!(tool.reads_usage(), "{tool}");
}
assert_eq!(
documents().map(|tool| tool.id()).collect::<Vec<_>>(),
[ProviderId::Claude, ProviderId::Codex]
);
assert_eq!(
sign_in_view(ProviderId::Desktop, "https://claude.ai/login", false),
SignInView::default()
);
assert!(!refused_code(ProviderId::Desktop, "Invalid code"));
}
#[test]
fn the_tool_is_absent_off_macos() {
use crate::host::Os;
for os in [Os::MacOs, Os::Linux, Os::Windows] {
assert_eq!(
ProviderId::Desktop.supported_on(os),
os == Os::MacOs,
"{os:?}"
);
assert!(ProviderId::Claude.supported_on(os), "{os:?}");
assert!(ProviderId::Codex.supported_on(os), "{os:?}");
}
}
#[test]
fn the_code_is_what_serde_writes() {
for &id in ProviderId::ALL {
let written = serde_json::to_value(id).expect("a provider id serialises");
assert_eq!(written, serde_json::json!(id.code()), "{id}");
}
}
#[test]
fn every_implementation_agrees_about_which_tool_it_is() {
for &id in ProviderId::ALL {
let said = match of(id) {
LoginKind::Document(tool) => tool.id(),
LoginKind::Folder(app) => app.id(),
};
assert_eq!(said, id, "{id} is registered against another tool");
}
}
#[test]
fn claude_code_follows_a_switch_on_its_own_and_tolerates_a_copy() {
let claude = document(ProviderId::Claude);
assert_eq!(
claude.adoption(None),
Adoption::PollingWithin(33),
"measured: a session serves its credential from a 30 second cache"
);
assert_eq!(
claude.park_semantics(),
ParkSemantics::CopyWhileLive,
"nothing of Claude Code's revokes for presenting either copy, and the live document holds the machine's other keys"
);
assert_eq!(
claude.private_signin_isolation(&Context::for_unit_test()),
Isolation::Isolated,
"CLAUDE_CONFIG_DIR picks the keychain item by hashing the directory, and there is no second backend that escapes it"
);
}
#[test]
fn a_restart_is_not_a_countdown_of_zero() {
let restart = document(ProviderId::Codex).adoption(None);
assert_ne!(restart, Adoption::PollingWithin(0));
assert!(matches!(Adoption::PollingWithin(33), Adoption::PollingWithin(s) if s == 33));
}
struct Prefix(std::path::PathBuf);
impl Prefix {
fn new(name: &str) -> Prefix {
let root = std::env::temp_dir().join(format!(
"pitboard-search-path-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&root);
let bin = root.join("npm/bin");
std::fs::create_dir_all(&bin).expect("a scratch prefix");
for tool in documents().map(|tool| tool.id()) {
let program = bin.join(tool.program());
std::fs::write(&program, "").expect("a program");
crate::host::fs::testing::make_runnable(&program).expect("runnable");
}
Prefix(root)
}
fn bin(&self) -> std::path::PathBuf {
self.0.join("npm/bin")
}
fn decoys(&self) -> (std::path::PathBuf, std::path::PathBuf) {
let (dirs, files) = (self.0.join("dirs"), self.0.join("files"));
for tool in documents().map(|tool| tool.id()) {
std::fs::create_dir_all(dirs.join(tool.program())).expect("a directory");
std::fs::create_dir_all(&files).expect("a directory");
std::fs::write(files.join(tool.program()), "").expect("a file");
}
(dirs, files)
}
}
impl Drop for Prefix {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn env_of<'a>(command: &'a std::process::Command, name: &str) -> Option<&'a std::ffi::OsStr> {
command
.get_envs()
.find(|(key, _)| *key == name)
.and_then(|(_, value)| value)
}
#[test]
#[cfg_attr(windows, ignore = "W17: finding programs on Windows")]
fn a_program_is_looked_for_on_the_search_path_it_is_given() {
let prefix = Prefix::new("find");
let search = format!("/nowhere/at/all::{}", prefix.bin().display());
let found = crate::host::program::find(std::path::Path::new("codex"), search.as_ref());
assert_eq!(found, Some(prefix.bin().join("codex")));
assert_eq!(
crate::host::program::find(std::path::Path::new("ls"), search.as_ref()),
None,
"ls is on this process's PATH and not on the one given"
);
}
#[test]
#[cfg_attr(windows, ignore = "W17: finding programs on Windows")]
fn what_cannot_be_run_is_passed_over() {
let prefix = Prefix::new("decoys");
let (dirs, files) = prefix.decoys();
let search = format!(
"{}:{}:{}",
dirs.display(),
files.display(),
prefix.bin().display()
);
assert_eq!(
crate::host::program::find(std::path::Path::new("codex"), search.as_ref()),
Some(prefix.bin().join("codex"))
);
for decoy in [dirs.join("codex"), files.join("codex")] {
assert_eq!(
crate::host::program::find(&decoy, "".as_ref()),
None,
"{}",
decoy.display()
);
}
}
#[test]
#[cfg_attr(windows, ignore = "W17: finding programs on Windows")]
fn a_sign_in_runs_the_program_found_with_the_search_path_as_it_is() {
let prefix = Prefix::new("sign-in");
let search = format!("/nowhere/before:{}", prefix.bin().display());
let ctx =
Context::new(std::path::PathBuf::from("/nowhere")).with_search_path(search.clone());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
for tool in documents().map(|tool| tool.id()) {
let command = document(tool).sign_in(&ctx, Permit::for_a_test(), dir);
let program = prefix.bin().join(tool.program());
assert_eq!(command.get_program(), program.as_os_str(), "{tool}");
assert_eq!(env_of(&command, "PATH"), Some(search.as_ref()), "{tool}");
assert_eq!(document(tool).program(&ctx), Some(program), "{tool}");
}
}
#[test]
#[cfg_attr(windows, ignore = "W17: finding programs on Windows")]
fn a_program_named_outright_is_run_with_its_own_directory_on_path() {
let prefix = Prefix::new("named");
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_claude_program(prefix.bin().join("claude"))
.with_codex_program(prefix.bin().join("codex"))
.with_search_path("/usr/bin:/bin".into());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
for tool in documents().map(|tool| tool.id()) {
let command = document(tool).sign_in(&ctx, Permit::for_a_test(), dir);
assert_eq!(
command.get_program(),
prefix.bin().join(tool.program()).as_os_str(),
"{tool}"
);
assert_eq!(
env_of(&command, "PATH"),
Some(format!("{}:/usr/bin:/bin", prefix.bin().display()).as_ref()),
"{tool}"
);
}
let on_it = format!("/usr/bin:{}/", prefix.bin().display());
let ctx = ctx.with_search_path(on_it.clone());
for tool in documents().map(|tool| tool.id()) {
let command = document(tool).sign_in(&ctx, Permit::for_a_test(), dir);
assert_eq!(env_of(&command, "PATH"), Some(on_it.as_ref()), "{tool}");
}
}
#[test]
fn a_program_found_nowhere_is_left_to_the_search_path() {
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_search_path("/nowhere/at/all".into());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
for tool in documents().map(|tool| tool.id()) {
let command = document(tool).sign_in(&ctx, Permit::for_a_test(), dir);
assert_eq!(command.get_program(), tool.program(), "{tool}");
assert_eq!(
env_of(&command, "PATH"),
Some("/nowhere/at/all".as_ref()),
"{tool}"
);
assert_eq!(document(tool).program(&ctx), None, "{tool}");
}
}
#[test]
fn the_address_to_open_is_the_one_the_person_goes_to() {
let mut said = String::from(
"Starting local login server on http://localhost:1455.\n\
If your browser did not open, navigate to this URL to authenticate:\n\n\
https://auth.openai.com/oauth/authorize?response_type=code&state=x\u{1b}[0m\n",
);
assert_eq!(
sign_in_view(ProviderId::Codex, &said, false).url.as_deref(),
Some("https://auth.openai.com/oauth/authorize?response_type=code&state=x")
);
said.push_str("Paste code here if prompted > ");
assert!(!sign_in_view(ProviderId::Codex, &said, false).wants_code);
let said = "Paste code here if prompted > ";
assert!(sign_in_view(ProviderId::Claude, said, false).wants_code);
}
#[test]
fn a_code_is_asked_for_only_where_the_tool_takes_one() {
let said = "Paste code here if prompted > ";
for (tool, takes) in [(ProviderId::Claude, true), (ProviderId::Codex, false)] {
assert_eq!(sign_in_view(tool, said, false).wants_code, takes, "{tool}");
assert!(
!sign_in_view(tool, said, true).wants_code,
"{tool}: asked once"
);
}
}
#[test]
fn a_sign_in_that_has_said_nothing_offers_nothing() {
for &tool in ProviderId::ALL {
assert_eq!(
sign_in_view(tool, "", false),
SignInView::default(),
"{tool}"
);
}
}
#[test]
fn an_address_ends_where_an_address_printed_bare_cannot_go_on() {
let ends = [
'\t', '\n', '\u{b}', '\u{c}', '\r', ' ', '"', '\'', '<', '>', '\u{1b}', '\u{85}',
'\u{a0}', '\u{1680}', '\u{2000}', '\u{200a}', '\u{2028}', '\u{2029}', '\u{202f}',
'\u{205f}', '\u{3000}',
];
for end in ends {
let said = format!("x https://a.b/c{end}d e");
assert_eq!(
https_address(&said).as_deref(),
Some("https://a.b/c"),
"{:?}",
end
);
}
for kept in ['\u{1c}', '\u{7f}', '\u{180e}', '\u{200b}', '\u{feff}'] {
let said = format!("x https://a.b/c{kept}d e");
assert_eq!(
https_address(&said),
Some(format!("https://a.b/c{kept}d")),
"{:?}",
kept
);
}
}
#[test]
fn only_an_https_address_with_something_in_it_is_one_to_open() {
assert_eq!(
https_address("https:// then https://x.y").as_deref(),
Some("https://x.y")
);
assert_eq!(https_address("http://localhost:1455/auth/callback"), None);
assert_eq!(https_address("HTTPS://x.y"), None);
}
#[test]
fn a_hyperlinks_address_is_where_it_goes() {
let cases = [
(
"\u{1b}]8;;https://a.b/c\u{7}sign in\u{1b}]8;;\u{7}",
"https://a.b/c",
),
(
"\u{1b}]8;id=1;https://a.b/c\u{1b}\\https://x.y\u{1b}]8;;\u{1b}\\",
"https://a.b/c",
),
(
"\u{1b}]8;;http://localhost:1455\u{7}\u{1b}[94mhttps://x.y\u{1b}[39m\u{1b}]8;;\u{7}",
"https://x.y",
),
(
"\u{1b}]8;;https://a.b/c d\u{7}https://x.y\u{1b}]8;;\u{7}",
"https://x.y",
),
];
for (said, address) in cases {
assert_eq!(
https_address(&format!("visit: {said}\n")).as_deref(),
Some(address),
"{said:?}"
);
}
}
#[test]
fn an_address_a_terminal_does_not_show_is_none_to_open() {
assert_eq!(https_address("\u{1b}]0;https://a.b/c\u{7}ready"), None);
}
#[test]
fn a_hyperlink_read_part_way_through_offers_all_of_its_address_or_none() {
let said = "visit: \u{1b}]8;;https://a.b/c?d=e\u{7}https://a.b/c?d=e\u{1b}]8;;\u{7}\n";
let whole = said.find('\u{7}').expect("the end of where it goes") + 1;
for (at, _) in said.char_indices().chain([(said.len(), ' ')]) {
let offered = https_address(&said[..at]);
if at < whole {
assert_eq!(offered, None, "{:?}", &said[..at]);
} else {
assert_eq!(offered.as_deref(), Some("https://a.b/c?d=e"), "{at}");
}
}
}
#[test]
#[allow(
clippy::disallowed_methods,
reason = "this process's own PATH is what the context is meant to read"
)]
fn the_search_path_is_this_processs_own_path_unless_given() {
let ctx = Context::new(std::path::PathBuf::from("/nowhere"));
assert_eq!(
ctx.search_path(),
std::env::var_os("PATH").unwrap_or_default()
);
assert_eq!(
ctx.with_search_path("/opt/tools/bin".into()).search_path(),
"/opt/tools/bin"
);
}
}