1"""
2    :codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
3"""
4
5import logging
6
7import pytest
8import salt.modules.schedule as schedule
9import salt.utils.odict
10from salt.utils.event import SaltEvent
11from tests.support.mock import MagicMock, patch
12
13log = logging.getLogger(__name__)
14
15
16@pytest.fixture
17def job1():
18    return {
19        "function": "test.ping",
20        "maxrunning": 1,
21        "name": "job1",
22        "jid_include": True,
23        "enabled": True,
24    }
25
26
27@pytest.fixture
28def sock_dir(tmp_path):
29    return str(tmp_path / "test-socks")
30
31
32@pytest.fixture
33def configure_loader_modules():
34    return {schedule: {}}
35
36
37# 'purge' function tests: 1
38@pytest.mark.slow_test
39def test_purge(sock_dir):
40    """
41    Test if it purge all the jobs currently scheduled on the minion.
42    """
43    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
44        mock = MagicMock(return_value=True)
45        with patch.dict(schedule.__salt__, {"event.fire": mock}):
46            _ret_value = {"complete": True, "schedule": {}}
47            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
48                assert schedule.purge() == {
49                    "comment": ["Deleted job: schedule from schedule."],
50                    "result": True,
51                }
52
53
54# 'delete' function tests: 1
55@pytest.mark.slow_test
56def test_delete(sock_dir):
57    """
58    Test if it delete a job from the minion's schedule.
59    """
60    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
61        mock = MagicMock(return_value=True)
62        with patch.dict(schedule.__salt__, {"event.fire": mock}):
63            _ret_value = {"complete": True, "schedule": {}}
64            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
65                assert schedule.delete("job1") == {
66                    "comment": "Job job1 does not exist.",
67                    "changes": {},
68                    "result": False,
69                }
70
71
72# 'build_schedule_item' function tests: 1
73def test_build_schedule_item(sock_dir):
74    """
75    Test if it build a schedule job.
76    """
77    comment = (
78        'Unable to use "seconds", "minutes", "hours", '
79        'or "days" with "when" or "cron" options.'
80    )
81    comment1 = 'Unable to use "when" and "cron" options together.  Ignoring.'
82    with patch.dict(schedule.__opts__, {"job1": {}}):
83        assert schedule.build_schedule_item("") == {
84            "comment": "Job name is required.",
85            "result": False,
86        }
87
88        assert schedule.build_schedule_item("job1", function="test.ping") == {
89            "function": "test.ping",
90            "maxrunning": 1,
91            "name": "job1",
92            "jid_include": True,
93            "enabled": True,
94        }
95
96        assert schedule.build_schedule_item(
97            "job1", function="test.ping", seconds=3600, when="2400"
98        ) == {"comment": comment, "result": False}
99
100        assert schedule.build_schedule_item(
101            "job1", function="test.ping", when="2400", cron="2"
102        ) == {"comment": comment1, "result": False}
103
104
105# 'build_schedule_item_invalid_when' function tests: 1
106
107
108def test_build_schedule_item_invalid_when(sock_dir):
109    """
110    Test if it build a schedule job.
111    """
112    comment = 'Schedule item garbage for "when" in invalid.'
113    with patch.dict(schedule.__opts__, {"job1": {}}):
114        assert schedule.build_schedule_item(
115            "job1", function="test.ping", when="garbage"
116        ) == {"comment": comment, "result": False}
117
118
119# 'add' function tests: 1
120
121
122@pytest.mark.slow_test
123def test_add(sock_dir):
124    """
125    Test if it add a job to the schedule.
126    """
127    comm1 = "Job job1 already exists in schedule."
128    comm2 = (
129        'Error: Unable to use "seconds", "minutes", "hours", '
130        'or "days" with "when" or "cron" options.'
131    )
132    comm3 = 'Unable to use "when" and "cron" options together.  Ignoring.'
133    comm4 = "Job: job2 would be added to schedule."
134    with patch.dict(
135        schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir}
136    ):
137        mock = MagicMock(return_value=True)
138        with patch.dict(schedule.__salt__, {"event.fire": mock}):
139            _ret_value = {"complete": True, "schedule": {"job1": {"salt": "salt"}}}
140            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
141                assert schedule.add("job1") == {
142                    "comment": comm1,
143                    "changes": {},
144                    "result": False,
145                }
146
147            _ret_value = {"complete": True, "schedule": {}}
148            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
149                assert schedule.add(
150                    "job2", function="test.ping", seconds=3600, when="2400"
151                ) == {"comment": comm2, "changes": {}, "result": False}
152
153            _ret_value = {"complete": True, "schedule": {}}
154            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
155                assert schedule.add(
156                    "job2", function="test.ping", when="2400", cron="2"
157                ) == {"comment": comm3, "changes": {}, "result": False}
158            _ret_value = {"complete": True, "schedule": {}}
159            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
160                assert schedule.add("job2", function="test.ping", test=True) == {
161                    "comment": comm4,
162                    "changes": {},
163                    "result": True,
164                }
165
166
167# 'run_job' function tests: 1
168
169
170@pytest.mark.slow_test
171def test_run_job(sock_dir, job1):
172    """
173    Test if it run a scheduled job on the minion immediately.
174    """
175    with patch.dict(
176        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
177    ):
178        mock = MagicMock(return_value=True)
179        with patch.dict(schedule.__salt__, {"event.fire": mock}):
180            _ret_value = {"complete": True, "schedule": {"job1": job1}}
181            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
182                assert schedule.run_job("job1") == {
183                    "comment": "Scheduling Job job1 on minion.",
184                    "result": True,
185                }
186
187
188# 'enable_job' function tests: 1
189
190
191@pytest.mark.slow_test
192def test_enable_job(sock_dir):
193    """
194    Test if it enable a job in the minion's schedule.
195    """
196    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
197        mock = MagicMock(return_value=True)
198        with patch.dict(schedule.__salt__, {"event.fire": mock}):
199            _ret_value = {"complete": True, "schedule": {}}
200            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
201                assert schedule.enable_job("job1") == {
202                    "comment": "Job job1 does not exist.",
203                    "changes": {},
204                    "result": False,
205                }
206
207
208# 'disable_job' function tests: 1
209
210
211@pytest.mark.slow_test
212def test_disable_job(sock_dir):
213    """
214    Test if it disable a job in the minion's schedule.
215    """
216    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
217        mock = MagicMock(return_value=True)
218        with patch.dict(schedule.__salt__, {"event.fire": mock}):
219            _ret_value = {"complete": True, "schedule": {}}
220            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
221                assert schedule.disable_job("job1") == {
222                    "comment": "Job job1 does not exist.",
223                    "changes": {},
224                    "result": False,
225                }
226
227
228# 'save' function tests: 1
229
230
231@pytest.mark.slow_test
232def test_save(sock_dir):
233    """
234    Test if it save all scheduled jobs on the minion.
235    """
236    comm1 = "Schedule (non-pillar items) saved."
237    with patch.dict(
238        schedule.__opts__,
239        {"schedule": {}, "default_include": "/tmp", "sock_dir": sock_dir},
240    ):
241
242        mock = MagicMock(return_value=True)
243        with patch.dict(schedule.__salt__, {"event.fire": mock}):
244            _ret_value = {"complete": True, "schedule": {}}
245            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
246                assert schedule.save() == {"comment": comm1, "result": True}
247
248
249# 'enable' function tests: 1
250
251
252def test_enable(sock_dir):
253    """
254    Test if it enable all scheduled jobs on the minion.
255    """
256    assert schedule.enable(test=True) == {
257        "comment": "Schedule would be enabled.",
258        "changes": {},
259        "result": True,
260    }
261
262
263# 'disable' function tests: 1
264
265
266def test_disable(sock_dir):
267    """
268    Test if it disable all scheduled jobs on the minion.
269    """
270    assert schedule.disable(test=True) == {
271        "comment": "Schedule would be disabled.",
272        "changes": {},
273        "result": True,
274    }
275
276
277# 'move' function tests: 1
278
279
280@pytest.mark.slow_test
281def test_move(sock_dir, job1):
282    """
283    Test if it move scheduled job to another minion or minions.
284    """
285    comm1 = "no servers answered the published schedule.add command"
286    comm2 = "the following minions return False"
287    comm3 = "Moved Job job1 from schedule."
288    with patch.dict(
289        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
290    ):
291        mock = MagicMock(return_value=True)
292        with patch.dict(schedule.__salt__, {"event.fire": mock}):
293            _ret_value = {"complete": True, "schedule": {"job1": job1}}
294            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
295                mock = MagicMock(return_value={})
296                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
297                    assert schedule.move("job1", "minion1") == {
298                        "comment": comm1,
299                        "result": True,
300                    }
301
302                mock = MagicMock(return_value={"minion1": ""})
303                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
304                    assert schedule.move("job1", "minion1") == {
305                        "comment": comm2,
306                        "minions": ["minion1"],
307                        "result": True,
308                    }
309
310                mock = MagicMock(return_value={"minion1": "job1"})
311                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
312                    mock = MagicMock(return_value=True)
313                    with patch.dict(schedule.__salt__, {"event.fire": mock}):
314                        assert schedule.move("job1", "minion1") == {
315                            "comment": comm3,
316                            "minions": ["minion1"],
317                            "result": True,
318                        }
319
320                assert schedule.move("job3", "minion1") == {
321                    "comment": "Job job3 does not exist.",
322                    "result": False,
323                }
324
325    mock = MagicMock(side_effect=[{}, {"job1": {}}])
326    with patch.dict(schedule.__opts__, {"schedule": mock, "sock_dir": sock_dir}):
327        mock = MagicMock(return_value=True)
328        with patch.dict(schedule.__salt__, {"event.fire": mock}):
329            _ret_value = {"complete": True, "schedule": {"job1": job1}}
330            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
331                with patch.dict(schedule.__pillar__, {"schedule": {"job1": job1}}):
332                    mock = MagicMock(return_value={})
333                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
334                        assert schedule.move("job1", "minion1") == {
335                            "comment": comm1,
336                            "result": True,
337                        }
338
339                    mock = MagicMock(return_value={"minion1": ""})
340                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
341                        assert schedule.move("job1", "minion1") == {
342                            "comment": comm2,
343                            "minions": ["minion1"],
344                            "result": True,
345                        }
346
347                    mock = MagicMock(return_value={"minion1": "job1"})
348                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
349                        mock = MagicMock(return_value=True)
350                        with patch.dict(schedule.__salt__, {"event.fire": mock}):
351                            assert schedule.move("job1", "minion1") == {
352                                "comment": comm3,
353                                "minions": ["minion1"],
354                                "result": True,
355                            }
356
357
358# 'copy' function tests: 1
359
360
361@pytest.mark.slow_test
362def test_copy(sock_dir, job1):
363    """
364    Test if it copy scheduled job to another minion or minions.
365    """
366    comm1 = "no servers answered the published schedule.add command"
367    comm2 = "the following minions return False"
368    comm3 = "Copied Job job1 from schedule to minion(s)."
369    with patch.dict(
370        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
371    ):
372        mock = MagicMock(return_value=True)
373        with patch.dict(schedule.__salt__, {"event.fire": mock}):
374            _ret_value = {"complete": True, "schedule": {"job1": {"job1": job1}}}
375            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
376                mock = MagicMock(return_value={})
377                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
378                    assert schedule.copy("job1", "minion1") == {
379                        "comment": comm1,
380                        "result": True,
381                    }
382
383                mock = MagicMock(return_value={"minion1": ""})
384                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
385                    assert schedule.copy("job1", "minion1") == {
386                        "comment": comm2,
387                        "minions": ["minion1"],
388                        "result": True,
389                    }
390
391                mock = MagicMock(return_value={"minion1": "job1"})
392                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
393                    mock = MagicMock(return_value=True)
394                    with patch.dict(schedule.__salt__, {"event.fire": mock}):
395                        assert schedule.copy("job1", "minion1") == {
396                            "comment": comm3,
397                            "minions": ["minion1"],
398                            "result": True,
399                        }
400
401                assert schedule.copy("job3", "minion1") == {
402                    "comment": "Job job3 does not exist.",
403                    "result": False,
404                }
405
406    mock = MagicMock(side_effect=[{}, {"job1": {}}])
407    with patch.dict(schedule.__opts__, {"schedule": mock, "sock_dir": sock_dir}):
408        with patch.dict(schedule.__pillar__, {"schedule": {"job1": job1}}):
409            mock = MagicMock(return_value=True)
410            with patch.dict(schedule.__salt__, {"event.fire": mock}):
411                _ret_value = {
412                    "complete": True,
413                    "schedule": {"job1": {"job1": job1}},
414                }
415                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
416
417                    mock = MagicMock(return_value={})
418                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
419                        assert schedule.copy("job1", "minion1") == {
420                            "comment": comm1,
421                            "result": True,
422                        }
423
424                    mock = MagicMock(return_value={"minion1": ""})
425                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
426                        assert schedule.copy("job1", "minion1") == {
427                            "comment": comm2,
428                            "minions": ["minion1"],
429                            "result": True,
430                        }
431
432                    mock = MagicMock(return_value={"minion1": "job1"})
433                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
434                        mock = MagicMock(return_value=True)
435                        with patch.dict(schedule.__salt__, {"event.fire": mock}):
436                            assert schedule.copy("job1", "minion1") == {
437                                "comment": comm3,
438                                "minions": ["minion1"],
439                                "result": True,
440                            }
441
442
443# 'modify' function tests: 1
444
445
446@pytest.mark.slow_test
447def test_modify(sock_dir):
448    """
449    Test if modifying job to the schedule.
450    """
451    current_job1 = {
452        "function": "salt",
453        "seconds": "3600",
454        "maxrunning": 1,
455        "name": "job1",
456        "enabled": True,
457        "jid_include": True,
458    }
459
460    new_job1 = {
461        "function": "salt",
462        "seconds": "60",
463        "maxrunning": 1,
464        "name": "job1",
465        "enabled": True,
466        "jid_include": True,
467    }
468
469    comm1 = "Modified job: job1 in schedule."
470    changes1 = {
471        "job1": {
472            "new": salt.utils.odict.OrderedDict(new_job1),
473            "old": salt.utils.odict.OrderedDict(current_job1),
474        }
475    }
476
477    new_job4 = {
478        "function": "test.version",
479        "seconds": "3600",
480        "maxrunning": 1,
481        "name": "job1",
482        "enabled": True,
483        "jid_include": True,
484    }
485
486    changes4 = {
487        "job1": {
488            "new": salt.utils.odict.OrderedDict(new_job4),
489            "old": salt.utils.odict.OrderedDict(current_job1),
490        }
491    }
492
493    expected1 = {"comment": comm1, "changes": changes1, "result": True}
494
495    comm2 = (
496        'Error: Unable to use "seconds", "minutes", "hours", '
497        'or "days" with "when" option.'
498    )
499    expected2 = {"comment": comm2, "changes": {}, "result": False}
500
501    comm3 = 'Unable to use "when" and "cron" options together.  Ignoring.'
502    expected3 = {"comment": comm3, "changes": {}, "result": False}
503
504    comm4 = "Job: job1 would be modified in schedule."
505    expected4 = {"comment": comm4, "changes": changes4, "result": True}
506
507    comm5 = "Job job2 does not exist in schedule."
508    expected5 = {"comment": comm5, "changes": {}, "result": False}
509
510    with patch.dict(
511        schedule.__opts__, {"schedule": {"job1": current_job1}, "sock_dir": sock_dir}
512    ):
513        mock = MagicMock(return_value=True)
514        with patch.dict(schedule.__salt__, {"event.fire": mock}):
515            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
516            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
517                ret = schedule.modify("job1", seconds="60")
518                assert "job1" in ret["changes"]
519                assert "new" in ret["changes"]["job1"]
520                assert "old" in ret["changes"]["job1"]
521
522                for key in [
523                    "maxrunning",
524                    "function",
525                    "seconds",
526                    "jid_include",
527                    "name",
528                    "enabled",
529                ]:
530                    assert (
531                        ret["changes"]["job1"]["new"][key]
532                        == expected1["changes"]["job1"]["new"][key]
533                    )
534                    assert (
535                        ret["changes"]["job1"]["old"][key]
536                        == expected1["changes"]["job1"]["old"][key]
537                    )
538
539                assert ret["comment"] == expected1["comment"]
540                assert ret["result"] == expected1["result"]
541
542            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
543            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
544                ret = schedule.modify(
545                    "job1", function="test.ping", seconds=3600, when="2400"
546                )
547                assert ret == expected2
548
549            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
550            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
551                ret = schedule.modify(
552                    "job1", function="test.ping", when="2400", cron="2"
553                )
554                assert ret == expected3
555
556            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
557            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
558                ret = schedule.modify("job1", function="test.version", test=True)
559
560                assert "job1" in ret["changes"]
561                assert "new" in ret["changes"]["job1"]
562                assert "old" in ret["changes"]["job1"]
563
564                for key in [
565                    "maxrunning",
566                    "function",
567                    "seconds",
568                    "jid_include",
569                    "name",
570                    "enabled",
571                ]:
572                    assert (
573                        ret["changes"]["job1"]["new"][key]
574                        == expected4["changes"]["job1"]["new"][key]
575                    )
576                    assert (
577                        ret["changes"]["job1"]["old"][key]
578                        == expected4["changes"]["job1"]["old"][key]
579                    )
580
581                assert ret["comment"] == expected4["comment"]
582                assert ret["result"] == expected4["result"]
583
584            _ret_value = {"complete": True, "schedule": {}}
585            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
586                ret = schedule.modify("job2", function="test.version", test=True)
587                assert ret == expected5
588
589
590# 'is_enabled' function tests: 1
591
592
593def test_is_enabled(sock_dir):
594    """
595    Test is_enabled
596    """
597    job1 = {"function": "salt", "seconds": 3600}
598
599    comm1 = "Modified job: job1 in schedule."
600
601    mock_schedule = {"enabled": True, "job1": job1}
602
603    mock_lst = MagicMock(return_value=mock_schedule)
604
605    with patch.dict(
606        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
607    ):
608        mock = MagicMock(return_value=True)
609        with patch.dict(
610            schedule.__salt__, {"event.fire": mock, "schedule.list": mock_lst}
611        ):
612            _ret_value = {"complete": True, "schedule": {"job1": job1}}
613            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
614                ret = schedule.is_enabled("job1")
615                assert ret == job1
616
617                ret = schedule.is_enabled()
618                assert ret
619
620
621# 'job_status' function tests: 1
622
623
624def test_job_status(sock_dir):
625    """
626    Test is_enabled
627    """
628    job1 = {"function": "salt", "seconds": 3600}
629
630    comm1 = "Modified job: job1 in schedule."
631
632    mock_schedule = {"enabled": True, "job1": job1}
633
634    mock_lst = MagicMock(return_value=mock_schedule)
635
636    with patch.dict(
637        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
638    ):
639        mock = MagicMock(return_value=True)
640        with patch.dict(schedule.__salt__, {"event.fire": mock}):
641            _ret_value = {"complete": True, "data": job1}
642            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
643                ret = schedule.job_status("job1")
644                assert ret == job1
645