core-framework 0.12.7__py3-none-any.whl → 0.12.9__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.
core/__init__.py CHANGED
@@ -278,7 +278,7 @@ from core.exceptions import (
278
278
  MissingDependency,
279
279
  )
280
280
 
281
- __version__ = "0.12.7"
281
+ __version__ = "0.12.9"
282
282
  __all__ = [
283
283
  # Models
284
284
  "Model",
core/cli/__init__.py CHANGED
@@ -10,6 +10,8 @@ Comandos disponíveis:
10
10
  - core run: Executa o servidor de desenvolvimento
11
11
  - core shell: Abre shell interativo
12
12
  - core routes: Lista rotas registradas
13
+ - core test: Executa testes com ambiente isolado
14
+ - core version: Mostra versão do framework
13
15
  """
14
16
 
15
17
  from core.cli.main import cli, main
core/cli/main.py CHANGED
@@ -13,7 +13,16 @@ Comandos:
13
13
  run Executa servidor de desenvolvimento
14
14
  shell Abre shell interativo async
15
15
  routes Lista rotas registradas
16
+ test Executa testes com ambiente isolado
16
17
  version Mostra versão do framework
18
+
19
+ Comando test:
20
+ core test # Roda todos os testes em tests/
21
+ core test tests/test_auth.py # Roda arquivo específico
22
+ core test -v # Saída verbosa
23
+ core test --cov # Com cobertura de código
24
+ core test -k "test_login" # Filtrar por keyword
25
+ core test -m unit # Apenas testes unitários
17
26
  """
18
27
 
19
28
  from __future__ import annotations
@@ -472,6 +481,112 @@ def cmd_version(args: argparse.Namespace) -> int:
472
481
  return 0
473
482
 
474
483
 
484
+ def cmd_test(args: argparse.Namespace) -> int:
485
+ """
486
+ Run tests with auto-discovery and isolated environment.
487
+
488
+ This command:
489
+ - Automatically sets up an isolated test environment
490
+ - Initializes database with in-memory SQLite
491
+ - Configures auth, settings, and middleware
492
+ - Runs pytest with appropriate options
493
+ - Supports coverage reporting
494
+
495
+ Usage:
496
+ core test # Run all tests in tests/
497
+ core test tests/test_auth.py # Run specific file
498
+ core test -v # Verbose output
499
+ core test --cov # With coverage
500
+ core test -k "test_login" # Filter by keyword
501
+ core test -m unit # Only unit tests
502
+ """
503
+ import subprocess
504
+ import shutil
505
+
506
+ # Check if pytest is installed
507
+ pytest_path = shutil.which("pytest")
508
+ if not pytest_path:
509
+ print(error("pytest not installed. Install with:"))
510
+ print(info(" pip install pytest pytest-asyncio"))
511
+ return 1
512
+
513
+ print(bold("🧪 Core Framework Test Runner"))
514
+ print()
515
+
516
+ # Build pytest command
517
+ cmd = ["python", "-m", "pytest"]
518
+
519
+ # Add test path
520
+ cmd.append(args.path)
521
+
522
+ # Verbose
523
+ if args.verbose:
524
+ cmd.append("-v")
525
+
526
+ # Keyword filter
527
+ if args.keyword:
528
+ cmd.extend(["-k", args.keyword])
529
+
530
+ # Exit on first failure
531
+ if args.exitfirst:
532
+ cmd.append("-x")
533
+
534
+ # Marker filter
535
+ if args.marker:
536
+ cmd.extend(["-m", args.marker])
537
+
538
+ # No header
539
+ if args.no_header:
540
+ cmd.append("--no-header")
541
+
542
+ # Coverage
543
+ if args.cov:
544
+ # Check if pytest-cov is installed
545
+ if not shutil.which("pytest-cov") and not _check_pytest_cov():
546
+ print(warning("pytest-cov not installed. Install with:"))
547
+ print(info(" pip install pytest-cov"))
548
+ print()
549
+
550
+ cmd.append(f"--cov={args.cov}")
551
+ cmd.append(f"--cov-report={args.cov_report}")
552
+
553
+ # Always use asyncio mode auto
554
+ cmd.extend(["--asyncio-mode=auto"])
555
+
556
+ # Show short traceback
557
+ cmd.append("--tb=short")
558
+
559
+ print(info(f"Running: {' '.join(cmd)}"))
560
+ print()
561
+
562
+ # Set environment variables for isolated testing
563
+ env = os.environ.copy()
564
+ env["TESTING"] = "true"
565
+ env["DEBUG"] = "true"
566
+ env.setdefault("DATABASE_URL", "sqlite+aiosqlite:///:memory:")
567
+ env.setdefault("SECRET_KEY", "test-secret-key-for-testing-only")
568
+
569
+ # Run pytest
570
+ try:
571
+ result = subprocess.run(cmd, env=env)
572
+ return result.returncode
573
+ except KeyboardInterrupt:
574
+ print(warning("\nTests interrupted"))
575
+ return 130
576
+ except Exception as e:
577
+ print(error(f"Error running tests: {e}"))
578
+ return 1
579
+
580
+
581
+ def _check_pytest_cov() -> bool:
582
+ """Check if pytest-cov is installed as a module."""
583
+ try:
584
+ import pytest_cov
585
+ return True
586
+ except ImportError:
587
+ return False
588
+
589
+
475
590
  def check_uv_installed() -> bool:
476
591
  """Verifica se uv está instalado."""
477
592
  import shutil
@@ -3747,6 +3862,54 @@ For more information, visit: https://github.com/SorPuti/core-framework
3747
3862
  )
3748
3863
  tasks_parser.set_defaults(func=cmd_tasks)
3749
3864
 
3865
+ # test
3866
+ test_parser = subparsers.add_parser(
3867
+ "test",
3868
+ help="Run tests with auto-discovery and isolated environment"
3869
+ )
3870
+ test_parser.add_argument(
3871
+ "path",
3872
+ nargs="?",
3873
+ default="tests",
3874
+ help="Test path or file (default: tests)"
3875
+ )
3876
+ test_parser.add_argument(
3877
+ "-v", "--verbose",
3878
+ action="store_true",
3879
+ help="Verbose output"
3880
+ )
3881
+ test_parser.add_argument(
3882
+ "-k", "--keyword",
3883
+ help="Only run tests matching keyword expression"
3884
+ )
3885
+ test_parser.add_argument(
3886
+ "-x", "--exitfirst",
3887
+ action="store_true",
3888
+ help="Exit on first failure"
3889
+ )
3890
+ test_parser.add_argument(
3891
+ "--cov",
3892
+ nargs="?",
3893
+ const=".",
3894
+ help="Enable coverage (optionally specify source)"
3895
+ )
3896
+ test_parser.add_argument(
3897
+ "--cov-report",
3898
+ choices=["term", "html", "xml", "json"],
3899
+ default="term",
3900
+ help="Coverage report format"
3901
+ )
3902
+ test_parser.add_argument(
3903
+ "-m", "--marker",
3904
+ help="Only run tests with this marker (e.g., 'unit', 'integration')"
3905
+ )
3906
+ test_parser.add_argument(
3907
+ "--no-header",
3908
+ action="store_true",
3909
+ help="Disable pytest header"
3910
+ )
3911
+ test_parser.set_defaults(func=cmd_test)
3912
+
3750
3913
  return parser
3751
3914
 
3752
3915
 
@@ -0,0 +1,99 @@
1
+ """
2
+ Testing utilities for core-framework applications.
3
+
4
+ This module provides a complete testing toolkit for applications built
5
+ with core-framework, including:
6
+
7
+ - TestClient: HTTP client with automatic test database setup
8
+ - AuthenticatedClient: Pre-authenticated HTTP client
9
+ - TestDatabase: Database utilities for testing
10
+ - MockKafka, MockRedis, MockHTTP: Mock services
11
+ - Factory: Data factory pattern for test data generation
12
+
13
+ Usage:
14
+ # In your conftest.py
15
+ import pytest
16
+ from your_app import app as _app
17
+
18
+ @pytest.fixture(scope="session")
19
+ def app():
20
+ return _app.app
21
+
22
+ # In your tests
23
+ class TestUsers:
24
+ async def test_register(self, client):
25
+ response = await client.post("/auth/register", json={...})
26
+ assert response.status_code == 201
27
+
28
+ async def test_profile(self, auth_client):
29
+ response = await auth_client.get("/auth/me")
30
+ assert response.status_code == 200
31
+
32
+ Quick Start:
33
+ 1. Install test dependencies: pip install core-framework[testing]
34
+ 2. Add to pyproject.toml:
35
+ [tool.pytest.ini_options]
36
+ asyncio_mode = "auto"
37
+ plugins = ["core.testing.plugin"]
38
+ 3. Create conftest.py with your app fixture
39
+ 4. Write tests using provided fixtures
40
+ """
41
+
42
+ from core.testing.client import (
43
+ TestClient,
44
+ AuthenticatedClient,
45
+ create_test_client,
46
+ create_auth_client,
47
+ )
48
+ from core.testing.database import (
49
+ TestDatabase,
50
+ setup_test_db,
51
+ teardown_test_db,
52
+ get_test_session,
53
+ )
54
+ from core.testing.mocks import (
55
+ MockKafka,
56
+ MockRedis,
57
+ MockHTTP,
58
+ MockMessage,
59
+ MockHTTPResponse,
60
+ )
61
+ from core.testing.factories import (
62
+ Factory,
63
+ UserFactory,
64
+ fake,
65
+ )
66
+ from core.testing.assertions import (
67
+ assert_status,
68
+ assert_json_contains,
69
+ assert_error_code,
70
+ assert_validation_error,
71
+ )
72
+
73
+ __all__ = [
74
+ # Client
75
+ "TestClient",
76
+ "AuthenticatedClient",
77
+ "create_test_client",
78
+ "create_auth_client",
79
+ # Database
80
+ "TestDatabase",
81
+ "setup_test_db",
82
+ "teardown_test_db",
83
+ "get_test_session",
84
+ # Mocks
85
+ "MockKafka",
86
+ "MockRedis",
87
+ "MockHTTP",
88
+ "MockMessage",
89
+ "MockHTTPResponse",
90
+ # Factories
91
+ "Factory",
92
+ "UserFactory",
93
+ "fake",
94
+ # Assertions
95
+ "assert_status",
96
+ "assert_json_contains",
97
+ "assert_error_code",
98
+ "assert_validation_error",
99
+ ]
@@ -0,0 +1,347 @@
1
+ """
2
+ Custom assertions for API testing.
3
+
4
+ Provides helper functions for common test assertions.
5
+
6
+ Usage:
7
+ response = await client.get("/users/1")
8
+
9
+ assert_status(response, 200)
10
+ assert_json_contains(response, {"email": "test@example.com"})
11
+ assert_error_code(response, "not_found")
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from httpx import Response
20
+
21
+
22
+ def assert_status(response: "Response", expected: int, msg: str = "") -> None:
23
+ """
24
+ Assert response has expected status code.
25
+
26
+ Args:
27
+ response: HTTP response
28
+ expected: Expected status code
29
+ msg: Optional message
30
+
31
+ Raises:
32
+ AssertionError: If status doesn't match
33
+
34
+ Example:
35
+ assert_status(response, 200)
36
+ assert_status(response, 201, "User should be created")
37
+ """
38
+ actual = response.status_code
39
+ if actual != expected:
40
+ body = _safe_json(response)
41
+ error_msg = f"Expected status {expected}, got {actual}. Response: {body}"
42
+ if msg:
43
+ error_msg = f"{msg}. {error_msg}"
44
+ raise AssertionError(error_msg)
45
+
46
+
47
+ def assert_status_ok(response: "Response", msg: str = "") -> None:
48
+ """Assert response is 2xx."""
49
+ if not 200 <= response.status_code < 300:
50
+ body = _safe_json(response)
51
+ error_msg = f"Expected 2xx status, got {response.status_code}. Response: {body}"
52
+ if msg:
53
+ error_msg = f"{msg}. {error_msg}"
54
+ raise AssertionError(error_msg)
55
+
56
+
57
+ def assert_status_error(response: "Response", msg: str = "") -> None:
58
+ """Assert response is 4xx or 5xx."""
59
+ if response.status_code < 400:
60
+ body = _safe_json(response)
61
+ error_msg = f"Expected error status, got {response.status_code}. Response: {body}"
62
+ if msg:
63
+ error_msg = f"{msg}. {error_msg}"
64
+ raise AssertionError(error_msg)
65
+
66
+
67
+ def assert_json_contains(
68
+ response: "Response",
69
+ expected: dict[str, Any],
70
+ msg: str = "",
71
+ ) -> None:
72
+ """
73
+ Assert response JSON contains expected fields.
74
+
75
+ Args:
76
+ response: HTTP response
77
+ expected: Dict of expected fields and values
78
+ msg: Optional message
79
+
80
+ Raises:
81
+ AssertionError: If fields don't match
82
+
83
+ Example:
84
+ assert_json_contains(response, {"email": "test@example.com"})
85
+ assert_json_contains(response, {"status": "active", "role": "admin"})
86
+ """
87
+ actual = _safe_json(response)
88
+
89
+ if not isinstance(actual, dict):
90
+ raise AssertionError(
91
+ f"Expected JSON object, got {type(actual).__name__}: {actual}"
92
+ )
93
+
94
+ for key, value in expected.items():
95
+ if key not in actual:
96
+ error_msg = f"Missing key '{key}' in response. Response: {actual}"
97
+ if msg:
98
+ error_msg = f"{msg}. {error_msg}"
99
+ raise AssertionError(error_msg)
100
+
101
+ if actual[key] != value:
102
+ error_msg = (
103
+ f"Value mismatch for '{key}': expected {value!r}, "
104
+ f"got {actual[key]!r}. Response: {actual}"
105
+ )
106
+ if msg:
107
+ error_msg = f"{msg}. {error_msg}"
108
+ raise AssertionError(error_msg)
109
+
110
+
111
+ def assert_json_equals(
112
+ response: "Response",
113
+ expected: dict[str, Any] | list[Any],
114
+ msg: str = "",
115
+ ) -> None:
116
+ """
117
+ Assert response JSON equals expected exactly.
118
+
119
+ Args:
120
+ response: HTTP response
121
+ expected: Expected JSON value
122
+ msg: Optional message
123
+ """
124
+ actual = _safe_json(response)
125
+
126
+ if actual != expected:
127
+ error_msg = f"JSON mismatch. Expected: {expected}. Got: {actual}"
128
+ if msg:
129
+ error_msg = f"{msg}. {error_msg}"
130
+ raise AssertionError(error_msg)
131
+
132
+
133
+ def assert_json_list(
134
+ response: "Response",
135
+ min_length: int = 0,
136
+ max_length: int | None = None,
137
+ msg: str = "",
138
+ ) -> list[Any]:
139
+ """
140
+ Assert response is a JSON list and return it.
141
+
142
+ Args:
143
+ response: HTTP response
144
+ min_length: Minimum list length
145
+ max_length: Maximum list length (None = no limit)
146
+ msg: Optional message
147
+
148
+ Returns:
149
+ The response JSON list
150
+ """
151
+ actual = _safe_json(response)
152
+
153
+ if not isinstance(actual, list):
154
+ raise AssertionError(
155
+ f"Expected JSON list, got {type(actual).__name__}: {actual}"
156
+ )
157
+
158
+ if len(actual) < min_length:
159
+ error_msg = f"List too short: expected at least {min_length}, got {len(actual)}"
160
+ if msg:
161
+ error_msg = f"{msg}. {error_msg}"
162
+ raise AssertionError(error_msg)
163
+
164
+ if max_length is not None and len(actual) > max_length:
165
+ error_msg = f"List too long: expected at most {max_length}, got {len(actual)}"
166
+ if msg:
167
+ error_msg = f"{msg}. {error_msg}"
168
+ raise AssertionError(error_msg)
169
+
170
+ return actual
171
+
172
+
173
+ def assert_error_code(
174
+ response: "Response",
175
+ code: str,
176
+ msg: str = "",
177
+ ) -> None:
178
+ """
179
+ Assert response contains specific error code.
180
+
181
+ Looks for code in: detail.code, code, error.code
182
+
183
+ Args:
184
+ response: HTTP response
185
+ code: Expected error code
186
+ msg: Optional message
187
+
188
+ Example:
189
+ assert_error_code(response, "not_found")
190
+ assert_error_code(response, "validation_error")
191
+ """
192
+ actual = _safe_json(response)
193
+
194
+ if not isinstance(actual, dict):
195
+ raise AssertionError(f"Expected JSON object, got: {actual}")
196
+
197
+ # Try different locations for error code
198
+ actual_code = (
199
+ actual.get("code") or
200
+ (actual.get("detail", {}).get("code") if isinstance(actual.get("detail"), dict) else None) or
201
+ actual.get("error", {}).get("code") if isinstance(actual.get("error"), dict) else None
202
+ )
203
+
204
+ if actual_code != code:
205
+ error_msg = f"Expected error code '{code}', got '{actual_code}'. Response: {actual}"
206
+ if msg:
207
+ error_msg = f"{msg}. {error_msg}"
208
+ raise AssertionError(error_msg)
209
+
210
+
211
+ def assert_validation_error(
212
+ response: "Response",
213
+ field: str | None = None,
214
+ msg: str = "",
215
+ ) -> None:
216
+ """
217
+ Assert response is a validation error.
218
+
219
+ Args:
220
+ response: HTTP response
221
+ field: Optional field that should have error
222
+ msg: Optional message
223
+
224
+ Example:
225
+ assert_validation_error(response)
226
+ assert_validation_error(response, field="email")
227
+ """
228
+ if response.status_code != 422:
229
+ raise AssertionError(
230
+ f"Expected 422 validation error, got {response.status_code}. "
231
+ f"Response: {_safe_json(response)}"
232
+ )
233
+
234
+ if field is not None:
235
+ actual = _safe_json(response)
236
+
237
+ # Look for field in various error formats
238
+ found = False
239
+
240
+ # FastAPI format: detail[].loc
241
+ detail = actual.get("detail", [])
242
+ if isinstance(detail, list):
243
+ for error in detail:
244
+ loc = error.get("loc", [])
245
+ if field in loc or (len(loc) > 1 and loc[-1] == field):
246
+ found = True
247
+ break
248
+
249
+ # Our format: errors[].field
250
+ errors = actual.get("errors", [])
251
+ if isinstance(errors, list):
252
+ for error in errors:
253
+ if error.get("field") == field:
254
+ found = True
255
+ break
256
+
257
+ if not found:
258
+ error_msg = f"Expected validation error for field '{field}'. Response: {actual}"
259
+ if msg:
260
+ error_msg = f"{msg}. {error_msg}"
261
+ raise AssertionError(error_msg)
262
+
263
+
264
+ def assert_header(
265
+ response: "Response",
266
+ header: str,
267
+ expected: str | None = None,
268
+ msg: str = "",
269
+ ) -> str | None:
270
+ """
271
+ Assert response has header, optionally with specific value.
272
+
273
+ Args:
274
+ response: HTTP response
275
+ header: Header name
276
+ expected: Expected value (None = just check exists)
277
+ msg: Optional message
278
+
279
+ Returns:
280
+ Header value
281
+ """
282
+ actual = response.headers.get(header)
283
+
284
+ if actual is None:
285
+ error_msg = f"Missing header '{header}'. Headers: {dict(response.headers)}"
286
+ if msg:
287
+ error_msg = f"{msg}. {error_msg}"
288
+ raise AssertionError(error_msg)
289
+
290
+ if expected is not None and actual != expected:
291
+ error_msg = f"Header '{header}' mismatch: expected '{expected}', got '{actual}'"
292
+ if msg:
293
+ error_msg = f"{msg}. {error_msg}"
294
+ raise AssertionError(error_msg)
295
+
296
+ return actual
297
+
298
+
299
+ def assert_no_error(response: "Response", msg: str = "") -> None:
300
+ """
301
+ Assert response is not an error (2xx status).
302
+
303
+ Provides detailed error message on failure.
304
+ """
305
+ if response.status_code >= 400:
306
+ body = _safe_json(response)
307
+ error_msg = (
308
+ f"Request failed with {response.status_code}. "
309
+ f"Response: {body}"
310
+ )
311
+ if msg:
312
+ error_msg = f"{msg}. {error_msg}"
313
+ raise AssertionError(error_msg)
314
+
315
+
316
+ def assert_created(response: "Response", msg: str = "") -> dict[str, Any]:
317
+ """
318
+ Assert response is 201 Created and return JSON body.
319
+
320
+ Returns:
321
+ Response JSON
322
+ """
323
+ assert_status(response, 201, msg)
324
+ return _safe_json(response)
325
+
326
+
327
+ def assert_not_found(response: "Response", msg: str = "") -> None:
328
+ """Assert response is 404 Not Found."""
329
+ assert_status(response, 404, msg)
330
+
331
+
332
+ def assert_unauthorized(response: "Response", msg: str = "") -> None:
333
+ """Assert response is 401 Unauthorized."""
334
+ assert_status(response, 401, msg)
335
+
336
+
337
+ def assert_forbidden(response: "Response", msg: str = "") -> None:
338
+ """Assert response is 403 Forbidden."""
339
+ assert_status(response, 403, msg)
340
+
341
+
342
+ def _safe_json(response: "Response") -> Any:
343
+ """Safely get JSON from response."""
344
+ try:
345
+ return response.json()
346
+ except Exception:
347
+ return response.text