From 41054bb5d3f2a5017c2c85e84cfc20feadb495ba Mon Sep 17 00:00:00 2001 From: Prasad Deshpande Date: Mon, 19 Oct 2026 01:37:45 +0530 Subject: [PATCH 1/3] fix(ctf): reject max_invoice_amount=None at construction Signed-off-by: Prasad Deshpande --- .../ctf/detectors/implementations/invoice_threshold_bypass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finbot/ctf/detectors/implementations/invoice_threshold_bypass.py b/finbot/ctf/detectors/implementations/invoice_threshold_bypass.py index 1641e927..8f865e34 100644 --- a/finbot/ctf/detectors/implementations/invoice_threshold_bypass.py +++ b/finbot/ctf/detectors/implementations/invoice_threshold_bypass.py @@ -43,7 +43,7 @@ class InvoiceThresholdBypassDetector(BaseDetector): def _validate_config(self) -> None: threshold = self.config.get("max_invoice_amount") - if threshold is not None: + if "max_invoice_amount" in self.config: if not isinstance(threshold, (int, float)) or threshold <= 0: raise ValueError("max_invoice_amount must be a positive number") From 52833e1c6d5d7e4da69cf80309e48ef3355bce2b Mon Sep 17 00:00:00 2001 From: Prasad Deshpande Date: Mon, 19 Oct 2026 01:38:33 +0530 Subject: [PATCH 2/3] fix(ctf): reject min_amount=None at construction Signed-off-by: Prasad Deshpande --- finbot/ctf/detectors/implementations/invoice_trust_override.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finbot/ctf/detectors/implementations/invoice_trust_override.py b/finbot/ctf/detectors/implementations/invoice_trust_override.py index 26cb3db0..748a1e87 100644 --- a/finbot/ctf/detectors/implementations/invoice_trust_override.py +++ b/finbot/ctf/detectors/implementations/invoice_trust_override.py @@ -46,7 +46,7 @@ class InvoiceTrustOverrideDetector(BaseDetector): def _validate_config(self) -> None: min_amount = self.config.get("min_amount") - if min_amount is not None: + if "min_amount" in self.config: if not isinstance(min_amount, (int, float)) or min_amount <= 0: raise ValueError("min_amount must be a positive number") From 5de98efe902ef7ce2c42a940c88ee89f7e70b940 Mon Sep 17 00:00:00 2001 From: Prasad Deshpande Date: Mon, 19 Oct 2026 01:40:02 +0530 Subject: [PATCH 3/3] test(ctf): cover the numeric detector config keys Signed-off-by: Prasad Deshpande --- ...test_detector_numeric_config_validation.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/unit/ctf/test_detector_numeric_config_validation.py diff --git a/tests/unit/ctf/test_detector_numeric_config_validation.py b/tests/unit/ctf/test_detector_numeric_config_validation.py new file mode 100644 index 00000000..1f661737 --- /dev/null +++ b/tests/unit/ctf/test_detector_numeric_config_validation.py @@ -0,0 +1,67 @@ +""" +Numeric detector configuration validation tests. + +Covers the numeric detector config keys that are validated in +_validate_config() and then read back with a default in check_event(). +A key present with a None value has to be rejected at construction, +otherwise the default never applies and check_event() compares against +None, which raises TypeError and takes the detector out for the rest of +the process. + +This is the numeric counterpart of the list-typed keys covered in +test_detector_config_validation.py. +""" + +import pytest + +from finbot.ctf.detectors.implementations.invoice_threshold_bypass import ( + DEFAULT_MAX_INVOICE_AMOUNT, + InvoiceThresholdBypassDetector, +) +from finbot.ctf.detectors.implementations.invoice_trust_override import ( + DEFAULT_MIN_AMOUNT, + InvoiceTrustOverrideDetector, +) + +NUMERIC_CONFIG_KEYS = [ + (InvoiceThresholdBypassDetector, "max_invoice_amount", DEFAULT_MAX_INVOICE_AMOUNT), + (InvoiceTrustOverrideDetector, "min_amount", DEFAULT_MIN_AMOUNT), +] + + +class TestNoneRejected: + """A present-but-None numeric config must not reach check_event().""" + + @pytest.mark.parametrize("detector_cls, key, _default", NUMERIC_CONFIG_KEYS) + def test_none_value_rejected(self, detector_cls, key, _default) -> None: + with pytest.raises(ValueError, match="positive number"): + detector_cls(challenge_id="c", config={key: None}) + + @pytest.mark.parametrize("detector_cls, key, _default", NUMERIC_CONFIG_KEYS) + def test_non_numeric_still_rejected(self, detector_cls, key, _default) -> None: + with pytest.raises(ValueError, match="positive number"): + detector_cls(challenge_id="c", config={key: "not a number"}) + + @pytest.mark.parametrize("detector_cls, key, _default", NUMERIC_CONFIG_KEYS) + def test_non_positive_still_rejected(self, detector_cls, key, _default) -> None: + with pytest.raises(ValueError, match="positive number"): + detector_cls(challenge_id="c", config={key: 0}) + + +class TestValidConfigs: + """The accepted shapes keep working.""" + + @pytest.mark.parametrize("detector_cls, key, _default", NUMERIC_CONFIG_KEYS) + def test_positive_value_accepted(self, detector_cls, key, _default) -> None: + detector = detector_cls(challenge_id="c", config={key: 5000}) + assert detector.config[key] == 5000 + + @pytest.mark.parametrize("detector_cls, key, default", NUMERIC_CONFIG_KEYS) + def test_omitted_key_falls_back_to_default(self, detector_cls, key, default) -> None: + detector = detector_cls(challenge_id="c", config={}) + assert detector.config.get(key, default) == default + + @pytest.mark.parametrize("detector_cls, key, default", NUMERIC_CONFIG_KEYS) + def test_no_config_falls_back_to_default(self, detector_cls, key, default) -> None: + detector = detector_cls(challenge_id="c") + assert detector.config.get(key, default) == default