pytest-openapi 0.1.1.dev202601050151__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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1.dev202601050151"
@@ -0,0 +1,433 @@
1
+ """Generate test cases from OpenAPI schemas."""
2
+
3
+
4
+ def generate_string_test_cases(schema):
5
+ """Generate string test cases from schema.
6
+
7
+ Args:
8
+ schema: OpenAPI schema for a string field
9
+
10
+ Returns:
11
+ list: List of test values
12
+ """
13
+ test_cases = []
14
+
15
+ # Check for pattern (regex)
16
+ if "pattern" in schema:
17
+ try:
18
+ import exrex
19
+
20
+ # Generate a few test cases from the regex
21
+ test_cases.extend(list(exrex.generate(schema["pattern"]))[:3])
22
+ except ImportError:
23
+ print(
24
+ "⚠️ Warning: 'exrex' package not installed. Cannot generate test cases from regex patterns."
25
+ )
26
+ print(" Install with: pip install exrex")
27
+ test_cases.append("test-string")
28
+ except Exception as e:
29
+ print(
30
+ f"⚠️ Warning: Could not generate from pattern '{schema['pattern']}': {e}"
31
+ )
32
+ test_cases.append("test-string")
33
+
34
+ # Check for enum
35
+ elif "enum" in schema:
36
+ test_cases.extend(schema["enum"])
37
+
38
+ # Check for format
39
+ elif "format" in schema:
40
+ format_type = schema["format"]
41
+ if format_type == "email":
42
+ test_cases.extend(
43
+ [
44
+ "test@example.com",
45
+ "user+tag@subdomain.example.co.uk",
46
+ "test.user@example.com",
47
+ ]
48
+ )
49
+ elif format_type in ["ipv4", "ip"]:
50
+ test_cases.extend(["192.168.1.1", "10.0.0.1", "127.0.0.1"])
51
+ elif format_type == "ipv6":
52
+ test_cases.extend(
53
+ ["2001:0db8:85a3:0000:0000:8a2e:0370:7334", "::1", "fe80::1"]
54
+ )
55
+ elif format_type in ["hostname", "idn-hostname"]:
56
+ test_cases.extend(
57
+ ["example.com", "subdomain.example.com", "test-server.local"]
58
+ )
59
+ elif format_type in ["uri", "url"]:
60
+ test_cases.extend(
61
+ [
62
+ "https://example.com/path",
63
+ "http://localhost:8080/api/v1/resource",
64
+ ]
65
+ )
66
+ elif format_type == "date":
67
+ test_cases.extend(["2025-12-23", "2024-01-01"])
68
+ elif format_type == "date-time":
69
+ test_cases.extend(
70
+ ["2025-12-23T10:30:00Z", "2024-01-01T00:00:00+00:00"]
71
+ )
72
+ elif format_type == "time":
73
+ test_cases.extend(["10:30:00", "23:59:59"])
74
+ elif format_type == "uuid":
75
+ test_cases.extend(
76
+ [
77
+ "550e8400-e29b-41d4-a716-446655440000",
78
+ "123e4567-e89b-12d3-a456-426614174000",
79
+ ]
80
+ )
81
+ else:
82
+ # Unknown format, use basic string
83
+ test_cases.append(f"test-{format_type}")
84
+
85
+ # No specific constraints, generate edge cases
86
+ else:
87
+ test_cases.extend(
88
+ [
89
+ "Lorem ipsum dolor sit amet", # Normal text
90
+ "Test with 'single' quotes", # Single quotes
91
+ 'Test with "double" quotes', # Double quotes
92
+ "Test:with:colons", # Colons
93
+ "Test\\with\\backslashes", # Backslashes
94
+ "Test\nwith\nnewlines", # Newlines
95
+ "Test\r\nwith\r\nCRLF", # Carriage returns
96
+ "Test with UTF-8: café, naïve, 中文, 日本語", # UTF-8
97
+ "Test!@#$%^&*()_+-=[]{}|;:<>?,./`~", # Special characters
98
+ "", # Empty string (if allowed)
99
+ ]
100
+ )
101
+
102
+ # Check length constraints
103
+ min_length = schema.get("minLength", 0)
104
+ max_length = schema.get("maxLength")
105
+
106
+ # Filter test cases by length
107
+ filtered = []
108
+ for ex in test_cases:
109
+ if len(ex) >= min_length:
110
+ if max_length is None or len(ex) <= max_length:
111
+ filtered.append(ex)
112
+
113
+ # If we have length constraints but no valid test cases, generate one
114
+ if not filtered and (min_length > 0 or max_length is not None):
115
+ if max_length:
116
+ target_length = min(min_length + 5, max_length)
117
+ else:
118
+ target_length = min_length + 5
119
+ filtered.append("a" * target_length)
120
+
121
+ return filtered if filtered else ["test-string"]
122
+
123
+
124
+ def generate_integer_test_cases(schema, field_name="field"):
125
+ """Generate integer test cases from schema.
126
+
127
+ Args:
128
+ schema: OpenAPI schema for an integer field
129
+ field_name: Name of the field (for error messages)
130
+
131
+ Returns:
132
+ tuple: (list of test values, optional warning message)
133
+ """
134
+ test_cases = []
135
+ warning = None
136
+
137
+ # Check for enum
138
+ if "enum" in schema:
139
+ return schema["enum"], None
140
+
141
+ # Get constraints
142
+ minimum = schema.get("minimum")
143
+ maximum = schema.get("maximum")
144
+ exclusive_min = schema.get("exclusiveMinimum")
145
+ exclusive_max = schema.get("exclusiveMaximum")
146
+ multiple_of = schema.get("multipleOf", 1)
147
+ format_type = schema.get("format")
148
+
149
+ # Determine actual bounds
150
+ if exclusive_min is not None:
151
+ min_val = exclusive_min + multiple_of
152
+ elif minimum is not None:
153
+ min_val = minimum
154
+ else:
155
+ # No minimum specified
156
+ if format_type == "int32":
157
+ min_val = -2147483648
158
+ elif format_type == "int64":
159
+ min_val = -9223372036854775808
160
+ else:
161
+ min_val = -1000000
162
+ warning = f"⚠️ Field '{field_name}': No minimum specified. Testing with very large negative numbers. Add 'minimum' to schema to restrict."
163
+
164
+ if exclusive_max is not None:
165
+ max_val = exclusive_max - multiple_of
166
+ elif maximum is not None:
167
+ max_val = maximum
168
+ else:
169
+ # No maximum specified
170
+ if format_type == "int32":
171
+ max_val = 2147483647
172
+ elif format_type == "int64":
173
+ max_val = 9223372036854775807
174
+ else:
175
+ max_val = 1000000
176
+ if not warning:
177
+ warning = f"⚠️ Field '{field_name}': No maximum specified. Testing with very large positive numbers. Add 'maximum' to schema to restrict."
178
+
179
+ # Generate examples respecting multipleOf
180
+ def round_to_multiple(val):
181
+ """Round value to nearest multiple."""
182
+ return int(round(val / multiple_of) * multiple_of)
183
+
184
+ # Add boundary values
185
+ test_cases.append(round_to_multiple(min_val))
186
+ test_cases.append(round_to_multiple(max_val))
187
+
188
+ # Add middle value
189
+ mid_val = round_to_multiple((min_val + max_val) / 2)
190
+ if mid_val not in test_cases:
191
+ test_cases.append(mid_val)
192
+
193
+ # Add zero if in range and valid
194
+ if min_val <= 0 <= max_val:
195
+ zero_val = round_to_multiple(0)
196
+ if zero_val not in test_cases:
197
+ test_cases.append(zero_val)
198
+
199
+ # Add a negative value if allowed and not already present
200
+ if min_val < 0 and max_val > 0:
201
+ neg_val = (
202
+ round_to_multiple(min_val / 2)
203
+ if min_val > -1000
204
+ else round_to_multiple(-100)
205
+ )
206
+ if neg_val not in test_cases and min_val <= neg_val <= max_val:
207
+ test_cases.append(neg_val)
208
+
209
+ # Ensure all test cases respect multipleOf
210
+ test_cases = [int(ex) for ex in test_cases if ex % multiple_of == 0]
211
+
212
+ return sorted(set(test_cases)), warning
213
+
214
+
215
+ def generate_number_test_cases(schema, field_name="field"):
216
+ """Generate number (float) test cases from schema.
217
+
218
+ Args:
219
+ schema: OpenAPI schema for a number field
220
+ field_name: Name of the field (for error messages)
221
+
222
+ Returns:
223
+ tuple: (list of test values, optional warning message)
224
+ """
225
+ test_cases = []
226
+ warning = None
227
+
228
+ # Check for enum
229
+ if "enum" in schema:
230
+ return schema["enum"], None
231
+
232
+ # Get constraints
233
+ minimum = schema.get("minimum")
234
+ maximum = schema.get("maximum")
235
+ exclusive_min = schema.get("exclusiveMinimum")
236
+ exclusive_max = schema.get("exclusiveMaximum")
237
+ multiple_of = schema.get("multipleOf")
238
+
239
+ # Determine actual bounds
240
+ if exclusive_min is not None:
241
+ min_val = exclusive_min + 0.01
242
+ elif minimum is not None:
243
+ min_val = minimum
244
+ else:
245
+ min_val = -1000000.0
246
+ warning = f"⚠️ Field '{field_name}': No minimum specified. Testing with very large negative numbers. Add 'minimum' to schema to restrict."
247
+
248
+ if exclusive_max is not None:
249
+ max_val = exclusive_max - 0.01
250
+ elif maximum is not None:
251
+ max_val = maximum
252
+ else:
253
+ max_val = 1000000.0
254
+ if not warning:
255
+ warning = f"⚠️ Field '{field_name}': No maximum specified. Testing with very large positive numbers. Add 'maximum' to schema to restrict."
256
+
257
+ # Add boundary values
258
+ test_cases.append(float(min_val))
259
+ test_cases.append(float(max_val))
260
+
261
+ # Add middle value
262
+ mid_val = (min_val + max_val) / 2
263
+ test_cases.append(mid_val)
264
+
265
+ # Add zero if in range
266
+ if min_val <= 0 <= max_val:
267
+ if multiple_of:
268
+ zero_val = 0.0 if 0.0 % multiple_of == 0 else multiple_of
269
+ test_cases.append(zero_val)
270
+ else:
271
+ test_cases.append(0.0)
272
+
273
+ # Add high-precision numbers
274
+ if min_val < 1 < max_val:
275
+ test_cases.extend([0.123456789, 0.999999999, 1.111111111])
276
+
277
+ # Add negative if allowed
278
+ if min_val < 0 < max_val:
279
+ test_cases.append(-0.123456789)
280
+
281
+ # Apply multipleOf constraint if specified
282
+ if multiple_of:
283
+ filtered = []
284
+ for ex in test_cases:
285
+ # Check if it's a valid multiple
286
+ if abs((ex / multiple_of) - round(ex / multiple_of)) < 0.0001:
287
+ filtered.append(ex)
288
+ if filtered:
289
+ test_cases = filtered
290
+ else:
291
+ # Generate some valid multiples
292
+ test_cases = [
293
+ multiple_of * i
294
+ for i in range(
295
+ int(min_val / multiple_of), int(max_val / multiple_of) + 1
296
+ )
297
+ if min_val <= multiple_of * i <= max_val
298
+ ][:5]
299
+
300
+ # Filter to ensure within bounds
301
+ test_cases = [ex for ex in test_cases if min_val <= ex <= max_val]
302
+
303
+ return sorted(set(test_cases)), warning
304
+
305
+
306
+ def generate_boolean_test_cases(schema):
307
+ """Generate boolean test cases.
308
+
309
+ Args:
310
+ schema: OpenAPI schema for a boolean field
311
+
312
+ Returns:
313
+ list: [True, False]
314
+ """
315
+ return [True, False]
316
+
317
+
318
+ def generate_array_test_cases(schema, field_name="field"):
319
+ """Generate array test cases from schema.
320
+
321
+ Args:
322
+ schema: OpenAPI schema for an array field
323
+ field_name: Name of the field (for error messages)
324
+
325
+ Returns:
326
+ tuple: (list of test arrays, optional warning message)
327
+ """
328
+ items_schema = schema.get("items", {})
329
+ min_items = schema.get("minItems", 0)
330
+ max_items = schema.get("maxItems", 3)
331
+
332
+ # Generate test cases for the item type
333
+ item_test_cases, warning = generate_test_cases_for_schema(
334
+ items_schema, f"{field_name}[]"
335
+ )
336
+
337
+ arrays = []
338
+
339
+ # Empty array if allowed
340
+ if min_items == 0:
341
+ arrays.append([])
342
+
343
+ # Single item array if allowed
344
+ if min_items <= 1 <= max_items and item_test_cases:
345
+ arrays.append([item_test_cases[0]])
346
+
347
+ # Min items array
348
+ if min_items > 0 and item_test_cases:
349
+ arrays.append(item_test_cases[:min_items])
350
+
351
+ # Max items array
352
+ if item_test_cases:
353
+ arrays.append(item_test_cases[:max_items])
354
+
355
+ return arrays, warning
356
+
357
+
358
+ def generate_object_test_cases(schema, field_name="field"):
359
+ """Generate object test cases from schema.
360
+
361
+ Args:
362
+ schema: OpenAPI schema for an object field
363
+ field_name: Name of the field (for error messages)
364
+
365
+ Returns:
366
+ tuple: (list of test objects, list of warnings)
367
+ """
368
+ properties = schema.get("properties", {})
369
+
370
+ warnings = []
371
+
372
+ # Collect test cases for each property
373
+ prop_values = {}
374
+ for prop_name, prop_schema in properties.items():
375
+ prop_test_cases, warning = generate_test_cases_for_schema(
376
+ prop_schema, f"{field_name}.{prop_name}"
377
+ )
378
+ if warning:
379
+ warnings.append(warning)
380
+ # Ensure we have at least one value
381
+ prop_values[prop_name] = prop_test_cases or [None]
382
+
383
+ # Create a bounded Cartesian product of property values to produce multiple objects
384
+ # Limit the total generated objects to avoid explosion
385
+ from itertools import product
386
+
387
+ MAX_COMBINATIONS = 10
388
+ keys = list(prop_values.keys())
389
+ # Build iterables in a deterministic order
390
+ iterables = [prop_values[k] for k in keys]
391
+
392
+ combos = []
393
+ for combo in product(*iterables):
394
+ obj = {}
395
+ for k, v in zip(keys, combo):
396
+ obj[k] = v
397
+ combos.append(obj)
398
+ if len(combos) >= MAX_COMBINATIONS:
399
+ break
400
+
401
+ # If no combos generated, fall back to one empty object
402
+ if not combos:
403
+ combos = [{}]
404
+
405
+ return combos, warnings
406
+
407
+
408
+ def generate_test_cases_for_schema(schema, field_name="field"):
409
+ """Generate test cases for any schema type.
410
+
411
+ Args:
412
+ schema: OpenAPI schema
413
+ field_name: Name of the field (for error messages)
414
+
415
+ Returns:
416
+ tuple: (list of test values, optional warning message or list of warnings)
417
+ """
418
+ schema_type = schema.get("type", "string")
419
+
420
+ if schema_type == "string":
421
+ return generate_string_test_cases(schema), None
422
+ elif schema_type == "integer":
423
+ return generate_integer_test_cases(schema, field_name)
424
+ elif schema_type == "number":
425
+ return generate_number_test_cases(schema, field_name)
426
+ elif schema_type == "boolean":
427
+ return generate_boolean_test_cases(schema), None
428
+ elif schema_type == "array":
429
+ return generate_array_test_cases(schema, field_name)
430
+ elif schema_type == "object":
431
+ return generate_object_test_cases(schema, field_name)
432
+ else:
433
+ return ["test-value"], None