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/android_unittest.py b/api/controller/android_unittest.py
index a67e26c..59f2342 100644
--- a/api/controller/android_unittest.py
+++ b/api/controller/android_unittest.py
@@ -7,6 +7,7 @@
 
 from __future__ import print_function
 
+from chromite.api import api_config
 from chromite.api.controller import android
 from chromite.api.gen.chromite.api import android_pb2
 from chromite.api.gen.chromiumos import common_pb2
@@ -17,7 +18,7 @@
 from chromite.lib import cros_test_lib
 from chromite.lib import osutils
 
-class MarkStableTest(cros_test_lib.MockTestCase):
+class MarkStableTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin):
   """Unittests for MarkStable."""
 
   def setUp(self):
@@ -32,33 +33,39 @@
     self.board2 = self.input_proto.boards.add()
     self.board2.name = 'board2'
 
+    self.response = android_pb2.MarkStableResponse()
+
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    patch = self.PatchObject(commands, 'MarkAndroidAsStable')
+
+    android.MarkStable(self.input_proto, self.response,
+                       self.validate_only_config)
+    patch.assert_not_called()
+
   def testFailsIfTrackingBranchMissing(self):
     """Fails if tracking_branch is missing."""
     self.input_proto.tracking_branch = ''
-    output_proto = android_pb2.MarkStableResponse()
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      android.MarkStable(self.input_proto, output_proto)
+      android.MarkStable(self.input_proto, self.response, self.api_config)
     self.command.assert_not_called()
 
   def testFailsIfPackageNameMissing(self):
     """Fails if package_name is missing."""
     self.input_proto.package_name = ''
-    output_proto = android_pb2.MarkStableResponse()
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      android.MarkStable(self.input_proto, output_proto)
+      android.MarkStable(self.input_proto, self.response, self.api_config)
     self.command.assert_not_called()
 
   def testFailsIfAndroidBuildBranchMissing(self):
     """Fails if android_build_branch is missing."""
     self.input_proto.android_build_branch = ''
-    output_proto = android_pb2.MarkStableResponse()
     with self.assertRaises(cros_build_lib.DieSystemExit):
-      android.MarkStable(self.input_proto, output_proto)
+      android.MarkStable(self.input_proto, self.response, self.api_config)
     self.command.assert_not_called()
 
   def testCallsCommandCorrectly(self):
     """Test that commands.MarkAndroidAsStable is called correctly."""
-    output_proto = android_pb2.MarkStableResponse()
     self.input_proto.android_version = 'android-version'
     self.input_proto.android_gts_build_branch = 'gts-branch'
     self.command.return_value = 'cat/android-1.2.3'
@@ -66,7 +73,7 @@
     atom.category = 'cat'
     atom.package_name = 'android'
     atom.version = '1.2.3'
-    android.MarkStable(self.input_proto, output_proto)
+    android.MarkStable(self.input_proto, self.response, self.api_config)
     self.command.assert_called_once_with(
         buildroot=self.input_proto.buildroot,
         tracking_branch=self.input_proto.tracking_branch,
@@ -75,17 +82,16 @@
         boards=self.input_proto.boards,
         android_version=self.input_proto.android_version,
         android_gts_build_branch=self.input_proto.android_gts_build_branch)
-    self.assertEqual(output_proto.android_atom, atom)
-    self.assertEqual(output_proto.status,
+    self.assertEqual(self.response.android_atom, atom)
+    self.assertEqual(self.response.status,
                      android_pb2.MARK_STABLE_STATUS_SUCCESS)
 
   def testHandlesEarlyExit(self):
     """Test that early exit is handled correctly."""
-    output_proto = android_pb2.MarkStableResponse()
     self.input_proto.android_version = 'android-version'
     self.input_proto.android_gts_build_branch = 'gts-branch'
     self.command.return_value = ''
-    android.MarkStable(self.input_proto, output_proto)
+    android.MarkStable(self.input_proto, self.response, self.api_config)
     self.command.assert_called_once_with(
         buildroot=self.input_proto.buildroot,
         tracking_branch=self.input_proto.tracking_branch,
@@ -94,12 +100,11 @@
         boards=self.input_proto.boards,
         android_version=self.input_proto.android_version,
         android_gts_build_branch=self.input_proto.android_gts_build_branch)
-    self.assertEqual(output_proto.status,
+    self.assertEqual(self.response.status,
                      android_pb2.MARK_STABLE_STATUS_EARLY_EXIT)
 
   def testHandlesPinnedUprevError(self):
     """Test that pinned error is handled correctly."""
-    output_proto = android_pb2.MarkStableResponse()
     self.input_proto.android_version = 'android-version'
     self.input_proto.android_gts_build_branch = 'gts-branch'
     self.command.side_effect = commands.AndroidIsPinnedUprevError('pin/xx-1.1')
@@ -107,7 +112,7 @@
     atom.category = 'pin'
     atom.package_name = 'xx'
     atom.version = '1.1'
-    android.MarkStable(self.input_proto, output_proto)
+    android.MarkStable(self.input_proto, self.response, self.api_config)
     self.command.assert_called_once_with(
         buildroot=self.input_proto.buildroot,
         tracking_branch=self.input_proto.tracking_branch,
@@ -116,12 +121,12 @@
         boards=self.input_proto.boards,
         android_version=self.input_proto.android_version,
         android_gts_build_branch=self.input_proto.android_gts_build_branch)
-    self.assertEqual(output_proto.android_atom, atom)
-    self.assertEqual(output_proto.status,
+    self.assertEqual(self.response.android_atom, atom)
+    self.assertEqual(self.response.status,
                      android_pb2.MARK_STABLE_STATUS_PINNED)
 
 
-class UnpinVersionTest(cros_test_lib.MockTestCase):
+class UnpinVersionTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin):
   """Unittests for UnpinVersion."""
 
   def testCallsUnlink(self):
@@ -131,5 +136,12 @@
 
     # This has the side effect of making sure that input and output proto are
     # not actually used.
-    android.UnpinVersion(None, None)
+    android.UnpinVersion(None, None, self.api_config)
     safeunlink.assert_called_once_with(android.ANDROIDPIN_MASK_PATH)
+
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    safeunlink = self.PatchObject(osutils, 'SafeUnlink')
+
+    android.UnpinVersion(None, None, self.validate_only_config)
+    safeunlink.assert_not_called()