From 83cb7cbcbeb71770a2a4979e72e64a7be0455255 Mon Sep 17 00:00:00 2001 From: downtownallday Date: Tue, 9 Jun 2020 20:46:59 -0400 Subject: [PATCH] Automated QA tests for remote nextcloud --- .../assets/mail/roundcube/carddav_refresh.sh | 136 ++++++++ .../system-setup/remote-nextcloud-docker.sh | 200 +++++++++++ tests/assets/system-setup/setup-defaults.sh | 33 ++ tests/assets/system-setup/setup-funcs.sh | 97 ++++++ tests/runner.sh | 53 ++- tests/suites/_init.sh | 22 +- tests/suites/_ldap-functions.sh | 10 +- tests/suites/_locations.sh | 8 + tests/suites/remote-nextcloud.sh | 313 ++++++++++++++++++ 9 files changed, 852 insertions(+), 20 deletions(-) create mode 100755 tests/assets/mail/roundcube/carddav_refresh.sh create mode 100755 tests/assets/system-setup/remote-nextcloud-docker.sh create mode 100755 tests/assets/system-setup/setup-defaults.sh create mode 100755 tests/assets/system-setup/setup-funcs.sh create mode 100644 tests/suites/_locations.sh create mode 100644 tests/suites/remote-nextcloud.sh diff --git a/tests/assets/mail/roundcube/carddav_refresh.sh b/tests/assets/mail/roundcube/carddav_refresh.sh new file mode 100755 index 00000000..e399c9bc --- /dev/null +++ b/tests/assets/mail/roundcube/carddav_refresh.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env php +] username password\n"; + print "Force a sync of a user's addressbook with the remote server\n"; + print "Place this script in /path/to/roundcubemail/bin, then change the working directory to /path/to/roundcubemail, then run ./bin/cardav_refresh.sh"; + exit(1); +} + +function _die($msg) +{ + fwrite(STDERR, $msg . "\n"); + exit(1); +} + +$args = rcube_utils::get_opt(array('id' => 'dbid')); + +$dbid = 0; +if (!empty($args['dbid'])) { + $dbid = intval($args['dbid']); +} + +$username = trim($args[0]); +if (empty($username)) { + print "Missing username"; + usage(); +} +$password = trim($args[1]); +if (empty($password)) { + usage(); +} + + +// ----- +// From index.php -- initialization and login +// ----- + +// init application, start session, init output class, etc. +$RCMAIL = rcmail::get_instance(0, $GLOBALS['env']); + +// trigger startup plugin hook +$startup = $RCMAIL->plugins->exec_hook('startup', array('task' => $RCMAIL->task, 'action' => $RCMAIL->action)); +$RCMAIL->set_task($startup['task']); +$RCMAIL->action = $startup['action']; +$auth = $RCMAIL->plugins->exec_hook('authenticate', array( + 'host' => $RCMAIL->autoselect_host(), + 'user' => $username, + 'pass' => $password, + 'valid' => true, + 'cookiecheck' => false,)); + +// Login +if ($auth['valid'] && !$auth['abort'] + && $RCMAIL->login($auth['user'], $auth['pass'], $auth['host'], $auth['cookiecheck'])) + { + print "login ok\n"; + } + else + { + _die("login failed"); + } + + +// ---------------------------------------------------- +// ensure the carddav tables are created and populated +// ---------------------------------------------------- + +require_once('plugins/carddav/carddav_backend.php'); +require_once('plugins/carddav/carddav.php'); + +$c = new carddav(rcube_plugin_api::get_instance()); +$c->task .= "|cli"; +$c->init(); +print "done: init\n"; +// this ensures the carddav tables are created +$c->checkMigrations(); +print "done: init tables\n"; +// this populates carddav_addressbooks from config +$c->init_presets(); +print "done: init addressbooks\n"; + +// ------------------------------------------------------------- +// Set the last_updated field for addressbooks to an old date. +// That will force a sync/update +// ------------------------------------------------------------- +$db = $rcmail->get_dbh(); +$db->db_connect('w'); +if (!$db->is_connected() || $db->is_error()) { + _die("No DB connection\n" . $db->is_error()); +} +print "db connected\n"; + +$db->query("update " . $db->table_name('carddav_addressbooks') . " set last_updated=? WHERE active=1", '2000-01-01 00:00:00'); +print "update made\n"; +if ($db->is_error()) { + _die("DB error occurred: " . $db->is_error()); +} + + +// ------------------------------------------------------ +// Update/sync all active address books +// ------------------------------------------------------ + +// first get all row ids +$dbid=array(); +$sql_result = $db->query('SELECT id FROM ' . + $db->table_name('carddav_addressbooks') . + ' WHERE active=1'); +if ($db->is_error()) { + _die("DB error occurred: " . $db->is_error()); +} + +while ($row = $db->fetch_assoc($sql_result)) { + $dbid += array(intval($row['id'])); + print "carddav_addressbooks id: " . $row['id'] . "\n"; +} + +// instantiating carddav_backend causes the update/sync +foreach($dbid as $id) { + $config = carddav_backend::carddavconfig($id); + if ($config['needs_update']) { + print "instantiating carddav_backend: " . $id . "\n"; + $b = new carddav_backend($id); + print("success\n"); + } +} + diff --git a/tests/assets/system-setup/remote-nextcloud-docker.sh b/tests/assets/system-setup/remote-nextcloud-docker.sh new file mode 100755 index 00000000..c9418ae4 --- /dev/null +++ b/tests/assets/system-setup/remote-nextcloud-docker.sh @@ -0,0 +1,200 @@ +#!/bin/bash + +# setup MiaB-LDAP with a remote Nextcloud running on the same +# host under Docker exposed as localhost:8000 +# +# this script must be run with the working directory set to 'tests' + +usage() { + echo "Usage: $(basename "$0") [\"before-miab-install\"|\"miab-install\"|\"after-miab-install\"]" + echo "Install MiaB-LDAP and a remote Nextcloud running under docker exposed as localhost:8000" + echo "With no arguments, all three stages are run." + exit 1 +} + +# ensure working directory +if [ ! -d "tests/assets/system-setup" ]; then + echo "This script must be run from the MiaB root directory" + exit 1 +fi + +# load helper scripts +. "tests/assets/system-setup/setup-defaults.sh" \ + || die "Could not load setup-defaults" +. "tests/assets/system-setup/setup-funcs.sh" \ + || die "Could not load setup-funcs" + +# ensure running as root +if [ "$EUID" != "0" ]; then + die "This script must be run as root (sudo)" +fi + + + +before_miab_install() { + H1 "BEFORE MIAB-LDAP INSTALL" + + # create /etc/hosts entry for PRIVATE_IP + H2 "Update /etc/hosts" + update_hosts_for_private_ip || die "Could not update /etc/hosts" + + # install prerequisites + H2 "QA prerequisites" + install_qa_prerequisites || die "Error installing QA prerequisites" + + # update system time (ignore errors) + H2 "Set system time" + update_system_time + + # copy in pre-built MiaB-LDAP ssl files + # 1. avoid the lengthy generation of DH params + H2 "Install QA pre-built" + mkdir -p $STORAGE_ROOT/ssl || die "Unable to create $STORAGE_ROOT/ssl" + cp tests/assets/ssl/dh2048.pem $STORAGE_ROOT/ssl \ + || die "Copy dhparams failed" + + # create miab_ldap.conf to specify what the Nextcloud LDAP service + # account password will be to avoid a random one created by start.sh + mkdir -p $STORAGE_ROOT/ldap + [ -e $STORAGE_ROOT/ldap/miab_ldap.conf ] && \ + echo "Warning: exists: $STORAGE_ROOT/ldap/miab_ldap.conf" 1>&2 + echo "LDAP_NEXTCLOUD_PASSWORD=\"$LDAP_NEXTCLOUD_PASSWORD\"" >> $STORAGE_ROOT/ldap/miab_ldap.conf + + # enable the remote Nextcloud setup mod, which tells MiaB-LDAP to use + # the remote Nextcloud for calendar and contacts instead of the + # MiaB-installed one + if [ ! -e "setup/mods.d/remote-nextcloud.sh" ]; then + ln -s "../mods.available/remote-nextcloud.sh" "setup/mods.d/remote-nextcloud.sh" || die "Could not create remote-nextcloud.sh symlink" + fi + + + # install Docker + H2 "Install Docker" + install_docker || die "Could not install Docker! ($?)" +} + + +miab_install() { + H1 "MIAB-LDAP INSTALL" + setup/start.sh -v || die "setup/start.sh failed!" +} + + +after_miab_install() { + H1 "AFTER MIAB-LDAP INSTALL" + + . /etc/mailinabox.conf || die "Could not load /etc/mailinabox.conf" + + # TRAVIS: fix nsd startup problem + H2 "Apply Travis-CI nsd fix" + travis_fix_nsd || die "Could not fix NSD startup issue for TRAVIS-CI" + + # run Nextcloud docker image + H2 "Start Nextcloud docker container" + docker run -d --name NC -p 8000:80 \ + --env SQLITE_DATABASE=nextclouddb.sqlite \ + --env NEXTCLOUD_ADMIN_USER="$NC_ADMIN_USER" \ + --env NEXTCLOUD_ADMIN_PASSWORD="$NC_ADMIN_PASSWORD" \ + --env NEXTCLOUD_TRUSTED_DOMAINS="127.0.0.1 ::1" \ + --env NEXTCLOUD_UPDATE=1 \ + --env SMTP_HOST="$PRIMARY_HOSTNAME" \ + --env SMTP_SECURE="tls" \ + --env SMTP_PORT=587 \ + --env SMTP_AUTHTYPE="LOGIN" \ + --env SMTP_NAME="$EMAIL_ADDR" \ + --env SMTP_PASSWORD="$EMAIL_PW" \ + --env SMTP_FROM_ADDRESS="$(awk -F@ '{print $1}' <<< "$EMAIL_ADDR")" \ + --env MAIL_DOMAIN="$(awk -F@ '{print $2}' <<< "$EMAIL_ADDR")" \ + nextcloud:latest \ + || die "Docker run failed!" + + H2 "docker: Update /etc/hosts so it can find MiaB-LDAP by name" + echo "$PRIVATE_IP $PRIMARY_HOSTNAME" | \ + docker exec -i NC bash -c 'cat >>/etc/hosts' \ + || die "docker: could not update /etc/hosts" + + # apt-get update + H2 "docker: apt-get update" + docker exec NC apt-get update || die "docker: apt-get update failed" + + # allow LDAP access from docker image + H2 "Allow ldaps through firewall so Nextcloud can perform LDAP searches" + ufw allow ldaps || die "Unable to modify firewall to permit ldaps" + + # add MiaB-LDAP's ca_certificate.pem to docker's trusted cert list + H2 "docker: update trusted CA list" + docker cp \ + $STORAGE_ROOT/ssl/ca_certificate.pem \ + NC:/usr/local/share/ca-certificates/mailinabox.crt \ + || die "docker: copy ca_certificate.pem failed" + docker exec NC update-ca-certificates \ + || die "docker: update-ca-certificates failed" + + # wait for Nextcloud installation to complete + H2 "Wait for Nextcloud installation to complete" + echo -n "Waiting ..." + local count=0 + while true; do + if [ $count -ge 10 ]; then + echo "FAILED" + die "Giving up" + fi + sleep 6 + let count+=1 + if [ $(docker exec NC php -n -r "include 'config/config.php'; print \$CONFIG['installed']?'true':'false';") == "true" ]; then + echo "ok" + break + fi + echo -n "${count}..." + done + + # install and enable Nextcloud and apps + H2 "docker: install Nextcloud calendar app" + docker exec -u www-data NC ./occ app:install calendar \ + || die "docker: installing calendar app failed" + H2 "docker: install Nextcloud contacts app" + docker exec -u www-data NC ./occ app:install contacts \ + || die "docker: installing contacts app failed" + H2 "docker: enable user_ldap" + docker exec -u www-data NC ./occ app:enable user_ldap \ + || die "docker: enabling user_ldap failed" + + # integrate Nextcloud with MiaB-LDAP + H2 "docker: integrate Nextcloud with MiaB-LDAP" + docker cp setup/mods.available/remote-nextcloud-use-miab.sh NC:/tmp \ + || die "docker: cp remote-nextcloud-use-miab.sh failed" + docker exec NC /tmp/remote-nextcloud-use-miab.sh \ + . \ + "$NC_ADMIN_USER" \ + "$NC_ADMIN_PASSWORD" \ + "$PRIMARY_HOSTNAME" \ + "$LDAP_NEXTCLOUD_PASSWORD" \ + || die "docker: error running remote-nextcloud-use-miab.sh" +} + + + +# +# process command line +# + +case "$1" in + before-miab-install ) + before_miab_install + ;; + after-miab-install ) + after_miab_install + ;; + miab-install ) + miab_install + ;; + "" ) + before_miab_install + miab_install + after_miab_install + ;; + * ) + + usage + ;; +esac diff --git a/tests/assets/system-setup/setup-defaults.sh b/tests/assets/system-setup/setup-defaults.sh new file mode 100755 index 00000000..851f92d6 --- /dev/null +++ b/tests/assets/system-setup/setup-defaults.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Used by MiaB-LDAP setup/start.sh +export NONINTERACTIVE=${NONINTERACTIVE:-1} +export SKIP_NETWORK_CHECKS=${SKIP_NETWORK_CHECKS:-1} +export STORAGE_USER="${STORAGE_USER:-user-data}" +export STORAGE_ROOT="${STORAGE_ROOT:-/home/$STORAGE_USER}" +export EMAIL_ADDR="${EMAIL_ADDR:-qa@abc.com}" +export EMAIL_PW="${EMAIL_PW:-Test_1234}" +export PUBLIC_IP="${PUBLIC_IP:-$(source setup/functions.sh; get_default_privateip 4)}" + +if [ "$TRAVIS" == "true" ]; then + export PRIMARY_HOSTNAME=${PRIMARY_HOSTNAME:-box.abc.com} +elif [ -z "$PRIMARY_HOSTNAME" ]; then + export PRIMARY_HOSTNAME=$(hostname --fqdn || hostname) +fi + + +# Placing this var in STORAGE_ROOT/ldap/miab_ldap.conf before running +# setup/start.sh will avoid a random password from being used for the +# Nextcloud LDAP service account +export LDAP_NEXTCLOUD_PASSWORD=${LDAP_NEXTCLOUD_PASSWORD:-Test_LDAP_1234} + +# Used by setup/mods.available/remote-nextcloud.sh. These define to +# MiaB-LDAP the remote Nextcloud that serves calendar and contacts +export NC_PROTO=${NC_PROTO:-http} +export NC_HOST=${NC_HOST:-127.0.0.1} +export NC_PORT=${NC_PORT:-8000} +export NC_PREFIX=${NC_PREFIX:-/} + +# For setup scripts that are installing a remote Nextcloud +export NC_ADMIN_USER="${NC_ADMIN_USER:-admin}" +export NC_ADMIN_PASSWORD="${NC_ADMIN_PASSWORD:-Test_1234}" diff --git a/tests/assets/system-setup/setup-funcs.sh b/tests/assets/system-setup/setup-funcs.sh new file mode 100755 index 00000000..b5768b1a --- /dev/null +++ b/tests/assets/system-setup/setup-funcs.sh @@ -0,0 +1,97 @@ + +die() { + local msg="$1" + echo "$msg" 1>&2 + exit 1 +} + +H1() { + local msg="$1" + echo "----------------------------------------------" + echo " $msg" + echo "----------------------------------------------" +} + +H2() { + local msg="$1" + echo "*** $msg ***" +} + +install_qa_prerequisites() { + # python3-dnspython: is used by the python scripts in 'tests' and is + # not installed by setup + # ntpdate: is used by this script + apt-get install -y \ + ntpdate \ + python3-dnspython +} + +update_system_time() { + ntpdate -s ntp.ubuntu.com && echo "System time updated" +} + +update_hosts() { + local host="$1" + local ip="$2" + local line="$ip $host" + if ! grep -F "$line" /etc/hosts 1>/dev/null; then + echo "$line" >>/etc/hosts + fi +} + +update_hosts_for_private_ip() { + # create /etc/hosts entry for PRIVATE_IP + # PRIMARY_HOSTNAME must already be set + local ip=$(source setup/functions.sh; get_default_privateip 4) + [ -z "$ip" ] && return 1 + update_hosts "$PRIMARY_HOSTNAME" "$ip" || return 1 +} + +install_docker() { + if [ -x /usr/bin/docker ]; then + echo "Docker already installed" + return 0 + fi + + apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg-agent \ + software-properties-common \ + || return 1 + + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \ + || return 2 + + apt-key fingerprint 0EBFCD88 || return 3 + + add-apt-repository -y --update "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" || return 4 + + apt-get install -y \ + docker-ce \ + docker-ce-cli \ + containerd.io \ + || return 5 +} + + +travis_fix_nsd() { + if [ "$TRAVIS" != "true" ]; then + return 0 + fi + + # nsd won't start on Travis-CI without the changes below: ip6 off and + # control-enable set to no. Even though the nsd docs says the + # default value for control-enable is no, running "nsd-checkconf -o + # control-enable /etc/nsd/nsd.conf" returns "yes", so we explicitly + # set it here. + # + # we're assuming that the "ip-address" line is the last line in the + # "server" section of nsd.conf. if this generated file output + # changes, the sed command below may need to be adjusted. + sed -i 's/ip-address\(.\)\(.*\)/ip-address\1\2\n do-ip4\1 yes\n do-ip6\1 no\n verbosity\1 3\nremote-control\1\n control-enable\1 no/' /etc/nsd/nsd.conf || return 1 + cat /etc/nsd/nsd.conf + systemctl reset-failed nsd.service || return 2 + systemctl restart nsd.service || return 3 +} diff --git a/tests/runner.sh b/tests/runner.sh index db0511a5..8119d462 100755 --- a/tests/runner.sh +++ b/tests/runner.sh @@ -11,7 +11,7 @@ cd "$(dirname $0)" # load global functions and variables . suites/_init.sh -runner_suites=( +default_suites=( ldap-connection ldap-access mail-basic @@ -21,21 +21,37 @@ runner_suites=( management-users ) +extra_suites=( + remote-nextcloud +) + usage() { echo "" - echo "Usage: $(basename $0) [-failfatal] [suite-name ...]" - echo "Valid suite names:" - for runner_suite in ${runner_suites[@]}; do - echo " $runner_suite" + echo "Usage: $(basename $0) [options] [suite-name ...]" + echo "Run QA tests" + + echo "" + echo "Default test suites:" + echo "--------------------" + for runner_suite in ${default_suites[@]}; do + echo " $runner_suite" done - echo "If no suite-name(s) given, all suites are run" + + echo "" + echo "Extra test suites:" + echo "------------------" + echo " remote-nextcloud : test the setup mod for remote Nextcloud" + echo "" + + echo "If no suite-name(s) are given, all default suites are run" echo "" echo "Options:" + echo "--------" echo " -failfatal The runner will stop if any test fails" echo " -dumpoutput After all tests have run, dump all failed test output" echo " -no-smtp-remote Skip tests requiring a remote SMTP server" echo "" - echo "Output directory: $(dirname $0)/${base_outputdir}" + echo "Output directory: ${BASE_OUTPUTDIR}" echo "" exit 1 } @@ -59,7 +75,20 @@ while [ $# -gt 0 ]; do ;; * ) # run named suite - if array_contains "$1" ${runner_suites[@]}; then + if [ $OVERALL_COUNT_SUITES -eq 0 ]; then + rm -rf "${BASE_OUTPUTDIR}" + fi + + if [ "$1" == "default" ] + then + # run all default suites + for suite in ${default_suites[@]}; do + . suites/$suite.sh + done + elif array_contains "$1" ${default_suites[@]} || \ + array_contains "$1" ${extra_suites[@]} + then + # run specified suite . "suites/$1.sh" else echo "Unknown suite '$1'" 1>&2 @@ -70,11 +99,11 @@ while [ $# -gt 0 ]; do shift done -# if no suites specified on command line, run all suites +# if no suites specified on command line, run all default suites if [ $OVERALL_COUNT_SUITES -eq 0 ]; then - rm -rf "${base_outputdir}" - for runner_suite in ${runner_suites[@]}; do - . suites/$runner_suite.sh + rm -rf "${BASE_OUTPUTDIR}" + for suite in ${default_suites[@]}; do + . suites/$suite.sh done fi diff --git a/tests/suites/_init.sh b/tests/suites/_init.sh index 2b29e576..bee4fb20 100644 --- a/tests/suites/_init.sh +++ b/tests/suites/_init.sh @@ -6,12 +6,13 @@ set +eu # load test suite helper functions +. suites/_locations.sh || exit 1 . suites/_ldap-functions.sh || exit 1 . suites/_mail-functions.sh || exit 1 . suites/_mgmt-functions.sh || exit 1 # globals - all global variables are UPPERCASE -BASE_OUTPUTDIR="out" +BASE_OUTPUTDIR="$(realpath out)" PYMAIL="./test_mail.py" declare -i OVERALL_SUCCESSES=0 declare -i OVERALL_FAILURES=0 @@ -45,11 +46,12 @@ suite_start() { mkdir -p "$OUTDIR" echo "" echo "Starting suite: $SUITE_NAME" - suite_setup "$2" + shift + suite_setup "$@" } suite_end() { - suite_cleanup "$1" + suite_cleanup "$@" echo "Suite $SUITE_NAME finished" let OVERALL_SUCCESSES+=$SUITE_COUNT_SUCCESS let OVERALL_FAILURES+=$SUITE_COUNT_FAILURE @@ -61,14 +63,16 @@ suite_end() { suite_setup() { [ -z "$1" ] && return 0 TEST_OF="$OUTDIR/setup" - eval "$1" + local script + for script; do eval "$script"; done TEST_OF="" } suite_cleanup() { [ -z "$1" ] && return 0 TEST_OF="$OUTDIR/cleanup" - eval "$1" + local script + for script; do eval "$script"; done TEST_OF="" } @@ -105,7 +109,7 @@ test_end() { let idx+=1 done echo "$TEST_OF" >>$FAILED_TESTS_MANIFEST - echo " see: $(dirname $0)/$TEST_OF" + echo " see: $TEST_OF" let SUITE_COUNT_FAILURE+=1 if [ "$FAILURE_IS_FATAL" == "yes" ]; then record "FATAL: failures are fatal option enabled" @@ -205,6 +209,12 @@ python_error() { [ $? -eq 1 ] && echo "$output" } +copy_or_die() { + local src="$1" + local dst="$2" + cp "$src" "$dst" || die "Unable to copy '$src' => '$dst'" +} + dump_failed_tests_output() { if [ "$DUMP_FAILED_TESTS_OUTPUT" == "yes" ]; then echo "" diff --git a/tests/suites/_ldap-functions.sh b/tests/suites/_ldap-functions.sh index 4dcac8cb..77e102fe 100644 --- a/tests/suites/_ldap-functions.sh +++ b/tests/suites/_ldap-functions.sh @@ -7,6 +7,11 @@ generate_uuid() { echo "$uuid" } +sha1() { + local txt="$1" + python3 -c "import hashlib; m=hashlib.sha1(); m.update(bytearray(r'''$txt''','utf-8')); print(m.hexdigest());" || die "Unable to generate sha1 hash" +} + delete_user() { local email="$1" local domainpart="$(awk -F@ '{print $2}' <<< "$email")" @@ -32,12 +37,13 @@ create_user() { local priv="${3:-test}" local localpart="$(awk -F@ '{print $1}' <<< "$email")" local domainpart="$(awk -F@ '{print $2}' <<< "$email")" - local uid="$localpart" + #local uid="$localpart" + local uid="$(sha1 "$email")" local dn="uid=${uid},${LDAP_USERS_BASE}" delete_user "$email" - record "[create user $email]" + record "[create user $email ($dn)]" delete_dn "$dn" ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <>$TEST_OF) + local code=$? + + # http status is last 3 characters of output, extract it + REST_HTTP_CODE=$(awk '{S=substr($0,length($0)-2)} END {print S}' <<<"$output") + REST_OUTPUT=$(awk 'BEGIN{L=""}{ if(L!="") print L; L=$0 } END { print substr(L,1,length(L)-3) }' <<<"$output") + REST_ERROR="" + [ -z "$REST_HTTP_CODE" ] && REST_HTTP_CODE="000" + + if [ $code -ne 0 -o \ + $REST_HTTP_CODE -lt 200 -o \ + $REST_HTTP_CODE -ge 300 ] + then + REST_ERROR="REST status $REST_HTTP_CODE: $REST_OUTPUT" + REST_ERROR_BRIEF=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.fromstring(r'''$REST_OUTPUT''').find('s:message',{'s':'http://sabredav.org/ns'}).text)" 2>/dev/null) + if [ -z "$REST_ERROR_BRIEF" ]; then + REST_ERROR_BRIEF="$REST_ERROR" + else + REST_ERROR_BRIEF="$REST_HTTP_CODE: $REST_ERROR_BRIEF" + fi + if [ $code -ne 0 ]; then + REST_ERROR_BRIEF="curl exit code $code: $REST_ERROR_BRIEF" + REST_ERROR="curl exit code $code: $REST_ERROR" + fi + record "${F_DANGER}$REST_ERROR${F_RESET}" + return 2 + fi + record "CURL succeded, HTTP status $REST_HTTP_CODE" + record "$output" + return 0 +} + + +carddav_ls() { + # return all .vcf files in array 'FILES' + local user="$1" + local pass="$2" + carddav_rest PROPFIND "" "$user" "$pass" || return $? + local file FILES=() + python3 -c "import xml.etree.ElementTree as ET; [print(el.find('d:href',{'d':'DAV:'}).text) for el in ET.fromstring(r'''$REST_OUTPUT''').findall('d:response',{'d':'DAV:'}) if el.find('d:href',{'d':'DAV:'}) is not None]" | + while read file; do + # skip non .vcf entries + case "$file" in + *.vcf ) + FILES+=( "$(basename "$file")" ) + ;; + * ) + ;; + esac + done +} + + +make_collection() { + local user="$1" + local pass="$2" + local name="$3" + local desc="${4:-$name}" + local xml=" + + + + + + + + $name + $desc + + +" + record "[create address book '$name' for $user]" + local url="$(carddav_url "$user" CARDDAV_PATH)" + carddav_rest MKCOL "$url" "$user" "$pass" "$xml" +} + + + +add_contact() { + local user="$1" + local pass="$2" + local c_name="$3" + local c_phone="$4" + local c_email="$5" + local c_uid="${6:-$(generate_uuid)}" + local file_name="$c_uid.vcf" + + local vcard="BEGIN:VCARD +VERSION:3.0 +UID:$c_uid +REV;VALUE=DATE-AND-OR-TIME:$(date -u +%Y%m%dT%H%M%SZ) +FN:$c_name +EMAIL;TYPE=INTERNET,PREF:$c_email +NOTE:Miab-LDAP QA +ORG:Miab-LDAP +TEL;TYPE=WORK,VOICE:$c_phone +END:VCARD" + record "[add contact '$c_name' to $user]" + carddav_rest PUT "$file_name" "$user" "$pass" "$vcard" +} + +delete_contact() { + local user="$1" + local pass="$2" + local c_uid="$3" + local file_name="$c_uid.vcf" + record "[delete contact with vcard uid '$c_uid' from $user]" + carddav_rest DELETE "$file_name" "$user" "$pass" +} + +force_roundcube_carddav_refresh() { + local user="$1" + local pass="$2" + local code + record "[forcing refresh of roundcube contact for $user]" + copy_or_die assets/mail/roundcube/carddav_refresh.sh $RCM_DIR/bin + pushd "$RCM_DIR" >/dev/null + bin/carddav_refresh.sh "$user" "$pass" >>$TEST_OF 2>&1 + code=$? + popd >/dev/null + return $code +} + +assert_roundcube_carddav_contact_exists() { + local user="$1" + local pass="$2" + local c_uid="$3" + local output + record "[checking that roundcube contact with vcard UID=$c_uid exists]" + output="$(sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite "select name from carddav_contacts where cuid='$c_uid'" 2>>$TEST_OF)" + if [ $? -ne 0 ]; then + test_failure "Error querying roundcube sqlite database" + return 1 + fi + if [ -z "$output" ]; then + test_failure "Contact not found in Roundcube" + record "Not found" + record "Existing entries (name,vcard-uid):" + output="$(sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite "select name,cuid FROM carddav_contacts" 2>>$TEST_OF)" + return 1 + else + record "$output" + fi + return 0 +} + + +test_mail_from_nextcloud() { + test_start "mail_from_nextcloud" + test_end +} + +test_nextcloud_contacts() { + test_start "nextcloud_contacts" + + assert_is_configured || (test_end && return) + + local alice="alice3@somedomain.com" + local alice_pw="$(generate_password 16)" + + # create local user alice + mgmt_assert_create_user "$alice" "$alice_pw" + + + # + # 1. create contact in Nextcloud - ensure it is available in Roundcube + # + # this will validate Nextcloud's ability to authenticate users via + # LDAP and that Roundcube is able to reach Nextcloud for contacts + # + + #make_collection "$alice" "$alice_pw" "contacts" + + # add new contact to alice's Nextcloud account using CardDAV API + local c_uid="$(generate_uuid)" + add_contact \ + "$alice" \ + "$alice_pw" \ + "JimIno" \ + "555-1212" \ + "jim@ino.com" \ + "$c_uid" \ + || test_failure "Could not add contact for $alice in Nextcloud: $REST_ERROR_BRIEF" + + # force a refresh/sync of the contacts in Roundcube + force_roundcube_carddav_refresh "$alice" "$alice_pw" || \ + test_failure "Could not refresh roundcube contacts for $alice" + + # query the roundcube sqlite database for the new contact + assert_roundcube_carddav_contact_exists "$alice" "$alice_pw" "$c_uid" + + # delete the contact + delete_contact "$alice" "$alice_pw" "$c_uid" || \ + test_failure "Unable to delete contact for $alice in Nextcloud" + + + # + # 2. create contact in Roundcube - ensure contact appears in Nextcloud + # + # TODO + + + # clean up + mgmt_assert_delete_user "$alice" + + test_end +} + + +suite_start "remote-nextcloud" mgmt_start + +#test_mail_from_nextcloud +test_nextcloud_contacts + +suite_end mgmt_end + + + +