Build API: Implement validate_only calls.

Add validate-only support to all existing endpoints and
tests to enforce the setting is respected.
Add is_in validator to help transition some endpoints
to decorator-only validation.
Some cleanup and standardization in the controller tests.

BUG=chromium:987263
TEST=run_tests

Cq-Depend: chromium:1726252
Change-Id: I108dfc1a221847eae47a18f2f60e12d2575c9ea8
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1726253
Reviewed-by: David Burger <dburger@chromium.org>
Commit-Queue: Alex Klein <saklein@chromium.org>
Tested-by: Alex Klein <saklein@chromium.org>
diff --git a/api/controller/sysroot_unittest.py b/api/controller/sysroot_unittest.py
index d722936..7e06c10 100644
--- a/api/controller/sysroot_unittest.py
+++ b/api/controller/sysroot_unittest.py
@@ -9,6 +9,7 @@
 
 import os
 
+from chromite.api import api_config
 from chromite.api import controller
 from chromite.api.controller import sysroot as sysroot_controller
 from chromite.api.gen.chromite.api import sysroot_pb2
@@ -21,7 +22,7 @@
 from chromite.service import sysroot as sysroot_service
 
 
-class CreateTest(cros_test_lib.MockTestCase):
+class CreateTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin):
   """Create function tests."""
 
   def _InputProto(self, build_target=None, profile=None, replace=False,
@@ -43,20 +44,34 @@
     """Helper to build output proto instance."""
     return sysroot_pb2.SysrootCreateResponse()
 
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    patch = self.PatchObject(sysroot_service, 'Create')
+
+    board = 'board'
+    profile = None
+    force = False
+    upgrade_chroot = True
+    in_proto = self._InputProto(build_target=board, profile=profile,
+                                replace=force, current=not upgrade_chroot)
+    sysroot_controller.Create(in_proto, self._OutputProto(),
+                              self.validate_only_config)
+    patch.assert_not_called()
+
   def testArgumentValidation(self):
     """Test the input argument validation."""
     # Error when no name provided.
     in_proto = self._InputProto()
     out_proto = self._OutputProto()
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.Create(in_proto, out_proto)
+      sysroot_controller.Create(in_proto, out_proto, self.api_config)
 
     # Valid when board passed.
     result = sysroot_lib.Sysroot('/sysroot/path')
     patch = self.PatchObject(sysroot_service, 'Create', return_value=result)
     in_proto = self._InputProto('board')
     out_proto = self._OutputProto()
-    sysroot_controller.Create(in_proto, out_proto)
+    sysroot_controller.Create(in_proto, out_proto, self.api_config)
     patch.assert_called_once()
 
   def testArgumentHandling(self):
@@ -77,7 +92,7 @@
     in_proto = self._InputProto(build_target=board, profile=profile,
                                 replace=force, current=not upgrade_chroot)
     out_proto = self._OutputProto()
-    sysroot_controller.Create(in_proto, out_proto)
+    sysroot_controller.Create(in_proto, out_proto, self.api_config)
 
     # Default value checks.
     target_patch.assert_called_with(name=board, profile=profile)
@@ -94,7 +109,7 @@
     in_proto = self._InputProto(build_target=board, profile=profile,
                                 replace=force, current=not upgrade_chroot)
     out_proto = self._OutputProto()
-    sysroot_controller.Create(in_proto, out_proto)
+    sysroot_controller.Create(in_proto, out_proto, self.api_config)
 
     # Not default value checks.
     target_patch.assert_called_with(name=board, profile=profile)
@@ -103,7 +118,8 @@
     self.assertEqual(sysroot_path, out_proto.sysroot.path)
 
 
-class InstallToolchainTest(cros_test_lib.MockTempDirTestCase):
+class InstallToolchainTest(cros_test_lib.MockTempDirTestCase,
+                           api_config.ApiConfigMixin):
   """Install toolchain function tests."""
 
   def setUp(self):
@@ -132,6 +148,16 @@
     """Helper to build output proto instance."""
     return sysroot_pb2.InstallToolchainResponse()
 
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    patch = self.PatchObject(sysroot_service, 'InstallToolchain')
+
+    in_proto = self._InputProto(build_target=self.board,
+                                sysroot_path=self.sysroot)
+    sysroot_controller.InstallToolchain(in_proto, self._OutputProto(),
+                                        self.validate_only_config)
+    patch.assert_not_called()
+
   def testArgumentValidation(self):
     """Test the argument validation."""
     # Test errors on missing inputs.
@@ -139,23 +165,23 @@
     # Both missing.
     in_proto = self._InputProto()
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallToolchain(in_proto, out_proto)
+      sysroot_controller.InstallToolchain(in_proto, out_proto, self.api_config)
 
     # Sysroot path missing.
     in_proto = self._InputProto(build_target=self.board)
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallToolchain(in_proto, out_proto)
+      sysroot_controller.InstallToolchain(in_proto, out_proto, self.api_config)
 
     # Build target name missing.
     in_proto = self._InputProto(sysroot_path=self.sysroot)
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallToolchain(in_proto, out_proto)
+      sysroot_controller.InstallToolchain(in_proto, out_proto, self.api_config)
 
     # Both provided, but invalid sysroot path.
     in_proto = self._InputProto(build_target=self.board,
                                 sysroot_path=self.invalid_sysroot)
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallToolchain(in_proto, out_proto)
+      sysroot_controller.InstallToolchain(in_proto, out_proto, self.api_config)
 
   def testSuccessOutputHandling(self):
     """Test the output is processed and recorded correctly."""
@@ -164,7 +190,8 @@
     in_proto = self._InputProto(build_target=self.board,
                                 sysroot_path=self.sysroot)
 
-    rc = sysroot_controller.InstallToolchain(in_proto, out_proto)
+    rc = sysroot_controller.InstallToolchain(in_proto, out_proto,
+                                             self.api_config)
     self.assertFalse(rc)
     self.assertFalse(out_proto.failed_packages)
 
@@ -183,7 +210,8 @@
                                             tc_info=err_cpvs)
     self.PatchObject(sysroot_service, 'InstallToolchain', side_effect=err)
 
-    rc = sysroot_controller.InstallToolchain(in_proto, out_proto)
+    rc = sysroot_controller.InstallToolchain(in_proto, out_proto,
+                                             self.api_config)
     self.assertEqual(controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc)
     self.assertTrue(out_proto.failed_packages)
     for package in out_proto.failed_packages:
@@ -191,7 +219,8 @@
       self.assertIn(cat_pkg, expected)
 
 
-class InstallPackagesTest(cros_test_lib.MockTempDirTestCase):
+class InstallPackagesTest(cros_test_lib.MockTempDirTestCase,
+                          api_config.ApiConfigMixin):
   """InstallPackages tests."""
 
   def setUp(self):
@@ -220,26 +249,36 @@
     """Helper to build an empty output proto instance."""
     return sysroot_pb2.InstallPackagesResponse()
 
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    patch = self.PatchObject(sysroot_service, 'BuildPackages')
+
+    in_proto = self._InputProto(build_target=self.build_target,
+                                sysroot_path=self.sysroot)
+    sysroot_controller.InstallPackages(in_proto, self._OutputProto(),
+                                       self.validate_only_config)
+    patch.assert_not_called()
+
   def testArgumentValidationAllMissing(self):
     """Test missing all arguments."""
     out_proto = self._OutputProto()
     in_proto = self._InputProto()
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallPackages(in_proto, out_proto)
+      sysroot_controller.InstallPackages(in_proto, out_proto, self.api_config)
 
   def testArgumentValidationNoSysroot(self):
     """Test missing sysroot path."""
     out_proto = self._OutputProto()
     in_proto = self._InputProto(build_target=self.build_target)
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallPackages(in_proto, out_proto)
+      sysroot_controller.InstallPackages(in_proto, out_proto, self.api_config)
 
   def testArgumentValidationNoBuildTarget(self):
     """Test missing build target name."""
     out_proto = self._OutputProto()
     in_proto = self._InputProto(sysroot_path=self.sysroot)
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallPackages(in_proto, out_proto)
+      sysroot_controller.InstallPackages(in_proto, out_proto, self.api_config)
 
   def testArgumentValidationInvalidSysroot(self):
     """Test sysroot that hasn't had the toolchain installed."""
@@ -249,7 +288,7 @@
     self.PatchObject(sysroot_lib.Sysroot, 'IsToolchainInstalled',
                      return_value=False)
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      sysroot_controller.InstallPackages(in_proto, out_proto)
+      sysroot_controller.InstallPackages(in_proto, out_proto, self.api_config)
 
   def testSuccessOutputHandling(self):
     """Test successful call output handling."""
@@ -262,7 +301,8 @@
     out_proto = self._OutputProto()
     self.PatchObject(sysroot_service, 'BuildPackages')
 
-    rc = sysroot_controller.InstallPackages(in_proto, out_proto)
+    rc = sysroot_controller.InstallPackages(in_proto, out_proto,
+                                            self.api_config)
     self.assertFalse(rc)
     self.assertFalse(out_proto.failed_packages)
 
@@ -287,7 +327,8 @@
                                             packages=err_cpvs)
     self.PatchObject(sysroot_service, 'BuildPackages', side_effect=error)
 
-    rc = sysroot_controller.InstallPackages(in_proto, out_proto)
+    rc = sysroot_controller.InstallPackages(in_proto, out_proto,
+                                            self.api_config)
     # This needs to return 2 to indicate the available error response.
     self.assertEqual(controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc)
     for package in out_proto.failed_packages:
@@ -310,7 +351,8 @@
                                             packages=[])
     self.PatchObject(sysroot_service, 'BuildPackages', side_effect=error)
 
-    rc = sysroot_controller.InstallPackages(in_proto, out_proto)
+    rc = sysroot_controller.InstallPackages(in_proto, out_proto,
+                                            self.api_config)
     # All we really care about is it's not 0 or 2 (response available), so
     # test for that rather than a specific return code.
     self.assertTrue(rc)