From 73192230dc2907861a6de3e3bc29e40d5355dd90 Mon Sep 17 00:00:00 2001
From: Lars Kellogg-Stedman <lars@redhat.com>
Date: Fri, 17 May 2024 15:10:31 -0400
Subject: [PATCH] Add validation for overlapping date ranges

---
 src/nerc_rates/models.py           | 21 +++++++++++
 src/nerc_rates/tests/test_rates.py | 60 +++++++++++++++++++++++-------
 2 files changed, 68 insertions(+), 13 deletions(-)

diff --git a/src/nerc_rates/models.py b/src/nerc_rates/models.py
index ab03cbf..5f1cd86 100644
--- a/src/nerc_rates/models.py
+++ b/src/nerc_rates/models.py
@@ -35,6 +35,27 @@ class RateItem(Base):
     name: str
     history: list[RateValue]
 
+    @pydantic.model_validator(mode="after")
+    @classmethod
+    def validate_no_overlap(cls, data: Self):
+        for x in data.history:
+            for y in data.history:
+                if x is not y:
+                    if (
+                        (x.date_until is None and y.date_until is None)
+                        or (
+                            y.date_from >= x.date_from
+                            and (x.date_until and y.date_from <= x.date_until)
+                        )
+                        or (
+                            y.date_from <= x.date_from
+                            and (y.date_until and y.date_until >= x.date_from)
+                        )
+                    ):
+                        raise ValueError("date ranges overlap")
+
+        return data
+
 
 RateItemDict = Annotated[
     dict[str, RateItem],
diff --git a/src/nerc_rates/tests/test_rates.py b/src/nerc_rates/tests/test_rates.py
index 9a28d5b..87658ac 100644
--- a/src/nerc_rates/tests/test_rates.py
+++ b/src/nerc_rates/tests/test_rates.py
@@ -1,7 +1,7 @@
 import pytest
 import requests_mock
 
-from nerc_rates import load_from_url, rates
+from nerc_rates import load_from_url, rates, models
 
 
 def test_load_from_url():
@@ -17,18 +17,52 @@ def test_load_from_url():
         assert r.get_value_at("CPU SU Rate", "2023-06") == "0.013"
 
 
-def test_invalid_dates():
-    mock_response_text = """
-    - name: CPU SU Rate
-      history:
-        - value: "0.013"
-          from: 2023-06
-          until: 2023-04
-    """
-    with requests_mock.Mocker() as m:
-        m.get(rates.DEFAULT_URL, text=mock_response_text)
-        with pytest.raises(ValueError):
-            load_from_url()
+def test_invalid_date_order():
+    rate = ({"value": "1", "from": "2020-04", "until": "2020-03"},)
+    with pytest.raises(ValueError):
+        models.RateValue.model_validate(rate)
+
+
+@pytest.mark.parametrize(
+    "rate",
+    [
+        # Two values with no end date
+        {
+            "name": "Test Rate",
+            "history": [
+                {"value": "1", "from": "2020-01"},
+                {"value": "2", "from": "2020-03"},
+            ],
+        },
+        # Second value overlaps first value at end
+        {
+            "name": "Test Rate",
+            "history": [
+                {"value": "1", "from": "2020-01", "until": "2020-04"},
+                {"value": "2", "from": "2020-03"},
+            ],
+        },
+        # Second value overlaps first value at start
+        {
+            "name": "Test Rate",
+            "history": [
+                {"value": "1", "from": "2020-04", "until": "2020-06"},
+                {"value": "2", "from": "2020-03", "until": "2020-05"},
+            ],
+        },
+        # Second value is contained by first value
+        {
+            "name": "Test Rate",
+            "history": [
+                {"value": "1", "from": "2020-01", "until": "2020-06"},
+                {"value": "2", "from": "2020-03", "until": "2020-05"},
+            ],
+        },
+    ],
+)
+def test_invalid_date_overlap(rate):
+    with pytest.raises(ValueError):
+        models.RateItem.model_validate(rate)
 
 
 def test_rates_get_value_at():