use std::fs::{File, Permissions};
use std::path::PathBuf;
use super::*;
fn target(extension: &str, provider: &str) -> TestingProvider {
TestingProvider {
extension: tau_proto::ExtensionName::parse(extension).expect("extension"),
provider: tau_proto::ProviderName::new(provider),
}
}
#[test]
fn missing_testing_config_keeps_provider_access_disabled() {
let access = provider_access_from_settings(None, PathBuf::from("/tmp/scratch/state"), None);
assert!(access.is_missing_config());
assert!(!access.provider_extension_enabled());
}
#[test]
fn provider_allowlist_copies_exact_instance_registration() {
let temp = tempfile::tempdir().expect("tempdir");
let source = temp.path().join("real");
let scratch = temp.path().join("scratch");
let allowed = target("provider-builtin", "chatgpt");
let denied = target("provider-builtin", "openrouter");
for entry in [&allowed, &denied] {
let settings = extension_provider_settings_dir_of(&source, entry.extension.as_str())
.expect("settings root")
.join(format!("{}.json", entry.provider));
std::fs::create_dir_all(settings.parent().expect("parent")).expect("settings dir");
std::fs::write(
&settings,
format!(
r#"{{"kind":"chatgpt","credential":{{"kind":"oauth","identity":"{}"}}}}"#,
if entry.provider.as_str() == "chatgpt" {
"0123456789abcdef0123456789abcdef"
} else {
"fedcba9876543210fedcba9876543210"
}
),
)
.expect("settings");
let secrets = extension_secret_dir_of(&source, entry.extension.as_str())
.expect("secret root")
.join("providers")
.join(if entry.provider.as_str() == "chatgpt" {
"0123456789abcdef0123456789abcdef"
} else {
"fedcba9876543210fedcba9876543210"
});
std::fs::create_dir_all(&secrets).expect("secret dir");
std::fs::write(
secrets.join("oauth.json"),
format!("secret:{}", entry.provider),
)
.expect("secret");
}
let access = provider_access_from_settings(
Some(source),
scratch.clone(),
Some(TestingSettings {
testing_providers: vec![allowed.clone()],
}),
);
access.copy_allowed_profiles().expect("copy registration");
let copied_settings = extension_provider_settings_dir_of(&scratch, allowed.extension.as_str())
.expect("settings")
.join("chatgpt.json");
let copied_secret = extension_secret_dir_of(&scratch, allowed.extension.as_str())
.expect("secrets")
.join("providers/0123456789abcdef0123456789abcdef/oauth.json");
assert_eq!(
std::fs::read_to_string(copied_settings).expect("read"),
r#"{"kind":"chatgpt","credential":{"kind":"oauth","identity":"0123456789abcdef0123456789abcdef"}}"#
);
assert_eq!(
std::fs::read_to_string(copied_secret).expect("read"),
"secret:chatgpt"
);
assert!(
!extension_provider_settings_dir_of(&scratch, denied.extension.as_str())
.expect("settings")
.join("openrouter.json")
.exists()
);
}
#[cfg(unix)]
#[test]
fn provider_allowlist_copies_external_config_profile_symlink() {
use std::os::unix::fs::{PermissionsExt as _, symlink};
let temp = tempfile::tempdir().expect("tempdir");
let config = temp.path().join("config");
let state = temp.path().join("state");
let scratch = temp.path().join("scratch");
let allowed = target("provider-builtin", "chatgpt");
let deployment = temp.path().join("nix-store-chatgpt.json");
std::fs::write(
&deployment,
r#"{"kind":"chatgpt","credential":{"kind":"oauth","identity":"0123456789abcdef0123456789abcdef"}}"#,
)
.expect("deployment");
std::fs::set_permissions(&deployment, Permissions::from_mode(0o444))
.expect("read-only deployment");
let profile = extension_provider_config_dir_of(&config, allowed.extension.as_str())
.expect("config root")
.join("chatgpt.json");
std::fs::create_dir_all(profile.parent().expect("profile parent")).expect("config instance");
symlink(&deployment, profile).expect("profile symlink");
let secrets = extension_secret_dir_of(&state, allowed.extension.as_str())
.expect("secret root")
.join("providers/0123456789abcdef0123456789abcdef");
std::fs::create_dir_all(&secrets).expect("secret directory");
std::fs::write(secrets.join("oauth.json"), "host-secret").expect("secret");
let access = provider_access_from_dirs_and_settings(
Some(config),
Some(state),
scratch.clone(),
Some(TestingSettings {
testing_providers: vec![allowed],
}),
);
access.copy_allowed_profiles().expect("copy registration");
assert_eq!(
std::fs::read_to_string(scratch.join("providers/provider-builtin/chatgpt.json"))
.expect("copied profile"),
r#"{"kind":"chatgpt","credential":{"kind":"oauth","identity":"0123456789abcdef0123456789abcdef"}}"#
);
assert_eq!(
std::fs::read_to_string(scratch.join(
"secrets/ext/provider-builtin/providers/0123456789abcdef0123456789abcdef/oauth.json"
))
.expect("copied secret"),
"host-secret"
);
}
#[cfg(unix)]
#[test]
fn provider_allowlist_rejects_oversized_external_config_profile() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().expect("tempdir");
let config = temp.path().join("config");
let state = temp.path().join("state");
let scratch = temp.path().join("scratch");
let allowed = target("provider-builtin", "chatgpt");
let deployment = temp.path().join("oversized.json");
let file = File::create(&deployment).expect("deployment");
file.set_len(tau_config::provider_settings::MAX_PROVIDER_PROFILE_FILE_BYTES + 1)
.expect("oversized deployment");
let profile = extension_provider_config_dir_of(&config, allowed.extension.as_str())
.expect("config root")
.join("chatgpt.json");
std::fs::create_dir_all(profile.parent().expect("profile parent")).expect("config instance");
symlink(deployment, profile).expect("profile symlink");
let access = provider_access_from_dirs_and_settings(
Some(config),
Some(state),
scratch.clone(),
Some(TestingSettings {
testing_providers: vec![allowed],
}),
);
let error = access
.copy_allowed_profiles()
.expect_err("oversized profile");
assert!(error.to_string().contains("exceeds"));
assert!(!scratch.join(PROVIDER_SETTINGS_DIR).exists());
}
#[test]
fn provider_allowlist_rejects_too_many_profiles() {
let temp = tempfile::tempdir().expect("tempdir");
let profiles = (0..=tau_config::provider_settings::MAX_PROVIDER_PROFILE_FILES)
.map(|index| target("provider-builtin", &format!("p-{index}")))
.collect();
let access = provider_access_from_dirs_and_settings(
None,
None,
temp.path().join("scratch"),
Some(TestingSettings {
testing_providers: profiles,
}),
);
let error = access
.copy_allowed_profiles()
.expect_err("too many profiles");
assert!(error.to_string().contains("allowlist for instance"));
assert!(error.to_string().contains("exceeds"));
}
#[test]
fn provider_allowlist_rejects_per_instance_aggregate_profile_bytes() {
let temp = tempfile::tempdir().expect("tempdir");
let config = temp.path().join("config");
let state = temp.path().join("state");
let scratch = temp.path().join("scratch");
let mut profiles = Vec::new();
for index in 0..17 {
let entry = target("provider-builtin", &format!("p-{index}"));
let profile = extension_provider_config_dir_of(&config, entry.extension.as_str())
.expect("config root")
.join(format!("{}.json", entry.provider));
std::fs::create_dir_all(profile.parent().expect("profile parent"))
.expect("config instance");
let size = usize::try_from(tau_config::provider_settings::MAX_PROVIDER_PROFILE_FILE_BYTES)
.expect("profile limit fits usize");
let mut settings = br#"{"credential":{"kind":"none"}}"#.to_vec();
settings.resize(size, b' ');
std::fs::write(profile, settings).expect("bounded profile");
profiles.push(entry);
}
let access = provider_access_from_dirs_and_settings(
Some(config),
Some(state),
scratch.clone(),
Some(TestingSettings {
testing_providers: profiles,
}),
);
let error = access.copy_allowed_profiles().expect_err("aggregate bound");
assert!(
error.to_string().contains("snapshot for instance"),
"{error}"
);
assert!(error.to_string().contains("exceeds"), "{error}");
assert!(!scratch.join(PROVIDER_SETTINGS_DIR).exists());
}
#[test]
fn empty_allowlist_removes_stale_settings_and_secrets() {
let temp = tempfile::tempdir().expect("tempdir");
let scratch = temp.path().join("scratch");
let settings = scratch.join(PROVIDER_SETTINGS_DIR).join("stale/file.json");
let secret = scratch
.join(EXTENSION_SECRETS_DIR)
.join("stale/providers/p/oauth.json");
std::fs::create_dir_all(settings.parent().expect("parent")).expect("settings dir");
std::fs::create_dir_all(secret.parent().expect("parent")).expect("secret dir");
std::fs::write(settings, "stale").expect("settings");
std::fs::write(secret, "stale").expect("secret");
let access = provider_access_from_settings(
None,
scratch.clone(),
Some(TestingSettings {
testing_providers: Vec::new(),
}),
);
access.copy_allowed_profiles().expect("reconcile");
assert!(!scratch.join(PROVIDER_SETTINGS_DIR).exists());
assert!(!scratch.join(EXTENSION_SECRETS_DIR).exists());
}
#[test]
fn partial_copy_failure_reconciles_both_scratch_trees() {
let temp = tempfile::tempdir().expect("tempdir");
let source = temp.path().join("real");
let scratch = temp.path().join("scratch");
let valid = target("provider-builtin", "chatgpt");
let missing = target("provider-work", "missing");
let settings = extension_provider_settings_dir_of(&source, valid.extension.as_str())
.expect("settings root")
.join("chatgpt.json");
std::fs::create_dir_all(settings.parent().expect("parent")).expect("settings dir");
std::fs::write(
settings,
r#"{"kind":"chatgpt","credential":{"kind":"oauth","identity":"0123456789abcdef0123456789abcdef"}}"#,
)
.expect("settings");
let secrets = extension_secret_dir_of(&source, valid.extension.as_str())
.expect("secret root")
.join("providers/0123456789abcdef0123456789abcdef");
std::fs::create_dir_all(&secrets).expect("secret dir");
std::fs::write(secrets.join("oauth.json"), "secret").expect("secret");
let access = provider_access_from_settings(
Some(source),
scratch.clone(),
Some(TestingSettings {
testing_providers: vec![valid, missing],
}),
);
access
.copy_allowed_profiles()
.expect_err("missing registration must fail");
assert!(!scratch.join(PROVIDER_SETTINGS_DIR).exists());
assert!(!scratch.join(EXTENSION_SECRETS_DIR).exists());
}
#[cfg(unix)]
#[test]
fn source_secret_symlink_fails_closed() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().expect("tempdir");
let source = temp.path().join("real");
let scratch = temp.path().join("scratch");
let entry = target("provider-builtin", "chatgpt");
let settings = extension_provider_settings_dir_of(&source, entry.extension.as_str())
.expect("settings")
.join("chatgpt.json");
std::fs::create_dir_all(settings.parent().expect("parent")).expect("dir");
std::fs::write(
settings,
r#"{"kind":"chatgpt","credential":{"kind":"oauth","identity":"0123456789abcdef0123456789abcdef"}}"#,
)
.expect("settings");
let credential_dir = extension_secret_dir_of(&source, entry.extension.as_str())
.expect("secrets")
.join("providers/0123456789abcdef0123456789abcdef");
std::fs::create_dir_all(&credential_dir).expect("dir");
let outside = temp.path().join("outside");
std::fs::write(&outside, "secret").expect("outside");
symlink(outside, credential_dir.join("oauth.json")).expect("symlink");
let access = provider_access_from_settings(
Some(source),
scratch,
Some(TestingSettings {
testing_providers: vec![entry],
}),
);
assert!(access.copy_allowed_profiles().is_err());
}
#[cfg(unix)]
#[test]
fn source_settings_ancestor_symlink_fails_closed() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().expect("tempdir");
let source = temp.path().join("real");
let scratch = temp.path().join("scratch");
let outside = temp.path().join("outside");
std::fs::create_dir_all(outside.join("provider-builtin")).expect("outside");
std::fs::write(
outside.join("provider-builtin/chatgpt.json"),
"outside-settings",
)
.expect("outside settings");
std::fs::create_dir_all(&source).expect("source");
symlink(&outside, source.join(PROVIDER_SETTINGS_DIR)).expect("settings ancestor symlink");
let access = provider_access_from_settings(
Some(source),
scratch.clone(),
Some(TestingSettings {
testing_providers: vec![target("provider-builtin", "chatgpt")],
}),
);
access
.copy_allowed_profiles()
.expect_err("ancestor symlink must fail");
assert!(!scratch.join(PROVIDER_SETTINGS_DIR).exists());
assert!(!scratch.join(EXTENSION_SECRETS_DIR).exists());
}