systemdaemon 1.0.1

System daemon building blocks. Designed for but not limited to systemd.
Documentation
// SPDX-FileCopyrightText: 2025 Simon BrummeR
//
// SPDX-License-Identifier: MPL-2.0

#[cfg(test)]
mod tests {
    use crate::error::mocks::MockError;
    use crate::{LoadError, MockConfigFormat, SaveError, ServiceConfig};

    use mockall::predicate::eq;
    use std::io::ErrorKind;
    use std::path::PathBuf;

    /// Expected behavior:
    /// If an load attempt fails with an IO error (e.g. file does not exist) then
    /// saving the default configuration is attempted. If this fails for any reason an error is returned.
    #[test]
    fn try_load_service_config_loading_and_saving_fails_return_error() {
        let path = PathBuf::default();
        let config_data = String::default();

        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Err(LoadError::Io(ErrorKind::NotFound.into())));

        mock.expect_try_save()
            .with(eq(path.clone()), eq(config_data.clone()))
            .times(1)
            .returning(|_, _| Err(SaveError::Io(ErrorKind::NotFound.into())));

        assert!(ServiceConfig::try_load(path.as_path(), mock).is_err());
    }

    /// Expected behavior:
    /// If an load attempt fails due to an serialization error, then a configuration file exists
    /// and with bogus content. In this case the configuration is not overwritten with a default
    /// configuration because it would jeopardize users attempt to fix it. Just return an error in
    /// this case.
    #[test]
    fn try_load_service_config_loading_fails_due_to_serialization_skip_saving() {
        let path = PathBuf::default();
        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Err(LoadError::Deserialize(MockError.into())));

        assert!(ServiceConfig::try_load(path.as_path(), mock).is_err());
    }

    /// Expected behavior:
    /// If an load attempt fails with an IO error (e.g. file does not exist) then
    /// saving the default configuration is attempted. If this is successful a ServiceConfig
    /// is constructed containing the default version of the underlying data type.
    #[test]
    fn try_load_service_config_loading_fails_and_saving_success_return_ok() {
        let path = PathBuf::default();
        let config_data = String::default();

        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Err(LoadError::Io(ErrorKind::NotFound.into())));

        mock.expect_try_save()
            .with(eq(path.clone()), eq(config_data.clone()))
            .times(1)
            .returning(|_, _| Ok(()));

        let config = ServiceConfig::try_load(path.as_path(), mock).unwrap();
        assert_eq!(config.get(), String::default());
    }

    /// Expected behavior:
    /// If an load is successful the deserialzed configuration object is stored and available as
    /// latest configuration. Nothing is saved.
    #[test]
    fn try_load_service_config_loading_success() {
        let path = PathBuf::default();
        let config_data = "this_is_a_non_default_string";

        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Ok(config_data.to_string()));

        let config = ServiceConfig::try_load(path.as_path(), mock).unwrap();
        assert_eq!(config.get(), config_data);
    }

    /// Expected behavior:
    /// If an load attempt fails with an IO error (e.g. file does not exist) then
    /// saving the default configuration is attempted. If this fails, a functional ServiceConfig is returned
    /// containing the default of the configuration type.
    #[test]
    fn try_load_or_default_service_config_loading_and_saving_fails() {
        let path = PathBuf::default();
        let config_data = String::default();

        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Err(LoadError::Io(ErrorKind::NotFound.into())));

        mock.expect_try_save()
            .with(eq(path.clone()), eq(config_data.clone()))
            .times(1)
            .returning(|_, _| Err(SaveError::Io(ErrorKind::NotFound.into())));

        assert_eq!(
            ServiceConfig::try_load_or_default(path.as_path(), mock).get(),
            String::default()
        );
    }

    /// Expected behavior:
    /// If an load attempt fails due to an serialization error, then a configuration file exists
    /// and with bogus content. In this case the configuration is not overwritten with a default
    /// configuration because it would jeopardize users attempt to fix it.
    /// Instead of an error try_load_or_default returns a functional ServiceConfig containing the
    /// default of the configuration type.
    #[test]
    fn try_load_or_default_service_config_loading_fails_due_to_serialization_skip_saving() {
        let path = PathBuf::default();
        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Err(LoadError::Deserialize(MockError.into())));

        assert_eq!(
            ServiceConfig::try_load_or_default(path.as_path(), mock).get(),
            String::default()
        );
    }

    /// Expected behavior:
    /// If an load attempt fails with an IO error (e.g. file does not exist) then
    /// saving the default configuration is attempted. If this is successful a ServiceConfig
    /// is constructed containing the default version of the underlying data type. The behavior is
    /// equivalent with happy cases of try_load.
    #[test]
    fn try_load_or_default_service_config_loading_fails_and_saving_success_return_ok() {
        let path = PathBuf::default();
        let config_data = String::default();

        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Err(LoadError::Io(ErrorKind::NotFound.into())));
        mock.expect_try_save()
            .with(eq(path.clone()), eq(config_data.clone()))
            .times(1)
            .returning(|_, _| Ok(()));

        assert_eq!(
            ServiceConfig::try_load_or_default(path.as_path(), mock).get(),
            String::default()
        );
    }

    /// Expected behavior:
    /// If an load is successful the deserialzed configuration object is stored and available as
    /// latest configuration. The behavior is equivalent with happy cases of try_load.
    #[test]
    fn try_load_or_default_service_config_loading_success() {
        let path = PathBuf::default();
        let config_data = "this_is_a_non_default_string";

        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .with(eq(path.clone()))
            .times(1)
            .returning(|_| Ok(config_data.to_string()));

        assert_eq!(
            ServiceConfig::try_load_or_default(path.as_path(), mock).get(),
            config_data
        );
    }
}