diff --git a/pyoaev/contracts/contract_config.py b/pyoaev/contracts/contract_config.py index d22a6d7..b2ea50f 100644 --- a/pyoaev/contracts/contract_config.py +++ b/pyoaev/contracts/contract_config.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List +from typing import Dict, List, Optional from pyoaev import utils from pyoaev.contracts.contract_utils import ContractCardinality, ContractVariable @@ -285,7 +285,28 @@ def get_type(self) -> str: @dataclass class ContractExpectations(ContractCardinalityElement): cardinality: str = ContractCardinality.Multiple + # Full set the user may choose from in the "add expectation" picker. availableExpectations: List[Expectation] = field(default_factory=list) + # Subset pre-filled by default when the inject / atomic testing is created. + # The OpenAEV platform reads this field-level array (not the per-expectation + # flag) to pre-populate expectations, so it must be emitted in the contract. + # None means "not provided": the list is then derived from the expectations + # flagged expectation_is_predefined=True. Pass an explicit list (possibly + # empty) to override the derivation. + predefinedExpectations: Optional[List[Expectation]] = None + + def __post_init__(self): + super().__post_init__() + # When the caller did not pass an explicit predefined list, derive it from + # the expectations flagged predefined in the available set. This keeps the + # ergonomic single-list API (flag each Expectation with + # expectation_is_predefined=True) working end-to-end on the platform. + if self.predefinedExpectations is None: + self.predefinedExpectations = [ + expectation + for expectation in self.availableExpectations + if expectation.expectation_is_predefined + ] @property def get_type(self) -> str: diff --git a/test/contracts/__init__.py b/test/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/contracts/test_contract_expectations.py b/test/contracts/test_contract_expectations.py new file mode 100644 index 0000000..f054517 --- /dev/null +++ b/test/contracts/test_contract_expectations.py @@ -0,0 +1,109 @@ +import json +import unittest + +from pyoaev import utils +from pyoaev.contracts.contract_config import ( + ContractCardinality, + ContractExpectations, + Expectation, + ExpectationType, +) + + +def _expectation(expectation_type, name, is_predefined): + return Expectation( + expectation_type=expectation_type, + expectation_name=name, + expectation_description="", + expectation_score=100, + expectation_expectation_group=False, + expectation_is_predefined=is_predefined, + ) + + +def _serialize(field): + return json.loads(json.dumps(field, cls=utils.EnhancedJSONEncoder)) + + +class TestContractExpectations(unittest.TestCase): + def test_predefined_derived_from_flag(self): + """ + The platform pre-fills expectations from the field-level + ``predefinedExpectations`` array, so it must be populated from the + expectations flagged ``expectation_is_predefined=True``. + """ + field = ContractExpectations( + key="expectations", + label="Expectations", + cardinality=ContractCardinality.Multiple, + availableExpectations=[ + _expectation(ExpectationType.detection, "Detection", True), + _expectation(ExpectationType.prevention, "Prevention", True), + _expectation(ExpectationType.vulnerability, "Not vulnerable", False), + ], + ) + + data = _serialize(field) + + self.assertEqual("expectation", data["type"]) + self.assertEqual( + ["DETECTION", "PREVENTION", "VULNERABILITY"], + [e["expectation_type"] for e in data["availableExpectations"]], + ) + # Only the flagged ones are pre-filled. + self.assertEqual( + ["DETECTION", "PREVENTION"], + [e["expectation_type"] for e in data["predefinedExpectations"]], + ) + + def test_explicit_predefined_is_kept(self): + detection = _expectation(ExpectationType.detection, "Detection", False) + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[detection], + predefinedExpectations=[detection], + ) + + data = _serialize(field) + + self.assertEqual( + ["DETECTION"], + [e["expectation_type"] for e in data["predefinedExpectations"]], + ) + + def test_explicit_empty_list_overrides_flags(self): + """ + An explicit empty ``predefinedExpectations`` list must suppress the + derivation even when some available expectations carry the predefined + flag, while still serializing as an (empty) array. + """ + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation(ExpectationType.detection, "Detection", True), + ], + predefinedExpectations=[], + ) + + data = _serialize(field) + + self.assertEqual([], data["predefinedExpectations"]) + + def test_no_flag_yields_no_predefined(self): + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation(ExpectationType.vulnerability, "Not vulnerable", False), + ], + ) + + data = _serialize(field) + + self.assertEqual([], data["predefinedExpectations"]) + + +if __name__ == "__main__": + unittest.main()