tg-cli 0.2.1

A "unix-like" utility for sending yourself Telegram messages from the terminal
Documentation
use std::io;

use keyring::{Entry, Error, mock, mock::MockCredential};

use super::{
    SecretStoreError, backend_unavailable, delete_token_from_entry, ensure_non_mock_backend,
    is_unavailable, load_token_from_entry, map_delete_result, save_token_to_entry,
};

macro_rules! io_error_variant {
    ($variant:ident, $message:expr) => {
        Error::$variant(Box::new(io::Error::other($message)))
    };
}

#[test]
fn marks_no_storage_access_as_unavailable() {
    assert!(backend_unavailable(&io_error_variant!(
        NoStorageAccess,
        "permission denied"
    )));
}

#[test]
fn marks_platform_failures_with_secret_service_signals_as_unavailable() {
    assert!(backend_unavailable(&io_error_variant!(
        PlatformFailure,
        "Secret Service is not available on this session bus"
    )));
}

#[test]
fn does_not_mark_unrelated_platform_failures_as_unavailable() {
    assert!(!backend_unavailable(&io_error_variant!(
        PlatformFailure,
        "unexpected backend timeout"
    )));
}

#[test]
fn does_not_mark_no_entry_as_unavailable() {
    assert!(!backend_unavailable(&Error::NoEntry));
}

#[test]
fn unavailable_helper_matches_only_unavailable_variant() {
    assert!(is_unavailable(&SecretStoreError::Unavailable(
        "dbus unavailable".to_string()
    )));

    assert!(!is_unavailable(&SecretStoreError::Backend(
        keyring::Error::NoEntry
    )));
}

#[test]
fn delete_mapping_treats_no_entry_as_success() {
    assert!(map_delete_result(Err(Error::NoEntry)).is_ok());
}

#[test]
fn delete_mapping_marks_storage_access_errors_as_unavailable() {
    assert!(matches!(
        map_delete_result(Err(io_error_variant!(NoStorageAccess, "permission denied"))),
        Err(SecretStoreError::Unavailable(_))
    ));
}

#[test]
fn delete_mapping_preserves_non_unavailable_backend_errors() {
    assert!(matches!(
        map_delete_result(Err(Error::Invalid(
            "service".to_string(),
            "bad entry".to_string(),
        ))),
        Err(SecretStoreError::Backend(_))
    ));
}

#[test]
fn mock_store_is_treated_as_unavailable() {
    assert!(matches!(
        ensure_non_mock_backend(mock_entry()),
        Err(SecretStoreError::Unavailable(_))
    ));
}

#[test]
fn unavailable_store_errors_are_mapped_to_unavailable() {
    let err = || io_error_variant!(NoStorageAccess, "dbus unavailable");

    assert!(matches!(
        run_with_mock_error(err(), load_token_from_entry),
        Err(SecretStoreError::Unavailable(_))
    ));

    assert!(matches!(
        run_with_mock_error(err(), |entry| save_token_to_entry(entry, "abc123")),
        Err(SecretStoreError::Unavailable(_))
    ));

    assert!(matches!(
        run_with_mock_error(err(), delete_token_from_entry),
        Err(SecretStoreError::Unavailable(_))
    ));
}

#[test]
fn generic_store_errors_are_mapped_to_backend() {
    let err = || Error::Invalid("service".to_string(), "bad request".to_string());

    assert!(matches!(
        run_with_mock_error(err(), load_token_from_entry),
        Err(SecretStoreError::Backend(_))
    ));

    assert!(matches!(
        run_with_mock_error(err(), |entry| save_token_to_entry(entry, "abc123")),
        Err(SecretStoreError::Backend(_))
    ));

    assert!(matches!(
        run_with_mock_error(err(), delete_token_from_entry),
        Err(SecretStoreError::Backend(_))
    ));
}

#[test]
fn token_round_trip_succeeds_with_persistent_backend() {
    let entry = mock_entry();

    assert_eq!(
        load_token_from_entry(&entry).expect("initial load should succeed"),
        None
    );

    save_token_to_entry(&entry, "abc123").expect("save should succeed");

    assert_eq!(
        load_token_from_entry(&entry).expect("load after save should succeed"),
        Some("abc123".to_string())
    );

    delete_token_from_entry(&entry).expect("delete should succeed");

    assert_eq!(
        load_token_from_entry(&entry).expect("load after delete should succeed"),
        None
    );
}

fn mock_entry() -> Entry {
    let builder = mock::default_credential_builder();
    let credential = builder
        .build(None, "service", "user")
        .expect("failed to build mock credential");
    Entry::new_with_credential(credential)
}

fn set_mock_error(entry: &Entry, err: Error) {
    let mock: &MockCredential = entry
        .get_credential()
        .downcast_ref()
        .expect("downcast to MockCredential failed");
    mock.set_error(err);
}

fn run_with_mock_error<T>(
    err: Error,
    operation: impl FnOnce(&Entry) -> Result<T, SecretStoreError>,
) -> Result<T, SecretStoreError> {
    let entry = mock_entry();
    set_mock_error(&entry, err);
    operation(&entry)
}