#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -e

SYSUSERS="${1:-systemd-sysusers}"

# shellcheck disable=SC1090
[ -e "$(dirname "$0")/../systemd-runtest.env" ] && . "$(dirname "$0")/../systemd-runtest.env"
SYSTEMD_TEST_DATA=${SYSTEMD_TEST_DATA:-"/usr/lib/systemd/tests/testdata"}
SOURCE=$SYSTEMD_TEST_DATA/test-sysusers

TESTDIR=$(mktemp --tmpdir --directory "test-sysusers.XXXXXXXXXX")
# shellcheck disable=SC2064
trap "rm -rf '$TESTDIR'" EXIT INT QUIT PIPE

skip_nis() {
    # musl seems to not support NIS entries. Let's skip the test case for NIS entries.
    local path=${1:?}
    [[ "${SYSTEMD_LIBC:-}" == musl && "${path##*/}" == "test-11.input" ]]
}

prepare_testdir() {
    mkdir -p "$TESTDIR/etc/sysusers.d/"
    mkdir -p "$TESTDIR/usr/lib/sysusers.d/"
    rm -f "$TESTDIR"/etc/*{passwd,group,shadow}
    for i in $1.initial-{passwd,group,shadow}; do
        test -f "$i" && cp "$i" "$TESTDIR/etc/${i#*.initial-}"
    done
    return 0
}

# shellcheck disable=SC2050
[ 999 -lt 999 ] && system_guid_max=999 || system_guid_max=999

preprocess() {
    m=${2:-$system_guid_max}

    # shellcheck disable=SC2140
    sed -e "s/SYSTEM_UGID_MAX/$m/g;
            s#NOLOGIN#"/usr/bin/nologin"#g" "$1"
}

compare() {
    if ! diff -u "$TESTDIR/etc/passwd" <(preprocess "$1.expected-passwd" "$3") >&2; then
        echo >&2 "**** Unexpected output for $f $2"
        exit 1
    fi

    if ! diff -u "$TESTDIR/etc/group" <(preprocess "$1.expected-group" "$3") >&2; then
        echo >&2 "**** Unexpected output for $f $2"
        exit 1
    fi
}

rm -f "$TESTDIR"/etc/sysusers.d/* "$TESTDIR"/usr/lib/sysusers.d/*

# happy tests
for f in $(find "$SOURCE"/test-*.input | sort -V); do
    skip_nis "$f" && continue
    echo "*** Running $f"
    prepare_testdir "${f%.input}"
    cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
    "$SYSUSERS" --root="$TESTDIR"

    compare "${f%.*}" ""
done

for f in $(find "$SOURCE"/test-*.input | sort -V); do
    skip_nis "$f" && continue
    echo "*** Running $f on stdin"
    prepare_testdir "${f%.input}"
    touch "$TESTDIR/etc/sysusers.d/test.conf"
    "$SYSUSERS" --root="$TESTDIR" - <"$f"

    compare "${f%.*}" "on stdin"
done

for f in $(find "$SOURCE"/test-*.input | sort -V); do
    skip_nis "$f" && continue
    echo "*** Running $f on stdin with --replace"
    prepare_testdir "${f%.input}"
    touch "$TESTDIR/etc/sysusers.d/test.conf"
    # this overrides test.conf which is masked on disk
    "$SYSUSERS" --root="$TESTDIR" --replace=/etc/sysusers.d/test.conf - <"$f"
    # this should be ignored
    "$SYSUSERS" --root="$TESTDIR" --replace=/usr/lib/sysusers.d/test.conf - <"$SOURCE/test-1.input"

    compare "${f%.*}" "on stdin with --replace"
done

# test --inline
echo "*** Testing --inline"
prepare_testdir "$SOURCE/inline"
# copy a random file to make sure it is ignored
cp "$f" "$TESTDIR/etc/sysusers.d/confuse.conf"
"$SYSUSERS" --root="$TESTDIR" --inline \
            "u     u1   222 -     - /bin/zsh" \
            "g     g1   111"

compare "$SOURCE/inline" "(--inline)"

# test --replace
echo "*** Testing --inline with --replace"
prepare_testdir "$SOURCE/inline"
# copy a random file to make sure it is ignored
cp "$f" "$TESTDIR/etc/sysusers.d/confuse.conf"
"$SYSUSERS" --root="$TESTDIR" \
            --inline \
            --replace=/etc/sysusers.d/confuse.conf \
            "u     u1   222 -     - /bin/zsh" \
            "g     g1   111"

compare "$SOURCE/inline" "(--inline --replace=…)"

echo "*** Testing --inline with no /etc"
rm -rf "${TESTDIR:?}/etc"
"$SYSUSERS" --root="$TESTDIR" --inline \
            "u     u1   222 -     - /bin/zsh" \
            "g     g1   111"

compare "$SOURCE/inline" "(--inline)"

rm -f "$TESTDIR"/etc/sysusers.d/* "$TESTDIR"/usr/lib/sysusers.d/*

cat >"$TESTDIR/etc/login.defs" <<EOF
SYS_UID_MIN abcd
SYS_UID_MAX abcd
SYS_GID_MIN abcd
SYS_GID_MAX abcd
SYS_UID_MIN 401
SYS_UID_MAX 555
SYS_GID_MIN 405
SYS_GID_MAX 666
SYS_UID_MIN abcd
SYS_UID_MAX abcd
SYS_GID_MIN abcd
SYS_GID_MAX abcd
SYS_UID_MIN999
SYS_UID_MAX999
SYS_GID_MIN999
SYS_GID_MAX999
EOF

for f in $(find "$SOURCE"/test-*.input | sort -V); do
    skip_nis "$f" && continue
    echo "*** Running $f (with login.defs)"
    prepare_testdir "${f%.input}"
    cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
    "$SYSUSERS" --root="$TESTDIR"

    # shellcheck disable=SC2050
    [ 0 = 1 ] && bound=555 || bound=$system_guid_max
    compare "${f%.*}" "(with login.defs)" "$bound"
done

rm -f "$TESTDIR"/etc/sysusers.d/* "$TESTDIR"/usr/lib/sysusers.d/*

mv "$TESTDIR/etc/login.defs" "$TESTDIR/etc/login.defs.moved"
ln -s ../../../../../etc/login.defs.moved "$TESTDIR/etc/login.defs"

for f in $(find "$SOURCE"/test-*.input | sort -V); do
    skip_nis "$f" && continue
    echo "*** Running $f (with login.defs symlinked)"
    prepare_testdir "${f%.input}"
    cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
    "$SYSUSERS" --root="$TESTDIR"

    # shellcheck disable=SC2050
    [ 0 = 1 ] && bound=555 || bound=$system_guid_max
    compare "${f%.*}" "(with login.defs symlinked)" "$bound"
done

rm -f "$TESTDIR"/etc/sysusers.d/* "$TESTDIR"/usr/lib/sysusers.d/*

preprocess_error() {
    # Convert error message for ERANGE from glibc to musl.
    if [[ "${SYSTEMD_LIBC:-}" == musl ]]; then
        sed -e 's/Numerical result out of range/Result not representable/' "${1:?}"
    else
        cat "${1:?}"
    fi
}

# tests for error conditions
for f in $(find "$SOURCE"/unhappy-*.input | sort -V); do
    echo "*** Running test $f"
    prepare_testdir "${f%.input}"
    cp "$f" "$TESTDIR/usr/lib/sysusers.d/test.conf"
    SYSTEMD_LOG_LEVEL=info "$SYSUSERS" --root="$TESTDIR" 2>&1 | tail -n1 | sed -r 's/^[^:]+:[^:]+://' >"$TESTDIR/err"
    if ! diff -u "$TESTDIR/err"  <(preprocess_error "${f%.*}.expected-err") >&2; then
        echo >&2 "**** Unexpected error output for $f"
        cat >&2 "$TESTDIR/err"
        exit 1
    fi
done
