sysknife-daemon 0.2.7

Policy-enforced, audit-logged Linux action daemon for SysKnife
Documentation
use sysknife_daemon::auth::highest_role_from_groups;
use sysknife_daemon::jobs::JobStateMachine;
use sysknife_daemon::state::DaemonConfig;
use sysknife_daemon::transactions::{NewTransaction, TransactionStore};
use sysknife_daemon::transport::listen::{bind_unix_listener, ListenTarget};
use sysknife_types::{CallerRole, JobState, RiskLevel};
use tempfile::tempdir;

#[test]
fn unix_socket_startup_rejects_tcp_uris() {
    let unix =
        ListenTarget::try_from_uri("unix:///tmp/sysknife.sock").expect("unix uri should parse");

    assert!(matches!(unix, ListenTarget::Unix(_)));
    assert!(ListenTarget::try_from_uri("unix://relative.sock").is_err());
    assert!(ListenTarget::try_from_uri("tcp://127.0.0.1:7000").is_err());
}

#[test]
fn unix_socket_listener_is_created_on_disk() {
    let dir = tempdir().expect("tempdir");
    let socket_path = dir.path().join("sysknife.sock");
    let target = ListenTarget::try_from_uri(&format!("unix://{}", socket_path.display()))
        .expect("unix uri should parse");

    let listener = bind_unix_listener(&target).expect("bind unix listener");
    assert!(socket_path.exists());
    drop(listener);
}

#[test]
fn unix_socket_listener_rejects_existing_non_socket_path() {
    let dir = tempdir().expect("tempdir");
    let socket_path = dir.path().join("sysknife.sock");
    std::fs::write(&socket_path, "not a socket").expect("write placeholder file");
    let target = ListenTarget::try_from_uri(&format!("unix://{}", socket_path.display()))
        .expect("unix uri should parse");

    assert!(bind_unix_listener(&target).is_err());
}

#[test]
fn daemon_bootstrap_opens_listener_and_state() {
    let dir = tempdir().expect("tempdir");
    let socket_path = dir.path().join("daemon.sock");
    let db_path = dir.path().join("daemon.sqlite");
    let target = ListenTarget::try_from_uri(&format!("unix://{}", socket_path.display()))
        .expect("unix uri should parse");
    let config = DaemonConfig::new(target, &db_path);

    let runtime = sysknife_daemon::state::DaemonState::bootstrap(config).expect("bootstrap");
    assert!(socket_path.exists());
    assert!(db_path.exists());
    assert_eq!(runtime.state.config.database_path, db_path);
}

#[test]
fn highest_role_is_derived_from_local_groups() {
    assert_eq!(highest_role_from_groups(["sysknife-dev"]), CallerRole::Dev);
    assert_eq!(
        highest_role_from_groups(["wheel", "sysknife-dev"]),
        CallerRole::Admin
    );
    assert_eq!(
        highest_role_from_groups(["sysknife-boot", "wheel"]),
        CallerRole::Boot
    );
    assert_eq!(
        highest_role_from_groups(std::iter::empty::<&str>()),
        CallerRole::Observer
    );
}

#[test]
fn transaction_records_are_persisted() {
    let dir = tempdir().expect("tempdir");
    let db_path = dir.path().join("sysknife.sqlite");
    let store = TransactionStore::open(&db_path).expect("open store");

    let transaction = NewTransaction {
        request_id: "request-1".into(),
        request_hash: "hash-1".into(),
        action_name: "UpdateSystem".into(),
        risk_level: RiskLevel::High,
        approval_id: Some("approval-1".into()),
        summary: "Stage system update".into(),
        warnings: vec!["reboot required".into()],
    };

    let record = store.record(transaction).expect("record tx");
    let loaded = store
        .get(&record.transaction_id)
        .expect("load tx")
        .expect("transaction exists");

    assert_eq!(loaded.request_id, "request-1");
    assert_eq!(loaded.request_hash, "hash-1");
    assert_eq!(loaded.action_name, "UpdateSystem");
    assert_eq!(loaded.status, JobState::Queued);
    assert_eq!(loaded.approval_id.as_deref(), Some("approval-1"));
    assert_eq!(loaded.warnings, vec!["reboot required".to_string()]);
}

#[test]
fn job_state_machine_rejects_invalid_transitions() {
    let mut job = JobStateMachine::new("job-1");

    assert_eq!(job.state(), JobState::Queued);
    job.transition_to(JobState::Running)
        .expect("queued -> running");
    job.transition_to(JobState::NeedsReboot)
        .expect("running -> needs reboot");
    assert_eq!(job.state(), JobState::NeedsReboot);
    assert!(job.transition_to(JobState::Running).is_err());
}