-
Notifications
You must be signed in to change notification settings - Fork 656
Expand file tree
/
Copy pathtest_integrations.py
More file actions
259 lines (212 loc) · 10.5 KB
/
test_integrations.py
File metadata and controls
259 lines (212 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0; you may not use this file except in compliance with the Elastic License
# 2.0.
"""Test integration version resolution against EPR manifest ranges."""
import unittest
from semver import Version
from detection_rules.integrations import (
_parse_clause,
_parse_kibana_range,
_satisfies_kibana_range,
find_latest_compatible_version,
find_least_compatible_version,
)
def _manifest(kibana_version: str) -> dict:
"""Build a minimal manifest dict with the given conditions.kibana.version string."""
return {"conditions": {"kibana": {"version": kibana_version}}}
class TestParseClause(unittest.TestCase):
"""Test parsing of individual npm-style range clauses."""
def test_caret(self):
"""Caret range expands to [X.Y.Z, (X+1).0.0)."""
lo, hi = _parse_clause("^9.1.0")
self.assertEqual(lo, Version(9, 1, 0))
self.assertEqual(hi, Version(10, 0, 0))
def test_tilde(self):
"""Tilde range expands to [X.Y.Z, X.(Y+1).0)."""
lo, hi = _parse_clause("~8.10.0")
self.assertEqual(lo, Version(8, 10, 0))
self.assertEqual(hi, Version(8, 11, 0))
def test_gte(self):
"""Greater-than-or-equal leaves the upper bound unbounded."""
lo, hi = _parse_clause(">=8.12.0")
self.assertEqual(lo, Version(8, 12, 0))
self.assertIsNone(hi)
def test_gt(self):
"""Strict greater-than bumps the patch on the lower bound."""
lo, hi = _parse_clause(">8.12.0")
self.assertEqual(lo, Version(8, 12, 1))
self.assertIsNone(hi)
def test_lte(self):
"""Less-than-or-equal produces an exclusive upper bound at the next patch."""
lo, hi = _parse_clause("<=9.0.0")
self.assertEqual(lo, Version(0, 0, 0))
self.assertEqual(hi, Version(9, 0, 1))
def test_lt(self):
"""Strict less-than is the exclusive upper bound."""
lo, hi = _parse_clause("<9.0.0")
self.assertEqual(lo, Version(0, 0, 0))
self.assertEqual(hi, Version(9, 0, 0))
def test_eq_explicit(self):
"""Explicit ``=X.Y.Z`` pins the range to that single version."""
lo, hi = _parse_clause("=8.12.0")
self.assertEqual(lo, Version(8, 12, 0))
self.assertEqual(hi, Version(8, 12, 1))
def test_bare(self):
"""A bare version token pins the range to that single version."""
lo, hi = _parse_clause("8.12.0")
self.assertEqual(lo, Version(8, 12, 0))
self.assertEqual(hi, Version(8, 12, 1))
def test_anded_tokens(self):
"""Whitespace-separated tokens in a clause are AND'd together."""
lo, hi = _parse_clause(">=8.12.0 <9.0.0")
self.assertEqual(lo, Version(8, 12, 0))
self.assertEqual(hi, Version(9, 0, 0))
def test_caret_on_zero_major_raises(self):
"""``^0.x.y`` is unsupported (npm semantics differ) and must raise."""
with self.assertRaises(ValueError):
_parse_clause("^0.1.0")
def test_unsupported_token_raises(self):
"""Unknown operators must raise ``ValueError`` so we fail loudly."""
with self.assertRaises(ValueError):
_parse_clause("!9.1.0")
class TestParseKibanaRange(unittest.TestCase):
"""Test parsing of full EPR ``conditions.kibana.version`` strings."""
def test_single_clause(self):
"""A single clause yields a single [lo, hi) tuple."""
self.assertEqual(
_parse_kibana_range("^9.1.0"),
[(Version(9, 1, 0), Version(10, 0, 0))],
)
def test_or_clauses(self):
"""Clauses separated by ``||`` are returned as a list of OR'd ranges."""
self.assertEqual(
_parse_kibana_range("^8.12.0 || ^9.0.0"),
[
(Version(8, 12, 0), Version(9, 0, 0)),
(Version(9, 0, 0), Version(10, 0, 0)),
],
)
def test_mixed_and_or(self):
"""AND'd tokens inside each clause and OR'd clauses compose correctly."""
self.assertEqual(
_parse_kibana_range(">=8.12.0 <9.0.0 || ^9.1.0"),
[
(Version(8, 12, 0), Version(9, 0, 0)),
(Version(9, 1, 0), Version(10, 0, 0)),
],
)
class TestSatisfiesKibanaRange(unittest.TestCase):
"""Test range satisfaction for a given stack version."""
def test_caret_matches_same_major(self):
"""Caret on X.Y.Z matches later minors/patches within the same major."""
self.assertTrue(_satisfies_kibana_range(Version(9, 4, 0), "^9.1.0"))
self.assertTrue(_satisfies_kibana_range(Version(9, 1, 0), "^9.1.0"))
def test_caret_rejects_lower_minor(self):
"""Caret rejects stacks below its floor minor."""
self.assertFalse(_satisfies_kibana_range(Version(9, 0, 0), "^9.1.0"))
def test_caret_rejects_next_major(self):
"""Caret rejects the next major as its upper bound is exclusive."""
self.assertFalse(_satisfies_kibana_range(Version(10, 0, 0), "^9.1.0"))
def test_caret_rejects_prior_major(self):
"""Regression: 9.1 must NOT satisfy ^9.4.0 (drove 46 rule failures)."""
self.assertFalse(_satisfies_kibana_range(Version(9, 1, 0), "^9.4.0"))
def test_or_union(self):
"""OR'd clauses accept stacks inside either clause and reject otherwise."""
self.assertTrue(_satisfies_kibana_range(Version(8, 12, 5), "^8.12.0 || ^9.0.0"))
self.assertTrue(_satisfies_kibana_range(Version(9, 0, 1), "^8.12.0 || ^9.0.0"))
self.assertFalse(_satisfies_kibana_range(Version(10, 0, 0), "^8.12.0 || ^9.0.0"))
def test_anded_bounds(self):
"""AND'd bounds produce a half-open interval [lo, hi)."""
self.assertTrue(_satisfies_kibana_range(Version(8, 13, 0), ">=8.12.0 <9.0.0"))
self.assertFalse(_satisfies_kibana_range(Version(9, 0, 0), ">=8.12.0 <9.0.0"))
class TestFindLatestCompatibleVersion(unittest.TestCase):
"""Regression + behavior coverage for ``find_latest_compatible_version``."""
def test_picks_latest_compatible_on_same_major(self):
"""Returns the newest manifest whose range admits the stack, with a notice for any skipped newer manifest."""
manifests = {
"ded": {
"1.0.0": _manifest("^8.12.0"),
"2.0.0": _manifest("^9.0.0"),
"2.1.0": _manifest("^9.1.0"),
"3.0.0": _manifest("^9.4.0"),
}
}
version, notice = find_latest_compatible_version("ded", "ded", Version(9, 1, 0), manifests)
self.assertEqual(version, "2.1.0")
self.assertTrue(notice)
self.assertIn("3.0.0", notice[0])
self.assertIn("9.4.0", notice[1])
def test_regression_91_does_not_pick_ded_300(self):
"""Regression: on a 9.1 stack we must not select a manifest that requires ^9.4.0."""
manifests = {
"ded": {
"2.1.0": _manifest("^9.1.0"),
"3.0.0": _manifest("^9.4.0"),
}
}
version, _ = find_latest_compatible_version("ded", "ded", Version(9, 1, 0), manifests)
self.assertEqual(version, "2.1.0")
def test_exact_match_on_rule_stack(self):
"""When the only manifest exactly satisfies the stack, notice stays empty."""
manifests = {"pkg": {"1.0.0": _manifest("^9.4.0")}}
version, notice = find_latest_compatible_version("pkg", "pkg", Version(9, 4, 0), manifests)
self.assertEqual(version, "1.0.0")
self.assertEqual(notice, [""])
def test_or_clause_match(self):
"""A stack that falls in any OR'd sub-range is considered compatible."""
manifests = {"pkg": {"1.0.0": _manifest("^8.12.0 || ^9.0.0")}}
version, _ = find_latest_compatible_version("pkg", "pkg", Version(8, 15, 0), manifests)
self.assertEqual(version, "1.0.0")
def test_no_compatible_version_raises(self):
"""``ValueError`` when no manifest is compatible with the rule stack."""
manifests = {"pkg": {"1.0.0": _manifest("^9.4.0")}}
with self.assertRaises(ValueError):
find_latest_compatible_version("pkg", "pkg", Version(8, 12, 0), manifests)
def test_missing_conditions_raises(self):
"""Manifest without ``conditions.kibana.version`` raises ``ValueError``."""
manifests = {"pkg": {"1.0.0": {"conditions": {}}}}
with self.assertRaises(ValueError):
find_latest_compatible_version("pkg", "pkg", Version(9, 1, 0), manifests)
def test_unknown_package_raises(self):
"""Unknown package raises ``ValueError``."""
with self.assertRaises(ValueError):
find_latest_compatible_version("missing", "missing", Version(9, 1, 0), {})
class TestFindLeastCompatibleVersion(unittest.TestCase):
"""Behavior coverage for ``find_least_compatible_version``."""
def test_picks_oldest_compatible_in_latest_major(self):
"""Returns the oldest manifest in the latest major whose range admits the stack."""
manifests = {
"pkg": {
"1.0.0": _manifest("^8.12.0"),
"1.5.0": _manifest("^8.12.0"),
"2.0.0": _manifest("^9.0.0"),
"2.1.0": _manifest("^9.1.0"),
"2.5.0": _manifest("^9.1.0"),
}
}
# 2.0.0 (^9.0.0) is the oldest 9.x manifest that admits a 9.1.0 stack.
self.assertEqual(find_least_compatible_version("pkg", "pkg", "9.1.0", manifests), "^2.0.0")
def test_no_compatible_in_any_major_raises(self):
"""When neither the latest nor any prior major admits the stack, raise."""
manifests = {
"pkg": {
"1.0.0": _manifest("^8.12.0"),
"2.0.0": _manifest("^9.4.0"),
}
}
with self.assertRaises(ValueError):
find_least_compatible_version("pkg", "pkg", "9.1.0", manifests)
def test_cross_major_fallback(self):
"""Falls back to an earlier major when the latest major is incompatible."""
manifests = {
"pkg": {
"1.0.0": _manifest("^8.12.0"),
"2.0.0": _manifest("^9.4.0"),
}
}
self.assertEqual(find_least_compatible_version("pkg", "pkg", "8.12.0", manifests), "^1.0.0")
def test_or_clause(self):
"""OR'd clauses are honored by the least-compatible search."""
manifests = {"pkg": {"1.0.0": _manifest("^8.12.0 || ^9.0.0")}}
self.assertEqual(find_least_compatible_version("pkg", "pkg", "9.1.0", manifests), "^1.0.0")