BuildAPI: Add unit tests and mocks for Artifacts service.

Also added unit tests for FetchPinnedGuestImages.

BUG=chromium:1000845
TEST=run_tests

Change-Id: Ib8d44b03c0ac849055fb8330fef72b2737efce2f
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1937729
Reviewed-by: Alex Klein <saklein@chromium.org>
Tested-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Nicolas Boichat <drinkcat@chromium.org>
diff --git a/api/controller/artifacts.py b/api/controller/artifacts.py
index 11d88d9..4a69f6f 100644
--- a/api/controller/artifacts.py
+++ b/api/controller/artifacts.py
@@ -45,7 +45,16 @@
   return image_dir
 
 
-@faux.all_empty
+def _BundleImageArchivesResponse(input_proto, output_proto, _config):
+  """Add artifact paths to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(input_proto.output_dir,
+                                                   'path0.tar.xz')
+  output_proto.artifacts.add().path = os.path.join(input_proto.output_dir,
+                                                   'path1.tar.xz')
+
+
+@faux.success(_BundleImageArchivesResponse)
+@faux.empty_error
 @validate.require('build_target.name')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -63,7 +72,14 @@
     output_proto.artifacts.add().path = os.path.join(output_dir, archive)
 
 
-@faux.all_empty
+def _BundleImageZipResponse(input_proto, output_proto, _config):
+  """Add artifact zip files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(input_proto.output_dir,
+                                                   'image.zip')
+
+
+@faux.success(_BundleImageZipResponse)
+@faux.empty_error
 @validate.require('build_target.name', 'output_dir')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -85,7 +101,14 @@
   output_proto.artifacts.add().path = os.path.join(output_dir, archive)
 
 
-@faux.all_empty
+def _BundleTestUpdatePayloadsResponse(input_proto, output_proto, _config):
+  """Add test payload files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(input_proto.output_dir,
+                                                   'payload1.bin')
+
+
+@faux.success(_BundleTestUpdatePayloadsResponse)
+@faux.empty_error
 @validate.require('build_target.name', 'output_dir')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -123,7 +146,14 @@
     output_proto.artifacts.add().path = payload
 
 
-@faux.all_empty
+def _BundleAutotestFilesResponse(input_proto, output_proto, _config):
+  """Add test autotest files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(input_proto.output_dir,
+                                                   'autotest-a.tar.gz')
+
+
+@faux.success(_BundleAutotestFilesResponse)
+@faux.empty_error
 @validate.require('output_dir')
 @validate.exists('output_dir')
 def BundleAutotestFiles(input_proto, output_proto, config):
@@ -166,7 +196,14 @@
     output_proto.artifacts.add().path = archive
 
 
-@faux.all_empty
+def _BundleTastFilesResponse(input_proto, output_proto, _config):
+  """Add test tast files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(input_proto.output_dir,
+                                                   'tast_bundles.tar.gz')
+
+
+@faux.success(_BundleTastFilesResponse)
+@faux.empty_error
 @validate.require('output_dir')
 @validate.exists('output_dir')
 def BundleTastFiles(input_proto, output_proto, config):
@@ -214,7 +251,14 @@
   output_proto.artifacts.add().path = archive
 
 
-@faux.all_empty
+def _BundlePinnedGuestImagesResponse(input_proto, output_proto, _config):
+  """Add test pinned guest image files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'pinned-guest-images.tar.gz')
+
+
+@faux.success(_BundlePinnedGuestImagesResponse)
+@faux.empty_error
 @validate.require('build_target.name', 'output_dir')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -241,7 +285,15 @@
   output_proto.artifacts.add().path = os.path.join(output_dir, archive)
 
 
-@faux.all_empty
+def _FetchPinnedGuestImagesResponse(_input_proto, output_proto, _config):
+  """Add test fetched pinned guest image files to a successful response."""
+  pinned_image = output_proto.pinned_images.add()
+  pinned_image.filename = 'pinned_file.tar.gz'
+  pinned_image.uri = 'https://testuri.com'
+
+
+@faux.success(_FetchPinnedGuestImagesResponse)
+@faux.empty_error
 @validate.require('sysroot.path')
 @validate.validation_complete
 def FetchPinnedGuestImages(input_proto, output_proto, _config):
@@ -265,7 +317,14 @@
     pinned_image.uri = pin.uri
 
 
-@faux.all_empty
+def _BundleFirmwareResponse(input_proto, output_proto, _config):
+  """Add test firmware image files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'firmware.tar.gz')
+
+
+@faux.success(_BundleFirmwareResponse)
+@faux.empty_error
 @validate.require('output_dir', 'sysroot.path')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -298,7 +357,14 @@
   output_proto.artifacts.add().path = archive
 
 
-@faux.all_empty
+def _BundleEbuildLogsResponse(input_proto, output_proto, _config):
+  """Add test log files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'ebuild-logs.tar.gz')
+
+
+@faux.success(_BundleEbuildLogsResponse)
+@faux.empty_error
 @validate.exists('output_dir')
 def BundleEbuildLogs(input_proto, output_proto, config):
   """Tar the ebuild logs for a build target.
@@ -334,7 +400,14 @@
   output_proto.artifacts.add().path = os.path.join(output_dir, archive)
 
 
-@faux.all_empty
+def _BundleChromeOSConfigResponse(input_proto, output_proto, _config):
+  """Add test config files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'config.yaml')
+
+
+@faux.success(_BundleChromeOSConfigResponse)
+@faux.empty_error
 @validate.exists('output_dir')
 @validate.validation_complete
 def BundleChromeOSConfig(input_proto, output_proto, _config):
@@ -366,7 +439,14 @@
   output_proto.artifacts.add().path = os.path.join(output_dir, chromeos_config)
 
 
-@faux.all_empty
+def _BundleSimpleChromeArtifactsResponse(input_proto, output_proto, _config):
+  """Add test simple chrome files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'simple_chrome.txt')
+
+
+@faux.success(_BundleSimpleChromeArtifactsResponse)
+@faux.empty_error
 @validate.require('output_dir', 'sysroot.build_target.name', 'sysroot.path')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -403,7 +483,14 @@
     output_proto.artifacts.add().path = file_name
 
 
-@faux.all_empty
+def _BundleVmFilesResponse(input_proto, output_proto, _config):
+  """Add test vm files to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'f1.tar')
+
+
+@faux.success(_BundleVmFilesResponse)
+@faux.empty_error
 @validate.require('chroot.path', 'test_results_dir', 'output_dir')
 @validate.exists('output_dir')
 @validate.validation_complete
@@ -424,17 +511,23 @@
   for archive in archives:
     output_proto.artifacts.add().path = archive
 
+def _BundleAFDOGenerationArtifactsResponse(input_proto, output_proto, _config):
+  """Add test tarball AFDO file to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'artifact1')
+
 
 _VALID_ARTIFACT_TYPES = [toolchain_pb2.BENCHMARK_AFDO,
                          toolchain_pb2.ORDERFILE]
-@faux.all_empty
+@faux.success(_BundleAFDOGenerationArtifactsResponse)
+@faux.empty_error
 @validate.require('build_target.name', 'output_dir')
 @validate.is_in('artifact_type', _VALID_ARTIFACT_TYPES)
 @validate.exists('output_dir')
 @validate.exists('chroot.chrome_dir')
 @validate.validation_complete
 def BundleAFDOGenerationArtifacts(input_proto, output_proto, _config):
-  """Generic function for creating tarballs of both AFDO and orerfile.
+  """Generic function for creating tarballs of both AFDO and orderfile.
 
   Args:
     input_proto (BundleChromeAFDORequest): The input proto.
@@ -465,7 +558,16 @@
     output_proto.artifacts.add().path = file_name
 
 
-@faux.all_empty
+def _ExportCpeReportResponse(input_proto, output_proto, _config):
+  """Add test cpe results to a successful response."""
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'cpe_report.txt')
+  output_proto.artifacts.add().path = os.path.join(
+      input_proto.output_dir, 'cpe_warnings.txt')
+
+
+@faux.success(_ExportCpeReportResponse)
+@faux.empty_error
 @validate.exists('output_dir')
 def ExportCpeReport(input_proto, output_proto, config):
   """Export a CPE report.
diff --git a/api/controller/artifacts_unittest.py b/api/controller/artifacts_unittest.py
index d11a595..3dc2271 100644
--- a/api/controller/artifacts_unittest.py
+++ b/api/controller/artifacts_unittest.py
@@ -7,6 +7,7 @@
 
 from __future__ import print_function
 
+import collections
 import os
 
 import mock
@@ -25,6 +26,10 @@
 from chromite.service import artifacts as artifacts_svc
 
 
+PinnedGuestImage = collections.namedtuple('PinnedGuestImage',
+                                          ['filename', 'uri'])
+
+
 class BundleRequestMixin(object):
   """Mixin to provide bundle request methods."""
 
@@ -106,6 +111,18 @@
                                   self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'ArchiveImages')
+    artifacts.BundleImageArchives(self.target_request, self.response,
+                                  self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 2)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'path0.tar.xz'))
+    self.assertEqual(self.response.artifacts[1].path,
+                     os.path.join(self.output_dir, 'path1.tar.xz'))
+
   def testNoBuildTarget(self):
     """Test that no build target fails."""
     request = self.BuildTargetRequest(output_dir=self.tempdir)
@@ -147,6 +164,16 @@
                              self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(commands, 'BuildImageZip')
+    artifacts.BundleImageZip(self.target_request, self.response,
+                             self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'image.zip'))
+
   def testBundleImageZip(self):
     """BundleImageZip calls cbuildbot/commands with correct args."""
     bundle_image_zip = self.PatchObject(
@@ -181,6 +208,16 @@
                                   self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleAutotestFiles')
+    artifacts.BundleAutotestFiles(self.target_request, self.response,
+                                  self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'autotest-a.tar.gz'))
+
   def testBundleAutotestFilesLegacy(self):
     """BundleAutotestFiles calls service correctly with legacy args."""
     files = {
@@ -257,6 +294,16 @@
                               self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleTastFiles')
+    artifacts.BundleTastFiles(self.target_request, self.response,
+                              self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'tast_bundles.tar.gz'))
+
   def testBundleTastFilesNoLogs(self):
     """BundleTasteFiles dies when no tast files found."""
     self.PatchObject(commands, 'BuildTastBundleTarball',
@@ -322,6 +369,17 @@
                                       self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(commands, 'BuildPinnedGuestImagesTarball')
+    artifacts.BundlePinnedGuestImages(self.target_request, self.response,
+                                      self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir,
+                                  'pinned-guest-images.tar.gz'))
+
   def testBundlePinnedGuestImages(self):
     """BundlePinnedGuestImages calls cbuildbot/commands with correct args."""
     build_pinned_guest_images_tarball = self.PatchObject(
@@ -345,6 +403,59 @@
     self.assertFalse(self.response.artifacts)
 
 
+class FetchPinnedGuestImagesTest(cros_test_lib.MockTempDirTestCase,
+                                 api_config.ApiConfigMixin, BundleRequestMixin):
+  """Unittests for FetchPinnedGuestImages."""
+
+  def setUp(self):
+    self.build_target = 'board'
+    self.chroot_dir = os.path.join(self.tempdir, 'chroot_dir')
+    self.sysroot_path = '/sysroot'
+    self.sysroot_dir = os.path.join(self.chroot_dir, 'sysroot')
+    osutils.SafeMakedirs(self.sysroot_dir)
+
+    self.input_request = artifacts_pb2.PinnedGuestImageUriRequest(
+        sysroot={'path': self.sysroot_path,
+                 'build_target': {'name': self.build_target}},
+        chroot={'path': self.chroot_dir})
+
+    self.response = artifacts_pb2.PinnedGuestImageUriResponse()
+
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    patch = self.PatchObject(artifacts_svc, 'FetchPinnedGuestImages')
+    artifacts.FetchPinnedGuestImages(self.input_request, self.response,
+                                     self.validate_only_config)
+    patch.assert_not_called()
+
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'FetchPinnedGuestImages')
+    artifacts.FetchPinnedGuestImages(self.input_request, self.response,
+                                     self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.pinned_images), 1)
+    self.assertEqual(self.response.pinned_images[0].filename,
+                     'pinned_file.tar.gz')
+    self.assertEqual(self.response.pinned_images[0].uri,
+                     'https://testuri.com')
+
+  def testFetchPinnedGuestImages(self):
+    """FetchPinnedGuestImages calls service with correct args."""
+    pins = []
+    pins.append(PinnedGuestImage(
+        filename='my_pinned_file.tar.gz', uri='https://the_testuri.com'))
+    self.PatchObject(artifacts_svc, 'FetchPinnedGuestImages',
+                     return_value=pins)
+    artifacts.FetchPinnedGuestImages(self.input_request, self.response,
+                                     self.api_config)
+    self.assertEqual(len(self.response.pinned_images), 1)
+    self.assertEqual(self.response.pinned_images[0].filename,
+                     'my_pinned_file.tar.gz')
+    self.assertEqual(self.response.pinned_images[0].uri,
+                     'https://the_testuri.com')
+
+
 class BundleFirmwareTest(BundleTestCase):
   """Unittests for BundleFirmware."""
 
@@ -355,6 +466,16 @@
                              self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleTastFiles')
+    artifacts.BundleFirmware(self.sysroot_request, self.response,
+                             self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'firmware.tar.gz'))
+
   def testBundleFirmware(self):
     """BundleFirmware calls cbuildbot/commands with correct args."""
     self.PatchObject(
@@ -386,6 +507,16 @@
                                self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(commands, 'BuildEbuildLogsTarball')
+    artifacts.BundleEbuildLogs(self.target_request, self.response,
+                               self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'ebuild-logs.tar.gz'))
+
   def testBundleEbuildLogs(self):
     """BundleEbuildLogs calls cbuildbot/commands with correct args."""
     bundle_ebuild_logs_tarball = self.PatchObject(
@@ -430,6 +561,16 @@
                                    self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleChromeOSConfig')
+    artifacts.BundleChromeOSConfig(self.target_request, self.response,
+                                   self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'config.yaml'))
+
   def testBundleChromeOSConfigCallWithSysroot(self):
     """Call with a request that sets sysroot."""
     bundle_chromeos_config = self.PatchObject(
@@ -504,6 +645,16 @@
                                        self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleTestUpdatePayloads')
+    artifacts.BundleTestUpdatePayloads(self.input_proto, self.output_proto,
+                                       self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.output_proto.artifacts), 1)
+    self.assertEqual(self.output_proto.artifacts[0].path,
+                     os.path.join(self.archive_root, 'payload1.bin'))
+
   def testBundleTestUpdatePayloads(self):
     """BundleTestUpdatePayloads calls cbuildbot/commands with correct args."""
     image_path = os.path.join(self.image_root, constants.BASE_IMAGE_BIN)
@@ -581,6 +732,19 @@
                                           self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleSimpleChromeArtifacts')
+    request = self._GetRequest(chroot=self.chroot_dir,
+                               sysroot=self.sysroot_path,
+                               build_target='board', output_dir=self.output_dir)
+    artifacts.BundleSimpleChromeArtifacts(request, self.response,
+                                          self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'simple_chrome.txt'))
+
   def testNoBuildTarget(self):
     """Test no build target fails."""
     request = self._GetRequest(chroot=self.chroot_dir,
@@ -677,6 +841,18 @@
     artifacts.BundleVmFiles(in_proto, self.response, self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'BundleVmFiles')
+    in_proto = self._GetInput(chroot='/chroot/dir', sysroot='/build/board',
+                              test_results_dir='/test/results',
+                              output_dir=self.output_dir)
+    artifacts.BundleVmFiles(in_proto, self.response, self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'f1.tar'))
+
   def testChrootMissing(self):
     """Test error handling for missing chroot."""
     in_proto = self._GetInput(sysroot='/build/board',
@@ -793,6 +969,22 @@
                                             self.validate_only_config)
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc,
+                             'BundleAFDOGenerationArtifacts')
+    request = self._GetRequest(chroot=self.chroot_dir,
+                               build_target=self.build_target,
+                               chrome_root=self.chrome_root,
+                               output_dir=self.output_dir,
+                               artifact_type=self.valid_artifact_type)
+    artifacts.BundleAFDOGenerationArtifacts(request, self.response,
+                                            self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 1)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.output_dir, 'artifact1'))
+
   def testNoBuildTarget(self):
     """Test no build target fails."""
     request = self._GetRequest(chroot=self.chroot_dir,
@@ -914,6 +1106,23 @@
 
     patch.assert_not_called()
 
+  def testMockCall(self):
+    """Test that a mock call does not execute logic, returns mocked value."""
+    patch = self.PatchObject(artifacts_svc, 'GenerateCpeReport')
+
+    request = artifacts_pb2.BundleRequest()
+    request.build_target.name = 'board'
+    request.output_dir = self.tempdir
+
+    artifacts.ExportCpeReport(request, self.response, self.mock_call_config)
+
+    patch.assert_not_called()
+    self.assertEqual(len(self.response.artifacts), 2)
+    self.assertEqual(self.response.artifacts[0].path,
+                     os.path.join(self.tempdir, 'cpe_report.txt'))
+    self.assertEqual(self.response.artifacts[1].path,
+                     os.path.join(self.tempdir, 'cpe_warnings.txt'))
+
   def testNoBuildTarget(self):
     request = artifacts_pb2.BundleRequest()
     request.output_dir = self.tempdir