Getting Started
###############
Pytest-mh is not a plugin to support unit testing. It is a plugin designed to
support testing your application as a complete product, this is often referred
to as a black-box, application or system testing. The application is installed
on the target host and tested by running commands on the host (and on other
hosts that are required). These hosts can be virtual machines or containers.
.. seealso::
SSH, podman and docker may be used to execute commands on the remote hosts. See
:doc:`running-commands` for more information.
As such, it is often useful to write a high level API that will support testing
your application and make your test smaller and more readable. This may require
a non-trivial initial investment, but it will pay off in the long run. However,
implementing such API is not required and it is perfectly possible to run
commands on the host directly from each test. This approach makes tests usually
larger and more difficult to understand and maintain -- but every project is
different and it is up to you to choose how are you going to test your
application.
.. seealso::
Pytest-mh provides several building blocks to help you design your test
framework. See :doc:`extending`. **It would be good to open this document
and read it side by side.**
We will use `sudo `__ as the application to test for this
getting started guide as sudo is known by every power user so chances are that
you are already familiar with it. Additionally, sudo tests allows us to show
many pytest-mh features. Note, that these tests were written only as an example
and sudo itself is not using pytest-mh for its tests at this moment and as far
as we know there are no plans to do so.
.. seealso::
The example code can be found in the `example
`__ folder of
the git repository.
Example project: sudo
=====================
`Sudo `__ is a widely known tool that can elevate current
user's privileges by running command as different user -- usually ``root``. It
is possible to write a set of rules to define which user can run which command
and these rules can be stored either locally in ``/etc/sudoers`` or in `LDAP
`__
database.
Our goals are:
* write basic tests
* allow the user to run all commands, user must authenticate
* allow the user to run all commands, without authentication
* allow the user to run all commands if they are a member of a group, user
must authenticate
* allow the user to run all commands if they are a member of a group, without
authentication
* these tests must be written for all possible sources of data
* file: ``/etc/sudoers``
* LDAP pulled directly by sudo
* LDAP pulled by SSSD
* write a simple test framework that will help us to extend the tests easily
* every change must be reverted after each test
As you can see, these goals require us to write 12 tests in total. But since the
result is the same and only the data is fetched from different sources, we can
use :ref:`topology parametrization `. Topology
parametrization allows us to write only one test but run it against
different backends and thus we will do less work but get more code coverage.
We will take the following steps to achieve it:
#. :ref:`get_started_structure`
#. :ref:`get_started_topologies`
#. :ref:`get_started_config_file`
#. :ref:`get_started_config_domain`
#. :ref:`get_started_framework`
#. :ref:`get_started_enable`
#. :ref:`get_started_write_tests`
#. :ref:`get_started_run_tests`
.. _get_started_structure:
Prepare a file structure
------------------------
The following snippet shows a recommended file structure for your test utilizing
pytest-mh. Look at :doc:`extending` to get more information about the meaning of
individual classes.
.. code-block:: text
.
├── framework/ # Test framework, high-level API
│ ├── hosts/ # Subclasses of MultihostHost
│ │ └── __init__.py
│ ├── roles/ # Subclasses of MultihostRole
│ │ └── __init__.py
│ ├── utils/ # Subclasses of MultihostUtility
│ │ └── __init__.py
│ ├── __init__.py
│ ├── config.py # Definition of MultihostConfig, MultihostDomain
│ ├── topology_controllers.py # Custom topology controllers
│ └── topology.py # Definition of multihost topologies
|
├── tests/ # Tests
|
├── conftest.py # Pytest conftest.py
├── pytest.ini # Pytest configuration file
├── py.typed # Declare that this project uses type hints
|
├── mhc.yaml # Pytest-mh configuration file
|
├── readme.md # Tests readme
└── requirements.txt # Tests requirements
.. _get_started_topologies:
Define multihost topologies
---------------------------
This is the first step when designing a test framework since it defines what
hosts and roles your project needs. For sudo, we want sudo rules to be fetched
from different sources. We can consider each data source to be a single
topology.
* **sudoers**
* only one host needed
* users, groups and sudo rules will be created locally
* **ldap**
* we need a host where we will run sudo and a host that runs an LDAP server
* users, groups and sudo rules will be added to the LDAP database
* sudo will read data from LDAP
* **sssd**
* we need a host where we will run sudo and SSSD and a host that runs an LDAP server
* SSSD will be connected to the LDAP domain
* users, groups and sudo rules will be added to the LDAP database
* sudo will read data from SSSD which in turn reads it from LDAP
These are the three topologies that we will define. We will also define a
topology group as a shortcut for :ref:`topology parametrization
`.
.. dropdown:: See the code
:color: primary
:icon: code
.. tab-set::
.. tab-item:: ./framework/topology.py
.. literalinclude:: ../../example/framework/topology.py
:language: python
.. _get_started_config_file:
Write configuration file
------------------------
The topology defines which hosts and roles are needed to run sudo tests. We can
convert it into a configuration file that can be used to run all sudo tests.
The configuration file will define one domain with two hosts - one ``client``
which will run sudo and SSSD and one ``ldap`` which will run the LDAP server.
.. seealso::
The full format of the configuration file can be found at :doc:`mhc-yaml`.
.. dropdown:: See the code
:color: primary
:icon: code
.. tab-set::
.. tab-item:: ./mhc.yml
.. literalinclude:: ../../example/mhc.yaml
:language: yaml
.. _get_started_config_domain:
Define :class:`~pytest_mh.MultihostConfig` and :class:`~pytest_mh.MultihostDomain`
----------------------------------------------------------------------------------
These two classes are required to correctly map the configuration file into your
Python code. Look for more information at :doc:`extending/multihost-config` and
:doc:`extending/multihost-domains`. It is possible to extend these classes in
order to add custom configuration options, use different topology mark and so
on. In this example, they only provide the mapping from configuration file to
Python classes.
.. dropdown:: See the code
:color: primary
:icon: code
.. tab-set::
.. tab-item:: ./framework/config.py
.. literalinclude:: ../../example/framework/config.py
:language: python
.. _get_started_framework:
Design and implement the framework
----------------------------------
This step is more complicated and can not be treated universally as every
project has different needs. It is possible to use multiple building blocks
provided by ``pytest-mh`` in order to build a high-level API for your tests, see
:doc:`extending` and :doc:`life-cycle` to get a good grasp of all the classes
and how to use them.
For the sudo tests, we have implemented several hosts, roles and utility classes
and one topology controller for each topology. The following table describes the
main idea behind each of these classes.
.. dropdown:: See the table
:color: primary
:icon: code
.. list-table::
:header-rows: 1
* - Class name/Subclass of
- Description
* - | ``ClientHost``
| :class:`~pytest_mh.MultihostBackupHost`
- * Implements backup and restore methods for the client.
* - | ``LDAPHost``
| :class:`~pytest_mh.MultihostBackupHost`
- * Implements backup and restore methods for the LDAP server.
* Opens and maintains connection to the LDAP server using
python-ldap library.
* - | ``SudoersTopologyController``
| :class:`~pytest_mh.BackupTopologyController`
- * Configures environment for the sudoers topology
* Sets expected content of ``/etc/nsswitch.conf``
* Creates backup of this setup and automatically restores its
state when a test is finished
* - | ``LDAPTopologyController``
| :class:`~pytest_mh.BackupTopologyController`
- * Configures environment for the LDAP topology
* Sets expected content of ``/etc/nsswitch.conf``
* Configures SSSD for identity and authentication
* Configures ``/etc/ldap.conf`` that is read by sudo
* Creates backup of this setup and automatically restores its
state when a test is finished
* - | ``SSSDTopologyController``
| :class:`~pytest_mh.BackupTopologyController`
- * Configures environment for the SSSD topology
* Sets expected content of ``/etc/nsswitch.conf``
* Configures SSSD for identity, authentication and sudo rules
* Creates backup of this setup and automatically restores its
state when a test is finished
* - | ``Client``
| :class:`~pytest_mh.MultihostRole`
- * Implements ``GenericProvider`` which defines interface for
managing users, groups and sudoers.
* The implementation uses local files to store the content.
* - | ``LDAP``
| :class:`~pytest_mh.MultihostRole`
- * Implements ``GenericProvider`` which defines interface for
managing users, groups and sudoers.
* The implementation uses LDAP to store the content.
* - | ``LocalUsersUtils``
| :class:`~pytest_mh.MultihostUtility`
- * Provides shareable implementation of local users and groups
management.
* Every user and group added during testing is automatically
removed.
* - | ``SUDOUtils``
| :class:`~pytest_mh.MultihostUtility`
- * Implements methods to execute sudo and assert the result
.. seealso::
Look at the `example code
`__ to see
how this was implemented.
.. _get_started_enable:
Enable pytest-mh in conftest.py
-------------------------------
When the test framework is written and ready to use, we can tell pytest to
start using it in our tests. First, configure pytest to load pytest-mh plugin and
then inform pytest-mh which config class it should instantiate.
.. dropdown:: See the code
:color: primary
:icon: code
.. tab-set::
.. tab-item:: ./conftest.py
.. literalinclude:: ../../example/conftest.py
:language: python
.. _get_started_write_tests:
Write the tests
===============
The example code shows four tests in total, but 12 tests are executed when
pytest is run because each test is run once per topology against different data
sources. See :doc:`writing-tests` to get more information on how to write
the tests.
* allow the user to run all commands, user must authenticate
* allow the user to run all commands, without authentication
* allow the user to run all commands if they are a member of a group, user
must authenticate
* allow the user to run all commands if they are a member of a group, without
authentication
.. dropdown:: See the code
:color: primary
:icon: code
.. tab-set::
.. tab-item:: ./tests/test_user.py
.. literalinclude:: ../../example/tests/test_user.py
:language: python
.. tab-item:: ./tests/test_group.py
.. literalinclude:: ../../example/tests/test_group.py
:language: python
.. _get_started_run_tests:
Run the tests
=============
The example code provides a set of containers that can be started and used as
hosts for testing. See the example `readme.md
`__ to
get the instruction on how to start the containers and install requirements.
When the containers or virtual machines are ready, it is possible to run the
tests with the ``pytest`` command that you are already familiar with. The only
additional thing needed to run pytest-mh tests is to provide the path to the
pytest-mh configuration file with ``--mh-config``.
.. code-block:: text
$ pytest --color=yes --mh-config=./mhc.yaml -vvv
Multihost configuration:
domains:
- id: sudo
hosts:
- hostname: master.ldap.test
conn:
type: ssh
host: 172.16.200.3
role: ldap
- hostname: client.test
conn:
type: ssh
host: 172.16.200.4
role: client
artifacts:
- /var/log/sssd
Detected topology:
- id: sudo
hosts:
ldap: 1
client: 1
Additional settings:
config file: ./example/mhc.yaml
log path: None
lazy ssh: False
topology filter:
require exact topology: False
collect artifacts: on-failure
artifacts directory: artifacts
collect logs: on-failure
============================= test session starts ==============================
platform linux -- Python 3.11.9, pytest-8.3.3, pluggy-1.5.0 -- /home/runner/work/pytest-mh/pytest-mh/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/runner/work/pytest-mh/pytest-mh/example
configfile: pytest.ini
collecting ...
Selected tests will use the following hosts:
client: client.test
ldap: master.ldap.test
collected 12 items
example/tests/test_group.py::test_group__passwd (ldap) PASSED [ 8%]
example/tests/test_group.py::test_group__nopasswd (ldap) PASSED [ 16%]
example/tests/test_user.py::test_user__passwd (ldap) PASSED [ 25%]
example/tests/test_user.py::test_user__nopasswd (ldap) PASSED [ 33%]
example/tests/test_group.py::test_group__passwd (sssd) PASSED [ 41%]
example/tests/test_group.py::test_group__nopasswd (sssd) PASSED [ 50%]
example/tests/test_user.py::test_user__passwd (sssd) PASSED [ 58%]
example/tests/test_user.py::test_user__nopasswd (sssd) PASSED [ 66%]
example/tests/test_group.py::test_group__passwd (sudoers) PASSED [ 75%]
example/tests/test_group.py::test_group__nopasswd (sudoers) PASSED [ 83%]
example/tests/test_user.py::test_user__passwd (sudoers) PASSED [ 91%]
example/tests/test_user.py::test_user__nopasswd (sudoers) PASSED [100%]
============================= 12 passed in 24.80s ==============================