salt/tests/pytests/unit/utils/jinja/test_salt_cache_loader.py
Pedro Algarvio 2e3f98a95c Add unit test to validate logic on calls to destroy()
Signed-off-by: Pedro Algarvio <palgarvio@vmware.com>
2023-04-28 06:24:27 +01:00

268 lines
8.9 KiB
Python

"""
Tests for salt.utils.jinja
"""
import copy
import os
import pytest
from jinja2 import Environment, exceptions
# dateutils is needed so that the strftime jinja filter is loaded
import salt.utils.dateutils # pylint: disable=unused-import
import salt.utils.files # pylint: disable=unused-import
import salt.utils.json # pylint: disable=unused-import
import salt.utils.stringutils # pylint: disable=unused-import
import salt.utils.yaml # pylint: disable=unused-import
from salt.utils.jinja import SaltCacheLoader
from tests.support.mock import Mock, call, patch
@pytest.fixture
def minion_opts(tmp_path, minion_opts):
minion_opts.update(
{
"file_buffer_size": 1048576,
"cachedir": str(tmp_path),
"file_roots": {"test": [str(tmp_path / "files" / "test")]},
"pillar_roots": {"test": [str(tmp_path / "files" / "test")]},
"extension_modules": os.path.join(
os.path.dirname(os.path.abspath(__file__)), "extmods"
),
}
)
return minion_opts
@pytest.fixture
def hello_simple(template_dir):
contents = """world
"""
with pytest.helpers.temp_file(
"hello_simple", directory=template_dir, contents=contents
) as hello_simple_filename:
yield hello_simple_filename
@pytest.fixture
def hello_include(template_dir):
contents = """{% include 'hello_import' -%}"""
with pytest.helpers.temp_file(
"hello_include", directory=template_dir, contents=contents
) as hello_include_filename:
yield hello_include_filename
@pytest.fixture
def relative_dir(template_dir):
relative_dir = template_dir / "relative"
relative_dir.mkdir()
return relative_dir
@pytest.fixture
def relative_rhello(relative_dir):
contents = """{% from './rmacro' import rmacro with context -%}
{{ rmacro('Hey') ~ rmacro(a|default('a'), b|default('b')) }}
"""
with pytest.helpers.temp_file(
"rhello", directory=relative_dir, contents=contents
) as relative_rhello:
yield relative_rhello
@pytest.fixture
def relative_rmacro(relative_dir):
contents = """{% from '../macro' import mymacro with context %}
{% macro rmacro(greeting, greetee='world') -%}
{{ mymacro(greeting, greetee) }}
{%- endmacro %}
"""
with pytest.helpers.temp_file(
"rmacro", directory=relative_dir, contents=contents
) as relative_rmacro:
yield relative_rmacro
@pytest.fixture
def relative_rescape(relative_dir):
contents = """{% import '../../rescape' as xfail -%}
"""
with pytest.helpers.temp_file(
"rescape", directory=relative_dir, contents=contents
) as relative_rescape:
yield relative_rescape
@pytest.fixture
def get_loader(mock_file_client, minion_opts):
def run_command(opts=None, saltenv="base", **kwargs):
"""
Now that we instantiate the client in the __init__, we need to mock it
"""
if opts is None:
opts = minion_opts
mock_file_client.opts = opts
loader = SaltCacheLoader(opts, saltenv, _file_client=mock_file_client)
# Create a mock file client and attach it to the loader
return loader
return run_command
def get_test_saltenv(get_loader):
"""
Setup a simple jinja test environment
"""
loader = get_loader(saltenv="test")
jinja = Environment(loader=loader)
return loader._file_client, jinja
def test_searchpath(minion_opts, get_loader, tmp_path):
"""
The searchpath is based on the cachedir option and the saltenv parameter
"""
opts = copy.deepcopy(minion_opts)
opts.update({"cachedir": str(tmp_path)})
loader = get_loader(opts=minion_opts, saltenv="test")
assert loader.searchpath == [str(tmp_path / "files" / "test")]
def test_mockclient(minion_opts, template_dir, hello_simple, get_loader):
"""
A MockFileClient is used that records all file requests normally sent
to the master.
"""
loader = get_loader(opts=minion_opts, saltenv="test")
res = loader.get_source(None, "hello_simple")
assert len(res) == 3
# res[0] on Windows is unicode and use os.linesep so it works cross OS
assert str(res[0]) == "world" + os.linesep
assert res[1] == str(hello_simple)
assert res[2](), "Template up to date?"
assert loader._file_client.requests
assert loader._file_client.requests[0]["path"] == "salt://hello_simple"
def test_import(get_loader, hello_import):
"""
You can import and use macros from other files
"""
fc, jinja = get_test_saltenv(get_loader)
result = jinja.get_template("hello_import").render()
assert result == "Hey world !a b !"
assert len(fc.requests) == 2
assert fc.requests[0]["path"] == "salt://hello_import"
assert fc.requests[1]["path"] == "salt://macro"
def test_relative_import(
get_loader, relative_rhello, relative_rmacro, relative_rescape, macro_template
):
"""
You can import using relative paths
issue-13889
"""
fc, jinja = get_test_saltenv(get_loader)
tmpl = jinja.get_template(os.path.join("relative", "rhello"))
result = tmpl.render()
assert result == "Hey world !a b !"
assert len(fc.requests) == 3
assert fc.requests[0]["path"] == "salt://relative/rhello"
assert fc.requests[1]["path"] == "salt://relative/rmacro"
assert fc.requests[2]["path"] == "salt://macro"
# This must fail when rendered: attempts to import from outside file root
template = jinja.get_template("relative/rescape")
pytest.raises(exceptions.TemplateNotFound, template.render)
def test_include(get_loader, hello_include, hello_import):
"""
You can also include a template that imports and uses macros
"""
fc, jinja = get_test_saltenv(get_loader)
result = jinja.get_template("hello_include").render()
assert result == "Hey world !a b !"
assert len(fc.requests) == 3
assert fc.requests[0]["path"] == "salt://hello_include"
assert fc.requests[1]["path"] == "salt://hello_import"
assert fc.requests[2]["path"] == "salt://macro"
def test_include_context(get_loader, hello_include, hello_import):
"""
Context variables are passes to the included template by default.
"""
_, jinja = get_test_saltenv(get_loader)
result = jinja.get_template("hello_include").render(a="Hi", b="Salt")
assert result == "Hey world !Hi Salt !"
def test_cached_file_client(get_loader, minion_opts):
"""
Multiple instantiations of SaltCacheLoader use the cached file client
"""
with patch("salt.channel.client.ReqChannel.factory", Mock()):
loader_a = SaltCacheLoader(minion_opts)
loader_b = SaltCacheLoader(minion_opts)
assert loader_a._file_client is loader_b._file_client
def test_file_client_kwarg(minion_opts, mock_file_client):
"""
A file client can be passed to SaltCacheLoader overriding the any
cached file client
"""
mock_file_client.opts = minion_opts
loader = SaltCacheLoader(minion_opts, _file_client=mock_file_client)
assert loader._file_client is mock_file_client
def test_cache_loader_passed_file_client(minion_opts, mock_file_client):
"""
The shudown method can be called without raising an exception when the
file_client does not have a destroy method
"""
# Test SaltCacheLoader creating and destroying the file client created
file_client = Mock()
with patch("salt.fileclient.get_file_client", return_value=file_client):
loader = SaltCacheLoader(minion_opts)
assert loader._file_client is None
with loader:
assert loader._file_client is file_client
assert loader._file_client is None
assert file_client.mock_calls == [call.destroy()]
# Test SaltCacheLoader reusing the file client passed
file_client = Mock()
file_client.opts = {"file_roots": minion_opts["file_roots"]}
with patch("salt.fileclient.get_file_client", return_value=Mock()):
loader = SaltCacheLoader(minion_opts, _file_client=file_client)
assert loader._file_client is file_client
with loader:
assert loader._file_client is file_client
assert loader._file_client is file_client
assert file_client.mock_calls == []
# Test SaltCacheLoader creating a client even though a file client was
# passed because the "file_roots" option is different, and, as such,
# the destroy method on the new file client is called, but not on the
# file client passed in.
file_client = Mock()
file_client.opts = {"file_roots": ""}
new_file_client = Mock()
with patch("salt.fileclient.get_file_client", return_value=new_file_client):
loader = SaltCacheLoader(minion_opts, _file_client=file_client)
assert loader._file_client is file_client
with loader:
assert loader._file_client is not file_client
assert loader._file_client is new_file_client
assert loader._file_client is None
assert file_client.mock_calls == []
assert new_file_client.mock_calls == [call.destroy()]