core-framework 0.12.6__tar.gz → 0.12.8__tar.gz

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.
Files changed (138) hide show
  1. {core_framework-0.12.6 → core_framework-0.12.8}/PKG-INFO +6 -1
  2. {core_framework-0.12.6 → core_framework-0.12.8}/core/__init__.py +1 -1
  3. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/decorators.py +28 -7
  4. {core_framework-0.12.6 → core_framework-0.12.8}/core/cli/__init__.py +2 -0
  5. {core_framework-0.12.6 → core_framework-0.12.8}/core/cli/main.py +163 -0
  6. {core_framework-0.12.6 → core_framework-0.12.8}/core/permissions.py +29 -8
  7. core_framework-0.12.8/core/testing/__init__.py +99 -0
  8. core_framework-0.12.8/core/testing/assertions.py +347 -0
  9. core_framework-0.12.8/core/testing/client.py +247 -0
  10. core_framework-0.12.8/core/testing/database.py +307 -0
  11. core_framework-0.12.8/core/testing/factories.py +393 -0
  12. core_framework-0.12.8/core/testing/mocks.py +658 -0
  13. core_framework-0.12.8/core/testing/plugin.py +635 -0
  14. {core_framework-0.12.6 → core_framework-0.12.8}/pyproject.toml +10 -1
  15. core_framework-0.12.8/tests/test_auth_helpers.py +206 -0
  16. core_framework-0.12.8/tests/test_imports.py +263 -0
  17. core_framework-0.12.8/tests/test_permissions.py +373 -0
  18. {core_framework-0.12.6 → core_framework-0.12.8}/.gitignore +0 -0
  19. {core_framework-0.12.6 → core_framework-0.12.8}/README.md +0 -0
  20. {core_framework-0.12.6 → core_framework-0.12.8}/core/app.py +0 -0
  21. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/__init__.py +0 -0
  22. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/backends.py +0 -0
  23. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/base.py +0 -0
  24. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/hashers.py +0 -0
  25. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/helpers.py +0 -0
  26. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/middleware.py +0 -0
  27. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/models.py +0 -0
  28. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/permissions.py +0 -0
  29. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/schemas.py +0 -0
  30. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/tokens.py +0 -0
  31. {core_framework-0.12.6 → core_framework-0.12.8}/core/auth/views.py +0 -0
  32. {core_framework-0.12.6 → core_framework-0.12.8}/core/choices.py +0 -0
  33. {core_framework-0.12.6 → core_framework-0.12.8}/core/config.py +0 -0
  34. {core_framework-0.12.6 → core_framework-0.12.8}/core/database.py +0 -0
  35. {core_framework-0.12.6 → core_framework-0.12.8}/core/datetime.py +0 -0
  36. {core_framework-0.12.6 → core_framework-0.12.8}/core/dependencies.py +0 -0
  37. {core_framework-0.12.6 → core_framework-0.12.8}/core/deployment/__init__.py +0 -0
  38. {core_framework-0.12.6 → core_framework-0.12.8}/core/deployment/docker.py +0 -0
  39. {core_framework-0.12.6 → core_framework-0.12.8}/core/deployment/kubernetes.py +0 -0
  40. {core_framework-0.12.6 → core_framework-0.12.8}/core/deployment/pm2.py +0 -0
  41. {core_framework-0.12.6 → core_framework-0.12.8}/core/exceptions.py +0 -0
  42. {core_framework-0.12.6 → core_framework-0.12.8}/core/fields.py +0 -0
  43. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/__init__.py +0 -0
  44. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/avro.py +0 -0
  45. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/base.py +0 -0
  46. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/config.py +0 -0
  47. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/confluent/__init__.py +0 -0
  48. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/confluent/consumer.py +0 -0
  49. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/confluent/producer.py +0 -0
  50. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/decorators.py +0 -0
  51. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/kafka/__init__.py +0 -0
  52. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/kafka/admin.py +0 -0
  53. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/kafka/broker.py +0 -0
  54. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/kafka/consumer.py +0 -0
  55. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/kafka/producer.py +0 -0
  56. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/rabbitmq/__init__.py +0 -0
  57. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/rabbitmq/broker.py +0 -0
  58. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/rabbitmq/consumer.py +0 -0
  59. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/rabbitmq/producer.py +0 -0
  60. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/redis/__init__.py +0 -0
  61. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/redis/broker.py +0 -0
  62. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/redis/consumer.py +0 -0
  63. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/redis/producer.py +0 -0
  64. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/registry.py +0 -0
  65. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/topics.py +0 -0
  66. {core_framework-0.12.6 → core_framework-0.12.8}/core/messaging/workers.py +0 -0
  67. {core_framework-0.12.6 → core_framework-0.12.8}/core/middleware.py +0 -0
  68. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/__init__.py +0 -0
  69. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/analyzer.py +0 -0
  70. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/cli.py +0 -0
  71. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/engine.py +0 -0
  72. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/migration.py +0 -0
  73. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/operations.py +0 -0
  74. {core_framework-0.12.6 → core_framework-0.12.8}/core/migrations/state.py +0 -0
  75. {core_framework-0.12.6 → core_framework-0.12.8}/core/models.py +0 -0
  76. {core_framework-0.12.6 → core_framework-0.12.8}/core/querysets.py +0 -0
  77. {core_framework-0.12.6 → core_framework-0.12.8}/core/relations.py +0 -0
  78. {core_framework-0.12.6 → core_framework-0.12.8}/core/routing.py +0 -0
  79. {core_framework-0.12.6 → core_framework-0.12.8}/core/serializers.py +0 -0
  80. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/__init__.py +0 -0
  81. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/base.py +0 -0
  82. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/config.py +0 -0
  83. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/decorators.py +0 -0
  84. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/registry.py +0 -0
  85. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/scheduler.py +0 -0
  86. {core_framework-0.12.6 → core_framework-0.12.8}/core/tasks/worker.py +0 -0
  87. {core_framework-0.12.6 → core_framework-0.12.8}/core/tenancy.py +0 -0
  88. {core_framework-0.12.6 → core_framework-0.12.8}/core/validators.py +0 -0
  89. {core_framework-0.12.6 → core_framework-0.12.8}/core/views.py +0 -0
  90. {core_framework-0.12.6 → core_framework-0.12.8}/docs/01-quickstart.md +0 -0
  91. {core_framework-0.12.6 → core_framework-0.12.8}/docs/02-viewsets.md +0 -0
  92. {core_framework-0.12.6 → core_framework-0.12.8}/docs/03-authentication.md +0 -0
  93. {core_framework-0.12.6 → core_framework-0.12.8}/docs/04-messaging.md +0 -0
  94. {core_framework-0.12.6 → core_framework-0.12.8}/docs/05-multi-service.md +0 -0
  95. {core_framework-0.12.6 → core_framework-0.12.8}/docs/06-tasks.md +0 -0
  96. {core_framework-0.12.6 → core_framework-0.12.8}/docs/07-deployment.md +0 -0
  97. {core_framework-0.12.6 → core_framework-0.12.8}/docs/08-complete-example.md +0 -0
  98. {core_framework-0.12.6 → core_framework-0.12.8}/docs/09-settings.md +0 -0
  99. {core_framework-0.12.6 → core_framework-0.12.8}/docs/10-migrations.md +0 -0
  100. {core_framework-0.12.6 → core_framework-0.12.8}/docs/11-permissions.md +0 -0
  101. {core_framework-0.12.6 → core_framework-0.12.8}/docs/12-auth-backends.md +0 -0
  102. {core_framework-0.12.6 → core_framework-0.12.8}/docs/13-validators.md +0 -0
  103. {core_framework-0.12.6 → core_framework-0.12.8}/docs/14-querysets.md +0 -0
  104. {core_framework-0.12.6 → core_framework-0.12.8}/docs/15-routing.md +0 -0
  105. {core_framework-0.12.6 → core_framework-0.12.8}/docs/16-serializers.md +0 -0
  106. {core_framework-0.12.6 → core_framework-0.12.8}/docs/17-datetime.md +0 -0
  107. {core_framework-0.12.6 → core_framework-0.12.8}/docs/18-dependencies.md +0 -0
  108. {core_framework-0.12.6 → core_framework-0.12.8}/docs/19-views.md +0 -0
  109. {core_framework-0.12.6 → core_framework-0.12.8}/docs/20-fields.md +0 -0
  110. {core_framework-0.12.6 → core_framework-0.12.8}/docs/21-tenancy.md +0 -0
  111. {core_framework-0.12.6 → core_framework-0.12.8}/docs/22-replicas.md +0 -0
  112. {core_framework-0.12.6 → core_framework-0.12.8}/docs/23-soft-delete.md +0 -0
  113. {core_framework-0.12.6 → core_framework-0.12.8}/docs/24-relations.md +0 -0
  114. {core_framework-0.12.6 → core_framework-0.12.8}/docs/25-exceptions.md +0 -0
  115. {core_framework-0.12.6 → core_framework-0.12.8}/docs/26-choices.md +0 -0
  116. {core_framework-0.12.6 → core_framework-0.12.8}/docs/27-workers.md +0 -0
  117. {core_framework-0.12.6 → core_framework-0.12.8}/docs/28-avro.md +0 -0
  118. {core_framework-0.12.6 → core_framework-0.12.8}/docs/29-topics.md +0 -0
  119. {core_framework-0.12.6 → core_framework-0.12.8}/docs/30-changelog-0.12.2.md +0 -0
  120. {core_framework-0.12.6 → core_framework-0.12.8}/docs/31-middleware.md +0 -0
  121. {core_framework-0.12.6 → core_framework-0.12.8}/docs/32-migration-guide-0.12.2.md +0 -0
  122. {core_framework-0.12.6 → core_framework-0.12.8}/docs/33-changelog-0.12.3.md +0 -0
  123. {core_framework-0.12.6 → core_framework-0.12.8}/docs/99-faq-troubleshooting.md +0 -0
  124. {core_framework-0.12.6 → core_framework-0.12.8}/docs/GUIDE.md +0 -0
  125. {core_framework-0.12.6 → core_framework-0.12.8}/docs/README.md +0 -0
  126. {core_framework-0.12.6 → core_framework-0.12.8}/example/__init__.py +0 -0
  127. {core_framework-0.12.6 → core_framework-0.12.8}/example/app.py +0 -0
  128. {core_framework-0.12.6 → core_framework-0.12.8}/example/auth.py +0 -0
  129. {core_framework-0.12.6 → core_framework-0.12.8}/example/models.py +0 -0
  130. {core_framework-0.12.6 → core_framework-0.12.8}/example/schemas.py +0 -0
  131. {core_framework-0.12.6 → core_framework-0.12.8}/example/views.py +0 -0
  132. {core_framework-0.12.6 → core_framework-0.12.8}/libs/__init__.py +0 -0
  133. {core_framework-0.12.6 → core_framework-0.12.8}/main.py +0 -0
  134. {core_framework-0.12.6 → core_framework-0.12.8}/tests/__init__.py +0 -0
  135. {core_framework-0.12.6 → core_framework-0.12.8}/tests/conftest.py +0 -0
  136. {core_framework-0.12.6 → core_framework-0.12.8}/tests/test_models.py +0 -0
  137. {core_framework-0.12.6 → core_framework-0.12.8}/tests/test_querysets.py +0 -0
  138. {core_framework-0.12.6 → core_framework-0.12.8}/tests/test_serializers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-framework
3
- Version: 0.12.6
3
+ Version: 0.12.8
4
4
  Summary: Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema.
5
5
  Project-URL: Homepage, https://github.com/SorPuti/core-framework
6
6
  Project-URL: Documentation, https://github.com/SorPuti/core-framework#readme
@@ -56,6 +56,11 @@ Provides-Extra: rabbitmq
56
56
  Requires-Dist: aio-pika>=9.0.0; extra == 'rabbitmq'
57
57
  Provides-Extra: redis
58
58
  Requires-Dist: redis>=5.0.0; extra == 'redis'
59
+ Provides-Extra: testing
60
+ Requires-Dist: faker>=20.0.0; extra == 'testing'
61
+ Requires-Dist: httpx>=0.26.0; extra == 'testing'
62
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'testing'
63
+ Requires-Dist: pytest>=7.4.0; extra == 'testing'
59
64
  Description-Content-Type: text/markdown
60
65
 
61
66
  # Core Framework
@@ -278,7 +278,7 @@ from core.exceptions import (
278
278
  MissingDependency,
279
279
  )
280
280
 
281
- __version__ = "0.12.6"
281
+ __version__ = "0.12.8"
282
282
  __all__ = [
283
283
  # Models
284
284
  "Model",
@@ -24,12 +24,33 @@ from typing import Any, TYPE_CHECKING
24
24
  from fastapi import Depends, HTTPException, Request, status
25
25
 
26
26
  from core.permissions import Permission as PermissionBase
27
- from core.auth.helpers import get_request_user
28
27
 
29
28
  if TYPE_CHECKING:
30
29
  pass
31
30
 
32
31
 
32
+ def _get_user(request: Request) -> Any | None:
33
+ """
34
+ Get authenticated user from request.
35
+
36
+ Internal helper to avoid circular imports.
37
+ Checks both Starlette and legacy patterns.
38
+ """
39
+ # Pattern 1: request.user (Starlette AuthenticationMiddleware)
40
+ user = getattr(request, "user", None)
41
+ if user is not None:
42
+ if getattr(user, "is_authenticated", False):
43
+ if hasattr(user, "_user"):
44
+ return user._user
45
+ return user
46
+
47
+ # Pattern 2: request.state.user (legacy)
48
+ if hasattr(request, "state"):
49
+ return getattr(request.state, "user", None)
50
+
51
+ return None
52
+
53
+
33
54
  # =============================================================================
34
55
  # Classes de Permissão (para uso em ViewSets)
35
56
  # =============================================================================
@@ -71,7 +92,7 @@ class HasPermission(PermissionBase):
71
92
  request: Request,
72
93
  view: Any = None,
73
94
  ) -> bool:
74
- user = get_request_user(request)
95
+ user = _get_user(request)
75
96
 
76
97
  if user is None:
77
98
  return False
@@ -136,7 +157,7 @@ class IsInGroup(PermissionBase):
136
157
  request: Request,
137
158
  view: Any = None,
138
159
  ) -> bool:
139
- user = get_request_user(request)
160
+ user = _get_user(request)
140
161
 
141
162
  if user is None:
142
163
  return False
@@ -180,7 +201,7 @@ class IsSuperuser(PermissionBase):
180
201
  request: Request,
181
202
  view: Any = None,
182
203
  ) -> bool:
183
- user = get_request_user(request)
204
+ user = _get_user(request)
184
205
 
185
206
  if user is None:
186
207
  return False
@@ -208,7 +229,7 @@ class IsStaff(PermissionBase):
208
229
  request: Request,
209
230
  view: Any = None,
210
231
  ) -> bool:
211
- user = get_request_user(request)
232
+ user = _get_user(request)
212
233
 
213
234
  if user is None:
214
235
  return False
@@ -236,7 +257,7 @@ class IsActive(PermissionBase):
236
257
  request: Request,
237
258
  view: Any = None,
238
259
  ) -> bool:
239
- user = get_request_user(request)
260
+ user = _get_user(request)
240
261
 
241
262
  if user is None:
242
263
  return False
@@ -372,7 +393,7 @@ def login_required():
372
393
  ...
373
394
  """
374
395
  async def check(request: Request):
375
- user = get_request_user(request)
396
+ user = _get_user(request)
376
397
 
377
398
  if user is None:
378
399
  raise HTTPException(
@@ -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
@@ -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
 
@@ -15,12 +15,33 @@ from typing import Any, TYPE_CHECKING
15
15
 
16
16
  from fastapi import HTTPException, Request, status
17
17
 
18
- from core.auth.helpers import get_request_user
19
-
20
18
  if TYPE_CHECKING:
21
19
  from core.views import APIView
22
20
 
23
21
 
22
+ def _get_user(request: Request) -> Any | None:
23
+ """
24
+ Get authenticated user from request.
25
+
26
+ Internal helper to avoid circular imports.
27
+ Checks both Starlette and legacy patterns.
28
+ """
29
+ # Pattern 1: request.user (Starlette AuthenticationMiddleware)
30
+ user = getattr(request, "user", None)
31
+ if user is not None:
32
+ if getattr(user, "is_authenticated", False):
33
+ # If it's our AuthenticatedUser wrapper, return the underlying model
34
+ if hasattr(user, "_user"):
35
+ return user._user
36
+ return user
37
+
38
+ # Pattern 2: request.state.user (legacy)
39
+ if hasattr(request, "state"):
40
+ return getattr(request.state, "user", None)
41
+
42
+ return None
43
+
44
+
24
45
  class Permission(ABC):
25
46
  """
26
47
  Classe base para permissões.
@@ -34,7 +55,7 @@ class Permission(ABC):
34
55
  request: Request,
35
56
  view: APIView | None = None,
36
57
  ) -> bool:
37
- user = get_request_user(request)
58
+ user = _get_user(request)
38
59
  return user is not None and user.is_admin
39
60
  """
40
61
 
@@ -216,7 +237,7 @@ class IsAuthenticated(Permission):
216
237
  request: Request,
217
238
  view: "APIView | None" = None,
218
239
  ) -> bool:
219
- user = get_request_user(request)
240
+ user = _get_user(request)
220
241
  return user is not None
221
242
 
222
243
 
@@ -238,7 +259,7 @@ class IsAuthenticatedOrReadOnly(Permission):
238
259
  if request.method in self.SAFE_METHODS:
239
260
  return True
240
261
 
241
- user = get_request_user(request)
262
+ user = _get_user(request)
242
263
  return user is not None
243
264
 
244
265
 
@@ -252,7 +273,7 @@ class IsAdmin(Permission):
252
273
  request: Request,
253
274
  view: "APIView | None" = None,
254
275
  ) -> bool:
255
- user = get_request_user(request)
276
+ user = _get_user(request)
256
277
  if user is None:
257
278
  return False
258
279
 
@@ -287,7 +308,7 @@ class IsOwner(Permission):
287
308
  if obj is None:
288
309
  return True
289
310
 
290
- user = get_request_user(request)
311
+ user = _get_user(request)
291
312
  if user is None:
292
313
  return False
293
314
 
@@ -320,7 +341,7 @@ class HasRole(Permission):
320
341
  request: Request,
321
342
  view: "APIView | None" = None,
322
343
  ) -> bool:
323
- user = get_request_user(request)
344
+ user = _get_user(request)
324
345
  if user is None:
325
346
  return False
326
347
 
@@ -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
+ ]