Upgrade devserver to support both Omaha v2 and v3 clients

We need this support for update_engine to go from Omaha v2 to v3.

BUG=chromium-os:35904
TEST=Updated devserver unit tests work fine.
TEST=Tested image_to_live on ZGB and it works fine for v2 and v3.

Change-Id: I2b96047a95a66aa920dc5fd1f54807f0541af554
Reviewed-on: https://gerrit.chromium.org/gerrit/36992
Commit-Ready: Jay Srinivasan <jaysri@chromium.org>
Reviewed-by: Jay Srinivasan <jaysri@chromium.org>
Tested-by: Jay Srinivasan <jaysri@chromium.org>
diff --git a/devserver_unittest.py b/devserver_unittest.py
index 815b5d7..4605e34 100755
--- a/devserver_unittest.py
+++ b/devserver_unittest.py
@@ -26,8 +26,11 @@
 TEST_FACTORY_CONFIG = 'testdata/devserver/miniomaha-test.conf'
 TEST_DATA_PATH = '/tmp/devserver-test'
 TEST_CLIENT_PREFIX = 'ChromeOSUpdateEngine'
+EXPECTED_HASH = 'kGcOinJ0vA8vdYX53FN0F5BdwfY='
 
-UPDATE_REQUEST = """<?xml version="1.0" encoding="UTF-8"?>
+# Update request based on Omaha v2 protocol format.
+UPDATE_REQUEST = {}
+UPDATE_REQUEST['2.0'] = """<?xml version="1.0" encoding="UTF-8"?>
 <o:gupdate xmlns:o="http://www.google.com/update2/request" version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" protocol="2.0" ismachine="1">
     <o:os version="Indy" platform="Chrome OS" sp="0.11.254.2011_03_09_1814_i686"></o:os>
     <o:app appid="{DEV-BUILD}" version="0.11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
@@ -36,8 +39,20 @@
     </o:app>
 </o:gupdate>
 """
+
+# Update request based on Omaha v3 protocol format.
+UPDATE_REQUEST['3.0'] = """<?xml version="1.0" encoding="UTF-8"?>
+<request version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" protocol="3.0" ismachine="1">
+    <os version="Indy" platform="Chrome OS" sp="0.11.254.2011_03_09_1814_i686"></os>
+    <app appid="{DEV-BUILD}" version="0.11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
+        <updatecheck></updatecheck>
+        <event eventtype="3" eventresult="2" previousversion="0.11.216.2011_03_02_1358"></event>
+    </app>
+</request>
+"""
 # TODO(girts): use a random available port.
 UPDATE_URL = 'http://127.0.0.1:8080/update'
+STATIC_URL = 'http://127.0.0.1:8080/static/'
 
 API_HOST_INFO_BAD_URL = 'http://127.0.0.1:8080/api/hostinfo/'
 API_HOST_INFO_URL = API_HOST_INFO_BAD_URL + '127.0.0.1'
@@ -73,18 +88,7 @@
     if os.path.exists(self.image):
       os.unlink(self.image)
 
-  def testValidateFactoryConfig(self):
-    """Tests --validate_factory_config."""
-    cmd = [
-        'python',
-        os.path.join(base_dir, 'devserver.py'),
-        '--validate_factory_config',
-        '--factory_config', self.factory_config,
-    ]
-    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
-    stdout, _ = process.communicate()
-    self.assertEqual(0, process.returncode)
-    self.assertTrue('Config file looks good.' in stdout)
+  # Helper methods begin here.
 
   def _StartServer(self, data_dir=''):
     """Starts devserver, returns process."""
@@ -100,13 +104,14 @@
     process = subprocess.Popen(cmd)
     return process.pid
 
-  def testHandleUpdate(self):
-    """Tests running the server and getting an update."""
-    pid = self._StartServer()
+  def VerifyHandleUpdate(self, protocol, data_dir):
+    """Tests running the server and getting an update for the given protocol.
+       Takes an optional data_dir to pass to the devserver. """
+    pid = self._StartServer(data_dir)
     try:
       # Wait for the server to start up.
       time.sleep(1)
-      request = urllib2.Request(UPDATE_URL, UPDATE_REQUEST)
+      request = urllib2.Request(UPDATE_URL, UPDATE_REQUEST[protocol])
       connection = urllib2.urlopen(request)
       response = connection.read()
       connection.close()
@@ -115,23 +120,52 @@
       # Parse the response and check if it contains the right result.
       dom = minidom.parseString(response)
       update = dom.getElementsByTagName('updatecheck')[0]
-
-      codebase = update.getAttribute('codebase')
-      self.assertEqual('http://127.0.0.1:8080/static/' + TEST_IMAGE_NAME,
-                       codebase)
-
-      hash_value = update.getAttribute('hash')
-      self.assertEqual('kGcOinJ0vA8vdYX53FN0F5BdwfY=', hash_value)
+      if protocol == '2.0':
+        url = self.VerifyV2Response(update)
+      else:
+        url = self.VerifyV3Response(update)
 
       # Try to fetch the image.
-      connection = urllib2.urlopen(codebase)
+      connection = urllib2.urlopen(url)
       contents = connection.read()
       connection.close()
       self.assertEqual('Developers, developers, developers!\n', contents)
     finally:
       os.kill(pid, signal.SIGKILL)
 
-  def testHandleDatadirUpdate(self):
+  def VerifyV2Response(self, update):
+    """Verifies the update DOM from a v2 response and returns the url."""
+    codebase = update.getAttribute('codebase')
+    self.assertEqual(STATIC_URL + TEST_IMAGE_NAME,
+                     codebase)
+
+    hash_value = update.getAttribute('hash')
+    self.assertEqual(EXPECTED_HASH, hash_value)
+
+    return codebase
+
+  def VerifyV3Response(self, update):
+    """Verifies the update DOM from a v3 response and returns the url."""
+    # Parse the response and check if it contains the right result.
+    urls = update.getElementsByTagName('urls')[0]
+    url = urls.getElementsByTagName('url')[0]
+
+    codebase = url.getAttribute('codebase')
+    self.assertEqual(STATIC_URL, codebase)
+
+    manifest = update.getElementsByTagName('manifest')[0]
+    packages = manifest.getElementsByTagName('packages')[0]
+    package = packages.getElementsByTagName('package')[0]
+    filename = package.getAttribute('name')
+    self.assertEqual(TEST_IMAGE_NAME, filename)
+
+    hash_value = package.getAttribute('hash')
+    self.assertEqual(EXPECTED_HASH, hash_value)
+
+    url = os.path.join(codebase, filename)
+    return url
+
+  def VerifyHandleDatadirUpdate(self, protocol):
     """Tests getting an update from a specified datadir"""
     # Push the image to the expected path where devserver picks it up.
     image_path = os.path.join(TEST_DATA_PATH, STATIC_DIR)
@@ -143,36 +177,35 @@
       os.unlink(foreign_image)
     shutil.copy(self.image_src, foreign_image)
 
-    pid = self._StartServer(data_dir=TEST_DATA_PATH)
-    try:
-      # Wait for the server to start up.
-      time.sleep(1)
+    self.VerifyHandleUpdate(protocol, TEST_DATA_PATH)
+    os.unlink(foreign_image)
 
-      request = urllib2.Request(UPDATE_URL, UPDATE_REQUEST)
-      connection = urllib2.urlopen(request)
-      response = connection.read()
-      connection.close()
-      self.assertNotEqual('', response)
+  # Tests begin here.
 
-      # Parse the response and check if it contains the right result.
-      dom = minidom.parseString(response)
-      update = dom.getElementsByTagName('updatecheck')[0]
+  def testValidateFactoryConfig(self):
+    """Tests --validate_factory_config."""
+    cmd = [
+        'python',
+        os.path.join(base_dir, 'devserver.py'),
+        '--validate_factory_config',
+        '--factory_config', self.factory_config,
+    ]
+    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+    stdout, _ = process.communicate()
+    self.assertEqual(0, process.returncode)
+    self.assertTrue('Config file looks good.' in stdout)
 
-      codebase = update.getAttribute('codebase')
-      self.assertEqual('http://127.0.0.1:8080/static/' + TEST_IMAGE_NAME,
-                       codebase)
+  def testHandleUpdateV2(self):
+    self.VerifyHandleUpdate('2.0', '')
 
-      hash_value = update.getAttribute('hash')
-      self.assertEqual('kGcOinJ0vA8vdYX53FN0F5BdwfY=', hash_value)
+  def testHandleUpdateV3(self):
+    self.VerifyHandleUpdate('3.0', '')
 
-      # Try to fetch the image.
-      connection = urllib2.urlopen(codebase)
-      contents = connection.read()
-      connection.close()
-      self.assertEqual('Developers, developers, developers!\n', contents)
-      os.unlink(foreign_image)
-    finally:
-      os.kill(pid, signal.SIGKILL)
+  def testHandleDatadirUpdateV2(self):
+    self.VerifyHandleDatadirUpdate('2.0')
+
+  def testHandleDatadirUpdateV3(self):
+    self.VerifyHandleDatadirUpdate('3.0')
 
   def testApiBadSetNextUpdateRequest(self):
     """Tests sending a bad setnextupdate request."""
@@ -252,6 +285,5 @@
     finally:
       os.kill(pid, signal.SIGKILL)
 
-
 if __name__ == '__main__':
   unittest.main()