enigma-api-client 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- enigma_api_client/__init__.py +16 -0
- enigma_api_client/client.py +134 -0
- enigma_api_client/client_inject_id_and_typename_helper.py +248 -0
- enigma_api_client/client_inject_urls_helper.py +26 -0
- enigma_api_client/plugins/.DS_Store +0 -0
- enigma_api_client/plugins/enigma-api/.claude-plugin/plugin.json +8 -0
- enigma_api_client/plugins/enigma-api/README.md +82 -0
- enigma_api_client/plugins/enigma-api/harness/conftest.py +42 -0
- enigma_api_client/plugins/enigma-api/harness/test_linter.py +863 -0
- enigma_api_client/plugins/enigma-api/harness/test_queries.py +143 -0
- enigma_api_client/plugins/enigma-api/harness/test_skill_rules.py +351 -0
- enigma_api_client/plugins/enigma-api/pyproject.toml +9 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/lint_query.cpython-313.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-313.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-314.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/generate_report.py +384 -0
- enigma_api_client/plugins/enigma-api/scripts/lint_query.py +712 -0
- enigma_api_client/plugins/enigma-api/scripts/query_validator.py +352 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/SKILL.md +620 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/03_brand_profile.md +163 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/13_brand_dump.md +95 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/derived/07_coalesce_ein_formation.md +146 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/01_coffee_shops_zip.md +287 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/11_ol_industry_discovery.md +116 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/export/08_segment_export.md +96 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/02_officers_lookup.md +366 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/10_kyb_verify.md +176 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/legal_entity/09_legal_entity_profile.md +97 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/locations/05_brand_locations_by_state.md +120 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/person_search/04_find_person_companies.md +160 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/revenue/06_brand_revenue.md +176 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/risk/12_risk_signals.md +118 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/card_transactions.md +91 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/field_corrections.json +106 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/filter_syntax.md +203 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/naics_2022_mapping.json +184 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/pagination_template.py +115 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/schema.graphql +44971 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/test_examples.py +1122 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/validate_query.py +177 -0
- enigma_api_client/plugins/enigma-api/test-cases/graphql-test-cases.md +196 -0
- enigma_api_client/plugins/enigma-api/tests/accuracy_cases.csv +116 -0
- enigma_api_client/plugins/enigma-api/tests/applicability_review.csv +64 -0
- enigma_api_client/plugins/enigma-api/tests/parallel-ground-truth.csv +101 -0
- enigma_api_client/plugins/enigma-api/tests/run_tests.sh +320 -0
- enigma_api_client/plugins/enigma-api/tests/test_cases.yaml +213 -0
- enigma_api_client/plugins/enigma-api/tests/validation_cases.csv +101 -0
- enigma_api_client/py.typed +0 -0
- enigma_api_client/query_router/__init__.py +3 -0
- enigma_api_client/query_router/geo.py +121 -0
- enigma_api_client/query_router/templates.py +475 -0
- enigma_api_client/strategies/__init__.py +16 -0
- enigma_api_client/strategies/agent.py +63 -0
- enigma_api_client/strategies/base.py +15 -0
- enigma_api_client/strategies/generative.py +143 -0
- enigma_api_client/strategies/llm_provider.py +137 -0
- enigma_api_client/strategies/service.py +103 -0
- enigma_api_client/strategies/template.py +189 -0
- enigma_api_client-0.3.1.dist-info/METADATA +257 -0
- enigma_api_client-0.3.1.dist-info/RECORD +63 -0
- enigma_api_client-0.3.1.dist-info/WHEEL +5 -0
- enigma_api_client-0.3.1.dist-info/licenses/LICENSE +201 -0
- enigma_api_client-0.3.1.dist-info/top_level.txt +5 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
# ABOUTME: Unit tests for the 18 lint rules in lint_query.py
|
|
2
|
+
# ABOUTME: Each rule has a test class with bad (triggers) and good (clean) queries
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Unit tests for the Enigma GraphQL query linter.
|
|
6
|
+
|
|
7
|
+
Each ENIGMA-NNN rule gets its own test class with:
|
|
8
|
+
- test_bad_query_triggers — a query that SHOULD fire the rule
|
|
9
|
+
- test_good_query_clean — a query that should NOT fire the rule
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
uv run pytest harness/test_linter.py -v
|
|
13
|
+
uv run pytest harness/test_linter.py -k "ENIGMA_006" -v
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
# Add scripts/ to sys.path
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
23
|
+
|
|
24
|
+
from lint_query import lint_query, LintResult, Severity # noqa: E402
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Helpers
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def assert_triggers(query: str, rule_id: str, *, severity: Severity | None = None):
|
|
32
|
+
"""Assert that linting the query triggers the given rule."""
|
|
33
|
+
result = lint_query(query)
|
|
34
|
+
rule_ids = [f.rule_id for f in result.findings]
|
|
35
|
+
assert rule_id in rule_ids, (
|
|
36
|
+
f"Expected rule {rule_id} to fire but got: {rule_ids}"
|
|
37
|
+
)
|
|
38
|
+
if severity is not None:
|
|
39
|
+
for f in result.findings:
|
|
40
|
+
if f.rule_id == rule_id:
|
|
41
|
+
assert f.severity == severity, (
|
|
42
|
+
f"Expected {severity.value} but got {f.severity.value}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assert_clean(query: str, rule_id: str):
|
|
47
|
+
"""Assert that linting the query does NOT trigger the given rule."""
|
|
48
|
+
result = lint_query(query)
|
|
49
|
+
rule_ids = [f.rule_id for f in result.findings]
|
|
50
|
+
assert rule_id not in rule_ids, (
|
|
51
|
+
f"Rule {rule_id} should NOT fire but it did: "
|
|
52
|
+
+ "; ".join(f.message for f in result.findings if f.rule_id == rule_id)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# ENIGMA-001: LegalEntity.persons usage
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
class TestENIGMA001PersonsOnLE:
|
|
61
|
+
RULE = "ENIGMA-001"
|
|
62
|
+
|
|
63
|
+
def test_bad_query_triggers(self):
|
|
64
|
+
query = """
|
|
65
|
+
query Bad($searchInput: SearchInput!) {
|
|
66
|
+
search(searchInput: $searchInput) {
|
|
67
|
+
... on LegalEntity {
|
|
68
|
+
id
|
|
69
|
+
persons(first: 10) {
|
|
70
|
+
edges { node { names { edges { node { fullName } } } } }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
76
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
77
|
+
|
|
78
|
+
def test_good_query_uses_registered_entities(self):
|
|
79
|
+
query = """
|
|
80
|
+
query Good($searchInput: SearchInput!) {
|
|
81
|
+
search(searchInput: $searchInput) {
|
|
82
|
+
... on LegalEntity {
|
|
83
|
+
id
|
|
84
|
+
registeredEntities {
|
|
85
|
+
edges {
|
|
86
|
+
node {
|
|
87
|
+
registrations(first: 5) {
|
|
88
|
+
edges { node { roles(first: 20) { edges { node { jobTitle } } } } }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
"""
|
|
97
|
+
assert_clean(query, self.RULE)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# ENIGMA-002: Direct roles on LegalEntity
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
class TestENIGMA002DirectRolesOnLE:
|
|
105
|
+
RULE = "ENIGMA-002"
|
|
106
|
+
|
|
107
|
+
def test_bad_direct_roles(self):
|
|
108
|
+
query = """
|
|
109
|
+
query Bad($searchInput: SearchInput!) {
|
|
110
|
+
search(searchInput: $searchInput) {
|
|
111
|
+
... on LegalEntity {
|
|
112
|
+
id
|
|
113
|
+
roles(first: 20) {
|
|
114
|
+
edges { node { jobTitle } }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
"""
|
|
120
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
121
|
+
|
|
122
|
+
def test_good_roles_under_registered_entities(self):
|
|
123
|
+
query = """
|
|
124
|
+
query Good($searchInput: SearchInput!) {
|
|
125
|
+
search(searchInput: $searchInput) {
|
|
126
|
+
... on LegalEntity {
|
|
127
|
+
id
|
|
128
|
+
registeredEntities {
|
|
129
|
+
edges {
|
|
130
|
+
node {
|
|
131
|
+
registrations(first: 5) {
|
|
132
|
+
edges {
|
|
133
|
+
node {
|
|
134
|
+
roles(first: 20) {
|
|
135
|
+
edges { node { jobTitle } }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
"""
|
|
147
|
+
assert_clean(query, self.RULE)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# ENIGMA-003: Missing pagination (first: N)
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
class TestENIGMA003MissingPagination:
|
|
155
|
+
RULE = "ENIGMA-003"
|
|
156
|
+
|
|
157
|
+
def test_bad_no_first(self):
|
|
158
|
+
query = """
|
|
159
|
+
query Bad($searchInput: SearchInput!) {
|
|
160
|
+
search(searchInput: $searchInput) {
|
|
161
|
+
... on Brand {
|
|
162
|
+
id
|
|
163
|
+
operatingLocations {
|
|
164
|
+
edges { node { id } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
"""
|
|
170
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
171
|
+
|
|
172
|
+
def test_good_with_first(self):
|
|
173
|
+
query = """
|
|
174
|
+
query Good($searchInput: SearchInput!) {
|
|
175
|
+
search(searchInput: $searchInput) {
|
|
176
|
+
... on Brand {
|
|
177
|
+
id
|
|
178
|
+
operatingLocations(first: 10) {
|
|
179
|
+
edges { node { id } }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
"""
|
|
185
|
+
assert_clean(query, self.RULE)
|
|
186
|
+
|
|
187
|
+
def test_skip_directive_exempt(self):
|
|
188
|
+
"""Fields with @skip(if: true) are exempt from pagination."""
|
|
189
|
+
query = """
|
|
190
|
+
query Skip($searchInput: SearchInput!) {
|
|
191
|
+
search(searchInput: $searchInput) {
|
|
192
|
+
... on OperatingLocation {
|
|
193
|
+
legalEntities @skip(if: true) {
|
|
194
|
+
edges { node { tins(first: 1) { edges { node { tin } } } } }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
"""
|
|
200
|
+
# Should not trigger for legalEntities since it has @skip
|
|
201
|
+
result = lint_query(query)
|
|
202
|
+
le_findings = [f for f in result.findings
|
|
203
|
+
if f.rule_id == self.RULE and "legalEntities" in f.message]
|
|
204
|
+
assert len(le_findings) == 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# ENIGMA-004: Missing entityType on searchInput
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
class TestENIGMA004MissingEntityType:
|
|
212
|
+
RULE = "ENIGMA-004"
|
|
213
|
+
|
|
214
|
+
def test_bad_no_entity_type(self):
|
|
215
|
+
query = """
|
|
216
|
+
query Bad {
|
|
217
|
+
search(searchInput: { name: "Acme" }) {
|
|
218
|
+
... on Brand { id }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
"""
|
|
222
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
223
|
+
|
|
224
|
+
def test_good_with_entity_type(self):
|
|
225
|
+
query = """
|
|
226
|
+
query Good {
|
|
227
|
+
search(searchInput: { name: "Acme", entityType: BRAND }) {
|
|
228
|
+
... on Brand { id }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
"""
|
|
232
|
+
assert_clean(query, self.RULE)
|
|
233
|
+
|
|
234
|
+
def test_variable_is_exempt(self):
|
|
235
|
+
"""When searchInput is a $variable, we can't lint it inline."""
|
|
236
|
+
query = """
|
|
237
|
+
query Good($searchInput: SearchInput!) {
|
|
238
|
+
search(searchInput: $searchInput) {
|
|
239
|
+
... on Brand { id }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
"""
|
|
243
|
+
assert_clean(query, self.RULE)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# ENIGMA-005: prompt with non-BRAND entityType
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
class TestENIGMA005PromptNonBrand:
|
|
251
|
+
RULE = "ENIGMA-005"
|
|
252
|
+
|
|
253
|
+
def test_bad_prompt_with_ol(self):
|
|
254
|
+
query = """
|
|
255
|
+
query Bad {
|
|
256
|
+
search(searchInput: { prompt: "coffee shop", entityType: OPERATING_LOCATION }) {
|
|
257
|
+
... on OperatingLocation { id }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
"""
|
|
261
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
262
|
+
|
|
263
|
+
def test_good_prompt_with_brand(self):
|
|
264
|
+
query = """
|
|
265
|
+
query Good {
|
|
266
|
+
search(searchInput: { prompt: "coffee shop", entityType: BRAND }) {
|
|
267
|
+
... on Brand { id }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
"""
|
|
271
|
+
assert_clean(query, self.RULE)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# ENIGMA-006: Unfiltered cardTransactions
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
class TestENIGMA006UnfilteredCardTransactions:
|
|
279
|
+
RULE = "ENIGMA-006"
|
|
280
|
+
|
|
281
|
+
def test_bad_no_conditions(self):
|
|
282
|
+
query = """
|
|
283
|
+
query Bad {
|
|
284
|
+
search(searchInput: { name: "Acme", entityType: BRAND }) {
|
|
285
|
+
... on Brand {
|
|
286
|
+
cardTransactions(first: 50) {
|
|
287
|
+
edges { node { projectedQuantity } }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
"""
|
|
293
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
294
|
+
|
|
295
|
+
def test_good_with_conditions(self):
|
|
296
|
+
query = """
|
|
297
|
+
query Good {
|
|
298
|
+
search(searchInput: { name: "Acme", entityType: BRAND }) {
|
|
299
|
+
... on Brand {
|
|
300
|
+
cardTransactions(first: 1, conditions: $revCond) {
|
|
301
|
+
edges { node { projectedQuantity } }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
"""
|
|
307
|
+
assert_clean(query, self.RULE)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# ENIGMA-007: Deep traversal (>5 levels)
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
class TestENIGMA007DeepTraversal:
|
|
315
|
+
RULE = "ENIGMA-007"
|
|
316
|
+
|
|
317
|
+
def test_bad_6_levels(self):
|
|
318
|
+
"""6 levels of non-leaf connections should trigger."""
|
|
319
|
+
query = """
|
|
320
|
+
query Deep($searchInput: SearchInput!) {
|
|
321
|
+
search(searchInput: $searchInput) {
|
|
322
|
+
... on Brand {
|
|
323
|
+
operatingLocations(first: 5) {
|
|
324
|
+
edges {
|
|
325
|
+
node {
|
|
326
|
+
brands(first: 3) {
|
|
327
|
+
edges {
|
|
328
|
+
node {
|
|
329
|
+
operatingLocations(first: 3) {
|
|
330
|
+
edges {
|
|
331
|
+
node {
|
|
332
|
+
brands(first: 3) {
|
|
333
|
+
edges {
|
|
334
|
+
node {
|
|
335
|
+
operatingLocations(first: 3) {
|
|
336
|
+
edges {
|
|
337
|
+
node {
|
|
338
|
+
brands(first: 3) {
|
|
339
|
+
edges { node { id } }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
"""
|
|
360
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
361
|
+
|
|
362
|
+
def test_good_3_levels(self):
|
|
363
|
+
query = """
|
|
364
|
+
query Shallow($searchInput: SearchInput!) {
|
|
365
|
+
search(searchInput: $searchInput) {
|
|
366
|
+
... on Brand {
|
|
367
|
+
operatingLocations(first: 5) {
|
|
368
|
+
edges {
|
|
369
|
+
node {
|
|
370
|
+
brands(first: 1) {
|
|
371
|
+
edges { node { id } }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
"""
|
|
380
|
+
assert_clean(query, self.RULE)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# ENIGMA-008: roles on Brand
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
class TestENIGMA008RolesOnBrand:
|
|
388
|
+
RULE = "ENIGMA-008"
|
|
389
|
+
|
|
390
|
+
def test_bad_roles_on_brand(self):
|
|
391
|
+
query = """
|
|
392
|
+
query Bad($searchInput: SearchInput!) {
|
|
393
|
+
search(searchInput: $searchInput) {
|
|
394
|
+
... on Brand {
|
|
395
|
+
id
|
|
396
|
+
roles(first: 10) {
|
|
397
|
+
edges { node { jobTitle } }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
"""
|
|
403
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
404
|
+
|
|
405
|
+
def test_good_roles_on_registration(self):
|
|
406
|
+
"""roles under legalEntities > registeredEntities > registrations is fine."""
|
|
407
|
+
query = """
|
|
408
|
+
query Good($searchInput: SearchInput!) {
|
|
409
|
+
search(searchInput: $searchInput) {
|
|
410
|
+
... on Brand {
|
|
411
|
+
id
|
|
412
|
+
legalEntities(first: 5) {
|
|
413
|
+
edges {
|
|
414
|
+
node {
|
|
415
|
+
registeredEntities(first: 3) {
|
|
416
|
+
edges {
|
|
417
|
+
node {
|
|
418
|
+
registrations(first: 5) {
|
|
419
|
+
edges {
|
|
420
|
+
node {
|
|
421
|
+
roles(first: 20) {
|
|
422
|
+
edges { node { jobTitle } }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
"""
|
|
437
|
+
assert_clean(query, self.RULE)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
# ENIGMA-009: Multi-field filter without AND/OR
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
class TestENIGMA009MultiFieldFilter:
|
|
445
|
+
RULE = "ENIGMA-009"
|
|
446
|
+
|
|
447
|
+
def test_bad_multiple_keys_no_wrapper(self):
|
|
448
|
+
query = """
|
|
449
|
+
query Bad {
|
|
450
|
+
search(searchInput: { name: "Acme", entityType: BRAND }) {
|
|
451
|
+
... on Brand {
|
|
452
|
+
operatingLocations(first: 10, conditions: {
|
|
453
|
+
filter: { EQ: ["addresses.state", "NY"], NE: ["addresses.city", "BUFFALO"] }
|
|
454
|
+
}) {
|
|
455
|
+
edges { node { id } }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
"""
|
|
461
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
462
|
+
|
|
463
|
+
def test_good_with_and_wrapper(self):
|
|
464
|
+
query = """
|
|
465
|
+
query Good {
|
|
466
|
+
search(searchInput: { name: "Acme", entityType: BRAND }) {
|
|
467
|
+
... on Brand {
|
|
468
|
+
operatingLocations(first: 10, conditions: {
|
|
469
|
+
filter: { AND: [{ EQ: ["addresses.state", "NY"] }, { NE: ["addresses.city", "BUFFALO"] }] }
|
|
470
|
+
}) {
|
|
471
|
+
edges { node { id } }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
"""
|
|
477
|
+
assert_clean(query, self.RULE)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ---------------------------------------------------------------------------
|
|
481
|
+
# ENIGMA-010: Missing search limit
|
|
482
|
+
# ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
class TestENIGMA010MissingSearchLimit:
|
|
485
|
+
RULE = "ENIGMA-010"
|
|
486
|
+
|
|
487
|
+
def test_bad_no_limit(self):
|
|
488
|
+
query = """
|
|
489
|
+
query Bad {
|
|
490
|
+
search(searchInput: { name: "Acme", entityType: BRAND }) {
|
|
491
|
+
... on Brand { id }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
"""
|
|
495
|
+
assert_triggers(query, self.RULE, severity=Severity.ERROR)
|
|
496
|
+
|
|
497
|
+
def test_good_with_limit(self):
|
|
498
|
+
query = """
|
|
499
|
+
query Good {
|
|
500
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 10 } }) {
|
|
501
|
+
... on Brand { id }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
"""
|
|
505
|
+
assert_clean(query, self.RULE)
|
|
506
|
+
|
|
507
|
+
def test_variable_exempt(self):
|
|
508
|
+
query = """
|
|
509
|
+
query Good($searchInput: SearchInput!) {
|
|
510
|
+
search(searchInput: $searchInput) {
|
|
511
|
+
... on Brand { id }
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
"""
|
|
515
|
+
assert_clean(query, self.RULE)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# ENIGMA-011: HAS operator usage
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
class TestENIGMA011HasOperator:
|
|
523
|
+
RULE = "ENIGMA-011"
|
|
524
|
+
|
|
525
|
+
def test_bad_has_operator(self):
|
|
526
|
+
query = """
|
|
527
|
+
query Bad {
|
|
528
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 10 } }) {
|
|
529
|
+
... on Brand {
|
|
530
|
+
operatingLocations(first: 10, conditions: {
|
|
531
|
+
filter: { HAS: ["phoneNumbers.phoneNumber"] }
|
|
532
|
+
}) {
|
|
533
|
+
edges { node { id } }
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
"""
|
|
539
|
+
assert_triggers(query, self.RULE, severity=Severity.WARNING)
|
|
540
|
+
|
|
541
|
+
def test_good_no_has(self):
|
|
542
|
+
query = """
|
|
543
|
+
query Good {
|
|
544
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 10 } }) {
|
|
545
|
+
... on Brand {
|
|
546
|
+
operatingLocations(first: 10, conditions: {
|
|
547
|
+
filter: { IS_NOT_NULL: ["phoneNumbers.phoneNumber"] }
|
|
548
|
+
}) {
|
|
549
|
+
edges { node { id } }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
"""
|
|
555
|
+
assert_clean(query, self.RULE)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
# ENIGMA-012: Conditions on unsupported fields
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
class TestENIGMA012ConditionsUnsupported:
|
|
563
|
+
RULE = "ENIGMA-012"
|
|
564
|
+
|
|
565
|
+
def test_bad_conditions_on_roles(self):
|
|
566
|
+
query = """
|
|
567
|
+
query Bad($searchInput: SearchInput!) {
|
|
568
|
+
search(searchInput: $searchInput) {
|
|
569
|
+
... on LegalEntity {
|
|
570
|
+
registeredEntities {
|
|
571
|
+
edges {
|
|
572
|
+
node {
|
|
573
|
+
registrations(first: 5) {
|
|
574
|
+
edges {
|
|
575
|
+
node {
|
|
576
|
+
roles(first: 20, conditions: { filter: { EQ: ["jobTitle", "CEO"] } }) {
|
|
577
|
+
edges { node { jobTitle } }
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
"""
|
|
589
|
+
assert_triggers(query, self.RULE, severity=Severity.WARNING)
|
|
590
|
+
|
|
591
|
+
def test_good_no_conditions_on_roles(self):
|
|
592
|
+
query = """
|
|
593
|
+
query Good($searchInput: SearchInput!) {
|
|
594
|
+
search(searchInput: $searchInput) {
|
|
595
|
+
... on LegalEntity {
|
|
596
|
+
registeredEntities {
|
|
597
|
+
edges {
|
|
598
|
+
node {
|
|
599
|
+
registrations(first: 5) {
|
|
600
|
+
edges {
|
|
601
|
+
node {
|
|
602
|
+
roles(first: 20) {
|
|
603
|
+
edges { node { jobTitle } }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
"""
|
|
615
|
+
assert_clean(query, self.RULE)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# ---------------------------------------------------------------------------
|
|
619
|
+
# ENIGMA-013: Address-only search
|
|
620
|
+
# ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
class TestENIGMA013AddressOnlySearch:
|
|
623
|
+
RULE = "ENIGMA-013"
|
|
624
|
+
|
|
625
|
+
def test_bad_address_only(self):
|
|
626
|
+
query = """
|
|
627
|
+
query Bad {
|
|
628
|
+
search(searchInput: { entityType: OPERATING_LOCATION, address: { city: "NEW YORK", state: "NY" }, conditions: { limit: 10 } }) {
|
|
629
|
+
... on OperatingLocation { id }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
"""
|
|
633
|
+
assert_triggers(query, self.RULE, severity=Severity.WARNING)
|
|
634
|
+
|
|
635
|
+
def test_good_name_plus_address(self):
|
|
636
|
+
query = """
|
|
637
|
+
query Good {
|
|
638
|
+
search(searchInput: { name: "Roto-Rooter", entityType: OPERATING_LOCATION, address: { street1: "123 Main", city: "Tulsa", state: "OK" }, conditions: { limit: 10 } }) {
|
|
639
|
+
... on OperatingLocation { id }
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
"""
|
|
643
|
+
assert_clean(query, self.RULE)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# ---------------------------------------------------------------------------
|
|
647
|
+
# ENIGMA-014: rawQuantity usage
|
|
648
|
+
# ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
class TestENIGMA014RawQuantity:
|
|
651
|
+
RULE = "ENIGMA-014"
|
|
652
|
+
|
|
653
|
+
def test_bad_raw_quantity(self):
|
|
654
|
+
query = """
|
|
655
|
+
query Bad {
|
|
656
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 1 } }) {
|
|
657
|
+
... on Brand {
|
|
658
|
+
cardTransactions(first: 1, conditions: $cond) {
|
|
659
|
+
edges { node { rawQuantity } }
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
"""
|
|
665
|
+
assert_triggers(query, self.RULE, severity=Severity.WARNING)
|
|
666
|
+
|
|
667
|
+
def test_good_projected_quantity(self):
|
|
668
|
+
query = """
|
|
669
|
+
query Good {
|
|
670
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 1 } }) {
|
|
671
|
+
... on Brand {
|
|
672
|
+
cardTransactions(first: 1, conditions: $cond) {
|
|
673
|
+
edges { node { projectedQuantity } }
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
"""
|
|
679
|
+
assert_clean(query, self.RULE)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
# ---------------------------------------------------------------------------
|
|
683
|
+
# ENIGMA-015: postalCode in return fields
|
|
684
|
+
# ---------------------------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
class TestENIGMA015PostalCodeInReturn:
|
|
687
|
+
RULE = "ENIGMA-015"
|
|
688
|
+
|
|
689
|
+
def test_bad_postal_code_in_node(self):
|
|
690
|
+
query = """
|
|
691
|
+
query Bad {
|
|
692
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 1 } }) {
|
|
693
|
+
... on Brand {
|
|
694
|
+
operatingLocations(first: 5) {
|
|
695
|
+
edges {
|
|
696
|
+
node {
|
|
697
|
+
addresses(first: 1) {
|
|
698
|
+
edges { node { fullAddress postalCode } }
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
"""
|
|
707
|
+
assert_triggers(query, self.RULE, severity=Severity.WARNING)
|
|
708
|
+
|
|
709
|
+
def test_good_zip_in_node(self):
|
|
710
|
+
query = """
|
|
711
|
+
query Good {
|
|
712
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 1 } }) {
|
|
713
|
+
... on Brand {
|
|
714
|
+
operatingLocations(first: 5) {
|
|
715
|
+
edges {
|
|
716
|
+
node {
|
|
717
|
+
addresses(first: 1) {
|
|
718
|
+
edges { node { fullAddress zip } }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
"""
|
|
727
|
+
assert_clean(query, self.RULE)
|
|
728
|
+
|
|
729
|
+
def test_postal_code_in_search_input_is_ok(self):
|
|
730
|
+
"""postalCode is correct in AddressInput (searchInput)."""
|
|
731
|
+
query = """
|
|
732
|
+
query OK {
|
|
733
|
+
search(searchInput: { name: "Acme", entityType: OPERATING_LOCATION, address: { postalCode: "10001" }, conditions: { limit: 10 } }) {
|
|
734
|
+
... on OperatingLocation {
|
|
735
|
+
addresses(first: 1) {
|
|
736
|
+
edges { node { fullAddress zip } }
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
"""
|
|
742
|
+
assert_clean(query, self.RULE)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
# ---------------------------------------------------------------------------
|
|
746
|
+
# ENIGMA-016: Address ranking on person search
|
|
747
|
+
# ---------------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
class TestENIGMA016AddressRanking:
|
|
750
|
+
RULE = "ENIGMA-016"
|
|
751
|
+
|
|
752
|
+
def test_info_address_on_person(self):
|
|
753
|
+
query = """
|
|
754
|
+
query Person {
|
|
755
|
+
search(searchInput: { entityType: PERSON, person: { firstName: "John", lastName: "Doe" }, address: { state: "NY" }, conditions: { limit: 10 } }) {
|
|
756
|
+
... on Person { id }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
"""
|
|
760
|
+
assert_triggers(query, self.RULE, severity=Severity.INFO)
|
|
761
|
+
|
|
762
|
+
def test_clean_address_on_brand(self):
|
|
763
|
+
query = """
|
|
764
|
+
query Brand {
|
|
765
|
+
search(searchInput: { name: "Acme", entityType: BRAND, address: { state: "NY" }, conditions: { limit: 10 } }) {
|
|
766
|
+
... on Brand { id }
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
"""
|
|
770
|
+
assert_clean(query, self.RULE)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ---------------------------------------------------------------------------
|
|
774
|
+
# ENIGMA-017: LegalEntity → Brand traversal trust
|
|
775
|
+
# ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
class TestENIGMA017LEBrandTrust:
|
|
778
|
+
RULE = "ENIGMA-017"
|
|
779
|
+
|
|
780
|
+
def test_info_le_to_brands(self):
|
|
781
|
+
query = """
|
|
782
|
+
query LEBrands($searchInput: SearchInput!) {
|
|
783
|
+
search(searchInput: $searchInput) {
|
|
784
|
+
... on LegalEntity {
|
|
785
|
+
id
|
|
786
|
+
brands(first: 5) {
|
|
787
|
+
edges { node { id } }
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
"""
|
|
793
|
+
assert_triggers(query, self.RULE, severity=Severity.INFO)
|
|
794
|
+
|
|
795
|
+
def test_clean_no_brands_on_le(self):
|
|
796
|
+
query = """
|
|
797
|
+
query LEOnly($searchInput: SearchInput!) {
|
|
798
|
+
search(searchInput: $searchInput) {
|
|
799
|
+
... on LegalEntity {
|
|
800
|
+
id
|
|
801
|
+
names { edges { node { name } } }
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
"""
|
|
806
|
+
assert_clean(query, self.RULE)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
# ---------------------------------------------------------------------------
|
|
810
|
+
# ENIGMA-018: Missing operation name
|
|
811
|
+
# ---------------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
class TestENIGMA018MissingOperationName:
|
|
814
|
+
RULE = "ENIGMA-018"
|
|
815
|
+
|
|
816
|
+
def test_bad_anonymous_query(self):
|
|
817
|
+
query = """
|
|
818
|
+
{
|
|
819
|
+
search(searchInput: { name: "Acme", entityType: BRAND, conditions: { limit: 1 } }) {
|
|
820
|
+
... on Brand { id }
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
"""
|
|
824
|
+
assert_triggers(query, self.RULE, severity=Severity.INFO)
|
|
825
|
+
|
|
826
|
+
def test_bad_query_without_name(self):
|
|
827
|
+
query = """
|
|
828
|
+
query($searchInput: SearchInput!) {
|
|
829
|
+
search(searchInput: $searchInput) {
|
|
830
|
+
... on Brand { id }
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
"""
|
|
834
|
+
assert_triggers(query, self.RULE, severity=Severity.INFO)
|
|
835
|
+
|
|
836
|
+
def test_good_named_query(self):
|
|
837
|
+
query = """
|
|
838
|
+
query BrandSearch($searchInput: SearchInput!) {
|
|
839
|
+
search(searchInput: $searchInput) {
|
|
840
|
+
... on Brand { id }
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
"""
|
|
844
|
+
assert_clean(query, self.RULE)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
# ---------------------------------------------------------------------------
|
|
848
|
+
# Parse errors
|
|
849
|
+
# ---------------------------------------------------------------------------
|
|
850
|
+
|
|
851
|
+
class TestParseErrors:
|
|
852
|
+
def test_empty_query(self):
|
|
853
|
+
result = lint_query("")
|
|
854
|
+
assert not result.query_valid
|
|
855
|
+
assert any(f.rule_id == "PARSE" for f in result.findings)
|
|
856
|
+
|
|
857
|
+
def test_unbalanced_braces(self):
|
|
858
|
+
result = lint_query("query Bad { search(")
|
|
859
|
+
assert not result.query_valid
|
|
860
|
+
|
|
861
|
+
def test_no_braces(self):
|
|
862
|
+
result = lint_query("just some text")
|
|
863
|
+
assert not result.query_valid
|