diff --git a/.gitignore b/.gitignore index 73f14ae3..1e80dfb8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ *.swp __pycache__ .cache +.pytest_cache +.tox +.eggs +*.egg-info + # Created by https://www.gitignore.io/api/jetbrains+iml diff --git a/.travis.yml b/.travis.yml index 2ca3b2d2..fa525e01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,6 @@ python: install: - pip install -r requirements.txt -script: py.test -vv +script: + # tox.ini handles setup, ordering of docker build first, and then run tests + - tox diff --git a/requirements.txt b/requirements.txt index 53737ca5..f2c61e42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ pytest pytest-xdist pytest-cov testinfra +tox diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..0e393bc1 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +from setuptools import setup + +setup( + setup_requires=['pytest-runner'], + tests_require=['pytest'], +) diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..f5a9b5e8 --- /dev/null +++ b/test/README.md @@ -0,0 +1,25 @@ +# Recommended way to run tests + +Make sure you have Docker and Python w/pip package manager. + +From command line all you need to do is: + +- `pip install tox` +- `tox` + +Tox handles setting up a virtual environment for python dependancies, installing dependancies, building the docker images used by tests, and finally running tests. It's an easy way to have travis-ci like build behavior locally. + +## Alternative py.test method of running tests + +You're responsible for setting up your virtual env and dependancies in this situation. + +``` +py.test -vv -n auto -m "build_stage" +py.test -vv -n auto -m "not build_stage" +``` + +The build_stage tests have to run first to create the docker images, followed by the actual tests which utilize said images. Unless you're changing your dockerfiles you shouldn't have to run the build_stage every time - but it's a good idea to rebuild at least once a day in case the base Docker images or packages change. + +# How do I debug python? + +Highly recommended: Setup PyCharm on a **Docker enabled** machine. Having a python debugger like PyCharm changes your life if you've never used it :) diff --git a/test/conftest.py b/test/conftest.py index 5960cc24..58530d38 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,14 +1,30 @@ import pytest import testinfra +from textwrap import dedent check_output = testinfra.get_backend( "local://" ).get_module("Command").check_output +SETUPVARS = { + 'PIHOLE_INTERFACE': 'eth99', + 'IPV4_ADDRESS': '1.1.1.1', + 'IPV6_ADDRESS': 'FE80::240:D0FF:FE48:4672', + 'PIHOLE_DNS_1': '4.2.2.1', + 'PIHOLE_DNS_2': '4.2.2.2' +} + +tick_box = "[\x1b[1;32m\xe2\x9c\x93\x1b[0m]".decode("utf-8") +cross_box = "[\x1b[1;31m\xe2\x9c\x97\x1b[0m]".decode("utf-8") +info_box = "[i]".decode("utf-8") + + @pytest.fixture def Pihole(Docker): - ''' used to contain some script stubbing, now pretty much an alias. - Also provides bash as the default run function shell ''' + ''' + used to contain some script stubbing, now pretty much an alias. + Also provides bash as the default run function shell + ''' def run_bash(self, command, *args, **kwargs): cmd = self.get_command(command, *args) if self.user is not None: @@ -22,12 +38,18 @@ def Pihole(Docker): return out funcType = type(Docker.run) - Docker.run = funcType(run_bash, Docker, testinfra.backend.docker.DockerBackend) + Docker.run = funcType(run_bash, + Docker, + testinfra.backend.docker.DockerBackend) return Docker + @pytest.fixture def Docker(request, args, image, cmd): - ''' combine our fixtures into a docker run command and setup finalizer to cleanup ''' + ''' + combine our fixtures into a docker run command and setup finalizer to + cleanup + ''' assert 'docker' in check_output('id'), "Are you in the docker group?" docker_run = "docker run {} {} {}".format(args, image, cmd) docker_id = check_output(docker_run) @@ -40,22 +62,95 @@ def Docker(request, args, image, cmd): docker_container.id = docker_id return docker_container + @pytest.fixture def args(request): - ''' -t became required when tput began being used ''' + ''' + -t became required when tput began being used + ''' return '-t -d' -@pytest.fixture(params=['debian', 'centos']) + +@pytest.fixture(params=['debian', 'centos', 'fedora']) def tag(request): - ''' consumed by image to make the test matrix ''' + ''' + consumed by image to make the test matrix + ''' return request.param + @pytest.fixture() def image(request, tag): - ''' built by test_000_build_containers.py ''' + ''' + built by test_000_build_containers.py + ''' return 'pytest_pihole:{}'.format(tag) + @pytest.fixture() def cmd(request): - ''' default to doing nothing by tailing null, but don't exit ''' + ''' + default to doing nothing by tailing null, but don't exit + ''' return 'tail -f /dev/null' + + +# Helper functions +def mock_command(script, args, container): + ''' + Allows for setup of commands we don't really want to have to run for real + in unit tests + ''' + full_script_path = '/usr/local/bin/{}'.format(script) + mock_script = dedent('''\ + #!/bin/bash -e + echo "\$0 \$@" >> /var/log/{script} + case "\$1" in'''.format(script=script)) + for k, v in args.iteritems(): + case = dedent(''' + {arg}) + echo {res} + exit {retcode} + ;;'''.format(arg=k, res=v[0], retcode=v[1])) + mock_script += case + mock_script += dedent(''' + esac''') + container.run(''' + cat < {script}\n{content}\nEOF + chmod +x {script} + rm -f /var/log/{scriptlog}'''.format(script=full_script_path, + content=mock_script, + scriptlog=script)) + + +def mock_command_2(script, args, container): + ''' + Allows for setup of commands we don't really want to have to run for real + in unit tests + ''' + full_script_path = '/usr/local/bin/{}'.format(script) + mock_script = dedent('''\ + #!/bin/bash -e + echo "\$0 \$@" >> /var/log/{script} + case "\$1 \$2" in'''.format(script=script)) + for k, v in args.iteritems(): + case = dedent(''' + \"{arg}\") + echo \"{res}\" + exit {retcode} + ;;'''.format(arg=k, res=v[0], retcode=v[1])) + mock_script += case + mock_script += dedent(''' + esac''') + container.run(''' + cat < {script}\n{content}\nEOF + chmod +x {script} + rm -f /var/log/{scriptlog}'''.format(script=full_script_path, + content=mock_script, + scriptlog=script)) + + +def run_script(Pihole, script): + result = Pihole.run(script) + assert result.rc == 0 + return result diff --git a/test/fedora.Dockerfile b/test/fedora.Dockerfile new file mode 100644 index 00000000..c4834388 --- /dev/null +++ b/test/fedora.Dockerfile @@ -0,0 +1,16 @@ +FROM fedora:latest + +ENV GITDIR /etc/.pihole +ENV SCRIPTDIR /opt/pihole + +RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole +ADD . $GITDIR +RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $SCRIPTDIR/ +ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR + +RUN true && \ + chmod +x $SCRIPTDIR/* + +ENV PH_TEST true + +#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/test_000_build_containers.py b/test/test_000_build_containers.py index c617f3ae..e9e9e7db 100644 --- a/test/test_000_build_containers.py +++ b/test/test_000_build_containers.py @@ -6,10 +6,15 @@ run_local = testinfra.get_backend( "local://" ).get_module("Command").run + @pytest.mark.parametrize("image,tag", [ - ( 'test/debian.Dockerfile', 'pytest_pihole:debian' ), - ( 'test/centos.Dockerfile', 'pytest_pihole:centos' ), + ('test/debian.Dockerfile', 'pytest_pihole:debian'), + ('test/centos.Dockerfile', 'pytest_pihole:centos'), + ('test/fedora.Dockerfile', 'pytest_pihole:fedora'), ]) +# mark as 'build_stage' so we can ensure images are build first when tests +# are executed in parallel. (not required when tests are executed serially) +@pytest.mark.build_stage def test_build_pihole_image(image, tag): build_cmd = run_local('docker build -f {} -t {} .'.format(image, tag)) if build_cmd.rc != 0: diff --git a/test/test_automated_install.py b/test/test_automated_install.py index 2c65c660..876b06eb 100644 --- a/test/test_automated_install.py +++ b/test/test_automated_install.py @@ -1,24 +1,40 @@ -import pytest from textwrap import dedent +import re +from conftest import ( + SETUPVARS, + tick_box, + info_box, + cross_box, + mock_command, + mock_command_2, + run_script +) -SETUPVARS = { - 'PIHOLE_INTERFACE' : 'eth99', - 'IPV4_ADDRESS' : '1.1.1.1', - 'IPV6_ADDRESS' : 'FE80::240:D0FF:FE48:4672', - 'PIHOLE_DNS_1' : '4.2.2.1', - 'PIHOLE_DNS_2' : '4.2.2.2' -} -tick_box="[\x1b[1;32m\xe2\x9c\x93\x1b[0m]".decode("utf-8") -cross_box="[\x1b[1;31m\xe2\x9c\x97\x1b[0m]".decode("utf-8") -info_box="[i]".decode("utf-8") +def test_supported_operating_system(Pihole): + ''' + confirm installer exists on unsupported distribution + ''' + # break supported package managers to emulate an unsupported distribution + Pihole.run('rm -rf /usr/bin/apt-get') + Pihole.run('rm -rf /usr/bin/rpm') + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = cross_box + ' OS distribution not supported' + assert expected_stdout in distro_check.stdout + # assert distro_check.rc == 1 + def test_setupVars_are_sourced_to_global_scope(Pihole): - ''' currently update_dialogs sources setupVars with a dot, + ''' + currently update_dialogs sources setupVars with a dot, then various other functions use the variables. - This confirms the sourced variables are in scope between functions ''' + This confirms the sourced variables are in scope between functions + ''' setup_var_file = 'cat < /etc/pihole/setupVars.conf\n' - for k,v in SETUPVARS.iteritems(): + for k, v in SETUPVARS.iteritems(): setup_var_file += "{}={}\n".format(k, v) setup_var_file += "EOF\n" Pihole.run(setup_var_file) @@ -43,13 +59,17 @@ def test_setupVars_are_sourced_to_global_scope(Pihole): output = run_script(Pihole, script).stdout - for k,v in SETUPVARS.iteritems(): + for k, v in SETUPVARS.iteritems(): assert "{}={}".format(k, v) in output + def test_setupVars_saved_to_file(Pihole): - ''' confirm saved settings are written to a file for future updates to re-use ''' - set_setup_vars = '\n' # dedent works better with this and padding matching script below - for k,v in SETUPVARS.iteritems(): + ''' + confirm saved settings are written to a file for future updates to re-use + ''' + # dedent works better with this and padding matching script below + set_setup_vars = '\n' + for k, v in SETUPVARS.iteritems(): set_setup_vars += " {}={}\n".format(k, v) Pihole.run(set_setup_vars).stdout @@ -67,15 +87,18 @@ def test_setupVars_saved_to_file(Pihole): output = run_script(Pihole, script).stdout - for k,v in SETUPVARS.iteritems(): + for k, v in SETUPVARS.iteritems(): assert "{}={}".format(k, v) in output + def test_configureFirewall_firewalld_running_no_errors(Pihole): - ''' confirms firewalld rules are applied when firewallD is running ''' + ''' + confirms firewalld rules are applied when firewallD is running + ''' # firewallD returns 'running' as status - mock_command('firewall-cmd', {'*':('running', 0)}, Pihole) + mock_command('firewall-cmd', {'*': ('running', 0)}, Pihole) # Whiptail dialog returns Ok for user prompt - mock_command('whiptail', {'*':('', 0)}, Pihole) + mock_command('whiptail', {'*': ('', 0)}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall @@ -84,26 +107,37 @@ def test_configureFirewall_firewalld_running_no_errors(Pihole): assert expected_stdout in configureFirewall.stdout firewall_calls = Pihole.run('cat /var/log/firewall-cmd').stdout assert 'firewall-cmd --state' in firewall_calls - assert 'firewall-cmd --permanent --add-service=http --add-service=dns' in firewall_calls + assert ('firewall-cmd ' + '--permanent ' + '--add-service=http ' + '--add-service=dns') in firewall_calls assert 'firewall-cmd --reload' in firewall_calls + def test_configureFirewall_firewalld_disabled_no_errors(Pihole): - ''' confirms firewalld rules are not applied when firewallD is not running ''' + ''' + confirms firewalld rules are not applied when firewallD is not running + ''' # firewallD returns non-running status - mock_command('firewall-cmd', {'*':('not running', '1')}, Pihole) + mock_command('firewall-cmd', {'*': ('not running', '1')}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall ''') - expected_stdout = 'No active firewall detected.. skipping firewall configuration' + expected_stdout = ('No active firewall detected.. ' + 'skipping firewall configuration') assert expected_stdout in configureFirewall.stdout + def test_configureFirewall_firewalld_enabled_declined_no_errors(Pihole): - ''' confirms firewalld rules are not applied when firewallD is running, user declines ruleset ''' + ''' + confirms firewalld rules are not applied when firewallD is running, user + declines ruleset + ''' # firewallD returns running status - mock_command('firewall-cmd', {'*':('running', 0)}, Pihole) + mock_command('firewall-cmd', {'*': ('running', 0)}, Pihole) # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*':('', 1)}, Pihole) + mock_command('whiptail', {'*': ('', 1)}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall @@ -111,6 +145,7 @@ def test_configureFirewall_firewalld_enabled_declined_no_errors(Pihole): expected_stdout = 'Not installing firewall rulesets.' assert expected_stdout in configureFirewall.stdout + def test_configureFirewall_no_firewall(Pihole): ''' confirms firewall skipped no daemon is running ''' configureFirewall = Pihole.run(''' @@ -120,14 +155,18 @@ def test_configureFirewall_no_firewall(Pihole): expected_stdout = 'No active firewall detected' assert expected_stdout in configureFirewall.stdout + def test_configureFirewall_IPTables_enabled_declined_no_errors(Pihole): - ''' confirms IPTables rules are not applied when IPTables is running, user declines ruleset ''' + ''' + confirms IPTables rules are not applied when IPTables is running, user + declines ruleset + ''' # iptables command exists - mock_command('iptables', {'*':('', '0')}, Pihole) + mock_command('iptables', {'*': ('', '0')}, Pihole) # modinfo returns always true (ip_tables module check) - mock_command('modinfo', {'*':('', '0')}, Pihole) + mock_command('modinfo', {'*': ('', '0')}, Pihole) # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*':('', '1')}, Pihole) + mock_command('whiptail', {'*': ('', '1')}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall @@ -135,14 +174,19 @@ def test_configureFirewall_IPTables_enabled_declined_no_errors(Pihole): expected_stdout = 'Not installing firewall rulesets.' assert expected_stdout in configureFirewall.stdout + def test_configureFirewall_IPTables_enabled_rules_exist_no_errors(Pihole): - ''' confirms IPTables rules are not applied when IPTables is running and rules exist ''' - # iptables command exists and returns 0 on calls (should return 0 on iptables -C) - mock_command('iptables', {'-S':('-P INPUT DENY', '0')}, Pihole) + ''' + confirms IPTables rules are not applied when IPTables is running and rules + exist + ''' + # iptables command exists and returns 0 on calls + # (should return 0 on iptables -C) + mock_command('iptables', {'-S': ('-P INPUT DENY', '0')}, Pihole) # modinfo returns always true (ip_tables module check) - mock_command('modinfo', {'*':('', '0')}, Pihole) + mock_command('modinfo', {'*': ('', '0')}, Pihole) # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*':('', '0')}, Pihole) + mock_command('whiptail', {'*': ('', '0')}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall @@ -150,18 +194,46 @@ def test_configureFirewall_IPTables_enabled_rules_exist_no_errors(Pihole): expected_stdout = 'Installing new IPTables firewall rulesets' assert expected_stdout in configureFirewall.stdout firewall_calls = Pihole.run('cat /var/log/iptables').stdout - assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 80 -j ACCEPT' not in firewall_calls - assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT' not in firewall_calls - assert 'iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT' not in firewall_calls + # General call type occurances + assert len(re.findall(r'iptables -S', firewall_calls)) == 1 + assert len(re.findall(r'iptables -C', firewall_calls)) == 4 + assert len(re.findall(r'iptables -I', firewall_calls)) == 0 + + # Specific port call occurances + assert len(re.findall(r'tcp --dport 80', firewall_calls)) == 1 + assert len(re.findall(r'tcp --dport 53', firewall_calls)) == 1 + assert len(re.findall(r'udp --dport 53', firewall_calls)) == 1 + assert len(re.findall(r'tcp --dport 4711:4720', firewall_calls)) == 1 + def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole): - ''' confirms IPTables rules are applied when IPTables is running and rules do not exist ''' + ''' + confirms IPTables rules are applied when IPTables is running and rules do + not exist + ''' # iptables command and returns 0 on calls (should return 1 on iptables -C) - mock_command('iptables', {'-S':('-P INPUT DENY', '0'), '-C':('', 1), '-I':('', 0)}, Pihole) + mock_command( + 'iptables', + { + '-S': ( + '-P INPUT DENY', + '0' + ), + '-C': ( + '', + 1 + ), + '-I': ( + '', + 0 + ) + }, + Pihole + ) # modinfo returns always true (ip_tables module check) - mock_command('modinfo', {'*':('', '0')}, Pihole) + mock_command('modinfo', {'*': ('', '0')}, Pihole) # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*':('', '0')}, Pihole) + mock_command('whiptail', {'*': ('', '0')}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall @@ -169,52 +241,160 @@ def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole): expected_stdout = 'Installing new IPTables firewall rulesets' assert expected_stdout in configureFirewall.stdout firewall_calls = Pihole.run('cat /var/log/iptables').stdout - assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 80 -j ACCEPT' in firewall_calls - assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT' in firewall_calls - assert 'iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT' in firewall_calls + # General call type occurances + assert len(re.findall(r'iptables -S', firewall_calls)) == 1 + assert len(re.findall(r'iptables -C', firewall_calls)) == 4 + assert len(re.findall(r'iptables -I', firewall_calls)) == 4 + + # Specific port call occurances + assert len(re.findall(r'tcp --dport 80', firewall_calls)) == 2 + assert len(re.findall(r'tcp --dport 53', firewall_calls)) == 2 + assert len(re.findall(r'udp --dport 53', firewall_calls)) == 2 + assert len(re.findall(r'tcp --dport 4711:4720', firewall_calls)) == 2 + + +def test_selinux_enforcing_default_exit(Pihole): + ''' + confirms installer prompts to exit when SELinux is Enforcing by default + ''' + # getenforce returns the running state of SELinux + mock_command('getenforce', {'*': ('Enforcing', '0')}, Pihole) + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*': ('', '1')}, Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = info_box + ' SELinux mode detected: Enforcing' + assert expected_stdout in check_selinux.stdout + expected_stdout = 'SELinux Enforcing detected, exiting installer' + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 1 + + +def test_selinux_enforcing_continue(Pihole): + ''' + confirms installer prompts to continue with custom policy warning + ''' + # getenforce returns the running state of SELinux + mock_command('getenforce', {'*': ('Enforcing', '0')}, Pihole) + # Whiptail dialog returns Continue for user prompt + mock_command('whiptail', {'*': ('', '0')}, Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = info_box + ' SELinux mode detected: Enforcing' + assert expected_stdout in check_selinux.stdout + expected_stdout = info_box + (' Continuing installation with SELinux ' + 'Enforcing') + assert expected_stdout in check_selinux.stdout + expected_stdout = info_box + (' Please refer to official SELinux ' + 'documentation to create a custom policy') + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 0 + + +def test_selinux_permissive(Pihole): + ''' + confirms installer continues when SELinux is Permissive + ''' + # getenforce returns the running state of SELinux + mock_command('getenforce', {'*': ('Permissive', '0')}, Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = info_box + ' SELinux mode detected: Permissive' + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 0 + + +def test_selinux_disabled(Pihole): + ''' + confirms installer continues when SELinux is Disabled + ''' + mock_command('getenforce', {'*': ('Disabled', '0')}, Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = info_box + ' SELinux mode detected: Disabled' + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 0 + def test_installPiholeWeb_fresh_install_no_errors(Pihole): - ''' confirms all web page assets from Core repo are installed on a fresh build ''' + ''' + confirms all web page assets from Core repo are installed on a fresh build + ''' installWeb = Pihole.run(''' source /opt/pihole/basic-install.sh installPiholeWeb ''') - assert info_box + ' Installing blocking page...' in installWeb.stdout - assert tick_box + ' Creating directory for blocking page, and copying files' in installWeb.stdout - assert cross_box + ' Backing up index.lighttpd.html' in installWeb.stdout - assert 'No default index.lighttpd.html file found... not backing up' in installWeb.stdout - assert tick_box + ' Installing sudoer file' in installWeb.stdout + expected_stdout = info_box + ' Installing blocking page...' + assert expected_stdout in installWeb.stdout + expected_stdout = tick_box + (' Creating directory for blocking page, ' + 'and copying files') + assert expected_stdout in installWeb.stdout + expected_stdout = cross_box + ' Backing up index.lighttpd.html' + assert expected_stdout in installWeb.stdout + expected_stdout = ('No default index.lighttpd.html file found... ' + 'not backing up') + assert expected_stdout in installWeb.stdout + expected_stdout = tick_box + ' Installing sudoer file' + assert expected_stdout in installWeb.stdout web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout assert 'index.php' in web_directory assert 'blockingpage.css' in web_directory + def test_update_package_cache_success_no_errors(Pihole): - ''' confirms package cache was updated without any errors''' + ''' + confirms package cache was updated without any errors + ''' updateCache = Pihole.run(''' source /opt/pihole/basic-install.sh distro_check update_package_cache ''') - assert tick_box + ' Update local cache of available packages' in updateCache.stdout - assert 'Error: Unable to update package cache.' not in updateCache.stdout + expected_stdout = tick_box + ' Update local cache of available packages' + assert expected_stdout in updateCache.stdout + assert 'error' not in updateCache.stdout.lower() + def test_update_package_cache_failure_no_errors(Pihole): - ''' confirms package cache was not updated''' - mock_command('apt-get', {'update':('', '1')}, Pihole) + ''' + confirms package cache was not updated + ''' + mock_command('apt-get', {'update': ('', '1')}, Pihole) updateCache = Pihole.run(''' source /opt/pihole/basic-install.sh distro_check update_package_cache ''') - assert cross_box + ' Update local cache of available packages' in updateCache.stdout + expected_stdout = cross_box + ' Update local cache of available packages' + assert expected_stdout in updateCache.stdout assert 'Error: Unable to update package cache.' in updateCache.stdout + def test_FTL_detect_aarch64_no_errors(Pihole): - ''' confirms only aarch64 package is downloaded for FTL engine ''' + ''' + confirms only aarch64 package is downloaded for FTL engine + ''' # mock uname to return aarch64 platform - mock_command('uname', {'-m':('aarch64', '0')}, Pihole) + mock_command('uname', {'-m': ('aarch64', '0')}, Pihole) # mock ldd to respond with aarch64 shared library - mock_command('ldd', {'/bin/ls':('/lib/ld-linux-aarch64.so.1', '0')}, Pihole) + mock_command( + 'ldd', + { + '/bin/ls': ( + '/lib/ld-linux-aarch64.so.1', + '0' + ) + }, + Pihole + ) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh FTLdetect @@ -226,29 +406,36 @@ def test_FTL_detect_aarch64_no_errors(Pihole): expected_stdout = tick_box + ' Downloading and Installing FTL' assert expected_stdout in detectPlatform.stdout + def test_FTL_detect_armv6l_no_errors(Pihole): - ''' confirms only armv6l package is downloaded for FTL engine ''' + ''' + confirms only armv6l package is downloaded for FTL engine + ''' # mock uname to return armv6l platform - mock_command('uname', {'-m':('armv6l', '0')}, Pihole) + mock_command('uname', {'-m': ('armv6l', '0')}, Pihole) # mock ldd to respond with aarch64 shared library - mock_command('ldd', {'/bin/ls':('/lib/ld-linux-armhf.so.3', '0')}, Pihole) + mock_command('ldd', {'/bin/ls': ('/lib/ld-linux-armhf.so.3', '0')}, Pihole) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh FTLdetect ''') expected_stdout = info_box + ' FTL Checks...' assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + ' Detected ARM-hf architecture (armv6 or lower)' + expected_stdout = tick_box + (' Detected ARM-hf architecture ' + '(armv6 or lower)') assert expected_stdout in detectPlatform.stdout expected_stdout = tick_box + ' Downloading and Installing FTL' assert expected_stdout in detectPlatform.stdout + def test_FTL_detect_armv7l_no_errors(Pihole): - ''' confirms only armv7l package is downloaded for FTL engine ''' + ''' + confirms only armv7l package is downloaded for FTL engine + ''' # mock uname to return armv7l platform - mock_command('uname', {'-m':('armv7l', '0')}, Pihole) + mock_command('uname', {'-m': ('armv7l', '0')}, Pihole) # mock ldd to respond with aarch64 shared library - mock_command('ldd', {'/bin/ls':('/lib/ld-linux-armhf.so.3', '0')}, Pihole) + mock_command('ldd', {'/bin/ls': ('/lib/ld-linux-armhf.so.3', '0')}, Pihole) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh FTLdetect @@ -260,8 +447,11 @@ def test_FTL_detect_armv7l_no_errors(Pihole): expected_stdout = tick_box + ' Downloading and Installing FTL' assert expected_stdout in detectPlatform.stdout + def test_FTL_detect_x86_64_no_errors(Pihole): - ''' confirms only x86_64 package is downloaded for FTL engine ''' + ''' + confirms only x86_64 package is downloaded for FTL engine + ''' detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh FTLdetect @@ -273,10 +463,11 @@ def test_FTL_detect_x86_64_no_errors(Pihole): expected_stdout = tick_box + ' Downloading and Installing FTL' assert expected_stdout in detectPlatform.stdout + def test_FTL_detect_unknown_no_errors(Pihole): ''' confirms only generic package is downloaded for FTL engine ''' # mock uname to return generic platform - mock_command('uname', {'-m':('mips', '0')}, Pihole) + mock_command('uname', {'-m': ('mips', '0')}, Pihole) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh FTLdetect @@ -284,8 +475,11 @@ def test_FTL_detect_unknown_no_errors(Pihole): expected_stdout = 'Not able to detect architecture (unknown: mips)' assert expected_stdout in detectPlatform.stdout + def test_FTL_download_aarch64_no_errors(Pihole): - ''' confirms only aarch64 package is downloaded for FTL engine ''' + ''' + confirms only aarch64 package is downloaded for FTL engine + ''' # mock uname to return generic platform download_binary = Pihole.run(''' source /opt/pihole/basic-install.sh @@ -293,13 +487,13 @@ def test_FTL_download_aarch64_no_errors(Pihole): ''') expected_stdout = tick_box + ' Downloading and Installing FTL' assert expected_stdout in download_binary.stdout - error = 'Error: Download of binary from Github failed' - assert error not in download_binary.stdout - error = 'Error: URL not found' - assert error not in download_binary.stdout + assert 'error' not in download_binary.stdout.lower() + def test_FTL_download_unknown_fails_no_errors(Pihole): - ''' confirms unknown binary is not downloaded for FTL engine ''' + ''' + confirms unknown binary is not downloaded for FTL engine + ''' # mock uname to return generic platform download_binary = Pihole.run(''' source /opt/pihole/basic-install.sh @@ -310,8 +504,11 @@ def test_FTL_download_unknown_fails_no_errors(Pihole): error = 'Error: URL not found' assert error in download_binary.stdout + def test_FTL_binary_installed_and_responsive_no_errors(Pihole): - ''' confirms FTL binary is copied and functional in installed location ''' + ''' + confirms FTL binary is copied and functional in installed location + ''' installed_binary = Pihole.run(''' source /opt/pihole/basic-install.sh FTLdetect @@ -320,8 +517,11 @@ def test_FTL_binary_installed_and_responsive_no_errors(Pihole): expected_stdout = 'v' assert expected_stdout in installed_binary.stdout + # def test_FTL_support_files_installed(Pihole): -# ''' confirms FTL support files are installed ''' +# ''' +# confirms FTL support files are installed +# ''' # support_files = Pihole.run(''' # source /opt/pihole/basic-install.sh # FTLdetect @@ -334,21 +534,46 @@ def test_FTL_binary_installed_and_responsive_no_errors(Pihole): # assert '644 /run/pihole-FTL.pid' in support_files.stdout # assert '644 /var/log/pihole-FTL.log' in support_files.stdout + def test_IPv6_only_link_local(Pihole): - ''' confirms IPv6 blocking is disabled for Link-local address ''' + ''' + confirms IPv6 blocking is disabled for Link-local address + ''' # mock ip -6 address to return Link-local address - mock_command_2('ip', {'-6 address':('inet6 fe80::d210:52fa:fe00:7ad7/64 scope link', '0')}, Pihole) + mock_command_2( + 'ip', + { + '-6 address': ( + 'inet6 fe80::d210:52fa:fe00:7ad7/64 scope link', + '0' + ) + }, + Pihole + ) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh useIPv6dialog ''') - expected_stdout = 'Unable to find IPv6 ULA/GUA address, IPv6 adblocking will not be enabled' + expected_stdout = ('Unable to find IPv6 ULA/GUA address, ' + 'IPv6 adblocking will not be enabled') assert expected_stdout in detectPlatform.stdout + def test_IPv6_only_ULA(Pihole): - ''' confirms IPv6 blocking is enabled for ULA addresses ''' + ''' + confirms IPv6 blocking is enabled for ULA addresses + ''' # mock ip -6 address to return ULA address - mock_command_2('ip', {'-6 address':('inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) + mock_command_2( + 'ip', + { + '-6 address': ( + 'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', + '0' + ) + }, + Pihole + ) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh useIPv6dialog @@ -356,10 +581,22 @@ def test_IPv6_only_ULA(Pihole): expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' assert expected_stdout in detectPlatform.stdout + def test_IPv6_only_GUA(Pihole): - ''' confirms IPv6 blocking is enabled for GUA addresses ''' + ''' + confirms IPv6 blocking is enabled for GUA addresses + ''' # mock ip -6 address to return GUA address - mock_command_2('ip', {'-6 address':('inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) + mock_command_2( + 'ip', + { + '-6 address': ( + 'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', + '0' + ) + }, + Pihole + ) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh useIPv6dialog @@ -367,10 +604,23 @@ def test_IPv6_only_GUA(Pihole): expected_stdout = 'Found IPv6 GUA address, using it for blocking IPv6 ads' assert expected_stdout in detectPlatform.stdout + def test_IPv6_GUA_ULA_test(Pihole): - ''' confirms IPv6 blocking is enabled for GUA and ULA addresses ''' + ''' + confirms IPv6 blocking is enabled for GUA and ULA addresses + ''' # mock ip -6 address to return GUA and ULA addresses - mock_command_2('ip', {'-6 address':('inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\ninet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) + mock_command_2( + 'ip', + { + '-6 address': ( + 'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\n' + 'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', + '0' + ) + }, + Pihole + ) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh useIPv6dialog @@ -378,61 +628,26 @@ def test_IPv6_GUA_ULA_test(Pihole): expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' assert expected_stdout in detectPlatform.stdout + def test_IPv6_ULA_GUA_test(Pihole): - ''' confirms IPv6 blocking is enabled for GUA and ULA addresses ''' + ''' + confirms IPv6 blocking is enabled for GUA and ULA addresses + ''' # mock ip -6 address to return ULA and GUA addresses - mock_command_2('ip', {'-6 address':('inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\ninet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) + mock_command_2( + 'ip', + { + '-6 address': ( + 'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\n' + 'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', + '0' + ) + }, + Pihole + ) detectPlatform = Pihole.run(''' source /opt/pihole/basic-install.sh useIPv6dialog ''') expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' assert expected_stdout in detectPlatform.stdout - -# Helper functions -def mock_command(script, args, container): - ''' Allows for setup of commands we don't really want to have to run for real in unit tests ''' - full_script_path = '/usr/local/bin/{}'.format(script) - mock_script = dedent('''\ - #!/bin/bash -e - echo "\$0 \$@" >> /var/log/{script} - case "\$1" in'''.format(script=script)) - for k, v in args.iteritems(): - case = dedent(''' - {arg}) - echo {res} - exit {retcode} - ;;'''.format(arg=k, res=v[0], retcode=v[1])) - mock_script += case - mock_script += dedent(''' - esac''') - container.run(''' - cat < {script}\n{content}\nEOF - chmod +x {script} - rm -f /var/log/{scriptlog}'''.format(script=full_script_path, content=mock_script, scriptlog=script)) - -def mock_command_2(script, args, container): - ''' Allows for setup of commands we don't really want to have to run for real in unit tests ''' - full_script_path = '/usr/local/bin/{}'.format(script) - mock_script = dedent('''\ - #!/bin/bash -e - echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in'''.format(script=script)) - for k, v in args.iteritems(): - case = dedent(''' - \"{arg}\") - echo \"{res}\" - exit {retcode} - ;;'''.format(arg=k, res=v[0], retcode=v[1])) - mock_script += case - mock_script += dedent(''' - esac''') - container.run(''' - cat < {script}\n{content}\nEOF - chmod +x {script} - rm -f /var/log/{scriptlog}'''.format(script=full_script_path, content=mock_script, scriptlog=script)) - -def run_script(Pihole, script): - result = Pihole.run(script) - assert result.rc == 0 - return result diff --git a/test/test_centos_fedora_support.py b/test/test_centos_fedora_support.py new file mode 100644 index 00000000..8318e44a --- /dev/null +++ b/test/test_centos_fedora_support.py @@ -0,0 +1,209 @@ +import pytest +from conftest import ( + tick_box, + info_box, + cross_box, + mock_command, + mock_command_2, +) + + +@pytest.mark.parametrize("tag", [('fedora'), ]) +def test_epel_and_remi_not_installed_fedora(Pihole): + ''' + confirms installer does not attempt to install EPEL/REMI repositories + on Fedora + ''' + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + assert distro_check.stdout == '' + + epel_package = Pihole.package('epel-release') + assert not epel_package.is_installed + remi_package = Pihole.package('remi-release') + assert not remi_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_release_supported_version_check_centos(Pihole): + ''' + confirms installer exits on unsupported releases of CentOS + ''' + # mock CentOS release < 7 (unsupported) + mock_command_2( + 'rpm', + {"-q --queryformat '%{VERSION}' centos-release'": ( + '5', + '0' + )}, + Pihole + ) + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = cross_box + (' CentOS is not suported.') + assert expected_stdout in distro_check.stdout + expected_stdout = 'Please update to CentOS release 7 or later' + assert expected_stdout in distro_check.stdout + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_enable_epel_repository_centos(Pihole): + ''' + confirms the EPEL package repository is enabled when installed on CentOS + ''' + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = info_box + (' Enabling EPEL package repository ' + '(https://fedoraproject.org/wiki/EPEL)') + assert expected_stdout in distro_check.stdout + expected_stdout = tick_box + ' Installed epel-release' + assert expected_stdout in distro_check.stdout + epel_package = Pihole.package('epel-release') + assert epel_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_php_upgrade_default_optout_centos(Pihole): + ''' + confirms the default behavior to opt-out of installing PHP7 from REMI + ''' + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' + 'Deprecated PHP may be in use.') + assert expected_stdout in distro_check.stdout + remi_package = Pihole.package('remi-release') + assert not remi_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_php_upgrade_user_optout_centos(Pihole): + ''' + confirms installer behavior when user opt-out of installing PHP7 from REMI + (php not currently installed) + ''' + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*': ('', '1')}, Pihole) + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' + 'Deprecated PHP may be in use.') + assert expected_stdout in distro_check.stdout + remi_package = Pihole.package('remi-release') + assert not remi_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_php_upgrade_user_optin_centos(Pihole): + ''' + confirms installer behavior when user opt-in to installing PHP7 from REMI + (php not currently installed) + ''' + # Whiptail dialog returns Continue for user prompt + mock_command('whiptail', {'*': ('', '0')}, Pihole) + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + assert 'opt-out' not in distro_check.stdout + expected_stdout = info_box + (' Enabling Remi\'s RPM repository ' + '(https://rpms.remirepo.net)') + assert expected_stdout in distro_check.stdout + expected_stdout = tick_box + (' Remi\'s RPM repository has ' + 'been enabled for PHP7') + assert expected_stdout in distro_check.stdout + remi_package = Pihole.package('remi-release') + assert remi_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_php_version_lt_7_detected_upgrade_default_optout_centos(Pihole): + ''' + confirms the default behavior to opt-out of upgrading to PHP7 from REMI + ''' + # first we will install the default php version to test installer behavior + php_install = Pihole.run('yum install -y php') + assert php_install.rc == 0 + php_package = Pihole.package('php') + default_centos_php_version = php_package.version.split('.')[0] + if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended + pytest.skip("Test deprecated . Detected default PHP version >= 7") + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' + 'Deprecated PHP may be in use.') + assert expected_stdout in distro_check.stdout + remi_package = Pihole.package('remi-release') + assert not remi_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_php_version_lt_7_detected_upgrade_user_optout_centos(Pihole): + ''' + confirms installer behavior when user opt-out to upgrade to PHP7 via REMI + ''' + # first we will install the default php version to test installer behavior + php_install = Pihole.run('yum install -y php') + assert php_install.rc == 0 + php_package = Pihole.package('php') + default_centos_php_version = php_package.version.split('.')[0] + if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended + pytest.skip("Test deprecated . Detected default PHP version >= 7") + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*': ('', '1')}, Pihole) + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + ''') + expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' + 'Deprecated PHP may be in use.') + assert expected_stdout in distro_check.stdout + remi_package = Pihole.package('remi-release') + assert not remi_package.is_installed + + +@pytest.mark.parametrize("tag", [('centos'), ]) +def test_php_version_lt_7_detected_upgrade_user_optin_centos(Pihole): + ''' + confirms installer behavior when user opt-in to upgrade to PHP7 via REMI + ''' + # first we will install the default php version to test installer behavior + php_install = Pihole.run('yum install -y php') + assert php_install.rc == 0 + php_package = Pihole.package('php') + default_centos_php_version = php_package.version.split('.')[0] + if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended + pytest.skip("Test deprecated . Detected default PHP version >= 7") + # Whiptail dialog returns Continue for user prompt + mock_command('whiptail', {'*': ('', '0')}, Pihole) + distro_check = Pihole.run(''' + source /opt/pihole/basic-install.sh + distro_check + install_dependent_packages PIHOLE_WEB_DEPS[@] + ''') + expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. ' + 'Deprecated PHP may be in use.') + assert expected_stdout not in distro_check.stdout + expected_stdout = info_box + (' Enabling Remi\'s RPM repository ' + '(https://rpms.remirepo.net)') + assert expected_stdout in distro_check.stdout + expected_stdout = tick_box + (' Remi\'s RPM repository has ' + 'been enabled for PHP7') + assert expected_stdout in distro_check.stdout + remi_package = Pihole.package('remi-release') + assert remi_package.is_installed + updated_php_package = Pihole.package('php') + updated_php_version = updated_php_package.version.split('.')[0] + assert int(updated_php_version) == 7 diff --git a/test/test_shellcheck.py b/test/test_shellcheck.py index 5b1a8961..43e8ad6f 100644 --- a/test/test_shellcheck.py +++ b/test/test_shellcheck.py @@ -1,13 +1,18 @@ -import pytest import testinfra run_local = testinfra.get_backend( "local://" ).get_module("Command").run + def test_scripts_pass_shellcheck(): - ''' Make sure shellcheck does not find anything wrong with our shell scripts ''' - shellcheck = "find . -type f -name 'update.sh' | while read file; do shellcheck -x \"$file\" -e SC1090,SC1091; done;" + ''' + Make sure shellcheck does not find anything wrong with our shell scripts + ''' + shellcheck = ("find . -type f -name 'update.sh' " + "| while read file; do " + "shellcheck -x \"$file\" -e SC1090,SC1091; " + "done;") results = run_local(shellcheck) print results.stdout assert '' == results.stdout diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..e7916e04 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py27 + +[testenv] +whitelist_externals = docker +deps = -rrequirements.txt +commands = docker build -f test/debian.Dockerfile -t pytest_pihole:debian . + docker build -f test/centos.Dockerfile -t pytest_pihole:centos . + docker build -f test/fedora.Dockerfile -t pytest_pihole:fedora . + pytest {posargs:-vv -n auto} -m "not build_stage" ./test/