Repository: dbt-labs/dbt-core

Issue:
Make parsing error easier to understand
### Housekeeping

- [X] I am a maintainer of dbt-core

### Short description

### Context:

Someone set `severity` to `warning` which is not a valid `severity` (only `warn` and `error` are valid): 

<img width="507" alt="image" src="https://github.com/dbt-labs/dbt-core/assets/135771777/4cdb9bde-0e4c-4937-bc4d-0c8de173382a">
From Gerda: This comes from jsonschema validation which often leads to somewhat strange and cryptic errors

### Origin:
[Coming from here](https://github.com/dbt-labs/dbt-core/blob/fc431010ef0bd11ee6a502fc6c9e5e3e75c5d72d/core/dbt/artifacts/resources/v1/config.py#L157)



### Acceptance criteria

The error is easy to understand.
"Severity can only be set to Warn or Error"

### Suggested Tests

validate that the parsing error raises as expected

### Impact to Other Teams

IDE surfaced, impact label is added.

### Will backports be required?

No

### Context

_No response_
Make parsing error easier to understand
### Housekeeping

- [X] I am a maintainer of dbt-core

### Short description

### Context:

Someone set `severity` to `warning` which is not a valid `severity` (only `warn` and `error` are valid): 

<img width="507" alt="image" src="https://github.com/dbt-labs/dbt-core/assets/135771777/4cdb9bde-0e4c-4937-bc4d-0c8de173382a">
From Gerda: This comes from jsonschema validation which often leads to somewhat strange and cryptic errors

### Origin:
[Coming from here](https://github.com/dbt-labs/dbt-core/blob/fc431010ef0bd11ee6a502fc6c9e5e3e75c5d72d/core/dbt/artifacts/resources/v1/config.py#L157)



### Acceptance criteria

The error is easy to understand.
"Severity can only be set to Warn or Error"

### Suggested Tests

validate that the parsing error raises as expected

### Impact to Other Teams

IDE surfaced, impact label is added.

### Will backports be required?

No

### Context

_No response_


Discussion:

Resolving patch:
diff --git a/core/dbt/artifacts/resources/v1/config.py b/core/dbt/artifacts/resources/v1/config.py
--- a/core/dbt/artifacts/resources/v1/config.py
+++ b/core/dbt/artifacts/resources/v1/config.py
@@ -1,3 +1,5 @@
+import re
+
 from dbt_common.dataclass_schema import dbtClassMixin, ValidationError
 from typing import Optional, List, Any, Dict, Union
 from typing_extensions import Annotated
@@ -250,6 +252,12 @@ def same_contents(cls, unrendered: Dict[str, Any], other: Dict[str, Any]) -> boo
 
     @classmethod
     def validate(cls, data):
+        if data.get("severity") and not re.match(SEVERITY_PATTERN, data.get("severity")):
+            raise ValidationError(
+                f"Severity must be either 'warn' or 'error'. Got '{data.get('severity')}'"
+            )
+
         super().validate(data)
+
         if data.get("materialized") and data.get("materialized") != "test":
             raise ValidationError("A test must have a materialized value of 'test'")


Test patch:
diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py
--- a/tests/unit/test_parser.py
+++ b/tests/unit/test_parser.py
@@ -1,5 +1,6 @@
 import os
 import unittest
+from argparse import Namespace
 from copy import deepcopy
 from unittest import mock
 
@@ -8,12 +9,12 @@
 import dbt.flags
 import dbt.parser
 from dbt import tracking
+from dbt.artifacts.resources import ModelConfig
 from dbt.artifacts.resources import RefArgs
 from dbt.context.context_config import ContextConfig
 from dbt.contracts.files import SourceFile, FileHash, FilePath, SchemaSourceFile
 from dbt.contracts.graph.manifest import Manifest
 from dbt.contracts.graph.model_config import NodeConfig, TestConfig, SnapshotConfig
-from dbt.artifacts.resources import ModelConfig
 from dbt.contracts.graph.nodes import (
     ModelNode,
     Macro,
@@ -23,7 +24,8 @@
     AnalysisNode,
     UnpatchedSourceDefinition,
 )
-from dbt.exceptions import CompilationError, ParsingError
+from dbt.exceptions import CompilationError, ParsingError, SchemaConfigError
+from dbt.flags import set_from_args
 from dbt.node_types import NodeType
 from dbt.parser import (
     ModelParser,
@@ -53,8 +55,6 @@
 from dbt.parser.search import FileBlock
 from dbt.parser.sources import SourcePatcher
 from .utils import config_from_parts_or_dicts, normalize, generate_name_macros, MockNode
-from dbt.flags import set_from_args
-from argparse import Namespace
 
 set_from_args(Namespace(WARN_ERROR=False), None)
 
@@ -273,6 +273,22 @@ def assertEqualNodes(node_one, node_two):
                 arg: 100
 """
 
+SINGLE_TABLE_MODEL_TESTS_WRONG_SEVERITY = """
+models:
+    - name: my_model
+      description: A description of my model
+      columns:
+        - name: color
+          description: The color value
+          data_tests:
+            - not_null:
+                severity: WARNING
+            - accepted_values:
+                values: ['red', 'blue', 'green']
+            - foreign_package.test_case:
+                arg: 100
+"""
+
 
 MULTIPLE_TABLE_VERSIONED_MODEL_TESTS = """
 models:
@@ -577,6 +593,14 @@ def test__read_basic_model_tests(self):
         self.assertEqual(len(list(self.parser.manifest.sources)), 0)
         self.assertEqual(len(list(self.parser.manifest.nodes)), 4)
 
+    def test__read_basic_model_tests_wrong_severity(self):
+        block = self.yaml_block_for(SINGLE_TABLE_MODEL_TESTS_WRONG_SEVERITY, "test_one.yml")
+        dct = yaml_from_file(block.file)
+        with self.assertRaisesRegex(
+            SchemaConfigError, "Severity must be either 'warn' or 'error'. Got 'WARNING'"
+        ):
+            self.parser.parse_file(block, dct)
+
     def test__parse_basic_model_tests(self):
         block = self.file_block_for(SINGLE_TABLE_MODEL_TESTS, "test_one.yml")
         self.parser.manifest.files[block.file.file_id] = block.file