diff --git a/pyproject.toml b/pyproject.toml index 262ca14..bbcccd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "socketdev" -version = "3.0.32" +version = "3.0.33" requires-python = ">= 3.9" dependencies = [ 'requests', diff --git a/socketdev/fullscans/__init__.py b/socketdev/fullscans/__init__.py index a2eb4f2..f295237 100644 --- a/socketdev/fullscans/__init__.py +++ b/socketdev/fullscans/__init__.py @@ -443,11 +443,20 @@ def to_dict(self): @classmethod def from_dict(cls, data: dict) -> "SocketAlert": + try: + category = SocketCategory(data["category"]) + except ValueError: + log.warning( + "Unknown SocketCategory %r; falling back to MISCELLANEOUS. " + "Upgrade socketdev to pick up newer categories.", + data["category"], + ) + category = SocketCategory.MISCELLANEOUS return cls( key=data["key"], type=data["type"], severity=SocketIssueSeverity(data["severity"]), - category=SocketCategory(data["category"]), + category=category, file=data.get("file"), start=data.get("start"), end=data.get("end"), diff --git a/socketdev/version.py b/socketdev/version.py index b70a418..46ae1b5 100644 --- a/socketdev/version.py +++ b/socketdev/version.py @@ -1 +1 @@ -__version__ = "3.0.32" +__version__ = "3.0.33" diff --git a/tests/unit/test_socket_alert_category.py b/tests/unit/test_socket_alert_category.py new file mode 100644 index 0000000..02ce1c1 --- /dev/null +++ b/tests/unit/test_socket_alert_category.py @@ -0,0 +1,63 @@ +""" +Unit tests for lenient SocketCategory parsing in SocketAlert.from_dict. + +Regression coverage for +https://github.com/SocketDev/socket-sdk-python/issues/78: the Socket API can +emit category values the SDK does not yet know about (e.g. ``"other"``). Strict +enum parsing turned that into a hard failure that took down every consumer +(notably socketsecurity CI runs) whenever a diff included one of those alerts. + +These tests pin the fallback behavior so the SDK stays forward-compatible with +new server-side categories. +""" + +import logging +import unittest + +from socketdev.fullscans import SocketAlert, SocketCategory, SocketIssueSeverity + + +class TestSocketAlertCategoryParsing(unittest.TestCase): + """SocketAlert.from_dict should tolerate unknown category values.""" + + def _base_payload(self, category: str) -> dict: + return { + "key": "alert-key", + "type": "someAlertType", + "severity": "low", + "category": category, + } + + def test_known_category_is_preserved(self): + alert = SocketAlert.from_dict(self._base_payload("supplyChainRisk")) + self.assertEqual(alert.category, SocketCategory.SUPPLY_CHAIN_RISK) + self.assertEqual(alert.severity, SocketIssueSeverity.LOW) + + def test_unknown_category_falls_back_to_miscellaneous(self): + alert = SocketAlert.from_dict(self._base_payload("other")) + self.assertEqual(alert.category, SocketCategory.MISCELLANEOUS) + + def test_unknown_category_does_not_raise(self): + # Explicit regression assertion: no ValueError for brand-new categories. + try: + SocketAlert.from_dict(self._base_payload("somethingCompletelyNew")) + except ValueError as exc: + self.fail(f"SocketAlert.from_dict raised ValueError for unknown category: {exc}") + + def test_unknown_category_emits_warning(self): + with self.assertLogs("socketdev", level=logging.WARNING) as captured: + SocketAlert.from_dict(self._base_payload("other")) + self.assertTrue( + any("Unknown SocketCategory" in message for message in captured.output), + f"expected a warning about the unknown category, got: {captured.output}", + ) + + def test_every_known_category_round_trips(self): + for category in SocketCategory: + with self.subTest(category=category): + alert = SocketAlert.from_dict(self._base_payload(category.value)) + self.assertEqual(alert.category, category) + + +if __name__ == "__main__": + unittest.main()