Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion pyoaev/contracts/contract_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Empty file added test/contracts/__init__.py
Empty file.
109 changes: 109 additions & 0 deletions test/contracts/test_contract_expectations.py
Original file line number Diff line number Diff line change
@@ -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()
Loading