Skip to content

Commit cbfdd9d

Browse files
committed
feat: Add comprehensive test suite for SDK-e2e-stack-v4
Add 17 new test suites with 323+ tests for deep references, JSON RTE, modular blocks, pagination, error handling, and performance testing. Includes base infrastructure (TestHelpers, PerformanceAssertion, ComplexQueryBuilder) and region-agnostic assertions via config.py
1 parent 0dda650 commit cbfdd9d

24 files changed

+9473
-152
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,7 @@ venv.bak/
118118
.mypy_cache/
119119
.idea/
120120
.vscode/
121+
122+
pipeline.yaml
123+
docs/
124+
test-results

tests/base_integration_test.py

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
"""
2+
Base Integration Test - Foundation for all comprehensive integration tests
3+
Provides common setup, utilities, and patterns
4+
"""
5+
6+
import unittest
7+
import logging
8+
import os
9+
import sys
10+
11+
# Add parent directory to path
12+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
13+
14+
import contentstack
15+
import config
16+
from tests.utils.test_helpers import TestHelpers
17+
from tests.utils.performance_assertions import PerformanceAssertion
18+
from tests.utils.complex_query_builder import ComplexQueryBuilder, PresetQueryBuilder
19+
20+
21+
class BaseIntegrationTest(unittest.TestCase):
22+
"""
23+
Base class for all integration tests
24+
25+
Provides:
26+
- Common SDK setup
27+
- Test data access
28+
- Helper utilities
29+
- Performance measurement
30+
- Logging configuration
31+
32+
Usage:
33+
class MyIntegrationTest(BaseIntegrationTest):
34+
def test_something(self):
35+
entry = self.stack.content_type(config.SIMPLE_CONTENT_TYPE_UID).entry(config.SIMPLE_ENTRY_UID)
36+
result = TestHelpers.safe_api_call("fetch", entry.fetch)
37+
self.assert_has_results(result)
38+
"""
39+
40+
@classmethod
41+
def setUpClass(cls):
42+
"""
43+
Setup once for all tests in the class
44+
Configure SDK, load test data, setup logging
45+
"""
46+
# Setup logging
47+
TestHelpers.setup_test_logging(level=logging.INFO)
48+
cls.logger = logging.getLogger(cls.__name__)
49+
50+
cls.logger.info("="*80)
51+
cls.logger.info(f"Setting up test class: {cls.__name__}")
52+
cls.logger.info("="*80)
53+
54+
# Initialize SDK
55+
cls.stack = contentstack.Stack(
56+
api_key=config.API_KEY,
57+
delivery_token=config.DELIVERY_TOKEN,
58+
environment=config.ENVIRONMENT,
59+
host=config.HOST
60+
)
61+
62+
cls.logger.info("✅ SDK initialized")
63+
64+
# Store config for easy access
65+
cls.config = config
66+
67+
# Log test data availability
68+
cls.log_test_data_availability()
69+
70+
@classmethod
71+
def tearDownClass(cls):
72+
"""Cleanup after all tests"""
73+
cls.logger.info("="*80)
74+
cls.logger.info(f"Tearing down test class: {cls.__name__}")
75+
cls.logger.info("="*80)
76+
77+
def setUp(self):
78+
"""Setup before each test"""
79+
self.logger.info(f"\n{'='*80}")
80+
self.logger.info(f"Running test: {self._testMethodName}")
81+
self.logger.info(f"{'='*80}")
82+
83+
def tearDown(self):
84+
"""Cleanup after each test"""
85+
test_result = "✅ PASSED" if sys.exc_info() == (None, None, None) else "❌ FAILED"
86+
self.logger.info(f"{test_result}: {self._testMethodName}\n")
87+
88+
# === TEST DATA HELPERS ===
89+
90+
@classmethod
91+
def log_test_data_availability(cls):
92+
"""Log available test data for debugging"""
93+
cls.logger.info("\n📊 Test Data Configuration:")
94+
cls.logger.info(f" Stack: {config.HOST}")
95+
cls.logger.info(f" API Key: {config.API_KEY}")
96+
cls.logger.info(f" Environment: {config.ENVIRONMENT}")
97+
cls.logger.info("")
98+
cls.logger.info(" Test Entries:")
99+
cls.logger.info(f" - SIMPLE: {config.SIMPLE_CONTENT_TYPE_UID}/{config.SIMPLE_ENTRY_UID}")
100+
cls.logger.info(f" - MEDIUM: {config.MEDIUM_CONTENT_TYPE_UID}/{config.MEDIUM_ENTRY_UID}")
101+
cls.logger.info(f" - COMPLEX: {config.COMPLEX_CONTENT_TYPE_UID}/{config.COMPLEX_ENTRY_UID}")
102+
cls.logger.info(f" - SELF-REF: {config.SELF_REF_CONTENT_TYPE_UID}/{config.SELF_REF_ENTRY_UID}")
103+
cls.logger.info("")
104+
105+
# === ASSERTION HELPERS ===
106+
107+
def assert_has_results(self, response):
108+
"""
109+
Assert response has results
110+
If no results, logs warning but doesn't fail (graceful degradation)
111+
112+
Args:
113+
response: API response
114+
115+
Returns:
116+
bool: True if has results, False otherwise
117+
"""
118+
has_data = TestHelpers.has_results(response)
119+
120+
if not has_data:
121+
self.logger.warning("⚠️ No results found - test data dependent")
122+
return False
123+
124+
return True
125+
126+
def assert_entry_structure(self, entry, required_fields):
127+
"""
128+
Assert entry has required fields
129+
130+
Args:
131+
entry: Entry dictionary
132+
required_fields: List of required field names
133+
"""
134+
valid, missing = TestHelpers.validate_entry_structure(entry, required_fields)
135+
136+
if not valid:
137+
self.logger.warning(f"⚠️ Missing fields: {missing}")
138+
139+
self.assertTrue(valid, f"Entry missing required fields: {missing}")
140+
141+
def assert_has_reference(self, entry, reference_field):
142+
"""
143+
Assert entry has a reference field populated
144+
145+
Args:
146+
entry: Entry dictionary
147+
reference_field: Reference field name
148+
"""
149+
has_ref = TestHelpers.has_reference(entry, reference_field)
150+
151+
if not has_ref:
152+
self.logger.warning(f"⚠️ Reference field '{reference_field}' not found or empty")
153+
154+
self.assertTrue(has_ref, f"Entry missing reference field: {reference_field}")
155+
156+
# === QUERY BUILDERS ===
157+
158+
def create_simple_query(self):
159+
"""Create query for simple content type"""
160+
return self.stack.content_type(config.SIMPLE_CONTENT_TYPE_UID).query()
161+
162+
def create_medium_query(self):
163+
"""Create query for medium content type"""
164+
return self.stack.content_type(config.MEDIUM_CONTENT_TYPE_UID).query()
165+
166+
def create_complex_query(self):
167+
"""Create query for complex content type"""
168+
return self.stack.content_type(config.COMPLEX_CONTENT_TYPE_UID).query()
169+
170+
def create_complex_query_builder(self, content_type_uid=None):
171+
"""
172+
Create complex query builder
173+
174+
Args:
175+
content_type_uid: Optional specific content type (defaults to SIMPLE)
176+
177+
Returns:
178+
ComplexQueryBuilder instance
179+
"""
180+
ct_uid = content_type_uid or config.SIMPLE_CONTENT_TYPE_UID
181+
query = self.stack.content_type(ct_uid).query()
182+
return ComplexQueryBuilder(query)
183+
184+
# === ENTRY FETCHING ===
185+
186+
def fetch_simple_entry(self, entry_uid=None):
187+
"""
188+
Fetch simple entry (with graceful error handling)
189+
190+
Args:
191+
entry_uid: Optional specific UID (defaults to config SIMPLE_ENTRY_UID)
192+
193+
Returns:
194+
Entry data or None
195+
"""
196+
uid = entry_uid or config.SIMPLE_ENTRY_UID
197+
entry = self.stack.content_type(config.SIMPLE_CONTENT_TYPE_UID).entry(uid)
198+
199+
return TestHelpers.safe_api_call("fetch_simple_entry", entry.fetch)
200+
201+
def fetch_medium_entry(self, entry_uid=None):
202+
"""Fetch medium entry"""
203+
uid = entry_uid or config.MEDIUM_ENTRY_UID
204+
entry = self.stack.content_type(config.MEDIUM_CONTENT_TYPE_UID).entry(uid)
205+
206+
return TestHelpers.safe_api_call("fetch_medium_entry", entry.fetch)
207+
208+
def fetch_complex_entry(self, entry_uid=None):
209+
"""Fetch complex entry"""
210+
uid = entry_uid or config.COMPLEX_ENTRY_UID
211+
entry = self.stack.content_type(config.COMPLEX_CONTENT_TYPE_UID).entry(uid)
212+
213+
return TestHelpers.safe_api_call("fetch_complex_entry", entry.fetch)
214+
215+
# === PERFORMANCE TESTING ===
216+
217+
def measure_query_performance(self, query_func, operation_name):
218+
"""
219+
Measure query performance
220+
221+
Args:
222+
query_func: Function that executes the query
223+
operation_name: Name for logging
224+
225+
Returns:
226+
Tuple of (result, elapsed_time_ms)
227+
"""
228+
return PerformanceAssertion.measure_operation(query_func, operation_name)
229+
230+
def compare_query_performance(self, queries):
231+
"""
232+
Compare performance of multiple queries
233+
234+
Args:
235+
queries: Dictionary of {name: query_function}
236+
237+
Returns:
238+
Dictionary of results with timings
239+
"""
240+
return PerformanceAssertion.measure_batch_operations(queries)
241+
242+
# === LOGGING HELPERS ===
243+
244+
def log_test_info(self, message):
245+
"""Log informational message"""
246+
self.logger.info(f"ℹ️ {message}")
247+
248+
def log_test_warning(self, message):
249+
"""Log warning message"""
250+
self.logger.warning(f"⚠️ {message}")
251+
252+
def log_test_error(self, message):
253+
"""Log error message"""
254+
self.logger.error(f"❌ {message}")
255+
256+
# === SKIP HELPERS ===
257+
258+
def skip_if_no_data(self, response, message="No test data available"):
259+
"""
260+
Skip test if response has no data
261+
262+
Args:
263+
response: API response
264+
message: Skip message
265+
"""
266+
if not TestHelpers.has_results(response):
267+
self.skipTest(message)
268+
269+
def skip_if_api_unavailable(self, result, feature_name="Feature"):
270+
"""
271+
Skip test if API feature unavailable
272+
273+
Args:
274+
result: API result (None if unavailable)
275+
feature_name: Name of feature for message
276+
"""
277+
if result is None:
278+
self.skipTest(f"{feature_name} API not available in this environment")
279+
280+
# === DATA VALIDATION HELPERS ===
281+
282+
def validate_response_structure(self, response, expected_keys):
283+
"""
284+
Validate response has expected structure
285+
286+
Args:
287+
response: API response
288+
expected_keys: List of expected keys
289+
"""
290+
for key in expected_keys:
291+
self.assertIn(key, response, f"Response missing key: {key}")
292+
293+
def validate_entry_metadata(self, entry):
294+
"""
295+
Validate entry has standard metadata
296+
297+
Args:
298+
entry: Entry dictionary
299+
"""
300+
metadata_fields = ['uid', '_version', 'locale']
301+
302+
for field in metadata_fields:
303+
if field not in entry:
304+
self.logger.warning(f"⚠️ Entry missing metadata field: {field}")
305+
306+
# === REFERENCE TESTING HELPERS ===
307+
308+
def fetch_entry_with_references(self, content_type_uid, entry_uid, reference_fields):
309+
"""
310+
Fetch entry with specified references
311+
312+
Args:
313+
content_type_uid: Content type UID
314+
entry_uid: Entry UID
315+
reference_fields: List of reference field paths
316+
317+
Returns:
318+
Entry data or None
319+
"""
320+
entry = self.stack.content_type(content_type_uid).entry(entry_uid)
321+
322+
# Add references
323+
for ref_field in reference_fields:
324+
entry.include_reference(ref_field)
325+
326+
return TestHelpers.safe_api_call("fetch_with_references", entry.fetch)
327+
328+
def validate_reference_depth(self, entry, reference_field, expected_depth):
329+
"""
330+
Validate reference depth
331+
332+
Args:
333+
entry: Entry dictionary
334+
reference_field: Reference field name
335+
expected_depth: Expected depth
336+
"""
337+
actual_depth = TestHelpers.count_references(entry, reference_field)
338+
339+
self.logger.info(f"Reference depth for '{reference_field}': {actual_depth}")
340+
341+
self.assertEqual(
342+
actual_depth,
343+
expected_depth,
344+
f"Reference depth mismatch: expected {expected_depth}, got {actual_depth}"
345+
)
346+
347+
348+
# === SAMPLE USAGE ===
349+
350+
if __name__ == '__main__':
351+
"""
352+
Example of using BaseIntegrationTest
353+
"""
354+
355+
class SampleTest(BaseIntegrationTest):
356+
"""Sample test to demonstrate usage"""
357+
358+
def test_simple_fetch(self):
359+
"""Test fetching simple entry"""
360+
result = self.fetch_simple_entry()
361+
362+
if not self.assert_has_results(result):
363+
return # No data, skip gracefully
364+
365+
entry = result['entry']
366+
self.assertIn('uid', entry)
367+
self.assertIn('title', entry)
368+
369+
self.log_test_info(f"Fetched entry: {entry.get('title')}")
370+
371+
def test_complex_query(self):
372+
"""Test complex query building"""
373+
builder = self.create_complex_query_builder(config.COMPLEX_CONTENT_TYPE_UID)
374+
375+
result = builder.include_count().limit(5).find()
376+
377+
if not self.assert_has_results(result):
378+
return
379+
380+
self.log_test_info(f"Found {len(result['entries'])} entries")
381+
382+
# Run sample tests
383+
unittest.main()
384+

0 commit comments

Comments
 (0)