1import logging
2import os
3import textwrap
4import threading
5import time
6
7import pytest
8import salt.loader
9import salt.utils.atomicfile
10import salt.utils.files
11import salt.utils.path
12import salt.utils.platform
13import salt.utils.stringutils
14
15log = logging.getLogger(__name__)
16
17
18pytestmark = [
19    pytest.mark.windows_whitelisted,
20]
21
22
23def test_show_highstate(state, state_testfile_dest_path):
24    """
25    state.show_highstate
26    """
27    high = state.show_highstate()
28    assert isinstance(high, dict)
29    assert str(state_testfile_dest_path) in high
30    assert high[str(state_testfile_dest_path)]["__env__"] == "base"
31
32
33def test_show_lowstate(state):
34    """
35    state.show_lowstate
36    """
37    low = state.show_lowstate()
38    assert isinstance(low, list)
39    for entry in low:
40        assert isinstance(entry, dict)
41
42
43def test_show_states(state):
44    """
45    state.show_states
46    """
47    states = state.show_states()
48    assert isinstance(states, list)
49    for entry in states:
50        assert isinstance(entry, str)
51    assert states == ["core"]
52
53
54def test_show_states_missing_sls(state, state_tree):
55    """
56    Test state.show_states with a sls file
57    defined in a top file is missing
58    """
59    top_sls_contents = """
60    base:
61      '*':
62        - core
63        - does-not-exist
64    """
65    with pytest.helpers.temp_file("top.sls", top_sls_contents, state_tree):
66        states = state.show_states()
67        assert isinstance(states, list)
68        assert states == ["No matching sls found for 'does-not-exist' in env 'base'"]
69
70
71def test_catch_recurse(state, state_tree):
72    """
73    state.show_sls used to catch a recursive ref
74    """
75    sls_contents = """
76    mysql:
77      service:
78        - running
79        - require:
80          - file: /etc/mysql/my.cnf
81
82    /etc/mysql/my.cnf:
83      file:
84        - managed
85        - source: salt://master.cnf
86        - require:
87          - service: mysql
88    """
89    with pytest.helpers.temp_file("recurse-fail.sls", sls_contents, state_tree):
90        ret = state.sls("recurse-fail")
91        assert ret.failed
92        assert (
93            'A recursive requisite was found, SLS "recurse-fail" ID "/etc/mysql/my.cnf" ID "mysql"'
94            in ret.errors
95        )
96
97
98RECURSE_SLS_ONE = """
99snmpd:
100  pkg:
101    - installed
102  service:
103    - running
104    - require:
105      - pkg: snmpd
106    - watch:
107      - file: /etc/snmp/snmpd.conf
108
109/etc/snmp/snmpd.conf:
110  file:
111    - managed
112    - source: salt://snmpd/snmpd.conf.jinja
113    - template: jinja
114    - user: root
115    - group: root
116    - mode: "0600"
117    - require:
118      - pkg: snmpd
119"""
120RECURSE_SLS_TWO = """
121nagios-nrpe-server:
122  pkg:
123    - installed
124  service:
125    - running
126    - watch:
127      - file: /etc/nagios/nrpe.cfg
128
129/etc/nagios/nrpe.cfg:
130  file:
131    - managed
132    - source: salt://baseserver/nrpe.cfg
133    - require:
134      - pkg: nagios-nrpe-server
135"""
136
137
138@pytest.mark.parametrize(
139    "sls_contents, expected_in_output",
140    [(RECURSE_SLS_ONE, "snmpd"), (RECURSE_SLS_TWO, "/etc/nagios/nrpe.cfg")],
141    ids=("recurse-scenario-1", "recurse-scenario-2"),
142)
143def test_no_recurse(state, state_tree, sls_contents, expected_in_output):
144    """
145    verify that a sls structure is NOT a recursive ref
146    """
147    with pytest.helpers.temp_file("recurse-ok.sls", sls_contents, state_tree):
148        ret = state.show_sls("recurse-ok")
149        assert expected_in_output in ret
150
151
152def test_running_dictionary_consistency(state):
153    """
154    Test the structure of the running dictionary so we don't change it
155    without deprecating/documenting the change
156    """
157    running_dict_fields = {
158        "__id__",
159        "__run_num__",
160        "__sls__",
161        "changes",
162        "comment",
163        "duration",
164        "name",
165        "result",
166        "start_time",
167    }
168
169    sls = state.single("test.succeed_without_changes", name="gndn")
170    ret_values_set = set(sls.full_return.keys())
171    assert running_dict_fields.issubset(ret_values_set)
172
173
174def test_running_dictionary_key_sls(state, state_tree):
175    """
176    Ensure the __sls__ key is either null or a string
177    """
178    sls1 = state.single("test.succeed_with_changes", name="gndn")
179    assert "__sls__" in sls1.full_return
180    assert sls1.full_return["__sls__"] is None
181
182    sls_contents = """
183    gndn:
184      test.succeed_with_changes
185    """
186    with pytest.helpers.temp_file("gndn.sls", sls_contents, state_tree):
187        sls2 = state.sls(mods="gndn")
188
189    for state_return in sls2:
190        assert "__sls__" in state_return.full_return
191        assert isinstance(state_return.full_return["__sls__"], str)
192
193
194@pytest.fixture
195def requested_sls_key(minion_opts, state_tree):
196    if not salt.utils.platform.is_windows():
197        sls_contents = """
198        count_root_dir_contents:
199          cmd.run:
200            - name: 'ls -a / | wc -l'
201        """
202        sls_key = "cmd_|-count_root_dir_contents_|-ls -a / | wc -l_|-run"
203    else:
204        sls_contents = r"""
205        count_root_dir_contents:
206          cmd.run:
207            - name: 'Get-ChildItem C:\ | Measure-Object | %{$_.Count}'
208            - shell: powershell
209        """
210        sls_key = (
211            r"cmd_|-count_root_dir_contents_|-Get-ChildItem C:\ | Measure-Object |"
212            r" %{$_.Count}_|-run"
213        )
214    try:
215        with pytest.helpers.temp_file(
216            "requested.sls", sls_contents, state_tree
217        ) as sls_path:
218            yield sls_key
219    finally:
220        cache_file = os.path.join(minion_opts["cachedir"], "req_state.p")
221        if os.path.exists(cache_file):
222            os.remove(cache_file)
223
224
225def test_request(state, requested_sls_key):
226    """
227    verify sending a state request to the minion(s)
228    """
229    ret = state.request("requested")
230    assert ret[requested_sls_key]["result"] is None
231
232
233def test_check_request(state, requested_sls_key):
234    """
235    verify checking a state request sent to the minion(s)
236    """
237    ret = state.request("requested")
238    assert ret[requested_sls_key]["result"] is None
239
240    ret = state.check_request()
241    assert ret["default"]["test_run"][requested_sls_key]["result"] is None
242
243
244def test_clear_request(state, requested_sls_key):
245    """
246    verify clearing a state request sent to the minion(s)
247    """
248    ret = state.request("requested")
249    assert ret[requested_sls_key]["result"] is None
250
251    ret = state.clear_request()
252    assert ret is True
253
254
255def test_run_request_succeeded(state, requested_sls_key):
256    """
257    verify running a state request sent to the minion(s)
258    """
259    ret = state.request("requested")
260    assert ret[requested_sls_key]["result"] is None
261
262    ret = state.run_request()
263    assert ret[requested_sls_key]["result"] is True
264
265
266def test_run_request_failed_no_request_staged(state, requested_sls_key):
267    """
268    verify not running a state request sent to the minion(s)
269    """
270    ret = state.request("requested")
271    assert ret[requested_sls_key]["result"] is None
272
273    ret = state.clear_request()
274    assert ret is True
275
276    ret = state.run_request()
277    assert ret == {}
278
279
280def test_issue_1876_syntax_error(state, state_tree, tmp_path):
281    """
282    verify that we catch the following syntax error::
283
284        /tmp/salttest/issue-1876:
285
286          file:
287            - managed
288            - source: salt://testfile
289
290          file.append:
291            - text: foo
292
293    """
294    testfile = tmp_path / "issue-1876.txt"
295    sls_contents = """
296    {}:
297      file:
298        - managed
299        - source: salt://testfile
300
301      file.append:
302        - text: foo
303    """.format(
304        testfile
305    )
306    with pytest.helpers.temp_file("issue-1876.sls", sls_contents, state_tree):
307        ret = state.sls("issue-1876")
308        assert ret.failed
309        errmsg = (
310            "ID '{}' in SLS 'issue-1876' contains multiple state declarations of the"
311            " same type".format(testfile)
312        )
313        assert errmsg in ret.errors
314
315
316def test_issue_1879_too_simple_contains_check(state, state_tree, tmp_path):
317    testfile = tmp_path / "issue-1979.txt"
318    init_sls_contents = """
319    {}:
320      file:
321        - touch
322    """.format(
323        testfile
324    )
325    step1_sls_contents = """
326    {}:
327      file.append:
328        - text: |
329            # set variable identifying the chroot you work in (used in the prompt below)
330            if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then
331                debian_chroot=$(cat /etc/debian_chroot)
332            fi
333
334    """.format(
335        testfile
336    )
337    step2_sls_contents = """
338    {}:
339      file.append:
340        - text: |
341            # enable bash completion in interactive shells
342            if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
343                . /etc/bash_completion
344            fi
345
346    """.format(
347        testfile
348    )
349
350    expected = textwrap.dedent(
351        """\
352        # set variable identifying the chroot you work in (used in the prompt below)
353        if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then
354            debian_chroot=$(cat /etc/debian_chroot)
355        fi
356        # enable bash completion in interactive shells
357        if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
358            . /etc/bash_completion
359        fi
360        """
361    )
362
363    issue_1879_dir = state_tree / "issue-1879"
364    with pytest.helpers.temp_file(
365        "init.sls", init_sls_contents, issue_1879_dir
366    ), pytest.helpers.temp_file(
367        "step-1.sls", step1_sls_contents, issue_1879_dir
368    ), pytest.helpers.temp_file(
369        "step-2.sls", step2_sls_contents, issue_1879_dir
370    ):
371        # Create the file
372        ret = state.sls("issue-1879")
373        for staterun in ret:
374            assert staterun.result is True
375
376        # The first append
377        ret = state.sls("issue-1879.step-1")
378        for staterun in ret:
379            assert staterun.result is True
380
381        # The second append
382        ret = state.sls("issue-1879.step-2")
383        for staterun in ret:
384            assert staterun.result is True
385
386        # Does it match?
387        contents = testfile.read_text()
388        assert contents == expected
389
390        # Make sure we don't re-append existing text
391        ret = state.sls("issue-1879.step-1")
392        for staterun in ret:
393            assert staterun.result is True
394
395        ret = state.sls("issue-1879.step-2")
396        for staterun in ret:
397            assert staterun.result is True
398
399        # Does it match?
400        contents = testfile.read_text()
401        assert contents == expected
402
403
404def test_include(state, state_tree, tmp_path):
405    testfile_path = tmp_path / "testfile"
406    testfile_path.write_text("foo")
407    include_test_path = tmp_path / "include-test.txt"
408    to_include_test_path = tmp_path / "to-include-test.txt"
409    exclude_test_path = tmp_path / "exclude-test.txt"
410    to_include_sls_contents = """
411    {}:
412      file.managed:
413        - source: salt://testfile
414    """.format(
415        to_include_test_path
416    )
417    include_sls_contents = """
418    include:
419      - to-include-test
420
421    {}:
422      file.managed:
423        - source: salt://testfile
424    """.format(
425        include_test_path
426    )
427    with pytest.helpers.temp_file(
428        "testfile", "foo", state_tree
429    ), pytest.helpers.temp_file(
430        "to-include-test.sls", to_include_sls_contents, state_tree
431    ), pytest.helpers.temp_file(
432        "include-test.sls", include_sls_contents, state_tree
433    ):
434        ret = state.sls("include-test")
435        for staterun in ret:
436            assert staterun.result is True
437
438    assert include_test_path.exists()
439    assert to_include_test_path.exists()
440    assert exclude_test_path.exists() is False
441
442
443def test_exclude(state, state_tree, tmp_path):
444    testfile_path = tmp_path / "testfile"
445    testfile_path.write_text("foo")
446    include_test_path = tmp_path / "include-test.txt"
447    to_include_test_path = tmp_path / "to-include-test.txt"
448    exclude_test_path = tmp_path / "exclude-test.txt"
449    to_include_sls_contents = """
450    {}:
451      file.managed:
452        - source: salt://testfile
453    """.format(
454        to_include_test_path
455    )
456    include_sls_contents = """
457    include:
458      - to-include-test
459
460    {}:
461      file.managed:
462        - source: salt://testfile
463    """.format(
464        include_test_path
465    )
466    exclude_sls_contents = """
467    exclude:
468      - to-include-test
469
470    include:
471      - include-test
472
473    {}:
474      file.managed:
475        - source: salt://testfile
476    """.format(
477        exclude_test_path
478    )
479    with pytest.helpers.temp_file(
480        "testfile", "foo", state_tree
481    ), pytest.helpers.temp_file(
482        "to-include-test.sls", to_include_sls_contents, state_tree
483    ), pytest.helpers.temp_file(
484        "include-test.sls", include_sls_contents, state_tree
485    ), pytest.helpers.temp_file(
486        "exclude-test.sls", exclude_sls_contents, state_tree
487    ):
488        ret = state.sls("exclude-test")
489        for staterun in ret:
490            assert staterun.result is True
491
492    assert include_test_path.exists()
493    assert exclude_test_path.exists()
494    assert to_include_test_path.exists() is False
495
496
497def test_issue_2068_template_str(state, state_tree):
498    template_str_no_dot_sls_contents = """
499    required_state:
500      test:
501        - succeed_without_changes
502
503    requiring_state:
504      test:
505        - succeed_without_changes
506        - require:
507          - test: required_state
508    """
509    template_str_sls_contents = """
510    required_state: test.succeed_without_changes
511
512    requiring_state:
513      test.succeed_without_changes:
514        - require:
515          - test: required_state
516    """
517    with pytest.helpers.temp_file(
518        "issue-2068-no-dot.sls", template_str_no_dot_sls_contents, state_tree
519    ) as template_str_no_dot_path, pytest.helpers.temp_file(
520        "issue-2068.sls", template_str_sls_contents, state_tree
521    ) as template_str_path:
522        # If running this state with state.sls works, so should using state.template_str
523        ret = state.sls("issue-2068-no-dot")
524        for staterun in ret:
525            assert staterun.result is True
526
527        template_str_no_dot_contents = template_str_no_dot_path.read_text()
528        ret = state.template_str(template_str_no_dot_contents)
529        for staterun in ret:
530            assert staterun.result is True
531
532        # Now using state.template
533        ret = state.template(str(template_str_no_dot_path))
534        for staterun in ret:
535            assert staterun.result is True
536
537        # Now the problematic #2068 including dot's
538        ret = state.sls("issue-2068")
539        for staterun in ret:
540            assert staterun.result is True
541
542        template_str_contents = template_str_path.read_text()
543        ret = state.template_str(template_str_contents)
544        for staterun in ret:
545            assert staterun.result is True
546
547        # Now using state.template
548        ret = state.template(str(template_str_path))
549        for staterun in ret:
550            assert staterun.result is True
551
552
553@pytest.mark.parametrize("item", ("include", "exclude", "extends"))
554def test_template_str_invalid_items(state, item):
555    TEMPLATE = textwrap.dedent(
556        """\
557        {}:
558          - existing-state
559
560        /tmp/test-template-invalid-items:
561          file:
562            - managed
563            - source: salt://testfile
564        """.format(
565            item
566        )
567    )
568
569    ret = state.template_str(TEMPLATE.format(item))
570    assert ret.failed
571    errmsg = (
572        "The '{}' declaration found on '<template-str>' is invalid when "
573        "rendering single templates".format(item)
574    )
575    assert errmsg in ret.errors
576
577
578@pytest.mark.skip_on_windows(
579    reason=(
580        "Functional testing this on windows raises unicode errors. "
581        "Tested in tests/pytests/integration/modules/state/test_state.py"
582    )
583)
584def test_pydsl(state, state_tree, tmp_path):
585    """
586    Test the basics of the pydsl
587    """
588    testfile = tmp_path / "testfile"
589    sls_contents = """
590    #!pydsl
591
592    state("{}").file("touch")
593    """.format(
594        testfile
595    )
596    with pytest.helpers.temp_file("pydsl.sls", sls_contents, state_tree):
597        ret = state.sls("pydsl")
598        for staterun in ret:
599            assert staterun.result is True
600        assert testfile.exists()
601
602
603def test_issues_7905_and_8174_sls_syntax_error(state, state_tree):
604    """
605    Call sls file with yaml syntax error.
606
607    Ensure theses errors are detected and presented to the user without
608    stack traces.
609    """
610    badlist_1_sls_contents = """
611    # Missing " " between "-" and "foo" or "name"
612    A:
613      cmd.run:
614        -name: echo foo
615        -foo:
616          - bar
617    """
618    badlist_2_sls_contents = """
619    # C should fail with bad list error message
620    B:
621      # ok
622      file.exist:
623        - name: /foo/bar/foobar
624    # ok
625    /foo/bar/foobar:
626      file.exist
627
628    # nok
629    C:
630      /foo/bar/foobar:
631        file.exist
632    """
633    with pytest.helpers.temp_file(
634        "badlist1.sls", badlist_1_sls_contents, state_tree
635    ), pytest.helpers.temp_file("badlist2.sls", badlist_2_sls_contents, state_tree):
636        ret = state.sls("badlist1")
637        assert ret.failed
638        assert ret.errors == ["State 'A' in SLS 'badlist1' is not formed as a list"]
639
640        ret = state.sls("badlist2")
641        assert ret.failed
642        assert ret.errors == ["State 'C' in SLS 'badlist2' is not formed as a list"]
643
644
645@pytest.mark.slow_test
646def test_retry_option(state, state_tree):
647    """
648    test the retry option on a simple state with defaults
649    ensure comment is as expected
650    ensure state duration is greater than configured the passed (interval * attempts)
651    """
652    sls_contents = """
653    file_test:
654      file.exists:
655        - name: /path/to/a/non-existent/file.txt
656        - retry:
657            until: True
658            attempts: 3
659            interval: 1
660            splay: 0
661    """
662    expected_comment = (
663        'Attempt 1: Returned a result of "False", with the following '
664        'comment: "Specified path /path/to/a/non-existent/file.txt does not exist"'
665    )
666    with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree):
667        ret = state.sls("retry")
668        for state_return in ret:
669            assert state_return.result is False
670            assert expected_comment in state_return.comment
671            assert state_return.full_return["duration"] >= 3
672
673
674def test_retry_option_success(state, state_tree, tmp_path):
675    """
676    test a state with the retry option that should return True immediately (i.e. no retries)
677    """
678    testfile = tmp_path / "testfile"
679    testfile.touch()
680    sls_contents = """
681    file_test:
682      file.exists:
683        - name: {}
684        - retry:
685            until: True
686            attempts: 5
687            interval: 2
688            splay: 0
689    """.format(
690        testfile
691    )
692    with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree):
693        ret = state.sls("retry")
694        for state_return in ret:
695            assert state_return.result is True
696            assert state_return.full_return["duration"] < 4
697            # It should not take 2 attempts
698            assert "Attempt 2" not in state_return.comment
699
700
701@pytest.mark.slow_test
702def test_retry_option_eventual_success(state, state_tree, tmp_path):
703    """
704    test a state with the retry option that should return True, eventually
705    """
706    testfile1 = tmp_path / "testfile-1"
707    testfile2 = tmp_path / "testfile-2"
708
709    def create_testfile(testfile1, testfile2):
710        while True:
711            if testfile1.exists():
712                break
713        time.sleep(2)
714        testfile2.touch()
715
716    thread = threading.Thread(target=create_testfile, args=(testfile1, testfile2))
717    sls_contents = """
718    file_test_a:
719      file.managed:
720        - name: {}
721        - content: 'a'
722
723    file_test:
724      file.exists:
725        - name: {}
726        - retry:
727            until: True
728            attempts: 5
729            interval: 2
730            splay: 0
731        - require:
732          - file_test_a
733    """.format(
734        testfile1, testfile2
735    )
736    with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree):
737        thread.start()
738        ret = state.sls("retry")
739        for state_return in ret:
740            assert state_return.result is True
741            assert state_return.full_return["duration"] > 4
742            # It should not take 5 attempts
743            assert "Attempt 5" not in state_return.comment
744
745
746@pytest.mark.slow_test
747def test_state_non_base_environment(state, state_tree_prod, tmp_path):
748    """
749    test state.sls with saltenv using a nonbase environment
750    with a salt source
751    """
752    testfile = tmp_path / "testfile"
753    sls_contents = """
754    {}:
755      file.managed:
756        - content: foo
757    """.format(
758        testfile
759    )
760    with pytest.helpers.temp_file("non-base-env.sls", sls_contents, state_tree_prod):
761        ret = state.sls("non-base-env", saltenv="prod")
762        for state_return in ret:
763            assert state_return.result is True
764        assert testfile.exists()
765
766
767@pytest.mark.skip_on_windows(
768    reason="Skipped until parallel states can be fixed on Windows"
769)
770def test_parallel_state_with_long_tag(state, state_tree):
771    """
772    This tests the case where the state being executed has a long ID dec or
773    name and states are being run in parallel. The filenames used for the
774    parallel state cache were previously based on the tag for each chunk,
775    and longer ID decs or name params can cause the cache file to be longer
776    than the operating system's max file name length. To counter this we
777    instead generate a SHA1 hash of the chunk's tag to use as the cache
778    filename. This test will ensure that long tags don't cause caching
779    failures.
780
781    See https://github.com/saltstack/salt/issues/49738 for more info.
782    """
783    short_command = "helloworld"
784    long_command = short_command * 25
785    sls_contents = """
786    test_cmd_short:
787      cmd.run:
788        - name: {}
789        - parallel: True
790
791    test_cmd_long:
792      cmd.run:
793        - name: {}
794        - parallel: True
795    """.format(
796        short_command, long_command
797    )
798    with pytest.helpers.temp_file("issue-49738.sls", sls_contents, state_tree):
799        ret = state.sls(
800            "issue-49738",
801            __pub_jid="1",  # Because these run in parallel we need a fake JID
802        )
803
804    comments = sorted([x.comment for x in ret])
805    expected = sorted(
806        ['Command "{}" run'.format(x) for x in (short_command, long_command)]
807    )
808    assert comments == expected, "{} != {}".format(comments, expected)
809
810
811@pytest.mark.skip_on_darwin(reason="Test is broken on macosx")
812@pytest.mark.skip_on_windows(
813    reason=(
814        "Functional testing this on windows raises unicode errors. "
815        "Tested in tests/pytests/integration/modules/state/test_state.py"
816    )
817)
818def test_state_sls_unicode_characters(state, state_tree):
819    """
820    test state.sls when state file contains non-ascii characters
821    """
822    sls_contents = """
823    echo1:
824      cmd.run:
825        - name: "echo 'This is Æ test!'"
826    """
827    with pytest.helpers.temp_file("issue-46672.sls", sls_contents, state_tree):
828        ret = state.sls("issue-46672")
829        expected = "cmd_|-echo1_|-echo 'This is Æ test!'_|-run"
830        assert expected in ret
831
832
833def test_state_sls_integer_name(state, state_tree):
834    """
835    This tests the case where the state file is named
836    only with integers
837    """
838    sls_contents = """
839    always-passes:
840      test.succeed_without_changes
841    """
842    state_id = "test_|-always-passes_|-always-passes_|-succeed_without_changes"
843    with pytest.helpers.temp_file("12345.sls", sls_contents, state_tree):
844        ret = state.sls("12345")
845        assert state_id in ret
846        for state_return in ret:
847            assert state_return.result is True
848            assert "Success!" in state_return.comment
849
850        ret = state.sls(mods=12345)
851        assert state_id in ret
852        for state_return in ret:
853            assert state_return.result is True
854            assert "Success!" in state_return.comment
855
856
857def test_state_sls_lazyloader_allows_recursion(state, state_tree):
858    """
859    This tests that referencing dunders like __salt__ work
860    context: https://github.com/saltstack/salt/pull/51499
861    """
862    sls_contents = """
863    {% if 'nonexistent_module.function' in salt %}
864    {% do salt.log.warning("Module is available") %}
865    {% endif %}
866    always-passes:
867      test.succeed_without_changes:
868        - name: foo
869    """
870    state_id = "test_|-always-passes_|-foo_|-succeed_without_changes"
871    with pytest.helpers.temp_file("issue-51499.sls", sls_contents, state_tree):
872        ret = state.sls("issue-51499")
873        assert state_id in ret
874        for state_return in ret:
875            assert state_return.result is True
876            assert "Success!" in state_return.comment
877