systemd-manager-tui 1.3.0

TUI for managing systemd services
use super::*;
use crate::test_support::{FakeRepository, service};
use std::sync::mpsc;
use std::time::Duration;

fn list_context(generation: u64) -> ListRequestContext {
    ListRequestContext {
        connection: ConnectionType::System,
        generation,
    }
}

#[test]
fn lists_sorts_and_deduplicates_runtime_and_file_units() {
    let fake = FakeRepository::with_services(
        vec![
            service("zeta.service", "active", "enabled"),
            service("alpha.service", "active", "enabled"),
        ],
        vec![
            service("alpha.service", "inactive", "disabled"),
            service("beta.service", "inactive", "disabled"),
        ],
    );
    let manager = ServicesManager::new(Box::new(fake));
    let (sender, receiver) = mpsc::channel();

    let services = manager
        .list_services(true, list_context(1), Arc::new(sender))
        .unwrap();

    let names: Vec<_> = services.iter().map(Service::name).collect();
    assert_eq!(names, ["alpha.service", "beta.service", "zeta.service"]);
    let states = match receiver
        .recv_timeout(Duration::from_secs(1))
        .expect("unit file states were not returned")
    {
        QueryUnitFile::Finished(context, states) => {
            assert_eq!(context, list_context(1));
            states
        }
        QueryUnitFile::Error(_, error) => panic!("unit file lookup failed: {error}"),
    };
    assert_eq!(
        states.get("alpha.service").map(String::as_str),
        Some("enabled")
    );
    assert_eq!(
        states.get("zeta.service").map(String::as_str),
        Some("enabled")
    );
}

#[test]
fn background_state_errors_are_returned_to_the_caller() {
    let fake =
        FakeRepository::with_services(vec![service("demo.service", "active", "enabled")], vec![]);
    fake.fail("states");
    let manager = ServicesManager::new(Box::new(fake));
    let (sender, receiver) = mpsc::channel();

    manager
        .list_services(false, list_context(2), Arc::new(sender))
        .unwrap();

    assert!(matches!(
        receiver.recv_timeout(Duration::from_secs(1)),
        Ok(QueryUnitFile::Error(context, error))
            if context == list_context(2) && error == "states failed"
    ));
}

#[test]
fn unit_file_lookup_reuses_the_services_from_the_foreground_query() {
    let fake =
        FakeRepository::with_services(vec![service("demo.service", "active", "enabled")], vec![]);
    let observer = fake.clone();
    let manager = ServicesManager::new(Box::new(fake));
    let (sender, receiver) = mpsc::channel();

    manager
        .list_services(false, list_context(6), Arc::new(sender))
        .unwrap();
    assert!(matches!(
        receiver.recv_timeout(Duration::from_secs(1)),
        Ok(QueryUnitFile::Finished(context, _)) if context == list_context(6)
    ));

    assert_eq!(observer.calls(), ["list:false", "states:1"]);
}

#[test]
fn service_only_listing_does_not_add_unit_files() {
    let fake = FakeRepository::with_services(
        vec![service("runtime.service", "active", "enabled")],
        vec![service("file-only.service", "inactive", "disabled")],
    );
    let manager = ServicesManager::new(Box::new(fake));
    let (sender, _receiver) = mpsc::channel();

    let services = manager
        .list_services(false, list_context(3), Arc::new(sender))
        .unwrap();

    assert_eq!(services.len(), 1);
    assert_eq!(services[0].name(), "runtime.service");
}

#[test]
fn enable_and_disable_reload_the_daemon() {
    let fake = FakeRepository::default();
    let observer = fake.clone();
    let manager = ServicesManager::new(Box::new(fake));
    let unit = service("demo.service", "inactive", "disabled");

    manager.enable_service(&unit).unwrap();
    manager.disable_service(&unit).unwrap();

    assert_eq!(
        observer.calls(),
        [
            "enable:demo.service",
            "reload:",
            "disable:demo.service",
            "reload:",
        ]
    );
}

#[test]
fn delegates_all_lifecycle_operations() {
    let fake = FakeRepository::default();
    let observer = fake.clone();
    let manager = ServicesManager::new(Box::new(fake));
    let unit = service("demo.service", "inactive", "disabled");

    manager.start_service(&unit).unwrap();
    manager.stop_service(&unit).unwrap();
    manager.restart_service(&unit).unwrap();
    manager.mask_service(&unit).unwrap();
    manager.unmask_service(&unit).unwrap();

    assert_eq!(
        observer.calls(),
        [
            "start:demo.service",
            "stop:demo.service",
            "restart:demo.service",
            "mask:demo.service",
            "unmask:demo.service",
        ]
    );
}

#[test]
fn propagates_repository_errors_without_reloading() {
    let fake = FakeRepository::default();
    fake.fail("enable");
    let observer = fake.clone();
    let manager = ServicesManager::new(Box::new(fake));
    let unit = service("broken.service", "inactive", "disabled");

    let error = manager.enable_service(&unit).unwrap_err();

    assert_eq!(error.to_string(), "enable failed");
    assert_eq!(observer.calls(), ["enable:broken.service"]);
}

#[test]
fn delegates_log_unit_file_timestamp_and_connection() {
    let fake = FakeRepository::with_content("journal output", "[Service]\nExecStart=/bin/true", 42);
    let observer = fake.clone();
    let mut manager = ServicesManager::new(Box::new(fake));
    let unit = service("demo.service", "active", "enabled");

    assert_eq!(manager.get_log(unit.name()).unwrap(), "journal output");
    assert!(manager.systemctl_cat(&unit).unwrap().contains("ExecStart"));
    manager
        .change_repository_connection(ConnectionType::Session)
        .unwrap();
    assert_eq!(
        manager
            .repository_handle()
            .lock()
            .unwrap()
            .get_active_enter_timestamp(unit.name())
            .unwrap(),
        42
    );

    assert_eq!(
        observer.calls(),
        [
            "log:demo.service",
            "cat:demo.service",
            "connection:session",
            "timestamp:demo.service"
        ]
    );
}

#[test]
fn list_failure_is_returned_without_attempting_file_lookup() {
    let fake = FakeRepository::default();
    fake.fail("list");
    let observer = fake.clone();
    let manager = ServicesManager::new(Box::new(fake));
    let (sender, _receiver) = mpsc::channel();

    let error = manager
        .list_services(true, list_context(4), Arc::new(sender))
        .unwrap_err();

    assert_eq!(error.to_string(), "list failed");
    assert_eq!(observer.calls(), ["list:true"]);
}

#[test]
fn daemon_reload_failure_is_propagated_after_enable() {
    let fake = FakeRepository::default();
    fake.fail("reload");
    let observer = fake.clone();
    let manager = ServicesManager::new(Box::new(fake));
    let unit = service("demo.service", "inactive", "disabled");

    let error = manager.enable_service(&unit).unwrap_err();

    assert_eq!(error.to_string(), "reload failed");
    assert_eq!(observer.calls(), ["enable:demo.service", "reload:"]);
}

#[test]
fn log_and_unit_file_failures_are_propagated() {
    let fake = FakeRepository::default();
    fake.fail("log");
    fake.fail("cat");
    let manager = ServicesManager::new(Box::new(fake));
    let unit = service("broken.service", "active", "enabled");

    assert_eq!(
        manager.get_log(unit.name()).unwrap_err().to_string(),
        "log failed"
    );
    assert_eq!(
        manager.systemctl_cat(&unit).unwrap_err().to_string(),
        "cat failed"
    );
}

#[test]
fn service_sorting_is_case_insensitive() {
    let fake = FakeRepository::with_services(
        vec![
            service("Zulu.service", "active", "enabled"),
            service("alpha.service", "active", "enabled"),
            service("Beta.service", "active", "enabled"),
        ],
        vec![],
    );
    let manager = ServicesManager::new(Box::new(fake));
    let (sender, _receiver) = mpsc::channel();

    let services = manager
        .list_services(false, list_context(5), Arc::new(sender))
        .unwrap();
    let names: Vec<_> = services.iter().map(Service::name).collect();

    assert_eq!(names, ["alpha.service", "Beta.service", "Zulu.service"]);
}