update charts
diff --git a/charts/k8s-gerrit/tests/conftest.py b/charts/k8s-gerrit/tests/conftest.py
new file mode 100644
index 0000000..eefb8f9
--- /dev/null
+++ b/charts/k8s-gerrit/tests/conftest.py
@@ -0,0 +1,337 @@
+# pylint: disable=W0613, W0212
+
+# Copyright (C) 2018 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.
+
+import argparse
+import getpass
+import os
+import sys
+
+from pathlib import Path
+
+import docker
+import pygit2 as git
+import pytest
+
+sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
+
+# pylint: disable=C0103
+pytest_plugins = ["fixtures.credentials", "fixtures.cluster", "fixtures.helm.gerrit"]
+
+# Base images that are not published and thus only tagged with "latest"
+BASE_IMGS = ["base", "gerrit-base"]
+
+
+# pylint: disable=W0622
+class PasswordPromptAction(argparse.Action):
+    def __init__(
+        self,
+        option_strings,
+        dest=None,
+        nargs=0,
+        default=None,
+        required=False,
+        type=None,
+        metavar=None,
+        help=None,
+    ):
+        super().__init__(
+            option_strings=option_strings,
+            dest=dest,
+            nargs=nargs,
+            default=default,
+            required=required,
+            metavar=metavar,
+            type=type,
+            help=help,
+        )
+
+    def __call__(self, parser, args, values, option_string=None):
+        password = getpass.getpass()
+        setattr(args, self.dest, password)
+
+
+def pytest_addoption(parser):
+    parser.addoption(
+        "--registry",
+        action="store",
+        default="",
+        help="Container registry to push (if --push=true) and pull container images"
+        + "from for tests on Kubernetes clusters (default: '')",
+    )
+    parser.addoption(
+        "--registry-user",
+        action="store",
+        default="",
+        help="Username for container registry (default: '')",
+    )
+    parser.addoption(
+        "--registry-pwd",
+        action="store",
+        default="",
+        help="Password for container registry (default: '')",
+    )
+    parser.addoption(
+        "--org",
+        action="store",
+        default="k8sgerrit",
+        help="Docker organization (default: 'k8sgerrit')",
+    )
+    parser.addoption(
+        "--push",
+        action="store_true",
+        help="If set, the docker images will be pushed to the registry configured"
+        + "by --registry (default: false)",
+    )
+    parser.addoption(
+        "--tag",
+        action="store",
+        default=None,
+        help="Tag of cached container images to test. Missing images will be built."
+        + "(default: All container images will be built)",
+    )
+    parser.addoption(
+        "--build-cache",
+        action="store_true",
+        help="If set, the docker cache will be used when building container images.",
+    )
+    parser.addoption(
+        "--kubeconfig",
+        action="store",
+        default=None,
+        help="Kubeconfig to use for cluster connection. If none is given the currently"
+        + "configured context is used.",
+    )
+    parser.addoption(
+        "--rwm-storageclass",
+        action="store",
+        default="shared-storage",
+        help="Name of the storageclass used for ReadWriteMany access."
+        + "(default: shared-storage)",
+    )
+    parser.addoption(
+        "--ingress-url",
+        action="store",
+        default=None,
+        help="URL of the ingress domain used by the cluster.",
+    )
+    parser.addoption(
+        "--gerrit-user",
+        action="store",
+        default="admin",
+        help="Gerrit admin username to be used for smoke tests. (default: admin)",
+    )
+    parser.addoption(
+        "--gerrit-pwd",
+        action=PasswordPromptAction,
+        default="secret",
+        help="Gerrit admin password to be used for smoke tests. (default: secret)",
+    )
+    parser.addoption(
+        "--skip-slow", action="store_true", help="If set, skip slow tests."
+    )
+
+
+def pytest_collection_modifyitems(config, items):
+    if config.getoption("--skip-slow"):
+        skip_slow = pytest.mark.skip(reason="--skip-slow was set.")
+        for item in items:
+            if "slow" in item.keywords:
+                item.add_marker(skip_slow)
+
+
+def pytest_runtest_makereport(item, call):
+    if "incremental" in item.keywords:
+        if call.excinfo is not None:
+            parent = item.parent
+            parent._previousfailed = item
+
+
+def pytest_runtest_setup(item):
+    if "incremental" in item.keywords:
+        previousfailed = getattr(item.parent, "_previousfailed", None)
+        if previousfailed is not None:
+            pytest.xfail(f"previous test failed ({previousfailed.name})")
+
+
+@pytest.fixture(scope="session")
+def tag_of_cached_container(request):
+    return request.config.getoption("--tag")
+
+
+@pytest.fixture(scope="session")
+def docker_client():
+    return docker.from_env()
+
+
+@pytest.fixture(scope="session")
+def repository_root():
+    return Path(git.discover_repository(os.path.realpath(__file__))).parent.absolute()
+
+
+@pytest.fixture(scope="session")
+def container_images(repository_root):
+    image_paths = {}
+    for directory in os.listdir(os.path.join(repository_root, "container-images")):
+        image_paths[directory] = os.path.join(
+            repository_root, "container-images", directory
+        )
+    return image_paths
+
+
+@pytest.fixture(scope="session")
+def docker_registry(request):
+    registry = request.config.getoption("--registry")
+    if registry and not registry[-1] == "/":
+        registry += "/"
+    return registry
+
+
+@pytest.fixture(scope="session")
+def docker_org(request):
+    org = request.config.getoption("--org")
+    if org and not org[-1] == "/":
+        org += "/"
+    return org
+
+
+@pytest.fixture(scope="session")
+def docker_tag(tag_of_cached_container, repository_root):
+    if tag_of_cached_container:
+        return tag_of_cached_container
+    return git.Repository(repository_root).describe(dirty_suffix="-dirty")
+
+
+@pytest.fixture(scope="session")
+def docker_build(
+    request,
+    docker_client,
+    tag_of_cached_container,
+    docker_registry,
+    docker_org,
+    docker_tag,
+):
+    def docker_build(image, name):
+        if name in BASE_IMGS:
+            image_name = f"{name}:latest"
+        else:
+            image_name = f"{docker_registry}{docker_org}{name}:{docker_tag}"
+
+        if tag_of_cached_container:
+            try:
+                return docker_client.images.get(image_name)
+            except docker.errors.ImageNotFound:
+                print(f"Image {image_name} could not be loaded. Building it now.")
+
+        no_cache = not request.config.getoption("--build-cache")
+
+        build = docker_client.images.build(
+            path=image,
+            nocache=no_cache,
+            rm=True,
+            tag=image_name,
+            platform="linux/amd64",
+        )
+        return build[0]
+
+    return docker_build
+
+
+@pytest.fixture(scope="session")
+def docker_login(request, docker_client, docker_registry):
+    username = request.config.getoption("--registry-user")
+    if username:
+        docker_client.login(
+            username=username,
+            password=request.config.getoption("--registry-pwd"),
+            registry=docker_registry,
+        )
+
+
+@pytest.fixture(scope="session")
+def docker_push(
+    request, docker_client, docker_registry, docker_login, docker_org, docker_tag
+):
+    def docker_push(image):
+        docker_repository = f"{docker_registry}{docker_org}{image}"
+        docker_client.images.push(docker_repository, tag=docker_tag)
+
+    return docker_push
+
+
+@pytest.fixture(scope="session")
+def docker_network(request, docker_client):
+    network = docker_client.networks.create(
+        name="k8sgerrit-test-network", scope="local"
+    )
+
+    yield network
+
+    network.remove()
+
+
+@pytest.fixture(scope="session")
+def base_image(container_images, docker_build):
+    return docker_build(container_images["base"], "base")
+
+
+@pytest.fixture(scope="session")
+def gerrit_base_image(container_images, docker_build, base_image):
+    return docker_build(container_images["gerrit-base"], "gerrit-base")
+
+
+@pytest.fixture(scope="session")
+def gitgc_image(request, container_images, docker_build, docker_push, base_image):
+    gitgc_image = docker_build(container_images["git-gc"], "git-gc")
+    if request.config.getoption("--push"):
+        docker_push("git-gc")
+    return gitgc_image
+
+
+@pytest.fixture(scope="session")
+def apache_git_http_backend_image(
+    request, container_images, docker_build, docker_push, base_image
+):
+    apache_git_http_backend_image = docker_build(
+        container_images["apache-git-http-backend"], "apache-git-http-backend"
+    )
+    if request.config.getoption("--push"):
+        docker_push("apache-git-http-backend")
+    return apache_git_http_backend_image
+
+
+@pytest.fixture(scope="session")
+def gerrit_image(
+    request, container_images, docker_build, docker_push, base_image, gerrit_base_image
+):
+    gerrit_image = docker_build(container_images["gerrit"], "gerrit")
+    if request.config.getoption("--push"):
+        docker_push("gerrit")
+    return gerrit_image
+
+
+@pytest.fixture(scope="session")
+def gerrit_init_image(
+    request, container_images, docker_build, docker_push, base_image, gerrit_base_image
+):
+    gerrit_init_image = docker_build(container_images["gerrit-init"], "gerrit-init")
+    if request.config.getoption("--push"):
+        docker_push("gerrit-init")
+    return gerrit_init_image
+
+
+@pytest.fixture(scope="session")
+def required_plugins(request):
+    return ["healthcheck"]
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py
new file mode 100644
index 0000000..8cd3443
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py
@@ -0,0 +1,92 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2018 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.
+
+import random
+import string
+import time
+
+import pytest
+
+
+class GitBackendContainer:
+    def __init__(self, docker_client, image, port, credentials_dir):
+        self.docker_client = docker_client
+        self.image = image
+        self.port = port
+        self.apache_credentials_dir = credentials_dir
+
+        self.container = None
+
+    def start(self):
+        self.container = self.docker_client.containers.run(
+            image=self.image.id,
+            ports={"80": self.port},
+            volumes={
+                self.apache_credentials_dir: {
+                    "bind": "/var/apache/credentials",
+                    "mode": "ro",
+                }
+            },
+            detach=True,
+            auto_remove=True,
+            platform="linux/amd64",
+        )
+
+    def stop(self):
+        self.container.stop(timeout=1)
+
+
+@pytest.fixture(scope="module")
+def container_run_factory(
+    docker_client, apache_git_http_backend_image, htpasswd, credentials_dir
+):
+    def run_container(port):
+        return GitBackendContainer(
+            docker_client,
+            apache_git_http_backend_image,
+            port,
+            str(credentials_dir),
+        )
+
+    return run_container
+
+
+@pytest.fixture(scope="module")
+def container_run(container_run_factory, free_port):
+    test_setup = container_run_factory(free_port)
+    test_setup.start()
+    time.sleep(3)
+
+    yield test_setup
+
+    test_setup.stop()
+
+
+@pytest.fixture(scope="module")
+def base_url(container_run):
+    return f"http://localhost:{container_run.port}"
+
+
+@pytest.fixture(scope="function")
+def random_repo_name():
+    return "".join(
+        [random.choice(string.ascii_letters + string.digits) for n in range(8)]
+    )
+
+
+@pytest.fixture(scope="function")
+def repo_creation_url(base_url, random_repo_name):
+    return f"{base_url}/a/projects/{random_repo_name}"
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py
new file mode 100644
index 0000000..984d6be
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_apache_git_http_backend_image(
+    apache_git_http_backend_image, tag_of_cached_container
+):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert apache_git_http_backend_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py
new file mode 100755
index 0000000..0d5ef65
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py
@@ -0,0 +1,96 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2018 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.
+
+from pathlib import Path
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+
+@pytest.fixture(scope="function")
+def repo_dir(tmp_path_factory, random_repo_name):
+    return tmp_path_factory.mktemp(random_repo_name)
+
+
+@pytest.fixture(scope="function")
+def mock_repo(repo_dir):
+    repo = git.init_repository(repo_dir, False)
+    file_name = os.path.join(repo_dir, "test.txt")
+    Path(file_name).touch()
+    repo.index.add("test.txt")
+    repo.index.write()
+    # pylint: disable=E1101
+    author = git.Signature("Gerrit Review", "gerrit@review.com")
+    committer = git.Signature("Gerrit Review", "gerrit@review.com")
+    message = "Initial commit"
+    tree = repo.index.write_tree()
+    repo.create_commit("HEAD", author, committer, message, tree, [])
+    return repo
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation(
+    container_run, htpasswd, repo_creation_url
+):
+    request = requests.put(
+        repo_creation_url,
+        auth=requests.auth.HTTPBasicAuth(htpasswd["user"], htpasswd["password"]),
+    )
+    assert request.status_code == 201
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_fails_without_credentials(
+    container_run, repo_creation_url
+):
+    request = requests.put(repo_creation_url)
+    assert request.status_code == 401
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_fails_wrong_fs_permissions(
+    container_run, htpasswd, repo_creation_url
+):
+    container_run.container.exec_run("chown -R root:root /var/gerrit/git")
+    request = requests.put(
+        repo_creation_url,
+        auth=requests.auth.HTTPBasicAuth(htpasswd["user"], htpasswd["password"]),
+    )
+    container_run.container.exec_run("chown -R gerrit:users /var/gerrit/git")
+    assert request.status_code == 500
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_push_repo(
+    container_run, base_url, htpasswd, mock_repo, random_repo_name
+):
+    container_run.container.exec_run(
+        f"su -c 'git init --bare /var/gerrit/git/{random_repo_name}.git' gerrit"
+    )
+    url = f"{base_url}/{random_repo_name}.git"
+    url = url.replace("//", f"//{htpasswd['user']}:{htpasswd['password']}@")
+    origin = mock_repo.remotes.create("origin", url)
+    origin.push(["refs/heads/master:refs/heads/master"])
+
+    remote_refs = origin.ls_remotes()
+    assert str(remote_refs[0]["oid"]) == mock_repo.revparse_single("HEAD").hex
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py
new file mode 100755
index 0000000..a138ef5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2018 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.
+
+import pytest
+import utils
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_apache_git_http_backend_inherits_from_base(apache_git_http_backend_image):
+    assert utils.check_if_ancestor_image_is_inherited(
+        apache_git_http_backend_image, "base:latest"
+    )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_apache2(container_run):
+    exit_code, _ = container_run.container.exec_run("which httpd")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_http_site_configured(container_run):
+    exit_code, _ = container_run.container.exec_run(
+        "test -f /etc/apache2/conf.d/git-http-backend.conf"
+    )
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_start_script(container_run):
+    exit_code, _ = container_run.container.exec_run("test -f /var/tools/start")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_repo_creation_cgi_script(container_run):
+    exit_code, _ = container_run.container.exec_run("test -f /var/cgi/project_admin.sh")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_apache_git_http_backend_has_entrypoint(apache_git_http_backend_image):
+    entrypoint = apache_git_http_backend_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert len(entrypoint) == 2
+    assert entrypoint[1] == "/var/tools/start"
diff --git a/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py b/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py
new file mode 100644
index 0000000..2a3afa5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_base(base_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert base_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py b/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py
new file mode 100755
index 0000000..528d2b4
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, base_image):
+    container_run = container_endless_run_factory(docker_client, base_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_contains_git(container_run):
+    exit_code, _ = container_run.exec_run("which git")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_has_non_root_user_gerrit(container_run):
+    exit_code, output = container_run.exec_run("id -u gerrit")
+    assert exit_code == 0
+    uid = int(output.strip().decode("utf-8"))
+    assert uid != 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_gerrit_no_root_permissions(container_run):
+    exit_code, _ = container_run.exec_run("su -c 'rm -rf /bin' gerrit")
+    assert exit_code > 0
diff --git a/charts/k8s-gerrit/tests/container-images/conftest.py b/charts/k8s-gerrit/tests/container-images/conftest.py
new file mode 100644
index 0000000..8a4b8f2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/conftest.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2018 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.
+
+import os.path
+import socket
+
+import pytest
+
+
+class GerritContainer:
+    def __init__(self, docker_client, docker_network, tmp_dir, image, configs, port):
+        self.docker_client = docker_client
+        self.docker_network = docker_network
+        self.tmp_dir = tmp_dir
+        self.image = image
+        self.configs = configs
+        self.port = port
+
+        self.container = None
+
+    def _create_config_files(self):
+        tmp_config_dir = os.path.join(self.tmp_dir, "configs")
+        if not os.path.isdir(tmp_config_dir):
+            os.mkdir(tmp_config_dir)
+        config_paths = {}
+        for filename, content in self.configs.items():
+            gerrit_config_file = os.path.join(tmp_config_dir, filename)
+            with open(gerrit_config_file, "w", encoding="utf-8") as config_file:
+                config_file.write(content)
+            config_paths[filename] = gerrit_config_file
+        return config_paths
+
+    def _define_volume_mounts(self):
+        volumes = {
+            v: {"bind": f"/var/gerrit/etc/{k}", "mode": "rw"}
+            for (k, v) in self._create_config_files().items()
+        }
+        volumes[os.path.join(self.tmp_dir, "lib")] = {
+            "bind": "/var/gerrit/lib",
+            "mode": "rw",
+        }
+        return volumes
+
+    def start(self):
+        self.container = self.docker_client.containers.run(
+            image=self.image.id,
+            user="gerrit",
+            volumes=self._define_volume_mounts(),
+            ports={8080: str(self.port)},
+            network=self.docker_network.name,
+            detach=True,
+            auto_remove=True,
+            platform="linux/amd64",
+        )
+
+    def stop(self):
+        self.container.stop(timeout=1)
+
+
+@pytest.fixture(scope="session")
+def gerrit_container_factory():
+    def get_gerrit_container(
+        docker_client, docker_network, tmp_dir, image, gerrit_config, port
+    ):
+        return GerritContainer(
+            docker_client, docker_network, tmp_dir, image, gerrit_config, port
+        )
+
+    return get_gerrit_container
+
+
+@pytest.fixture(scope="session")
+def container_endless_run_factory():
+    def get_container(docker_client, image):
+        return docker_client.containers.run(
+            image=image.id,
+            entrypoint="/bin/ash",
+            command=["-c", "tail -f /dev/null"],
+            user="gerrit",
+            detach=True,
+            auto_remove=True,
+            platform="linux/amd64",
+        )
+
+    return get_container
+
+
+@pytest.fixture(scope="session")
+def free_port():
+    skt = socket.socket()
+    skt.bind(("", 0))
+    port = skt.getsockname()[1]
+    skt.close()
+    return port
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py
new file mode 100644
index 0000000..93954d8
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gerrit_base(gerrit_base_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gerrit_base_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py
new file mode 100755
index 0000000..05161b2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2018 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.
+
+import re
+
+import pytest
+
+import utils
+
+
+JAVA_VER = 11
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_base_image):
+    container_run = container_endless_run_factory(docker_client, gerrit_base_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_base_inherits_from_base(gerrit_base_image):
+    assert utils.check_if_ancestor_image_is_inherited(gerrit_base_image, "base:latest")
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_contains_java(container_run):
+    _, output = container_run.exec_run("java -version")
+    output = output.strip().decode("utf-8")
+    assert re.search(re.compile(f'openjdk version "{JAVA_VER}.[0-9.]+"'), output)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_java_path(container_run):
+    exit_code, output = container_run.exec_run(
+        '/bin/ash -c "readlink -f $(which java)"'
+    )
+    output = output.strip().decode("utf-8")
+    assert exit_code == 0
+    assert output == f"/usr/lib/jvm/java-{JAVA_VER}-openjdk/bin/java"
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_contains_gerrit_war(container_run):
+    exit_code, _ = container_run.exec_run("test -f /var/war/gerrit.war")
+    assert exit_code == 0
+
+    exit_code, _ = container_run.exec_run("test -f /var/gerrit/bin/gerrit.war")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_war_contains_gerrit(container_run):
+    exit_code, output = container_run.exec_run("java -jar /var/war/gerrit.war version")
+    assert exit_code == 0
+    output = output.strip().decode("utf-8")
+    assert re.search(re.compile("gerrit version.*"), output)
+
+    exit_code, output = container_run.exec_run(
+        "java -jar /var/gerrit/bin/gerrit.war version"
+    )
+    assert exit_code == 0
+    output = output.strip().decode("utf-8")
+    assert re.search(re.compile("gerrit version.*"), output)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_site_permissions(container_run):
+    exit_code, _ = container_run.exec_run("test -O /var/gerrit")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_war_dir_permissions(container_run):
+    exit_code, _ = container_run.exec_run("test -O /var/war")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gerrit_base_has_entrypoint(gerrit_base_image):
+    entrypoint = gerrit_base_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert "/var/tools/start" in entrypoint
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py
new file mode 100644
index 0000000..dc16d74
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gerrit_init(gerrit_init_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gerrit_init_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py
new file mode 100644
index 0000000..4dac6e0
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py
@@ -0,0 +1,190 @@
+# pylint: disable=E1101
+
+# Copyright (C) 2018 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.
+
+import os.path
+import re
+
+from docker.errors import NotFound
+
+import pytest
+import yaml
+
+
+@pytest.fixture(scope="class")
+def container_run_default(request, docker_client, gerrit_init_image, tmp_path_factory):
+    tmp_site_dir = tmp_path_factory.mktemp("gerrit_site")
+    container_run = docker_client.containers.run(
+        image=gerrit_init_image.id,
+        user="gerrit",
+        volumes={tmp_site_dir: {"bind": "/var/gerrit", "mode": "rw"}},
+        detach=True,
+        auto_remove=True,
+        platform="linux/amd64",
+    )
+
+    def stop_container():
+        try:
+            container_run.stop(timeout=1)
+        except NotFound:
+            print("Container already stopped.")
+
+    request.addfinalizer(stop_container)
+
+    return container_run
+
+
+@pytest.fixture(scope="class")
+def init_config_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("init_config")
+
+
+@pytest.fixture(scope="class")
+def tmp_site_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit_site")
+
+
+@pytest.fixture(scope="class")
+def container_run_endless(
+    docker_client, gerrit_init_image, init_config_dir, tmp_site_dir
+):
+    container_run = docker_client.containers.run(
+        image=gerrit_init_image.id,
+        entrypoint="/bin/ash",
+        command=["-c", "tail -f /dev/null"],
+        user="gerrit",
+        volumes={
+            tmp_site_dir: {"bind": "/var/gerrit", "mode": "rw"},
+            init_config_dir: {"bind": "/var/config", "mode": "rw"},
+        },
+        detach=True,
+        auto_remove=True,
+        platform="linux/amd64",
+    )
+
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+class TestGerritInitEmptySite:
+    @pytest.mark.timeout(60)
+    def test_gerrit_init_gerrit_is_initialized(self, container_run_default):
+        def wait_for_init_success_message():
+            log = container_run_default.logs().decode("utf-8")
+            return log, re.search(r"Initialized /var/gerrit", log)
+
+        while not wait_for_init_success_message():
+            continue
+
+    @pytest.mark.timeout(60)
+    def test_gerrit_init_exits_after_init(self, container_run_default):
+        assert container_run_default.wait()["StatusCode"] == 0
+
+
+@pytest.fixture(
+    scope="function",
+    params=[
+        ["replication", "reviewnotes"],
+        ["replication", "reviewnotes", "hooks"],
+        ["download-commands"],
+        [],
+    ],
+)
+def plugins_to_install(request):
+    return request.param
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+class TestGerritInitPluginInstallation:
+    def _configure_packaged_plugins(self, file_path, plugins):
+        with open(file_path, "w", encoding="utf-8") as f:
+            yaml.dump(
+                {"plugins": [{"name": p} for p in plugins]}, f, default_flow_style=False
+            )
+
+    def test_gerrit_init_plugins_are_installed(
+        self,
+        container_run_endless,
+        init_config_dir,
+        plugins_to_install,
+        tmp_site_dir,
+        required_plugins,
+    ):
+        self._configure_packaged_plugins(
+            os.path.join(init_config_dir, "init.yaml"), plugins_to_install
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+        )
+        assert exit_code == 0
+
+        plugins_path = os.path.join(tmp_site_dir, "plugins")
+
+        for plugin in plugins_to_install:
+            assert os.path.exists(os.path.join(plugins_path, f"{plugin}.jar"))
+
+        installed_plugins = os.listdir(plugins_path)
+        expected_plugins = plugins_to_install + required_plugins
+        for plugin in installed_plugins:
+            assert os.path.splitext(plugin)[0] in expected_plugins
+
+    def test_required_plugins_are_installed(
+        self, container_run_endless, init_config_dir, tmp_site_dir, required_plugins
+    ):
+        self._configure_packaged_plugins(
+            os.path.join(init_config_dir, "init.yaml"), ["hooks"]
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+        )
+        assert exit_code == 0
+
+        for plugin in required_plugins:
+            assert os.path.exists(
+                os.path.join(tmp_site_dir, "plugins", f"{plugin}.jar")
+            )
+
+    def test_libraries_are_symlinked(
+        self, container_run_endless, init_config_dir, tmp_site_dir
+    ):
+        with open(
+            os.path.join(init_config_dir, "init.yaml"), "w", encoding="utf-8"
+        ) as f:
+            yaml.dump(
+                {"plugins": [{"name": "hooks", "installAsLibrary": True}]},
+                f,
+                default_flow_style=False,
+            )
+
+        exit_code, _ = container_run_endless.exec_run(
+            "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+        )
+        assert exit_code == 0
+
+        assert os.path.exists(os.path.join(tmp_site_dir, "plugins", "hooks.jar"))
+        assert os.path.islink(os.path.join(tmp_site_dir, "lib", "hooks.jar"))
+
+        exit_code, output = container_run_endless.exec_run(
+            "readlink -f /var/gerrit/lib/hooks.jar"
+        )
+        assert exit_code == 0
+        assert output.decode("utf-8").strip() == "/var/gerrit/plugins/hooks.jar"
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py
new file mode 100644
index 0000000..c8a5b49
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py
@@ -0,0 +1,160 @@
+# pylint: disable=E1101
+
+# Copyright (C) 2018 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.
+
+import os
+
+import pytest
+
+
+@pytest.fixture(scope="function")
+def temp_site(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-index-test")
+
+
+@pytest.fixture(scope="function")
+def container_run_endless(request, docker_client, gerrit_init_image, temp_site):
+    container_run = docker_client.containers.run(
+        image=gerrit_init_image.id,
+        entrypoint="/bin/ash",
+        command=["-c", "tail -f /dev/null"],
+        volumes={str(temp_site): {"bind": "/var/gerrit", "mode": "rw"}},
+        user="gerrit",
+        detach=True,
+        auto_remove=True,
+        platform="linux/amd64",
+    )
+
+    def stop_container():
+        container_run.stop(timeout=1)
+
+    request.addfinalizer(stop_container)
+
+    return container_run
+
+
+@pytest.mark.incremental
+class TestGerritReindex:
+    def _get_indices(self, container):
+        _, indices = container.exec_run(
+            "git config -f /var/gerrit/index/gerrit_index.config "
+            + "--name-only "
+            + "--get-regexp index"
+        )
+        indices = indices.decode().strip().splitlines()
+        return [index.split(".")[1] for index in indices]
+
+    def test_gerrit_init_skips_reindexing_on_fresh_site(
+        self, temp_site, container_run_endless
+    ):
+        assert not os.path.exists(
+            os.path.join(temp_site, "index", "gerrit_index.config")
+        )
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+        assert exit_code == 0
+        expected_files = ["gerrit_index.config"] + self._get_indices(
+            container_run_endless
+        )
+        for expected_file in expected_files:
+            assert os.path.exists(os.path.join(temp_site, "index", expected_file))
+
+        timestamp_index_dir = os.path.getctime(os.path.join(temp_site, "index"))
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+        assert timestamp_index_dir == os.path.getctime(os.path.join(temp_site, "index"))
+
+    def test_gerrit_init_fixes_missing_index_config(
+        self, container_run_endless, temp_site
+    ):
+        container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+        os.remove(os.path.join(temp_site, "index", "gerrit_index.config"))
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+
+        exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+        assert exit_code == 0
+
+    def test_gerrit_init_fixes_not_ready_indices(self, container_run_endless):
+        container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+
+        indices = self._get_indices(container_run_endless)
+        assert indices
+        container_run_endless.exec_run(
+            f"git config -f /var/gerrit/index/gerrit_index.config {indices[0]} false"
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+
+        exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+        assert exit_code == 0
+
+    def test_gerrit_init_fixes_outdated_indices(self, container_run_endless, temp_site):
+        container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+
+        index = self._get_indices(container_run_endless)[0]
+        (name, version) = index.split("_")
+        os.rename(
+            os.path.join(temp_site, "index", index),
+            os.path.join(temp_site, "index", f"{name}_{int(version) - 1:04d}"),
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+
+        exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+        assert exit_code == 0
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
new file mode 100755
index 0000000..5861a5e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_init_image):
+    container_run = container_endless_run_factory(docker_client, gerrit_init_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+@pytest.fixture(
+    scope="function",
+    params=[
+        "/var/tools/gerrit-initializer/__main__.py",
+        "/var/tools/gerrit-initializer/main.py",
+    ],
+)
+def expected_script(request):
+    return request.param
+
+
+@pytest.fixture(scope="function", params=["python3"])
+def expected_tool(request):
+    return request.param
+
+
+@pytest.fixture(scope="function", params=["pyyaml", "requests"])
+def expected_pip_package(request):
+    return request.param
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_init_inherits_from_gerrit_base(gerrit_init_image):
+    assert utils.check_if_ancestor_image_is_inherited(
+        gerrit_init_image, "gerrit-base:latest"
+    )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_init_contains_expected_scripts(container_run, expected_script):
+    exit_code, _ = container_run.exec_run(f"test -f {expected_script}")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_init_contains_expected_tools(container_run, expected_tool):
+    exit_code, _ = container_run.exec_run(f"which {expected_tool}")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gerrit_init_has_entrypoint(gerrit_init_image):
+    entrypoint = gerrit_init_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert len(entrypoint) >= 1
+    assert entrypoint == ["python3", "/var/tools/gerrit-initializer"]
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py
new file mode 100644
index 0000000..a2c3dd5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gerrit(gerrit_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gerrit_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py
new file mode 100644
index 0000000..9376a4a
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py
@@ -0,0 +1,108 @@
+# pylint: disable=W0613, E1101
+
+# Copyright (C) 2018 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.
+
+import re
+import time
+
+import pytest
+import requests
+
+
+@pytest.fixture(scope="module")
+def tmp_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-test")
+
+
+@pytest.fixture(scope="class")
+def container_run(
+    docker_client,
+    docker_network,
+    tmp_dir,
+    gerrit_image,
+    gerrit_container_factory,
+    free_port,
+):
+    configs = {
+        "gerrit.config": """
+      [gerrit]
+        basePath = git
+
+      [httpd]
+        listenUrl = http://*:8080
+
+      [test]
+        success = True
+      """,
+        "secure.config": """
+      [test]
+        success = True
+      """,
+        "replication.config": """
+      [test]
+        success = True
+      """,
+    }
+    test_setup = gerrit_container_factory(
+        docker_client, docker_network, tmp_dir, gerrit_image, configs, free_port
+    )
+    test_setup.start()
+
+    yield test_setup
+
+    test_setup.stop()
+
+
+@pytest.fixture(params=["gerrit.config", "secure.config", "replication.config"])
+def config_file_to_test(request):
+    return request.param
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.slow
+class TestGerritStartScript:
+    @pytest.mark.timeout(60)
+    def test_gerrit_gerrit_starts_up(self, container_run):
+        def wait_for_gerrit_start():
+            log = container_run.container.logs().decode("utf-8")
+            return re.search(r"Gerrit Code Review .+ ready", log)
+
+        while not wait_for_gerrit_start:
+            continue
+
+    def test_gerrit_custom_gerrit_config_available(
+        self, container_run, config_file_to_test
+    ):
+        exit_code, output = container_run.container.exec_run(
+            f"git config --file=/var/gerrit/etc/{config_file_to_test} --get test.success"
+        )
+        output = output.decode("utf-8").strip()
+        assert exit_code == 0
+        assert output == "True"
+
+    @pytest.mark.timeout(60)
+    def test_gerrit_httpd_is_responding(self, container_run):
+        status = None
+        while not status == 200:
+            try:
+                response = requests.get(f"http://localhost:{container_run.port}")
+                status = response.status_code
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert response.status_code == 200
+        assert re.search(r'content="Gerrit Code Review"', response.text)
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py
new file mode 100644
index 0000000..3673eab
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py
@@ -0,0 +1,122 @@
+# pylint: disable=W0613, E1101
+
+# Copyright (C) 2018 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.
+
+import os
+import os.path
+import re
+
+import pygit2 as git
+import pytest
+import requests
+
+CONFIG_FILES = ["gerrit.config", "secure.config"]
+
+
+@pytest.fixture(scope="module")
+def tmp_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-replica-test")
+
+
+@pytest.fixture(scope="class")
+def container_run(
+    request,
+    docker_client,
+    docker_network,
+    tmp_dir,
+    gerrit_image,
+    gerrit_container_factory,
+    free_port,
+):
+    configs = {
+        "gerrit.config": """
+      [gerrit]
+        basePath = git
+
+      [httpd]
+        listenUrl = http://*:8080
+
+      [container]
+        replica = true
+
+      [test]
+        success = True
+      """,
+        "secure.config": """
+      [test]
+          success = True
+      """,
+    }
+
+    test_setup = gerrit_container_factory(
+        docker_client, docker_network, tmp_dir, gerrit_image, configs, free_port
+    )
+    test_setup.start()
+
+    request.addfinalizer(test_setup.stop)
+
+    return test_setup
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.slow
+class TestGerritReplica:
+    @pytest.fixture(params=CONFIG_FILES)
+    def config_file_to_test(self, request):
+        return request.param
+
+    @pytest.fixture(params=["All-Users.git", "All-Projects.git"])
+    def expected_repository(self, request):
+        return request.param
+
+    @pytest.mark.timeout(60)
+    def test_gerrit_replica_gerrit_starts_up(self, container_run):
+        def wait_for_gerrit_start():
+            log = container_run.container.logs().decode("utf-8")
+            return re.search(r"Gerrit Code Review .+ ready", log)
+
+        while not wait_for_gerrit_start():
+            continue
+
+    def test_gerrit_replica_custom_gerrit_config_available(
+        self, container_run, config_file_to_test
+    ):
+        exit_code, output = container_run.container.exec_run(
+            f"git config --file=/var/gerrit/etc/{config_file_to_test} --get test.success"
+        )
+        output = output.decode("utf-8").strip()
+        assert exit_code == 0
+        assert output == "True"
+
+    def test_gerrit_replica_repository_exists(self, container_run, expected_repository):
+        exit_code, _ = container_run.container.exec_run(
+            f"test -d /var/gerrit/git/{expected_repository}"
+        )
+        assert exit_code == 0
+
+    def test_gerrit_replica_clone_repo_works(self, container_run, tmp_path_factory):
+        container_run.container.exec_run("git init --bare /var/gerrit/git/test.git")
+        clone_dest = tmp_path_factory.mktemp("gerrit_replica_clone_test")
+        repo = git.clone_repository(
+            f"http://localhost:{container_run.port}/test.git", clone_dest
+        )
+        assert repo.path == os.path.join(clone_dest, ".git/")
+
+    def test_gerrit_replica_webui_not_accessible(self, container_run):
+        response = requests.get(f"http://localhost:{container_run.port}")
+        assert response.status_code == 404
+        assert response.text == "Not Found"
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py
new file mode 100755
index 0000000..7ece25e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_image):
+    container_run = container_endless_run_factory(docker_client, gerrit_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_inherits_from_gerrit_base(gerrit_image):
+    assert utils.check_if_ancestor_image_is_inherited(
+        gerrit_image, "gerrit-base:latest"
+    )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_contains_start_script(container_run):
+    exit_code, _ = container_run.exec_run("test -f /var/tools/start")
+    assert exit_code == 0
diff --git a/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py
new file mode 100644
index 0000000..a640d20
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gitgc(gitgc_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gitgc_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py
new file mode 100644
index 0000000..9f03644
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2018 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.
+
+import pytest
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gitgc_image):
+    container_run = container_endless_run_factory(docker_client, gitgc_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gitgc_inherits_from_base(gitgc_image):
+    assert utils.check_if_ancestor_image_is_inherited(gitgc_image, "base:latest")
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gitgc_log_dir_writable_by_gerrit(container_run):
+    exit_code, _ = container_run.exec_run("touch /var/log/git/test.log")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gitgc_contains_gc_script(container_run):
+    exit_code, _ = container_run.exec_run("test -f /var/tools/gc.sh")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gitgc_has_entrypoint(gitgc_image):
+    entrypoint = gitgc_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert len(entrypoint) == 1
+    assert entrypoint[0] == "/var/tools/gc.sh"
diff --git a/charts/k8s-gerrit/tests/fixtures/__init__.py b/charts/k8s-gerrit/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/__init__.py
diff --git a/charts/k8s-gerrit/tests/fixtures/cluster.py b/charts/k8s-gerrit/tests/fixtures/cluster.py
new file mode 100644
index 0000000..eb94968
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/cluster.py
@@ -0,0 +1,144 @@
+# pylint: disable=W0613
+
+# 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.
+
+import base64
+import json
+import warnings
+
+from kubernetes import client, config
+
+import pytest
+
+from .helm.client import HelmClient
+
+
+class Cluster:
+    def __init__(self, kube_config):
+        self.kube_config = kube_config
+
+        self.image_pull_secrets = []
+        self.namespaces = []
+
+        context = self._load_kube_config()
+        self.helm = HelmClient(self.kube_config, context)
+
+    def _load_kube_config(self):
+        config.load_kube_config(config_file=self.kube_config)
+        _, context = config.list_kube_config_contexts(config_file=self.kube_config)
+        return context["name"]
+
+    def _apply_image_pull_secrets(self, namespace):
+        for ips in self.image_pull_secrets:
+            try:
+                client.CoreV1Api().create_namespaced_secret(namespace, ips)
+            except client.rest.ApiException as exc:
+                if exc.status == 409 and exc.reason == "Conflict":
+                    warnings.warn(
+                        "Kubernetes Cluster not empty. Image pull secret already exists."
+                    )
+                else:
+                    raise exc
+
+    def add_container_registry(self, secret_name, url, user, pwd):
+        data = {
+            "auths": {
+                url: {
+                    "auth": base64.b64encode(str.encode(f"{user}:{pwd}")).decode(
+                        "utf-8"
+                    )
+                }
+            }
+        }
+        metadata = client.V1ObjectMeta(name=secret_name)
+        self.image_pull_secrets.append(
+            client.V1Secret(
+                api_version="v1",
+                kind="Secret",
+                metadata=metadata,
+                type="kubernetes.io/dockerconfigjson",
+                data={
+                    ".dockerconfigjson": base64.b64encode(
+                        json.dumps(data).encode()
+                    ).decode("utf-8")
+                },
+            )
+        )
+
+    def create_namespace(self, name):
+        namespace_metadata = client.V1ObjectMeta(name=name)
+        namespace_body = client.V1Namespace(
+            kind="Namespace", api_version="v1", metadata=namespace_metadata
+        )
+        client.CoreV1Api().create_namespace(body=namespace_body)
+        self.namespaces.append(name)
+        self._apply_image_pull_secrets(name)
+
+    def delete_namespace(self, name):
+        if name not in self.namespaces:
+            return
+
+        client.CoreV1Api().delete_namespace(name, body=client.V1DeleteOptions())
+        self.namespaces.remove(name)
+
+    def cleanup(self):
+        while self.namespaces:
+            self.helm.delete_all(
+                namespace=self.namespaces[0],
+            )
+            self.delete_namespace(self.namespaces[0])
+
+
+@pytest.fixture(scope="session")
+def test_cluster(request):
+    kube_config = request.config.getoption("--kubeconfig")
+
+    test_cluster = Cluster(kube_config)
+    test_cluster.add_container_registry(
+        "image-pull-secret",
+        request.config.getoption("--registry"),
+        request.config.getoption("--registry-user"),
+        request.config.getoption("--registry-pwd"),
+    )
+
+    yield test_cluster
+
+    test_cluster.cleanup()
+
+
+@pytest.fixture(scope="session")
+def ldap_credentials(test_cluster):
+    ldap_secret = client.CoreV1Api().read_namespaced_secret(
+        "openldap-users", namespace="openldap"
+    )
+    users = base64.b64decode(ldap_secret.data["users"]).decode("utf-8").split(",")
+    passwords = (
+        base64.b64decode(ldap_secret.data["passwords"]).decode("utf-8").split(",")
+    )
+    credentials = {}
+    for i, user in enumerate(users):
+        credentials[user] = passwords[i]
+
+    yield credentials
+
+
+@pytest.fixture(scope="session")
+def ldap_admin_credentials(test_cluster):
+    ldap_secret = client.CoreV1Api().read_namespaced_secret(
+        "openldap-admin", namespace="openldap"
+    )
+    password = base64.b64decode(ldap_secret.data["adminpassword"]).decode("utf-8")
+
+    yield ("admin", password)
diff --git a/charts/k8s-gerrit/tests/fixtures/credentials.py b/charts/k8s-gerrit/tests/fixtures/credentials.py
new file mode 100644
index 0000000..de39dc1
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/credentials.py
@@ -0,0 +1,39 @@
+# pylint: disable=W0613
+
+# 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.
+
+import os
+
+import pytest
+
+from passlib.apache import HtpasswdFile
+
+import utils
+
+
+@pytest.fixture(scope="session")
+def credentials_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("creds")
+
+
+@pytest.fixture(scope="session")
+def htpasswd(credentials_dir):
+    basic_auth_creds = {"user": "admin", "password": utils.create_random_string(16)}
+    htpasswd_file = HtpasswdFile(os.path.join(credentials_dir, ".htpasswd"), new=True)
+    htpasswd_file.set_password(basic_auth_creds["user"], basic_auth_creds["password"])
+    htpasswd_file.save()
+    basic_auth_creds["htpasswd_string"] = htpasswd_file.to_string()
+    basic_auth_creds["htpasswd_file"] = credentials_dir
+    yield basic_auth_creds
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/__init__.py b/charts/k8s-gerrit/tests/fixtures/helm/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/__init__.py
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py b/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py
new file mode 100644
index 0000000..517cfe2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py
@@ -0,0 +1,99 @@
+import abc
+import random
+import re
+import string
+
+from time import time
+
+from kubernetes import client
+
+
+class AbstractDeployment(abc.ABC):
+    def __init__(self, tmp_dir):
+        self.tmp_dir = tmp_dir
+        self.namespace = "".join(
+            [random.choice(string.ascii_letters) for n in range(8)]
+        ).lower()
+        self.values_file = self._set_values_file()
+        self.chart_opts = {}
+
+    @abc.abstractmethod
+    def install(self, wait=True):
+        pass
+
+    @abc.abstractmethod
+    def update(self):
+        pass
+
+    @abc.abstractmethod
+    def uninstall(self):
+        pass
+
+    @abc.abstractmethod
+    def _set_values_file(self):
+        pass
+
+    def set_helm_value(self, combined_key, value):
+        nested_keys = re.split(r"(?<!\\)\.", combined_key)
+        dct_pointer = self.chart_opts
+        for key in nested_keys[:-1]:
+            # pylint: disable=W1401
+            key.replace("\.", ".")
+            dct_pointer = dct_pointer.setdefault(key, {})
+        # pylint: disable=W1401
+        dct_pointer[nested_keys[-1].replace("\.", ".")] = value
+
+    def _wait_for_pod_readiness(self, pod_labels, timeout=180):
+        """Helper function that can be used to wait for all pods with a given set of
+        labels to be ready.
+
+        Arguments:
+        pod_labels {str} -- Label selector string to be used to select pods.
+            (https://kubernetes.io/docs/concepts/overview/working-with-objects/\
+                labels/#label-selectors)
+
+        Keyword Arguments:
+        timeout {int} -- Time in seconds to wait for the pod status to become ready.
+            (default: {180})
+
+        Returns:
+        boolean -- Whether pods were ready in time.
+        """
+
+        def check_pod_readiness():
+            core_v1 = client.CoreV1Api()
+            pod_list = core_v1.list_pod_for_all_namespaces(
+                watch=False, label_selector=pod_labels
+            )
+            for pod in pod_list.items:
+                for condition in pod.status.conditions:
+                    if condition.type != "Ready" and condition.status != "True":
+                        return False
+            return True
+
+        return self._exec_fn_with_timeout(check_pod_readiness, limit=timeout)
+
+    def _exec_fn_with_timeout(self, func, limit=60):
+        """Helper function that executes a given function until it returns True or a
+        given time limit is reached.
+
+        Arguments:
+        func {function} -- Function to execute. The function can return some output
+                        (or None) and as a second return value a boolean indicating,
+                        whether the event the function was waiting for has happened.
+
+        Keyword Arguments:
+        limit {int} -- Maximum time in seconds to wait for a positive response of
+                        the function (default: {60})
+
+        Returns:
+        boolean -- False, if the timeout was reached
+        any -- Last output of fn
+        """
+
+        timeout = time() + limit
+        while time() < timeout:
+            is_finished = func()
+            if is_finished:
+                return True
+        return False
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/client.py b/charts/k8s-gerrit/tests/fixtures/helm/client.py
new file mode 100644
index 0000000..eb3285f
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/client.py
@@ -0,0 +1,202 @@
+# Copyright (C) 2019 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.
+
+import json
+import subprocess
+
+
+class HelmClient:
+    def __init__(self, kubeconfig, kubecontext):
+        """Wrapper for Helm CLI.
+
+        Arguments:
+            kubeconfig {str} -- Path to kubeconfig-file describing the cluster to
+                                connect to.
+            kubecontext {str} -- Name of the context to use.
+        """
+
+        self.kubeconfig = kubeconfig
+        self.kubecontext = kubecontext
+
+    def _exec_command(self, cmd, fail_on_err=True):
+        base_cmd = [
+            "helm",
+            "--kubeconfig",
+            self.kubeconfig,
+            "--kube-context",
+            self.kubecontext,
+        ]
+        return subprocess.run(
+            base_cmd + cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            check=fail_on_err,
+            text=True,
+        )
+
+    def install(
+        self,
+        chart,
+        name,
+        values_file=None,
+        set_values=None,
+        namespace=None,
+        fail_on_err=True,
+        wait=True,
+    ):
+        """Installs a chart on the cluster
+
+        Arguments:
+            chart {str} -- Release name or path of a helm chart
+            name {str} -- Name with which the chart will be installed on the cluster
+
+        Keyword Arguments:
+            values_file {str} -- Path to a custom values.yaml file (default: {None})
+            set_values {dict} -- Dictionary containing key-value-pairs that are used
+                                to overwrite values in the values.yaml-file.
+                                (default: {None})
+            namespace {str} -- Namespace to install the release into (default: {default})
+            fail_on_err {bool} -- Whether to fail with an exception if the installation
+                                fails (default: {True})
+            wait {bool} -- Whether to wait for all pods to be ready (default: {True})
+
+        Returns:
+            CompletedProcess -- CompletedProcess-object returned by subprocess
+                                containing details about the result and output of the
+                                executed command.
+        """
+
+        helm_cmd = ["install", name, chart, "--dependency-update"]
+        if values_file:
+            helm_cmd.extend(("-f", values_file))
+        if set_values:
+            opt_list = [f"{k}={v}" for k, v in set_values.items()]
+            helm_cmd.extend(("--set", ",".join(opt_list)))
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
+        if wait:
+            helm_cmd.append("--wait")
+        return self._exec_command(helm_cmd, fail_on_err)
+
+    def list(self, namespace=None):
+        """Lists helm charts installed on the cluster.
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace (default: {None})
+
+        Returns:
+            list -- List of helm chart realeases installed on the cluster.
+        """
+
+        helm_cmd = ["list", "--all", "--output", "json"]
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
+        output = self._exec_command(helm_cmd).stdout
+        return json.loads(output)
+
+    def upgrade(
+        self,
+        chart,
+        name,
+        namespace,
+        values_file=None,
+        set_values=None,
+        reuse_values=True,
+        fail_on_err=True,
+    ):
+        """Updates a chart on the cluster
+
+        Arguments:
+            chart {str} -- Release name or path of a helm chart
+            name {str} -- Name with which the chart will be installed on the cluster
+            namespace {str} -- Kubernetes namespace
+
+        Keyword Arguments:
+            values_file {str} -- Path to a custom values.yaml file (default: {None})
+            set_values {dict} -- Dictionary containing key-value-pairs that are used
+                                to overwrite values in the values.yaml-file.
+                                (default: {None})
+            reuse_values {bool} -- Whether to reuse existing not overwritten values
+                                (default: {True})
+            fail_on_err {bool} -- Whether to fail with an exception if the installation
+                                fails (default: {True})
+
+        Returns:
+            CompletedProcess -- CompletedProcess-object returned by subprocess
+                                containing details about the result and output of the
+                                executed command.
+        """
+        helm_cmd = ["upgrade", name, chart, "--namespace", namespace, "--wait"]
+        if values_file:
+            helm_cmd.extend(("-f", values_file))
+        if reuse_values:
+            helm_cmd.append("--reuse-values")
+        if set_values:
+            opt_list = [f"{k}={v}" for k, v in set_values.items()]
+            helm_cmd.extend(("--set", ",".join(opt_list)))
+        return self._exec_command(helm_cmd, fail_on_err)
+
+    def delete(self, name, namespace=None):
+        """Deletes a chart from the cluster
+
+        Arguments:
+            name {str} -- Name of the chart to delete
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace (default: {None})
+
+        Returns:
+            CompletedProcess -- CompletedProcess-object returned by subprocess
+                                containing details about the result and output of
+                                the executed command.
+        """
+
+        if name not in self.list(namespace):
+            return None
+
+        helm_cmd = ["delete", name]
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
+        return self._exec_command(helm_cmd)
+
+    def delete_all(self, namespace=None, exceptions=None):
+        """Deletes all charts on the cluster
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace (default: {None})
+            exceptions {list} -- List of chart names not to delete (default: {None})
+        """
+
+        charts = self.list(namespace)
+        for chart in charts:
+            if exceptions and chart["name"] in exceptions:
+                continue
+            self.delete(chart["name"], namespace)
+
+    def is_installed(self, namespace, chart):
+        """Checks if a chart is installed in the cluster
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace
+            chart {str} -- Name of the chart
+
+        Returns:
+            bool -- Whether the chart is installed
+        """
+
+        for installed_chart in self.list(namespace):
+            if installed_chart["name"] == chart:
+                return True
+
+        return False
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py b/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py
new file mode 100644
index 0000000..ec7a7c1
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py
@@ -0,0 +1,279 @@
+# 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.
+
+import os.path
+
+from copy import deepcopy
+from pathlib import Path
+
+import pytest
+import yaml
+
+import pygit2 as git
+import chromedriver_autoinstaller
+from kubernetes import client
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+
+from .abstract_deployment import AbstractDeployment
+
+
+class TimeOutException(Exception):
+    """Exception to be raised, if some action does not finish in time."""
+
+
+def dict_to_git_config(config_dict):
+    config = ""
+    for section, options in config_dict.items():
+        config += f"[{section}]\n"
+        for key, value in options.items():
+            if isinstance(value, bool):
+                value = "true" if value else "false"
+            elif isinstance(value, list):
+                for opt in value:
+                    config += f"  {key} = {opt}\n"
+                continue
+            config += f"  {key} = {value}\n"
+    return config
+
+
+GERRIT_STARTUP_TIMEOUT = 240
+
+DEFAULT_GERRIT_CONFIG = {
+    "auth": {
+        "type": "LDAP",
+    },
+    "container": {
+        "user": "gerrit",
+        "javaHome": "/usr/lib/jvm/java-11-openjdk",
+        "javaOptions": [
+            "-Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore",
+            "-Xms200m",
+            "-Xmx4g",
+        ],
+    },
+    "gerrit": {
+        "basePath": "git",
+        "canonicalWebUrl": "http://example.com/",
+        "serverId": "gerrit-1",
+    },
+    "httpd": {
+        "listenUrl": "proxy-https://*:8080/",
+        "requestLog": True,
+        "gracefulStopTimeout": "1m",
+    },
+    "index": {"type": "LUCENE", "onlineUpgrade": False},
+    "ldap": {
+        "server": "ldap://openldap.openldap.svc.cluster.local:1389",
+        "accountbase": "dc=example,dc=org",
+        "username": "cn=admin,dc=example,dc=org",
+    },
+    "sshd": {"listenAddress": "off"},
+}
+
+DEFAULT_VALUES = {
+    "gitRepositoryStorage": {"externalPVC": {"use": True, "name": "repo-storage"}},
+    "gitGC": {"logging": {"persistence": {"enabled": False}}},
+    "gerrit": {
+        "etc": {"config": {"gerrit.config": dict_to_git_config(DEFAULT_GERRIT_CONFIG)}}
+    },
+}
+
+
+# pylint: disable=R0902
+class GerritDeployment(AbstractDeployment):
+    def __init__(
+        self,
+        tmp_dir,
+        cluster,
+        storageclass,
+        container_registry,
+        container_org,
+        container_version,
+        ingress_url,
+        ldap_admin_credentials,
+        ldap_credentials,
+    ):
+        super().__init__(tmp_dir)
+        self.cluster = cluster
+        self.storageclass = storageclass
+        self.ldap_credentials = ldap_credentials
+
+        self.chart_name = "gerrit-" + self.namespace
+        self.chart_path = os.path.join(
+            # pylint: disable=E1101
+            Path(git.discover_repository(os.path.realpath(__file__))).parent.absolute(),
+            "helm-charts",
+            "gerrit",
+        )
+
+        self.gerrit_config = deepcopy(DEFAULT_GERRIT_CONFIG)
+        self.chart_opts = deepcopy(DEFAULT_VALUES)
+
+        self._configure_container_images(
+            container_registry, container_org, container_version
+        )
+        self.hostname = f"{self.namespace}.{ingress_url}"
+        self._configure_ingress()
+        self.set_gerrit_config_value(
+            "gerrit", "canonicalWebUrl", f"http://{self.hostname}"
+        )
+        # pylint: disable=W1401
+        self.set_helm_value(
+            "gerrit.etc.secret.secure\.config",
+            dict_to_git_config({"ldap": {"password": ldap_admin_credentials[1]}}),
+        )
+
+    def install(self, wait=True):
+        if self.cluster.helm.is_installed(self.namespace, self.chart_name):
+            self.update()
+            return
+
+        with open(self.values_file, "w", encoding="UTF-8") as f:
+            yaml.dump(self.chart_opts, f)
+
+        self.cluster.create_namespace(self.namespace)
+        self._create_pvc()
+
+        self.cluster.helm.install(
+            self.chart_path,
+            self.chart_name,
+            values_file=self.values_file,
+            fail_on_err=True,
+            namespace=self.namespace,
+            wait=wait,
+        )
+
+    def create_admin_account(self):
+        self.wait_until_ready()
+        chromedriver_autoinstaller.install()
+        options = webdriver.ChromeOptions()
+        options.add_argument("--headless")
+        options.add_argument("--no-sandbox")
+        options.add_argument("--ignore-certificate-errors")
+        options.set_capability("acceptInsecureCerts", True)
+        driver = webdriver.Chrome(
+            options=options,
+        )
+        driver.get(f"http://{self.hostname}/login")
+        user_input = driver.find_element(By.ID, "f_user")
+        user_input.send_keys("gerrit-admin")
+
+        pwd_input = driver.find_element(By.ID, "f_pass")
+        pwd_input.send_keys(self.ldap_credentials["gerrit-admin"])
+
+        submit_btn = driver.find_element(By.ID, "b_signin")
+        submit_btn.click()
+
+        driver.close()
+
+    def update(self):
+        with open(self.values_file, "w", encoding="UTF-8") as f:
+            yaml.dump(self.chart_opts, f)
+
+        self.cluster.helm.upgrade(
+            self.chart_path,
+            self.chart_name,
+            values_file=self.values_file,
+            fail_on_err=True,
+            namespace=self.namespace,
+        )
+
+    def wait_until_ready(self):
+        pod_labels = f"app=gerrit,release={self.chart_name}"
+        finished_in_time = self._wait_for_pod_readiness(
+            pod_labels, timeout=GERRIT_STARTUP_TIMEOUT
+        )
+
+        if not finished_in_time:
+            raise TimeOutException(
+                f"Gerrit pod was not ready in time ({GERRIT_STARTUP_TIMEOUT} s)."
+            )
+
+    def uninstall(self):
+        self.cluster.helm.delete(self.chart_name, namespace=self.namespace)
+        self.cluster.delete_namespace(self.namespace)
+
+    def set_gerrit_config_value(self, section, key, value):
+        if isinstance(self.gerrit_config[section][key], list):
+            self.gerrit_config[section][key].append(value)
+        else:
+            self.gerrit_config[section][key] = value
+        # pylint: disable=W1401
+        self.set_helm_value(
+            "gerrit.etc.config.gerrit\.config", dict_to_git_config(self.gerrit_config)
+        )
+
+    def _set_values_file(self):
+        return os.path.join(self.tmp_dir, "values.yaml")
+
+    def _configure_container_images(
+        self, container_registry, container_org, container_version
+    ):
+        self.set_helm_value("images.registry.name", container_registry)
+        self.set_helm_value("gitGC.image", f"{container_org}/git-gc")
+        self.set_helm_value("gerrit.images.gerritInit", f"{container_org}/gerrit-init")
+        self.set_helm_value("gerrit.images.gerrit", f"{container_org}/gerrit")
+        self.set_helm_value("images.version", container_version)
+
+    def _configure_ingress(self):
+        self.set_helm_value("ingress.enabled", True)
+        self.set_helm_value("ingress.host", self.hostname)
+
+    def _create_pvc(self):
+        core_v1 = client.CoreV1Api()
+        core_v1.create_namespaced_persistent_volume_claim(
+            self.namespace,
+            body=client.V1PersistentVolumeClaim(
+                kind="PersistentVolumeClaim",
+                api_version="v1",
+                metadata=client.V1ObjectMeta(name="repo-storage"),
+                spec=client.V1PersistentVolumeClaimSpec(
+                    access_modes=["ReadWriteMany"],
+                    storage_class_name=self.storageclass,
+                    resources=client.V1ResourceRequirements(
+                        requests={"storage": "1Gi"}
+                    ),
+                ),
+            ),
+        )
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment(
+    request, tmp_path_factory, test_cluster, ldap_admin_credentials, ldap_credentials
+):
+    deployment = GerritDeployment(
+        tmp_path_factory.mktemp("gerrit_deployment"),
+        test_cluster,
+        request.config.getoption("--rwm-storageclass").lower(),
+        request.config.getoption("--registry"),
+        request.config.getoption("--org"),
+        request.config.getoption("--tag"),
+        request.config.getoption("--ingress-url"),
+        ldap_admin_credentials,
+        ldap_credentials,
+    )
+
+    yield deployment
+
+    deployment.uninstall()
+
+
+@pytest.fixture(scope="class")
+def default_gerrit_deployment(gerrit_deployment):
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
new file mode 100644
index 0000000..62981ac
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
@@ -0,0 +1,343 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 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.
+
+import hashlib
+import json
+import os.path
+import time
+
+import pytest
+import requests
+
+from kubernetes import client
+from kubernetes.stream import stream
+
+PLUGINS = ["avatars-gravatar", "readonly"]
+LIBS = ["global-refdb"]
+GERRIT_VERSION = "3.8"
+
+
+@pytest.fixture(scope="module")
+def plugin_list():
+    plugin_list = []
+    for plugin in PLUGINS:
+        url = (
+            f"https://gerrit-ci.gerritforge.com/view/Plugins-stable-{GERRIT_VERSION}/"
+            f"job/plugin-{plugin}-bazel-master-stable-{GERRIT_VERSION}/lastSuccessfulBuild/"
+            f"artifact/bazel-bin/plugins/{plugin}/{plugin}.jar"
+        )
+        jar = requests.get(url, verify=False).content
+        plugin_list.append(
+            {"name": plugin, "url": url, "sha1": hashlib.sha1(jar).hexdigest()}
+        )
+    return plugin_list
+
+
+@pytest.fixture(scope="module")
+def lib_list():
+    lib_list = []
+    for lib in LIBS:
+        url = (
+            f"https://gerrit-ci.gerritforge.com/view/Plugins-stable-{GERRIT_VERSION}/"
+            f"job/module-{lib}-bazel-stable-{GERRIT_VERSION}/lastSuccessfulBuild/"
+            f"artifact/bazel-bin/plugins/{lib}/{lib}.jar"
+        )
+        jar = requests.get(url, verify=False).content
+        lib_list.append(
+            {"name": lib, "url": url, "sha1": hashlib.sha1(jar).hexdigest()}
+        )
+    return lib_list
+
+
+@pytest.fixture(
+    scope="class",
+    params=[
+        [{"name": "replication"}],
+        [{"name": "replication"}, {"name": "download-commands"}],
+    ],
+    ids=["single-packaged-plugin", "multiple-packaged-plugins"],
+)
+def gerrit_deployment_with_packaged_plugins(request, gerrit_deployment):
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", request.param)
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment, request.param
+
+
+@pytest.fixture(
+    scope="class", params=[1, 2], ids=["single-other-plugin", "multiple-other-plugins"]
+)
+def gerrit_deployment_with_other_plugins(
+    request,
+    plugin_list,
+    gerrit_deployment,
+):
+    selected_plugins = plugin_list[: request.param]
+
+    gerrit_deployment.set_helm_value(
+        "gerrit.pluginManagement.plugins", selected_plugins
+    )
+
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment, selected_plugins
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_libs(
+    request,
+    lib_list,
+    gerrit_deployment,
+):
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.libs", lib_list)
+
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment, lib_list
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_other_plugin_wrong_sha(plugin_list, gerrit_deployment):
+    plugin = plugin_list[0]
+    plugin["sha1"] = "notAValidSha"
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", [plugin])
+
+    gerrit_deployment.install(wait=False)
+
+    yield gerrit_deployment
+
+
+def get_gerrit_plugin_list(gerrit_url, user="admin", password="secret"):
+    list_plugins_url = f"{gerrit_url}/a/plugins/?all"
+    response = requests.get(list_plugins_url, auth=(user, password))
+    if not response.status_code == 200:
+        return None
+    body = response.text
+    return json.loads(body[body.index("\n") + 1 :])
+
+
+def get_gerrit_lib_list(gerrit_deployment):
+    response = (
+        stream(
+            client.CoreV1Api().connect_get_namespaced_pod_exec,
+            gerrit_deployment.chart_name + "-gerrit-stateful-set-0",
+            gerrit_deployment.namespace,
+            command=["/bin/ash", "-c", "ls /var/gerrit/lib"],
+            stdout=True,
+        )
+        .strip()
+        .split()
+    )
+    return [os.path.splitext(r)[0] for r in response]
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestgerritChartPackagedPluginInstall:
+    def _assert_installed_plugins(self, expected_plugins, installed_plugins):
+        for plugin in expected_plugins:
+            plugin_name = plugin["name"]
+            assert plugin_name in installed_plugins
+            assert installed_plugins[plugin_name]["filename"] == f"{plugin_name}.jar"
+
+    @pytest.mark.timeout(300)
+    def test_install_packaged_plugins(
+        self, request, gerrit_deployment_with_packaged_plugins, ldap_credentials
+    ):
+        gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
+        response = None
+        while not response:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        self._assert_installed_plugins(expected_plugins, response)
+
+    @pytest.mark.timeout(300)
+    def test_install_packaged_plugins_are_removed_with_update(
+        self,
+        request,
+        test_cluster,
+        gerrit_deployment_with_packaged_plugins,
+        ldap_credentials,
+    ):
+        gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
+        removed_plugin = expected_plugins.pop()
+
+        gerrit_deployment.set_helm_value(
+            "gerrit.pluginManagement.plugins", expected_plugins
+        )
+        gerrit_deployment.update()
+
+        response = None
+        while True:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+                if response is not None and removed_plugin["name"] not in response:
+                    break
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert removed_plugin["name"] not in response
+        self._assert_installed_plugins(expected_plugins, response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartOtherPluginInstall:
+    def _assert_installed_plugins(self, expected_plugins, installed_plugins):
+        for plugin in expected_plugins:
+            assert plugin["name"] in installed_plugins
+            assert (
+                installed_plugins[plugin["name"]]["filename"] == f"{plugin['name']}.jar"
+            )
+
+    @pytest.mark.timeout(300)
+    def test_install_other_plugins(
+        self, gerrit_deployment_with_other_plugins, ldap_credentials
+    ):
+        gerrit_deployment, expected_plugins = gerrit_deployment_with_other_plugins
+        response = None
+        while not response:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+            except requests.exceptions.ConnectionError:
+                continue
+        self._assert_installed_plugins(expected_plugins, response)
+
+    @pytest.mark.timeout(300)
+    def test_install_other_plugins_are_removed_with_update(
+        self, gerrit_deployment_with_other_plugins, ldap_credentials
+    ):
+        gerrit_deployment, installed_plugins = gerrit_deployment_with_other_plugins
+        removed_plugin = installed_plugins.pop()
+        gerrit_deployment.set_helm_value(
+            "gerrit.pluginManagement.plugins", installed_plugins
+        )
+        gerrit_deployment.update()
+
+        response = None
+        while True:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+                if response is not None and removed_plugin["name"] not in response:
+                    break
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert removed_plugin["name"] not in response
+        self._assert_installed_plugins(installed_plugins, response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartLibModuleInstall:
+    def _assert_installed_libs(self, expected_libs, installed_libs):
+        for lib in expected_libs:
+            assert lib["name"] in installed_libs
+
+    @pytest.mark.timeout(300)
+    def test_install_libs(self, gerrit_deployment_with_libs):
+        gerrit_deployment, expected_libs = gerrit_deployment_with_libs
+        response = get_gerrit_lib_list(gerrit_deployment)
+        self._assert_installed_libs(expected_libs, response)
+
+    @pytest.mark.timeout(300)
+    def test_install_other_plugins_are_removed_with_update(
+        self, gerrit_deployment_with_libs
+    ):
+        gerrit_deployment, installed_libs = gerrit_deployment_with_libs
+        removed_lib = installed_libs.pop()
+        gerrit_deployment.set_helm_value("gerrit.pluginManagement.libs", installed_libs)
+        gerrit_deployment.update()
+
+        response = None
+        while True:
+            try:
+                response = get_gerrit_lib_list(gerrit_deployment)
+                if response is not None and removed_lib["name"] not in response:
+                    break
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert removed_lib["name"] not in response
+        self._assert_installed_libs(installed_libs, response)
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.timeout(180)
+def test_install_other_plugins_fails_wrong_sha(
+    gerrit_deployment_with_other_plugin_wrong_sha,
+):
+    pod_labels = f"app.kubernetes.io/component=gerrit,release={gerrit_deployment_with_other_plugin_wrong_sha.chart_name}"
+    core_v1 = client.CoreV1Api()
+    pod_name = ""
+    while not pod_name:
+        pod_list = core_v1.list_namespaced_pod(
+            namespace=gerrit_deployment_with_other_plugin_wrong_sha.namespace,
+            watch=False,
+            label_selector=pod_labels,
+        )
+        if len(pod_list.items) > 1:
+            raise RuntimeError("Too many gerrit pods with the same release name.")
+        elif len(pod_list.items) == 1:
+            pod_name = pod_list.items[0].metadata.name
+
+    current_status = None
+    while not current_status:
+        pod = core_v1.read_namespaced_pod_status(
+            pod_name, gerrit_deployment_with_other_plugin_wrong_sha.namespace
+        )
+        if not pod.status.init_container_statuses:
+            time.sleep(1)
+            continue
+        for init_container_status in pod.status.init_container_statuses:
+            if (
+                init_container_status.name == "gerrit-init"
+                and init_container_status.last_state.terminated
+            ):
+                current_status = init_container_status
+                assert current_status.last_state.terminated.exit_code > 0
+                return
+
+    assert current_status.last_state.terminated.exit_code > 0
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py
new file mode 100644
index 0000000..306d41c
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py
@@ -0,0 +1,29 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 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.
+
+import pytest
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+def test_deployment(test_cluster, default_gerrit_deployment):
+    installed_charts = test_cluster.helm.list(default_gerrit_deployment.namespace)
+    gerrit_chart = None
+    for chart in installed_charts:
+        if chart["name"].startswith("gerrit"):
+            gerrit_chart = chart
+    assert gerrit_chart is not None
+    assert gerrit_chart["status"].lower() == "deployed"
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py
new file mode 100644
index 0000000..b3ee757
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py
@@ -0,0 +1,110 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 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.
+
+import os.path
+import re
+import shutil
+
+from pathlib import Path
+
+import pygit2 as git
+import pytest
+import requests
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def admin_creds(request):
+    user = request.config.getoption("--gerrit-user")
+    pwd = request.config.getoption("--gerrit-pwd")
+    return user, pwd
+
+
+@pytest.fixture(scope="class")
+def tmp_test_repo(request, tmp_path_factory):
+    tmp_dir = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+    yield tmp_dir
+    shutil.rmtree(tmp_dir)
+
+
+@pytest.fixture(scope="class")
+def random_repo_name():
+    return utils.create_random_string(16)
+
+
+@pytest.mark.smoke
+def test_ui_connection(request):
+    response = requests.get(request.config.getoption("--ingress-url"))
+    assert response.status_code == requests.codes["OK"]
+    assert re.search(r'content="Gerrit Code Review"', response.text)
+
+
+@pytest.mark.smoke
+@pytest.mark.incremental
+class TestGerritRestGitCalls:
+    def _is_delete_project_plugin_enabled(self, gerrit_url, user, pwd):
+        url = f"{gerrit_url}/a/plugins/delete-project/gerrit~status"
+        response = requests.get(url, auth=(user, pwd))
+        return response.status_code == requests.codes["OK"]
+
+    def test_create_project_rest(self, request, random_repo_name, admin_creds):
+        ingress_url = request.config.getoption("--ingress-url")
+        create_project_url = f"{ingress_url}/a/projects/{random_repo_name}"
+        response = requests.put(create_project_url, auth=admin_creds)
+        assert response.status_code == requests.codes["CREATED"]
+
+    def test_cloning_project(
+        self, request, tmp_test_repo, random_repo_name, admin_creds
+    ):
+        repo_url = f"{request.config.getoption('--ingress-url')}/{random_repo_name}.git"
+        repo_url = repo_url.replace("//", f"//{admin_creds[0]}:{admin_creds[1]}@")
+        repo = git.clone_repository(repo_url, tmp_test_repo)
+        assert repo.path == os.path.join(tmp_test_repo, ".git/")
+
+    def test_push_commit(self, tmp_test_repo):
+        repo = git.Repository(tmp_test_repo)
+        file_name = os.path.join(tmp_test_repo, "test.txt")
+        Path(file_name).touch()
+        repo.index.add("test.txt")
+        repo.index.write()
+        # pylint: disable=E1101
+        author = git.Signature("Gerrit Review", "gerrit@review.com")
+        committer = git.Signature("Gerrit Review", "gerrit@review.com")
+        message = "Initial commit"
+        tree = repo.index.write_tree()
+        repo.create_commit("HEAD", author, committer, message, tree, [])
+
+        origin = repo.remotes["origin"]
+        origin.push(["refs/heads/master:refs/heads/master"])
+
+        remote_refs = origin.ls_remotes()
+        assert remote_refs[0]["name"] == repo.revparse_single("HEAD").hex
+
+    def test_delete_project_rest(self, request, random_repo_name, admin_creds):
+        ingress_url = request.config.getoption("--ingress-url")
+        if not self._is_delete_project_plugin_enabled(
+            ingress_url, admin_creds[0], admin_creds[1]
+        ):
+            pytest.skip(
+                "Delete-project plugin not installed."
+                + f"The test project ({random_repo_name}) has to be deleted manually."
+            )
+        project_url = (
+            f"{ingress_url}/a/projects/{random_repo_name}/delete-project~delete"
+        )
+        response = requests.post(project_url, auth=admin_creds)
+        assert response.status_code == requests.codes["NO_CONTENT"]
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py
new file mode 100644
index 0000000..0eee0f4
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2019 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.
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+import git_callbacks
+import mock_ssl
+
+
+@pytest.fixture(scope="module")
+def cert_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-cert")
+
+
+def _create_ssl_certificate(url, cert_dir):
+    keypair = mock_ssl.MockSSLKeyPair("*." + url.split(".", 1)[1], url)
+    with open(os.path.join(cert_dir, "server.crt"), "wb") as f:
+        f.write(keypair.get_cert())
+    with open(os.path.join(cert_dir, "server.key"), "wb") as f:
+        f.write(keypair.get_key())
+    return keypair
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_ssl(cert_dir, gerrit_deployment):
+    ssl_certificate = _create_ssl_certificate(gerrit_deployment.hostname, cert_dir)
+    gerrit_deployment.set_helm_value("ingress.tls.enabled", True)
+    gerrit_deployment.set_helm_value(
+        "ingress.tls.cert", ssl_certificate.get_cert().decode()
+    )
+    gerrit_deployment.set_helm_value(
+        "ingress.tls.key", ssl_certificate.get_key().decode()
+    )
+    gerrit_deployment.set_gerrit_config_value(
+        "httpd", "listenUrl", "proxy-https://*:8080/"
+    )
+    gerrit_deployment.set_gerrit_config_value(
+        "gerrit",
+        "canonicalWebUrl",
+        f"https://{gerrit_deployment.hostname}",
+    )
+
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment
+
+
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.slow
+class TestgerritChartSetup:
+    # pylint: disable=W0613
+    def test_create_project_rest(
+        self, cert_dir, gerrit_deployment_with_ssl, ldap_credentials
+    ):
+        create_project_url = (
+            f"https://{gerrit_deployment_with_ssl.hostname}/a/projects/test"
+        )
+        response = requests.put(
+            create_project_url,
+            auth=("gerrit-admin", ldap_credentials["gerrit-admin"]),
+            verify=os.path.join(cert_dir, "server.crt"),
+        )
+        assert response.status_code == 201
+
+    def test_cloning_project(self, tmp_path_factory, gerrit_deployment_with_ssl):
+        clone_dest = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+        repo_url = f"https://{gerrit_deployment_with_ssl.hostname}/test.git"
+        repo = git.clone_repository(
+            repo_url, clone_dest, callbacks=git_callbacks.TestRemoteCallbacks()
+        )
+        assert repo.path == os.path.join(clone_dest, ".git/")
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py
new file mode 100644
index 0000000..f63d209
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py
@@ -0,0 +1,51 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 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.
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartSetup:
+    @pytest.mark.timeout(240)
+    def test_create_project_rest(self, default_gerrit_deployment, ldap_credentials):
+        create_project_url = (
+            f"http://{default_gerrit_deployment.hostname}/a/projects/test"
+        )
+        response = None
+
+        while not response:
+            try:
+                response = requests.put(
+                    create_project_url,
+                    auth=("gerrit-admin", ldap_credentials["gerrit-admin"]),
+                )
+            except requests.exceptions.ConnectionError:
+                break
+
+        assert response.status_code == 201
+
+    def test_cloning_project(self, tmp_path_factory, default_gerrit_deployment):
+        clone_dest = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+        repo_url = f"http://{default_gerrit_deployment.hostname}/test.git"
+        repo = git.clone_repository(repo_url, clone_dest)
+        assert repo.path == os.path.join(clone_dest, ".git/")
diff --git a/charts/k8s-gerrit/tests/helpers/git_callbacks.py b/charts/k8s-gerrit/tests/helpers/git_callbacks.py
new file mode 100644
index 0000000..3922a24
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/git_callbacks.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2019 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.
+
+import pygit2 as git
+
+
+class TestRemoteCallbacks(git.RemoteCallbacks):
+    def certificate_check(self, certificate, valid, host):
+        return True
diff --git a/charts/k8s-gerrit/tests/helpers/mock_ssl.py b/charts/k8s-gerrit/tests/helpers/mock_ssl.py
new file mode 100644
index 0000000..46d766c
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/mock_ssl.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2019 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.
+
+from OpenSSL import crypto
+
+
+class MockSSLKeyPair:
+    def __init__(self, common_name, subject_alt_name):
+        self.common_name = common_name
+        self.subject_alt_name = subject_alt_name
+        self.cert = None
+        self.key = None
+
+        self._create_keypair()
+
+    def _create_keypair(self):
+        self.key = crypto.PKey()
+        self.key.generate_key(crypto.TYPE_RSA, 2048)
+
+        self.cert = crypto.X509()
+        self.cert.set_version(2)
+        self.cert.get_subject().O = "Gerrit"
+        self.cert.get_subject().CN = self.common_name
+        san = f"DNS:{self.subject_alt_name}"
+        self.cert.add_extensions(
+            [crypto.X509Extension(b"subjectAltName", False, san.encode())]
+        )
+        self.cert.gmtime_adj_notBefore(0)
+        self.cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+        self.cert.set_issuer(self.cert.get_subject())
+        self.cert.set_pubkey(self.key)
+        self.cert.sign(self.key, "sha256")
+
+    def get_key(self):
+        return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
+
+    def get_cert(self):
+        return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
diff --git a/charts/k8s-gerrit/tests/helpers/utils.py b/charts/k8s-gerrit/tests/helpers/utils.py
new file mode 100644
index 0000000..804217e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/utils.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2018 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.
+
+import random
+import string
+
+
+def check_if_ancestor_image_is_inherited(image, ancestor):
+    """Helper function that looks for a given ancestor image in the layers of a
+      provided image. It can be used to check, whether an image uses the expected
+      FROM-statement
+
+    Arguments:
+      image {docker.images.Image} -- Docker image object to be checked
+      ancestor {str} -- Complete name of the expected ancestor image
+
+    Returns:
+      boolean -- True, if ancestor is inherited by image
+    """
+
+    contains_tag = False
+    for layer in image.history():
+        contains_tag = layer["Tags"] is not None and ancestor in layer["Tags"]
+        if contains_tag:
+            break
+    return contains_tag
+
+
+def create_random_string(length=8):
+    return "".join([random.choice(string.ascii_letters) for n in range(length)]).lower()