File size: 9,839 Bytes
27c799c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# fashionistar_backend/conftest.py
"""
Fashionistar — Root pytest conftest.

Provides shared fixtures for the entire test suite.
Import order: fixtures defined here are available in ALL test modules.

Fixture scopes:
  - session: Created once per test session (expensive setup: DB, test users)
  - module:  Created once per test module
  - function: Created fresh for every test (default, most common)

Usage pattern:
    def test_something(api_client, registered_user):
        response = api_client.post('/api/v1/auth/login/', {...})
        assert response.status_code == 200
"""
import pytest
from rest_framework.test import APIClient


# ─────────────────────────────────────────────────────────────────────────────
#  REDIS AVAILABILITY CHECK (session-scoped — probed once per test run)
# ─────────────────────────────────────────────────────────────────────────────

def _probe_redis() -> bool:
    """
    Returns True if a live Redis instance is reachable on 127.0.0.1:6379.

    Used by pytest_collection_modifyitems to auto-skip @pytest.mark.redis tests
    when Redis is unavailable (CI without Redis, local dev without Docker, etc.)
    """
    try:
        import redis as _redis
        client = _redis.Redis(host="127.0.0.1", port=6379, socket_connect_timeout=1)
        client.ping()
        return True
    except Exception:
        return False


# Probe once at import time so the skip decision is consistent for all tests.
_REDIS_AVAILABLE = _probe_redis()


def pytest_collection_modifyitems(config, items):
    """
    Auto-skip tests marked with @pytest.mark.redis when Redis is unavailable.

    This hook runs after collection so every test module has already been
    imported — no circular-import risk.
    """
    if _REDIS_AVAILABLE:
        return  # Redis is up — let all tests run normally

    skip_redis = pytest.mark.skip(
        reason="Redis unavailable (127.0.0.1:6379 unreachable) — skipping @pytest.mark.redis tests"
    )
    for item in items:
        if "redis" in item.keywords:
            item.add_marker(skip_redis)


# ─────────────────────────────────────────────────────────────────────────────
#  CORE FIXTURES
# ─────────────────────────────────────────────────────────────────────────────

@pytest.fixture
def api_client():
    """
    Unauthenticated DRF APIClient.
    Use for testing public endpoints (register, login, verify-otp, etc.)
    """
    return APIClient()


@pytest.fixture
def auth_api_client(registered_verified_user):
    """
    Authenticated APIClient: JWT access token pre-set in Authorization header.
    Use for testing endpoints that require IsAuthenticated.
    """
    from rest_framework_simplejwt.tokens import RefreshToken
    client = APIClient()
    user = registered_verified_user
    refresh = RefreshToken.for_user(user)
    client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
    return client


# ─────────────────────────────────────────────────────────────────────────────
#  UNIFIEDUSER FIXTURES
# ─────────────────────────────────────────────────────────────────────────────

@pytest.fixture
def unverified_user_data():
    """Dict of valid registration payload (email-based)."""
    return {
        'email': 'testuser@fashionistar.io',
        'password': 'SecurePass123!@#',
        'password2': 'SecurePass123!@#',
        'role': 'client',
    }


@pytest.fixture
def vendor_registration_data():
    """Dict of valid vendor registration payload (email-based)."""
    return {
        'email': 'vendor@fashionistar.io',
        'password': 'SecureVendor456!@#',
        'password2': 'SecureVendor456!@#',
        'role': 'vendor',
    }


@pytest.fixture
def phone_registration_data():
    """Dict of valid phone-based registration payload."""
    return {
        'phone': '+2348012345678',
        'password': 'SecurePhone789!@#',
        'password2': 'SecurePhone789!@#',
        'role': 'client',
    }


@pytest.fixture
@pytest.mark.django_db
def registered_user(db):
    """
    UnifiedUser: created, is_active=False, is_verified=False.
    Simulates a user who just registered but hasn't verified OTP yet.
    """
    from apps.authentication.models import UnifiedUser
    user = UnifiedUser.objects.create_user(
        email='registered@fashionistar.io',
        password='SecurePass123!@#',
        role='client',
        is_active=False,
        is_verified=False,
    )
    return user


@pytest.fixture
@pytest.mark.django_db
def registered_verified_user(db):
    """
    UnifiedUser: created, is_active=True, is_verified=True.
    Simulates a fully onboarded user. Use for authenticated endpoint tests.
    """
    from apps.authentication.models import UnifiedUser
    user = UnifiedUser.objects.create_user(
        email='verified@fashionistar.io',
        password='SecurePass123!@#',
        role='client',
        is_active=True,
        is_verified=True,
    )
    return user


@pytest.fixture
@pytest.mark.django_db
def vendor_user(db):
    """Active, verified vendor UnifiedUser."""
    from apps.authentication.models import UnifiedUser
    user = UnifiedUser.objects.create_user(
        email='vendor@fashionistar.io',
        password='VendorPass456!@#',
        role='vendor',
        is_active=True,
        is_verified=True,
    )
    return user


# ─────────────────────────────────────────────────────────────────────────────
#  REDIS MOCK FIXTURE
# ─────────────────────────────────────────────────────────────────────────────

@pytest.fixture(autouse=True)
def mock_redis(mocker):
    """
    Replace get_redis_connection_safe and redis.from_url globally with in-memory mocks
    when a live Redis is not available, preventing tests from failing on health checks.
    """
    mock_client = mocker.MagicMock()
    mock_client.exists.return_value = True
    mock_client.get.return_value = b'mockdata'
    mock_client.set.return_value = True
    mock_client.delete.return_value = True
    mock_client.setex.return_value = True
    mock_client.ping.return_value = True

    # Suppress outbound Redis connectivity globally for tests
    mocker.patch(
        'apps.common.utils.get_redis_connection_safe',
        return_value=mock_client,
    )
    
    if not _REDIS_AVAILABLE:
        mocker.patch(
            'redis.from_url',
            return_value=mock_client,
        )
        mocker.patch(
            'redis.Redis.from_url',
            return_value=mock_client,
        )
    return mock_client


# ─────────────────────────────────────────────────────────────────────────────
#  EMAIL MOCK FIXTURE
# ─────────────────────────────────────────────────────────────────────────────

@pytest.fixture
def mock_email(mocker):
    """
    Suppress email sending in tests (patches EmailManager.send_mail).
    Returns the mock so tests can assert on call count / args.
    """
    return mocker.patch('apps.common.managers.email.EmailManager.send_mail')


@pytest.fixture
def mock_sms(mocker):
    """Suppress SMS sending in tests (patches SMSManager.send_sms)."""
    return mocker.patch('apps.common.managers.sms.SMSManager.send_sms')


@pytest.fixture(autouse=True)
def mock_cloudinary(mocker):
    """
    Mock all Cloudinary API endpoints globally to avoid outbound requests and NotFound exceptions.
    """
    mock_upload = mocker.patch('cloudinary.uploader.upload')
    mock_upload.return_value = {
        'public_id': 'test_public_id',
        'url': 'http://res.cloudinary.com/demo/image/upload/v1234/test_public_id.jpg',
        'secure_url': 'https://res.cloudinary.com/demo/image/upload/v1234/test_public_id.jpg',
        'format': 'jpg',
        'resource_type': 'image',
    }

    mock_explicit = mocker.patch('cloudinary.uploader.explicit')
    mock_explicit.return_value = {
        'public_id': 'test_public_id',
        'url': 'http://res.cloudinary.com/demo/image/upload/v1234/test_public_id.jpg',
        'secure_url': 'https://res.cloudinary.com/demo/image/upload/v1234/test_public_id.jpg',
        'format': 'jpg',
        'resource_type': 'image',
    }

    mock_destroy = mocker.patch('cloudinary.uploader.destroy')
    mock_destroy.return_value = {
        'result': 'ok',
    }

    return {
        'upload': mock_upload,
        'explicit': mock_explicit,
        'destroy': mock_destroy,
    }