import os import re import time import pytest import testinfra.utils.ansible_runner testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') @pytest.mark.parametrize("expected_group, expected_group_id", [ ('web-parameters-mandatory', 1003), ('web-parameters-optional_local', 5001), ('web-parameters-paste-req', 5002), ]) def test_website_group(host, expected_group, expected_group_id): """ Tests if website group has been created correctly. """ group = host.group(expected_group) assert group.exists assert group.gid == expected_group_id @pytest.mark.parametrize("fqdn, expected_group, expected_admin, expected_user_id", [ ('parameters-mandatory', 'web-parameters-mandatory', 'admin-parameters-mandatory', 1003), ('parameters-optional.local', 'web-parameters-optional_local', 'admin-parameters-optional_local', 5000), ('parameters-paste-req', 'web-parameters-paste-req', 'admin-parameters-paste-req', 5002) ]) def test_website_admin_user(host, fqdn, expected_group, expected_admin, expected_user_id): user = host.user(expected_admin) assert user.exists assert user.uid == expected_user_id assert user.group == expected_group assert user.groups == [expected_group] assert user.shell == '/bin/bash' assert user.home == '/var/www/' + fqdn @pytest.mark.parametrize("home, expected_owner, expected_group", [ ('/var/www/parameters-mandatory', 'admin-parameters-mandatory', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local', 'admin-parameters-optional_local', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req', 'admin-parameters-paste-req', 'web-parameters-paste-req'), ]) def test_website_admin_home(host, home, expected_owner, expected_group): """ Tests if permissions on website admin home directory are correct. """ home = host.file(home) assert home.is_directory assert home.user == expected_owner assert home.group == expected_group assert home.mode == 0o750 @pytest.mark.parametrize("profile_dir, expected_owner, expected_group", [ ('/var/www/parameters-mandatory/.profile.d', 'admin-parameters-mandatory', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local/.profile.d', 'admin-parameters-optional_local', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req/.profile.d', 'admin-parameters-paste-req', 'web-parameters-paste-req'), ]) def test_home_profile_directory(host, profile_dir, expected_owner, expected_group): """ Tests if profile directory has been set-up correctly for the website administrator/application user. """ with host.sudo(): directory = host.file(profile_dir) assert directory.is_directory assert directory.user == expected_owner assert directory.group == expected_group assert directory.mode == 0o750 @pytest.mark.parametrize("profile_file, expected_group", [ ('/var/www/parameters-mandatory/.profile.d/virtualenv.sh', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local/.profile.d/virtualenv.sh', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req/.profile.d/virtualenv.sh', 'web-parameters-paste-req'), ]) def test_virtualenv_profile_configuration(host, profile_file, expected_group): """ Tests if profile configuration file for auto-activation of virtual environment has been deployed correctly. """ with host.sudo(): config = host.file(profile_file) assert config.is_file assert config.user == 'root' assert config.group == expected_group assert config.mode == 0o640 @pytest.mark.parametrize("profile_file, expected_group, expected_content", [ ('/var/www/parameters-mandatory/.profile.d/environment.sh', 'web-parameters-mandatory', '\n'), ('/var/www/parameters-optional.local/.profile.d/environment.sh', 'web-parameters-optional_local', "export MY_ENV_VAR='My environment variable'\n"), ('/var/www/parameters-paste-req/.profile.d/environment.sh', 'web-parameters-paste-req', '\n'), ]) def test_environment_profile_configuration(host, profile_file, expected_group, expected_content): """ Tests if profile configuration file for setting-up environment variables has been deployed correctly. """ with host.sudo(): config = host.file(profile_file) assert config.is_file assert config.user == 'root' assert config.group == expected_group assert config.mode == 0o640 assert config.content_string == expected_content @pytest.mark.parametrize("admin_user, expected_virtualenv_path", [ ('admin-parameters-mandatory', '/var/www/parameters-mandatory/virtualenv\n'), ('admin-parameters-optional_local', '/var/www/parameters-optional.local/virtualenv\nMy environment variable\n'), ('admin-parameters-paste-req', '/var/www/parameters-paste-req/virtualenv\n'), ]) def test_profile_configuration(host, admin_user, expected_virtualenv_path): """ Tests if profile configuration is behaving correctly (setting appropriate vars via login shell). """ env = host.run("sudo -i -u " + admin_user + " printenv VIRTUAL_ENV MY_ENV_VAR") assert env.stdout == expected_virtualenv_path @pytest.mark.parametrize("expected_group", [ 'web-parameters-mandatory', 'web-parameters-optional_local', 'web-parameters-paste-req' ]) def test_nginx_user(host, expected_group): """ Tests if web server user has been added to website group. """ user = host.user('www-data') assert expected_group in user.groups @pytest.mark.parametrize("forward_file, expected_group, expected_content", [ ('/var/www/parameters-mandatory/.forward', 'web-parameters-mandatory', 'root\n'), ('/var/www/parameters-optional.local/.forward', 'web-parameters-optional_local', 'user\n'), ('/var/www/parameters-paste-req/.forward', 'web-parameters-paste-req', 'root\n'), ]) def test_forward_file(host, forward_file, expected_group, expected_content): """ Tests if the forward file has correct permissions and content. """ with host.sudo(): config = host.file(forward_file) assert config.is_file assert config.user == 'root' assert config.group == expected_group assert config.mode == 0o640 assert config.content_string == expected_content @pytest.mark.parametrize("original_destination, expected_destination_user", [ ('web-parameters-mandatory@localhost', 'vagrant'), ('web-parameters-optional_local@localhost', 'user'), ('web-parameters-paste-req@localhost', 'vagrant'), ]) def test_mail_forwarding(host, original_destination, expected_destination_user): """ Tests if mail forwarding works as expected. """ hostname = host.run('hostname').stdout.strip() send = host.run('swaks --suppress-data --to ' + original_destination) assert send.rc == 0 original_queue_id = re.search('Ok: queued as (.*)', send.stdout).group(1) # Sleep for a couple of seconds so the mail can get delivered. time.sleep(5) with host.sudo(): mail_log = host.file('/var/log/mail.log') # First extract message ID of forwarded mail. pattern = r"%s: to=<%s>.*status=sent \(forwarded as ([^)]*)\)" % (original_queue_id, original_destination) forward_queue_id = re.search(pattern, mail_log.content_string).group(1) # Now try to determine where the forward ended-up at. pattern = "%s: to=<%s@%s>, orig_to=<%s>.*status=sent" % (forward_queue_id, expected_destination_user, hostname, original_destination) assert re.search(pattern, mail_log.content_string) is not None @pytest.mark.parametrize("virtualenv_dir, expected_owner, expected_group", [ ('/var/www/parameters-mandatory/virtualenv', 'admin-parameters-mandatory', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local/virtualenv', 'admin-parameters-optional_local', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req/virtualenv', 'admin-parameters-paste-req', 'web-parameters-paste-req'), ]) def test_python_virtualenv_created(host, virtualenv_dir, expected_owner, expected_group): """ Tests if Python virtual environment has been created correctly. """ with host.sudo(): virtualenv = host.file(virtualenv_dir) assert virtualenv.is_directory assert virtualenv.user == expected_owner assert virtualenv.group == expected_group assert virtualenv.mode == 0o2750 virtualenv_activate = host.file(virtualenv_dir + "/bin/activate") assert virtualenv_activate.is_file assert virtualenv_activate.user == expected_owner assert virtualenv_activate.group == expected_group assert virtualenv_activate.mode == 0o640 @pytest.mark.parametrize("project_file, expected_owner, expected_group", [ ('/var/www/parameters-mandatory/virtualenv/.project', 'admin-parameters-mandatory', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local/virtualenv/.project', 'admin-parameters-optional_local', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req/virtualenv/.project', 'admin-parameters-paste-req', 'web-parameters-paste-req'), ]) def test_python_virtualenv_project_directory_config(host, project_file, expected_owner, expected_group): """ Tests if project directory configuration within virtualenv is set-up correctly. """ with host.sudo(): project = host.file(project_file) assert project.is_file assert project.user == expected_owner assert project.group == expected_group assert project.mode == 0o640 @pytest.mark.parametrize("virtualenv_dir, admin_user, expected_prompt", [ ('/var/www/parameters-mandatory/virtualenv', 'admin-parameters-mandatory', '(parameters-mandatory) '), ('/var/www/parameters-optional.local/virtualenv', 'admin-parameters-optional_local', '(parameters-optional.local) '), ('/var/www/parameters-paste-req/virtualenv', 'admin-parameters-paste-req', '(parameters-paste-req) '), ]) def test_python_virtualenv_prompt(host, virtualenv_dir, admin_user, expected_prompt): """ Tests if Python virtual environment prompt has been set-up correctly. """ with host.sudo(admin_user): prompt = host.run('bash -c "source %s/bin/activate; printenv PS1"', virtualenv_dir) # Chop off trailing newline if present (this is from the # host.run itself). if prompt.stdout.endswith("\n"): prompt_stdout = prompt.stdout[:-1] else: prompt_stdout = prompt.stdout assert prompt_stdout == expected_prompt @pytest.mark.parametrize("wrapper_script, expected_owner, expected_group", [ ('/var/www/parameters-mandatory/virtualenv/bin/exec', 'admin-parameters-mandatory', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local/virtualenv/bin/exec', 'admin-parameters-optional_local', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req/virtualenv/bin/exec', 'admin-parameters-paste-req', 'web-parameters-paste-req'), ]) def test_python_virtualenv_wrapper_script(host, wrapper_script, expected_owner, expected_group): """ Tests if Python virtualenv wrapper script is functioning correctly. """ with host.sudo(): wrapper = host.file(wrapper_script) assert wrapper.is_file assert wrapper.user == expected_owner assert wrapper.group == expected_group assert wrapper.mode == 0o750 command = host.run("sudo -u %s %s python -c 'import gunicorn'", expected_owner, wrapper_script) assert command.rc == 0 @pytest.mark.parametrize("admin_user, pip_path, expected_packages", [ ('admin-parameters-mandatory', '/var/www/parameters-mandatory/virtualenv/bin/pip', [ "gunicorn==21.2.0", "packaging==23.2", ]), ('admin-parameters-optional_local', '/var/www/parameters-optional.local/virtualenv/bin/pip', [ "appdirs==1.4.4", "dnspython==2.6.1", "gunicorn==21.1.0", "jedi==0.19.1", "packaging==23.2", "parso==0.8.3", "prompt-toolkit==3.0.43", "ptpython==3.0.26", "pygments==2.17.2", "wcwidth==0.2.13", ]), ('admin-parameters-paste-req', '/var/www/parameters-paste-req/virtualenv/bin/pip', [ "Flask==3.0.2", "Jinja2==3.1.3", "MarkupSafe==2.1.5", "Paste==3.7.1", "PasteDeploy==3.1.0", "Werkzeug==3.0.1", "blinker==1.7.0", "click==8.1.7", "gunicorn==21.1.0", "importlib-metadata==7.0.1", "itsdangerous==2.1.2", "packaging==23.2", "six==1.16.0", "zipp==3.17.0", ]), ]) def test_virtualenv_packages(host, admin_user, pip_path, expected_packages): """ Tests if correct packages are installed in virtualenv. """ packages = host.run("sudo -u %s %s freeze", admin_user, pip_path) # Normalise package names and order. expected_packages = sorted([p.lower() for p in expected_packages]) actual_packages = sorted(packages.stdout.lower().strip().split("\n")) assert actual_packages == expected_packages @pytest.mark.parametrize("config_file, expected_website_name, expected_socket_file_path", [ ('/etc/systemd/system/parameters-mandatory.socket', 'parameters-mandatory', '/run/wsgi/parameters-mandatory.sock'), ('/etc/systemd/system/parameters-optional.local.socket', 'parameters-optional.local', '/run/wsgi/parameters-optional.local.sock'), ('/etc/systemd/system/parameters-paste-req.socket', 'parameters-paste-req', '/run/wsgi/parameters-paste-req.sock'), ]) def test_systemd_socket_configuration_file(host, config_file, expected_website_name, expected_socket_file_path): """ Tests if systemd socket configuration file has been set-up correctly. """ config = host.file(config_file) assert config.is_file assert config.user == 'root' assert config.group == 'root' assert config.mode == 0o644 assert "Socket for website %s" % expected_website_name in config.content_string assert "ListenStream=%s" % expected_socket_file_path in config.content_string @pytest.mark.parametrize("socket_file_path", [ '/run/wsgi/parameters-mandatory.sock', '/run/wsgi/parameters-optional.local.sock', '/run/wsgi/parameters-paste-req.sock', ]) def test_systemd_socket(host, socket_file_path): """ Tests if systemd socket has correct permissions and is available. """ with host.sudo(): socket_file = host.file(socket_file_path) assert socket_file.is_socket assert socket_file.user == 'www-data' assert socket_file.group == 'www-data' assert socket_file.mode == 0o660 socket = host.socket("unix://%s" % socket_file_path) assert socket.is_listening @pytest.mark.parametrize("service_file, expected_fqdn", [ ('/etc/systemd/system/parameters-mandatory.service', 'parameters-mandatory'), ('/etc/systemd/system/parameters-optional.local.service', 'parameters-optional.local'), ('/etc/systemd/system/parameters-paste-req.service', 'parameters-paste-req'), ]) def test_systemd_service_configuration_file(host, service_file, expected_fqdn): """ Tests if systemd service configuration file has been set-up correctly. """ config = host.file(service_file) assert config.is_file assert config.user == 'root' assert config.group == 'root' assert config.mode == 0o644 assert expected_fqdn in config.content_string @pytest.mark.parametrize("service_name", [ 'parameters-mandatory', 'parameters-optional.local', 'parameters-paste-req', ]) def test_systemd_service(host, service_name): """ Tests if the systemd service is enabled at boot and running. """ service = host.service(service_name) assert service.is_enabled assert service.is_running @pytest.mark.parametrize("directory_path, expected_owner, expected_group", [ ('/var/www/parameters-mandatory/htdocs', 'admin-parameters-mandatory', 'web-parameters-mandatory'), ('/var/www/parameters-optional.local/htdocs', 'admin-parameters-optional_local', 'web-parameters-optional_local'), ('/var/www/parameters-paste-req/htdocs', 'admin-parameters-paste-req', 'web-parameters-paste-req'), ]) def test_static_file_directory(host, directory_path, expected_owner, expected_group): """ Tests if directory for serving static files has been created correctly. """ with host.sudo(): directory = host.file(directory_path) assert directory.is_directory assert directory.user == expected_owner assert directory.group == expected_group assert directory.mode == 0o2750 @pytest.mark.parametrize("private_key_path, certificate_path, expected_private_key, expected_certificate", [ ('/etc/ssl/private/parameters-mandatory_https.key', '/etc/ssl/certs/parameters-mandatory_https.pem', 'tests/data/x509/server/parameters-mandatory_https.key.pem', 'tests/data/x509/server/parameters-mandatory_https.cert.pem'), ('/etc/ssl/private/parameters-optional.local_https.key', '/etc/ssl/certs/parameters-optional.local_https.pem', 'tests/data/x509/server/parameters-optional.local_https.key.pem', 'tests/data/x509/server/parameters-optional.local_https.cert.pem'), ('/etc/ssl/private/parameters-paste-req_https.key', '/etc/ssl/certs/parameters-paste-req_https.pem', 'tests/data/x509/server/parameters-paste-req_https.key.pem', 'tests/data/x509/server/parameters-paste-req_https.cert.pem'), ]) def test_nginx_tls_files(host, private_key_path, certificate_path, expected_private_key, expected_certificate): """ Tests if TLS private key and certificate have been deployed correctly. """ with host.sudo(): tls_file = host.file(private_key_path) assert tls_file.is_file assert tls_file.user == 'root' assert tls_file.group == 'root' assert tls_file.mode == 0o640 assert tls_file.content_string == open(expected_private_key, "r").read().rstrip() tls_file = host.file(certificate_path) assert tls_file.is_file assert tls_file.user == 'root' assert tls_file.group == 'root' assert tls_file.mode == 0o644 assert tls_file.content_string == open(expected_certificate, "r").read().rstrip() @pytest.mark.parametrize("config_file_path, expected_content", [ ('/etc/check_certificate/parameters-mandatory_https.conf', '/etc/ssl/certs/parameters-mandatory_https.pem'), ('/etc/check_certificate/parameters-optional.local_https.conf', '/etc/ssl/certs/parameters-optional.local_https.pem'), ('/etc/check_certificate/parameters-paste-req_https.conf', '/etc/ssl/certs/parameters-paste-req_https.pem'), ]) def test_certificate_validity_check_configuration(host, config_file_path, expected_content): """ Tests if certificate validity check configuration file has been deployed correctly. """ config = host.file(config_file_path) assert config.is_file assert config.user == 'root' assert config.group == 'root' assert config.mode == 0o644 assert config.content_string == expected_content @pytest.mark.parametrize("config_file", [ '/etc/nginx/sites-available/parameters-mandatory', '/etc/nginx/sites-available/parameters-optional.local', '/etc/nginx/sites-available/parameters-paste-req', ]) def test_vhost_file(host, config_file): """ Tests permissions of vhost configuration file. """ config = host.file(config_file) assert config.is_file assert config.user == 'root' assert config.group == 'root' assert config.mode == 0o640 @pytest.mark.parametrize("config_file, expected_content", [ ('/etc/nginx/sites-enabled/parameters-mandatory', '/etc/nginx/sites-available/parameters-mandatory'), ('/etc/nginx/sites-enabled/parameters-optional.local', '/etc/nginx/sites-available/parameters-optional.local'), ('/etc/nginx/sites-enabled/parameters-paste-req', '/etc/nginx/sites-available/parameters-paste-req'), ]) def test_website_enabled(host, config_file, expected_content): """ Tests if website has been enabled. """ config = host.file(config_file) assert config.is_symlink assert config.linked_to == expected_content @pytest.mark.parametrize("python_path", [ ("/var/www/parameters-mandatory/virtualenv/bin/python"), ("/var/www/parameters-optional.local/virtualenv/bin/python"), ("/var/www/parameters-paste-req/virtualenv/bin/python"), ]) def test_python_version_in_virtualenv(host, python_info, python_path,): """ Tests if correct Python version is used inside of virtual environment. """ expected_version = "Python %s" % python_info.python_version with host.sudo(): python_version = host.run(python_path + " --version") assert python_version.rc == 0 assert expected_version in python_version.stdout @pytest.mark.parametrize("fqdn, pip_check_requirements_upgrades_directory, expected_requirements_in, expected_requirements", [ ("parameters-mandatory", "/etc/pip_check_requirements_upgrades", ["gunicorn"], ["gunicorn==21.2.0", "packaging==23.2"]), ("parameters-optional.local", "/etc/pip_check_requirements_upgrades", ["gunicorn"], ["gunicorn==21.1.0", "packaging==23.2"]), ("parameters-paste-req", "/etc/pip_check_requirements_upgrades", ["gunicorn"], ["gunicorn==21.1.0", "packaging==23.2"]), ]) def test_wsgi_requirements(host, fqdn, pip_check_requirements_upgrades_directory, expected_requirements_in, expected_requirements): """ Tests if Python requirements files are set-up correctly (for both installation and upgrade checks). """ with host.sudo(): directory = host.file('%s/%s' % (pip_check_requirements_upgrades_directory, fqdn)) assert directory.is_directory assert directory.user == 'root' assert directory.group == 'pipreqcheck' assert directory.mode == 0o750 config = host.file('%s/%s/wsgi_requirements.in' % (pip_check_requirements_upgrades_directory, fqdn)) assert config.is_file assert config.user == 'root' assert config.group == 'pipreqcheck' assert config.mode == 0o640 check_requirements_in = config.content_string.strip().split("\n") assert check_requirements_in == expected_requirements_in config = host.file('%s/%s/wsgi_requirements.txt' % (pip_check_requirements_upgrades_directory, fqdn)) assert config.is_file assert config.user == 'root' assert config.group == 'pipreqcheck' assert config.mode == 0o640 check_requirements = config.content_string.strip().split("\n") assert check_requirements == expected_requirements requirements = host.file('/var/www/%s/.wsgi_requirements.txt' % fqdn) assert requirements.is_file assert requirements.user == 'admin-%s' % fqdn.replace(".", "_") assert requirements.group == 'web-%s' % fqdn.replace(".", "_") assert requirements.mode == 0o640 install_requirements = requirements.content_string.strip().split("\n") assert install_requirements == expected_requirements @pytest.mark.parametrize('fqdn', [ 'parameters-mandatory', 'parameters-optional.local', 'parameters-paste-req' ]) def test_https_enforcement(host, fqdn): """ Tests if HTTPS is being enforced. """ https_enforcement = host.run('curl -I http://%s/', fqdn) assert https_enforcement.rc == 0 assert 'HTTP/1.1 301 Moved Permanently' in https_enforcement.stdout assert 'Location: https://%s/' % fqdn in https_enforcement.stdout https_enforcement = host.run('curl -I https://%s/', fqdn) assert https_enforcement.rc == 0 assert 'Strict-Transport-Security: max-age=31536000; includeSubDomains' in https_enforcement.stdout