File fix-the-regression-in-schedule-module-releasded-in-3.patch of Package salt

From 7803275a8aaeedf2124706f51b6a54cfcfb2d032 Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <Victor.Zhestkov@suse.com>
Date: Thu, 1 Sep 2022 14:45:13 +0300
Subject: [PATCH] Fix the regression in schedule module releasded in
 3004 (bsc#1202631)

Co-authored-by: Gareth J. Greenaway <gareth@saltstack.com>
---
 changelog/61324.changed                     |   1 +
 salt/modules/schedule.py                    | 449 ++++++++++++++------
 tests/pytests/unit/modules/test_schedule.py | 138 +++++-
 3 files changed, 442 insertions(+), 146 deletions(-)
 create mode 100644 changelog/61324.changed

diff --git a/changelog/61324.changed b/changelog/61324.changed
new file mode 100644
index 0000000000..d67051a8da
--- /dev/null
+++ b/changelog/61324.changed
@@ -0,0 +1 @@
+Adding the ability to add, delete, purge, and modify Salt scheduler jobs when the Salt minion is not running.
diff --git a/salt/modules/schedule.py b/salt/modules/schedule.py
index bcd64f2851..913a101ea6 100644
--- a/salt/modules/schedule.py
+++ b/salt/modules/schedule.py
@@ -15,6 +15,7 @@ import salt.utils.event
 import salt.utils.files
 import salt.utils.odict
 import salt.utils.yaml
+import yaml
 
 try:
     import dateutil.parser as dateutil_parser
@@ -64,7 +65,35 @@ SCHEDULE_CONF = [
 ]
 
 
-def list_(show_all=False, show_disabled=True, where=None, return_yaml=True):
+def _get_schedule_config_file():
+    """
+    Return the minion schedule configuration file
+    """
+    config_dir = __opts__.get("conf_dir", None)
+    if config_dir is None and "conf_file" in __opts__:
+        config_dir = os.path.dirname(__opts__["conf_file"])
+    if config_dir is None:
+        config_dir = salt.syspaths.CONFIG_DIR
+
+    minion_d_dir = os.path.join(
+        config_dir,
+        os.path.dirname(
+            __opts__.get(
+                "default_include",
+                salt.config.DEFAULT_MINION_OPTS["default_include"],
+            )
+        ),
+    )
+
+    if not os.path.isdir(minion_d_dir):
+        os.makedirs(minion_d_dir)
+
+    return os.path.join(minion_d_dir, "_schedule.conf")
+
+
+def list_(
+    show_all=False, show_disabled=True, where=None, return_yaml=True, offline=False
+):
     """
     List the jobs currently scheduled on the minion
 
@@ -83,24 +112,33 @@ def list_(show_all=False, show_disabled=True, where=None, return_yaml=True):
     """
 
     schedule = {}
-    try:
-        with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
-            res = __salt__["event.fire"](
-                {"func": "list", "where": where}, "manage_schedule"
-            )
-            if res:
-                event_ret = event_bus.get_event(
-                    tag="/salt/minion/minion_schedule_list_complete", wait=30
+    if offline:
+        schedule_config = _get_schedule_config_file()
+        if os.path.exists(schedule_config):
+            with salt.utils.files.fopen(schedule_config) as fp_:
+                schedule_yaml = fp_.read()
+                if schedule_yaml:
+                    schedule_contents = yaml.safe_load(schedule_yaml)
+                    schedule = schedule_contents.get("schedule", {})
+    else:
+        try:
+            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
+                res = __salt__["event.fire"](
+                    {"func": "list", "where": where}, "manage_schedule"
                 )
-                if event_ret and event_ret["complete"]:
-                    schedule = event_ret["schedule"]
-    except KeyError:
-        # Effectively a no-op, since we can't really return without an event system
-        ret = {}
-        ret["comment"] = "Event module not available. Schedule list failed."
-        ret["result"] = True
-        log.debug("Event module not available. Schedule list failed.")
-        return ret
+                if res:
+                    event_ret = event_bus.get_event(
+                        tag="/salt/minion/minion_schedule_list_complete", wait=30
+                    )
+                    if event_ret and event_ret["complete"]:
+                        schedule = event_ret["schedule"]
+        except KeyError:
+            # Effectively a no-op, since we can't really return without an event system
+            ret = {}
+            ret["comment"] = "Event module not available. Schedule list failed."
+            ret["result"] = True
+            log.debug("Event module not available. Schedule list failed.")
+            return ret
 
     _hidden = ["enabled", "skip_function", "skip_during_range"]
     for job in list(schedule.keys()):  # iterate over a copy since we will mutate it
@@ -139,14 +177,11 @@ def list_(show_all=False, show_disabled=True, where=None, return_yaml=True):
             # remove _seconds from the listing
             del schedule[job]["_seconds"]
 
-    if schedule:
-        if return_yaml:
-            tmp = {"schedule": schedule}
-            return salt.utils.yaml.safe_dump(tmp, default_flow_style=False)
-        else:
-            return schedule
+    if return_yaml:
+        tmp = {"schedule": schedule}
+        return salt.utils.yaml.safe_dump(tmp, default_flow_style=False)
     else:
-        return {"schedule": {}}
+        return schedule
 
 
 def is_enabled(name=None):
@@ -186,11 +221,18 @@ def purge(**kwargs):
     .. code-block:: bash
 
         salt '*' schedule.purge
+
+        # Purge jobs on Salt minion
+        salt '*' schedule.purge
+
     """
 
-    ret = {"comment": [], "result": True}
+    ret = {"comment": [], "changes": {}, "result": True}
 
-    for name in list_(show_all=True, return_yaml=False):
+    current_schedule = list_(
+        show_all=True, return_yaml=False, offline=kwargs.get("offline")
+    )
+    for name in pycopy.deepcopy(current_schedule):
         if name == "enabled":
             continue
         if name.startswith("__"):
@@ -202,37 +244,65 @@ def purge(**kwargs):
                 "Job: {} would be deleted from schedule.".format(name)
             )
         else:
-            persist = kwargs.get("persist", True)
+            if kwargs.get("offline"):
+                del current_schedule[name]
 
-            try:
-                with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
-                    res = __salt__["event.fire"](
-                        {"name": name, "func": "delete", "persist": persist},
-                        "manage_schedule",
-                    )
-                    if res:
-                        event_ret = event_bus.get_event(
-                            tag="/salt/minion/minion_schedule_delete_complete", wait=30
+                ret["comment"].append("Deleted job: {} from schedule.".format(name))
+                ret["changes"][name] = "removed"
+
+            else:
+                persist = kwargs.get("persist", True)
+                try:
+                    with salt.utils.event.get_event(
+                        "minion", opts=__opts__
+                    ) as event_bus:
+                        res = __salt__["event.fire"](
+                            {"name": name, "func": "delete", "persist": persist},
+                            "manage_schedule",
                         )
-                        if event_ret and event_ret["complete"]:
-                            _schedule_ret = event_ret["schedule"]
-                            if name not in _schedule_ret:
-                                ret["result"] = True
-                                ret["comment"].append(
-                                    "Deleted job: {} from schedule.".format(name)
-                                )
-                            else:
-                                ret["comment"].append(
-                                    "Failed to delete job {} from schedule.".format(
-                                        name
+                        if res:
+                            event_ret = event_bus.get_event(
+                                tag="/salt/minion/minion_schedule_delete_complete",
+                                wait=30,
+                            )
+                            if event_ret and event_ret["complete"]:
+                                _schedule_ret = event_ret["schedule"]
+                                if name not in _schedule_ret:
+                                    ret["result"] = True
+                                    ret["changes"][name] = "removed"
+                                    ret["comment"].append(
+                                        "Deleted job: {} from schedule.".format(name)
                                     )
-                                )
-                                ret["result"] = True
+                                else:
+                                    ret["comment"].append(
+                                        "Failed to delete job {} from schedule.".format(
+                                            name
+                                        )
+                                    )
+                                    ret["result"] = True
+
+                except KeyError:
+                    # Effectively a no-op, since we can't really return without an event system
+                    ret["comment"] = "Event module not available. Schedule add failed."
+                    ret["result"] = True
+
+    # wait until the end to write file in offline mode
+    if kwargs.get("offline"):
+        schedule_conf = _get_schedule_config_file()
+
+        try:
+            with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
+                fp_.write(
+                    salt.utils.stringutils.to_bytes(
+                        salt.utils.yaml.safe_dump({"schedule": current_schedule})
+                    )
+                )
+        except OSError:
+            log.error(
+                "Failed to persist the updated schedule",
+                exc_info_on_loglevel=logging.DEBUG,
+            )
 
-            except KeyError:
-                # Effectively a no-op, since we can't really return without an event system
-                ret["comment"] = "Event module not available. Schedule add failed."
-                ret["result"] = True
     return ret
 
 
@@ -245,6 +315,10 @@ def delete(name, **kwargs):
     .. code-block:: bash
 
         salt '*' schedule.delete job1
+
+        # Delete job on Salt minion when the Salt minion is not running
+        salt '*' schedule.delete job1
+
     """
 
     ret = {
@@ -260,45 +334,86 @@ def delete(name, **kwargs):
         ret["comment"] = "Job: {} would be deleted from schedule.".format(name)
         ret["result"] = True
     else:
-        persist = kwargs.get("persist", True)
+        if kwargs.get("offline"):
+            current_schedule = list_(
+                show_all=True,
+                where="opts",
+                return_yaml=False,
+                offline=kwargs.get("offline"),
+            )
 
-        if name in list_(show_all=True, where="opts", return_yaml=False):
-            event_data = {"name": name, "func": "delete", "persist": persist}
-        elif name in list_(show_all=True, where="pillar", return_yaml=False):
-            event_data = {
-                "name": name,
-                "where": "pillar",
-                "func": "delete",
-                "persist": False,
-            }
-        else:
-            ret["comment"] = "Job {} does not exist.".format(name)
-            return ret
+            del current_schedule[name]
 
-        try:
-            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
-                res = __salt__["event.fire"](event_data, "manage_schedule")
-                if res:
-                    event_ret = event_bus.get_event(
-                        tag="/salt/minion/minion_schedule_delete_complete",
-                        wait=30,
+            schedule_conf = _get_schedule_config_file()
+
+            try:
+                with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
+                    fp_.write(
+                        salt.utils.stringutils.to_bytes(
+                            salt.utils.yaml.safe_dump({"schedule": current_schedule})
+                        )
                     )
-                    if event_ret and event_ret["complete"]:
-                        schedule = event_ret["schedule"]
-                        if name not in schedule:
-                            ret["result"] = True
-                            ret["comment"] = "Deleted Job {} from schedule.".format(
-                                name
-                            )
-                            ret["changes"][name] = "removed"
-                        else:
-                            ret[
-                                "comment"
-                            ] = "Failed to delete job {} from schedule.".format(name)
-                        return ret
-        except KeyError:
-            # Effectively a no-op, since we can't really return without an event system
-            ret["comment"] = "Event module not available. Schedule add failed."
+            except OSError:
+                log.error(
+                    "Failed to persist the updated schedule",
+                    exc_info_on_loglevel=logging.DEBUG,
+                )
+
+            ret["result"] = True
+            ret["comment"] = "Deleted Job {} from schedule.".format(name)
+            ret["changes"][name] = "removed"
+        else:
+            persist = kwargs.get("persist", True)
+
+            if name in list_(
+                show_all=True,
+                where="opts",
+                return_yaml=False,
+                offline=kwargs.get("offline"),
+            ):
+                event_data = {"name": name, "func": "delete", "persist": persist}
+            elif name in list_(
+                show_all=True,
+                where="pillar",
+                return_yaml=False,
+                offline=kwargs.get("offline"),
+            ):
+                event_data = {
+                    "name": name,
+                    "where": "pillar",
+                    "func": "delete",
+                    "persist": False,
+                }
+            else:
+                ret["comment"] = "Job {} does not exist.".format(name)
+                return ret
+
+            try:
+                with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
+                    res = __salt__["event.fire"](event_data, "manage_schedule")
+                    if res:
+                        event_ret = event_bus.get_event(
+                            tag="/salt/minion/minion_schedule_delete_complete",
+                            wait=30,
+                        )
+                        if event_ret and event_ret["complete"]:
+                            schedule = event_ret["schedule"]
+                            if name not in schedule:
+                                ret["result"] = True
+                                ret["comment"] = "Deleted Job {} from schedule.".format(
+                                    name
+                                )
+                                ret["changes"][name] = "removed"
+                            else:
+                                ret[
+                                    "comment"
+                                ] = "Failed to delete job {} from schedule.".format(
+                                    name
+                                )
+                            return ret
+            except KeyError:
+                # Effectively a no-op, since we can't really return without an event system
+                ret["comment"] = "Event module not available. Schedule add failed."
     return ret
 
 
@@ -438,6 +553,10 @@ def add(name, **kwargs):
         salt '*' schedule.add job1 function='test.ping' seconds=3600
         # If function have some arguments, use job_args
         salt '*' schedule.add job2 function='cmd.run' job_args="['date >> /tmp/date.log']" seconds=60
+
+        # Add job to Salt minion when the Salt minion is not running
+        salt '*' schedule.add job1 function='test.ping' seconds=3600 offline=True
+
     """
 
     ret = {
@@ -445,8 +564,11 @@ def add(name, **kwargs):
         "result": False,
         "changes": {},
     }
+    current_schedule = list_(
+        show_all=True, return_yaml=False, offline=kwargs.get("offline")
+    )
 
-    if name in list_(show_all=True, return_yaml=False):
+    if name in current_schedule:
         ret["comment"] = "Job {} already exists in schedule.".format(name)
         ret["result"] = False
         return ret
@@ -486,32 +608,56 @@ def add(name, **kwargs):
         ret["comment"] = "Job: {} would be added to schedule.".format(name)
         ret["result"] = True
     else:
-        try:
-            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
-                res = __salt__["event.fire"](
-                    {
-                        "name": name,
-                        "schedule": schedule_data,
-                        "func": "add",
-                        "persist": persist,
-                    },
-                    "manage_schedule",
+        if kwargs.get("offline"):
+            current_schedule.update(schedule_data)
+
+            schedule_conf = _get_schedule_config_file()
+
+            try:
+                with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
+                    fp_.write(
+                        salt.utils.stringutils.to_bytes(
+                            salt.utils.yaml.safe_dump({"schedule": current_schedule})
+                        )
+                    )
+            except OSError:
+                log.error(
+                    "Failed to persist the updated schedule",
+                    exc_info_on_loglevel=logging.DEBUG,
                 )
-                if res:
-                    event_ret = event_bus.get_event(
-                        tag="/salt/minion/minion_schedule_add_complete",
-                        wait=30,
+
+            ret["result"] = True
+            ret["comment"] = "Added job: {} to schedule.".format(name)
+            ret["changes"][name] = "added"
+        else:
+            try:
+                with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
+                    res = __salt__["event.fire"](
+                        {
+                            "name": name,
+                            "schedule": schedule_data,
+                            "func": "add",
+                            "persist": persist,
+                        },
+                        "manage_schedule",
                     )
-                    if event_ret and event_ret["complete"]:
-                        schedule = event_ret["schedule"]
-                        if name in schedule:
-                            ret["result"] = True
-                            ret["comment"] = "Added job: {} to schedule.".format(name)
-                            ret["changes"][name] = "added"
-                            return ret
-        except KeyError:
-            # Effectively a no-op, since we can't really return without an event system
-            ret["comment"] = "Event module not available. Schedule add failed."
+                    if res:
+                        event_ret = event_bus.get_event(
+                            tag="/salt/minion/minion_schedule_add_complete",
+                            wait=30,
+                        )
+                        if event_ret and event_ret["complete"]:
+                            schedule = event_ret["schedule"]
+                            if name in schedule:
+                                ret["result"] = True
+                                ret["comment"] = "Added job: {} to schedule.".format(
+                                    name
+                                )
+                                ret["changes"][name] = "added"
+                                return ret
+            except KeyError:
+                # Effectively a no-op, since we can't really return without an event system
+                ret["comment"] = "Event module not available. Schedule add failed."
     return ret
 
 
@@ -524,6 +670,10 @@ def modify(name, **kwargs):
     .. code-block:: bash
 
         salt '*' schedule.modify job1 function='test.ping' seconds=3600
+
+        # Modify job on Salt minion when the Salt minion is not running
+        salt '*' schedule.modify job1 function='test.ping' seconds=3600 offline=True
+
     """
 
     ret = {"comment": "", "changes": {}, "result": True}
@@ -549,7 +699,9 @@ def modify(name, **kwargs):
         ret["comment"] = 'Unable to use "when" and "cron" options together.  Ignoring.'
         return ret
 
-    current_schedule = list_(show_all=True, return_yaml=False)
+    current_schedule = list_(
+        show_all=True, return_yaml=False, offline=kwargs.get("offline")
+    )
 
     if name not in current_schedule:
         ret["comment"] = "Job {} does not exist in schedule.".format(name)
@@ -566,8 +718,7 @@ def modify(name, **kwargs):
         _current["seconds"] = _current.pop("_seconds")
 
     # Copy _current _new, then update values from kwargs
-    _new = pycopy.deepcopy(_current)
-    _new.update(kwargs)
+    _new = build_schedule_item(name, **kwargs)
 
     # Remove test from kwargs, it's not a valid schedule option
     _new.pop("test", None)
@@ -587,29 +738,51 @@ def modify(name, **kwargs):
     if "test" in kwargs and kwargs["test"]:
         ret["comment"] = "Job: {} would be modified in schedule.".format(name)
     else:
-        persist = kwargs.get("persist", True)
-        if name in list_(show_all=True, where="opts", return_yaml=False):
-            event_data = {
-                "name": name,
-                "schedule": _new,
-                "func": "modify",
-                "persist": persist,
-            }
-        elif name in list_(show_all=True, where="pillar", return_yaml=False):
-            event_data = {
-                "name": name,
-                "schedule": _new,
-                "where": "pillar",
-                "func": "modify",
-                "persist": False,
-            }
+        if kwargs.get("offline"):
+            current_schedule[name].update(_new)
 
-        out = __salt__["event.fire"](event_data, "manage_schedule")
-        if out:
+            schedule_conf = _get_schedule_config_file()
+
+            try:
+                with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
+                    fp_.write(
+                        salt.utils.stringutils.to_bytes(
+                            salt.utils.yaml.safe_dump({"schedule": current_schedule})
+                        )
+                    )
+            except OSError:
+                log.error(
+                    "Failed to persist the updated schedule",
+                    exc_info_on_loglevel=logging.DEBUG,
+                )
+
+            ret["result"] = True
             ret["comment"] = "Modified job: {} in schedule.".format(name)
+
         else:
-            ret["comment"] = "Failed to modify job {} in schedule.".format(name)
-            ret["result"] = False
+            persist = kwargs.get("persist", True)
+            if name in list_(show_all=True, where="opts", return_yaml=False):
+                event_data = {
+                    "name": name,
+                    "schedule": _new,
+                    "func": "modify",
+                    "persist": persist,
+                }
+            elif name in list_(show_all=True, where="pillar", return_yaml=False):
+                event_data = {
+                    "name": name,
+                    "schedule": _new,
+                    "where": "pillar",
+                    "func": "modify",
+                    "persist": False,
+                }
+
+            out = __salt__["event.fire"](event_data, "manage_schedule")
+            if out:
+                ret["comment"] = "Modified job: {} in schedule.".format(name)
+            else:
+                ret["comment"] = "Failed to modify job {} in schedule.".format(name)
+                ret["result"] = False
     return ret
 
 
diff --git a/tests/pytests/unit/modules/test_schedule.py b/tests/pytests/unit/modules/test_schedule.py
index e6cb134982..02914be82f 100644
--- a/tests/pytests/unit/modules/test_schedule.py
+++ b/tests/pytests/unit/modules/test_schedule.py
@@ -8,7 +8,8 @@ import pytest
 import salt.modules.schedule as schedule
 import salt.utils.odict
 from salt.utils.event import SaltEvent
-from tests.support.mock import MagicMock, patch
+from salt.utils.odict import OrderedDict
+from tests.support.mock import MagicMock, call, mock_open, patch
 
 log = logging.getLogger(__name__)
 
@@ -29,6 +30,11 @@ def sock_dir(tmp_path):
     return str(tmp_path / "test-socks")
 
 
+@pytest.fixture
+def schedule_config_file(tmp_path):
+    return "/etc/salt/minion.d/_schedule.conf"
+
+
 @pytest.fixture
 def configure_loader_modules():
     return {schedule: {}}
@@ -36,24 +42,56 @@ def configure_loader_modules():
 
 # 'purge' function tests: 1
 @pytest.mark.slow_test
-def test_purge(sock_dir):
+def test_purge(sock_dir, job1, schedule_config_file):
     """
     Test if it purge all the jobs currently scheduled on the minion.
     """
+    _schedule_data = {"job1": job1}
     with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
         mock = MagicMock(return_value=True)
         with patch.dict(schedule.__salt__, {"event.fire": mock}):
             _ret_value = {"complete": True, "schedule": {}}
             with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                assert schedule.purge() == {
-                    "comment": ["Deleted job: schedule from schedule."],
+                with patch.object(
+                    schedule, "list_", MagicMock(return_value=_schedule_data)
+                ):
+                    assert schedule.purge() == {
+                        "comment": ["Deleted job: job1 from schedule."],
+                        "changes": {"job1": "removed"},
+                        "result": True,
+                    }
+
+    _schedule_data = {"job1": job1, "job2": job1, "job3": job1}
+    comm = [
+        "Deleted job: job1 from schedule.",
+        "Deleted job: job2 from schedule.",
+        "Deleted job: job3 from schedule.",
+    ]
+
+    changes = {"job1": "removed", "job2": "removed", "job3": "removed"}
+
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir}
+    ):
+        with patch("salt.utils.files.fopen", mock_open(read_data="")) as fopen_mock:
+            with patch.object(
+                schedule, "list_", MagicMock(return_value=_schedule_data)
+            ):
+                assert schedule.purge(offline=True) == {
+                    "comment": comm,
+                    "changes": changes,
                     "result": True,
                 }
+                _call = call(b"schedule: {}\n")
+                write_calls = fopen_mock.filehandles[schedule_config_file][
+                    0
+                ].write._mock_mock_calls
+                assert _call in write_calls
 
 
 # 'delete' function tests: 1
 @pytest.mark.slow_test
-def test_delete(sock_dir):
+def test_delete(sock_dir, job1, schedule_config_file):
     """
     Test if it delete a job from the minion's schedule.
     """
@@ -68,6 +106,28 @@ def test_delete(sock_dir):
                     "result": False,
                 }
 
+    _schedule_data = {"job1": job1}
+    comm = "Deleted Job job1 from schedule."
+    changes = {"job1": "removed"}
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir}
+    ):
+        with patch("salt.utils.files.fopen", mock_open(read_data="")) as fopen_mock:
+            with patch.object(
+                schedule, "list_", MagicMock(return_value=_schedule_data)
+            ):
+                assert schedule.delete("job1", offline="True") == {
+                    "comment": comm,
+                    "changes": changes,
+                    "result": True,
+                }
+
+                _call = call(b"schedule: {}\n")
+                write_calls = fopen_mock.filehandles[schedule_config_file][
+                    0
+                ].write._mock_mock_calls
+                assert _call in write_calls
+
 
 # 'build_schedule_item' function tests: 1
 def test_build_schedule_item(sock_dir):
@@ -120,7 +180,7 @@ def test_build_schedule_item_invalid_when(sock_dir):
 
 
 @pytest.mark.slow_test
-def test_add(sock_dir):
+def test_add(sock_dir, schedule_config_file):
     """
     Test if it add a job to the schedule.
     """
@@ -163,6 +223,24 @@ def test_add(sock_dir):
                     "result": True,
                 }
 
+    comm1 = "Added job: job3 to schedule."
+    changes1 = {"job3": "added"}
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir}
+    ):
+        with patch("salt.utils.files.fopen", mock_open(read_data="")) as fopen_mock:
+            assert schedule.add(
+                "job3", function="test.ping", seconds=3600, offline="True"
+            ) == {"comment": comm1, "changes": changes1, "result": True}
+
+            _call = call(
+                b"schedule:\n  job3: {function: test.ping, seconds: 3600, maxrunning: 1, name: job3, enabled: true,\n    jid_include: true}\n"
+            )
+            write_calls = fopen_mock.filehandles[schedule_config_file][
+                1
+            ].write._mock_mock_calls
+            assert _call in write_calls
+
 
 # 'run_job' function tests: 1
 
@@ -444,7 +522,7 @@ def test_copy(sock_dir, job1):
 
 
 @pytest.mark.slow_test
-def test_modify(sock_dir):
+def test_modify(sock_dir, job1, schedule_config_file):
     """
     Test if modifying job to the schedule.
     """
@@ -564,7 +642,6 @@ def test_modify(sock_dir):
                 for key in [
                     "maxrunning",
                     "function",
-                    "seconds",
                     "jid_include",
                     "name",
                     "enabled",
@@ -586,6 +663,51 @@ def test_modify(sock_dir):
                 ret = schedule.modify("job2", function="test.version", test=True)
                 assert ret == expected5
 
+    _schedule_data = {"job1": job1}
+    comm = "Modified job: job1 in schedule."
+    changes = {"job1": "removed"}
+
+    changes = {
+        "job1": {
+            "new": OrderedDict(
+                [
+                    ("function", "test.version"),
+                    ("maxrunning", 1),
+                    ("name", "job1"),
+                    ("enabled", True),
+                    ("jid_include", True),
+                ]
+            ),
+            "old": OrderedDict(
+                [
+                    ("function", "test.ping"),
+                    ("maxrunning", 1),
+                    ("name", "job1"),
+                    ("jid_include", True),
+                    ("enabled", True),
+                ]
+            ),
+        }
+    }
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir}
+    ):
+        with patch("salt.utils.files.fopen", mock_open(read_data="")) as fopen_mock:
+            with patch.object(
+                schedule, "list_", MagicMock(return_value=_schedule_data)
+            ):
+                assert schedule.modify(
+                    "job1", function="test.version", offline="True"
+                ) == {"comment": comm, "changes": changes, "result": True}
+
+                _call = call(
+                    b"schedule:\n  job1: {enabled: true, function: test.version, jid_include: true, maxrunning: 1,\n    name: job1}\n"
+                )
+                write_calls = fopen_mock.filehandles[schedule_config_file][
+                    0
+                ].write._mock_mock_calls
+                assert _call in write_calls
+
 
 # 'is_enabled' function tests: 1
 
-- 
2.37.2


openSUSE Build Service is sponsored by