sefrone-api-e2e 1.0.3__py3-none-any.whl → 1.0.4__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.
@@ -1,5 +1,9 @@
1
1
  from .api_e2e_manager import ApiE2ETestsManager
2
+ from .email_mock_server import EmailMockServer
3
+ from .json_mock_server import JsonMockServer
2
4
 
3
5
  __all__ = [
4
- "ApiE2ETestsManager"
6
+ "ApiE2ETestsManager",
7
+ "EmailMockServer",
8
+ "JsonMockServer"
5
9
  ]
@@ -427,12 +427,23 @@ class ApiE2ETestsManager:
427
427
  header_value = ApiE2ETestsManager.substitute_stored(header_value, scenario_name)
428
428
  request_headers[header_key] = header_value
429
429
 
430
+ request_query_params = {}
431
+ if "request_query_params" in step:
432
+ params = step["request_query_params"]
433
+ if isinstance(params, dict):
434
+ for param_key, param_value in params.items():
435
+ if isinstance(param_value, str):
436
+ param_value = ApiE2ETestsManager.substitute_stored(param_value, scenario_name)
437
+ request_query_params[param_key] = param_value
438
+ else:
439
+ raise ValueError(f"query_params must be a dictionary, got {type(params)}")
440
+
430
441
  body = step.get("body")
431
442
  if body is not None:
432
443
  body = ApiE2ETestsManager.substitute_stored_in_body(body, scenario_name)
433
444
  expect = step.get("expect", {})
434
445
 
435
- resp = session.request(method, url, json=body, headers=request_headers, timeout=ApiE2ETestsManager.DEFAULT_TIMEOUT)
446
+ resp = session.request(method, url, json=body, headers=request_headers, params=request_query_params, timeout=ApiE2ETestsManager.DEFAULT_TIMEOUT)
436
447
  print(f" -> {method} {url} -> {resp.status_code}")
437
448
 
438
449
  expected_status = expect.get("status")
@@ -0,0 +1,480 @@
1
+ import threading
2
+ import json
3
+ import socket
4
+ import email
5
+ from http.server import HTTPServer, BaseHTTPRequestHandler
6
+ from typing import List, Dict, Any, Optional
7
+ from datetime import datetime
8
+
9
+
10
+ class EmailMockServer:
11
+ """
12
+ Comprehensive mock email server that supports SMTP, IMAP, and HTTP protocols.
13
+
14
+ Features:
15
+ - SMTP server for receiving emails (standard protocol)
16
+ - IMAP server for checking emails (standard protocol)
17
+ - HTTP API for test automation and verification
18
+ - Query captured emails via GET /emails
19
+ - Clear emails via DELETE /emails
20
+ """
21
+
22
+ def __init__(self, host: str = "localhost", http_port: int = 8025, smtp_port: int = 1025, imap_port: int = 1143):
23
+ self.host = host
24
+ self.http_port = http_port
25
+ self.smtp_port = smtp_port
26
+ self.imap_port = imap_port
27
+ self.emails: List[Dict[str, Any]] = []
28
+ self._http_server: Optional[HTTPServer] = None
29
+ self._smtp_thread: Optional[threading.Thread] = None
30
+ self._imap_thread: Optional[threading.Thread] = None
31
+ self._http_thread: Optional[threading.Thread] = None
32
+ self._running = False
33
+ self._smtp_socket: Optional[socket.socket] = None
34
+ self._imap_socket: Optional[socket.socket] = None
35
+ self._next_uid = 1
36
+
37
+ def start(self):
38
+ """Start all email server protocols in background threads."""
39
+ if self._running:
40
+ print(f"[EmailMockServer] Already running")
41
+ return
42
+
43
+ self._running = True
44
+
45
+ # Start HTTP server
46
+ handler = self._create_http_handler()
47
+ self._http_server = HTTPServer((self.host, self.http_port), handler)
48
+ self._http_thread = threading.Thread(target=self._http_server.serve_forever, daemon=True)
49
+ self._http_thread.start()
50
+ print(f"[EmailMockServer] HTTP API started on http://{self.host}:{self.http_port}")
51
+
52
+ # Start SMTP server
53
+ self._smtp_thread = threading.Thread(target=self._run_smtp_server, daemon=True)
54
+ self._smtp_thread.start()
55
+ print(f"[EmailMockServer] SMTP server started on {self.host}:{self.smtp_port}")
56
+
57
+ # Start IMAP server
58
+ self._imap_thread = threading.Thread(target=self._run_imap_server, daemon=True)
59
+ self._imap_thread.start()
60
+ print(f"[EmailMockServer] IMAP server started on {self.host}:{self.imap_port}")
61
+
62
+ def stop(self):
63
+ """Stop all email server protocols."""
64
+ if not self._running:
65
+ return
66
+
67
+ self._running = False
68
+
69
+ # Stop HTTP server
70
+ if self._http_server:
71
+ self._http_server.shutdown()
72
+ self._http_server.server_close()
73
+ if self._http_thread:
74
+ self._http_thread.join(timeout=2)
75
+
76
+ # Stop SMTP server
77
+ if self._smtp_socket:
78
+ try:
79
+ self._smtp_socket.close()
80
+ except:
81
+ pass
82
+ if self._smtp_thread:
83
+ self._smtp_thread.join(timeout=2)
84
+
85
+ # Stop IMAP server
86
+ if self._imap_socket:
87
+ try:
88
+ self._imap_socket.close()
89
+ except:
90
+ pass
91
+ if self._imap_thread:
92
+ self._imap_thread.join(timeout=2)
93
+
94
+ print(f"[EmailMockServer] Stopped all protocols")
95
+
96
+ def clear(self):
97
+ """Clear all captured emails."""
98
+ self.emails.clear()
99
+ print(f"[EmailMockServer] Cleared all emails")
100
+
101
+ def get_emails(self) -> List[Dict[str, Any]]:
102
+ """Get all captured emails."""
103
+ return self.emails.copy()
104
+
105
+ def get_email(self, index: int) -> Optional[Dict[str, Any]]:
106
+ """Get a specific email by index."""
107
+ if 0 <= index < len(self.emails):
108
+ return self.emails[index]
109
+ return None
110
+
111
+ def count_emails(self) -> int:
112
+ """Get the count of captured emails."""
113
+ return len(self.emails)
114
+
115
+ def find_emails(self, **filters) -> List[Dict[str, Any]]:
116
+ """
117
+ Find emails matching the given filters.
118
+
119
+ Example:
120
+ find_emails(to="user@example.com", subject="Welcome")
121
+ """
122
+ results = []
123
+ for email_data in self.emails:
124
+ match = True
125
+ for key, value in filters.items():
126
+ if key not in email_data or email_data[key] != value:
127
+ match = False
128
+ break
129
+ if match:
130
+ results.append(email_data)
131
+ return results
132
+
133
+ def _run_smtp_server(self):
134
+ """Run a simple SMTP server to receive emails."""
135
+ self._smtp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
136
+ self._smtp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
137
+ self._smtp_socket.bind((self.host, self.smtp_port))
138
+ self._smtp_socket.listen(5)
139
+ self._smtp_socket.settimeout(1.0)
140
+
141
+ while self._running:
142
+ try:
143
+ client_socket, addr = self._smtp_socket.accept()
144
+ threading.Thread(target=self._handle_smtp_client, args=(client_socket,), daemon=True).start()
145
+ except socket.timeout:
146
+ continue
147
+ except Exception as e:
148
+ if self._running:
149
+ print(f"[EmailMockServer] SMTP error: {e}")
150
+ break
151
+
152
+ def _handle_smtp_client(self, client_socket: socket.socket):
153
+ """Handle SMTP client connection."""
154
+ try:
155
+ client_socket.sendall(b"220 Mock SMTP Server Ready\r\n")
156
+
157
+ mail_from = None
158
+ rcpt_to = []
159
+ data_mode = False
160
+ email_data = []
161
+
162
+ while True:
163
+ data = client_socket.recv(4096).decode('utf-8', errors='ignore')
164
+ if not data:
165
+ break
166
+
167
+ lines = data.strip().split('\r\n')
168
+ for line in lines:
169
+ if not line:
170
+ continue
171
+
172
+ if data_mode:
173
+ if line == '.':
174
+ # End of email data
175
+ data_mode = False
176
+ email_content = '\r\n'.join(email_data)
177
+ self._store_smtp_email(mail_from, rcpt_to, email_content)
178
+ client_socket.sendall(b"250 OK: Message accepted\r\n")
179
+ email_data = []
180
+ mail_from = None
181
+ rcpt_to = []
182
+ else:
183
+ email_data.append(line)
184
+ elif line.upper().startswith('EHLO') or line.upper().startswith('HELO'):
185
+ client_socket.sendall(b"250 Hello\r\n")
186
+ elif line.upper().startswith('MAIL FROM:'):
187
+ mail_from = line[10:].strip().strip('<>')
188
+ client_socket.sendall(b"250 OK\r\n")
189
+ elif line.upper().startswith('RCPT TO:'):
190
+ rcpt_to.append(line[8:].strip().strip('<>'))
191
+ client_socket.sendall(b"250 OK\r\n")
192
+ elif line.upper() == 'DATA':
193
+ data_mode = True
194
+ client_socket.sendall(b"354 Start mail input; end with <CRLF>.<CRLF>\r\n")
195
+ elif line.upper() == 'QUIT':
196
+ client_socket.sendall(b"221 Bye\r\n")
197
+ return
198
+ else:
199
+ client_socket.sendall(b"250 OK\r\n")
200
+ except Exception as e:
201
+ print(f"[EmailMockServer] SMTP client error: {e}")
202
+ finally:
203
+ client_socket.close()
204
+
205
+ def _store_smtp_email(self, mail_from: str, rcpt_to: List[str], raw_content: str):
206
+ """Parse and store an SMTP email."""
207
+ try:
208
+ msg = email.message_from_string(raw_content)
209
+
210
+ # Extract email details
211
+ subject = msg.get('Subject', '')
212
+ to = msg.get('To', ', '.join(rcpt_to) if rcpt_to else '')
213
+ from_addr = msg.get('From', mail_from or '')
214
+
215
+ # Get body
216
+ body = ""
217
+ if msg.is_multipart():
218
+ for part in msg.walk():
219
+ if part.get_content_type() == "text/plain":
220
+ body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
221
+ break
222
+ else:
223
+ body = msg.get_payload(decode=True)
224
+ if body:
225
+ body = body.decode('utf-8', errors='ignore')
226
+ else:
227
+ body = msg.get_payload()
228
+
229
+ email_data = {
230
+ "index": len(self.emails),
231
+ "uid": self._next_uid,
232
+ "to": to,
233
+ "from": from_addr,
234
+ "subject": subject,
235
+ "body": body,
236
+ "raw": raw_content,
237
+ "received_at": datetime.utcnow().isoformat(),
238
+ "flags": []
239
+ }
240
+
241
+ self._next_uid += 1
242
+ self.emails.append(email_data)
243
+
244
+ print(f"[EmailMockServer] SMTP captured email #{email_data['index']}: "
245
+ f"from={from_addr}, to={to}, subject={subject}")
246
+ except Exception as e:
247
+ print(f"[EmailMockServer] Error parsing SMTP email: {e}")
248
+
249
+ def _run_imap_server(self):
250
+ """Run a simple IMAP server to allow checking emails."""
251
+ self._imap_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
252
+ self._imap_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
253
+ self._imap_socket.bind((self.host, self.imap_port))
254
+ self._imap_socket.listen(5)
255
+ self._imap_socket.settimeout(1.0)
256
+
257
+ while self._running:
258
+ try:
259
+ client_socket, addr = self._imap_socket.accept()
260
+ threading.Thread(target=self._handle_imap_client, args=(client_socket,), daemon=True).start()
261
+ except socket.timeout:
262
+ continue
263
+ except Exception as e:
264
+ if self._running:
265
+ print(f"[EmailMockServer] IMAP error: {e}")
266
+ break
267
+
268
+ def _handle_imap_client(self, client_socket: socket.socket):
269
+ """Handle IMAP client connection."""
270
+ try:
271
+ client_socket.sendall(b"* OK Mock IMAP Server Ready\r\n")
272
+
273
+ authenticated = False
274
+ selected_mailbox = None
275
+
276
+ while True:
277
+ data = client_socket.recv(4096).decode('utf-8', errors='ignore')
278
+ if not data:
279
+ break
280
+
281
+ lines = data.strip().split('\r\n')
282
+ for line in lines:
283
+ if not line:
284
+ continue
285
+
286
+ parts = line.split(' ', 2)
287
+ if len(parts) < 2:
288
+ continue
289
+
290
+ tag = parts[0]
291
+ command = parts[1].upper()
292
+ args = parts[2] if len(parts) > 2 else ""
293
+
294
+ if command == 'CAPABILITY':
295
+ client_socket.sendall(b"* CAPABILITY IMAP4rev1\r\n")
296
+ client_socket.sendall(f"{tag} OK CAPABILITY completed\r\n".encode())
297
+
298
+ elif command == 'LOGIN':
299
+ authenticated = True
300
+ client_socket.sendall(f"{tag} OK LOGIN completed\r\n".encode())
301
+
302
+ elif command == 'SELECT' or command == 'EXAMINE':
303
+ if not authenticated:
304
+ client_socket.sendall(f"{tag} NO Not authenticated\r\n".encode())
305
+ continue
306
+
307
+ selected_mailbox = "INBOX"
308
+ count = len(self.emails)
309
+ client_socket.sendall(f"* {count} EXISTS\r\n".encode())
310
+ client_socket.sendall(b"* 0 RECENT\r\n")
311
+ client_socket.sendall(b"* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n")
312
+ client_socket.sendall(f"{tag} OK [{command}] completed\r\n".encode())
313
+
314
+ elif command == 'FETCH':
315
+ if not authenticated or not selected_mailbox:
316
+ client_socket.sendall(f"{tag} NO Not authenticated or no mailbox selected\r\n".encode())
317
+ continue
318
+
319
+ # Parse FETCH command: e.g., "1:* (FLAGS BODY[])"
320
+ fetch_parts = args.split(' ', 1)
321
+ sequence = fetch_parts[0]
322
+ items = fetch_parts[1] if len(fetch_parts) > 1 else "(FLAGS)"
323
+
324
+ # Simple implementation: fetch all emails
325
+ for i, email_data in enumerate(self.emails, 1):
326
+ response_items = []
327
+
328
+ if 'FLAGS' in items:
329
+ flags = ' '.join(email_data.get('flags', []))
330
+ response_items.append(f"FLAGS ({flags})")
331
+
332
+ if 'BODY[]' in items or 'RFC822' in items:
333
+ raw = email_data.get('raw', '')
334
+ if not raw:
335
+ # Construct simple email if raw not available
336
+ raw = f"From: {email_data.get('from', '')}\r\n"
337
+ raw += f"To: {email_data.get('to', '')}\r\n"
338
+ raw += f"Subject: {email_data.get('subject', '')}\r\n"
339
+ raw += f"\r\n{email_data.get('body', '')}"
340
+
341
+ response_items.append(f"BODY[] {{{len(raw.encode())}}}\r\n{raw}")
342
+
343
+ if 'BODY[HEADER.FIELDS (SUBJECT)]' in items or 'ENVELOPE' in items:
344
+ subject = email_data.get('subject', '')
345
+ response_items.append(f"BODY[HEADER.FIELDS (SUBJECT)] {{Subject: {subject}}}")
346
+
347
+ response = f"* {i} FETCH ({' '.join(response_items)})\r\n"
348
+ client_socket.sendall(response.encode())
349
+
350
+ client_socket.sendall(f"{tag} OK FETCH completed\r\n".encode())
351
+
352
+ elif command == 'SEARCH':
353
+ if not authenticated or not selected_mailbox:
354
+ client_socket.sendall(f"{tag} NO Not authenticated or no mailbox selected\r\n".encode())
355
+ continue
356
+
357
+ # Simple SEARCH implementation - return all emails
358
+ matches = ' '.join(str(i) for i in range(1, len(self.emails) + 1))
359
+ client_socket.sendall(f"* SEARCH {matches}\r\n".encode())
360
+ client_socket.sendall(f"{tag} OK SEARCH completed\r\n".encode())
361
+
362
+ elif command == 'LOGOUT':
363
+ client_socket.sendall(b"* BYE Logging out\r\n")
364
+ client_socket.sendall(f"{tag} OK LOGOUT completed\r\n".encode())
365
+ return
366
+
367
+ else:
368
+ client_socket.sendall(f"{tag} BAD Command not recognized\r\n".encode())
369
+
370
+ except Exception as e:
371
+ print(f"[EmailMockServer] IMAP client error: {e}")
372
+ finally:
373
+ client_socket.close()
374
+
375
+ def _create_http_handler(self):
376
+ """Create the HTTP request handler with access to this server instance."""
377
+ server_instance = self
378
+
379
+ class EmailMockHandler(BaseHTTPRequestHandler):
380
+ def log_message(self, format, *args):
381
+ """Suppress default logging."""
382
+ pass
383
+
384
+ def _send_json_response(self, status: int, data: Any):
385
+ """Send a JSON response."""
386
+ self.send_response(status)
387
+ self.send_header("Content-Type", "application/json")
388
+ self.send_header("Access-Control-Allow-Origin", "*")
389
+ self.end_headers()
390
+ self.wfile.write(json.dumps(data).encode())
391
+
392
+ def do_OPTIONS(self):
393
+ """Handle CORS preflight requests."""
394
+ self.send_response(200)
395
+ self.send_header("Access-Control-Allow-Origin", "*")
396
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
397
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
398
+ self.end_headers()
399
+
400
+ def do_POST(self):
401
+ """Handle POST requests to capture emails."""
402
+ if self.path == "/send":
403
+ content_length = int(self.headers.get("Content-Length", 0))
404
+ body = self.rfile.read(content_length).decode("utf-8")
405
+
406
+ try:
407
+ email_data = json.loads(body)
408
+ email_data["received_at"] = datetime.utcnow().isoformat()
409
+ email_data["index"] = len(server_instance.emails)
410
+ server_instance.emails.append(email_data)
411
+
412
+ print(f"[EmailMockServer] Captured email #{email_data['index']}: "
413
+ f"to={email_data.get('to', 'N/A')}, "
414
+ f"subject={email_data.get('subject', 'N/A')}")
415
+
416
+ self._send_json_response(200, {
417
+ "status": "success",
418
+ "message": "Email captured",
419
+ "index": email_data["index"]
420
+ })
421
+ except json.JSONDecodeError:
422
+ self._send_json_response(400, {
423
+ "status": "error",
424
+ "message": "Invalid JSON"
425
+ })
426
+ else:
427
+ self._send_json_response(404, {
428
+ "status": "error",
429
+ "message": "Not found"
430
+ })
431
+
432
+ def do_GET(self):
433
+ """Handle GET requests to query emails."""
434
+ if self.path == "/emails":
435
+ self._send_json_response(200, {
436
+ "count": len(server_instance.emails),
437
+ "emails": server_instance.emails
438
+ })
439
+ elif self.path.startswith("/emails/"):
440
+ try:
441
+ index = int(self.path.split("/")[-1])
442
+ email = server_instance.get_email(index)
443
+ if email:
444
+ self._send_json_response(200, email)
445
+ else:
446
+ self._send_json_response(404, {
447
+ "status": "error",
448
+ "message": f"Email at index {index} not found"
449
+ })
450
+ except ValueError:
451
+ self._send_json_response(400, {
452
+ "status": "error",
453
+ "message": "Invalid index"
454
+ })
455
+ elif self.path == "/health":
456
+ self._send_json_response(200, {
457
+ "status": "healthy",
458
+ "emails_count": len(server_instance.emails)
459
+ })
460
+ else:
461
+ self._send_json_response(404, {
462
+ "status": "error",
463
+ "message": "Not found"
464
+ })
465
+
466
+ def do_DELETE(self):
467
+ """Handle DELETE requests to clear emails."""
468
+ if self.path == "/emails":
469
+ server_instance.clear()
470
+ self._send_json_response(200, {
471
+ "status": "success",
472
+ "message": "All emails cleared"
473
+ })
474
+ else:
475
+ self._send_json_response(404, {
476
+ "status": "error",
477
+ "message": "Not found"
478
+ })
479
+
480
+ return EmailMockHandler
@@ -0,0 +1,309 @@
1
+ import threading
2
+ import json
3
+ import re
4
+ from http.server import HTTPServer, BaseHTTPRequestHandler
5
+ from typing import List, Dict, Any, Optional, Callable
6
+ from datetime import datetime
7
+ from urllib.parse import urlparse, parse_qs
8
+
9
+
10
+ class JsonMockServer:
11
+ """
12
+ Simple mock JSON/REST API server that can mock endpoints and record requests.
13
+
14
+ Features:
15
+ - Configure mock endpoints with expected responses
16
+ - Record all incoming requests for verification
17
+ - Query recorded requests
18
+ - Support for dynamic response functions
19
+ - Pattern matching for URLs
20
+ """
21
+
22
+ def __init__(self, host: str = "localhost", port: int = 8080):
23
+ self.host = host
24
+ self.port = port
25
+ self.mocks: List[Dict[str, Any]] = []
26
+ self.requests: List[Dict[str, Any]] = []
27
+ self._server: Optional[HTTPServer] = None
28
+ self._thread: Optional[threading.Thread] = None
29
+ self._running = False
30
+
31
+ def start(self):
32
+ """Start the mock JSON server in a background thread."""
33
+ if self._running:
34
+ print(f"[JsonMockServer] Already running on {self.host}:{self.port}")
35
+ return
36
+
37
+ handler = self._create_handler()
38
+ self._server = HTTPServer((self.host, self.port), handler)
39
+ self._running = True
40
+
41
+ self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
42
+ self._thread.start()
43
+ print(f"[JsonMockServer] Started on http://{self.host}:{self.port}")
44
+
45
+ def stop(self):
46
+ """Stop the mock JSON server."""
47
+ if not self._running:
48
+ return
49
+
50
+ self._running = False
51
+ if self._server:
52
+ self._server.shutdown()
53
+ self._server.server_close()
54
+ if self._thread:
55
+ self._thread.join(timeout=2)
56
+ print(f"[JsonMockServer] Stopped")
57
+
58
+ def clear_mocks(self):
59
+ """Clear all configured mocks."""
60
+ self.mocks.clear()
61
+ print(f"[JsonMockServer] Cleared all mocks")
62
+
63
+ def clear_requests(self):
64
+ """Clear all recorded requests."""
65
+ self.requests.clear()
66
+ print(f"[JsonMockServer] Cleared all recorded requests")
67
+
68
+ def clear_all(self):
69
+ """Clear both mocks and recorded requests."""
70
+ self.clear_mocks()
71
+ self.clear_requests()
72
+
73
+ def add_mock(
74
+ self,
75
+ method: str,
76
+ path: str,
77
+ response_body: Any = None,
78
+ status_code: int = 200,
79
+ response_headers: Optional[Dict[str, str]] = None,
80
+ response_fn: Optional[Callable] = None,
81
+ is_regex: bool = False
82
+ ):
83
+ """
84
+ Add a mock endpoint.
85
+
86
+ Args:
87
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
88
+ path: URL path or regex pattern
89
+ response_body: Response body to return (dict or any JSON-serializable)
90
+ status_code: HTTP status code to return
91
+ response_headers: Additional response headers
92
+ response_fn: Optional function(request_data) -> response_body for dynamic responses
93
+ is_regex: Whether path is a regex pattern
94
+
95
+ Example:
96
+ server.add_mock("GET", "/api/users", {"users": []})
97
+ server.add_mock("POST", "/api/users", {"id": 1}, status_code=201)
98
+ server.add_mock("GET", r"/api/users/\d+", response_fn=lambda req: {"id": req["path_params"][0]}, is_regex=True)
99
+ """
100
+ mock = {
101
+ "method": method.upper(),
102
+ "path": path,
103
+ "response_body": response_body,
104
+ "status_code": status_code,
105
+ "response_headers": response_headers or {},
106
+ "response_fn": response_fn,
107
+ "is_regex": is_regex,
108
+ "hit_count": 0
109
+ }
110
+ self.mocks.append(mock)
111
+ print(f"[JsonMockServer] Added mock: {method.upper()} {path} -> {status_code}")
112
+
113
+ def get_requests(self) -> List[Dict[str, Any]]:
114
+ """Get all recorded requests."""
115
+ return self.requests.copy()
116
+
117
+ def get_request(self, index: int) -> Optional[Dict[str, Any]]:
118
+ """Get a specific request by index."""
119
+ if 0 <= index < len(self.requests):
120
+ return self.requests[index]
121
+ return None
122
+
123
+ def count_requests(self, method: Optional[str] = None, path: Optional[str] = None) -> int:
124
+ """
125
+ Count recorded requests, optionally filtered by method and/or path.
126
+
127
+ Example:
128
+ count_requests() # all requests
129
+ count_requests(method="POST")
130
+ count_requests(path="/api/users")
131
+ count_requests(method="POST", path="/api/users")
132
+ """
133
+ filtered = self.requests
134
+ if method:
135
+ filtered = [r for r in filtered if r["method"] == method.upper()]
136
+ if path:
137
+ filtered = [r for r in filtered if r["path"] == path]
138
+ return len(filtered)
139
+
140
+ def find_requests(self, **filters) -> List[Dict[str, Any]]:
141
+ """
142
+ Find requests matching the given filters.
143
+
144
+ Example:
145
+ find_requests(method="POST", path="/api/users")
146
+ """
147
+ results = []
148
+ for request in self.requests:
149
+ match = True
150
+ for key, value in filters.items():
151
+ if key not in request or request[key] != value:
152
+ match = False
153
+ break
154
+ if match:
155
+ results.append(request)
156
+ return results
157
+
158
+ def _find_mock(self, method: str, path: str) -> Optional[Dict[str, Any]]:
159
+ """Find a matching mock for the given method and path."""
160
+ for mock in self.mocks:
161
+ if mock["method"] != method:
162
+ continue
163
+
164
+ if mock["is_regex"]:
165
+ if re.match(mock["path"], path):
166
+ return mock
167
+ else:
168
+ if mock["path"] == path:
169
+ return mock
170
+ return None
171
+
172
+ def _create_handler(self):
173
+ """Create the HTTP request handler with access to this server instance."""
174
+ server_instance = self
175
+
176
+ class JsonMockHandler(BaseHTTPRequestHandler):
177
+ def log_message(self, format, *args):
178
+ """Suppress default logging."""
179
+ pass
180
+
181
+ def _send_json_response(self, status: int, data: Any, headers: Optional[Dict[str, str]] = None):
182
+ """Send a JSON response."""
183
+ self.send_response(status)
184
+ self.send_header("Content-Type", "application/json")
185
+ self.send_header("Access-Control-Allow-Origin", "*")
186
+ if headers:
187
+ for key, value in headers.items():
188
+ self.send_header(key, value)
189
+ self.end_headers()
190
+ self.wfile.write(json.dumps(data).encode())
191
+
192
+ def do_OPTIONS(self):
193
+ """Handle CORS preflight requests."""
194
+ self.send_response(200)
195
+ self.send_header("Access-Control-Allow-Origin", "*")
196
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
197
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
198
+ self.end_headers()
199
+
200
+ def _handle_request(self, method: str):
201
+ """Common request handling logic."""
202
+ parsed_url = urlparse(self.path)
203
+ path = parsed_url.path
204
+ query_params = parse_qs(parsed_url.query)
205
+
206
+ # Read request body if present
207
+ body = None
208
+ content_length = int(self.headers.get("Content-Length", 0))
209
+ if content_length > 0:
210
+ body_bytes = self.rfile.read(content_length)
211
+ try:
212
+ body = json.loads(body_bytes.decode("utf-8"))
213
+ except json.JSONDecodeError:
214
+ body = body_bytes.decode("utf-8")
215
+
216
+ # Record the request
217
+ request_data = {
218
+ "index": len(server_instance.requests),
219
+ "method": method,
220
+ "path": path,
221
+ "query_params": query_params,
222
+ "headers": dict(self.headers),
223
+ "body": body,
224
+ "received_at": datetime.utcnow().isoformat()
225
+ }
226
+ server_instance.requests.append(request_data)
227
+
228
+ print(f"[JsonMockServer] Recorded request #{request_data['index']}: {method} {path}")
229
+
230
+ # Special endpoint to query recorded requests
231
+ if path == "/_mock/requests":
232
+ self._send_json_response(200, {
233
+ "count": len(server_instance.requests),
234
+ "requests": server_instance.requests
235
+ })
236
+ return
237
+ elif path.startswith("/_mock/requests/"):
238
+ try:
239
+ index = int(path.split("/")[-1])
240
+ request = server_instance.get_request(index)
241
+ if request:
242
+ self._send_json_response(200, request)
243
+ else:
244
+ self._send_json_response(404, {
245
+ "status": "error",
246
+ "message": f"Request at index {index} not found"
247
+ })
248
+ return
249
+ except ValueError:
250
+ self._send_json_response(400, {
251
+ "status": "error",
252
+ "message": "Invalid index"
253
+ })
254
+ return
255
+ elif path == "/_mock/health":
256
+ self._send_json_response(200, {
257
+ "status": "healthy",
258
+ "mocks_count": len(server_instance.mocks),
259
+ "requests_count": len(server_instance.requests)
260
+ })
261
+ return
262
+
263
+ # Find matching mock
264
+ mock = server_instance._find_mock(method, path)
265
+ if mock:
266
+ mock["hit_count"] += 1
267
+
268
+ # Generate response
269
+ if mock["response_fn"]:
270
+ try:
271
+ response_body = mock["response_fn"](request_data)
272
+ except Exception as e:
273
+ self._send_json_response(500, {
274
+ "status": "error",
275
+ "message": f"Mock response function error: {str(e)}"
276
+ })
277
+ return
278
+ else:
279
+ response_body = mock["response_body"]
280
+
281
+ print(f"[JsonMockServer] Matched mock: {method} {path} -> {mock['status_code']}")
282
+ self._send_json_response(
283
+ mock["status_code"],
284
+ response_body,
285
+ mock["response_headers"]
286
+ )
287
+ else:
288
+ print(f"[JsonMockServer] No mock found for: {method} {path}")
289
+ self._send_json_response(404, {
290
+ "status": "error",
291
+ "message": f"No mock configured for {method} {path}"
292
+ })
293
+
294
+ def do_GET(self):
295
+ self._handle_request("GET")
296
+
297
+ def do_POST(self):
298
+ self._handle_request("POST")
299
+
300
+ def do_PUT(self):
301
+ self._handle_request("PUT")
302
+
303
+ def do_DELETE(self):
304
+ self._handle_request("DELETE")
305
+
306
+ def do_PATCH(self):
307
+ self._handle_request("PATCH")
308
+
309
+ return JsonMockHandler
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sefrone-api-e2e
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: A Python package to provide e2e testing helpers for sefrone API projects
5
5
  Home-page: https://bitbucket.org/sefrone/sefrone_pypi
6
6
  Author: Sefrone
@@ -0,0 +1,8 @@
1
+ sefrone_api_e2e/__init__.py,sha256=qXaEtQP7TY_yTKMOQeLhdqaRXWjFEkJeyUckK0ZR1og,232
2
+ sefrone_api_e2e/api_e2e_manager.py,sha256=noCjvItZqbjXZIkvvhMft7m0T85FN3afSbhKJWw9XU8,24306
3
+ sefrone_api_e2e/email_mock_server.py,sha256=M3tY84pL0JKD0KLi9yuj2SBEXF24z_1wM4NfD1VB4pE,21479
4
+ sefrone_api_e2e/json_mock_server.py,sha256=Trg2iezPyUtTsrq8PlFzBE15z8wHLZtEnJDU-QMjaW0,12530
5
+ sefrone_api_e2e-1.0.4.dist-info/METADATA,sha256=eM2GpmOL9noNLeq1fkvG0xVKNQe58PooYTOggL7Fc6o,721
6
+ sefrone_api_e2e-1.0.4.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
7
+ sefrone_api_e2e-1.0.4.dist-info/top_level.txt,sha256=19WO3CsUWUiGtZBotT587N-tkxxjctKOPDEHWpHpS8M,16
8
+ sefrone_api_e2e-1.0.4.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- sefrone_api_e2e/__init__.py,sha256=NNUHigTCBkjdwCfQXVEA7urxF1-A8HF1_IuWJ8LuO-k,91
2
- sefrone_api_e2e/api_e2e_manager.py,sha256=vAA0-H3dVKIUptyxXbYaiZP_9PYu58z9LCj_vi53vwY,23584
3
- sefrone_api_e2e-1.0.3.dist-info/METADATA,sha256=cHEc8npSLW23SM7hxt0GYKY6q8vHCghpPv7oUB-IhUQ,721
4
- sefrone_api_e2e-1.0.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
5
- sefrone_api_e2e-1.0.3.dist-info/top_level.txt,sha256=19WO3CsUWUiGtZBotT587N-tkxxjctKOPDEHWpHpS8M,16
6
- sefrone_api_e2e-1.0.3.dist-info/RECORD,,