load common

@test 'prepare images directory' {
    shopt -s nullglob  # globs that match nothing yield empty string
    if [[ -e $IMGDIR ]]; then
        # Images directory exists. If all it contains is Charliecloud images
        # or supporting directories, or nothing, then we're ok. Remove any
        # images (this makes test-build and test-run follow the same path when
        # run on the same or different machines). Otherwise, error.
        for i in $IMGDIR/*; do
            if [[ -d $i && -f $i/WEIRD_AL_YANKOVIC ]]; then
                echo "found image $i; removing"
                rm -Rf --one-file-system $i
            else
                echo "found non-image $i; aborting"
                false
            fi
        done
    fi
    mkdir -p $IMGDIR
    mkdir -p $IMGDIR/bind1
    touch $IMGDIR/bind1/WEIRD_AL_YANKOVIC  # fool logic above
    touch $IMGDIR/bind1/file1
    mkdir -p $IMGDIR/bind2
    touch $IMGDIR/bind2/WEIRD_AL_YANKOVIC
    touch $IMGDIR/bind2/file2
}

@test 'executables --help' {
    ch-tar2dir --help
    ch-run --help
    ch-ssh --help
}

@test 'setuid bit matches --is-setuid' {
    test $CH_RUN_FILE -ef $(which ch-run)
    [[ -e $CH_RUN_FILE ]]
    ls -l $CH_RUN_FILE
    if ( ch-run --is-setuid ); then
        [[ -n $CH_RUN_SETUID ]]
        [[ -u $CH_RUN_FILE ]]
        [[ $(stat -c %U $CH_RUN_FILE) = root ]]
    else
        [[ -z $CH_RUN_SETUID ]]
        [[ ! -u $CH_RUN_FILE ]]
        #[[ $(stat -c %U $CH_RUN_FILE) != root ]]
    fi
}

@test 'setgid bit is off' {
    [[ -e $CH_RUN_FILE ]]
    [[ ! -g $CH_RUN_FILE ]]
    #[[ $(stat -c %G $CH_RUN_FILE) != root ]]
}

@test 'ch-run refuses to run if setgid' {
    CH_RUN_TMP=$BATS_TMPDIR/ch-run.setgid
    GID=$(id -g)
    GID2=$(id -G | cut -d' ' -f2)
    echo "GIDs: $GID $GID2"
    [[ $GID != $GID2 ]]
    cp -a $CH_RUN_FILE $CH_RUN_TMP
    ls -l $CH_RUN_TMP
    chgrp $GID2 $CH_RUN_TMP
    chmod g+s $CH_RUN_TMP
    ls -l $CH_RUN_TMP
    [[ -g $CH_RUN_TMP ]]
    run $CH_RUN_TMP --version
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'ch-run.setgid: error (' ]]
    rm $CH_RUN_TMP
}

@test 'ch-run refuses to run if setuid' {
    [[ -z $CH_RUN_SETUID ]] || skip 'compiled in setuid mode'
    [[ -n $CHTEST_HAVE_SUDO ]] || skip 'sudo not available'
    CH_RUN_TMP=$BATS_TMPDIR/ch-run.setuid
    cp -a $CH_RUN_FILE $CH_RUN_TMP
    ls -l $CH_RUN_TMP
    sudo chown root $CH_RUN_TMP
    sudo chmod u+s $CH_RUN_TMP
    ls -l $CH_RUN_TMP
    [[ -u $CH_RUN_TMP ]]
    run $CH_RUN_TMP --version
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'ch-run.setuid: error (' ]]
    sudo rm $CH_RUN_TMP
}

@test 'ch-run as root: --version and --test' {
    [[ -n $CHTEST_HAVE_SUDO ]] || skip 'sudo not available'
    sudo $CH_RUN_FILE --version
    sudo $CH_RUN_FILE --help
}

@test 'ch-run as root: run image' {
    # Running an image should work as root, but it doesn't, and I'm not sure
    # why, so skip this test. This fails in the test suite with:
    #
    #   ch-run: couldn't resolve image path: No such file or directory (ch-run.c:139:2)
    #
    # but when run manually (with same arguments?) it fails differently with:
    #
    #   $ sudo bin/ch-run $CH_TEST_IMGDIR/chtest -- true
    #   ch-run: [...]/chtest: Permission denied (ch-run.c:195:13)
    #
    skip 'issue #76'
    sudo $CH_RUN_FILE $CHTEST_IMG -- true
}

@test 'ch-run as root: root with non-zero GID refused' {
    [[ -z $TRAVIS ]] || skip 'not permitted on Travis'
    run sudo -u root -g $(id -gn) $CH_RUN_FILE -v --version
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'error (' ]]
}

@test 'ch-run -u and -g refused in setuid mode' {
    [[ -n $CH_RUN_SETUID ]] || skip 'not compiled for setuid'
    run ch-run -u 65534
    echo "$output"
    [[ $status -eq 64 ]]
    [[ $output =~ "ch-run: invalid option -- 'u'" ]]
    run ch-run -g 65534
    echo "$output"
    [[ $status -eq 64 ]]
    [[ $output =~ "ch-run: invalid option -- 'g'" ]]
}

@test 'syscalls/pivot_root' {
    [[ -n $CH_RUN_SETUID ]] && skip
    cd ../examples/syscalls
    ./pivot_root
}

@test 'unpack chtest image' {
    if ( image_ok $CHTEST_IMG ); then
        # image exists, remove so we can test new unpack
        rm -Rf --one-file-system $CHTEST_IMG
    fi
    ch-tar2dir $CHTEST_TARBALL $IMGDIR  # new unpack
    image_ok $CHTEST_IMG
    ch-tar2dir $CHTEST_TARBALL $IMGDIR  # overwrite
    image_ok $CHTEST_IMG
}

@test 'ch-tar2dir errors' {
    # tarball doesn't exist
    run ch-tar2dir does_not_exist.tar.gz $IMGDIR
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output = "can't read does_not_exist.tar.gz" ]]

    # tarball exists but isn't readable
    touch $BATS_TMPDIR/unreadable.tar.gz
    chmod 000 $BATS_TMPDIR/unreadable.tar.gz
    run ch-tar2dir $BATS_TMPDIR/unreadable.tar.gz $IMGDIR
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output = "can't read $BATS_TMPDIR/unreadable.tar.gz" ]]
}

@test 'workaround for /bin not in $PATH' {
    echo "$PATH"
    # if /bin is in $PATH, latter passes through unchanged
    PATH2="$CH_BIN:/bin:/usr/bin"
    echo $PATH2
    PATH=$PATH2 run ch-run $CHTEST_IMG -- /bin/sh -c 'echo $PATH'
    echo "$output"
    [[ $status -eq 0 ]]
    [[ $output = $PATH2 ]]
    PATH2="/bin:$CH_BIN:/usr/bin"
    echo $PATH2
    PATH=$PATH2 run ch-run $CHTEST_IMG -- /bin/sh -c 'echo $PATH'
    echo "$output"
    [[ $status -eq 0 ]]
    [[ $output = $PATH2 ]]
    # if /bin isn't in $PATH, former is added to end
    PATH2="$CH_BIN:/usr/bin"
    echo $PATH2
    PATH=$PATH2 run ch-run $CHTEST_IMG -- /bin/sh -c 'echo $PATH'
    echo "$output"
    [[ $status -eq 0 ]]
    [[ $output = $PATH2:/bin ]]
}

@test '$PATH unset' {
    BACKUP_PATH=$PATH
    unset PATH
    run $CH_RUN_FILE $CHTEST_IMG -- \
        /usr/bin/python3 -c 'import os; print(os.getenv("PATH") is None)'
    PATH=$BACKUP_PATH
    echo "$output"
    [[ $status -eq 0 ]]
    [[ $output = "True" ]]
}

@test 'mountns id differs' {
    host_ns=$(stat -Lc '%i' /proc/self/ns/mnt)
    echo "host:  $host_ns"
    guest_ns=$(ch-run $CHTEST_IMG -- stat -Lc '%i' /proc/self/ns/mnt)
    echo "guest: $guest_ns"
    [[ -n $host_ns && -n $guest_ns && $host_ns -ne $guest_ns ]]
}

@test 'userns id differs' {
    [[ -n $CH_RUN_SETUID ]] && skip
    host_ns=$(stat -Lc '%i' /proc/self/ns/user)
    echo "host:  $host_userns"
    guest_ns=$(ch-run $CHTEST_IMG -- stat -Lc '%i' /proc/self/ns/user)
    echo "guest: $guest_ns"
    [[ -n $host_ns && -n $guest_ns && $host_ns -ne $guest_ns ]]
}

@test 'distro differs' {
    # This is a catch-all and a bit of a guess. Even if it fails, however, we
    # get an empty string, which is fine for the purposes of the test.
    echo hello world
    host_distro=$(  cat /etc/os-release /etc/*-release /etc/*_version \
                  | egrep -m1 '[A-Za-z] [0-9]' \
                  | sed -r 's/^(.*")?(.+)(")$/\2/')
    echo "host: $host_distro"
    guest_expected='Alpine Linux v3.6'
    echo "guest expected: $guest_expected"
    if [[ $host_distro = $guest_expected ]]; then
        skip 'host matches expected guest distro'
    fi
    guest_distro=$(ch-run $CHTEST_IMG -- \
                          cat /etc/os-release \
                   | fgrep PRETTY_NAME \
                   | sed -r 's/^(.*")?(.+)(")$/\2/')
    echo "guest: $guest_distro"
    [[ $guest_distro = $guest_expected ]]
    [[ $guest_distro != $host_distro ]]
}

@test 'user and group match host' {
    host_uid=$(id -u)
    guest_uid=$(ch-run $CHTEST_IMG -- id -u)
    [[ $host_uid = $guest_uid ]]
    host_pgid=$(id -g)
    guest_pgid=$(ch-run $CHTEST_IMG -- id -g)
    [[ $host_pgid = $guest_pgid ]]
    host_username=$(id -un)
    guest_username=$(ch-run $CHTEST_IMG -- id -un)
    [[ $host_username = $guest_username ]]
    host_pgroup=$(id -gn)
    guest_pgroup=$(ch-run $CHTEST_IMG -- id -gn)
    [[ $host_pgroup = $guest_pgroup ]]
}

@test 'mount image read-only' {
    run ch-run $CHTEST_IMG sh <<EOF
set -e
test -w /WEIRD_AL_YANKOVIC
dd if=/dev/zero bs=1 count=1 of=/WEIRD_AL_YANKOVIC
EOF
    echo "$output"
    [[ $status -ne 0 ]]
    [[ $output =~ 'Read-only file system' ]]
}

@test 'mount image read-write' {
   ch-run -w $CHTEST_IMG -- sh -c 'echo writable > write'
   ch-run -w $CHTEST_IMG rm write
}

@test 'ch-run --bind' {
    # one bind, default destination (/mnt/0)
    ch-run -b $IMGDIR/bind1 $CHTEST_IMG -- cat /mnt/0/file1
    # one bind, explicit destination
    ch-run -b $IMGDIR/bind1:/mnt/9 $CHTEST_IMG -- cat /mnt/9/file1

    # two binds, default destination
    ch-run -b $IMGDIR/bind1 -b $IMGDIR/bind2 $CHTEST_IMG \
           -- cat /mnt/0/file1 /mnt/1/file2
    # two binds, explicit destinations
    ch-run -b $IMGDIR/bind1:/mnt/8 -b $IMGDIR/bind2:/mnt/9 $CHTEST_IMG \
           -- cat /mnt/8/file1 /mnt/9/file2
    # two binds, default/explicit
    ch-run -b $IMGDIR/bind1 -b $IMGDIR/bind2:/mnt/9 $CHTEST_IMG \
           -- cat /mnt/0/file1 /mnt/9/file2
    # two binds, explicit/default
    ch-run -b $IMGDIR/bind1:/mnt/8 -b $IMGDIR/bind2 $CHTEST_IMG \
           -- cat /mnt/8/file1 /mnt/1/file2

    # bind one source at two destinations
    ch-run -b $IMGDIR/bind1:/mnt/8 -b $IMGDIR/bind1:/mnt/9 $CHTEST_IMG \
           -- diff -u /mnt/8/file1 /mnt/9/file1
    # bind two sources at one destination
    ch-run -b $IMGDIR/bind1:/mnt/9 -b $IMGDIR/bind2:/mnt/9 $CHTEST_IMG \
           -- sh -c '[ ! -e /mnt/9/file1 ] && cat /mnt/9/file2'

    # omit tmpfs at /home, which shouldn't be empty
    ch-run --no-home $CHTEST_IMG -- cat /home/overmount-me
    # overmount tmpfs at /home
    ch-run -b $IMGDIR/bind1:/home $CHTEST_IMG -- cat /home/file1
    # bind to /home without overmount
    ch-run --no-home -b $IMGDIR/bind1:/home $CHTEST_IMG -- cat /home/file1
    # omit default /home, with unrelated --bind
    ch-run --no-home -b $IMGDIR/bind1 $CHTEST_IMG -- cat /mnt/0/file1
}

@test 'ch-run --bind errors' {

    # too many binds (11)
    run ch-run -b0 -b1 -b2 -b3 -b4 -b5 -b6 -b7 -b8 -b9 -b10 $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'ch-run: --bind can be used at most 10 times' ]]

    # no argument to --bind
    run ch-run $CHTEST_IMG -b
    echo "$output"
    [[ $status -eq 64 ]]
    [[ $output =~ 'ch-run: option requires an argument' ]]

    # empty argument to --bind
    run ch-run -b '' $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'ch-run: --bind: no source provided' ]]

    # source not provided
    run ch-run -b :/mnt/9 $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'ch-run: --bind: no source provided' ]]

    # destination not provided
    run ch-run -b $IMGDIR/bind1: $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ 'ch-run: --bind: no destination provided' ]]

    # source does not exist
    run ch-run -b $IMGDIR/hoops $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    r='^ch-run: could not bind .+/hoops to /mnt/0: No such file or directory'
    [[ $output =~ $r ]]

    # destination does not exist
    run ch-run -b $IMGDIR/bind1:/goops $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    r='^ch-run: could not bind .+/bind1 to /goops: No such file or directory'
    [[ $output =~ $r ]]

    # neither source nor destination exist
    run ch-run -b $IMGDIR/hoops:/goops $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    r='^ch-run: could not bind .+/hoops to /goops: No such file or directory'
    [[ $output =~ $r ]]

    # correct bind followed by source does not exist
    run ch-run -b $IMGDIR/bind1:/mnt/9 -b $IMGDIR/hoops $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    r='^ch-run: could not bind .+/hoops to /mnt/1: No such file or directory'
    [[ $output =~ $r ]]

    # correct bind followed by destination does not exist
    run ch-run -b $IMGDIR/bind1 -b $IMGDIR/bind2:/goops $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    r='^ch-run: could not bind .+/bind2 to /goops: No such file or directory'
    [[ $output =~ $r ]]
}

@test 'ch-run --cd' {
    # Default initial working directory is /.
    run ch-run $CHTEST_IMG -- pwd
    echo "$output"
    [[ $status -eq 0 ]]
    [[ $output = '/' ]]

    # Specify initial working directory.
    run ch-run --cd /dev $CHTEST_IMG -- pwd
    echo "$output"
    [[ $status -eq 0 ]]
    [[ $output = '/dev' ]]

    # Error if directory does not exist.
    run ch-run --cd /goops $CHTEST_IMG -- true
    echo "$output"
    [[ $status -eq 1 ]]
    [[ $output =~ "ch-run: can't cd to /goops: No such file or directory" ]]
}

@test '/usr/bin/ch-ssh' {
    ls -l $CH_BIN/ch-ssh
    ch-run $CHTEST_IMG -- ls -l /usr/bin/ch-ssh
    ch-run $CHTEST_IMG -- test -x /usr/bin/ch-ssh
    host_size=$(stat -c %s $CH_BIN/ch-ssh)
    guest_size=$(ch-run $CHTEST_IMG -- stat -c %s /usr/bin/ch-ssh)
    echo "host: $host_size, guest: $guest_size"
    [[ $host_size -eq $guest_size ]]
}

@test 'permissions test directories exist' {
    if [[ $CH_TEST_PERMDIRS = skip ]]; then
        skip
    fi
    for d in $CH_TEST_PERMDIRS; do
        d=$d/perms_test
        echo $d
        test -d $d
        test -d $d/pass
        test -f $d/pass/file
        test -d $d/nopass
        test -d $d/nopass/dir
        test -f $d/nopass/file
    done
}

@test 'relative path to image' {
   # bug number 6
   DIRNAME=$(dirname $CHTEST_IMG)
   BASEDIR=$(basename $CHTEST_IMG)
   cd $DIRNAME && ch-run $BASEDIR -- true
}

@test 'symlink to image' {
   # bug number 50
   ln -s $CHTEST_IMG $BATS_TMPDIR/symlink-test
   ch-run $BATS_TMPDIR/symlink-test -- true
}
