Merge "Adding basic test to exercise the heat event APIs."
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_template_resource.py b/functional/test_template_resource.py
index a967888..1ec05d9 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
@@ -713,3 +715,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)