Provide capability to preview absolute task paths

The '--include-paths' switch allows us to preview the absolute location
of a task. This would be helpful in debugging when we spread out tasks
in multiple repositories in future.

Add .firstTimeRedirect file while setting up the environment for docker
tests to prevent redirection at the very first access to Gerrit [1]
resulting in empty response for the curl call.

[1] https://gerrit.googlesource.com/plugins/out-of-the-box/+/refs/heads/master

Change-Id: I48ef2ac9e214de3e1d7e920b652bfba704445f40
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index 4ef065e..44564ed 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -15,6 +15,8 @@
 package com.googlesource.gerrit.plugins.task;
 
 import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -23,7 +25,6 @@
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.gerrit.sshd.commands.Query;
-import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
 import java.util.ArrayList;
@@ -31,9 +32,14 @@
 import org.kohsuke.args4j.Option;
 
 public class Modules {
-  public static class Module extends AbstractModule {
+  public static class Module extends FactoryModule {
     @Override
     protected void configure() {
+      bind(CapabilityDefinition.class)
+          .annotatedWith(Exports.named(ViewPathsCapability.VIEW_PATHS))
+          .to(ViewPathsCapability.class);
+      factory(TaskPath.Factory.class);
+
       bind(ChangePluginDefinedInfoFactory.class)
           .annotatedWith(Exports.named("task"))
           .to(TaskAttributeFactory.class);
@@ -58,6 +64,9 @@
         usage = "Include only invalid tasks and the tasks referencing them in the output")
     public boolean onlyInvalid = false;
 
+    @Option(name = "--include-paths", usage = "Include absolute path to each task")
+    public boolean includePaths = false;
+
     @Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task")
     public boolean evaluationTime = false;
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 5affd56..6262420 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -17,10 +17,12 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
 import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
@@ -38,8 +40,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
-  private static final FluentLogger log = FluentLogger.forEnclosingClass();
-
+  public static final TaskPath MISSING_VIEW_PATH_CAPABILITY =
+          new TaskPath(
+                  String.format(
+                          "Can't perform operation, need %s capability", ViewPathsCapability.VIEW_PATHS));
   public enum Status {
     INVALID,
     UNKNOWN,
@@ -77,6 +81,7 @@
     public Boolean hasPass;
     public String hint;
     public Boolean inProgress;
+    public TaskPath path;
     public String name;
     public Integer change;
     public Status status;
@@ -94,17 +99,31 @@
     public Statistics queryStatistics;
   }
 
+  protected final String pluginName;
   protected final TaskTree definitions;
   protected final PredicateCache predicateCache;
+  protected final boolean hasViewPathsCapability;
+  protected final TaskPath.Factory taskPathFactory;
 
   protected Modules.MyOptions options;
   protected TaskPluginAttribute lastTaskPluginAttribute;
   protected Statistics statistics;
 
   @Inject
-  public TaskAttributeFactory(TaskTree definitions, PredicateCache predicateCache) {
+  public TaskAttributeFactory(
+      String pluginName,
+      TaskTree definitions,
+      PredicateCache predicateCache,
+      PermissionBackend permissionBackend,
+      TaskPath.Factory taskPathFactory) {
+    this.pluginName = pluginName;
     this.definitions = definitions;
     this.predicateCache = predicateCache;
+    this.hasViewPathsCapability =
+        permissionBackend
+            .currentUser()
+            .testOrFalse(new PluginPermission(this.pluginName, ViewPathsCapability.VIEW_PATHS));
+    this.taskPathFactory = taskPathFactory;
   }
 
   @Override
@@ -198,6 +217,13 @@
           if (options.onlyInvalid && !isValidQueries()) {
             attribute.status = Status.INVALID;
           }
+          if (options.includePaths) {
+            if (hasViewPathsCapability) {
+              attribute.path = taskPathFactory.create(node.taskKey);
+            } else {
+              attribute.path = MISSING_VIEW_PATH_CAPABILITY;
+            }
+          }
           boolean groupApplicable = attribute.status != null;
 
           if (groupApplicable || !options.onlyApplicable) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index be7f120..f5f0bd4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -179,7 +179,7 @@
   public static final String SECTION_NAMES_FACTORY = "names-factory";
   public static final String SECTION_ROOT = "root";
   public static final String SECTION_TASK = TaskKey.CONFIG_SECTION;
-  public static final String SECTION_TASKS_FACTORY = "tasks-factory";
+  public static final String SECTION_TASKS_FACTORY = TaskKey.CONFIG_TASKS_FACTORY;
   public static final String KEY_APPLICABLE = "applicable";
   public static final String KEY_CHANGES = "changes";
   public static final String KEY_DUPLICATE_KEY = "duplicate-key";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
index c56202d..f21e732 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -21,6 +21,7 @@
 @AutoValue
 public abstract class TaskKey {
   protected static final String CONFIG_SECTION = "task";
+  protected static final String CONFIG_TASKS_FACTORY = "tasks-factory";
 
   /** Creates a TaskKey with task name as the name of sub section. */
   public static TaskKey create(SubSectionKey section) {
@@ -44,4 +45,8 @@
   public abstract SubSectionKey subSection();
 
   public abstract String task();
+
+  public boolean isTasksFactoryGenerated() {
+    return subSection().section().equals(CONFIG_TASKS_FACTORY);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
new file mode 100644
index 0000000..c0c5d8c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
@@ -0,0 +1,76 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class TaskPath {
+  public interface Factory {
+    TaskPath create(TaskKey key);
+  }
+
+  protected String name;
+  protected String type;
+  protected String tasksFactory;
+  protected String user;
+  protected String project;
+  protected String ref;
+  protected String file;
+  protected String error;
+
+  @Inject
+  public TaskPath(AllUsersNameProvider allUsers, Accounts accounts, @Assisted TaskKey key) {
+    name = key.task();
+    type = key.subSection().section();
+    tasksFactory = key.isTasksFactoryGenerated() ? key.subSection().subSection() : null;
+    user = getUserOrNull(accounts, allUsers.get(), key);
+    project = key.branch().project().get();
+    ref = key.branch().branch();
+    file = key.subSection().file().file();
+  }
+
+  public TaskPath(String error) {
+    this.error = error;
+  }
+
+  private String getUserOrNull(Accounts accounts, Project.NameKey allUsers, TaskKey key) {
+    try {
+      if (allUsers.get().equals(key.branch().project().get())) {
+        String ref = key.branch().branch();
+        Account.Id id = Account.Id.fromRef(ref);
+        if (id != null) {
+          Optional<AccountState> state = accounts.get(id);
+          if (state.isPresent()) {
+            Optional<String> userName = state.get().userName();
+            if (userName.isPresent()) {
+              return userName.get();
+            }
+          }
+        }
+      }
+    } catch (ConfigInvalidException | IOException e) {
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
new file mode 100644
index 0000000..1fd1e3a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
@@ -0,0 +1,26 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class ViewPathsCapability extends CapabilityDefinition {
+  public static final String VIEW_PATHS = "viewTaskPaths";
+
+  @Override
+  public String getDescription() {
+    return "View Task Paths";
+  }
+}
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index d756518..85fd25a 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -343,6 +343,8 @@
               Backup Optional Subtask {$_name} Backup |
               Default Subtask # Must exist if the above two don't!
 ```
+
+<a id="tasks_factory"/>
 Tasks-Factory
 -------------
 A tasks-factory section supports all the keys supported by task sections.  In
@@ -557,6 +559,32 @@
 not output anything. This switch is particularly useful in combination
 with the **\-\-@PLUGIN@\-\-preview** switch.
 
+**\-\-@PLUGIN@\-\-include-paths**
+
+This switch will show the absolute path of each task. This is meant for
+debugging when tasks are spread out in different files. A task path includes
+task name, type (indicating one of root, task, tasks-factory),
+[task factory](#tasks_factory) name (if it is generated by one), file name,
+project, and branch the file belongs to. Additionally, if a task is on a user
+ref, it also shows the identity of that user. Only users with `viewTaskPaths`
+capability on the server can view absolute task paths with this switch.
+
+```
+  $ ssh -x -p 29418 example.com gerrit query change:123 \-\-@PLUGIN@\-\-include-paths
+  ...
+  plugins:
+    name: task
+    roots:
+      name: Jenkins Build and Test
+      inProgress: false
+      status: READY
+      path:
+        name: Jenkins Build and Test
+        project: All-Projects.git
+        branch: refs/meta/config
+        file: task.config
+```
+
 **\-\-@PLUGIN@\-\-evaluation-time**
 
 This switch is meant as a debug switch to evaluate task performance. This
diff --git a/src/main/resources/Documentation/test/paths.md b/src/main/resources/Documentation/test/paths.md
new file mode 100644
index 0000000..8f43cb0
--- /dev/null
+++ b/src/main/resources/Documentation/test/paths.md
@@ -0,0 +1,208 @@
+`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[root "Root Task PATHS"]
+  subtask = subtask pass
+
+[task "subtask pass"]
+  applicable = is:open
+  pass = is:open
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Task PATHS",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root Task PATHS",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "subtask pass",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task.config",
+            "name" : "subtask pass",
+            "project" : "All-Projects",
+            "type" : "task"
+         },
+         "status" : "PASS"
+      }
+   ]
+}
+
+[root "Root other FILE"]
+  applicable = is:open
+  subtasks-file = common.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root other FILE",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root other FILE",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config PASS",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task/common.config",
+            "name" : "file task/common.config PASS",
+            "project" : "All-Projects",
+            "type" : "task"
+         },
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config FAIL",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task/common.config",
+            "name" : "file task/common.config FAIL",
+            "project" : "All-Projects",
+            "type" : "task"
+         },
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root tasks-factory"]
+  subtasks-factory = tasks-factory example
+
+[tasks-factory "tasks-factory example"]
+  names-factory = names-factory example list
+
+[names-factory "names-factory example list"]
+  type = static
+  name = my a task
+  name = my b task
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root tasks-factory",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "my a task",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task.config",
+            "name" : "my a task",
+            "project" : "All-Projects",
+            "tasksFactory" : "tasks-factory example",
+            "type" : "tasks-factory"
+         },
+         "status" : "INVALID"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "my b task",
+         "path" : {
+            "ref" : "refs/meta/config",
+            "file" : "task.config",
+            "name" : "my b task",
+            "project" : "All-Projects",
+            "tasksFactory" : "tasks-factory example",
+            "type" : "tasks-factory"
+         },
+         "status" : "INVALID"
+      }
+   ]
+}
+
+[root "Root other PROJECT"]
+  subtasks-external = user ref
+
+[external "user ref"]
+  user = testuser
+  file = common.config
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root other PROJECT",
+   "path" : {
+      "ref" : "refs/meta/config",
+      "file" : "task.config",
+      "name" : "Root other PROJECT",
+      "project" : "All-Projects",
+      "type" : "root"
+   },
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config PASS",
+         "path" : {
+            "ref" : "{testuser_user_ref}",
+            "file" : "task/common.config",
+            "name" : "file task/common.config PASS",
+            "project" : "All-Users",
+            "user" : "testuser",
+            "type" : "task"
+         },
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config FAIL",
+         "path" : {
+            "ref" : "{testuser_user_ref}",
+            "file" : "task/common.config",
+            "name" : "file task/common.config FAIL",
+            "project" : "All-Users",
+            "user" : "testuser",
+            "type" : "task"
+         },
+         "status" : "FAIL"
+      }
+   ]
+}
+```
+`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[root "Root Capability Error"]
+    applicable = is:open
+    pass = true
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Capability Error",
+   "path" : {
+      "error" : "Can't perform operation, need viewTaskPaths capability"
+   },
+   "status" : "PASS"
+}
+```
\ No newline at end of file
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 1880432..ace3680 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -191,6 +191,24 @@
     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
 
@@ -223,8 +241,8 @@
         print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
 }
 
-example() { # example_num > text_for_example_num
-    echo "$DOC_STATES" | awk '/```/{Q++;E=(Q+1)/2};E=='"$1" | grep -v '```' | replace_user
+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
@@ -342,12 +360,12 @@
 USERS=$OUT/All-Users
 USER_TASKS=$USERS/task
 
-DOC_PREVIEW=$DOCS/preview.md
 EXPECTED=$OUT/expected
 ACTUAL=$OUT/actual
 
 ROOT_CFG=$ALL/task.config
 COMMON_CFG=$ALL_TASKS/common.config
+USER_COMMON_CFG=$USER_TASKS/common.config
 INVALIDS_CFG=$ALL_TASKS/invalids.config
 USER_SPECIAL_CFG=$USER_TASKS/special.config
 
@@ -370,6 +388,7 @@
 
 
 PORT=29418
+HTTP_PORT=8080
 PROJECT=test
 BRANCH=master
 REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects
@@ -381,6 +400,9 @@
 
 CONFIG=$ROOT_CFG
 
+declare -A USER_REFS
+USER_REFS["{testuser_user_ref}"]="$(get_user_ref "$USER")"
+
 mkdir -p "$OUT" "$ALL_TASKS" "$USER_TASKS"
 
 q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
@@ -390,12 +412,16 @@
 changes=$(gssh query "status:open limit:2" --format json)
 set_change "$(echo "$changes" | awk 'NR==1')" ; CHANGE1=("${CHANGE[@]}")
 set_change "$(echo "$changes" | awk 'NR==2')" ; CHANGE2=("${CHANGE[@]}")
-DOC_STATES=$(replace_default_changes < "$DOCS/task_states.md")
 
-example 2 | replace_user | testdoc_2_cfg > "$ROOT_CFG"
-example 3 > "$COMMON_CFG"
-example 4 > "$INVALIDS_CFG"
-example 5 > "$USER_SPECIAL_CFG"
+DOC_STATES=$(replace_tokens < "$DOCS/task_states.md")
+DOC_PREVIEW=$(replace_tokens < "$DOCS/preview.md")
+DOC_PATHS=$(replace_tokens < "$DOCS/paths.md")
+
+example "$DOC_STATES" 2 | testdoc_2_cfg > "$ROOT_CFG"
+example "$DOC_STATES" 3 > "$COMMON_CFG"
+example "$DOC_STATES" 3 > "$USER_COMMON_CFG"
+example "$DOC_STATES" 4 > "$INVALIDS_CFG"
+example "$DOC_STATES" 5 > "$USER_SPECIAL_CFG"
 
 ROOTS=$(config_section_keys "root") || err "Invalid ROOTS"
 
@@ -407,7 +433,7 @@
 change4_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change4_id")
 change3_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change3_id")
 
-ex2_pjson=$(example 2 | testdoc_2_pjson)
+ex2_pjson=$(example "$DOC_STATES" 2 | testdoc_2_pjson)
 all_pjson=$(echo "$ex2_pjson" | \
     replace_change_properties \
         "" \
@@ -452,7 +478,7 @@
 strip_non_invalid < "$EXPECTED".applicable > "$EXPECTED".invalid-applicable
 
 
-preview_pjson=$(testdoc_2_pjson < "$DOC_PREVIEW" | replace_default_changes)
+preview_pjson=$(echo "$DOC_PREVIEW" | testdoc_2_pjson)
 echo "$preview_pjson" | remove_suites "invalid" "secret" | \
     ensure json_pp > "$EXPECTED".preview-non-secret
 echo "$preview_pjson" | remove_suites "invalid" "!secret" | \
@@ -460,7 +486,7 @@
 echo "$preview_pjson" | remove_suites "secret" "!invalid" | \
     strip_non_invalid > "$EXPECTED".preview-invalid
 
-testdoc_2_cfg < "$DOC_PREVIEW" | replace_user > "$ROOT_CFG"
+echo "$DOC_PREVIEW" | testdoc_2_cfg | replace_user > "$ROOT_CFG"
 cnum=$(create_repo_change "$ALL" "$REMOTE_ALL" "$REF_ALL")
 PREVIEW_ROOTS=$(config_section_keys "root")
 
@@ -479,4 +505,17 @@
 test_generated preview-non-secret -l "$NON_SECRET_USER" --task--preview "$cnum,1" --task--all "$query"
 test_generated preview-invalid --task--preview "$cnum,1" --task--invalid "$query"
 
+example "$DOC_PATHS" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG"
+q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+ROOTS=$(config_section_keys "root")
+example "$DOC_PATHS" 1 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths
+
+test_generated task-paths --task--all --task--include-paths "$query"
+
+example "$DOC_PATHS" 2 | testdoc_2_cfg > "$ROOT_CFG"
+q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+ROOTS=$(config_section_keys "root")
+example "$DOC_PATHS" 2 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths.non-secret
+test_generated task-paths.non-secret -l "$NON_SECRET_USER" --task--all --task--include-paths "$query"
+
 exit $RESULT
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 35e39f2..d638517 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -3,6 +3,7 @@
 ENV GERRIT_SITE /var/gerrit
 RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
     DEVELOPMENT_BECOME_ANY_ACCOUNT
+RUN touch "$GERRIT_SITE"/.firstTimeRedirect
 
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
index 58eb6c8..dd5ba8c 100755
--- a/test/docker/run_tests/Dockerfile
+++ b/test/docker/run_tests/Dockerfile
@@ -7,7 +7,7 @@
 ENV RUN_TESTS_DIR task/test/docker/run_tests
 ENV WORKSPACE $USER_HOME/workspace
 
-RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd
+RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd curl jq
 RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
 
 RUN groupadd -f -g $GID users2
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index 2cc3df2..22ab394 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -16,12 +16,16 @@
 
 ./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 -t 60 -- echo "gerrit is up"
 
-echo "Creating a default user account ..."
+echo "Update admin account ..."
 
 cat "$USER_HOME"/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \
     "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit set-account \
     admin --add-ssh-key -
 
+PASSWORD=$(uuidgen)
+echo "machine $GERRIT_HOST login $USER password $PASSWORD" > "$USER_HOME"/.netrc
+ssh -p 29418 "$GERRIT_HOST" gerrit set-account --http-password "$PASSWORD" "$USER"
+
 is_plugin_loaded "task" || die "Task plugin is not installed"
 
 NON_SECRET_USER="non_secret_user"