Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions code-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions code-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ strip = "symbols"
codegen-units = 1

[patch.crates-io]
cc = { git = "https://github.com/alexcrichton/cc-rs", rev = "d740f9b1f5d65b09ccac41cac2e40caa8958e348" }
# ratatui = { path = "../../ratatui" }
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }

Expand Down
144 changes: 144 additions & 0 deletions code-rs/core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ fn load_auth(
None => Ok(None),
};
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(None);
}
// Though if auth.json exists but is malformed, do not fall back to the
// env var because the user may be expecting to use AuthMode::ChatGPT.
Err(e) => {
Expand Down Expand Up @@ -384,6 +387,9 @@ pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
}

pub fn write_auth_json(auth_file: &Path, auth_dot_json: &AuthDotJson) -> std::io::Result<()> {
if let Some(parent) = auth_file.parent() {
std::fs::create_dir_all(parent)?;
}
let json_data = serde_json::to_string_pretty(auth_dot_json)?;
let mut options = OpenOptions::new();
options.truncate(true).write(true).create(true);
Expand Down Expand Up @@ -519,6 +525,8 @@ mod tests {
use serde::Serialize;
use serde_json::json;
use tempfile::tempdir;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

const LAST_REFRESH: &str = "2025-08-06T20:41:36.232376Z";

Expand All @@ -542,6 +550,28 @@ mod tests {
assert_eq!(auth_dot_json, same_auth_dot_json);
}

#[test]
fn write_auth_json_creates_missing_parent_directory() {
let temp = tempdir().unwrap();
let auth_path = temp
.path()
.join("nested")
.join("dir")
.join("auth.json");

let auth_dot_json = AuthDotJson {
openai_api_key: Some("sk-test".to_string()),
tokens: None,
last_refresh: None,
};

write_auth_json(&auth_path, &auth_dot_json).expect("write should create parent dirs");
assert!(auth_path.exists());

let read_back = try_read_auth_json(&auth_path).expect("auth json should parse");
assert_eq!(read_back.openai_api_key.as_deref(), Some("sk-test"));
}

#[test]
fn login_with_api_key_overwrites_existing_auth_json() {
let dir = tempdir().unwrap();
Expand All @@ -568,6 +598,50 @@ mod tests {
assert!(auth.tokens.is_none(), "tokens should be cleared");
}

#[test]
fn login_with_api_key_creates_code_home_directory() {
let dir = tempdir().unwrap();
let missing = dir.path().join("missing").join("code_home");

super::login_with_api_key(&missing, "sk-new")
.expect("login_with_api_key should create directory");

let auth = super::try_read_auth_json(&get_auth_file(&missing)).expect("auth json exists");
assert_eq!(auth.openai_api_key.as_deref(), Some("sk-new"));
}

#[cfg(target_os = "windows")]
#[test]
fn login_with_api_key_supports_backslash_paths() {
use std::path::PathBuf;

let dir = tempdir().unwrap();
let path = PathBuf::from(format!("{}\\nested\\code_home", dir.path().display()));

super::login_with_api_key(&path, "sk-windows")
.expect("login_with_api_key should succeed on Windows-style path");

let auth_path = get_auth_file(&path);
assert!(auth_path.exists(), "auth.json should be created for Windows path");
}

#[cfg(target_os = "macos")]
#[test]
fn login_with_api_key_handles_paths_with_spaces() {
let dir = tempdir().unwrap();
let path = dir
.path()
.join("Library")
.join("Application Support")
.join("Code Home");

super::login_with_api_key(&path, "sk-macos")
.expect("login_with_api_key should succeed for paths with spaces");

let auth = super::try_read_auth_json(&get_auth_file(&path)).expect("auth json exists");
assert_eq!(auth.openai_api_key.as_deref(), Some("sk-macos"));
}

#[tokio::test]
async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
let code_home = tempdir().unwrap();
Expand Down Expand Up @@ -718,6 +792,76 @@ mod tests {
assert!(auth.get_token_data().await.is_err());
}

#[test]
fn load_auth_returns_none_when_auth_json_missing() {
let dir = tempdir().unwrap();

let result = super::load_auth(dir.path(), false, AuthMode::ChatGPT, "code_cli_rs")
.expect("missing auth.json should not error");

assert!(result.is_none(), "missing auth.json should return None");
}

#[cfg(unix)]
#[test]
fn write_auth_json_propagates_permission_denied_unix() {
let dir = tempdir().unwrap();
let read_only_dir = dir.path().join("readonly");
std::fs::create_dir(&read_only_dir).unwrap();
let mut perms = std::fs::metadata(&read_only_dir).unwrap().permissions();
perms.set_mode(0o500);
std::fs::set_permissions(&read_only_dir, perms).unwrap();

let auth_path = read_only_dir.join("nested").join("auth.json");
let result = write_auth_json(
&auth_path,
&AuthDotJson {
openai_api_key: Some("sk-unix".to_string()),
tokens: None,
last_refresh: None,
},
);

assert_eq!(
result.unwrap_err().kind(),
std::io::ErrorKind::PermissionDenied
);
}

#[test]
fn write_auth_json_fails_when_file_is_readonly() {
let dir = tempdir().unwrap();
let auth_path = dir.path().join("auth.json");

write_auth_json(
&auth_path,
&AuthDotJson {
openai_api_key: Some("sk-initial".to_string()),
tokens: None,
last_refresh: None,
},
)
.expect("initial write succeeds");

let mut perms = std::fs::metadata(&auth_path).unwrap().permissions();
perms.set_readonly(true);
std::fs::set_permissions(&auth_path, perms.clone()).unwrap();

let result = write_auth_json(
&auth_path,
&AuthDotJson {
openai_api_key: Some("sk-readonly".to_string()),
tokens: None,
last_refresh: None,
},
);

assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied);

perms.set_readonly(false);
std::fs::set_permissions(&auth_path, perms).unwrap();
}

#[test]
fn logout_removes_auth_file() -> Result<(), std::io::Error> {
let dir = tempdir()?;
Expand Down
Loading