core-framework 0.12.6__py3-none-any.whl → 0.12.8__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/testing/plugin.py ADDED
@@ -0,0 +1,635 @@
1
+ """
2
+ Pytest plugin for core-framework applications.
3
+
4
+ This plugin provides:
5
+ - Automatic test environment setup (no manual configuration needed)
6
+ - Isolated database with in-memory SQLite
7
+ - Mocked external services (Kafka, Redis, HTTP)
8
+ - Auto-discovery of tests
9
+ - Pre-configured fixtures for common use cases
10
+
11
+ The plugin automatically initializes:
12
+ - Database session factories
13
+ - Auth configuration
14
+ - Settings with test defaults
15
+ - Middleware registry
16
+
17
+ Usage:
18
+ # Just run pytest - everything is auto-configured!
19
+ pytest tests/
20
+
21
+ # With coverage
22
+ pytest tests/ --cov=src --cov-report=html
23
+
24
+ # For integration tests that need your app:
25
+ # Create conftest.py with app fixture
26
+
27
+ @pytest.fixture(scope="session")
28
+ def app():
29
+ from your_app.main import app
30
+ return app
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ import os
37
+ import sys
38
+ from typing import TYPE_CHECKING, Any
39
+
40
+ import pytest
41
+
42
+ if TYPE_CHECKING:
43
+ from httpx import AsyncClient
44
+ from sqlalchemy.ext.asyncio import AsyncSession
45
+ from core.testing.mocks import MockKafka, MockRedis, MockHTTP
46
+
47
+ logger = logging.getLogger("core.testing")
48
+
49
+ # Track if environment has been initialized
50
+ _environment_initialized = False
51
+
52
+
53
+ # =============================================================================
54
+ # Environment Setup (runs once before any tests)
55
+ # =============================================================================
56
+
57
+ def _setup_test_environment():
58
+ """
59
+ Setup isolated test environment.
60
+
61
+ This initializes all core-framework components with test-safe defaults,
62
+ preventing errors like "Database not initialized" or "Auth not configured".
63
+ """
64
+ global _environment_initialized
65
+
66
+ if _environment_initialized:
67
+ return
68
+
69
+ logger.info("Setting up core-framework test environment...")
70
+
71
+ # Set environment variables for test mode
72
+ os.environ.setdefault("TESTING", "true")
73
+ os.environ.setdefault("DEBUG", "true")
74
+ os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///:memory:")
75
+ os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing-only-do-not-use-in-production")
76
+
77
+ # Initialize settings with test defaults
78
+ _init_test_settings()
79
+
80
+ # Initialize database factories
81
+ _init_test_database()
82
+
83
+ # Initialize auth with test defaults
84
+ _init_test_auth()
85
+
86
+ # Clear any existing middleware
87
+ _init_test_middleware()
88
+
89
+ _environment_initialized = True
90
+ logger.info("Test environment ready")
91
+
92
+
93
+ def _init_test_settings():
94
+ """Initialize settings module with test defaults."""
95
+ try:
96
+ from core.config import Settings
97
+
98
+ # Create test settings instance
99
+ class TestSettings(Settings):
100
+ """Test settings with safe defaults."""
101
+
102
+ debug: bool = True
103
+ testing: bool = True
104
+ database_url: str = "sqlite+aiosqlite:///:memory:"
105
+ secret_key: str = "test-secret-key-for-testing-only"
106
+
107
+ class Config:
108
+ env_prefix = ""
109
+
110
+ # Override get_settings to return test settings
111
+ import core.config as config_module
112
+
113
+ _test_settings = None
114
+
115
+ def get_test_settings() -> Settings:
116
+ nonlocal _test_settings
117
+ if _test_settings is None:
118
+ _test_settings = TestSettings()
119
+ return _test_settings
120
+
121
+ config_module.get_settings = get_test_settings
122
+ config_module._settings = None # Clear cache
123
+
124
+ logger.debug("Test settings initialized")
125
+ except ImportError:
126
+ logger.debug("core.config not available, skipping settings init")
127
+ except Exception as e:
128
+ logger.debug(f"Could not initialize test settings: {e}")
129
+
130
+
131
+ def _init_test_database():
132
+ """Initialize database module with test factories."""
133
+ try:
134
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
135
+ import core.database as db_module
136
+
137
+ # Create in-memory SQLite engine
138
+ test_engine = create_async_engine(
139
+ "sqlite+aiosqlite:///:memory:",
140
+ echo=False,
141
+ connect_args={"check_same_thread": False},
142
+ )
143
+
144
+ # Create session factory
145
+ test_session_factory = async_sessionmaker(
146
+ test_engine,
147
+ class_=AsyncSession,
148
+ expire_on_commit=False,
149
+ )
150
+
151
+ # Set both read and write to same factory (no replicas in tests)
152
+ db_module._write_session_factory = test_session_factory
153
+ db_module._read_session_factory = test_session_factory
154
+
155
+ # Store engine for table creation
156
+ db_module._test_engine = test_engine
157
+
158
+ logger.debug("Test database factories initialized")
159
+ except ImportError:
160
+ logger.debug("core.database not available, skipping database init")
161
+ except Exception as e:
162
+ logger.debug(f"Could not initialize test database: {e}")
163
+
164
+
165
+ def _init_test_auth():
166
+ """Initialize auth module with test defaults."""
167
+ try:
168
+ from core.auth.base import configure_auth, AuthConfig
169
+
170
+ # Configure auth with test defaults
171
+ configure_auth(
172
+ secret_key="test-secret-key-for-testing-only",
173
+ algorithm="HS256",
174
+ access_token_expire_minutes=30,
175
+ refresh_token_expire_days=7,
176
+ warn_missing_middleware=False, # Don't warn in tests
177
+ )
178
+
179
+ logger.debug("Test auth initialized")
180
+ except ImportError:
181
+ logger.debug("core.auth not available, skipping auth init")
182
+ except Exception as e:
183
+ logger.debug(f"Could not initialize test auth: {e}")
184
+
185
+
186
+ def _init_test_middleware():
187
+ """Clear middleware registry for clean test state."""
188
+ try:
189
+ from core.middleware import clear_middleware_registry
190
+ clear_middleware_registry()
191
+ logger.debug("Middleware registry cleared")
192
+ except ImportError:
193
+ logger.debug("core.middleware not available")
194
+ except Exception as e:
195
+ logger.debug(f"Could not clear middleware: {e}")
196
+
197
+
198
+ async def _create_test_tables():
199
+ """Create all database tables for testing."""
200
+ try:
201
+ import core.database as db_module
202
+ from core.models import Model
203
+
204
+ engine = getattr(db_module, '_test_engine', None)
205
+ if engine:
206
+ async with engine.begin() as conn:
207
+ await conn.run_sync(Model.metadata.create_all)
208
+ logger.debug("Test tables created")
209
+ except Exception as e:
210
+ logger.debug(f"Could not create test tables: {e}")
211
+
212
+
213
+ async def _drop_test_tables():
214
+ """Drop all database tables after testing."""
215
+ try:
216
+ import core.database as db_module
217
+ from core.models import Model
218
+
219
+ engine = getattr(db_module, '_test_engine', None)
220
+ if engine:
221
+ async with engine.begin() as conn:
222
+ await conn.run_sync(Model.metadata.drop_all)
223
+ logger.debug("Test tables dropped")
224
+ except Exception as e:
225
+ logger.debug(f"Could not drop test tables: {e}")
226
+
227
+
228
+ # =============================================================================
229
+ # Pytest Hooks
230
+ # =============================================================================
231
+
232
+ def pytest_configure(config):
233
+ """
234
+ Configure pytest for core-framework testing.
235
+
236
+ This runs before any tests are collected.
237
+ """
238
+ # Register markers
239
+ config.addinivalue_line(
240
+ "markers",
241
+ "integration: mark as integration test (may require external services)"
242
+ )
243
+ config.addinivalue_line(
244
+ "markers",
245
+ "slow: mark as slow test"
246
+ )
247
+ config.addinivalue_line(
248
+ "markers",
249
+ "auth: mark as requiring authentication"
250
+ )
251
+ config.addinivalue_line(
252
+ "markers",
253
+ "database: mark as requiring database"
254
+ )
255
+ config.addinivalue_line(
256
+ "markers",
257
+ "unit: mark as unit test (no external dependencies)"
258
+ )
259
+
260
+ # Setup test environment immediately
261
+ _setup_test_environment()
262
+
263
+
264
+ def pytest_collection_modifyitems(config, items):
265
+ """
266
+ Modify collected test items.
267
+
268
+ Auto-discovers and marks tests based on their location/name.
269
+ """
270
+ for item in items:
271
+ # Auto-mark tests based on path
272
+ if "integration" in str(item.fspath):
273
+ item.add_marker(pytest.mark.integration)
274
+ if "unit" in str(item.fspath):
275
+ item.add_marker(pytest.mark.unit)
276
+
277
+
278
+ def pytest_sessionstart(session):
279
+ """Called after the Session object has been created."""
280
+ logger.debug("Pytest session starting")
281
+
282
+
283
+ def pytest_sessionfinish(session, exitstatus):
284
+ """Called after whole test run finished."""
285
+ logger.debug(f"Pytest session finished with status {exitstatus}")
286
+
287
+
288
+ # =============================================================================
289
+ # App Fixture
290
+ # =============================================================================
291
+
292
+ @pytest.fixture(scope="session")
293
+ def app():
294
+ """
295
+ Application instance fixture.
296
+
297
+ Override this in your conftest.py if you need to test HTTP endpoints:
298
+
299
+ @pytest.fixture(scope="session")
300
+ def app():
301
+ from your_app.main import app
302
+ return app
303
+
304
+ For unit tests that don't need HTTP, you don't need to override this.
305
+ """
306
+ # Return a minimal app for unit tests
307
+ try:
308
+ from fastapi import FastAPI
309
+ return FastAPI(title="Test App")
310
+ except ImportError:
311
+ return None
312
+
313
+
314
+ # =============================================================================
315
+ # Database Fixtures
316
+ # =============================================================================
317
+
318
+ @pytest.fixture(scope="session")
319
+ async def test_engine():
320
+ """
321
+ Database engine for the test session.
322
+
323
+ Creates tables at start, drops at end.
324
+ """
325
+ await _create_test_tables()
326
+
327
+ try:
328
+ import core.database as db_module
329
+ yield getattr(db_module, '_test_engine', None)
330
+ except ImportError:
331
+ yield None
332
+
333
+ await _drop_test_tables()
334
+
335
+
336
+ @pytest.fixture
337
+ async def db(test_engine) -> "AsyncSession":
338
+ """
339
+ Database session for tests.
340
+
341
+ Each test gets a fresh session that's rolled back after the test.
342
+
343
+ Usage:
344
+ async def test_create_user(db):
345
+ user = User(email="test@example.com")
346
+ db.add(user)
347
+ await db.commit()
348
+
349
+ assert user.id is not None
350
+ """
351
+ try:
352
+ import core.database as db_module
353
+
354
+ factory = db_module._write_session_factory
355
+ if factory is None:
356
+ pytest.skip("Database not initialized")
357
+
358
+ session = factory()
359
+ try:
360
+ yield session
361
+ await session.commit()
362
+ except Exception:
363
+ await session.rollback()
364
+ raise
365
+ finally:
366
+ await session.close()
367
+ except ImportError:
368
+ pytest.skip("core.database not available")
369
+
370
+
371
+ @pytest.fixture
372
+ async def clean_db(db) -> "AsyncSession":
373
+ """
374
+ Database session with clean tables.
375
+
376
+ Truncates all tables before the test.
377
+ """
378
+ try:
379
+ from core.models import Model
380
+ import core.database as db_module
381
+
382
+ engine = getattr(db_module, '_test_engine', None)
383
+ if engine:
384
+ async with engine.begin() as conn:
385
+ for table in reversed(Model.metadata.sorted_tables):
386
+ await conn.execute(table.delete())
387
+ except Exception:
388
+ pass
389
+
390
+ yield db
391
+
392
+
393
+ # =============================================================================
394
+ # HTTP Client Fixtures
395
+ # =============================================================================
396
+
397
+ @pytest.fixture
398
+ async def client(app, test_engine) -> "AsyncClient":
399
+ """
400
+ HTTP test client with initialized database.
401
+
402
+ Usage:
403
+ async def test_health(client):
404
+ response = await client.get("/health")
405
+ assert response.status_code == 200
406
+ """
407
+ if app is None:
408
+ pytest.skip("No app fixture defined")
409
+
410
+ try:
411
+ from httpx import AsyncClient, ASGITransport
412
+
413
+ async with AsyncClient(
414
+ transport=ASGITransport(app=app),
415
+ base_url="http://test",
416
+ follow_redirects=True,
417
+ ) as c:
418
+ yield c
419
+ except ImportError:
420
+ pytest.skip("httpx not installed")
421
+
422
+
423
+ @pytest.fixture
424
+ async def auth_client(app, test_engine) -> "AsyncClient":
425
+ """
426
+ Authenticated HTTP test client.
427
+
428
+ Creates a test user and includes auth token in requests.
429
+
430
+ Usage:
431
+ async def test_profile(auth_client):
432
+ response = await auth_client.get("/api/v1/auth/me")
433
+ assert response.status_code == 200
434
+ """
435
+ if app is None:
436
+ pytest.skip("No app fixture defined")
437
+
438
+ try:
439
+ from core.testing import AuthenticatedClient
440
+
441
+ async with AuthenticatedClient(app) as c:
442
+ yield c
443
+ except ImportError:
444
+ pytest.skip("core.testing.AuthenticatedClient not available")
445
+ except Exception as e:
446
+ pytest.skip(f"Could not create authenticated client: {e}")
447
+
448
+
449
+ @pytest.fixture
450
+ async def client_factory(app, test_engine):
451
+ """
452
+ Factory for creating multiple authenticated clients.
453
+
454
+ Usage:
455
+ async def test_two_users(client_factory):
456
+ user1 = await client_factory("user1@example.com")
457
+ user2 = await client_factory("user2@example.com")
458
+ """
459
+ if app is None:
460
+ pytest.skip("No app fixture defined")
461
+
462
+ from core.testing import AuthenticatedClient
463
+
464
+ clients = []
465
+
466
+ async def factory(
467
+ email: str = "test@example.com",
468
+ password: str = "TestPass123!",
469
+ ) -> "AsyncClient":
470
+ client = AuthenticatedClient(app, email=email, password=password)
471
+ c = await client.__aenter__()
472
+ clients.append(client)
473
+ return c
474
+
475
+ yield factory
476
+
477
+ for client in clients:
478
+ try:
479
+ await client.__aexit__(None, None, None)
480
+ except Exception:
481
+ pass
482
+
483
+
484
+ # =============================================================================
485
+ # Mock Fixtures
486
+ # =============================================================================
487
+
488
+ @pytest.fixture
489
+ def mock_kafka() -> "MockKafka":
490
+ """
491
+ Mock Kafka producer/consumer.
492
+
493
+ Usage:
494
+ async def test_event(mock_kafka):
495
+ await mock_kafka.send("events", {"type": "test"})
496
+ mock_kafka.assert_sent("events", count=1)
497
+ """
498
+ from core.testing.mocks import MockKafka
499
+
500
+ kafka = MockKafka()
501
+ yield kafka
502
+ kafka.clear()
503
+
504
+
505
+ @pytest.fixture
506
+ def mock_redis() -> "MockRedis":
507
+ """
508
+ Mock Redis client.
509
+
510
+ Usage:
511
+ async def test_cache(mock_redis):
512
+ await mock_redis.set("key", "value")
513
+ assert await mock_redis.get("key") == "value"
514
+ """
515
+ from core.testing.mocks import MockRedis
516
+
517
+ redis = MockRedis()
518
+ yield redis
519
+ redis.clear()
520
+
521
+
522
+ @pytest.fixture
523
+ def mock_http() -> "MockHTTP":
524
+ """
525
+ Mock HTTP client.
526
+
527
+ Usage:
528
+ def test_api(mock_http):
529
+ mock_http.when("GET", "https://api.example.com/data").respond(
530
+ status=200, json={"result": "ok"}
531
+ )
532
+ """
533
+ from core.testing.mocks import MockHTTP
534
+
535
+ http = MockHTTP()
536
+ yield http
537
+ http.clear()
538
+
539
+
540
+ # =============================================================================
541
+ # Utility Fixtures
542
+ # =============================================================================
543
+
544
+ @pytest.fixture
545
+ def fake():
546
+ """Faker instance for test data generation."""
547
+ from core.testing.factories import fake
548
+ return fake
549
+
550
+
551
+ @pytest.fixture
552
+ def user_factory():
553
+ """Factory for creating test users."""
554
+ from core.testing.factories import UserFactory
555
+ return UserFactory
556
+
557
+
558
+ @pytest.fixture
559
+ def assert_status():
560
+ """HTTP status assertion helper."""
561
+ from core.testing.assertions import assert_status
562
+ return assert_status
563
+
564
+
565
+ @pytest.fixture
566
+ def assert_json():
567
+ """JSON assertion helper."""
568
+ from core.testing.assertions import assert_json_contains
569
+ return assert_json_contains
570
+
571
+
572
+ @pytest.fixture
573
+ async def logged_in_user(auth_client):
574
+ """Info about the authenticated test user."""
575
+ return {
576
+ "email": "test@example.com",
577
+ "password": "TestPass123!",
578
+ }
579
+
580
+
581
+ # =============================================================================
582
+ # Settings Fixture
583
+ # =============================================================================
584
+
585
+ @pytest.fixture
586
+ def settings():
587
+ """
588
+ Test settings instance.
589
+
590
+ Usage:
591
+ def test_config(settings):
592
+ assert settings.testing == True
593
+ """
594
+ try:
595
+ from core.config import get_settings
596
+ return get_settings()
597
+ except ImportError:
598
+ return None
599
+
600
+
601
+ @pytest.fixture
602
+ def override_settings():
603
+ """
604
+ Context manager to temporarily override settings.
605
+
606
+ Usage:
607
+ def test_with_custom_setting(override_settings):
608
+ with override_settings(debug=False):
609
+ # Test with debug=False
610
+ pass
611
+ """
612
+ from contextlib import contextmanager
613
+
614
+ @contextmanager
615
+ def _override(**overrides):
616
+ try:
617
+ from core.config import get_settings
618
+ settings = get_settings()
619
+
620
+ # Store original values
621
+ original = {}
622
+ for key, value in overrides.items():
623
+ if hasattr(settings, key):
624
+ original[key] = getattr(settings, key)
625
+ setattr(settings, key, value)
626
+
627
+ yield settings
628
+
629
+ # Restore original values
630
+ for key, value in original.items():
631
+ setattr(settings, key, value)
632
+ except ImportError:
633
+ yield None
634
+
635
+ return _override
@@ -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