Add tests for task visibility when previewing config updates

Tests for following kinds of updates are added. In each of these, we
assert that only tasks on refs visible to the current user are shown
and remaining tasks are shown as 'unknown' name and status.

1. --task-preview root file with subtasks-external pointing
   to secret user ref.
2. --task-preview root file with subtasks-external pointing
   to a non-secret user ref with subtasks-external pointing
   to a secret user ref.
3. --task-preview a non-secret user ref with subtasks-external
   pointing to secret user ref.
4. --task-preview a new root, original root with subtasks-external
   pointing to secret user ref.

Since helper functions are common with already existing test suite,
create a generic helper script in order to avoid duplication.

Originally-Authored-By: Adithya Chakilam
Change-Id: I6283f2d1f2f65f4f9abebe272690a8ccbf975e83
diff --git a/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
new file mode 100644
index 0000000..3d1d7cc
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview a new root, original root with subtasks-external pointing to secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root with SECRET external"]
+     applicable = is:open
+     subtasks-external = SECRET
+
+ [external "SECRET"]
+     user = {secret_user}
+     file = secret.config
++
++[root "Root Preview Simple"]
++    subtask = simple task
+
++[task "simple task"]
++    applicable = is:open
++    pass = True
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root with SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",            # Only Test Suite: non-secret
+         "status" : "UNKNOWN"           # Only Test Suite: non-secret
+         "applicable" : true,           # Only Test Suite: secret
+         "hasPass" : true,              # Only Test Suite: secret
+         "name" : "SECRET task",        # Only Test Suite: secret
+         "status" : "READY"             # Only Test Suite: secret
+      }
+   ]
+}
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Preview Simple",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "simple task",
+         "status" : "PASS"
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
new file mode 100644
index 0000000..d192050
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview a non-secret user ref with subtasks-external pointing to secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Root for NON-SECRET external Preview with SECRET external"]
+    applicable = "is:open"
+    pass = True
+    subtasks-external = NON-SECRET
+
+[external "NON-SECRET"]
+    user = {non_secret_user}
+    file = sample.config
+```
+
+file: `All-Users:{non_secret_user_ref}:task/sample.config`
+```
+ [task "NON-SECRET task"]
+     applicable = is:open
+     pass = Fail
++    subtasks-external = SECRET
+
++[external "SECRET"]
++    user = {secret_user}
++    file = secret.config
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root for NON-SECRET external Preview with SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET task",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
new file mode 100644
index 0000000..c0add08
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview root file with subtasks-external pointing to a non-secret user ref with subtasks-external pointing to a secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview NON-SECRET external with SECRET external"]
+     applicable = "is:open"
+     pass = True
++    subtasks-external = NON-SECRET with SECRET External
+
++[external "NON-SECRET with SECRET External"]
++    user = {non_secret_user}
++    file = secret_external.config
+```
+
+file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET external"]
+    applicable = is:open
+    pass = True
+    subtasks-external = SECRET external
+
+[external "SECRET external"]
+    user = {secret_user}
+    file = secret.config
+```
+
+file: `All-Users:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview NON-SECRET external with SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET with SECRET external",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
new file mode 100644
index 0000000..4d81b12
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
@@ -0,0 +1,40 @@
+# --task-preview root file with subtasks-external pointing to secret user ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external"]
+     applicable = is:open
+     pass = True
++    subtasks-external = SECRET external
+
++[external "SECRET external"]
++    user = {secret_user}
++    file = secret.config
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET Task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview SECRET external",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",                  # Only Test Suite: non-secret
+         "status" : "UNKNOWN"                 # Only Test Suite: non-secret
+         "applicable" : true,                 # Only Test Suite: secret
+         "hasPass" : true,                    # Only Test Suite: secret
+         "name" : "SECRET Task",              # Only Test Suite: secret
+         "status" : "READY"                   # Only Test Suite: secret
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index dd33b11..d5c965e 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -17,306 +17,6 @@
 # Usage:
 # All-Projects.git - must have 'Push' rights on refs/meta/config
 
-# ---- TEST RESULTS ----
-result() { # test [error_message]
-    local result=$?
-    if [ $result -eq 0 ] ; then
-        echo "PASSED - $1 test"
-    else
-        echo "*** FAILED *** - $1 test"
-        RESULT=$result
-        [ $# -gt 1 ] && echo "$2"
-    fi
-}
-
-# output must match expected to pass
-result_out() { # test expected actual
-    local name=$1 expected=$2 actual=$3
-
-    [ "$expected" = "$actual" ]
-    result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
-}
-
-result_root() { # group root
-    local name="$1 - $(echo "$2" | sed -es'/Root //')"
-    result_out "$name" "${EXPECTED_ROOTS[$2]}" "${OUTPUT_ROOTS[$2]}"
-}
-
-# -------- Git Config
-
-config() { git config -f "$CONFIG" "$@" ; } # [args]...
-config_section_keys() { # section > keys ...
-    # handlers.handler-filter filter.sh -> handler-filter
-    config -l --name-only |\
-        grep "^$1\." | \
-        sed -es"/^$1\.//;s/\..*$//" |\
-        awk '$0 != prev ; {prev = $0}'
-}
-
-# -------- Pre JSON --------
-#
-# pre_json is a "templated json" used in the test docs to express test results. It looks
-# like json but has some extra comments to express when a certain output should be used.
-# These comments look like: "# Only Test Suite: <suite>"
-#
-
-remove_suites() { # suites... < pre_json > json
-    grep -vE "# Only Test Suite: ($(echo "$@" | sed "s/ /|/g"))" | \
-         sed -e's/# Only Test Suite:.*$//; s/ *$//'
-}
-
-remove_not_suite() { remove_suites !"$1" ; } # suite < pre_json > json
-
-# -------- Test Doc Format --------
-#
-# Test Doc Format has intermixed git config task definitions with json roots. This
-# makes it easy to define tests close to their outputs. Be aware that all of the
-# config will get consolidated into a single file, so non root config will be shared
-# amongst all the roots.
-#
-
-# Sample Test Doc for 2 roots:
-#
-# [root "Root PASS"]
-#   pass = True
-#
-# {
-#    "applicable" : true,
-#    "hasPass" : true,
-#    "name" : "Root PASS",
-#    "status" : "PASS"
-# }
-#
-# [root "Root FAIL"]
-#   fail = True
-#
-# {
-#    <other root>
-# }
-
-# Strip the json from Test Doc formatted text. For the sample above, the output would be:
-#
-# [root "Root PASS"]
-#   pass = True
-#
-# [root "Root FAIL"]
-#   fail = True
-# ...
-#
-testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config
-
-# Strip the git config from Test Doc formatted text. For the sample above, the output would be:
-#
-# { "plugins" : [
-#     { "name" : "task",
-#       "roots" : [
-#         {
-#           "applicable" : true,
-#           "hasPass" : true,
-#           "name" : "Root PASS",
-#           "status" : "PASS"
-#        },
-#        {
-#           <other root>
-#        },
-#    ...
-# }
-testdoc_2_pjson() { # < testdoc_format > pjson_task_roots
-    awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \
-         /^\{/  { open=1 }; \
-         open && end { print "}," ; end=0 }; \
-         /^\}/  { open=0 ; end=1 }; \
-         open; \
-         END   { print "}]}]}" }'
-}
-
-# ---- JSON PARSING ----
-
-json_pp() { # < json > json
-    python -c "import sys, json; \
-            print json.dumps(json.loads(sys.stdin.read()), indent=3, \
-            separators=(',', ' : '), sort_keys=True)"
-}
-
-json_val_by() { # json index|'key' > value
-    echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]"
-}
-json_val_by_key() { json_val_by "$1" "'$2'" ; }  # json key > value
-
-# --------
-
-gssh() {  # [-l user] cmd [args]...
-    local user_args=()
-    [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
-    ssh -x -p "$PORT" "${user_args[@]}" "$SERVER" gerrit "$@"
-}
-
-q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
-
-gen_change_id() { echo "I$(uuidgen | sha1sum | awk '{print $1}')"; } # > change_id
-
-commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
-
-err() { echo "ERROR: $1" >&2 ; exit 1 ; }
-
-# Run a test setup command quietly, exit on failure
-q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...]
-
-ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data
-
-set_change() { # change_json
-    { CHANGE=("$(json_val_by_key "$1" number)" \
-        "$(json_val_by_key "$1" id)" \
-        "$(json_val_by_key "$1" project)" \
-        "refs/heads/$(json_val_by_key "$1" branch)" \
-        "$(json_val_by_key "$1" status)" \
-        "$(json_val_by_key "$1" topic)") ; } 2> /dev/null
-}
-
-# change_token change_number change_id project branch status topic < templated_txt > change_txt
-replace_change_properties() {
-    sed -e "s|_change$1_number|$2|g" \
-        -e "s|_change$1_id|$3|g" \
-        -e "s|_change$1_project|$4|g" \
-        -e "s|_change$1_branch|$5|g" \
-        -e "s|_change$1_status|$6|g" \
-        -e "s|_change$1_topic|$7|g"
-}
-
-replace_default_changes() {
-    replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
-}
-
-replace_user() { # < text_with_testuser > text_with_$USER
-    sed -e"s/testuser/$USER/"
-}
-
-get_user_ref() { # username > refs/users/<accountidshard>/<accountid>
-    local user_account_id="$(curl --netrc --silent "http://$SERVER:$HTTP_PORT/a/accounts/$1" | \
-    sed -e '1!b' -e "/^)]}'$/d" | jq ._account_id)"
-    echo "refs/users/${user_account_id:(-2)}/$user_account_id"
-}
-
-replace_user_refs() { # < text_with_user_refs > test_with_expanded_user_refs
-    local text="$(< /dev/stdin)"
-    for user in "${!USER_REFS[@]}" ; do
-        text="${text//"$user"/${USER_REFS["$user"]}}"
-    done
-    echo "$text"
-}
-
-replace_tokens() { # < text > text with replacing all tokens(changes, user)
-    replace_default_changes | replace_user_refs | replace_user
-}
-
-strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
-strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
-
-define_jsonByRoot() { # task_plugin_ouptut > jsonByRoot_array_definition
-    local record root=''
-    local -A jsonByRoot
-    while IFS= read -r -d '' record ; do
-        if [ -z "$root" ] ; then
-            root=$record
-        else
-            jsonByRoot[$root]=$record
-            root=''
-        fi
-    done < <(python -c "if True: # NOP to start indent
-        import sys, json
-
-        roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
-        for root in roots:
-            root_json = json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)
-            sys.stdout.write(root['name'] + '\x00' + root_json + '\x00')"
-    )
-
-    local def=$(declare -p jsonByRoot)
-    echo "${def#*=}" # declare -A jsonByRoot='(...)' > '(...)'
-}
-
-get_plugins() { # < change_json > plugins_json
-    python -c "import sys, json; \
-        plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
-        print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
-}
-
-example() { # doc example_num > text_for_example_num
-    echo "$1" | awk '/```/{Q++;E=(Q+1)/2};E=='"$2" | grep -v '```'
-}
-
-get_change_num() { # < gerrit_push_response > changenum
-    local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
-    echo "${url##*\/}" | tr -d -c '[:digit:]'
-}
-
-install_changeid_hook() { # repo
-    local hook=$(git rev-parse --git-dir)/hooks/commit-msg
-    scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
-    chmod +x "$hook"
-}
-
-setup_repo() { # repo remote ref [--initial-commit]
-    local repo=$1 remote=$2 ref=$3 init=$4
-    git init "$repo"
-    (
-        cd "$repo"
-        install_changeid_hook "$repo"
-        git fetch "$remote" "$ref"
-        if ! git checkout FETCH_HEAD ; then
-            if [ "$init" = "--initial-commit" ] ; then
-                git commit --allow-empty -a -m "Initial Commit"
-            fi
-        fi
-    )
-}
-
-update_repo() { # repo remote ref
-    local repo=$1 remote=$2 ref=$3
-    (
-        cd "$repo"
-        git add .
-        git commit -m 'Testing task plugin'
-        git push "$remote" HEAD:"$ref"
-    )
-}
-
-create_repo_change() { # repo remote ref [change_id] > change_num
-    local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
-    (
-        q cd "$repo"
-        uuidgen > file
-        q git add .
-        [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
-        q git commit -m "$msg"
-        git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
-    )
-}
-
-query() {  # [-l user] query > json lines
-  local user_args=()
-  [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
-  gssh "${user_args[@]}" query "$@" --format json
-}
-
-# N < json lines > changeN_json
-change_plugins() { awk "NR==$1" | get_plugins | json_pp ; }
-
-results_suite() { # name expected_file plugins_json
-    local name=$1 expected=$2 actual=$3
-
-    local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected")
-    local -A OUTPUT_ROOTS=$(echo "$actual" | define_jsonByRoot)
-
-    local out root
-    echo "$ROOTS" | while read root ; do
-        result_root "$name" "$root"
-    done
-    out=$(diff "$expected" <(echo "$actual") | head -15)
-    [ -z "$out" ]
-    result "$name - Full Test Suite" "$out"
-}
-
 test_2generated() { # name task_args...
     local name=$1 ; shift
     local out=$(query "$@")
@@ -351,6 +51,9 @@
 readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
 MYDIR=$(dirname -- "$(readlink -f -- "$0")")
 MYPROG=$(basename -- "$0")
+
+source "$MYDIR/lib/lib_helper.sh"
+
 DOCS=$MYDIR/.././src/main/resources/Documentation/test
 OUT=$MYDIR/../target/tests
 
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh
new file mode 100755
index 0000000..553918e
--- /dev/null
+++ b/test/check_task_visibility.sh
@@ -0,0 +1,292 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+MYPROG=$(basename -- "$0")
+
+source "$MYDIR/lib/lib_helper.sh"
+
+# Visibility tests cases are described using a markdown file.
+# Each file has a list of config files specified by file
+# markers. The initial state of task configs is created using
+# them. Only one of the config file has an inline diff. Gerrit
+# change is created by applying that diff to the specified file
+# marker and the expected json is asserted by using that change
+# as an input to the '--task-preview' switch.
+
+# The syntax for inline diff is similar to  diff --unified=MAX_INT.
+# All lines start with a leading space and if a specific line is
+# part of diff, we use diff indicators (+/-) instead of a leading
+# space.
+
+# Example markdown file:
+# (Using block comment to better understand the file syntax.)
+
+: <<'END'
+# Test case description header
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Test root"]
+    applicable = "is:open"
+    pass = True
+```
+
+file: `All-Users:refs/users/some_ref:task/sample.config`
+```
+ [task "NON-SECRET task"]
+     applicable = is:open
+     pass = Fail
++    subtasks-external = SECRET
+
++[external "SECRET"]
++    user = {secret_user}
++    file = secret.config
+```
+
+json:
+```
+{
+   {
+     "some": "example"
+   }
+}
+END
+
+# (For example above)
+# out:
+# `All-Projects.git:refs/meta/config:task.config`
+# `All-Users:refs/users/some_ref:task/sample.config`
+get_file_markers() {
+    echo "$TEST_DOC" | grep -o "^file: .*" | cut -f2 -d'`'
+}
+
+# (For example above)
+# in: `All-Projects.git:refs/meta/config:task.config`
+# out:
+#[root "Test root"]
+#    applicable = "is:open"
+#    pass = True
+#
+# in: json:
+# out :
+# {
+#    {
+#      "some": "example"
+#    }
+# }
+get_marker_content() { # marker
+    local start_line=$(echo "$TEST_DOC" | grep -n "$1" | cut -f1 -d':')
+    echo "$TEST_DOC" | tail -n+"$start_line" | \
+        sed '1,/```/d;/```/,$d' | grep -v '```'
+}
+
+# file_marker > project
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+# out: All-Projects.git
+get_project_from_marker() {
+    echo "$WORKSPACE_DIR/$(echo "$1" | cut -f1 -d':')"
+}
+
+# file_marker > ref
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+# out: refs/meta/config
+get_ref_from_marker() {
+    echo "$1" | cut -f2 -d':'
+}
+
+# file_marker > file
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+#out: task/task.config
+get_file_from_marker() {
+    echo "$1" | cut -f3 -d':'
+}
+
+# Example input for all diff functions:
+#
+#  [root "Root Preview SECRET external"]
+#      applicable = is:open
+#      pass = True
+# -    subtask = Subtask APPLICABLE
+# +    subtasks-external = SECRET external
+#
+# +[external "SECRET external"]
+# +    user = {secret_user}
+# +    file = secret.config
+
+
+# Returns if a config has inline diff or not.
+diff_indicators_present() { # file_content
+    echo "$1" | grep -q "^-\|^+"
+}
+
+# file_content_with_diff_indicators > file_content_with_diff_applied
+# out:
+#[root "Root Preview SECRET external"]
+#    applicable = is:open
+#    pass = True
+#    subtask = Subtask APPLICABLE
+diff_apply() {
+    sed -e '/^-/d' -e 's/^.//'
+}
+
+# file_content_with_diff_indicators > file_content_with_diff_reverted
+# out:
+#[root "Root Preview SECRET external"]
+#    applicable = is:open
+#    pass = True
+#    subtasks-external = SECRET external
+#
+#[external "SECRET external"]
+#    user = {secret_user}
+#    file = secret.config
+diff_revert() {
+    sed -e '/^+/d' -e 's/^.//'
+}
+
+config_ensure() { # config_file_path
+    q git config --list -f "$1" || err "Invalid config file: $1"
+}
+
+get_remote() { # project > remote_url
+    echo "ssh://$SERVER:$PORT/$(basename "$1")"
+}
+
+# Gets json from the preview doc and creates
+# expected json in workspace to assert later.
+create_expected_json() {
+    local json=$(get_marker_content "json:")
+
+    echo "$json" | remove_suites "non-secret" | \
+      testdoc_2_pjson | ensure json_pp > "$EXPECTED_SECRET"
+    echo "$json" | remove_suites "secret" | \
+      testdoc_2_pjson | ensure json_pp > "$EXPECTED_NON_SECRET"
+}
+
+test_preview() { # preview_change_number
+    query --task--all --task--preview "$1,1" "change:1" \
+      | change_plugins 1 > "$ACTUAL_SECRET"
+    query -l "$NON_SECRET_USER" --task--all --task--preview "$1,1" "change:1" \
+      | change_plugins 1 > "$ACTUAL_NON_SECRET"
+
+    ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_SECRET")
+    results_suite "Visibility Secret Test" "$EXPECTED_SECRET" "$( < "$ACTUAL_SECRET" )"
+
+    ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_NON_SECRET")
+    results_suite "Visibility Non-Secret Test" "$EXPECTED_NON_SECRET" "$( < "$ACTUAL_NON_SECRET" )"
+}
+
+init_configs() {
+    for marker in $(get_file_markers) ; do
+        local project="$(get_project_from_marker "$marker")"
+        local ref="$(get_ref_from_marker "$marker")"
+        local file="$(get_file_from_marker "$marker")"
+        local content="$(get_marker_content "$marker")"
+        local tip_content
+
+        q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
+        mkdir -p "$(dirname "$project/$file")"
+
+        if diff_indicators_present "$content" ; then
+            CHANGE_FILE_MARKER=$marker
+            CHANGE_CONTENT=$(echo "$content" | diff_apply)
+            tip_content=$(echo "$content" | diff_revert)
+        else
+            tip_content=$content
+        fi
+
+        echo "$tip_content" > "$project/$file"
+        config_ensure "$project/$file"
+        q_setup update_repo "$project" "$(get_remote "$project")" "$ref"
+    done
+}
+
+test_change() {
+    local project="$(get_project_from_marker "$CHANGE_FILE_MARKER")"
+    local ref="$(get_ref_from_marker "$CHANGE_FILE_MARKER")"
+    local file="$(get_file_from_marker "$CHANGE_FILE_MARKER")"
+    q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
+
+    echo "$CHANGE_CONTENT"  > "$project/$file"
+    config_ensure "$project/$file"
+    local cnum=$(create_repo_change "$project" "$(get_remote "$project")" "$ref")
+
+    create_expected_json
+    test_preview "$cnum"
+}
+
+usage() { # [error_message]
+    cat <<-EOF
+Usage:
+    "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user>
+
+    --help|-h                     help text
+    --server|-s                   gerrit host
+    --non-secret-user             user who doesn't have permission
+                                  to view other user refs.
+EOF
+
+    [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
+    exit 0
+}
+
+while (( "$#" )) ; do
+    case "$1" in
+        --help|-h)                usage ;;
+        --server|-s)              shift ; SERVER=$1 ;;
+        --non-secret-user)        shift ; NON_SECRET_USER=$1 ;;
+        *)                        usage "invalid argument $1" ;;
+    esac
+    shift
+done
+
+[ -z "$SERVER" ] && usage "You must specify --server"
+[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
+
+RESULT=0
+PORT=29418
+HTTP_PORT=8080
+WORKSPACE_DIR=$MYDIR/../target/preview
+EXPECTED_SECRET="$WORKSPACE_DIR/expected-secret"
+EXPECTED_NON_SECRET="$WORKSPACE_DIR/expected-non-secret"
+ACTUAL_SECRET="$WORKSPACE_DIR/actual-secret"
+ACTUAL_NON_SECRET="$WORKSPACE_DIR/actual-non-secret"
+TEST_DOC_DIR="$MYDIR/../src/main/resources/Documentation/test/task-preview/"
+
+declare -A USERS
+declare -A USER_REFS
+USERS["{secret_user}"]="$USER"
+USER_REFS["{secret_user_ref}"]="$(get_user_ref "$USER")"
+USERS["{non_secret_user}"]="$NON_SECRET_USER"
+USER_REFS["{non_secret_user_ref}"]="$(get_user_ref "$NON_SECRET_USER")"
+
+mkdir -p "$WORKSPACE_DIR"
+trap 'rm -rf "$WORKSPACE_DIR"' EXIT
+
+TESTS=(
+"new_root_with_original_with_external_secret_ref.md"
+"non-secret_ref_with_external_secret_ref.md"
+"root_with_external_non-secret_ref_with_external_secret_ref.md"
+"root_with_external_secret_ref.md")
+
+for test in "${TESTS[@]}" ; do
+    TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users)"
+    init_configs
+    test_change
+done
+
+exit $RESULT
\ No newline at end of file
diff --git a/test/docker/run_tests/create-one-time-test-data.sh b/test/docker/run_tests/create-one-time-test-data.sh
index 9d9fcbd..6f5562b 100755
--- a/test/docker/run_tests/create-one-time-test-data.sh
+++ b/test/docker/run_tests/create-one-time-test-data.sh
@@ -30,6 +30,11 @@
           --add access."refs/meta/config".read "group Visible-All-Projects-Config"
       git config -f "project.config" \
           --add capability.viewTaskPaths "group Administrators"
+#     After migrating to version 3.5, it is no longer feasible to assign read permissions to
+#     Administrators for another user's ref. To address this, add the 'accessDatabase' capability,
+#     allowing admins to read the user ref of other users
+      git config -f "project.config" \
+                --add capability.accessDatabase "group Administrators"
       q git add . && q git commit -m "project config update"
       q git push origin HEAD:refs/meta/config
     )
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index 13b829a..1bd5970 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -37,6 +37,13 @@
 
 echo "Running Task plugin tests ..."
 
-cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh \
+RESULT=0
+
+"$USER_RUN_TESTS_DIR"/../../check_task_statuses.sh \
     --server "$GERRIT_HOST" --non-secret-user "$NON_SECRET_USER" \
-    --untrusted-user "$UNTRUSTED_USER"
+    --untrusted-user "$UNTRUSTED_USER" || RESULT=1
+
+"$USER_RUN_TESTS_DIR"/../../check_task_visibility.sh --server "$GERRIT_HOST" \
+    --non-secret-user "$NON_SECRET_USER" || RESULT=1
+
+exit $RESULT
diff --git a/test/docker/run_tests/update-all-users-project.sh b/test/docker/run_tests/update-all-users-project.sh
index ee78a68..e8912c4 100755
--- a/test/docker/run_tests/update-all-users-project.sh
+++ b/test/docker/run_tests/update-all-users-project.sh
@@ -4,9 +4,7 @@
 
 cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":29418/All-Users allusers && cd allusers
 git fetch origin refs/meta/config && git checkout FETCH_HEAD
-git config -f project.config access."refs/users/*".read "group Administrators"
 git config -f project.config access."refs/users/*".push "group Administrators"
-git config -f project.config access."refs/users/*".create "group Administrators"
 
 git config -f project.config access.'refs/users/${shardeduserid}'.read "group Registered Users"
 git config -f project.config access.'refs/users/${shardeduserid}'.push "group Registered Users"
diff --git a/test/lib/lib_helper.sh b/test/lib/lib_helper.sh
new file mode 100644
index 0000000..6ee1b89
--- /dev/null
+++ b/test/lib/lib_helper.sh
@@ -0,0 +1,323 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# ---- TEST RESULTS ----
+result() { # test [error_message]
+    local result=$?
+    if [ $result -eq 0 ] ; then
+        echo "PASSED - $1 test"
+    else
+        echo "*** FAILED *** - $1 test"
+        RESULT=$result
+        [ $# -gt 1 ] && echo "$2"
+    fi
+}
+
+# output must match expected to pass
+result_out() { # test expected actual
+    local name=$1 expected=$2 actual=$3
+
+    [ "$expected" = "$actual" ]
+    result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
+}
+
+result_root() { # group root
+    local name="$1 - $(echo "$2" | sed -es'/Root //')"
+    result_out "$name" "${EXPECTED_ROOTS[$2]}" "${OUTPUT_ROOTS[$2]}"
+}
+
+# -------- Git Config
+
+config() { git config -f "$CONFIG" "$@" ; } # [args]...
+config_section_keys() { # section > keys ...
+    # handlers.handler-filter filter.sh -> handler-filter
+    config -l --name-only |\
+        grep "^$1\." | \
+        sed -es"/^$1\.//;s/\..*$//" |\
+        awk '$0 != prev ; {prev = $0}'
+}
+
+# -------- Pre JSON --------
+#
+# pre_json is a "templated json" used in the test docs to express test results. It looks
+# like json but has some extra comments to express when a certain output should be used.
+# These comments look like: "# Only Test Suite: <suite>"
+#
+
+remove_suites() { # suites... < pre_json > json
+    grep -vE "# Only Test Suite: ($(echo "$@" | sed "s/ /|/g"))" | \
+         sed -e's/# Only Test Suite:.*$//; s/ *$//'
+}
+
+remove_not_suite() { remove_suites !"$1" ; } # suite < pre_json > json
+
+# -------- Test Doc Format --------
+#
+# Test Doc Format has intermixed git config task definitions with json roots. This
+# makes it easy to define tests close to their outputs. Be aware that all of the
+# config will get consolidated into a single file, so non root config will be shared
+# amongst all the roots.
+#
+
+# Sample Test Doc for 2 roots:
+#
+# [root "Root PASS"]
+#   pass = True
+#
+# {
+#    "applicable" : true,
+#    "hasPass" : true,
+#    "name" : "Root PASS",
+#    "status" : "PASS"
+# }
+#
+# [root "Root FAIL"]
+#   fail = True
+#
+# {
+#    <other root>
+# }
+
+# Strip the json from Test Doc formatted text. For the sample above, the output would be:
+#
+# [root "Root PASS"]
+#   pass = True
+#
+# [root "Root FAIL"]
+#   fail = True
+# ...
+#
+testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config
+
+# Strip the git config from Test Doc formatted text. For the sample above, the output would be:
+#
+# { "plugins" : [
+#     { "name" : "task",
+#       "roots" : [
+#         {
+#           "applicable" : true,
+#           "hasPass" : true,
+#           "name" : "Root PASS",
+#           "status" : "PASS"
+#        },
+#        {
+#           <other root>
+#        },
+#    ...
+# }
+testdoc_2_pjson() { # < testdoc_format > pjson_task_roots
+    awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \
+         /^\{/  { open=1 }; \
+         open && end { print "}," ; end=0 }; \
+         /^\}/  { open=0 ; end=1 }; \
+         open; \
+         END   { print "}]}]}" }'
+}
+
+# ---- JSON PARSING ----
+
+json_pp() { # < json > json
+    python -c "import sys, json; \
+            print json.dumps(json.loads(sys.stdin.read()), indent=3, \
+            separators=(',', ' : '), sort_keys=True)"
+}
+
+json_val_by() { # json index|'key' > value
+    echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]"
+}
+json_val_by_key() { json_val_by "$1" "'$2'" ; }  # json key > value
+
+# --------
+
+gssh() {  # [-l user] cmd [args]...
+    local user_args=()
+    [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
+    ssh -x -p "$PORT" "${user_args[@]}" "$SERVER" gerrit "$@"
+}
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
+
+gen_change_id() { echo "I$(uuidgen | sha1sum | awk '{print $1}')"; } # > change_id
+
+commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
+
+err() { echo "ERROR: $1" >&2 ; exit 1 ; }
+
+# Run a test setup command quietly, exit on failure
+q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...]
+
+ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data
+
+set_change() { # change_json
+    { CHANGE=("$(json_val_by_key "$1" number)" \
+        "$(json_val_by_key "$1" id)" \
+        "$(json_val_by_key "$1" project)" \
+        "refs/heads/$(json_val_by_key "$1" branch)" \
+        "$(json_val_by_key "$1" status)" \
+        "$(json_val_by_key "$1" topic)") ; } 2> /dev/null
+}
+
+# change_token change_number change_id project branch status topic < templated_txt > change_txt
+replace_change_properties() {
+    sed -e "s|_change$1_number|$2|g" \
+        -e "s|_change$1_id|$3|g" \
+        -e "s|_change$1_project|$4|g" \
+        -e "s|_change$1_branch|$5|g" \
+        -e "s|_change$1_status|$6|g" \
+        -e "s|_change$1_topic|$7|g"
+}
+
+replace_default_changes() {
+    replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
+}
+
+replace_users() { # < text_with_users > test_with_expanded_users
+  local text="$(< /dev/stdin)"
+  for user in "${!USERS[@]}" ; do
+    text="${text//"$user"/${USERS["$user"]}}"
+  done
+  echo "$text"
+}
+
+replace_user() { # < text_with_testuser > text_with_$USER
+    sed -e"s/testuser/$USER/"
+}
+
+get_user_ref() { # username > refs/users/<accountidshard>/<accountid>
+    local user_account_id="$(curl --netrc --silent "http://$SERVER:$HTTP_PORT/a/accounts/$1" | \
+    sed -e '1!b' -e "/^)]}'$/d" | jq ._account_id)"
+    echo "refs/users/${user_account_id:(-2)}/$user_account_id"
+}
+
+replace_user_refs() { # < text_with_user_refs > test_with_expanded_user_refs
+    local text="$(< /dev/stdin)"
+    for user in "${!USER_REFS[@]}" ; do
+        text="${text//"$user"/${USER_REFS["$user"]}}"
+    done
+    echo "$text"
+}
+
+replace_tokens() { # < text > text with replacing all tokens(changes, user)
+    replace_default_changes | replace_user_refs | replace_user
+}
+
+strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
+strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
+
+define_jsonByRoot() { # task_plugin_ouptut > jsonByRoot_array_definition
+    local record root=''
+    local -A jsonByRoot
+    while IFS= read -r -d '' record ; do
+        if [ -z "$root" ] ; then
+            root=$record
+        else
+            jsonByRoot[$root]=$record
+            root=''
+        fi
+    done < <(python -c "if True: # NOP to start indent
+        import sys, json
+
+        roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
+        for root in roots:
+            root_json = json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)
+            sys.stdout.write(root['name'] + '\x00' + root_json + '\x00')"
+    )
+
+    local def=$(declare -p jsonByRoot)
+    echo "${def#*=}" # declare -A jsonByRoot='(...)' > '(...)'
+}
+
+get_plugins() { # < change_json > plugins_json
+    python -c "import sys, json; \
+        plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
+        print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
+}
+
+example() { # doc example_num > text_for_example_num
+    echo "$1" | awk '/```/{Q++;E=(Q+1)/2};E=='"$2" | grep -v '```'
+}
+
+get_change_num() { # < gerrit_push_response > changenum
+    local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
+    echo "${url##*\/}" | tr -d -c '[:digit:]'
+}
+
+install_changeid_hook() { # repo
+    local hook=$(git rev-parse --git-dir)/hooks/commit-msg
+    scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
+    chmod +x "$hook"
+}
+
+setup_repo() { # repo remote ref [--initial-commit]
+    local repo=$1 remote=$2 ref=$3 init=$4
+    git init "$repo"
+    (
+        cd "$repo"
+        install_changeid_hook "$repo"
+        git fetch "$remote" "$ref"
+        if ! git checkout FETCH_HEAD ; then
+            if [ "$init" = "--initial-commit" ] ; then
+                git commit --allow-empty -a -m "Initial Commit"
+            fi
+        fi
+    )
+}
+
+update_repo() { # repo remote ref
+    local repo=$1 remote=$2 ref=$3
+    (
+        cd "$repo"
+        git add .
+        git commit -m 'Testing task plugin'
+        git push "$remote" HEAD:"$ref"
+    )
+}
+
+create_repo_change() { # repo remote ref [change_id] > change_num
+    local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
+    (
+        q cd "$repo"
+        uuidgen > file
+        q git add .
+        [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
+        q git commit -m "$msg"
+        git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
+    )
+}
+
+query() {  # [-l user] query > json lines
+  local user_args=()
+  [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
+  gssh "${user_args[@]}" query "$@" --format json
+}
+
+# N < json lines > changeN_json
+change_plugins() { awk "NR==$1" | get_plugins | json_pp ; }
+
+results_suite() { # name expected_file plugins_json
+    local name=$1 expected=$2 actual=$3
+
+    local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected")
+    local -A OUTPUT_ROOTS=$(echo "$actual" | define_jsonByRoot)
+
+    local out root
+    echo "$ROOTS" | while read root ; do
+        result_root "$name" "$root"
+    done
+    out=$(diff "$expected" <(echo "$actual") | head -15)
+    [ -z "$out" ]
+    result "$name - Full Test Suite" "$out"
+}