diff --git a/docker-compose-armhf.yml b/docker-compose-armhf.yml index 8e4fb019..ca8aa6b4 100644 --- a/docker-compose-armhf.yml +++ b/docker-compose-armhf.yml @@ -9,6 +9,7 @@ services: - "4443:4443" volumes: - ncdata:/data + container_name: nextcloudpi volumes: ncdata: diff --git a/docker-compose-nc-armhf.yml b/docker-compose-nc-armhf.yml index d70713d0..8dd06e97 100644 --- a/docker-compose-nc-armhf.yml +++ b/docker-compose-nc-armhf.yml @@ -8,6 +8,7 @@ services: - "443:443" volumes: - ncdata:/data + container_name: nextcloudpi volumes: ncdata: diff --git a/docker-compose-nc.yml b/docker-compose-nc.yml index 3fb7d33d..b8c76d19 100644 --- a/docker-compose-nc.yml +++ b/docker-compose-nc.yml @@ -8,6 +8,7 @@ services: - "443:443" volumes: - ncdata:/data + container_name: nextcloudpi volumes: ncdata: diff --git a/docker-compose-ncpdev.yml b/docker-compose-ncpdev.yml index 58dc00de..e4e5d7db 100644 --- a/docker-compose-ncpdev.yml +++ b/docker-compose-ncpdev.yml @@ -12,6 +12,7 @@ services: - ./etc:/usr/local/etc - ./changelog.md:/changelog.md - ncdata:/data + container_name: nextcloudpi volumes: ncdata: diff --git a/docker-compose.yml b/docker-compose.yml index 8ec6d990..f38cfab3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - "4443:4443" volumes: - ncdata:/data + container_name: nextcloudpi volumes: ncdata: diff --git a/etc/ncp-config.d/nc-passwd.sh b/etc/ncp-config.d/nc-passwd.sh index 89fdaba2..1cb54775 100644 --- a/etc/ncp-config.d/nc-passwd.sh +++ b/etc/ncp-config.d/nc-passwd.sh @@ -26,6 +26,9 @@ configure() ln -s /data/etc/shadow /etc/shadow } + # Run cron.php once now to get all checks right in CI. + sudo -u www-data php /var/www/nextcloud/cron.php + # activate NCP a2ensite ncp nextcloud a2dissite ncp-activation diff --git a/tests/activation_tests.py b/tests/activation_tests.py new file mode 100755 index 00000000..94f54788 --- /dev/null +++ b/tests/activation_tests.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +""" +Automatic testing for NextCloudPi + +Copyleft 2017 by Ignacio Nunez Hernanz +GPL licensed (see LICENSE file in repository root). +Use at your own risk! + + ./activation_tests.py [IP] + +More at https://ownyourbits.com +""" + +import sys +import time +import urllib +import os +import getopt +import configparser +import signal +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import UnexpectedAlertPresentException +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import TimeoutException + +suite_name = "activation tests" +test_cfg = 'test_cfg.txt' +test_log = 'test_log.txt' + +class tc: + "terminal colors" + brown='\033[33m' + yellow='\033[33;1m' + green='\033[32m' + red='\033[31m' + normal='\033[0m' + +def usage(): + "Print usage" + print("usage: activation_tests.py [ip]") + +class Test: + title = "test" + result = True + + def new(self, title): + self.title = title + print("[check] " + "{:16}".format(title), end=' ', flush = True) + + def check(self, expression): + if expression: + print(tc.green + "ok" + tc.normal) + self.log("ok") + else: + print(tc.red + "error" + tc.normal) + self.log("error") + sys.exit(1) + + def report(self, title, expression): + self.new(title) + self.check(expression) + + def log(self, result): + config = configparser.ConfigParser() + if os.path.exists(test_log): + config.read(test_log) + if not config.has_section(suite_name): + config[suite_name] = {} + config[suite_name][self.title] = result + with open(test_log, 'w') as logfile: + config.write(logfile) + +def is_element_present(driver, how, what): + try: driver.find_element(by=how, value=what) + except NoSuchElementException: return False + return True + +def signal_handler(sig, frame): + sys.exit(0) + +def test_activation(IP): + """ Activation process checks""" + + # activation page + test = Test() + driver = webdriver.Firefox(service_log_path='/dev/null') + driver.implicitly_wait(5) + test.new("activation opens") + driver.get("https://" + IP) + test.check("NextCloudPi Activation" in driver.title) + try: + ncp_pass = driver.find_element_by_id("ncp-pwd").get_attribute("value") + nc_pass = driver.find_element_by_id("nc-pwd").get_attribute("value") + + config = configparser.ConfigParser() + if not config.has_section('credentials'): + config['credentials'] = {} + config['credentials']['ncp_user' ] = 'ncp' + config['credentials']['ncp_pass' ] = ncp_pass + config['credentials']['nc_user' ] = 'ncp' + config['credentials']['nc_pass' ] = nc_pass + with open(test_cfg, 'w') as configfile: + config.write(configfile) + + driver.find_element_by_id("activate-ncp").click() + test.report("activation click", True) + except: + ncp_pass = "" + test.report("activation click", False) + + test.new("activation ends") + try: + wait = WebDriverWait(driver, 60) + wait.until(EC.text_to_be_present_in_element((By.ID,'error-box'), "ACTIVATION SUCCESSFUL")) + test.check(True) + except TimeoutException: + test.check(False) + except: + test.check(True) + try: driver.close() + except: pass + + # ncp-web + test.new("ncp-web") + driver = webdriver.Firefox(service_log_path='/dev/null') + try: + driver.get("https://ncp:" + urllib.parse.quote_plus(ncp_pass) + "@" + IP + ":4443") + except UnexpectedAlertPresentException: + pass + test.check("NextCloudPi Panel" in driver.title) + test.report("first run wizard", is_element_present(driver, By.ID, "first-run-wizard")) + + driver.close() + return test.result + +if __name__ == "__main__": + signal.signal(signal.SIGINT, signal_handler) + + # parse options + try: + opts, args = getopt.getopt(sys.argv[1:], 'h', ['help']) + except getopt.GetoptError: + usage() + sys.exit(2) + + for opt, arg in opts: + if opt in ('-h', '--help'): + usage() + sys.exit(2) + else: + usage() + sys.exit(2) + + # test + IP = args[0] if len(args) > 0 else 'localhost' + print("Activation tests " + tc.yellow + IP + tc.normal) + print("---------------------------") + test_activation(IP) + +# License +# +# This script is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this script; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA diff --git a/tests/nextcloud_tests.py b/tests/nextcloud_tests.py new file mode 100755 index 00000000..3dbacecc --- /dev/null +++ b/tests/nextcloud_tests.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +""" +Automatic testing for NextCloudPi + +Copyleft 2017 by Ignacio Nunez Hernanz +GPL licensed (see LICENSE file in repository root). +Use at your own risk! + + ./nextcloud_tests.py [IP] + +More at https://ownyourbits.com +""" + +import sys +import time +import urllib +import os +import getopt +import configparser +import signal +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +suite_name = "nextcloud tests" +test_cfg = 'test_cfg.txt' +test_log = 'test_log.txt' + +class tc: + "terminal colors" + brown='\033[33m' + yellow='\033[33;1m' + green='\033[32m' + red='\033[31m' + normal='\033[0m' + +class Test: + title = "test" + result = True + + def new(self, title): + self.title = title + print("[check] " + "{:16}".format(title), end=' ', flush = True) + + def check(self, expression): + if expression: + print(tc.green + "ok" + tc.normal) + self.log("ok") + else: + print(tc.red + "error" + tc.normal) + self.log("error") + sys.exit(1) + + def report(self, title, expression): + self.new(title) + self.check(expression) + + def log(self, result): + config = configparser.ConfigParser() + if os.path.exists(test_log): + config.read(test_log) + if not config.has_section(suite_name): + config[suite_name] = {} + config[suite_name][self.title] = result + with open(test_log, 'w') as logfile: + config.write(logfile) + +def usage(): + "Print usage" + print("usage: nextcloud_tests.py [--new] [ip]") + print("--new removes saved configuration") + +def signal_handler(sig, frame): + sys.exit(0) + +def test_nextcloud(IP): + """ Login and assert admin page checks""" + test = Test() + driver = webdriver.Firefox(service_log_path='/dev/null') + driver.implicitly_wait(60) + test.new("nextcloud page") + try: + driver.get("https://" + IP + "/index.php/settings/admin/overview") + except: + test.check(False) + print(tc.red + "error:" + tc.normal + " unable to reach " + tc.yellow + IP + tc.normal) + sys.exit(1) + test.check("NextCloudPi" in driver.title) + trusted_domain_str = "You are accessing the server from an untrusted domain" + test.report("trusted domain", trusted_domain_str not in driver.page_source) + try: + driver.find_element_by_id("user").send_keys(nc_user) + driver.find_element_by_id("password").send_keys(nc_pass) + driver.find_element_by_id("submit").click() + except: pass + test.report("password", "Wrong password" not in driver.page_source) + + test.report("settings page", "Settings" in driver.title) + test.new("settings config") + try: + wait = WebDriverWait(driver, 30) + wait.until(EC.visibility_of(driver.find_element_by_class_name("icon-checkmark-white"))) + test.check(True) + except: + test.check(False) + + driver.close() + return test.result + +if __name__ == "__main__": + signal.signal(signal.SIGINT, signal_handler) + + # parse options + try: + opts, args = getopt.getopt(sys.argv[1:], 'hn', ['help']) + except getopt.GetoptError: + usage() + sys.exit(2) + + for opt, arg in opts: + if opt in ('-h', '--help'): + usage() + sys.exit(2) + elif opt in ('-n', '--new'): + if os.path.exists(test_cfg): + os.unlink(test_cfg) + else: + usage() + sys.exit(2) + + nc_user = False + nc_pass = False + config = configparser.ConfigParser() + + if os.path.exists(test_cfg): + config.read(test_cfg) + try: + nc_user = config['credentials']['nc_user'] + nc_pass = config['credentials']['nc_pass'] + except: pass + + if not nc_user or not nc_pass: + nc_user = input("Nextcloud username (empty=ncp): ") + nc_user = "ncp" if nc_user == "" else nc_user + nc_pass = input("Nextcloud " + nc_user + " password (empty=ownyourbits): ") + nc_pass = "ownyourbits" if nc_pass == "" else nc_pass + print("") + + if not config.has_section('credentials'): + config['credentials'] = {} + config['credentials']['nc_user' ] = nc_user + config['credentials']['nc_pass' ] = nc_pass + with open(test_cfg, 'w') as configfile: + config.write(configfile) + + # test + IP = args[0] if len(args) > 0 else 'localhost' + print("Nextcloud tests " + tc.yellow + IP + tc.normal) + print("---------------------------") + test_nextcloud(IP) + +# License +# +# This script is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this script; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA diff --git a/tests/system_tests.py b/tests/system_tests.py new file mode 100755 index 00000000..72c39295 --- /dev/null +++ b/tests/system_tests.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +""" +Automatic system testing for NextCloudPi + +Copyleft 2018 by Ignacio Nunez Hernanz +GPL licensed (see LICENSE file in repository root). +Use at your own risk! + + ./system_tests.py [user@ip] + +More at https://ownyourbits.com +""" + +pre_cmd = [] + +import sys +import getopt +import os +import signal +from subprocess import run, PIPE + +processes_must_be_running = [ + 'apache2', + 'cron', + 'mysqld', + 'php-fpm', + 'postfix', + 'redis-server', + ] + +binaries_must_be_installed = [ + 'dialog', + 'dnsmasq', + 'git', + 'letsencrypt', + 'noip2', + 'rsync', + 'ssh', + ] + +binaries_no_docker = [ + 'btrfs', + 'fail2ban-server', + 'udiskie', + 'ufw', + 'samba', + 'wicd-curses', + ] + +class tc: + "terminal colors" + brown='\033[33m' + yellow='\033[33;1m' + green='\033[32m' + red='\033[31m' + normal='\033[0m' + +def usage(): + "Print usage" + print("usage: system_tests.py [user@ip]") + +def is_running(process): + "check that a process is running" + print("[running] " + tc.brown + "{:16}".format(process) + tc.normal, end=' ') + result = run(pre_cmd + ['pgrep', '-cf', process], stdout=PIPE, stderr=PIPE) + if result.returncode == 0: + print(tc.green + "ok" + tc.normal) + else: + print(tc.red + "error" + tc.normal) + return result.returncode == 0 + +def check_processes_running(processes): + "check that all processes are running" + ret = True + for process in processes: + if not is_running(process): + ret = False + return ret + +def is_installed(binary): + "check that a binary is installed" + print("[install] " + tc.brown + "{:16}".format(binary) + tc.normal, end=' ') + result = run(pre_cmd + ['sudo', 'which', binary], stdout=PIPE, stderr=PIPE) + if result.returncode == 0: + print(tc.green + "ok" + tc.normal) + else: + print(tc.red + "error" + tc.normal) + return result.returncode == 0 + +def check_binaries_installed(binaries): + "check that all the binaries are installed" + ret = True + for binary in binaries: + if not is_installed(binary): + ret = False + return ret + +def signal_handler(sig, frame): + sys.exit(0) + +if __name__ == "__main__": + + signal.signal(signal.SIGINT, signal_handler) + + # parse options + try: + opts, args = getopt.getopt(sys.argv[1:], 'h', ['help']) + except getopt.GetoptError: + usage() + sys.exit(2) + + for opt, arg in opts: + if opt in ('-h', '--help'): + usage() + sys.exit(2) + else: + usage() + sys.exit(2) + + # parse arguments + ssh_cmd = "ssh root@nextcloudpi.local" + if len(args) > 0: + if '@' in args[0]: + ssh_cmd = "ssh " + args[0] + else: + print(tc.brown + "* Ignoring invalid SSH argument " + tc.yellow + args[0] + tc.normal) + args = [] + + # detect if we are running this in a NCP instance + try: + dockers_running = run(['docker', 'ps', '--format', '{{.Image}}'], stdout=PIPE).stdout.decode('utf-8') + except: + dockers_running = '' + + # local method + if os.path.exists('/usr/local/etc/ncp-baseimage'): + print(tc.brown + "* local NCP instance detected" + tc.normal) + binaries_must_be_installed = binaries_must_be_installed + binaries_no_docker + pre_cmd = [] + + # docker method + elif 'ownyourbits/nextcloudpi-' in dockers_running: + print( tc.brown + "* local NCP docker instance detected" + tc.normal) + pre_cmd = ['docker', 'exec', '-ti', 'nextcloudpi'] + + # SSH method + else: + if len(args) == 0: + print( tc.brown + "* No local NCP instance detected, trying SSH with " + + tc.yellow + ssh_cmd + tc.normal + "...") + binaries_must_be_installed = binaries_must_be_installed + binaries_no_docker + pre_cmd = ['ssh', '-o UserKnownHostsFile=/dev/null' , '-o PasswordAuthentication=no', + '-o StrictHostKeyChecking=no', '-o ConnectTimeout=1', ssh_cmd[4:]] + + at_char = ssh_cmd.index('@') + ip = ssh_cmd[at_char+1:] + ping_cmd = run(['ping', '-c1', '-w1', ip], stdout=PIPE, stderr=PIPE) + if ping_cmd.returncode != 0: + print(tc.red + "No connectivity to " + tc.yellow + ip + tc.normal) + sys.exit(1) + + ssh_test = run(pre_cmd + [':'], stdout=PIPE, stderr=PIPE) + if ssh_test.returncode != 0: + ssh_copy = run(['ssh-copy-id', ssh_cmd[4:]], stderr=PIPE) + if ssh_copy.returncode != 0: + print(tc.red + "SSH connection failed" + tc.normal) + sys.exit(1) + + # checks + print("\nNextCloudPi system checks") + print("-------------------------") + running_result = check_processes_running(processes_must_be_running) + install_result = check_binaries_installed(binaries_must_be_installed) + + if running_result and install_result: + sys.exit(0) + else: + sys.exit(1) + +# License +# +# This script is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this script; if not, write to the diff --git a/tests/tests.py b/tests/tests.py deleted file mode 100755 index 6f521a7f..00000000 --- a/tests/tests.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 - -""" -Automatic testing for NextCloudPi - -Copyleft 2017 by Ignacio Nunez Hernanz -GPL licensed (see LICENSE file in repository root). -Use at your own risk! - - ./tests.py - -More at https://ownyourbits.com -""" - -import unittest -import sys -import time -from selenium import webdriver -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait - - -IP = sys.argv[1] - - -class AdminWebTest(unittest.TestCase): - """ - Log as admin and assert that all internal checks pass ("All checks passed!") - Also checks for correct trusted domain setting - """ - - def setUp(self): - self.driver = webdriver.Firefox() - - # @unittest.skip("Skipping...") - def test_admin_checks(self): - """ Login and assert admin page checks""" - driver = self.driver - driver.implicitly_wait(150) # first run can be really slow on QEMU - driver.get("https://" + IP + "/index.php/settings/admin") - self.assertIn("NextCloudPi", driver.title) - trusted_domain_str = "You are accessing the server from an untrusted domain" - self.assertNotIn(trusted_domain_str, driver.page_source) - driver.find_element_by_id("user").send_keys("ncp") - driver.find_element_by_id("password").send_keys("ownyourbits") - driver.find_element_by_id("submit").click() - self.assertNotIn("Wrong password", driver.page_source) - - wait = WebDriverWait(driver, 800) # first processing of this page is even slower in NC13 - wait.until(EC.visibility_of(driver.find_element_by_class_name("icon-checkmark"))) - - def tearDown(self): - self.driver.close() - - -class CreateUserTest(unittest.TestCase): - """ - Create a user, then navigate a little bit - """ - - def setUp(self): - self.driver = webdriver.Firefox() - - @unittest.skip("Skipping...") - def test_user_creation(self): - """ Create user test_user1 """ - driver = self.driver - driver.get("https://" + IP + "/index.php/settings/users") - - driver.find_element_by_id("user").send_keys("ncp") - driver.find_element_by_id("password").send_keys("ownyourbits") - driver.find_element_by_id("submit").click() - self.assertNotIn("Wrong password", driver.page_source) - - wait = WebDriverWait(driver, 150) - wait.until(lambda driver: driver.find_element_by_id("newusername")) - - driver.find_element_by_id("newusername").send_keys("test_user1") - driver.find_element_by_id("newuserpassword").send_keys("ownyourbits") - driver.find_element_by_id("newuserpassword").send_keys(Keys.RETURN) - - time.sleep(5) - - # navigate a little bit - driver.get("https://" + IP + "/index.php/settings/admin") - self.assertIn("NextCloudPi", driver.title) - driver.get("https://" + IP + "/index.php/settings/apps") - self.assertIn("NextCloudPi", driver.title) - - def tearDown(self): - self.driver.close() - - -class LoginNewUserTest(unittest.TestCase): - """ - Login as the newly created user and check that we are in the Files App - """ - - def setUp(self): - self.driver = webdriver.Firefox() - - @unittest.skip("Skipping...") - def test_user_login(self): - """ Login as test_user1 """ - driver = self.driver - driver.implicitly_wait(210) # first run can be really slow on QEMU - driver.get("https://" + IP) - - self.assertIn("NextCloudPi", driver.title) - driver.find_element_by_id("user").send_keys("test_user1") - driver.find_element_by_id("password").send_keys("ownyourbits") - driver.find_element_by_id("submit").click() - self.assertNotIn("Wrong password", driver.page_source) - - time.sleep(60) # first run can be really slow on QEMU - wait = WebDriverWait(driver, 210) - wait.until(lambda driver: driver.find_element_by_id("fileList")) - - # navigate a little bit - driver.get("https://" + IP + "/index.php/settings/personal") - self.assertIn("NextCloudPi", driver.title) - - def tearDown(self): - self.driver.close() - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("IP argument required") - sys.exit() - - unittest.main(argv=['first-arg-is-ignored'], verbosity=2) - -# License -# -# This script is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This script is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this script; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place, Suite 330, -# Boston, MA 02111-1307 USA