Add support to run docker based functional tests

This change adds the support to run functional tests in a docker based
environment. The test can be executed from the gerrit directory(in-tree)
using the below command:

bazel test plugins/remote-gerrit-account-cache:docker-tests

Change-Id: I3331bcb31592e92b5e485c5e9ce89703f745e499
diff --git a/BUILD b/BUILD
index 05321c7..0fb030b 100644
--- a/BUILD
+++ b/BUILD
@@ -28,3 +28,16 @@
     size = "small",
     srcs = ["src/test/always_pass_test.sh"],
 )
+
+sh_test(
+    name = "docker-tests",
+    size = "medium",
+    srcs = ["test/docker/run.sh"],
+    args = [
+        "--remote-gerrit-account-cache-jar",
+        "$(location :remote-gerrit-account-cache)",
+    ],
+    data = [plugin_name] + glob(["test/**"]),
+    local = True,
+    tags = ["docker"],
+)
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
new file mode 100755
index 0000000..da93273
--- /dev/null
+++ b/test/docker/docker-compose.yaml
@@ -0,0 +1,35 @@
+services:
+
+  remote-gerrit:
+    build:
+      context: remote-gerrit
+    networks:
+      - gerrit-net
+
+  gerrit:
+    build:
+      context: gerrit
+      args:
+        REMOTE_GERRIT_BASE_URL: "http://remote-gerrit:8080"
+        HTTP_USERNAME: "admin"
+        HTTP_PASSWORD: "secret"
+    networks:
+      - gerrit-net
+    depends_on:
+      - remote-gerrit
+
+  run_tests:
+    build:
+      context: run_tests
+    networks:
+      - gerrit-net
+    depends_on:
+      - remote-gerrit
+      - gerrit
+    environment:
+      - REMOTE_GERRIT_HOST=remote-gerrit
+      - GERRIT_HOST=gerrit
+
+networks:
+  gerrit-net:
+    driver: bridge
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
new file mode 100755
index 0000000..a248447
--- /dev/null
+++ b/test/docker/gerrit/Dockerfile
@@ -0,0 +1,14 @@
+FROM gerritcodereview/gerrit:3.12.0-ubuntu24
+
+ARG REMOTE_GERRIT_BASE_URL
+ARG HTTP_USERNAME
+ARG HTTP_PASSWORD
+
+ENV GERRIT_SITE=/var/gerrit
+RUN rm -rf "$GERRIT_SITE/plugins" && mkdir "$GERRIT_SITE/plugins"
+COPY etc/ "$GERRIT_SITE/etc/"
+RUN touch "$GERRIT_SITE"/.firstTimeRedirect
+
+COPY artifacts /tmp/
+RUN cp /tmp/remote-gerrit-account-cache.jar "$GERRIT_SITE/lib/remote-gerrit-account-cache.jar"
+RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
\ No newline at end of file
diff --git a/test/docker/gerrit/etc/gerrit.config b/test/docker/gerrit/etc/gerrit.config
new file mode 100644
index 0000000..f604cb2
--- /dev/null
+++ b/test/docker/gerrit/etc/gerrit.config
@@ -0,0 +1,34 @@
+[auth]
+    type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+
+[cache]
+    directory = cache
+
+[container]
+    user = gerrit
+
+[gerrit]
+    basePath = git
+    canonicalWebUrl = http://gerrit:8080/
+    installModule = com.googlesource.gerrit.plugins.remotegerritaccountcache.AccountCacheImpl$AccountCacheModule
+
+[httpd]
+    listenUrl = http://*:8080/
+
+[index]
+    type = LUCENE
+
+[plugins]
+    allowRemoteAdmin = true
+
+[remote-gerrit-account-cache]
+    remoteGerritBaseUrl = http://remote-gerrit:8080
+    httpUsername = admin
+    httpPassword = secret
+
+[sendemail]
+    smtpServer = localhost
+    enable = false
+
+[sshd]
+    listenAddress = *:29418
diff --git a/test/docker/remote-gerrit/Dockerfile b/test/docker/remote-gerrit/Dockerfile
new file mode 100755
index 0000000..85bd0ca
--- /dev/null
+++ b/test/docker/remote-gerrit/Dockerfile
@@ -0,0 +1,9 @@
+FROM gerritcodereview/gerrit:3.12.0-ubuntu24
+
+ENV GERRIT_SITE=/var/gerrit
+RUN rm -rf "$GERRIT_SITE/plugins" && mkdir "$GERRIT_SITE/plugins"
+COPY etc/ "$GERRIT_SITE/etc/"
+RUN touch "$GERRIT_SITE"/.firstTimeRedirect
+
+COPY artifacts /tmp/
+RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
\ No newline at end of file
diff --git a/test/docker/remote-gerrit/etc/gerrit.config b/test/docker/remote-gerrit/etc/gerrit.config
new file mode 100644
index 0000000..ff0b85d
--- /dev/null
+++ b/test/docker/remote-gerrit/etc/gerrit.config
@@ -0,0 +1,28 @@
+[auth]
+    type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+
+[cache]
+    directory = cache
+
+[container]
+    user = gerrit
+
+[gerrit]
+    basePath = git
+    canonicalWebUrl = http://gerrit:8080/
+
+[httpd]
+    listenUrl = http://*:8080/
+
+[index]
+    type = LUCENE
+
+[plugins]
+    allowRemoteAdmin = true
+
+[sendemail]
+    smtpServer = localhost
+    enable = false
+
+[sshd]
+    listenAddress = *:29418
diff --git a/test/docker/run.sh b/test/docker/run.sh
new file mode 100755
index 0000000..28dacca
--- /dev/null
+++ b/test/docker/run.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+
+readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+MYPROG=$(basename -- "$0")
+GERRIT_ARTIFACTS=$MYDIR/gerrit/artifacts
+REMOTE_GERRIT_ARTIFACTS=$MYDIR/remote-gerrit/artifacts
+
+die() { echo -e "\nERROR:" "$@" ; kill $$ ; exit 1 ; } # error_message
+
+progress() { # message cmd [args]...
+    local message=$1 ; shift
+    echo -n "$message"
+    "$@" &
+    local pid=$!
+    while kill -0 $pid 2> /dev/null ; do
+        echo -n "."
+        sleep 2
+    done
+    echo
+    wait "$pid"
+}
+
+usage() { # [error_message]
+    cat <<-EOF
+Usage:
+    $MYPROG [--remote-gerrit-account-cache-jar|-r <FILE_PATH>]
+            [--gerrit-war|-g <FILE_PATH>]
+
+    This tool runs the functional tests in a Docker environment built
+    from the gerritcodereview/gerrit base Docker image.
+
+    The remote-gerrit-account-cache JAR and optionally a Gerrit WAR are
+    expected to be in the $GERRIT_ARTIFACTS dir; however, the
+     --remote-gerrit-account-cache-jar and --gerrit-war switches may be
+     used as helpers to specify which files to copy there.
+
+    Options:
+    --help|-h
+    --gerrit-war|-g                           path to Gerrit WAR file
+    --remote-gerrit-account-cache-jar|-r      path to remote-gerrit-account-cache JAR file
+    --preserve                                To preserve the docker setup for debugging
+
+EOF
+
+    [ -n "$1" ] && echo -e "\nERROR: $1" && exit 1
+    exit 0
+}
+
+check_prerequisite() {
+    docker --version > /dev/null || die "docker is not installed"
+    docker compose version > /dev/null || die "docker compose is not installed"
+}
+
+build_images() {
+    docker compose "${COMPOSE_ARGS[@]}" build --quiet
+}
+
+execute_tests() {
+    docker compose "${COMPOSE_ARGS[@]}" up --detach
+    local run_tests_container="$(docker compose "${COMPOSE_ARGS[@]}" ps -q run_tests | \
+        xargs docker inspect --format '{{.Name}}' | sed 's|/||')"
+    docker cp "$MYDIR"/../../test "$run_tests_container":/
+    docker compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
+        '/test/docker/run_tests/start.sh'
+}
+
+get_run_test_container() {
+    docker compose "${COMPOSE_ARGS[@]}" ps | grep run_tests | awk '{ print $1 }'
+}
+
+cleanup() {
+    if [ "$PRESERVE" = "true" ] ; then
+        echo "Preserving the following docker setup"
+        docker compose "${COMPOSE_ARGS[@]}" ps
+        echo ""
+        echo "To exec into run_tests container, use following command:"
+        echo "docker exec -it $(get_run_test_container) /bin/bash"
+        echo ""
+        echo "Run the following command to bring down the setup:"
+        echo "docker compose" "${COMPOSE_ARGS[@]}" "down -v --rmi local"
+    else
+        docker compose "${COMPOSE_ARGS[@]}" down -v --rmi local 2>/dev/null
+    fi
+    rm -rf "$GERRIT_ARTIFACTS" "$REMOTE_GERRIT_ARTIFACTS"
+}
+
+PRESERVE="false"
+COMPOSE_ARGS=()
+while (( "$#" )) ; do
+    case "$1" in
+        --help|-h)                                usage ;;
+        --gerrit-war|-g)                          shift ; GERRIT_WAR=$1 ;;
+        --remote-gerrit-account-cache-jar|-r)     shift ; REMOTE_GERRIT_ACCOUNT_CACHE_JAR=$1 ;;
+        --preserve)                               PRESERVE="true" ;;
+        --compose-arg)                            shift ; COMPOSE_ARGS+=("$1") ;;
+        *)                                        usage "invalid argument $1" ;;
+    esac
+    shift
+done
+
+PROJECT_NAME="remote-gerrit-account-cache_$$"
+COMPOSE_YAML="$MYDIR/docker-compose.yaml"
+COMPOSE_ARGS=(--project-name "$PROJECT_NAME" -f "$COMPOSE_YAML")
+check_prerequisite
+mkdir -p -- "$GERRIT_ARTIFACTS" "$REMOTE_GERRIT_ARTIFACTS"
+
+[ -n "$REMOTE_GERRIT_ACCOUNT_CACHE_JAR" ] \
+    && cp -f -- "$REMOTE_GERRIT_ACCOUNT_CACHE_JAR" "$GERRIT_ARTIFACTS/remote-gerrit-account-cache.jar"
+if [ ! -e "$GERRIT_ARTIFACTS/remote-gerrit-account-cache.jar" ] ; then
+    MISSING="Missing $GERRIT_ARTIFACTS/remote-gerrit-account-cache.jar"
+    [ -n "$REMOTE_GERRIT_ACCOUNT_CACHE_JAR" ] && die "$MISSING, check for copy failure?"
+    usage "$MISSING, did you forget --remote-gerrit-account-cache-jar?"
+fi
+
+[ -n "$GERRIT_WAR" ] && cp -f -- "$GERRIT_WAR" "$GERRIT_ARTIFACTS/gerrit.war"
+[ -n "$GERRIT_WAR" ] && cp -f -- "$GERRIT_WAR" "$REMOTE_GERRIT_ARTIFACTS/gerrit.war"
+( trap cleanup EXIT SIGTERM
+    progress "Building docker images" build_images
+    execute_tests
+)
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
new file mode 100755
index 0000000..8dd1441
--- /dev/null
+++ b/test/docker/run_tests/Dockerfile
@@ -0,0 +1,17 @@
+FROM alpine:3.16
+
+ARG UID=1001
+ARG GID=1001
+ENV USER admin
+ENV USER_HOME /home/$USER
+ENV RUN_TESTS_DIR test/docker/run_tests
+
+RUN apk --update add --no-cache bash shadow curl jq
+
+RUN groupadd -f -g $GID users2
+RUN useradd -u $UID -g $GID $USER
+RUN mkdir -p $USER_HOME
+RUN chown -R $USER $USER_HOME
+
+USER $USER
+ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
new file mode 100755
index 0000000..334d1ad
--- /dev/null
+++ b/test/docker/run_tests/start.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+die() { echo "ERROR: $1" >&2 ; exit 1 ; } # error_msg
+
+USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
+cp -r /test "$USER_HOME"
+
+"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$REMOTE_GERRIT_HOST":8080 -t 60 || \
+    die "Failed to start remote gerrit"
+
+echo "remote gerrit is up"
+
+"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":8080 -t 60 || \
+    die "Failed to start gerrit"
+
+echo "gerrit is up"
+
+echo "Running tests ..."
+
+RESULT=0
+
+"$USER_RUN_TESTS_DIR"/../../test_remote_gerrit_account_cache.sh \
+    --remote-gerrit "$REMOTE_GERRIT_HOST" --gerrit "$GERRIT_HOST" || RESULT=1
+
+exit $RESULT
diff --git a/test/docker/run_tests/wait-for-it.sh b/test/docker/run_tests/wait-for-it.sh
new file mode 100755
index 0000000..17436f2
--- /dev/null
+++ b/test/docker/run_tests/wait-for-it.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+# https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh
+#   Use this script to test if a given TCP host/port are available
+
+cmdname=$(basename -- "$0")
+
+echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+    cat << USAGE >&2
+Usage:
+    $cmdname host:port [-s] [-t timeout] [-- command args]
+    -h HOST | --host=HOST       Host or IP under test
+    -p PORT | --port=PORT       TCP port under test
+                                Alternatively, you specify the host and port as host:port
+    -s | --strict               Only execute subcommand if the test succeeds
+    -q | --quiet                Don't output any status messages
+    -t TIMEOUT | --timeout=TIMEOUT
+                                Timeout in seconds, zero for no timeout
+    -- COMMAND ARGS             Execute command with args after the test finishes
+USAGE
+    exit 1
+}
+
+wait_for()
+{
+    if [[ $TIMEOUT -gt 0 ]]; then
+        echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
+    else
+        echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
+    fi
+    start_ts=$(date +%s)
+    while :
+    do
+        (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
+        result=$?
+        if [[ $result -eq 0 ]]; then
+            end_ts=$(date +%s)
+            echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
+            break
+        fi
+        sleep 1
+    done
+    return $result
+}
+
+wait_for_wrapper()
+{
+    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+    if [[ $QUIET -eq 1 ]]; then
+        timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    else
+        timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    fi
+    PID=$!
+    trap "kill -INT -$PID" INT
+    wait $PID
+    RESULT=$?
+    if [[ $RESULT -ne 0 ]]; then
+        echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
+    fi
+    return $RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+    case "$1" in
+        *:* )
+        hostport=(${1//:/ })
+        HOST=${hostport[0]}
+        PORT=${hostport[1]}
+        shift 1
+        ;;
+        --child)
+        CHILD=1
+        shift 1
+        ;;
+        -q | --quiet)
+        QUIET=1
+        shift 1
+        ;;
+        -s | --strict)
+        STRICT=1
+        shift 1
+        ;;
+        -h)
+        HOST="$2"
+        if [[ $HOST == "" ]]; then break; fi
+        shift 2
+        ;;
+        --host=*)
+        HOST="${1#*=}"
+        shift 1
+        ;;
+        -p)
+        PORT="$2"
+        if [[ $PORT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --port=*)
+        PORT="${1#*=}"
+        shift 1
+        ;;
+        -t)
+        TIMEOUT="$2"
+        if [[ $TIMEOUT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --timeout=*)
+        TIMEOUT="${1#*=}"
+        shift 1
+        ;;
+        --)
+        shift
+        CLI="$@"
+        break
+        ;;
+        --help)
+        usage
+        ;;
+        *)
+        echoerr "Unknown argument: $1"
+        usage
+        ;;
+    esac
+done
+
+if [[ "$HOST" == "" || "$PORT" == "" ]]; then
+    echoerr "Error: you need to provide a host and port to test."
+    usage
+fi
+
+TIMEOUT=${TIMEOUT:-15}
+STRICT=${STRICT:-0}
+CHILD=${CHILD:-0}
+QUIET=${QUIET:-0}
+
+if [[ $CHILD -gt 0 ]]; then
+    wait_for
+    RESULT=$?
+    exit $RESULT
+else
+    if [[ $TIMEOUT -gt 0 ]]; then
+        wait_for_wrapper
+        RESULT=$?
+    else
+        wait_for
+        RESULT=$?
+    fi
+fi
+
+if [[ $CLI != "" ]]; then
+    if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
+        echoerr "$cmdname: strict mode, refusing to execute subprocess"
+        exit $RESULT
+    fi
+    exec $CLI
+else
+    exit $RESULT
+fi
diff --git a/test/lib_helpers.sh b/test/lib_helpers.sh
new file mode 100755
index 0000000..22a3911
--- /dev/null
+++ b/test/lib_helpers.sh
@@ -0,0 +1,9 @@
+# ---- Low level execution helpers ----
+
+die() { echo -e "$@" ; exit 1 ; } # error_message
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
+
+gcurl() { # [args...]
+    curl --silent --user 'admin:secret' "$@" | sed -e '1!b' -e "/^)]}'$/d"
+}
\ No newline at end of file
diff --git a/test/lib_result.sh b/test/lib_result.sh
new file mode 100755
index 0000000..97b47c6
--- /dev/null
+++ b/test/lib_result.sh
@@ -0,0 +1,38 @@
+# ---- TEST RESULTS ----
+
+RESULT=0
+
+result() { # test [error_message]
+    local result=$?
+    local outcome="FAIL"
+    if [ $result -eq 0 ] ; then
+        echo "PASSED - $1 test"
+        outcome="PASS"
+    else
+        echo "*** FAILED *** - $1 test"
+        RESULT=$result
+        [ $# -gt 1 ] && echo "$2"
+    fi
+}
+
+# actual output must match expected to pass
+result_out() { # test expected actual
+    local disp=$(echo "Expected Output:" ;\
+                 echo "    $2" ;\
+                 echo "Actual Output:" ;\
+                 echo "    $3")
+
+    [ "$2" = "$3" ]
+    result "$1" "$disp"
+}
+
+result_sortize_json() {  # json
+    local json=$(echo "$1" | jq --indent 3 --sort-keys 2> /dev/null)
+    [ $? -ne 0 ] && json=$1
+    echo "$json"
+}
+
+result_out_json() { # test expected_json actual_json
+    local expected=$(result_sortize_json "$2") actual=$(result_sortize_json "$3")
+    result_out "$1" "$expected" "$actual" "Sorted JSON"
+}
\ No newline at end of file
diff --git a/test/test_remote_gerrit_account_cache.sh b/test/test_remote_gerrit_account_cache.sh
new file mode 100755
index 0000000..7aa5550
--- /dev/null
+++ b/test/test_remote_gerrit_account_cache.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+
+source "$MYDIR/lib_helpers.sh"
+source "$MYDIR/lib_result.sh"
+
+HTTP_PORT=8080
+REMOTE_GERRIT_BASE_URL="http://$REMOTE_GERRIT_HOST:$HTTP_PORT"
+GERRIT_BASE_URL="http://$GERRIT_HOST:$HTTP_PORT"
+
+q gcurl --header "Content-Type: application/json" \
+    --request PUT "$REMOTE_GERRIT_BASE_URL/a/accounts/user1" \
+    --data @<(cat <<EOF
+{
+  "email": "user1@example.com"
+}
+EOF
+)
+q gcurl --request PUT "$GERRIT_BASE_URL/a/accounts/user1"
+ACTUAL=$(gcurl --request GET "$GERRIT_BASE_URL/a/accounts/user1")
+EXPECTED=$(cat <<EOF
+{
+   "_account_id": 1000001,
+   "email": "user1@example.com",
+   "name": "user1",
+   "username": "user1"
+}
+EOF
+)
+result_out_json "Email added on remote gerrit site is available on the internal site" \
+    "$EXPECTED" "$ACTUAL"
+exit "$RESULT"
\ No newline at end of file