Skip to content

Commit 4ba623b

Browse files
authored
feat(client): add PrimitiveType and ContractElement.argumentType (#334) (#333)
1 parent 4dedc2f commit 4ba623b

3 files changed

Lines changed: 174 additions & 0 deletions

File tree

pyoaev/contracts/contract_config.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,53 @@ class ContractOutputType(str, Enum):
6565
KerberoastableAccount: str = "kerberoastable_account"
6666

6767

68+
class PrimitiveType(str, Enum):
69+
"""The semantic type of a contract argument's value, independent of how the
70+
field renders in the UI (see ``ContractFieldType``) and independent of
71+
chaining specifically — an argument can carry a ``PrimitiveType`` and be
72+
filled by hand, exactly as it can be auto-linked from a prior step's
73+
matching output. Mirrors ``io.openaev.database.model.PrimitiveType``
74+
(the openaev platform's own enum) label-for-label; every value here must
75+
stay in sync with it.
76+
"""
77+
78+
AccountWithPasswordNotRequired: str = "account_with_password_not_required"
79+
ActionOutput: str = "action_output"
80+
AdminUsername: str = "admin_username"
81+
AsreproastableAccount: str = "asreproastable_account"
82+
AssetGroupId: str = "asset_group_id"
83+
AssetId: str = "asset_id"
84+
ComputerName: str = "computer_name"
85+
CVE: str = "cve"
86+
DelegationAccount: str = "delegation_account"
87+
Document: str = "document"
88+
Domain: str = "domain"
89+
FileName: str = "file_name"
90+
FilePath: str = "file_path"
91+
GroupName: str = "group_name"
92+
Hash: str = "hash"
93+
Host: str = "host"
94+
IPv4: str = "ipv4"
95+
IPv6: str = "ipv6"
96+
IpSubnet: str = "ip_subnet"
97+
KerberoastableAccount: str = "kerberoastable_account"
98+
Key: str = "key"
99+
Number: str = "number"
100+
Password: str = "password"
101+
Permissions: str = "permissions"
102+
Port: str = "port"
103+
Service: str = "service"
104+
Severity: str = "severity"
105+
ShareName: str = "share_name"
106+
SID: str = "sid"
107+
TargetedAsset: str = "targeted-asset"
108+
Text: str = "text"
109+
Username: str = "username"
110+
Value: str = "value"
111+
VulnerabilityName: str = "vulnerability_name"
112+
VulnerabilityStatus: str = "vulnerability_status"
113+
114+
68115
class ExpectationType(str, Enum):
69116
text: str = "TEXT"
70117
document: str = "DOCUMENT"
@@ -131,6 +178,15 @@ class ContractElement(ABC):
131178
linkedFields: List[str] = field(default_factory=list)
132179
mandatory: bool = False
133180
readOnly: bool = False
181+
# The argument's chaining/semantic type (``PrimitiveType``), e.g. "username"
182+
# or "host" — matches the platform's own ``argumentType`` field
183+
# (``io.openaev.database.model.ContractElement``) label-for-label so a
184+
# contract pushed from here needs no translation on the other end. Left
185+
# unset (``None``) for fields with no established type: the platform
186+
# normalizes a missing/null/empty value to ``PrimitiveType.Text`` on its
187+
# own, so omitting this is always safe and never a breaking change for
188+
# existing contracts.
189+
argumentType: Optional[PrimitiveType] = None
134190

135191
@property
136192
@abstractmethod
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
import unittest
3+
4+
from pyoaev import utils
5+
from pyoaev.contracts.contract_config import ContractText, PrimitiveType
6+
7+
8+
def _serialize(field):
9+
return json.loads(json.dumps(field, cls=utils.EnhancedJSONEncoder))
10+
11+
12+
class ContractElementArgumentTypeTest(unittest.TestCase):
13+
def test_defaults_to_none_when_not_declared(self):
14+
"""Existing contracts that never pass argumentType keep working
15+
unchanged — the platform normalizes a null value to PrimitiveType.Text
16+
on its own, so this is not a breaking change for anything already
17+
deployed."""
18+
untyped = ContractText(key="uri", label="URL")
19+
self.assertIsNone(untyped.argumentType)
20+
self.assertIsNone(_serialize(untyped)["argumentType"])
21+
22+
def test_explicit_argument_type_round_trips(self):
23+
typed = ContractText(
24+
key="basicUser", label="Username", argumentType=PrimitiveType.Username
25+
)
26+
self.assertEqual(typed.argumentType, PrimitiveType.Username)
27+
self.assertEqual(_serialize(typed)["argumentType"], "username")
28+
29+
def test_argument_type_is_independent_of_widget_type(self):
30+
"""A field's rendering (ContractFieldType, on `type`) and its chaining
31+
semantics (PrimitiveType, on `argumentType`) are separate axes."""
32+
typed = ContractText(
33+
key="basicUser", label="Username", argumentType=PrimitiveType.Username
34+
)
35+
serialized = _serialize(typed)
36+
self.assertEqual(serialized["type"], "text")
37+
self.assertEqual(serialized["argumentType"], "username")
38+
39+
40+
if __name__ == "__main__":
41+
unittest.main()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import unittest
2+
3+
from pyoaev.contracts.contract_config import PrimitiveType
4+
5+
6+
class PrimitiveTypeTest(unittest.TestCase):
7+
def test_username_wire_label(self):
8+
# The wire label is a public contract shared with the platform enum
9+
# (io.openaev.database.model.PrimitiveType.Username); it must stay
10+
# exactly "username".
11+
self.assertEqual(PrimitiveType.Username.value, "username")
12+
self.assertEqual(PrimitiveType.Username, "username")
13+
14+
def test_host_wire_label(self):
15+
self.assertEqual(PrimitiveType.Host.value, "host")
16+
self.assertEqual(PrimitiveType.Host, "host")
17+
18+
def test_text_wire_label(self):
19+
# The platform's own normalizer defaults a missing argumentType to
20+
# exactly this value — it must stay "text".
21+
self.assertEqual(PrimitiveType.Text.value, "text")
22+
self.assertEqual(PrimitiveType.Text, "text")
23+
24+
def test_action_output_wire_label(self):
25+
# Shared with ContractOutputType.ActionOutput; both enums describe the
26+
# same platform-side value from two different angles (an output
27+
# producing it vs. an input typed to receive it) and must not drift
28+
# apart.
29+
self.assertEqual(PrimitiveType.ActionOutput.value, "action_output")
30+
31+
def test_every_value_matches_the_openaev_platform_enum(self):
32+
# io.openaev.database.model.PrimitiveType, transcribed label-for-label.
33+
# Keep this set in sync with that file, not just the four spot-checked
34+
# above.
35+
expected_labels = {
36+
"account_with_password_not_required",
37+
"action_output",
38+
"admin_username",
39+
"asreproastable_account",
40+
"asset_group_id",
41+
"asset_id",
42+
"computer_name",
43+
"cve",
44+
"delegation_account",
45+
"document",
46+
"domain",
47+
"file_name",
48+
"file_path",
49+
"group_name",
50+
"hash",
51+
"host",
52+
"ipv4",
53+
"ipv6",
54+
"ip_subnet",
55+
"kerberoastable_account",
56+
"key",
57+
"number",
58+
"password",
59+
"permissions",
60+
"port",
61+
"service",
62+
"severity",
63+
"share_name",
64+
"sid",
65+
"targeted-asset",
66+
"text",
67+
"username",
68+
"value",
69+
"vulnerability_name",
70+
"vulnerability_status",
71+
}
72+
actual_labels = {member.value for member in PrimitiveType}
73+
self.assertEqual(actual_labels, expected_labels)
74+
75+
76+
if __name__ == "__main__":
77+
unittest.main()

0 commit comments

Comments
 (0)