Merge "Add integration tests for preview update"
diff --git a/common/clients.py b/common/clients.py
index c7a7f60..daba708 100644
--- a/common/clients.py
+++ b/common/clients.py
@@ -28,7 +28,7 @@
     calling various OpenStack APIs.
     """
 
-    CINDERCLIENT_VERSION = '1'
+    CINDERCLIENT_VERSION = '2'
     HEATCLIENT_VERSION = '1'
     NOVACLIENT_VERSION = '2'
     CEILOMETER_VERSION = '2'
diff --git a/common/test.py b/common/test.py
index 5e37a39..4f3d923 100644
--- a/common/test.py
+++ b/common/test.py
@@ -250,9 +250,9 @@
 
     def _verify_status(self, stack, stack_identifier, status, fail_regexp):
         if stack.stack_status == status:
-            # Handle UPDATE_COMPLETE case: Make sure we don't
-            # wait for a stale UPDATE_COMPLETE status.
-            if status == 'UPDATE_COMPLETE':
+            # Handle UPDATE_COMPLETE/FAILED case: Make sure we don't
+            # wait for a stale UPDATE_COMPLETE/FAILED status.
+            if status in ('UPDATE_FAILED', 'UPDATE_COMPLETE'):
                 if self.updated_time.get(
                         stack_identifier) != stack.updated_time:
                     self.updated_time[stack_identifier] = stack.updated_time
@@ -263,8 +263,8 @@
         wait_for_action = status.split('_')[0]
         if (stack.action == wait_for_action and
                 fail_regexp.search(stack.stack_status)):
-            # Handle UPDATE_FAILED case.
-            if status == 'UPDATE_FAILED':
+            # Handle UPDATE_COMPLETE/UPDATE_FAILED case.
+            if status in ('UPDATE_FAILED', 'UPDATE_COMPLETE'):
                 if self.updated_time.get(
                         stack_identifier) != stack.updated_time:
                     self.updated_time[stack_identifier] = stack.updated_time
@@ -279,7 +279,7 @@
                     stack_status_reason=stack.stack_status_reason)
 
     def _wait_for_stack_status(self, stack_identifier, status,
-                               failure_pattern='^.*_FAILED$',
+                               failure_pattern=None,
                                success_on_not_found=False):
         """
         Waits for a Stack to reach a given status.
@@ -288,7 +288,13 @@
         CREATE_COMPLETE, not just COMPLETE which is exposed
         via the status property of Stack in heatclient
         """
-        fail_regexp = re.compile(failure_pattern)
+        if failure_pattern:
+            fail_regexp = re.compile(failure_pattern)
+        elif 'FAILED' in status:
+            # If we're looking for e.g CREATE_FAILED, COMPLETE is unexpected.
+            fail_regexp = re.compile('^.*_COMPLETE$')
+        else:
+            fail_regexp = re.compile('^.*_FAILED$')
         build_timeout = self.conf.build_timeout
         build_interval = self.conf.build_interval
 
@@ -323,10 +329,11 @@
             stack_identifier, 'DELETE_COMPLETE',
             success_on_not_found=True)
 
-    def update_stack(self, stack_identifier, template, environment=None,
+    def update_stack(self, stack_identifier, template=None, environment=None,
                      files=None, parameters=None, tags=None,
                      expected_status='UPDATE_COMPLETE',
-                     disable_rollback=True):
+                     disable_rollback=True,
+                     existing=False):
         env = environment or {}
         env_files = files or {}
         parameters = parameters or {}
@@ -335,6 +342,8 @@
         build_timeout = self.conf.build_timeout
         build_interval = self.conf.build_interval
         start = timeutils.utcnow()
+        self.updated_time[stack_identifier] = self.client.stacks.get(
+            stack_identifier).updated_time
         while timeutils.delta_seconds(start,
                                       timeutils.utcnow()) < build_timeout:
             try:
@@ -346,7 +355,8 @@
                     disable_rollback=disable_rollback,
                     parameters=parameters,
                     environment=env,
-                    tags=tags
+                    tags=tags,
+                    existing=existing
                 )
             except heat_exceptions.HTTPConflict as ex:
                 # FIXME(sirushtim): Wait a little for the stack lock to be
diff --git a/functional/test_autoscaling.py b/functional/test_autoscaling.py
index 1b9fe99..9041405 100644
--- a/functional/test_autoscaling.py
+++ b/functional/test_autoscaling.py
@@ -13,7 +13,9 @@
 import copy
 import json
 
+from heatclient import exc
 from oslo_log import log as logging
+import six
 from testtools import matchers
 
 from heat_integrationtests.common import test
@@ -728,8 +730,13 @@
         self._wait_for_resource_status(
             stack_identifier, 'JobServerGroup', 'SUSPEND_COMPLETE')
 
-        # Send a signal and confirm nothing happened.
-        self.client.resources.signal(stack_identifier, 'ScaleUpPolicy')
+        # Send a signal and a exception will raise
+        ex = self.assertRaises(exc.BadRequest,
+                               self.client.resources.signal,
+                               stack_identifier, 'ScaleUpPolicy')
+
+        error_msg = 'Signal resource during SUSPEND is not supported'
+        self.assertIn(error_msg, six.text_type(ex))
         ev = self.wait_for_event_with_reason(
             stack_identifier,
             reason='Cannot signal resource during SUSPEND',
diff --git a/functional/test_create_update.py b/functional/test_create_update.py
index b2aad34..4597d4c 100644
--- a/functional/test_create_update.py
+++ b/functional/test_create_update.py
@@ -102,11 +102,15 @@
 
     provider_group_template = '''
 heat_template_version: 2013-05-23
+parameters:
+  count:
+    type: number
+    default: 2
 resources:
   test_group:
     type: OS::Heat::ResourceGroup
     properties:
-      count: 2
+      count: {get_param: count}
       resource_def:
         type: My::TestResource
 '''
@@ -134,6 +138,20 @@
       user_data: {get_param: user_data}
 '''
 
+    fail_param_template = '''
+heat_template_version: 2014-10-16
+parameters:
+  do_fail:
+    type: boolean
+    default: False
+resources:
+  aresource:
+    type: OS::Heat::TestResource
+    properties:
+      value: Test
+      fail: {get_param: do_fail}
+'''
+
     def setUp(self):
         super(UpdateStackTest, self).setUp()
 
@@ -243,6 +261,29 @@
         self.assertEqual(updated_resources,
                          self.list_resources(stack_identifier))
 
+    def test_stack_update_from_failed(self):
+        # Prove it's possible to update from an UPDATE_FAILED state
+        template = _change_rsrc_properties(test_template_one_resource,
+                                           ['test1'],
+                                           {'value': 'test_update_failed'})
+        stack_identifier = self.stack_create(
+            template=template)
+        initial_resources = {'test1': 'OS::Heat::TestResource'}
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+
+        tmpl_update = _change_rsrc_properties(
+            test_template_one_resource, ['test1'], {'fail': True})
+        # Update with bad template, we should fail
+        self.update_stack(stack_identifier, tmpl_update,
+                          expected_status='UPDATE_FAILED')
+        # but then passing a good template should succeed
+        self.update_stack(stack_identifier, test_template_two_resource)
+        updated_resources = {'test1': 'OS::Heat::TestResource',
+                             'test2': 'OS::Heat::TestResource'}
+        self.assertEqual(updated_resources,
+                         self.list_resources(stack_identifier))
+
     def test_stack_update_provider(self):
         template = _change_rsrc_properties(
             test_template_one_resource, ['test1'],
@@ -294,7 +335,7 @@
         '''Test two-level nested update.'''
         # Create a ResourceGroup (which creates a nested stack),
         # containing provider resources (which create a nested
-        # stack), thus excercising an update which traverses
+        # stack), thus exercising an update which traverses
         # two levels of nesting.
         template = _change_rsrc_properties(
             test_template_one_resource, ['test1'],
@@ -391,3 +432,69 @@
             stack_identifier,
             template=self.update_userdata_template,
             parameters=parms_updated)
+
+    def test_stack_update_provider_group_patch(self):
+        '''Test two-level nested update with PATCH'''
+        template = _change_rsrc_properties(
+            test_template_one_resource, ['test1'],
+            {'value': 'test_provider_group_template'})
+        files = {'provider.template': json.dumps(template)}
+        env = {'resource_registry':
+               {'My::TestResource': 'provider.template'}}
+
+        stack_identifier = self.stack_create(
+            template=self.provider_group_template,
+            files=files,
+            environment=env
+        )
+
+        initial_resources = {'test_group': 'OS::Heat::ResourceGroup'}
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+
+        # Prove the resource is backed by a nested stack, save the ID
+        nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
+                                                            'test_group')
+
+        # Then check the expected resources are in the nested stack
+        nested_resources = {'0': 'My::TestResource',
+                            '1': 'My::TestResource'}
+        self.assertEqual(nested_resources,
+                         self.list_resources(nested_identifier))
+
+        # increase the count, pass only the paramter, no env or template
+        params = {'count': 3}
+        self.update_stack(stack_identifier, parameters=params, existing=True)
+
+        # Parent resources should be unchanged and the nested stack
+        # should have been updated in-place without replacement
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+
+        # Resource group stack should also be unchanged (but updated)
+        nested_stack = self.client.stacks.get(nested_identifier)
+        self.assertEqual('UPDATE_COMPLETE', nested_stack.stack_status)
+        # Add a resource, as we should have added one
+        nested_resources['2'] = 'My::TestResource'
+        self.assertEqual(nested_resources,
+                         self.list_resources(nested_identifier))
+
+    def test_stack_update_from_failed_patch(self):
+        '''Test PATCH update from a failed state.'''
+
+        # Start with empty template
+        stack_identifier = self.stack_create(
+            template='heat_template_version: 2014-10-16')
+
+        # Update with a good template, but bad parameter
+        self.update_stack(stack_identifier,
+                          template=self.fail_param_template,
+                          parameters={'do_fail': True},
+                          expected_status='UPDATE_FAILED')
+
+        # PATCH update, only providing the parameter
+        self.update_stack(stack_identifier,
+                          parameters={'do_fail': False},
+                          existing=True)
+        self.assertEqual({u'aresource': u'OS::Heat::TestResource'},
+                         self.list_resources(stack_identifier))
diff --git a/functional/test_create_update_neutron_port.py b/functional/test_create_update_neutron_port.py
index 4b2df59..575d21c 100644
--- a/functional/test_create_update_neutron_port.py
+++ b/functional/test_create_update_neutron_port.py
@@ -10,8 +10,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from testtools import testcase
-
 from heat_integrationtests.functional import functional_base
 
 
@@ -38,6 +36,12 @@
       fixed_ips:
         - subnet: {get_resource: subnet}
           ip_address: 11.11.11.11
+  test:
+    depends_on: port
+    type: OS::Heat::TestResource
+    properties:
+      value: Test1
+      fail: False
 outputs:
   port_ip:
     value: {get_attr: [port, fixed_ips, 0, ip_address]}
@@ -73,7 +77,6 @@
         self.assertNotEqual(_ip, new_ip)
         self.assertNotEqual(_id, new_id)
 
-    @testcase.skip('Skipped until bug #1455100 is fixed.')
     def test_stack_update_replace_with_ip(self):
         # create with default 'mac' parameter
         stack_identifier = self.stack_create(template=test_template)
@@ -92,6 +95,62 @@
         self.assertEqual(_ip, new_ip)
         self.assertNotEqual(_id, new_id)
 
+    def test_stack_update_replace_with_ip_rollback(self):
+        # create with default 'mac' parameter
+        stack_identifier = self.stack_create(template=test_template)
+
+        _id, _ip = self.get_port_id_and_ip(stack_identifier)
+
+        # Update with another 'mac' parameter
+        parameters = {'mac': '00-00-00-00-AA-AA'}
+
+        # make test resource failing during update
+        fail_template = test_template.replace('fail: False',
+                                              'fail: True')
+        fail_template = fail_template.replace('value: Test1',
+                                              'value: Rollback')
+
+        # port should be replaced with same ip
+        self.update_stack(stack_identifier, fail_template,
+                          parameters=parameters,
+                          expected_status='ROLLBACK_COMPLETE',
+                          disable_rollback=False)
+
+        new_id, new_ip = self.get_port_id_and_ip(stack_identifier)
+        # port id and ip should be the same after rollback
+        self.assertEqual(_ip, new_ip)
+        self.assertEqual(_id, new_id)
+
+    def test_stack_update_replace_with_ip_after_failed_update(self):
+        # create with default 'mac' parameter
+        stack_identifier = self.stack_create(template=test_template)
+
+        _id, _ip = self.get_port_id_and_ip(stack_identifier)
+
+        # Update with another 'mac' parameter
+        parameters = {'mac': '00-00-00-00-AA-AA'}
+
+        # make test resource failing during update
+        fail_template = test_template.replace('fail: False',
+                                              'fail: True')
+        fail_template = fail_template.replace('value: Test1',
+                                              'value: Rollback')
+
+        # port should be replaced with same ip
+        self.update_stack(stack_identifier, fail_template,
+                          parameters=parameters,
+                          expected_status='UPDATE_FAILED')
+
+        # port should be replaced with same ip
+        self.update_stack(stack_identifier, test_template,
+                          parameters=parameters)
+
+        new_id, new_ip = self.get_port_id_and_ip(stack_identifier)
+        # ip should be the same, but port id should be different, because it's
+        # restore replace
+        self.assertEqual(_ip, new_ip)
+        self.assertNotEqual(_id, new_id)
+
     def test_stack_update_in_place_remove_ip(self):
         # create with default 'mac' parameter and defined ip_address
         stack_identifier = self.stack_create(template=test_template)
diff --git a/functional/test_stack_events.py b/functional/test_stack_events.py
new file mode 100644
index 0000000..b1b2339
--- /dev/null
+++ b/functional/test_stack_events.py
@@ -0,0 +1,112 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from heat_integrationtests.functional import functional_base
+
+
+class StackEventsTest(functional_base.FunctionalTestsBase):
+
+    template = '''
+heat_template_version: 2014-10-16
+parameters:
+resources:
+  test_resource:
+    type: OS::Heat::TestResource
+    properties:
+      value: 'test1'
+      fail: False
+      update_replace: False
+      wait_secs: 0
+outputs:
+  resource_id:
+    description: 'ID of resource'
+    value: { get_resource: test_resource }
+'''
+
+    def setUp(self):
+        super(StackEventsTest, self).setUp()
+
+    def _verify_event_fields(self, event, event_characteristics):
+        self.assertIsNotNone(event_characteristics)
+        self.assertIsNotNone(event.event_time)
+        self.assertIsNotNone(event.links)
+        self.assertIsNotNone(event.logical_resource_id)
+        self.assertIsNotNone(event.resource_status)
+        self.assertIn(event.resource_status, event_characteristics[1])
+        self.assertIsNotNone(event.resource_status_reason)
+        self.assertIsNotNone(event.id)
+
+    def test_event(self):
+        parameters = {}
+
+        test_stack_name = self._stack_rand_name()
+        stack_identifier = self.stack_create(
+            stack_name=test_stack_name,
+            template=self.template,
+            parameters=parameters
+        )
+
+        expected_status = ['CREATE_IN_PROGRESS', 'CREATE_COMPLETE']
+        event_characteristics = {
+            test_stack_name: ('OS::Heat::Stack', expected_status),
+            'test_resource': ('OS::Heat::TestResource', expected_status)}
+
+        # List stack events
+        # API: GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/events
+        stack_events = self.client.events.list(stack_identifier)
+
+        for stack_event in stack_events:
+            # Key on an expected/valid resource name
+            self._verify_event_fields(
+                stack_event,
+                event_characteristics[stack_event.resource_name])
+
+            # Test the event filtering API based on this resource_name
+            # /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events
+            resource_events = self.client.events.list(
+                stack_identifier,
+                stack_event.resource_name)
+
+            # Resource events are a subset of the original stack event list
+            self.assertTrue(len(resource_events) < len(stack_events))
+
+            # Get the event details for each resource event
+            for resource_event in resource_events:
+                # A resource_event should be in the original stack event list
+                self.assertIn(resource_event, stack_events)
+                # Given a filtered list, the resource names should be identical
+                self.assertEqual(
+                    resource_event.resource_name,
+                    stack_event.resource_name)
+                # Verify all fields, keying off the resource_name
+                self._verify_event_fields(
+                    resource_event,
+                    event_characteristics[resource_event.resource_name])
+
+                # Exercise the event details API
+                # /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events/{event_id}
+                event_details = self.client.events.get(
+                    stack_identifier,
+                    resource_event.resource_name,
+                    resource_event.id)
+                self._verify_event_fields(
+                    event_details,
+                    event_characteristics[event_details.resource_name])
+                # The names should be identical to the non-detailed event
+                self.assertEqual(
+                    resource_event.resource_name,
+                    event_details.resource_name)
+                # Verify the extra field in the detail results
+                self.assertIsNotNone(event_details.resource_type)
+                self.assertEqual(
+                    event_characteristics[event_details.resource_name][0],
+                    event_details.resource_type)
diff --git a/functional/test_stack_tags.py b/functional/test_stack_tags.py
index 05600f5..4a97798 100644
--- a/functional/test_stack_tags.py
+++ b/functional/test_stack_tags.py
@@ -19,11 +19,18 @@
 heat_template_version: 2014-10-16
 description:
   foo
+parameters:
+  input:
+    type: string
+    default: test
+resources:
+  not-used:
+    type: OS::Heat::TestResource
+    properties:
+      wait_secs: 1
+      value: {get_param: input}
 '''
 
-    def setUp(self):
-        super(StackTagTest, self).setUp()
-
     def test_stack_tag(self):
         # Stack create with stack tags
         tags = 'foo,bar'
@@ -41,7 +48,8 @@
         self.update_stack(
             stack_identifier,
             template=self.template,
-            tags=updated_tags)
+            tags=updated_tags,
+            parameters={'input': 'next'})
 
         # Ensure property tag is populated and matches updated tags
         updated_stack = self.client.stacks.get(stack_identifier)
@@ -50,7 +58,8 @@
         # Delete tags
         self.update_stack(
             stack_identifier,
-            template=self.template
+            template=self.template,
+            parameters={'input': 'none'}
         )
 
         # Ensure property tag is not populated
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index a967888..9609664 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -12,6 +12,8 @@
 
 import json
 
+from heatclient import exc as heat_exceptions
+import six
 import yaml
 
 from heat_integrationtests.common import test
@@ -243,6 +245,42 @@
         self.assertEqual(old_way, test_attr2)
 
 
+class TemplateResourceFacadeTest(functional_base.FunctionalTestsBase):
+    """Prove that we can use ResourceFacade in a HOT template."""
+
+    main_template = '''
+heat_template_version: 2013-05-23
+resources:
+  the_nested:
+    type: the.yaml
+    metadata:
+      foo: bar
+outputs:
+  value:
+    value: {get_attr: [the_nested, output]}
+'''
+
+    nested_templ = '''
+heat_template_version: 2013-05-23
+resources:
+  test:
+    type: OS::Heat::TestResource
+    properties:
+      value: {"Fn::Select": [foo, {resource_facade: metadata}]}
+outputs:
+  output:
+    value: {get_attr: [test, output]}
+    '''
+
+    def test_metadata(self):
+        stack_identifier = self.stack_create(
+            template=self.main_template,
+            files={'the.yaml': self.nested_templ})
+        stack = self.client.stacks.get(stack_identifier)
+        value = self._stack_output(stack, 'value')
+        self.assertEqual('bar', value)
+
+
 class TemplateResourceUpdateTest(functional_base.FunctionalTestsBase):
     """Prove that we can do template resource updates."""
 
@@ -713,3 +751,91 @@
 
         self.stack_suspend(stack_identifier=stack_identifier)
         self.stack_resume(stack_identifier=stack_identifier)
+
+
+class ValidateFacadeTest(test.HeatIntegrationTest):
+    """Prove that nested stack errors don't suck."""
+    template = '''
+heat_template_version: 2015-10-15
+resources:
+  thisone:
+    type: OS::Thingy
+    properties:
+      one: pre
+      two: post
+outputs:
+  one:
+    value: {get_attr: [thisone, here-it-is]}
+'''
+    templ_facade = '''
+heat_template_version: 2015-04-30
+parameters:
+  one:
+    type: string
+  two:
+    type: string
+outputs:
+  here-it-is:
+    value: noop
+'''
+    env = '''
+resource_registry:
+  OS::Thingy: facade.yaml
+  resources:
+    thisone:
+      OS::Thingy: concrete.yaml
+'''
+
+    def setUp(self):
+        super(ValidateFacadeTest, self).setUp()
+        self.client = self.orchestration_client
+
+    def test_missing_param(self):
+        templ_missing_parameter = '''
+heat_template_version: 2015-04-30
+parameters:
+  one:
+    type: string
+resources:
+  str:
+    type: OS::Heat::RandomString
+outputs:
+  here-it-is:
+    value:
+      not-important
+'''
+        try:
+            self.stack_create(
+                template=self.template,
+                environment=self.env,
+                files={'facade.yaml': self.templ_facade,
+                       'concrete.yaml': templ_missing_parameter},
+                expected_status='CREATE_FAILED')
+        except heat_exceptions.HTTPBadRequest as exc:
+            exp = ('ERROR: Required property two for facade '
+                   'OS::Thingy missing in provider')
+            self.assertEqual(exp, six.text_type(exc))
+
+    def test_missing_output(self):
+        templ_missing_output = '''
+heat_template_version: 2015-04-30
+parameters:
+  one:
+    type: string
+  two:
+    type: string
+resources:
+  str:
+    type: OS::Heat::RandomString
+'''
+        try:
+            self.stack_create(
+                template=self.template,
+                environment=self.env,
+                files={'facade.yaml': self.templ_facade,
+                       'concrete.yaml': templ_missing_output},
+                expected_status='CREATE_FAILED')
+        except heat_exceptions.HTTPBadRequest as exc:
+            exp = ('ERROR: Attribute here-it-is for facade '
+                   'OS::Thingy missing in provider')
+            self.assertEqual(exp, six.text_type(exc))
diff --git a/functional/test_template_validate.py b/functional/test_template_validate.py
new file mode 100644
index 0000000..e62c31b
--- /dev/null
+++ b/functional/test_template_validate.py
@@ -0,0 +1,244 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+import six
+
+from heatclient import exc
+
+from heat_integrationtests.functional import functional_base
+
+
+class StackTemplateValidateTest(functional_base.FunctionalTestsBase):
+
+    random_template = '''
+heat_template_version: 2014-10-16
+description: the stack description
+parameters:
+  aparam:
+    type: number
+    default: 10
+    description: the param description
+resources:
+  myres:
+    type: OS::Heat::RandomString
+    properties:
+      length: {get_param: aparam}
+'''
+
+    parent_template = '''
+heat_template_version: 2014-10-16
+description: the parent template
+parameters:
+  pparam:
+    type: number
+    default: 5
+    description: the param description
+resources:
+  nres:
+    type: mynested.yaml
+    properties:
+      aparam: {get_param: pparam}
+'''
+
+    parent_template_noprop = '''
+heat_template_version: 2014-10-16
+description: the parent template
+resources:
+  nres:
+    type: mynested.yaml
+'''
+
+    random_template_groups = '''
+heat_template_version: 2014-10-16
+description: the stack description
+parameters:
+  aparam:
+    type: number
+    default: 10
+    description: the param description
+  bparam:
+    type: string
+    default: foo
+  cparam:
+    type: string
+    default: secret
+    hidden: true
+parameter_groups:
+- label: str_params
+  description: The string params
+  parameters:
+  - bparam
+  - cparam
+resources:
+  myres:
+    type: OS::Heat::RandomString
+    properties:
+      length: {get_param: aparam}
+'''
+
+    def test_template_validate_basic(self):
+        ret = self.client.stacks.validate(template=self.random_template)
+        expected = {'Description': 'the stack description',
+                    'Parameters': {
+                        'aparam': {'Default': 10,
+                                   'Description': 'the param description',
+                                   'Label': 'aparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_override_default(self):
+        env = {'parameters': {'aparam': 5}}
+        ret = self.client.stacks.validate(template=self.random_template,
+                                          environment=env)
+        expected = {'Description': 'the stack description',
+                    'Parameters': {
+                        'aparam': {'Default': 10,
+                                   'Value': 5,
+                                   'Description': 'the param description',
+                                   'Label': 'aparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_override_none(self):
+        env = {'resource_registry': {
+               'OS::Heat::RandomString': 'OS::Heat::None'}}
+        ret = self.client.stacks.validate(template=self.random_template,
+                                          environment=env)
+        expected = {'Description': 'the stack description',
+                    'Parameters': {
+                        'aparam': {'Default': 10,
+                                   'Description': 'the param description',
+                                   'Label': 'aparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_basic_required_param(self):
+        tmpl = self.random_template.replace('default: 10', '')
+        ret = self.client.stacks.validate(template=tmpl)
+        expected = {'Description': 'the stack description',
+                    'Parameters': {
+                        'aparam': {'Description': 'the param description',
+                                   'Label': 'aparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_fail_version(self):
+        fail_template = self.random_template.replace('2014-10-16', 'invalid')
+        ex = self.assertRaises(exc.HTTPBadRequest,
+                               self.client.stacks.validate,
+                               template=fail_template)
+        self.assertIn('The template version is invalid', six.text_type(ex))
+
+    def test_template_validate_parameter_groups(self):
+        ret = self.client.stacks.validate(template=self.random_template_groups)
+        expected = {'Description': 'the stack description',
+                    'ParameterGroups':
+                    [{'description': 'The string params',
+                      'label': 'str_params',
+                      'parameters': ['bparam', 'cparam']}],
+                    'Parameters':
+                    {'aparam':
+                     {'Default': 10,
+                      'Description': 'the param description',
+                      'Label': 'aparam',
+                      'NoEcho': 'false',
+                      'Type': 'Number'},
+                     'bparam':
+                     {'Default': 'foo',
+                      'Description': '',
+                      'Label': 'bparam',
+                      'NoEcho': 'false',
+                      'Type': 'String'},
+                     'cparam':
+                     {'Default': 'secret',
+                      'Description': '',
+                      'Label': 'cparam',
+                      'NoEcho': 'true',
+                      'Type': 'String'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_nested_off(self):
+        files = {'mynested.yaml': self.random_template}
+        ret = self.client.stacks.validate(template=self.parent_template,
+                                          files=files)
+        expected = {'Description': 'the parent template',
+                    'Parameters': {
+                        'pparam': {'Default': 5,
+                                   'Description': 'the param description',
+                                   'Label': 'pparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_nested_on(self):
+        files = {'mynested.yaml': self.random_template}
+        ret = self.client.stacks.validate(template=self.parent_template_noprop,
+                                          files=files,
+                                          show_nested=True)
+        expected = {'Description': 'the parent template',
+                    'Parameters': {},
+                    'NestedParameters': {
+                        'nres': {'Description': 'the stack description',
+                                 'Parameters': {'aparam': {'Default': 10,
+                                                           'Description':
+                                                           'the param '
+                                                           'description',
+                                                           'Label': 'aparam',
+                                                           'NoEcho': 'false',
+                                                           'Type': 'Number'}},
+                                 'Type': 'mynested.yaml'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_nested_on_multiple(self):
+        # parent_template -> nested_template -> random_template
+        nested_template = self.random_template.replace(
+            'OS::Heat::RandomString', 'mynested2.yaml')
+        files = {'mynested.yaml': nested_template,
+                 'mynested2.yaml': self.random_template}
+        ret = self.client.stacks.validate(template=self.parent_template,
+                                          files=files,
+                                          show_nested=True)
+
+        n_param2 = {'myres': {'Description': 'the stack description',
+                              'Parameters': {'aparam': {'Default': 10,
+                                                        'Description':
+                                                        'the param '
+                                                        'description',
+                                                        'Label': 'aparam',
+                                                        'NoEcho': 'false',
+                                                        'Type': 'Number'}},
+                              'Type': 'mynested2.yaml'}}
+        expected = {'Description': 'the parent template',
+                    'Parameters': {
+                        'pparam': {'Default': 5,
+                                   'Description': 'the param description',
+                                   'Label': 'pparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}},
+                    'NestedParameters': {
+                        'nres': {'Description': 'the stack description',
+                                 'Parameters': {'aparam': {'Default': 10,
+                                                           'Description':
+                                                           'the param '
+                                                           'description',
+                                                           'Label': 'aparam',
+                                                           'Value': 5,
+                                                           'NoEcho': 'false',
+                                                           'Type': 'Number'}},
+                                 'NestedParameters': n_param2,
+                                 'Type': 'mynested.yaml'}}}
+        self.assertEqual(expected, ret)