Repository: openlawlibrary/taf (language: python)

Issue:
Add support for testing repositories with dependencies to the updater testing framework 
These tests cannot be created as easily as other updater tests.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34757b6e..3f322edd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,8 +9,10 @@ and this project adheres to [Semantic Versioning][semver].
 
 ### Added
 
-- Added functionality for parallel execution of child repo during clone and update for performance enhancement [472]
-- Hid plaintext when users are prompted to insert YubiKey and press ENTER [473]
+
+- Testing repositories with dependencies ([479])
+- Added functionality for parallel execution of child repo during clone and update for performance enhancement ([472])
+- Hid plaintext when users are prompted to insert YubiKey and press ENTER ([473])
 - New flag --force allowing forced updates ([471])
 - Improved usability (TAF finds the repo if current directory has no repo, create a .taf directory to manage keys) ([466])
 - Added git hook check for updater ([460])
@@ -32,6 +34,8 @@ and this project adheres to [Semantic Versioning][semver].
 
 ### Fixed
 
+
+[479]: https://github.com/openlawlibrary/taf/pull/479
 [473]: https://github.com/openlawlibrary/taf/pull/473
 [472]: https://github.com/openlawlibrary/taf/pull/472
 [471]: https://github.com/openlawlibrary/taf/pull/471


Test patch:
diff --git a/taf/tests/test_updater/conftest.py b/taf/tests/test_updater/conftest.py
index 58e3c7e5..5b9e68bf 100644
--- a/taf/tests/test_updater/conftest.py
+++ b/taf/tests/test_updater/conftest.py
@@ -47,6 +47,7 @@
 
 
 TARGET_MISSMATCH_PATTERN = r"Update of (\w+)\/(\w+) failed due to error: Failure to validate (\w+)\/(\w+) commit ([0-9a-f]{40}) committed on (\d{4}-\d{2}-\d{2}): data repository ([\w\/-]+) was supposed to be at commit ([0-9a-z]{40}) but repo was at ([0-9a-f]{40})"
+TARGET_MISMATCH_PATTERN_DEPENDENCIES = r"Update of (\w+)/(\w+) failed due to error: Update of (\w+)/(\w+) failed. One or more referenced authentication repositories could not be validated:\n Failure to validate (\w+)/(\w+) commit ([0-9a-f]{40}) committed on (\d{4}-\d{2}-\d{2}): data repository ([\w\/-]+) was supposed to be at commit ([0-9a-f]{40}) but repo was at ([0-9a-f]{40})"
 TARGET_ADDITIONAL_COMMIT_PATTERN = r"Update of (\w+)\/(\w+) failed due to error: Failure to validate (\w+)\/(\w+) commit ([0-9a-f]{40}) committed on (\d{4}-\d{2}-\d{2}): data repository ([\w\/-]+) was supposed to be at commit ([0-9a-f]{40}) but commit not on branch (\w+)"
 TARGET_COMMIT_AFTER_LAST_VALIDATED_PATTERN = r"Update of (\w+)\/(\w+) failed due to error: Target repository ([\w\/-]+) does not allow unauthenticated commits, but contains commit\(s\) ([0-9a-f]{40}(?:, [0-9a-f]{40})*) on branch (\w+)"
 TARGET_MISSING_COMMIT_PATTERN = r"Update of (\w+)/(\w+) failed due to error: Failure to validate (\w+)/(\w+) commit ([0-9a-f]{40}) committed on (\d{4}-\d{2}-\d{2}): data repository ([\w\/-]+) was supposed to be at commit ([0-9a-f]{40}) but commit not on branch (\w+)"
@@ -62,7 +63,9 @@
 UPDATE_ERROR_PATTERN = r"Update of (\w+\/\w+) failed due to error: Validation of authentication repository (\w+\/\w+) failed at revision ([0-9a-f]+) due to error: .*"
 FORCED_UPATE_PATTERN = r"Update of repository failed due to error: Repository ([\w/-]+)/(\w+) has uncommitted changes. Commit and push or revert the changes and run the command again."
 REMOVED_COMMITS_PATTERN = r"Update of (\w+/\w+) failed due to error: Top commit of repository (\w+/\w+) ([0-9a-f]{40}) and is not equal to or newer than last successful commit"
-
+INVALID_TIMESTAMP_PATTERN = r"^Update of (\w+\/\w+) failed due to error: Update of (\w+\/\w+) failed. One or more referenced authentication repositories could not be validated:\n Validation of authentication repository (\w+\/\w+) failed at revision ([0-9a-f]{40}) due to error: timestamp was signed by (\d+)\/(\d+) keys$"
+CANNOT_CLONE_TARGET_PATTERN = r"^Update of (\w+/\w+) failed due to error: Update of (\w+/\w+) failed. One or more referenced authentication repositories could not be validated:\n Cannot clone (\w+/\w+) from any of the following URLs: \['.*'\]$"
+INVALID_TIMESTAMP_PATTERN_ROOT = r"^Update of (\w+\/\w+) failed due to error: Validation of authentication repository (\w+\/\w+) failed at revision ([0-9a-f]{40}) due to error: timestamp was signed by (\d+)\/(\d+) keys$"
 
 # Disable console logging for all tests
 disable_console_logging()
diff --git a/taf/tests/test_updater/test_update_library/test_clone_library.py b/taf/tests/test_updater/test_update_library/test_clone_library.py
deleted file mode 100644
index cab7b1f8..00000000
--- a/taf/tests/test_updater/test_update_library/test_clone_library.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from collections import defaultdict
-from taf.updater.types.update import UpdateType
-from taf.tests.test_updater.update_utils import (
-    check_if_commits_match,
-    check_last_validated_commit,
-    clone_repositories,
-)
-
-
-def test_update_repository_with_dependencies(
-    library_with_dependencies,
-    origin_dir,
-    client_dir,
-):
-    _clone_full_library(
-        library_with_dependencies,
-        origin_dir,
-        client_dir,
-        expected_repo_type=UpdateType.EITHER,
-        excluded_target_globs=None,
-    )
-
-
-def _clone_full_library(
-    library_dict,
-    origin_dir,
-    client_dir,
-    expected_repo_type=UpdateType.EITHER,
-    excluded_target_globs=None,
-):
-
-    all_repositories = []
-    for repo_info in library_dict.values():
-        # Add the auth repository
-        all_repositories.append(repo_info["auth_repo"])
-        # Extend the list with all target repositories
-        all_repositories.extend(repo_info["target_repos"])
-
-    start_head_shas = defaultdict(dict)
-    for repo in all_repositories:
-        for branch in repo.branches():
-            start_head_shas[repo.name][branch] = repo.top_commit_of_branch(branch)
-
-    origin_root_repo = library_dict["root/auth"]["auth_repo"]
-
-    clone_repositories(
-        origin_root_repo,
-        client_dir,
-        expected_repo_type=expected_repo_type,
-    )
-
-    repositories = {}
-    for auth_repo_name, repos in library_dict.items():
-        repositories[auth_repo_name] = repos["auth_repo"]
-        for target_repo in repos["target_repos"]:
-            repositories[target_repo.name] = target_repo
-        check_last_validated_commit(client_dir / repos["auth_repo"].name)
-    check_if_commits_match(
-        repositories, origin_dir, start_head_shas, excluded_target_globs
-    )
diff --git a/taf/tests/test_updater/test_update_library/test_clone_library_invalid.py b/taf/tests/test_updater/test_update_library/test_clone_library_invalid.py
new file mode 100644
index 00000000..a0e28bd7
--- /dev/null
+++ b/taf/tests/test_updater/test_update_library/test_clone_library_invalid.py
@@ -0,0 +1,162 @@
+"""
+# test_update_library.py
+import pytest
+from taf.exceptions import UpdateFailedError
+from taf.tests.test_updater.conftest import (
+    INVALID_TIMESTAMP_PATTERN,
+    INVALID_TIMESTAMP_PATTERN_ROOT,
+    TARGET_MISMATCH_PATTERN_DEPENDENCIES,
+    SetupManager,
+    add_unauthenticated_commits_to_all_target_repos,
+    add_valid_target_commits,
+    update_role_metadata_invalid_signature,
+)
+from taf.updater.types.update import UpdateType
+from taf.tests.test_updater.update_utils import _clone_full_library
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_clone_with_invalid_dependency_repo(
+    library_with_dependencies, origin_dir, client_dir
+):
+    # Invalidate one of the authentication repositories in dependencies
+    dependency_auth_repo = library_with_dependencies["namespace1/auth"]["auth_repo"]
+    setup_manager = SetupManager(dependency_auth_repo)
+    setup_manager.add_task(
+        update_role_metadata_invalid_signature, kwargs={"role": "timestamp"}
+    )
+    setup_manager.execute_tasks()
+
+    # Run the updater which will clone and then update
+    with pytest.raises(UpdateFailedError, match=INVALID_TIMESTAMP_PATTERN):
+        _clone_full_library(
+            library_with_dependencies,
+            origin_dir,
+            client_dir,
+            expected_repo_type=UpdateType.EITHER,
+            excluded_target_globs=None,
+        )
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_clone_invalid_target_repo(
+    library_with_dependencies,
+    origin_dir,
+    client_dir,
+):
+    # Invalidate one of the target repositories
+    auth_repo = library_with_dependencies["namespace1/auth"]["auth_repo"]
+    setup_manager = SetupManager(auth_repo)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.add_task(add_unauthenticated_commits_to_all_target_repos)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.execute_tasks()
+    # Run the updater which will clone and then update
+    with pytest.raises(UpdateFailedError, match=TARGET_MISMATCH_PATTERN_DEPENDENCIES):
+        _clone_full_library(
+            library_with_dependencies,
+            origin_dir,
+            client_dir,
+            expected_repo_type=UpdateType.EITHER,
+            excluded_target_globs=None,
+        )
+
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_clone_with_invalid_root_repo(
+    library_with_dependencies, origin_dir, client_dir
+):
+    # Invalidate the root repository
+    root_repo = library_with_dependencies["root/auth"]["auth_repo"]
+    setup_manager = SetupManager(root_repo)
+    setup_manager = SetupManager(root_repo)
+    setup_manager.add_task(
+        update_role_metadata_invalid_signature, kwargs={"role": "timestamp"}
+    )
+    setup_manager.execute_tasks()
+
+    with pytest.raises(UpdateFailedError, match=INVALID_TIMESTAMP_PATTERN_ROOT):
+        _clone_full_library(
+            library_with_dependencies,
+            origin_dir,
+            client_dir,
+            expected_repo_type=UpdateType.EITHER,
+            excluded_target_globs=None,
+        )
+"""
diff --git a/taf/tests/test_updater/test_update_library/test_clone_library_valid.py b/taf/tests/test_updater/test_update_library/test_clone_library_valid.py
new file mode 100644
index 00000000..96344722
--- /dev/null
+++ b/taf/tests/test_updater/test_update_library/test_clone_library_valid.py
@@ -0,0 +1,45 @@
+import pytest
+from taf.updater.types.update import UpdateType
+from taf.tests.test_updater.update_utils import (
+    _clone_full_library,
+)
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_clone_repository_with_dependencies(
+    library_with_dependencies,
+    origin_dir,
+    client_dir,
+):
+    _clone_full_library(
+        library_with_dependencies,
+        origin_dir,
+        client_dir,
+        expected_repo_type=UpdateType.EITHER,
+        excluded_target_globs=None,
+    )
diff --git a/taf/tests/test_updater/test_update_library/test_update_library_invalid.py b/taf/tests/test_updater/test_update_library/test_update_library_invalid.py
new file mode 100644
index 00000000..d13bcd3a
--- /dev/null
+++ b/taf/tests/test_updater/test_update_library/test_update_library_invalid.py
@@ -0,0 +1,187 @@
+"""
+# test_update_library.py
+import pytest
+from taf.exceptions import UpdateFailedError
+from taf.tests.test_updater.conftest import (
+    INVALID_TIMESTAMP_PATTERN,
+    TARGET_MISMATCH_PATTERN_DEPENDENCIES,
+    SetupManager,
+    add_unauthenticated_commits_to_all_target_repos,
+    add_valid_target_commits,
+    update_role_metadata_invalid_signature,
+)
+from taf.updater.types.update import UpdateType
+from taf.tests.test_updater.update_utils import (
+    _clone_full_library,
+    update_and_validate_repositories,
+)
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_update_with_invalid_dependency_repo(
+    library_with_dependencies, origin_dir, client_dir
+):
+
+    _clone_full_library(
+        library_with_dependencies,
+        origin_dir,
+        client_dir,
+        expected_repo_type=UpdateType.EITHER,
+        excluded_target_globs=None,
+    )
+    # Invalidate one of the authentication repositories in dependencies
+    dependency_auth_repo = library_with_dependencies["namespace1/auth"]["auth_repo"]
+    setup_manager = SetupManager(dependency_auth_repo)
+    setup_manager.add_task(
+        update_role_metadata_invalid_signature, kwargs={"role": "timestamp"}
+    )
+    setup_manager.execute_tasks()
+
+    with pytest.raises(UpdateFailedError, match=INVALID_TIMESTAMP_PATTERN):
+        update_and_validate_repositories(
+            library_with_dependencies, origin_dir, client_dir
+        )
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_update_invalid_target_repo(
+    library_with_dependencies,
+    origin_dir,
+    client_dir,
+):
+    _clone_full_library(
+        library_with_dependencies,
+        origin_dir,
+        client_dir,
+        expected_repo_type=UpdateType.EITHER,
+        excluded_target_globs=None,
+    )
+    # Invalidate one of the target repositories
+    dependency_auth_repo = library_with_dependencies["namespace1/auth"]["auth_repo"]
+    setup_manager = SetupManager(dependency_auth_repo)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.add_task(add_unauthenticated_commits_to_all_target_repos)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.execute_tasks()
+
+    with pytest.raises(UpdateFailedError, match=TARGET_MISMATCH_PATTERN_DEPENDENCIES):
+        update_and_validate_repositories(
+            library_with_dependencies, origin_dir, client_dir
+        )
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_update_all_except_invalid(
+    library_with_dependencies,
+    origin_dir,
+    client_dir,
+):
+    _clone_full_library(
+        library_with_dependencies,
+        origin_dir,
+        client_dir,
+        expected_repo_type=UpdateType.EITHER,
+        excluded_target_globs=None,
+    )
+
+    # Invalidate one of the target repositories of a referenced authentication repository
+    dependency_auth_repo = library_with_dependencies["namespace1/auth"]["auth_repo"]
+    setup_manager = SetupManager(dependency_auth_repo)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.add_task(add_unauthenticated_commits_to_all_target_repos)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.execute_tasks()
+
+    # Run the updater which will clone and then update
+    with pytest.raises(UpdateFailedError, match=TARGET_MISMATCH_PATTERN_DEPENDENCIES):
+        update_and_validate_repositories(
+            library_with_dependencies, origin_dir, client_dir
+        )
+
+    # Push valid commits to another target repository
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.execute_tasks()
+
+    # Validate that the valid repositories were updated successfully, skipping the invalid ones
+    update_and_validate_repositories(
+        library_with_dependencies,
+        origin_dir,
+        client_dir,
+        invalid_target_names=["namespace1/target1"],
+    )
+"""
diff --git a/taf/tests/test_updater/test_update_library/test_update_library_valid.py b/taf/tests/test_updater/test_update_library/test_update_library_valid.py
new file mode 100644
index 00000000..54efbb78
--- /dev/null
+++ b/taf/tests/test_updater/test_update_library/test_update_library_valid.py
@@ -0,0 +1,52 @@
+import pytest
+from taf.tests.test_updater.conftest import SetupManager, add_valid_target_commits
+from taf.updater.types.update import UpdateType
+from taf.tests.test_updater.update_utils import (
+    _clone_full_library,
+    update_and_validate_repositories,
+)
+
+
+@pytest.mark.parametrize(
+    "library_with_dependencies",
+    [
+        {
+            "targets_config": [{"name": "target1"}, {"name": "target2"}],
+            "dependencies_config": [
+                {
+                    "name": "namespace1/auth",
+                    "targets_config": [
+                        {"name": "namespace1/target1"},
+                        {"name": "namespace1/target2"},
+                    ],
+                },
+                {
+                    "name": "namespace2/auth",
+                    "targets_config": [
+                        {"name": "namespace2/target1"},
+                        {"name": "namespace2/target2"},
+                    ],
+                },
+                # Add additional dependencies as needed
+            ],
+        },
+    ],
+    indirect=True,
+)
+def test_update_repository_with_dependencies(
+    library_with_dependencies,
+    origin_dir,
+    client_dir,
+):
+    _clone_full_library(
+        library_with_dependencies,
+        origin_dir,
+        client_dir,
+        expected_repo_type=UpdateType.EITHER,
+        excluded_target_globs=None,
+    )
+    auth_repo = library_with_dependencies["namespace1/auth"]["auth_repo"]
+    setup_manager = SetupManager(auth_repo)
+    setup_manager.add_task(add_valid_target_commits)
+    setup_manager.execute_tasks()
+    update_and_validate_repositories(library_with_dependencies, origin_dir, client_dir)
diff --git a/taf/tests/test_updater/update_utils.py b/taf/tests/test_updater/update_utils.py
index e2cd1505..be8d093b 100644
--- a/taf/tests/test_updater/update_utils.py
+++ b/taf/tests/test_updater/update_utils.py
@@ -1,3 +1,4 @@
+import os
 import pytest
 from freezegun import freeze_time
 from collections import defaultdict
@@ -63,6 +64,52 @@ def check_if_last_validated_commit_exists(client_auth_repo, should_exist):
         )
 
 
+def _clone_full_library(
+    library_dict,
+    origin_dir,
+    client_dir,
+    expected_repo_type=UpdateType.EITHER,
+    excluded_target_globs=None,
+):
+    origin_root_repo = library_dict["root/auth"]["auth_repo"]
+
+    git_dir = os.path.join(origin_root_repo.path, ".git")
+    if not os.path.exists(git_dir):
+        # Log a warning and skip cloning this repository
+        print(
+            "Warning: Update of root/auth failed. One or more referenced authentication repositories could not be validated: "
+            "Repository root/auth is missing .git directory."
+        )
+        return
+
+    all_repositories = []
+    for repo_info in library_dict.values():
+        all_repositories.append(repo_info["auth_repo"])
+        all_repositories.extend(repo_info["target_repos"])
+
+    start_head_shas = defaultdict(dict)
+    for repo in all_repositories:
+        for branch in repo.branches():
+            start_head_shas[repo.name][branch] = repo.top_commit_of_branch(branch)
+
+    clone_repositories(
+        origin_root_repo,
+        client_dir,
+        expected_repo_type=expected_repo_type,
+    )
+
+    repositories = {}
+    for auth_repo_name, repos in library_dict.items():
+        repositories[auth_repo_name] = repos["auth_repo"]
+        for target_repo in repos["target_repos"]:
+            repositories[target_repo.name] = target_repo
+        check_last_validated_commit(client_dir / repos["auth_repo"].name)
+
+    check_if_commits_match(
+        repositories, origin_dir, start_head_shas, excluded_target_globs
+    )
+
+
 def clone_repositories(
     origin_auth_repo,
     clients_dir,
@@ -250,9 +297,71 @@ def _update_expect_error():
     _update_expect_error()
 
     if not expect_partial_update:
-        # the client repositories should not exits
+        # the client repositories should not exist
         for client_repository in client_repos.values():
             if client_repository.path in repositories_which_existed_paths:
                 assert client_repository.path.exists()
             else:
                 assert not client_repository.path.exists()
+
+
+def update_and_validate_repositories(
+    library_with_dependencies,
+    origin_dir,
+    client_dir,
+    invalid_target_names=None,
+    excluded_target_globs=None,
+):
+    if invalid_target_names is None:
+        invalid_target_names = []
+
+    all_repositories = []
+    for repo_info in library_with_dependencies.values():
+        all_repositories.append(repo_info["auth_repo"])
+        all_repositories.extend(repo_info["target_repos"])
+
+    start_head_shas = defaultdict(dict)
+    for repo in all_repositories:
+        for branch in repo.branches():
+            start_head_shas[repo.name][branch] = repo.top_commit_of_branch(branch)
+
+    origin_root_repo = library_with_dependencies["root/auth"]["auth_repo"]
+
+    try:
+        update_and_check_commit_shas(
+            OperationType.UPDATE,
+            origin_root_repo,
+            client_dir,
+            excluded_target_globs=excluded_target_globs,
+        )
+    except UpdateFailedError as e:
+        if any(
+            invalid_target_name in str(e)
+            for invalid_target_name in invalid_target_names
+        ):
+            pass  # Skip the repositories that are invalid
+        else:
+            raise e
+
+    def check_repository_state(repo_name):
+        repo = next(repo for repo in all_repositories if repo.name == repo_name)
+        for branch in start_head_shas[repo_name]:
+            assert start_head_shas[repo_name][branch] == repo.top_commit_of_branch(
+                branch
+            )
+
+    for auth_repo_name, repo_info in library_with_dependencies.items():
+        auth_repo = repo_info["auth_repo"]
+        for target_repo in repo_info["target_repos"]:
+            if target_repo.name not in invalid_target_names:
+                check_if_commits_match(
+                    {auth_repo_name: auth_repo, target_repo.name: target_repo},
+                    origin_dir,
+                    start_head_shas,
+                )
+            else:
+                check_repository_state(target_repo.name)
+
+    for auth_repo_name, repo_info in library_with_dependencies.items():
+        if repo_info["auth_repo"].name in invalid_target_names:
+            check_repository_state(repo_info["auth_repo"].name)