prodex 0.54.0

OpenAI profile pooling and safe auto-rotate for Codex CLI and Claude Code
Documentation
use super::*;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

fn temp_dir(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let dir = std::env::temp_dir().join(format!(
        "prodex-secret-store-{name}-{}-{nanos:x}",
        std::process::id()
    ));
    fs::create_dir_all(&dir).unwrap();
    dir
}

#[test]
fn auth_json_location_maps_to_expected_path() {
    let home = PathBuf::from("/tmp/codex-home");
    assert_eq!(auth_json_path(&home), home.join("auth.json"));
    assert_eq!(
        auth_json_location(&home),
        SecretLocation::File(home.join("auth.json"))
    );
}

#[test]
fn file_backend_round_trips_text_values() {
    let root = temp_dir("text");
    let path = root.join("nested/auth.json");
    let store = SecretManager::new(FileSecretBackend::new());
    let location = SecretLocation::file(&path);

    store
        .write_text(&location, "{\"access_token\":\"abc\"}")
        .unwrap();

    assert_eq!(
        store.read_text(&location).unwrap().as_deref(),
        Some("{\"access_token\":\"abc\"}")
    );
    assert_eq!(
        store.read(&location).unwrap(),
        Some(SecretValue::Text("{\"access_token\":\"abc\"}".to_string()))
    );

    store.delete(&location).unwrap();
    assert_eq!(store.read(&location).unwrap(), None);

    let _ = fs::remove_dir_all(root);
}

#[test]
fn file_backend_preserves_binary_values() {
    let root = temp_dir("binary");
    let path = root.join("secret.bin");
    let store = SecretManager::new(FileSecretBackend::new());
    let location = SecretLocation::file(&path);
    let payload = SecretValue::bytes(vec![0xff, 0x00, 0x41]);

    store.write(&location, payload.clone()).unwrap();

    assert_eq!(store.read(&location).unwrap(), Some(payload));
    assert!(store.read_text(&location).is_err());

    let _ = fs::remove_dir_all(root);
}

#[test]
fn file_backend_rejects_keyring_locations() {
    let store = SecretManager::new(FileSecretBackend::new());
    let location = SecretLocation::keyring("prodex", "auth");
    let err = store.write_text(&location, "value").unwrap_err();
    assert!(matches!(err, SecretError::UnsupportedLocation { .. }));
}

#[test]
fn keyring_backend_validates_service_name() {
    let backend = KeyringSecretBackend::new("prodex").unwrap();
    assert_eq!(backend.service(), "prodex");

    let err = KeyringSecretBackend::new("   ").unwrap_err();
    assert!(matches!(err, SecretError::InvalidLocation { .. }));
}

#[test]
fn selectable_backend_file_round_trips_text_values() {
    let root = temp_dir("selection-text");
    let path = root.join("nested/auth.json");
    let store = SecretBackendSelection::file().into_manager();
    let location = SecretLocation::file(&path);

    store
        .write_text(&location, "{\"access_token\":\"abc\"}")
        .unwrap();

    assert_eq!(
        store.read_text(&location).unwrap().as_deref(),
        Some("{\"access_token\":\"abc\"}")
    );
    assert_eq!(
        store.read(&location).unwrap(),
        Some(SecretValue::Text("{\"access_token\":\"abc\"}".to_string()))
    );

    store.delete(&location).unwrap();
    assert_eq!(store.read(&location).unwrap(), None);

    let _ = fs::remove_dir_all(root);
}

#[test]
fn selectable_backend_from_kind_requires_keyring_service() {
    assert_eq!(
        SecretBackendSelection::from_kind(SecretBackendKind::File, None)
            .unwrap()
            .kind(),
        SecretBackendKind::File
    );

    let err = SecretBackendSelection::from_kind(SecretBackendKind::Keyring, None).unwrap_err();
    assert!(matches!(err, SecretError::InvalidLocation { .. }));
}

#[test]
fn file_backend_probe_revision_tracks_metadata() {
    let root = temp_dir("revision");
    let path = root.join("secret.bin");
    let store = SecretBackendSelection::file().into_manager();
    let location = SecretLocation::file(&path);

    store
        .write(&location, SecretValue::bytes(vec![0xff, 0x00, 0x41]))
        .unwrap();

    let metadata = fs::metadata(&path).unwrap();
    let revision = store.probe_revision(&location).unwrap();
    assert_eq!(revision, Some(SecretRevision::from_metadata(&metadata)));
    assert_eq!(revision.as_ref().map(SecretRevision::size_bytes), Some(3));
    assert_eq!(
        revision.as_ref().and_then(SecretRevision::modified_at),
        metadata.modified().ok()
    );

    store
        .write(&location, SecretValue::bytes(vec![0xff, 0x00, 0x41, 0x42]))
        .unwrap();
    let updated_revision = store.probe_revision(&location).unwrap();
    assert_ne!(revision, updated_revision);

    store.delete(&location).unwrap();
    assert_eq!(store.probe_revision(&location).unwrap(), None);

    let _ = fs::remove_dir_all(root);
}

#[test]
fn auth_json_location_for_keyring_backend_uses_deterministic_account() {
    let selection = SecretBackendSelection::keyring("prodex").unwrap();
    let location = auth_json_location_for_backend("/tmp/codex-home", &selection);
    assert_eq!(
        location,
        SecretLocation::Keyring {
            service: "prodex".to_string(),
            account: "auth-json:/tmp/codex-home".to_string(),
        }
    );
    assert_eq!(
        describe_secret_location(&location),
        "keyring://prodex/auth-json:/tmp/codex-home"
    );
}