Repository: dbt-labs/dbt-core

Issue:
fix error handling for profiles.yml validation
I think these substrings are incorrect, and `ValidationException has not attribute 'path'

https://github.com/fishtown-analytics/dbt/blob/development/dbt/project.py#L234


Discussion:
@beckjake possibly fixed another one of these in one of the merges here: https://github.com/fishtown-analytics/dbt/pull/798

Resolving patch:
diff --git a/dbt/api/object.py b/dbt/api/object.py
--- a/dbt/api/object.py
+++ b/dbt/api/object.py
@@ -66,18 +66,17 @@ def validate(self):
         """
         validator = Draft4Validator(self.SCHEMA)
 
-        errors = []
+        errors = set()  # make errors a set to avoid duplicates
 
         for error in validator.iter_errors(self.serialize()):
-            errors.append('.'.join(
+            errors.add('.'.join(
                 list(map(str, error.path)) + [error.message]
             ))
 
         if errors:
-            raise ValidationException(
-                'Invalid arguments passed to "{}" instance: {}'
-                .format(type(self).__name__,
-                        ", ".join(errors)))
+            msg = ('Invalid arguments passed to "{}" instance: {}'.format(
+                type(self).__name__, ', '.join(errors)))
+            raise ValidationException(msg)
 
     # implement the Mapping protocol:
     # https://docs.python.org/3/library/collections.abc.html
diff --git a/dbt/exceptions.py b/dbt/exceptions.py
--- a/dbt/exceptions.py
+++ b/dbt/exceptions.py
@@ -1,5 +1,6 @@
 from dbt.compat import basestring
 from dbt.logger import GLOBAL_LOGGER as logger
+import re
 
 
 class Exception(BaseException):
diff --git a/dbt/project.py b/dbt/project.py
--- a/dbt/project.py
+++ b/dbt/project.py
@@ -234,15 +234,7 @@ class CredentialsValidator(APIObject):
         try:
             CredentialsValidator(**target_cfg)
         except dbt.exceptions.ValidationException as e:
-            if 'extra keys not allowed' in str(e):
-                raise DbtProjectError(
-                    "Extra project configuration '{}' is not recognized"
-                    .format('.'.join(e.path)), self)
-            else:
-                # TODO : does this fail if eg. project is missing?
-                raise DbtProjectError(
-                    "Expected project configuration '{}' was not supplied"
-                    .format('.'.join(e.path)), self)
+            raise DbtProjectError(str(e), self)
 
     def log_warnings(self):
         target_cfg = self.run_environment()


Test patch:
diff --git a/test/unit/test_project.py b/test/unit/test_project.py
new file mode 100644
--- /dev/null
+++ b/test/unit/test_project.py
@@ -0,0 +1,85 @@
+import unittest
+
+import os
+import dbt.project
+
+
+class ProjectTest(unittest.TestCase):
+    def setUp(self):
+        self.profiles = {
+            'test': {
+                'outputs': {
+                    'test': {
+                        'type': 'postgres',
+                        'threads': 4,
+                        'host': 'database',
+                        'port': 5432,
+                        'user': 'root',
+                        'pass': 'password',
+                        'dbname': 'dbt',
+                        'schema': 'dbt_test'
+                    }
+                },
+                'target': 'test'
+            }
+        }
+        self.cfg = {
+            'name': 'X',
+            'version': '0.1',
+            'profile': 'test',
+            'project-root': os.path.abspath('.'),
+        }
+
+    def test_profile_validate_success(self):
+        # Make sure we can instantiate + validate a valid profile
+
+        project = dbt.project.Project(
+            cfg=self.cfg,
+            profiles=self.profiles,
+            profiles_dir=None
+        )
+
+        project.validate()
+
+    def test_profile_validate_missing(self):
+        del self.profiles['test']['outputs']['test']['schema']
+
+        project = dbt.project.Project(
+            cfg=self.cfg,
+            profiles=self.profiles,
+            profiles_dir=None
+        )
+
+        message = r'.*schema.* is a required property.*'
+        with self.assertRaisesRegexp(dbt.project.DbtProjectError, message):
+            project.validate()
+
+    def test_profile_validate_extra(self):
+        self.profiles['test']['outputs']['test']['foo'] = 'bar'
+
+        project = dbt.project.Project(
+            cfg=self.cfg,
+            profiles=self.profiles,
+            profiles_dir=None
+        )
+
+        message = r'.*not allowed.*foo.* was unexpected.*'
+        with self.assertRaisesRegexp(dbt.project.DbtProjectError, message):
+            project.validate()
+
+    def test_profile_validate_missing_and_extra(self):
+        del self.profiles['test']['outputs']['test']['schema']
+        self.profiles['test']['outputs']['test']['foo'] = 'bar'
+
+        project = dbt.project.Project(
+            cfg=self.cfg,
+            profiles=self.profiles,
+            profiles_dir=None
+        )
+
+        unrecognized = r'not allowed.*foo.* was unexpected'
+        extra = r'schema.* is a required property'
+        # fun with regexp ordering: want both, don't care about order
+        message = '.*({0}.*{1}|{1}.*{0}).*'.format(unrecognized, extra)
+        with self.assertRaisesRegexp(dbt.project.DbtProjectError, message):
+            project.validate()