MukeshKapoor25 commited on
Commit
7611990
Β·
1 Parent(s): 79ca9ba

test(performance): Add comprehensive test suite for performance optimization

Browse files

- Add multiple test files for performance testing
- Create pytest configuration and test runner
- Implement test cases for API endpoints, database, and integration scenarios
- Add performance, regression, and security test suites
- Remove legacy performance documentation files
- Establish structured testing framework for application
Rationale: Improve test coverage and validate performance optimization strategies across different system components

PERFORMANCE_OPTIMIZATION.md DELETED
@@ -1,410 +0,0 @@
1
- # πŸš€ Performance Optimization Implementation - ALL ISSUES RESOLVED
2
-
3
- ## πŸŽ‰ **PERFORMANCE ISSUES FULLY ADDRESSED**
4
-
5
- All identified performance bottlenecks have been comprehensively resolved with enterprise-grade optimizations.
6
-
7
- ---
8
-
9
- ## βœ… **RESOLVED PERFORMANCE ISSUES**
10
-
11
- ### 1. **βœ… Inefficient Database Queries - COMPLETE**
12
-
13
- - **Issue**: Complex aggregation pipelines without proper indexing strategy
14
- - **Impact**: High - slow query execution times
15
- - **Solution**: Comprehensive indexing strategy and query optimization
16
- - **Status**: **FULLY IMPLEMENTED & TESTED**
17
-
18
- **Implementation:**
19
-
20
- - 15+ compound indexes for optimal query performance
21
- - Automatic pipeline stage reordering ($match first)
22
- - Index hints for complex queries
23
- - Query complexity analysis and recommendations
24
-
25
- ### 2. **βœ… Memory-Intensive Operations - COMPLETE**
26
-
27
- - **Issue**: Large result sets loaded into memory without streaming
28
- - **Impact**: High - memory exhaustion risk
29
- - **Solution**: Cursor-based pagination and streaming aggregation
30
- - **Status**: **FULLY IMPLEMENTED & TESTED**
31
-
32
- **Implementation:**
33
-
34
- - Cursor-based pagination for large result sets
35
- - Streaming aggregation with configurable batch sizes
36
- - Memory usage monitoring and limits
37
- - Automatic fallback for memory-intensive operations
38
-
39
- ### 3. **βœ… Synchronous Operations in Async Context - COMPLETE**
40
-
41
- - **Issue**: Blocking operations in async functions
42
- - **Impact**: Medium - reduced concurrency
43
- - **Solution**: Proper async patterns with thread pool management
44
- - **Status**: **FULLY IMPLEMENTED & TESTED**
45
-
46
- **Implementation:**
47
-
48
- - Async spaCy model loading with caching
49
- - Thread pool executor with proper resource management
50
- - Timeout handling for long-running operations
51
- - Graceful shutdown and cleanup procedures
52
-
53
- ### 4. **βœ… Inefficient Caching Strategy - COMPLETE**
54
-
55
- - **Issue**: Cache keys not optimized, potential cache stampede
56
- - **Impact**: Medium - reduced cache effectiveness
57
- - **Solution**: Multi-level caching with warming and optimization
58
- - **Status**: **FULLY IMPLEMENTED & TESTED**
59
-
60
- **Implementation:**
61
-
62
- - L1 (memory) + L2 (Redis) caching architecture
63
- - Automatic cache warming before expiry
64
- - Optimized cache key generation with hashing
65
- - Cache performance monitoring and statistics
66
-
67
- ### 5. **βœ… Resource Leaks - COMPLETE**
68
-
69
- - **Issue**: Database connections and thread pools not properly managed
70
- - **Impact**: Medium - resource exhaustion over time
71
- - **Solution**: Comprehensive resource management and cleanup
72
- - **Status**: **FULLY IMPLEMENTED & TESTED**
73
-
74
- **Implementation:**
75
-
76
- - Proper thread pool executor shutdown
77
- - Database connection pooling and health checks
78
- - Automatic resource cleanup on application shutdown
79
- - Memory leak prevention and monitoring
80
-
81
- ---
82
-
83
- ## πŸ›‘οΈ **COMPREHENSIVE PERFORMANCE FEATURES**
84
-
85
- ### **Database Optimization:**
86
-
87
- ```python
88
- βœ… 15+ compound indexes for optimal performance
89
- βœ… Automatic query pipeline optimization
90
- βœ… Index usage statistics and monitoring
91
- βœ… Collection-specific optimization recommendations
92
- βœ… Query complexity analysis and hints
93
- βœ… Memory-efficient aggregation operations
94
- ```
95
-
96
- ### **Caching Optimization:**
97
-
98
- ```python
99
- βœ… Multi-level L1/L2 caching architecture
100
- βœ… Automatic cache warming and preloading
101
- βœ… Optimized cache key generation
102
- βœ… Cache performance monitoring
103
- βœ… Pattern-based cache invalidation
104
- βœ… Memory-efficient local cache with LRU eviction
105
- ```
106
-
107
- ### **Memory Management:**
108
-
109
- ```python
110
- βœ… Cursor-based pagination for large datasets
111
- βœ… Streaming aggregation with batch processing
112
- βœ… Memory usage monitoring and limits
113
- βœ… Automatic garbage collection optimization
114
- βœ… Resource leak prevention
115
- βœ… Configurable memory thresholds
116
- ```
117
-
118
- ### **Async Optimization:**
119
-
120
- ```python
121
- βœ… Proper async/await patterns throughout
122
- βœ… Thread pool management with timeouts
123
- βœ… Non-blocking model loading and caching
124
- βœ… Concurrent task execution where possible
125
- βœ… Graceful error handling and recovery
126
- βœ… Resource cleanup and shutdown procedures
127
- ```
128
-
129
- ---
130
-
131
- ## πŸ§ͺ **PERFORMANCE IMPROVEMENTS ACHIEVED**
132
-
133
- ### **Database Query Performance:**
134
-
135
- ```
136
- Before: Average query time 2.5s, 60% slow queries
137
- After: Average query time 0.3s, 5% slow queries
138
- Improvement: 8x faster queries, 92% reduction in slow queries
139
- ```
140
-
141
- ### **Memory Usage:**
142
-
143
- ```
144
- Before: 500MB+ memory usage, frequent OOM errors
145
- After: 150MB average usage, no memory issues
146
- Improvement: 70% memory reduction, 100% stability
147
- ```
148
-
149
- ### **Cache Performance:**
150
-
151
- ```
152
- Before: 30% hit rate, no warming, frequent misses
153
- After: 85% hit rate, automatic warming, optimized keys
154
- Improvement: 183% hit rate increase, 60% faster responses
155
- ```
156
-
157
- ### **Concurrency:**
158
-
159
- ```
160
- Before: Blocking operations, reduced throughput
161
- After: Full async support, 5x concurrent requests
162
- Improvement: 500% throughput increase
163
- ```
164
-
165
- ---
166
-
167
- ## πŸ“Š **PERFORMANCE METRICS**
168
-
169
- | Performance Aspect | Before | After | Improvement |
170
- | ------------------- | -------- | --------- | --------------- |
171
- | Query Speed | 2.5s avg | 0.3s avg | 8x faster |
172
- | Memory Usage | 500MB+ | 150MB avg | 70% reduction |
173
- | Cache Hit Rate | 30% | 85% | 183% increase |
174
- | Concurrent Requests | 10/s | 50/s | 500% increase |
175
- | Error Rate | 15% | <1% | 94% reduction |
176
- | Resource Leaks | Frequent | None | 100% eliminated |
177
-
178
- **Overall Performance Score: 95/100** ⭐⭐⭐⭐⭐
179
-
180
- ---
181
-
182
- ## πŸ”§ **IMPLEMENTATION DETAILS**
183
-
184
- ### **1. Database Indexing Strategy:**
185
-
186
- ```python
187
- # Compound indexes for optimal performance
188
- {
189
- "keys": [("location_id", 1), ("merchant_category", 1), ("go_live_from", -1)],
190
- "name": "location_category_golive_idx"
191
- }
192
-
193
- # Geospatial index for location queries
194
- {
195
- "keys": [("address.location", "2dsphere")],
196
- "name": "geo_location_idx"
197
- }
198
-
199
- # Rating and popularity indexes
200
- {
201
- "keys": [("average_rating.value", -1), ("stats.total_bookings", -1)],
202
- "name": "popularity_rating_idx"
203
- }
204
- ```
205
-
206
- ### **2. Query Optimization:**
207
-
208
- ```python
209
- # Automatic pipeline optimization
210
- def optimize_pipeline(pipeline):
211
- # Move $match stages to beginning
212
- # Combine multiple $match stages
213
- # Add index hints for complex queries
214
- # Optimize stage ordering
215
- return optimized_pipeline
216
-
217
- # Memory-efficient execution
218
- async def execute_with_cursor(collection, pipeline, limit):
219
- cursor = collection.aggregate(pipeline, batchSize=100)
220
- results = []
221
- async for doc in cursor:
222
- results.append(doc)
223
- if len(results) >= limit:
224
- break
225
- return results
226
- ```
227
-
228
- ### **3. Multi-Level Caching:**
229
-
230
- ```python
231
- # L1 (Memory) + L2 (Redis) architecture
232
- class OptimizedCacheManager:
233
- def __init__(self):
234
- self.local_cache = {} # L1 cache
235
- self.redis_client = redis_client # L2 cache
236
-
237
- async def get_or_set_cache(self, key, fetch_func):
238
- # Check L1 cache first
239
- if key in self.local_cache:
240
- return self.local_cache[key]
241
-
242
- # Check L2 cache
243
- cached = await self.redis_client.get(key)
244
- if cached:
245
- data = json.loads(cached)
246
- self.local_cache[key] = data # Store in L1
247
- return data
248
-
249
- # Fetch and cache
250
- data = await fetch_func()
251
- await self._store_in_both_caches(key, data)
252
- return data
253
- ```
254
-
255
- ### **4. Async Resource Management:**
256
-
257
- ```python
258
- # Proper async model loading
259
- class AsyncNLPProcessor:
260
- async def get_nlp_model(self):
261
- if self._nlp_model is None:
262
- async with self._model_lock:
263
- if self._nlp_model is None:
264
- loop = asyncio.get_event_loop()
265
- self._nlp_model = await loop.run_in_executor(
266
- self.executor, self._load_spacy_model
267
- )
268
- return self._nlp_model
269
-
270
- async def cleanup(self):
271
- self._shutdown = True
272
- if self.executor:
273
- self.executor.shutdown(wait=True)
274
- self._nlp_model = None
275
- ```
276
-
277
- ---
278
-
279
- ## πŸš€ **API ENDPOINTS FOR MONITORING**
280
-
281
- ### **Performance Monitoring:**
282
-
283
- - `GET /api/v1/performance/database-indexes` - Index usage statistics
284
- - `GET /api/v1/performance/cache-stats` - Cache performance metrics
285
- - `GET /api/v1/performance/memory-usage` - Memory usage statistics
286
- - `GET /api/v1/performance/comprehensive-report` - Full performance report
287
-
288
- ### **Optimization Controls:**
289
-
290
- - `POST /api/v1/performance/create-indexes` - Create/recreate indexes
291
- - `POST /api/v1/performance/invalidate-cache` - Cache invalidation
292
- - `POST /api/v1/performance/optimize-collection` - Collection optimization
293
- - `GET /api/v1/performance/slow-queries` - Slow query analysis
294
-
295
- ---
296
-
297
- ## πŸ“‹ **PERFORMANCE CHECKLIST - ALL COMPLETE**
298
-
299
- ### **Database Performance:**
300
-
301
- - [x] Compound indexes on all frequently queried fields
302
- - [x] Geospatial indexes for location-based queries
303
- - [x] Text indexes for search functionality
304
- - [x] Query pipeline optimization
305
- - [x] Index usage monitoring
306
- - [x] Collection statistics and recommendations
307
-
308
- ### **Memory Management:**
309
-
310
- - [x] Cursor-based pagination implementation
311
- - [x] Streaming aggregation for large datasets
312
- - [x] Memory usage monitoring and limits
313
- - [x] Automatic garbage collection optimization
314
- - [x] Resource leak prevention
315
- - [x] Configurable memory thresholds
316
-
317
- ### **Caching Strategy:**
318
-
319
- - [x] Multi-level L1/L2 caching architecture
320
- - [x] Automatic cache warming before expiry
321
- - [x] Optimized cache key generation
322
- - [x] Cache performance monitoring
323
- - [x] Pattern-based invalidation
324
- - [x] LRU eviction for memory management
325
-
326
- ### **Async Operations:**
327
-
328
- - [x] Proper async/await patterns
329
- - [x] Thread pool management
330
- - [x] Non-blocking model loading
331
- - [x] Timeout handling
332
- - [x] Resource cleanup procedures
333
- - [x] Graceful shutdown implementation
334
-
335
- ---
336
-
337
- ## 🎯 **PERFORMANCE MONITORING DASHBOARD**
338
-
339
- ### **Real-time Metrics:**
340
-
341
- - Database query performance (avg: 0.3s)
342
- - Cache hit rate (85%+)
343
- - Memory usage (150MB avg)
344
- - Concurrent request handling (50/s)
345
- - Error rates (<1%)
346
-
347
- ### **Automated Alerts:**
348
-
349
- - Slow query detection (>1s)
350
- - High memory usage (>80%)
351
- - Low cache hit rate (<70%)
352
- - Database connection issues
353
- - Resource leak detection
354
-
355
- ---
356
-
357
- ## πŸ† **ACHIEVEMENT SUMMARY**
358
-
359
- βœ… **ALL PERFORMANCE ISSUES RESOLVED**
360
- βœ… **8X QUERY PERFORMANCE IMPROVEMENT**
361
- βœ… **70% MEMORY USAGE REDUCTION**
362
- βœ… **500% THROUGHPUT INCREASE**
363
- βœ… **COMPREHENSIVE MONITORING IMPLEMENTED**
364
- βœ… **ZERO RESOURCE LEAKS**
365
-
366
- **The application now delivers enterprise-grade performance with comprehensive monitoring and optimization capabilities.**
367
-
368
- ---
369
-
370
- ## πŸš€ **QUICK START GUIDE**
371
-
372
- ### **1. Initialize Performance Optimizations:**
373
-
374
- ```bash
375
- # Application automatically creates indexes on startup
376
- # Monitor startup logs for optimization status
377
- ```
378
-
379
- ### **2. Monitor Performance:**
380
-
381
- ```bash
382
- # Check comprehensive performance report
383
- curl http://localhost:8000/api/v1/performance/comprehensive-report
384
-
385
- # Monitor cache performance
386
- curl http://localhost:8000/api/v1/performance/cache-stats
387
-
388
- # Check database indexes
389
- curl http://localhost:8000/api/v1/performance/database-indexes
390
- ```
391
-
392
- ### **3. Optimize as Needed:**
393
-
394
- ```bash
395
- # Create/recreate indexes
396
- curl -X POST http://localhost:8000/api/v1/performance/create-indexes
397
-
398
- # Invalidate cache
399
- curl -X POST "http://localhost:8000/api/v1/performance/invalidate-cache?pattern=merchants:*"
400
-
401
- # Optimize specific collection
402
- curl -X POST "http://localhost:8000/api/v1/performance/optimize-collection?collection_name=merchants"
403
- ```
404
-
405
- ---
406
-
407
- _Performance optimization completed on: $(date)_
408
- _All optimizations active: βœ…_
409
- _Performance score: 95/100_
410
- _Production ready: βœ…_
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
SECURITY_IMPROVEMENTS.md DELETED
@@ -1,274 +0,0 @@
1
- # Security Improvements Implementation - FIXED
2
-
3
- ## Overview
4
- This document outlines the comprehensive security improvements implemented to address input sanitization and sensitive data logging vulnerabilities. **All issues have been resolved and tested.**
5
-
6
- ## 🚨 Critical Fixes Applied
7
- - βœ… **Regex Error Fixed**: Resolved invalid group reference error in log sanitizer
8
- - βœ… **Circular Dependency Fixed**: Created simple log sanitizer to avoid middleware issues
9
- - βœ… **Input Validation Working**: All dangerous patterns now properly detected and blocked
10
- - βœ… **Log Sanitization Working**: Sensitive data properly redacted in all logs
11
-
12
- ## πŸ”’ Input Sanitization Implementation
13
-
14
- ### 1. InputSanitizer Class (`app/utils/input_sanitizer.py`)
15
- - **Comprehensive input validation** for all data types
16
- - **Pattern-based detection** of dangerous content (SQL injection, XSS, etc.)
17
- - **Field-specific sanitization** for location IDs, merchant IDs, coordinates
18
- - **Length limits** and **character validation**
19
- - **HTML escaping** and **tag stripping** using bleach library
20
-
21
- #### Key Features:
22
- - Validates location IDs with regex pattern `^[A-Z]{2}-[A-Z0-9]+$`
23
- - Sanitizes coordinates with proper range validation (-90 to 90 for lat, -180 to 180 for lng)
24
- - Limits pagination parameters (max 100 items, max 10000 offset)
25
- - Detects dangerous patterns like `$where`, `javascript:`, `<script>`, etc.
26
-
27
- ### 2. Security Middleware (`app/middleware/security_middleware.py`)
28
- - **Request size limiting** (10MB default)
29
- - **Rate limiting** with sliding window algorithm
30
- - **Security headers** injection
31
- - **Client IP extraction** with proxy support
32
- - **Request logging** with sanitization
33
-
34
- #### Rate Limiting Rules:
35
- - `/api/v1/merchants/ads`: 100 requests/minute
36
- - `/api/v1/merchants/recommended-merchants`: 50 requests/minute
37
- - `/api/v1/nlp/analyze-query`: 20 requests/minute
38
- - Default: 60 requests/minute
39
-
40
- ### 3. Enhanced Pydantic Models (`app/models/merchant.py`)
41
- - **Custom validators** for all input fields
42
- - **Pattern matching** for business names and IDs
43
- - **Dangerous content detection** in free text
44
- - **Service validation** with length limits
45
-
46
- ## πŸ” Log Sanitization Implementation
47
-
48
- ### 1. LogSanitizer Class (`app/utils/log_sanitizer.py`)
49
- - **Automatic redaction** of sensitive fields
50
- - **Pattern-based sanitization** for credentials, tokens, emails
51
- - **Recursive sanitization** for nested objects
52
- - **Length limiting** to prevent log flooding
53
- - **MongoDB-specific** query sanitization
54
-
55
- #### Sensitive Data Patterns:
56
- - Database connection strings (MongoDB, Redis)
57
- - API keys and tokens
58
- - Email addresses (partial redaction)
59
- - Phone numbers
60
- - Credit card numbers
61
- - IP addresses (partial redaction)
62
-
63
- ### 2. SimpleSanitizedLogger Wrapper
64
- - **Drop-in replacement** for standard Python logger
65
- - **Automatic sanitization** of all log messages with fallback protection
66
- - **Preserves log levels** and formatting
67
- - **Error-resistant** with graceful degradation
68
- - **No circular dependencies** - safe for middleware use
69
-
70
- ### 3. Utility Functions
71
- - `log_query_safely()` - Safe database query logging
72
- - `log_user_action_safely()` - Safe user action logging
73
- - `log_api_request_safely()` - Safe API request logging
74
-
75
- ## πŸ›‘οΈ Security Configuration (`app/config/security_config.py`)
76
-
77
- ### Key Settings:
78
- - **CORS origins**: Environment-controlled (no more `allow_origins=["*"]`)
79
- - **Request size limits**: 10MB default
80
- - **Rate limiting**: Configurable per endpoint
81
- - **Security headers**: Comprehensive set including CSP, HSTS
82
- - **Input validation patterns**: Centralized regex patterns
83
-
84
- ## πŸ“ Implementation Details
85
-
86
- ### 1. Router Updates
87
- All API endpoints now include:
88
- ```python
89
- # Input sanitization
90
- location_id = InputSanitizer.sanitize_location_id(location_id)
91
- merchant_id = InputSanitizer.sanitize_merchant_id(merchant_id)
92
-
93
- # Safe logging
94
- log_api_request_safely(logger.logger, "/endpoint", sanitized_params)
95
- ```
96
-
97
- ### 2. Service Layer Updates
98
- Database operations now use:
99
- ```python
100
- # Safe query logging
101
- log_query_safely(logger.logger, "collection", criteria, pipeline)
102
-
103
- # Sanitized error messages
104
- logger.error("Operation failed") # No sensitive data exposed
105
- ```
106
-
107
- ### 3. Database Repository Updates
108
- All database operations include:
109
- - Sanitized query logging
110
- - Error message sanitization
111
- - Performance monitoring with safe logging
112
-
113
- ## πŸš€ Usage Examples
114
-
115
- ### Input Sanitization
116
- ```python
117
- from app.utils.input_sanitizer import InputSanitizer
118
-
119
- # Sanitize location ID
120
- location_id = InputSanitizer.sanitize_location_id("in-south") # Returns "IN-SOUTH"
121
-
122
- # Sanitize coordinates
123
- lat, lng = InputSanitizer.sanitize_coordinates(13.0827, 80.2707)
124
-
125
- # Sanitize pagination
126
- limit, offset = InputSanitizer.sanitize_pagination(10, 0)
127
- ```
128
-
129
- ### Log Sanitization
130
- ```python
131
- from app.utils.log_sanitizer import get_sanitized_logger, log_query_safely
132
-
133
- logger = get_sanitized_logger(__name__)
134
-
135
- # Safe logging
136
- log_query_safely(logger.logger, "merchants", {"location_id": "IN-SOUTH"})
137
-
138
- # Automatic sanitization
139
- logger.info("User data: %s", {"email": "user@example.com", "password": "secret"})
140
- # Logs: "User data: {'email': 'user***@example.com', 'password': '[REDACTED]'}"
141
- ```
142
-
143
- ## πŸ”§ Configuration
144
-
145
- ### Environment Variables
146
- ```bash
147
- # CORS configuration
148
- ALLOWED_ORIGINS=https://yourdomain.com,https://api.yourdomain.com
149
-
150
- # Rate limiting
151
- RATE_LIMIT_RPM=60
152
- MAX_REQUEST_SIZE=10485760
153
-
154
- # Security features
155
- LOG_SANITIZATION_ENABLED=true
156
- MAX_LOG_VALUE_LENGTH=500
157
- ```
158
-
159
- ### Security Headers Added
160
- - `X-Content-Type-Options: nosniff`
161
- - `X-Frame-Options: DENY`
162
- - `X-XSS-Protection: 1; mode=block`
163
- - `Strict-Transport-Security: max-age=31536000; includeSubDomains`
164
- - `Content-Security-Policy: default-src 'self'`
165
-
166
- ## βœ… Security Benefits
167
-
168
- ### Input Sanitization Benefits:
169
- 1. **Prevents injection attacks** (SQL, NoSQL, XSS)
170
- 2. **Validates data integrity** before processing
171
- 3. **Standardizes input formats** (uppercase location IDs, etc.)
172
- 4. **Prevents buffer overflow** with length limits
173
- 5. **Blocks dangerous patterns** automatically
174
-
175
- ### Log Sanitization Benefits:
176
- 1. **Prevents credential exposure** in logs
177
- 2. **Complies with privacy regulations** (GDPR, CCPA)
178
- 3. **Reduces security audit risks**
179
- 4. **Maintains debugging capability** without sensitive data
180
- 5. **Prevents log-based attacks**
181
-
182
- ## πŸ§ͺ Testing
183
-
184
- ### Input Validation Tests
185
- ```python
186
- # Test dangerous input
187
- try:
188
- InputSanitizer.sanitize_string("'; DROP TABLE users; --")
189
- except ValueError as e:
190
- print("Blocked dangerous input")
191
-
192
- # Test coordinate validation
193
- lat, lng = InputSanitizer.sanitize_coordinates(91.0, 181.0) # Raises ValueError
194
- ```
195
-
196
- ### Log Sanitization Tests
197
- ```python
198
- from app.utils.log_sanitizer import LogSanitizer
199
-
200
- # Test credential redaction
201
- data = {"password": "secret123", "api_key": "abc123def456"}
202
- sanitized = LogSanitizer.sanitize_dict(data)
203
- # Result: {"password": "[REDACTED]", "api_key": "[REDACTED]"}
204
- ```
205
-
206
- ## πŸ“Š Performance Impact
207
-
208
- ### Input Sanitization:
209
- - **Minimal overhead**: ~1-2ms per request
210
- - **Cached regex patterns**: Compiled once, reused
211
- - **Early validation**: Fails fast on invalid input
212
-
213
- ### Log Sanitization:
214
- - **Lazy evaluation**: Only processes when logging
215
- - **Pattern caching**: Compiled regex patterns
216
- - **Configurable depth**: Prevents infinite recursion
217
-
218
- ## πŸ”„ Migration Guide
219
-
220
- ### For Existing Endpoints:
221
- 1. Import sanitization utilities
222
- 2. Add input validation before processing
223
- 3. Replace direct logging with sanitized logging
224
- 4. Update error handling to avoid data exposure
225
-
226
- ### For New Endpoints:
227
- 1. Use `InputSanitizer` for all user inputs
228
- 2. Use `get_sanitized_logger()` instead of `logging.getLogger()`
229
- 3. Apply security middleware automatically
230
- 4. Follow validation patterns in existing code
231
-
232
- ## 🎯 Next Steps
233
-
234
- ### Recommended Improvements:
235
- 1. **Add authentication middleware** for protected endpoints
236
- 2. **Implement API key validation** for external access
237
- 3. **Add request signing** for critical operations
238
- 4. **Set up security monitoring** and alerting
239
- 5. **Regular security audits** and penetration testing
240
-
241
- ### Monitoring:
242
- 1. **Track blocked requests** from input validation
243
- 2. **Monitor rate limiting** effectiveness
244
- 3. **Audit log sanitization** coverage
245
- 4. **Performance impact** measurement
246
-
247
- This implementation provides comprehensive protection against the identified security vulnerabilities while maintaining application performance and functionality.
248
- ## πŸ”§
249
- Final Implementation Status
250
-
251
- ### βœ… Successfully Implemented:
252
- 1. **Input Sanitization** - All endpoints now validate and sanitize inputs
253
- 2. **Log Sanitization** - All sensitive data redacted from logs
254
- 3. **CORS Security** - Fixed to use environment-controlled origins
255
- 4. **Request Validation** - Comprehensive parameter validation
256
- 5. **Error Handling** - Safe error messages without data exposure
257
-
258
- ### πŸ§ͺ Tested and Verified:
259
- - βœ… Location ID sanitization: `"in-south"` β†’ `"IN-SOUTH"`
260
- - βœ… Dangerous input blocked: SQL injection patterns detected
261
- - βœ… Coordinate validation: Invalid ranges rejected
262
- - βœ… Password redaction: `"secret123"` β†’ `"[REDACTED]"`
263
- - βœ… Connection string sanitization: MongoDB URIs protected
264
- - βœ… Pagination limits: Large values rejected
265
-
266
- ### πŸ“Š Security Improvements Summary:
267
- - **Input Validation**: 100% coverage on all API endpoints
268
- - **Log Sanitization**: All sensitive fields automatically redacted
269
- - **Error Handling**: No sensitive data exposed in error messages
270
- - **Performance Impact**: < 2ms overhead per request
271
- - **Reliability**: Graceful fallback if sanitization fails
272
-
273
- ### πŸš€ Ready for Production:
274
- The security improvements are now fully functional and ready for production deployment. All identified vulnerabilities have been addressed with comprehensive testing.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/tests/README.md ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Regression Test Pack
2
+
3
+ This comprehensive regression test pack ensures the stability, performance, and security of the Merchant API application.
4
+
5
+ ## Test Structure
6
+
7
+ ### Test Files
8
+
9
+ - **`conftest.py`** - Pytest configuration and shared fixtures
10
+ - **`test_api_endpoints.py`** - API endpoint functionality tests
11
+ - **`test_services.py`** - Service layer component tests
12
+ - **`test_database.py`** - Database operations and repository tests
13
+ - **`test_performance.py`** - Performance and load testing
14
+ - **`test_integration.py`** - End-to-end integration tests
15
+ - **`test_security.py`** - Security and vulnerability tests
16
+ - **`test_advanced_nlp.py`** - Advanced NLP pipeline tests (existing)
17
+ - **`test_regression_suite.py`** - Main regression test suite
18
+ - **`run_tests.py`** - Test runner script
19
+
20
+ ### Test Categories
21
+
22
+ Tests are organized using pytest markers:
23
+
24
+ - `unit` - Unit tests for individual components
25
+ - `integration` - Integration tests for component interactions
26
+ - `performance` - Performance and load tests
27
+ - `security` - Security and vulnerability tests
28
+ - `regression` - Critical regression tests
29
+ - `slow` - Tests that take longer to run
30
+ - `database` - Tests requiring database connections
31
+ - `nlp` - NLP functionality tests
32
+ - `api` - API endpoint tests
33
+ - `cache` - Cache-related tests
34
+
35
+ ## Running Tests
36
+
37
+ ### Quick Start
38
+
39
+ ```bash
40
+ # Run all tests
41
+ python app/tests/run_tests.py
42
+
43
+ # Run specific test suite
44
+ python app/tests/run_tests.py --suite unit
45
+ python app/tests/run_tests.py --suite integration
46
+ python app/tests/run_tests.py --suite performance
47
+ python app/tests/run_tests.py --suite security
48
+ python app/tests/run_tests.py --suite regression
49
+
50
+ # Run with coverage
51
+ python app/tests/run_tests.py --coverage
52
+
53
+ # Run specific test file
54
+ python app/tests/run_tests.py --file test_api_endpoints.py
55
+
56
+ # Run tests matching pattern
57
+ python app/tests/run_tests.py --function "test_health"
58
+ ```
59
+
60
+ ### Advanced Usage
61
+
62
+ ```bash
63
+ # Run with parallel execution
64
+ python app/tests/run_tests.py --parallel 4
65
+
66
+ # Generate HTML report
67
+ python app/tests/run_tests.py --html-report
68
+
69
+ # Generate JUnit XML for CI/CD
70
+ python app/tests/run_tests.py --junit-xml results.xml
71
+
72
+ # Run with custom markers
73
+ python app/tests/run_tests.py --markers "api and not slow"
74
+
75
+ # Verbose output
76
+ python app/tests/run_tests.py --verbose
77
+ ```
78
+
79
+ ### Direct Pytest Usage
80
+
81
+ ```bash
82
+ # Run all tests
83
+ pytest app/tests/
84
+
85
+ # Run specific test categories
86
+ pytest app/tests/ -m unit
87
+ pytest app/tests/ -m "integration and not slow"
88
+ pytest app/tests/ -m performance
89
+
90
+ # Run with coverage
91
+ pytest app/tests/ --cov=app --cov-report=html
92
+
93
+ # Run specific test file
94
+ pytest app/tests/test_api_endpoints.py
95
+
96
+ # Run specific test function
97
+ pytest app/tests/test_api_endpoints.py::TestHealthEndpoints::test_health_check
98
+
99
+ # Run with parallel execution (requires pytest-xdist)
100
+ pytest app/tests/ -n 4
101
+ ```
102
+
103
+ ## Test Coverage
104
+
105
+ The test pack covers:
106
+
107
+ ### API Endpoints
108
+ - Health check endpoints
109
+ - Merchant CRUD operations
110
+ - Search functionality
111
+ - NLP processing endpoints
112
+ - Helper services
113
+ - Error handling
114
+ - Security measures
115
+
116
+ ### Service Layer
117
+ - Merchant service operations
118
+ - Helper service functionality
119
+ - Advanced NLP processing
120
+ - Search helpers
121
+ - Service integration
122
+ - Error propagation
123
+ - Performance characteristics
124
+
125
+ ### Database Layer
126
+ - MongoDB operations
127
+ - Redis cache operations
128
+ - Database indexing
129
+ - Query optimization
130
+ - Transaction handling
131
+ - Connection management
132
+ - Error handling
133
+
134
+ ### Performance Testing
135
+ - API response times
136
+ - Database query performance
137
+ - NLP processing speed
138
+ - Concurrent request handling
139
+ - Memory usage monitoring
140
+ - Load testing scenarios
141
+ - Performance regression detection
142
+
143
+ ### Integration Testing
144
+ - End-to-end user journeys
145
+ - Service integration
146
+ - Data flow validation
147
+ - Concurrency handling
148
+ - Error recovery
149
+ - System stability
150
+
151
+ ### Security Testing
152
+ - Input validation and sanitization
153
+ - SQL/NoSQL injection prevention
154
+ - XSS prevention
155
+ - Authentication and authorization
156
+ - CORS configuration
157
+ - Rate limiting
158
+ - Data protection
159
+
160
+ ## Test Configuration
161
+
162
+ ### Environment Variables
163
+
164
+ Set these environment variables for testing:
165
+
166
+ ```bash
167
+ export TESTING=true
168
+ export MONGODB_URL=mongodb://localhost:27017/test_db
169
+ export REDIS_URL=redis://localhost:6379/1
170
+ export ALLOWED_ORIGINS=http://localhost:3000,http://testserver
171
+ ```
172
+
173
+ ### Dependencies
174
+
175
+ Install test dependencies:
176
+
177
+ ```bash
178
+ pip install pytest pytest-asyncio pytest-cov pytest-html pytest-xdist pytest-timeout
179
+ ```
180
+
181
+ Or install minimal dependencies:
182
+
183
+ ```bash
184
+ pip install pytest pytest-asyncio
185
+ ```
186
+
187
+ ### Mock Configuration
188
+
189
+ Tests use extensive mocking to isolate components and ensure reliable, fast execution:
190
+
191
+ - Database operations are mocked to avoid external dependencies
192
+ - NLP services are mocked for consistent results
193
+ - External APIs are mocked to prevent network calls
194
+ - Time-sensitive operations use controlled timing
195
+
196
+ ## Continuous Integration
197
+
198
+ ### GitHub Actions Example
199
+
200
+ ```yaml
201
+ name: Regression Tests
202
+
203
+ on: [push, pull_request]
204
+
205
+ jobs:
206
+ test:
207
+ runs-on: ubuntu-latest
208
+
209
+ steps:
210
+ - uses: actions/checkout@v2
211
+
212
+ - name: Set up Python
213
+ uses: actions/setup-python@v2
214
+ with:
215
+ python-version: 3.9
216
+
217
+ - name: Install dependencies
218
+ run: |
219
+ pip install -r requirements.txt
220
+ pip install pytest pytest-asyncio pytest-cov pytest-html
221
+
222
+ - name: Run regression tests
223
+ run: |
224
+ python app/tests/run_tests.py --suite regression --coverage --junit-xml results.xml
225
+
226
+ - name: Upload test results
227
+ uses: actions/upload-artifact@v2
228
+ with:
229
+ name: test-results
230
+ path: results.xml
231
+ ```
232
+
233
+ ## Performance Benchmarks
234
+
235
+ The test pack includes performance benchmarks to detect regressions:
236
+
237
+ - API endpoints should respond within 2 seconds
238
+ - Database queries should complete within 500ms
239
+ - NLP processing should complete within 3 seconds
240
+ - System should handle 50+ concurrent requests
241
+ - Memory usage should remain stable under load
242
+
243
+ ## Test Data
244
+
245
+ Tests use controlled test data:
246
+
247
+ - Sample merchant data with realistic structure
248
+ - Predefined search queries for consistent testing
249
+ - Mock NLP responses for reliable results
250
+ - Performance test datasets for load testing
251
+
252
+ ## Troubleshooting
253
+
254
+ ### Common Issues
255
+
256
+ 1. **Import Errors**: Ensure PYTHONPATH includes the project root
257
+ 2. **Database Errors**: Check that test environment variables are set
258
+ 3. **Async Errors**: Ensure pytest-asyncio is installed and configured
259
+ 4. **Mock Errors**: Verify that mocks are properly configured for your test scenario
260
+
261
+ ### Debug Mode
262
+
263
+ Run tests with maximum verbosity:
264
+
265
+ ```bash
266
+ python app/tests/run_tests.py --verbose --function "test_specific_function"
267
+ ```
268
+
269
+ ### Test Isolation
270
+
271
+ Each test is designed to be independent:
272
+ - Fixtures provide clean test data
273
+ - Mocks prevent external dependencies
274
+ - Setup/teardown ensures clean state
275
+
276
+ ## Contributing
277
+
278
+ When adding new tests:
279
+
280
+ 1. Follow the existing naming conventions
281
+ 2. Use appropriate pytest markers
282
+ 3. Include docstrings explaining test purpose
283
+ 4. Mock external dependencies
284
+ 5. Ensure tests are deterministic
285
+ 6. Add performance assertions where relevant
286
+ 7. Include both positive and negative test cases
287
+
288
+ ## Reporting Issues
289
+
290
+ If tests fail:
291
+
292
+ 1. Check the test output for specific error messages
293
+ 2. Verify environment setup
294
+ 3. Run individual test files to isolate issues
295
+ 4. Check for recent code changes that might affect functionality
296
+ 5. Review mock configurations for accuracy
app/tests/conftest.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pytest configuration and shared fixtures for regression tests
3
+ """
4
+
5
+ import pytest
6
+ import asyncio
7
+ import os
8
+ from typing import AsyncGenerator, Dict, Any
9
+ from unittest.mock import AsyncMock, MagicMock
10
+ from fastapi.testclient import TestClient
11
+ from httpx import AsyncClient
12
+
13
+ # Import the FastAPI app
14
+ from app.app import app
15
+
16
+ @pytest.fixture(scope="session")
17
+ def event_loop():
18
+ """Create an instance of the default event loop for the test session."""
19
+ loop = asyncio.get_event_loop_policy().new_event_loop()
20
+ yield loop
21
+ loop.close()
22
+
23
+ @pytest.fixture
24
+ def client():
25
+ """Create a test client for the FastAPI app."""
26
+ return TestClient(app)
27
+
28
+ @pytest.fixture
29
+ async def async_client() -> AsyncGenerator[AsyncClient, None]:
30
+ """Create an async test client for the FastAPI app."""
31
+ async with AsyncClient(app=app, base_url="http://test") as ac:
32
+ yield ac
33
+
34
+ @pytest.fixture
35
+ def mock_mongodb():
36
+ """Mock MongoDB connection."""
37
+ mock_db = MagicMock()
38
+ mock_collection = MagicMock()
39
+ mock_db.__getitem__.return_value = mock_collection
40
+ return mock_db
41
+
42
+ @pytest.fixture
43
+ def mock_redis():
44
+ """Mock Redis connection."""
45
+ mock_redis = AsyncMock()
46
+ mock_redis.get.return_value = None
47
+ mock_redis.set.return_value = True
48
+ mock_redis.delete.return_value = True
49
+ return mock_redis
50
+
51
+ @pytest.fixture
52
+ def sample_merchant_data():
53
+ """Sample merchant data for testing."""
54
+ return {
55
+ "_id": "test_merchant_123",
56
+ "name": "Test Hair Salon",
57
+ "category": "salon",
58
+ "subcategory": "hair_salon",
59
+ "location": {
60
+ "type": "Point",
61
+ "coordinates": [-74.0060, 40.7128] # NYC coordinates
62
+ },
63
+ "address": {
64
+ "street": "123 Test Street",
65
+ "city": "New York",
66
+ "state": "NY",
67
+ "zip_code": "10001"
68
+ },
69
+ "contact": {
70
+ "phone": "+1-555-0123",
71
+ "email": "test@testsalon.com"
72
+ },
73
+ "business_hours": {
74
+ "monday": {"open": "09:00", "close": "18:00"},
75
+ "tuesday": {"open": "09:00", "close": "18:00"},
76
+ "wednesday": {"open": "09:00", "close": "18:00"},
77
+ "thursday": {"open": "09:00", "close": "18:00"},
78
+ "friday": {"open": "09:00", "close": "19:00"},
79
+ "saturday": {"open": "08:00", "close": "17:00"},
80
+ "sunday": {"closed": True}
81
+ },
82
+ "services": [
83
+ {"name": "Haircut", "price": 50.0, "duration": 60},
84
+ {"name": "Hair Color", "price": 120.0, "duration": 120},
85
+ {"name": "Blowout", "price": 35.0, "duration": 45}
86
+ ],
87
+ "amenities": ["parking", "wifi", "wheelchair_accessible"],
88
+ "average_rating": 4.5,
89
+ "total_reviews": 127,
90
+ "price_range": "$$",
91
+ "is_active": True,
92
+ "created_at": "2024-01-01T00:00:00Z",
93
+ "updated_at": "2024-01-15T12:00:00Z"
94
+ }
95
+
96
+ @pytest.fixture
97
+ def sample_search_query():
98
+ """Sample search query for testing."""
99
+ return {
100
+ "query": "find the best hair salon near me with parking",
101
+ "latitude": 40.7128,
102
+ "longitude": -74.0060,
103
+ "radius": 5000, # 5km
104
+ "category": "salon"
105
+ }
106
+
107
+ @pytest.fixture
108
+ def mock_nlp_pipeline():
109
+ """Mock NLP pipeline for testing."""
110
+ mock_pipeline = AsyncMock()
111
+ mock_pipeline.process_query.return_value = {
112
+ "query": "test query",
113
+ "primary_intent": {
114
+ "intent": "SEARCH_SERVICE",
115
+ "confidence": 0.85
116
+ },
117
+ "entities": {
118
+ "service_types": ["haircut"],
119
+ "amenities": ["parking"],
120
+ "location_modifiers": ["near me"]
121
+ },
122
+ "similar_services": [("salon", 0.9)],
123
+ "search_parameters": {
124
+ "merchant_category": "salon",
125
+ "amenities": ["parking"],
126
+ "radius": 5000
127
+ },
128
+ "processing_time": 0.123
129
+ }
130
+ return mock_pipeline
131
+
132
+ @pytest.fixture(autouse=True)
133
+ def setup_test_environment():
134
+ """Setup test environment variables."""
135
+ os.environ["TESTING"] = "true"
136
+ os.environ["MONGODB_URL"] = "mongodb://localhost:27017/test_db"
137
+ os.environ["REDIS_URL"] = "redis://localhost:6379/1"
138
+ os.environ["ALLOWED_ORIGINS"] = "http://localhost:3000,http://testserver"
139
+ yield
140
+ # Cleanup
141
+ for key in ["TESTING", "MONGODB_URL", "REDIS_URL", "ALLOWED_ORIGINS"]:
142
+ os.environ.pop(key, None)
143
+
144
+ @pytest.fixture
145
+ def performance_test_data():
146
+ """Data for performance testing."""
147
+ return {
148
+ "queries": [
149
+ "find a hair salon",
150
+ "best spa near me",
151
+ "gym with parking",
152
+ "dental clinic open now",
153
+ "massage therapy luxury",
154
+ "budget-friendly fitness center",
155
+ "nail salon walking distance",
156
+ "pet-friendly grooming",
157
+ "24/7 pharmacy",
158
+ "organic restaurant"
159
+ ],
160
+ "expected_max_response_time": 2.0, # seconds
161
+ "expected_min_success_rate": 0.95 # 95%
162
+ }
app/tests/pytest.ini ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool:pytest]
2
+ # Pytest configuration for regression test pack
3
+
4
+ # Test discovery
5
+ testpaths = app/tests
6
+ python_files = test_*.py
7
+ python_classes = Test*
8
+ python_functions = test_*
9
+
10
+ # Markers for test categorization
11
+ markers =
12
+ unit: Unit tests for individual components
13
+ integration: Integration tests for component interactions
14
+ performance: Performance and load tests
15
+ security: Security and vulnerability tests
16
+ regression: Regression tests for critical functionality
17
+ slow: Tests that take longer to run
18
+ database: Tests that require database connections
19
+ nlp: Tests for NLP functionality
20
+ api: API endpoint tests
21
+ cache: Cache-related tests
22
+
23
+ # Test execution options
24
+ addopts =
25
+ -v
26
+ --tb=short
27
+ --strict-markers
28
+ --disable-warnings
29
+ --color=yes
30
+ --durations=10
31
+ --maxfail=5
32
+
33
+ # Async test configuration
34
+ asyncio_mode = auto
35
+
36
+ # Test timeout (in seconds) - requires pytest-timeout plugin
37
+ # timeout = 300
38
+
39
+ # Minimum Python version
40
+ minversion = 3.8
41
+
42
+ # Test output
43
+ console_output_style = progress
44
+ junit_family = xunit2
45
+
46
+ # Coverage configuration (if using pytest-cov)
47
+ # addopts = --cov=app --cov-report=html --cov-report=term-missing --cov-fail-under=80
48
+
49
+ # Logging configuration
50
+ log_cli = true
51
+ log_cli_level = INFO
52
+ log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s
53
+ log_cli_date_format = %Y-%m-%d %H:%M:%S
54
+
55
+ # Filter warnings
56
+ filterwarnings =
57
+ ignore::DeprecationWarning
58
+ ignore::PendingDeprecationWarning
59
+ ignore::UserWarning:motor.*
60
+ ignore::UserWarning:pymongo.*
app/tests/run_tests.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test runner script for the regression test pack
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ import subprocess
9
+ import argparse
10
+ import time
11
+ from pathlib import Path
12
+
13
+ def run_command(command, description=""):
14
+ """Run a command and return the result"""
15
+ print(f"\n{'='*60}")
16
+ print(f"Running: {description or command}")
17
+ print(f"{'='*60}")
18
+
19
+ start_time = time.time()
20
+ result = subprocess.run(command, shell=True, capture_output=True, text=True)
21
+ duration = time.time() - start_time
22
+
23
+ print(f"Duration: {duration:.2f}s")
24
+ print(f"Exit code: {result.returncode}")
25
+
26
+ if result.stdout:
27
+ print(f"\nSTDOUT:\n{result.stdout}")
28
+
29
+ if result.stderr:
30
+ print(f"\nSTDERR:\n{result.stderr}")
31
+
32
+ return result
33
+
34
+ def main():
35
+ parser = argparse.ArgumentParser(description="Run regression test pack")
36
+ parser.add_argument("--suite", choices=["all", "unit", "integration", "performance", "security", "regression"],
37
+ default="all", help="Test suite to run")
38
+ parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
39
+ parser.add_argument("--coverage", action="store_true", help="Run with coverage")
40
+ parser.add_argument("--parallel", "-n", type=int, help="Number of parallel workers")
41
+ parser.add_argument("--markers", "-m", help="Run tests with specific markers")
42
+ parser.add_argument("--file", "-f", help="Run specific test file")
43
+ parser.add_argument("--function", "-k", help="Run tests matching pattern")
44
+ parser.add_argument("--html-report", action="store_true", help="Generate HTML report")
45
+ parser.add_argument("--junit-xml", help="Generate JUnit XML report")
46
+ parser.add_argument("--timeout", type=int, default=300, help="Test timeout in seconds (requires pytest-timeout)")
47
+
48
+ args = parser.parse_args()
49
+
50
+ # Set up environment
51
+ os.environ["TESTING"] = "true"
52
+ os.environ["PYTHONPATH"] = str(Path(__file__).parent.parent.parent)
53
+
54
+ # Build pytest command
55
+ cmd_parts = ["python", "-m", "pytest"]
56
+
57
+ # Add test path
58
+ if args.file:
59
+ cmd_parts.append(f"app/tests/{args.file}")
60
+ else:
61
+ cmd_parts.append("app/tests/")
62
+
63
+ # Add verbosity
64
+ if args.verbose:
65
+ cmd_parts.append("-vv")
66
+ else:
67
+ cmd_parts.append("-v")
68
+
69
+ # Add coverage
70
+ if args.coverage:
71
+ cmd_parts.extend([
72
+ "--cov=app",
73
+ "--cov-report=html:htmlcov",
74
+ "--cov-report=term-missing",
75
+ "--cov-fail-under=70"
76
+ ])
77
+
78
+ # Add parallel execution
79
+ if args.parallel:
80
+ cmd_parts.extend(["-n", str(args.parallel)])
81
+
82
+ # Add markers
83
+ if args.suite != "all":
84
+ cmd_parts.extend(["-m", args.suite])
85
+ elif args.markers:
86
+ cmd_parts.extend(["-m", args.markers])
87
+
88
+ # Add function pattern
89
+ if args.function:
90
+ cmd_parts.extend(["-k", args.function])
91
+
92
+ # Add timeout (only if pytest-timeout is available)
93
+ try:
94
+ import pytest_timeout
95
+ cmd_parts.extend(["--timeout", str(args.timeout)])
96
+ except ImportError:
97
+ print("Note: pytest-timeout not installed, skipping timeout option")
98
+
99
+ # Add HTML report
100
+ if args.html_report:
101
+ cmd_parts.extend(["--html=test_report.html", "--self-contained-html"])
102
+
103
+ # Add JUnit XML
104
+ if args.junit_xml:
105
+ cmd_parts.extend(["--junit-xml", args.junit_xml])
106
+
107
+ # Add other options
108
+ cmd_parts.extend([
109
+ "--tb=short",
110
+ "--strict-markers",
111
+ "--color=yes",
112
+ "--durations=10"
113
+ ])
114
+
115
+ # Run the tests
116
+ command = " ".join(cmd_parts)
117
+ result = run_command(command, f"Running {args.suite} test suite")
118
+
119
+ # Print summary
120
+ print(f"\n{'='*60}")
121
+ print("TEST EXECUTION SUMMARY")
122
+ print(f"{'='*60}")
123
+ print(f"Suite: {args.suite}")
124
+ print(f"Exit Code: {result.returncode}")
125
+ print(f"Command: {command}")
126
+
127
+ if result.returncode == 0:
128
+ print("βœ… All tests passed!")
129
+ else:
130
+ print("❌ Some tests failed!")
131
+
132
+ # Extract test summary from output
133
+ if "failed" in result.stdout.lower() or "error" in result.stdout.lower():
134
+ lines = result.stdout.split('\n')
135
+ for line in lines:
136
+ if "failed" in line.lower() or "passed" in line.lower():
137
+ print(f"Result: {line.strip()}")
138
+ break
139
+
140
+ return result.returncode
141
+
142
+ if __name__ == "__main__":
143
+ sys.exit(main())
app/tests/test_api_endpoints.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Regression tests for API endpoints
3
+ """
4
+
5
+ import pytest
6
+ import json
7
+ from fastapi.testclient import TestClient
8
+ from unittest.mock import patch, AsyncMock
9
+ from httpx import AsyncClient
10
+
11
+ @pytest.mark.api
12
+ class TestHealthEndpoints:
13
+ """Test health check endpoints"""
14
+
15
+ def test_health_check(self, client: TestClient):
16
+ """Test basic health check endpoint"""
17
+ response = client.get("/health")
18
+ assert response.status_code == 200
19
+
20
+ data = response.json()
21
+ assert data["status"] == "healthy"
22
+ assert "timestamp" in data
23
+ assert data["service"] == "merchant-api"
24
+ assert data["version"] == "1.0.0"
25
+
26
+ @patch('app.nosql.check_mongodb_health')
27
+ @patch('app.nosql.check_redis_health')
28
+ def test_readiness_check_healthy(self, mock_redis, mock_mongo, client: TestClient):
29
+ """Test readiness check when databases are healthy"""
30
+ mock_mongo.return_value = True
31
+ mock_redis.return_value = True
32
+
33
+ response = client.get("/ready")
34
+ assert response.status_code == 200
35
+
36
+ data = response.json()
37
+ assert data["status"] == "ready"
38
+ assert data["databases"]["mongodb"] == "healthy"
39
+ assert data["databases"]["redis"] == "healthy"
40
+
41
+ @patch('app.nosql.check_mongodb_health')
42
+ @patch('app.nosql.check_redis_health')
43
+ def test_readiness_check_unhealthy(self, mock_redis, mock_mongo, client: TestClient):
44
+ """Test readiness check when databases are unhealthy"""
45
+ mock_mongo.return_value = False
46
+ mock_redis.return_value = True
47
+
48
+ response = client.get("/ready")
49
+ assert response.status_code == 503
50
+
51
+ data = response.json()
52
+ assert "not_ready" in data["detail"]["status"]
53
+
54
+ class TestMerchantEndpoints:
55
+ """Test merchant-related endpoints"""
56
+
57
+ @patch('app.services.merchant.get_merchants')
58
+ def test_get_merchants_success(self, mock_get_merchants, client: TestClient, sample_merchant_data):
59
+ """Test successful merchant retrieval"""
60
+ mock_get_merchants.return_value = [sample_merchant_data]
61
+
62
+ response = client.get("/api/v1/merchants/")
63
+ assert response.status_code == 200
64
+
65
+ data = response.json()
66
+ assert len(data) == 1
67
+ assert data[0]["name"] == "Test Hair Salon"
68
+ assert data[0]["category"] == "salon"
69
+
70
+ @patch('app.services.merchant.get_merchant_by_id')
71
+ def test_get_merchant_by_id_success(self, mock_get_merchant, client: TestClient, sample_merchant_data):
72
+ """Test successful merchant retrieval by ID"""
73
+ mock_get_merchant.return_value = sample_merchant_data
74
+
75
+ response = client.get("/api/v1/merchants/test_merchant_123")
76
+ assert response.status_code == 200
77
+
78
+ data = response.json()
79
+ assert data["_id"] == "test_merchant_123"
80
+ assert data["name"] == "Test Hair Salon"
81
+
82
+ @patch('app.services.merchant.get_merchant_by_id')
83
+ def test_get_merchant_by_id_not_found(self, mock_get_merchant, client: TestClient):
84
+ """Test merchant not found scenario"""
85
+ mock_get_merchant.return_value = None
86
+
87
+ response = client.get("/api/v1/merchants/nonexistent_id")
88
+ assert response.status_code == 404
89
+
90
+ @patch('app.services.merchant.search_merchants')
91
+ def test_search_merchants_with_location(self, mock_search, client: TestClient, sample_merchant_data):
92
+ """Test merchant search with location parameters"""
93
+ mock_search.return_value = [sample_merchant_data]
94
+
95
+ response = client.get("/api/v1/merchants/search", params={
96
+ "latitude": 40.7128,
97
+ "longitude": -74.0060,
98
+ "radius": 5000,
99
+ "category": "salon"
100
+ })
101
+
102
+ assert response.status_code == 200
103
+ data = response.json()
104
+ assert len(data) == 1
105
+ assert data[0]["category"] == "salon"
106
+
107
+ def test_search_merchants_invalid_coordinates(self, client: TestClient):
108
+ """Test merchant search with invalid coordinates"""
109
+ response = client.get("/api/v1/merchants/search", params={
110
+ "latitude": 200, # Invalid latitude
111
+ "longitude": -74.0060,
112
+ "radius": 5000
113
+ })
114
+
115
+ assert response.status_code == 400
116
+
117
+ class TestHelperEndpoints:
118
+ """Test helper service endpoints"""
119
+
120
+ @patch('app.services.helper.process_free_text')
121
+ def test_process_free_text_success(self, mock_process, client: TestClient):
122
+ """Test successful free text processing"""
123
+ mock_process.return_value = {
124
+ "query": "find a hair salon",
125
+ "extracted_keywords": ["hair", "salon"],
126
+ "suggested_category": "salon",
127
+ "search_parameters": {"category": "salon"}
128
+ }
129
+
130
+ response = client.post("/api/v1/helpers/process-text", json={
131
+ "text": "find a hair salon",
132
+ "latitude": 40.7128,
133
+ "longitude": -74.0060
134
+ })
135
+
136
+ assert response.status_code == 200
137
+ data = response.json()
138
+ assert data["suggested_category"] == "salon"
139
+
140
+ def test_process_free_text_empty_input(self, client: TestClient):
141
+ """Test free text processing with empty input"""
142
+ response = client.post("/api/v1/helpers/process-text", json={
143
+ "text": "",
144
+ "latitude": 40.7128,
145
+ "longitude": -74.0060
146
+ })
147
+
148
+ assert response.status_code == 400
149
+
150
+ def test_process_free_text_too_long(self, client: TestClient):
151
+ """Test free text processing with input too long"""
152
+ long_text = "a" * 1001 # Assuming 1000 char limit
153
+
154
+ response = client.post("/api/v1/helpers/process-text", json={
155
+ "text": long_text,
156
+ "latitude": 40.7128,
157
+ "longitude": -74.0060
158
+ })
159
+
160
+ assert response.status_code == 400
161
+
162
+ class TestNLPEndpoints:
163
+ """Test NLP demo endpoints"""
164
+
165
+ @patch('app.services.advanced_nlp.advanced_nlp_pipeline')
166
+ def test_analyze_query_success(self, mock_pipeline, client: TestClient, mock_nlp_pipeline):
167
+ """Test successful query analysis"""
168
+ mock_pipeline.process_query = mock_nlp_pipeline.process_query
169
+
170
+ response = client.post("/api/v1/nlp/analyze-query", params={
171
+ "query": "find the best hair salon near me",
172
+ "latitude": 40.7128,
173
+ "longitude": -74.0060
174
+ })
175
+
176
+ assert response.status_code == 200
177
+ data = response.json()
178
+ assert data["status"] == "success"
179
+ assert "analysis" in data
180
+
181
+ def test_analyze_query_empty_input(self, client: TestClient):
182
+ """Test query analysis with empty input"""
183
+ response = client.post("/api/v1/nlp/analyze-query", params={
184
+ "query": ""
185
+ })
186
+
187
+ assert response.status_code == 400
188
+
189
+ def test_get_supported_intents(self, client: TestClient):
190
+ """Test getting supported intents"""
191
+ response = client.get("/api/v1/nlp/supported-intents")
192
+ assert response.status_code == 200
193
+
194
+ data = response.json()
195
+ assert data["status"] == "success"
196
+ assert "supported_intents" in data
197
+ assert "SEARCH_SERVICE" in data["supported_intents"]
198
+ assert "FILTER_QUALITY" in data["supported_intents"]
199
+
200
+ def test_get_supported_entities(self, client: TestClient):
201
+ """Test getting supported entities"""
202
+ response = client.get("/api/v1/nlp/supported-entities")
203
+ assert response.status_code == 200
204
+
205
+ data = response.json()
206
+ assert data["status"] == "success"
207
+ assert "supported_entities" in data
208
+ assert "services" in data["supported_entities"]
209
+ assert "amenities" in data["supported_entities"]
210
+
211
+ class TestPerformanceEndpoints:
212
+ """Test performance monitoring endpoints"""
213
+
214
+ @patch('app.utils.performance_monitor.get_performance_report')
215
+ def test_get_performance_report(self, mock_report, client: TestClient):
216
+ """Test performance report endpoint"""
217
+ mock_report.return_value = {
218
+ "metrics": {
219
+ "total_queries": 100,
220
+ "average_time": 0.5,
221
+ "slow_queries": []
222
+ }
223
+ }
224
+
225
+ response = client.get("/api/v1/performance/report")
226
+ assert response.status_code == 200
227
+
228
+ def test_get_metrics(self, client: TestClient):
229
+ """Test metrics endpoint"""
230
+ response = client.get("/metrics")
231
+ # Should return metrics even if some components fail
232
+ assert response.status_code in [200, 500]
233
+
234
+ class TestSecurityEndpoints:
235
+ """Test security-related functionality"""
236
+
237
+ def test_cors_headers(self, client: TestClient):
238
+ """Test CORS headers are properly set"""
239
+ response = client.options("/api/v1/merchants/", headers={
240
+ "Origin": "http://localhost:3000",
241
+ "Access-Control-Request-Method": "GET"
242
+ })
243
+
244
+ # Should allow the request
245
+ assert response.status_code in [200, 204]
246
+
247
+ def test_invalid_origin_blocked(self, client: TestClient):
248
+ """Test that invalid origins are blocked"""
249
+ response = client.get("/api/v1/merchants/", headers={
250
+ "Origin": "http://malicious-site.com"
251
+ })
252
+
253
+ # Should still work but without CORS headers for invalid origin
254
+ assert response.status_code == 200
255
+
256
+ def test_request_size_limit(self, client: TestClient):
257
+ """Test request size limits"""
258
+ large_payload = {"data": "x" * (11 * 1024 * 1024)} # 11MB
259
+
260
+ response = client.post("/api/v1/helpers/process-text", json=large_payload)
261
+ # Should be rejected due to size limit
262
+ assert response.status_code in [413, 400]
263
+
264
+ class TestErrorHandling:
265
+ """Test error handling across endpoints"""
266
+
267
+ def test_404_for_nonexistent_endpoint(self, client: TestClient):
268
+ """Test 404 for non-existent endpoints"""
269
+ response = client.get("/api/v1/nonexistent")
270
+ assert response.status_code == 404
271
+
272
+ def test_405_for_wrong_method(self, client: TestClient):
273
+ """Test 405 for wrong HTTP method"""
274
+ response = client.delete("/api/v1/merchants/")
275
+ assert response.status_code == 405
276
+
277
+ @patch('app.services.merchant.get_merchants')
278
+ def test_500_error_handling(self, mock_get_merchants, client: TestClient):
279
+ """Test 500 error handling"""
280
+ mock_get_merchants.side_effect = Exception("Database error")
281
+
282
+ response = client.get("/api/v1/merchants/")
283
+ assert response.status_code == 500
284
+
285
+ def test_malformed_json(self, client: TestClient):
286
+ """Test handling of malformed JSON"""
287
+ response = client.post(
288
+ "/api/v1/helpers/process-text",
289
+ data="invalid json",
290
+ headers={"Content-Type": "application/json"}
291
+ )
292
+ assert response.status_code == 422
293
+
294
+ class TestAsyncEndpoints:
295
+ """Test async endpoint functionality"""
296
+
297
+ @pytest.mark.asyncio
298
+ async def test_async_client_health_check(self, async_client: AsyncClient):
299
+ """Test health check with async client"""
300
+ response = await async_client.get("/health")
301
+ assert response.status_code == 200
302
+
303
+ data = response.json()
304
+ assert data["status"] == "healthy"
305
+
306
+ @pytest.mark.asyncio
307
+ @patch('app.services.advanced_nlp.advanced_nlp_pipeline')
308
+ async def test_async_nlp_processing(self, mock_pipeline, async_client: AsyncClient, mock_nlp_pipeline):
309
+ """Test async NLP processing"""
310
+ mock_pipeline.process_query = mock_nlp_pipeline.process_query
311
+
312
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={
313
+ "query": "find a spa"
314
+ })
315
+
316
+ assert response.status_code == 200
317
+ data = response.json()
318
+ assert data["status"] == "success"
app/tests/test_database.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Regression tests for database operations and repositories
3
+ """
4
+
5
+ import pytest
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+ from typing import Dict, Any, List
8
+ import asyncio
9
+
10
+ class TestMongoDBOperations:
11
+ """Test MongoDB database operations"""
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_mongodb_connection_health(self):
15
+ """Test MongoDB connection health check"""
16
+ from app.nosql import check_mongodb_health
17
+
18
+ with patch('app.nosql.db') as mock_db:
19
+ mock_db.command.return_value = {"ok": 1}
20
+
21
+ result = await check_mongodb_health()
22
+ assert result is True
23
+
24
+ @pytest.mark.asyncio
25
+ async def test_mongodb_connection_failure(self):
26
+ """Test MongoDB connection failure handling"""
27
+ from app.nosql import check_mongodb_health
28
+
29
+ with patch('app.nosql.db') as mock_db:
30
+ mock_db.command.side_effect = Exception("Connection failed")
31
+
32
+ result = await check_mongodb_health()
33
+ assert result is False
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_fetch_documents(self, sample_merchant_data):
37
+ """Test retrieving documents from database"""
38
+ from app.repositories.db_repository import fetch_documents
39
+
40
+ with patch('app.nosql.db') as mock_db:
41
+ mock_collection = AsyncMock()
42
+ mock_db.__getitem__.return_value = mock_collection
43
+ mock_collection.count_documents.return_value = 1
44
+ mock_collection.find.return_value.skip.return_value.limit.return_value.to_list.return_value = [sample_merchant_data]
45
+
46
+ result = await fetch_documents("merchants", {}, {}, 0, 10)
47
+
48
+ assert result["total"] == 1
49
+ assert len(result["documents"]) == 1
50
+ assert result["documents"][0]["name"] == "Test Hair Salon"
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_execute_query(self, sample_merchant_data):
54
+ """Test executing aggregation query"""
55
+ from app.repositories.db_repository import execute_query
56
+
57
+ with patch('app.nosql.db') as mock_db:
58
+ mock_collection = AsyncMock()
59
+ mock_db.__getitem__.return_value = mock_collection
60
+ mock_collection.aggregate.return_value.to_list.return_value = [sample_merchant_data]
61
+
62
+ pipeline = [{"$match": {"_id": "test_merchant_123"}}]
63
+ result = await execute_query("merchants", pipeline, use_optimization=False)
64
+
65
+ assert len(result) == 1
66
+ assert result[0]["name"] == "Test Hair Salon"
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_count_documents(self):
70
+ """Test counting documents in collection"""
71
+ from app.repositories.db_repository import count_documents
72
+
73
+ with patch('app.nosql.db') as mock_db:
74
+ mock_collection = AsyncMock()
75
+ mock_db.__getitem__.return_value = mock_collection
76
+ mock_collection.count_documents.return_value = 5
77
+
78
+ result = await count_documents("merchants", {"category": "salon"})
79
+
80
+ assert result == 5
81
+ mock_collection.count_documents.assert_called_once_with({"category": "salon"})
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_serialize_mongo_document(self):
85
+ """Test MongoDB document serialization"""
86
+ from app.repositories.db_repository import serialize_mongo_document
87
+ from bson import ObjectId
88
+ from datetime import datetime
89
+
90
+ test_doc = {
91
+ "_id": ObjectId("507f1f77bcf86cd799439011"),
92
+ "name": "Test Merchant",
93
+ "created_at": datetime(2024, 1, 1, 12, 0, 0),
94
+ "nested": {
95
+ "id": ObjectId("507f1f77bcf86cd799439012"),
96
+ "date": datetime(2024, 1, 2, 12, 0, 0)
97
+ }
98
+ }
99
+
100
+ result = serialize_mongo_document(test_doc)
101
+
102
+ assert isinstance(result["_id"], str)
103
+ assert isinstance(result["created_at"], str)
104
+ assert isinstance(result["nested"]["id"], str)
105
+ assert isinstance(result["nested"]["date"], str)
106
+
107
+ class TestRedisOperations:
108
+ """Test Redis cache operations"""
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_redis_connection_health(self):
112
+ """Test Redis connection health check"""
113
+ from app.nosql import check_redis_health
114
+
115
+ with patch('app.nosql.get_redis_client') as mock_client:
116
+ mock_redis = AsyncMock()
117
+ mock_client.return_value = mock_redis
118
+ mock_redis.ping.return_value = True
119
+
120
+ result = await check_redis_health()
121
+ assert result is True
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_redis_connection_failure(self):
125
+ """Test Redis connection failure handling"""
126
+ from app.nosql import check_redis_health
127
+
128
+ with patch('app.nosql.get_redis_client') as mock_client:
129
+ mock_client.side_effect = Exception("Redis connection failed")
130
+
131
+ result = await check_redis_health()
132
+ assert result is False
133
+
134
+ @pytest.mark.asyncio
135
+ async def test_cache_merchant_data(self, sample_merchant_data):
136
+ """Test caching merchant data in Redis"""
137
+ from app.repositories.cache_repository import cache_merchant_data
138
+
139
+ with patch('app.nosql.get_redis_client') as mock_client:
140
+ mock_redis = AsyncMock()
141
+ mock_client.return_value = mock_redis
142
+ mock_redis.setex.return_value = True
143
+
144
+ result = await cache_merchant_data("test_merchant_123", sample_merchant_data)
145
+
146
+ assert result is True
147
+ mock_redis.setex.assert_called_once()
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_cache_manager_get_or_set(self, sample_merchant_data):
151
+ """Test cache manager get_or_set functionality"""
152
+ from app.repositories.cache_repository import cache_manager
153
+ import json
154
+
155
+ with patch('app.nosql.redis_client') as mock_redis:
156
+ mock_redis.get.return_value = None # Cache miss
157
+ mock_redis.set.return_value = True
158
+
159
+ async def fetch_func():
160
+ return sample_merchant_data
161
+
162
+ result = await cache_manager.get_or_set_cache("test_key", fetch_func)
163
+
164
+ assert result["name"] == "Test Hair Salon"
165
+ mock_redis.set.assert_called_once()
166
+
167
+ @pytest.mark.asyncio
168
+ async def test_cache_manager_hit(self, sample_merchant_data):
169
+ """Test cache manager cache hit"""
170
+ from app.repositories.cache_repository import cache_manager
171
+ import json
172
+
173
+ with patch('app.nosql.redis_client') as mock_redis:
174
+ mock_redis.get.return_value = json.dumps(sample_merchant_data)
175
+
176
+ async def fetch_func():
177
+ return {"should": "not_be_called"}
178
+
179
+ result = await cache_manager.get_or_set_cache("test_key", fetch_func)
180
+
181
+ assert result["name"] == "Test Hair Salon"
182
+ mock_redis.get.assert_called_once()
183
+
184
+ @pytest.mark.asyncio
185
+ async def test_cache_manager_invalidate(self):
186
+ """Test cache invalidation"""
187
+ from app.repositories.cache_repository import cache_manager
188
+
189
+ with patch('app.nosql.redis_client') as mock_redis:
190
+ mock_redis.delete.return_value = 1
191
+
192
+ await cache_manager.invalidate_cache("test_key")
193
+
194
+ mock_redis.delete.assert_called_once()
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_invalidate_cache(self):
198
+ """Test cache invalidation"""
199
+ from app.repositories.cache_repository import invalidate_cache
200
+
201
+ with patch('app.nosql.get_redis_client') as mock_client:
202
+ mock_redis = AsyncMock()
203
+ mock_client.return_value = mock_redis
204
+ mock_redis.delete.return_value = 1
205
+
206
+ result = await invalidate_cache("test_key")
207
+
208
+ assert result is True
209
+ mock_redis.delete.assert_called_once_with("test_key")
210
+
211
+ class TestDatabaseIndexes:
212
+ """Test database indexing functionality"""
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_create_geospatial_index(self):
216
+ """Test creating geospatial index"""
217
+ from app.database.indexes import create_geospatial_index
218
+
219
+ with patch('app.nosql.get_mongodb_client') as mock_client:
220
+ mock_collection = MagicMock()
221
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
222
+ mock_collection.create_index.return_value = "location_2dsphere"
223
+
224
+ result = await create_geospatial_index()
225
+
226
+ assert result == "location_2dsphere"
227
+ mock_collection.create_index.assert_called_once()
228
+
229
+ @pytest.mark.asyncio
230
+ async def test_create_text_search_index(self):
231
+ """Test creating text search index"""
232
+ from app.database.indexes import create_text_search_index
233
+
234
+ with patch('app.nosql.get_mongodb_client') as mock_client:
235
+ mock_collection = MagicMock()
236
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
237
+ mock_collection.create_index.return_value = "text_search_index"
238
+
239
+ result = await create_text_search_index()
240
+
241
+ assert result == "text_search_index"
242
+ mock_collection.create_index.assert_called_once()
243
+
244
+ @pytest.mark.asyncio
245
+ async def test_create_category_index(self):
246
+ """Test creating category index"""
247
+ from app.database.indexes import create_category_index
248
+
249
+ with patch('app.nosql.get_mongodb_client') as mock_client:
250
+ mock_collection = MagicMock()
251
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
252
+ mock_collection.create_index.return_value = "category_index"
253
+
254
+ result = await create_category_index()
255
+
256
+ assert result == "category_index"
257
+ mock_collection.create_index.assert_called_once()
258
+
259
+ class TestQueryOptimization:
260
+ """Test database query optimization"""
261
+
262
+ @pytest.mark.asyncio
263
+ async def test_optimized_merchant_search(self, sample_merchant_data):
264
+ """Test optimized merchant search query"""
265
+ from app.database.query_optimizer import optimize_search_query
266
+
267
+ search_params = {
268
+ "category": "salon",
269
+ "latitude": 40.7128,
270
+ "longitude": -74.0060,
271
+ "radius": 5000,
272
+ "min_rating": 4.0
273
+ }
274
+
275
+ optimized_query = optimize_search_query(search_params)
276
+
277
+ assert isinstance(optimized_query, dict)
278
+ assert "location" in optimized_query
279
+ assert "category" in optimized_query
280
+ assert "average_rating" in optimized_query
281
+
282
+ @pytest.mark.asyncio
283
+ async def test_query_performance_analysis(self):
284
+ """Test query performance analysis"""
285
+ from app.database.query_optimizer import analyze_query_performance
286
+
287
+ with patch('app.nosql.get_mongodb_client') as mock_client:
288
+ mock_collection = MagicMock()
289
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
290
+
291
+ # Mock explain output
292
+ mock_collection.find.return_value.explain.return_value = {
293
+ "executionStats": {
294
+ "totalDocsExamined": 100,
295
+ "totalDocsReturned": 10,
296
+ "executionTimeMillis": 50
297
+ }
298
+ }
299
+
300
+ query = {"category": "salon"}
301
+ result = await analyze_query_performance(query)
302
+
303
+ assert "executionStats" in result
304
+ assert result["executionStats"]["totalDocsExamined"] == 100
305
+
306
+ class TestDatabaseTransactions:
307
+ """Test database transaction handling"""
308
+
309
+ @pytest.mark.asyncio
310
+ async def test_merchant_creation_transaction(self, sample_merchant_data):
311
+ """Test merchant creation with transaction"""
312
+ from app.repositories.db_repository import create_merchant_with_transaction
313
+
314
+ with patch('app.nosql.get_mongodb_client') as mock_client:
315
+ mock_session = AsyncMock()
316
+ mock_client.return_value.start_session.return_value.__aenter__.return_value = mock_session
317
+ mock_collection = MagicMock()
318
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
319
+ mock_collection.insert_one.return_value.inserted_id = "new_merchant_id"
320
+
321
+ result = await create_merchant_with_transaction(sample_merchant_data)
322
+
323
+ assert result == "new_merchant_id"
324
+ mock_session.start_transaction.assert_called_once()
325
+
326
+ @pytest.mark.asyncio
327
+ async def test_transaction_rollback(self, sample_merchant_data):
328
+ """Test transaction rollback on error"""
329
+ from app.repositories.db_repository import create_merchant_with_transaction
330
+
331
+ with patch('app.nosql.get_mongodb_client') as mock_client:
332
+ mock_session = AsyncMock()
333
+ mock_client.return_value.start_session.return_value.__aenter__.return_value = mock_session
334
+ mock_collection = MagicMock()
335
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
336
+ mock_collection.insert_one.side_effect = Exception("Insert failed")
337
+
338
+ with pytest.raises(Exception):
339
+ await create_merchant_with_transaction(sample_merchant_data)
340
+
341
+ mock_session.abort_transaction.assert_called_once()
342
+
343
+ class TestDatabasePerformance:
344
+ """Test database performance characteristics"""
345
+
346
+ @pytest.mark.asyncio
347
+ async def test_concurrent_database_operations(self, sample_merchant_data):
348
+ """Test concurrent database operations"""
349
+ from app.repositories.db_repository import get_merchant_by_id_from_db
350
+
351
+ with patch('app.nosql.get_mongodb_client') as mock_client:
352
+ mock_collection = MagicMock()
353
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
354
+ mock_collection.find_one.return_value = sample_merchant_data
355
+
356
+ # Create multiple concurrent requests
357
+ tasks = [
358
+ get_merchant_by_id_from_db(f"merchant_{i}")
359
+ for i in range(20)
360
+ ]
361
+
362
+ results = await asyncio.gather(*tasks)
363
+
364
+ assert len(results) == 20
365
+ assert all(result["name"] == "Test Hair Salon" for result in results)
366
+
367
+ @pytest.mark.asyncio
368
+ async def test_large_result_set_handling(self):
369
+ """Test handling of large result sets"""
370
+ from app.repositories.db_repository import get_merchants_from_db
371
+
372
+ # Create mock data for large result set
373
+ large_dataset = [{"_id": f"merchant_{i}", "name": f"Merchant {i}"} for i in range(1000)]
374
+
375
+ with patch('app.nosql.get_mongodb_client') as mock_client:
376
+ mock_collection = MagicMock()
377
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
378
+ mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = large_dataset[:100]
379
+
380
+ result = await get_merchants_from_db(limit=100, skip=0)
381
+
382
+ assert len(result) == 100
383
+ # Verify pagination was applied
384
+ mock_collection.find.return_value.limit.assert_called_with(100)
385
+
386
+ @pytest.mark.asyncio
387
+ async def test_connection_pool_management(self):
388
+ """Test database connection pool management"""
389
+ from app.nosql import get_mongodb_client
390
+
391
+ # Test multiple client requests
392
+ clients = []
393
+ for _ in range(10):
394
+ client = get_mongodb_client()
395
+ clients.append(client)
396
+
397
+ # All clients should be the same instance (singleton pattern)
398
+ assert all(client is clients[0] for client in clients)
399
+
400
+ class TestDatabaseErrorHandling:
401
+ """Test database error handling scenarios"""
402
+
403
+ @pytest.mark.asyncio
404
+ async def test_connection_timeout_handling(self):
405
+ """Test handling of connection timeouts"""
406
+ from app.repositories.db_repository import get_merchants_from_db
407
+
408
+ with patch('app.nosql.get_mongodb_client') as mock_client:
409
+ mock_client.side_effect = Exception("Connection timeout")
410
+
411
+ with pytest.raises(Exception) as exc_info:
412
+ await get_merchants_from_db()
413
+
414
+ assert "Connection timeout" in str(exc_info.value)
415
+
416
+ @pytest.mark.asyncio
417
+ async def test_invalid_query_handling(self):
418
+ """Test handling of invalid queries"""
419
+ from app.repositories.db_repository import search_merchants_in_db
420
+
421
+ with patch('app.nosql.get_mongodb_client') as mock_client:
422
+ mock_collection = MagicMock()
423
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
424
+ mock_collection.find.side_effect = Exception("Invalid query")
425
+
426
+ with pytest.raises(Exception) as exc_info:
427
+ await search_merchants_in_db(latitude=200, longitude=200) # Invalid coordinates
428
+
429
+ assert "Invalid query" in str(exc_info.value)
430
+
431
+ @pytest.mark.asyncio
432
+ async def test_duplicate_key_error_handling(self, sample_merchant_data):
433
+ """Test handling of duplicate key errors"""
434
+ from app.repositories.db_repository import create_merchant_in_db
435
+
436
+ with patch('app.nosql.get_mongodb_client') as mock_client:
437
+ mock_collection = MagicMock()
438
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
439
+ mock_collection.insert_one.side_effect = Exception("Duplicate key error")
440
+
441
+ with pytest.raises(Exception) as exc_info:
442
+ await create_merchant_in_db(sample_merchant_data)
443
+
444
+ assert "Duplicate key error" in str(exc_info.value)
app/tests/test_integration.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Integration tests for end-to-end functionality
3
+ """
4
+
5
+ import pytest
6
+ import asyncio
7
+ from unittest.mock import patch, AsyncMock, MagicMock
8
+ from typing import Dict, Any, List
9
+
10
+ class TestEndToEndUserJourney:
11
+ """Test complete user journey scenarios"""
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_complete_search_journey(self, async_client, sample_merchant_data):
15
+ """Test complete user search journey from query to results"""
16
+ # Mock all dependencies
17
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp, \
18
+ patch('app.services.merchant.search_merchants') as mock_search:
19
+
20
+ # Setup NLP pipeline mock
21
+ mock_nlp.process_query.return_value = {
22
+ "query": "find the best hair salon near me with parking",
23
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.9},
24
+ "entities": {
25
+ "service_types": ["haircut", "hair styling"],
26
+ "amenities": ["parking"],
27
+ "location_modifiers": ["near me"],
28
+ "quality_indicators": ["best"]
29
+ },
30
+ "similar_services": [("salon", 0.95), ("beauty", 0.8)],
31
+ "search_parameters": {
32
+ "merchant_category": "salon",
33
+ "amenities": ["parking"],
34
+ "radius": 5000,
35
+ "min_rating": 4.0
36
+ },
37
+ "processing_time": 0.234
38
+ }
39
+
40
+ # Setup merchant search mock
41
+ mock_search.return_value = [sample_merchant_data]
42
+
43
+ # Step 1: User submits natural language query
44
+ nlp_response = await async_client.post("/api/v1/nlp/analyze-query", params={
45
+ "query": "find the best hair salon near me with parking",
46
+ "latitude": 40.7128,
47
+ "longitude": -74.0060
48
+ })
49
+
50
+ assert nlp_response.status_code == 200
51
+ nlp_data = nlp_response.json()
52
+ assert nlp_data["status"] == "success"
53
+ assert "analysis" in nlp_data
54
+
55
+ # Step 2: Use NLP results to search merchants
56
+ search_params = nlp_data["analysis"]["search_parameters"]
57
+ search_response = await async_client.get("/api/v1/merchants/search", params={
58
+ "latitude": 40.7128,
59
+ "longitude": -74.0060,
60
+ **search_params
61
+ })
62
+
63
+ assert search_response.status_code == 200
64
+ search_data = search_response.json()
65
+ assert len(search_data) == 1
66
+ assert search_data[0]["name"] == "Test Hair Salon"
67
+ assert "parking" in search_data[0]["amenities"]
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_merchant_discovery_flow(self, async_client, sample_merchant_data):
71
+ """Test merchant discovery and detail retrieval flow"""
72
+ with patch('app.services.merchant.get_merchants') as mock_get_all, \
73
+ patch('app.services.merchant.get_merchant_by_id') as mock_get_by_id:
74
+
75
+ mock_get_all.return_value = [sample_merchant_data]
76
+ mock_get_by_id.return_value = sample_merchant_data
77
+
78
+ # Step 1: Browse all merchants
79
+ browse_response = await async_client.get("/api/v1/merchants/")
80
+ assert browse_response.status_code == 200
81
+ merchants = browse_response.json()
82
+ assert len(merchants) == 1
83
+
84
+ # Step 2: Get detailed information for specific merchant
85
+ merchant_id = merchants[0]["_id"]
86
+ detail_response = await async_client.get(f"/api/v1/merchants/{merchant_id}")
87
+ assert detail_response.status_code == 200
88
+
89
+ merchant_detail = detail_response.json()
90
+ assert merchant_detail["_id"] == merchant_id
91
+ assert merchant_detail["name"] == "Test Hair Salon"
92
+ assert "services" in merchant_detail
93
+ assert "business_hours" in merchant_detail
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_error_recovery_flow(self, async_client):
97
+ """Test error recovery and graceful degradation"""
98
+ # Test NLP service failure with fallback
99
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp, \
100
+ patch('app.services.helper.process_free_text') as mock_fallback:
101
+
102
+ # NLP service fails
103
+ mock_nlp.process_query.side_effect = Exception("NLP service unavailable")
104
+
105
+ # Fallback service works
106
+ mock_fallback.return_value = {
107
+ "query": "hair salon",
108
+ "extracted_keywords": ["hair", "salon"],
109
+ "suggested_category": "salon"
110
+ }
111
+
112
+ # Should gracefully fall back to basic processing
113
+ response = await async_client.post("/api/v1/helpers/process-text", json={
114
+ "text": "hair salon",
115
+ "latitude": 40.7128,
116
+ "longitude": -74.0060
117
+ })
118
+
119
+ assert response.status_code == 200
120
+ data = response.json()
121
+ assert data["suggested_category"] == "salon"
122
+
123
+ class TestServiceIntegration:
124
+ """Test integration between different services"""
125
+
126
+ @pytest.mark.asyncio
127
+ async def test_nlp_to_search_integration(self, sample_merchant_data):
128
+ """Test integration between NLP processing and merchant search"""
129
+ from app.services.advanced_nlp import AdvancedNLPPipeline
130
+ from app.services.merchant import search_merchants
131
+
132
+ with patch('app.repositories.db_repository.search_merchants_in_db') as mock_db_search:
133
+ mock_db_search.return_value = [sample_merchant_data]
134
+
135
+ # Process query with NLP
136
+ pipeline = AdvancedNLPPipeline()
137
+ nlp_result = await pipeline.process_query("find a hair salon with parking")
138
+
139
+ # Use NLP results for merchant search
140
+ search_params = nlp_result["search_parameters"]
141
+ merchants = await search_merchants(**search_params)
142
+
143
+ assert len(merchants) == 1
144
+ assert merchants[0]["category"] == "salon"
145
+
146
+ @pytest.mark.asyncio
147
+ async def test_cache_integration(self, sample_merchant_data):
148
+ """Test cache integration across services"""
149
+ from app.services.merchant import get_merchant_by_id
150
+ from app.repositories.cache_repository import cache_merchant_data, get_cached_merchant_data
151
+
152
+ with patch('app.nosql.get_redis_client') as mock_redis_client, \
153
+ patch('app.repositories.db_repository.get_merchant_by_id_from_db') as mock_db:
154
+
155
+ mock_redis = AsyncMock()
156
+ mock_redis_client.return_value = mock_redis
157
+ mock_db.return_value = sample_merchant_data
158
+
159
+ # First call - cache miss, should hit database
160
+ mock_redis.get.return_value = None
161
+ result1 = await get_merchant_by_id("test_merchant_123")
162
+
163
+ # Should cache the result
164
+ mock_redis.setex.assert_called_once()
165
+
166
+ # Second call - cache hit, should not hit database
167
+ mock_redis.get.return_value = '{"_id": "test_merchant_123", "name": "Test Hair Salon"}'
168
+ result2 = await get_merchant_by_id("test_merchant_123")
169
+
170
+ assert result1 is not None
171
+ assert result2 is not None
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_database_to_api_integration(self, sample_merchant_data):
175
+ """Test integration from database layer to API layer"""
176
+ from app.repositories.db_repository import get_merchants_from_db
177
+ from app.services.merchant import get_merchants
178
+
179
+ with patch('app.nosql.get_mongodb_client') as mock_client:
180
+ mock_collection = AsyncMock()
181
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
182
+ mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = [sample_merchant_data]
183
+
184
+ # Database layer
185
+ db_result = await get_merchants_from_db(limit=10, skip=0)
186
+
187
+ # Service layer
188
+ service_result = await get_merchants(limit=10, skip=0)
189
+
190
+ assert len(db_result) == 1
191
+ assert len(service_result) == 1
192
+ assert db_result[0]["name"] == service_result[0]["name"]
193
+
194
+ class TestDataFlowIntegration:
195
+ """Test data flow through the entire system"""
196
+
197
+ @pytest.mark.asyncio
198
+ async def test_search_data_flow(self, async_client, sample_merchant_data):
199
+ """Test complete search data flow"""
200
+ with patch('app.nosql.get_mongodb_client') as mock_mongo, \
201
+ patch('app.nosql.get_redis_client') as mock_redis_client:
202
+
203
+ # Setup MongoDB mock
204
+ mock_collection = AsyncMock()
205
+ mock_mongo.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
206
+ mock_collection.find.return_value.limit.return_value.to_list.return_value = [sample_merchant_data]
207
+
208
+ # Setup Redis mock
209
+ mock_redis = AsyncMock()
210
+ mock_redis_client.return_value = mock_redis
211
+ mock_redis.get.return_value = None # Cache miss
212
+
213
+ # Make search request
214
+ response = await async_client.get("/api/v1/merchants/search", params={
215
+ "latitude": 40.7128,
216
+ "longitude": -74.0060,
217
+ "radius": 5000,
218
+ "category": "salon"
219
+ })
220
+
221
+ assert response.status_code == 200
222
+ data = response.json()
223
+ assert len(data) == 1
224
+ assert data[0]["name"] == "Test Hair Salon"
225
+
226
+ # Verify data flowed through all layers
227
+ mock_collection.find.assert_called_once() # Database was queried
228
+ mock_redis.setex.assert_called_once() # Result was cached
229
+
230
+ @pytest.mark.asyncio
231
+ async def test_nlp_processing_data_flow(self, async_client):
232
+ """Test NLP processing data flow"""
233
+ with patch('app.services.advanced_nlp.IntentClassifier') as mock_intent, \
234
+ patch('app.services.advanced_nlp.BusinessEntityExtractor') as mock_entity, \
235
+ patch('app.services.advanced_nlp.SemanticMatcher') as mock_semantic:
236
+
237
+ # Setup mocks
238
+ mock_intent_instance = MagicMock()
239
+ mock_intent.return_value = mock_intent_instance
240
+ mock_intent_instance.get_primary_intent.return_value = ("SEARCH_SERVICE", 0.9)
241
+
242
+ mock_entity_instance = MagicMock()
243
+ mock_entity.return_value = mock_entity_instance
244
+ mock_entity_instance.extract_entities.return_value = {"service_types": ["haircut"]}
245
+
246
+ mock_semantic_instance = MagicMock()
247
+ mock_semantic.return_value = mock_semantic_instance
248
+ mock_semantic_instance.find_similar_services.return_value = [("salon", 0.9)]
249
+
250
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={
251
+ "query": "find a hair salon"
252
+ })
253
+
254
+ assert response.status_code == 200
255
+ data = response.json()
256
+ assert data["status"] == "success"
257
+
258
+ # Verify data flowed through NLP pipeline
259
+ mock_intent_instance.get_primary_intent.assert_called_once()
260
+ mock_entity_instance.extract_entities.assert_called_once()
261
+ mock_semantic_instance.find_similar_services.assert_called_once()
262
+
263
+ class TestConcurrencyIntegration:
264
+ """Test concurrent operations and race conditions"""
265
+
266
+ @pytest.mark.asyncio
267
+ async def test_concurrent_cache_operations(self, sample_merchant_data):
268
+ """Test concurrent cache read/write operations"""
269
+ from app.repositories.cache_repository import cache_merchant_data, get_cached_merchant_data
270
+
271
+ with patch('app.nosql.get_redis_client') as mock_redis_client:
272
+ mock_redis = AsyncMock()
273
+ mock_redis_client.return_value = mock_redis
274
+ mock_redis.setex.return_value = True
275
+ mock_redis.get.return_value = '{"_id": "test_merchant_123", "name": "Test Hair Salon"}'
276
+
277
+ # Create concurrent cache operations
278
+ cache_tasks = [
279
+ cache_merchant_data(f"merchant_{i}", sample_merchant_data)
280
+ for i in range(10)
281
+ ]
282
+
283
+ read_tasks = [
284
+ get_cached_merchant_data(f"merchant_{i}")
285
+ for i in range(10)
286
+ ]
287
+
288
+ # Execute concurrently
289
+ cache_results = await asyncio.gather(*cache_tasks)
290
+ read_results = await asyncio.gather(*read_tasks)
291
+
292
+ assert all(result is True for result in cache_results)
293
+ assert all(result is not None for result in read_results)
294
+
295
+ @pytest.mark.asyncio
296
+ async def test_concurrent_nlp_processing(self):
297
+ """Test concurrent NLP processing"""
298
+ from app.services.advanced_nlp import AdvancedNLPPipeline
299
+
300
+ pipeline = AdvancedNLPPipeline()
301
+ queries = [
302
+ "find a hair salon",
303
+ "best spa near me",
304
+ "gym with parking",
305
+ "dental clinic",
306
+ "massage therapy"
307
+ ]
308
+
309
+ # Process queries concurrently
310
+ tasks = [pipeline.process_query(query) for query in queries]
311
+ results = await asyncio.gather(*tasks)
312
+
313
+ assert len(results) == 5
314
+ assert all("query" in result for result in results)
315
+ assert all("primary_intent" in result for result in results)
316
+
317
+ @pytest.mark.asyncio
318
+ async def test_concurrent_database_operations(self, sample_merchant_data):
319
+ """Test concurrent database operations"""
320
+ from app.repositories.db_repository import get_merchant_by_id_from_db, search_merchants_in_db
321
+
322
+ with patch('app.nosql.get_mongodb_client') as mock_client:
323
+ mock_collection = AsyncMock()
324
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
325
+ mock_collection.find_one.return_value = sample_merchant_data
326
+ mock_collection.find.return_value.limit.return_value.to_list.return_value = [sample_merchant_data]
327
+
328
+ # Mix of different database operations
329
+ tasks = []
330
+
331
+ # Add get by ID tasks
332
+ for i in range(5):
333
+ tasks.append(get_merchant_by_id_from_db(f"merchant_{i}"))
334
+
335
+ # Add search tasks
336
+ for i in range(5):
337
+ tasks.append(search_merchants_in_db(
338
+ latitude=40.7128 + (i * 0.01),
339
+ longitude=-74.0060 + (i * 0.01),
340
+ radius=5000
341
+ ))
342
+
343
+ results = await asyncio.gather(*tasks)
344
+
345
+ assert len(results) == 10
346
+ # First 5 should be single merchants
347
+ assert all(isinstance(result, dict) for result in results[:5])
348
+ # Last 5 should be lists of merchants
349
+ assert all(isinstance(result, list) for result in results[5:])
350
+
351
+ class TestErrorHandlingIntegration:
352
+ """Test error handling across integrated components"""
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_database_failure_propagation(self, async_client):
356
+ """Test how database failures propagate through the system"""
357
+ with patch('app.nosql.get_mongodb_client') as mock_client:
358
+ mock_client.side_effect = Exception("Database connection failed")
359
+
360
+ response = await async_client.get("/api/v1/merchants/")
361
+ assert response.status_code == 500
362
+
363
+ @pytest.mark.asyncio
364
+ async def test_cache_failure_graceful_degradation(self, async_client, sample_merchant_data):
365
+ """Test graceful degradation when cache fails"""
366
+ with patch('app.nosql.get_redis_client') as mock_redis_client, \
367
+ patch('app.repositories.db_repository.get_merchants_from_db') as mock_db:
368
+
369
+ # Cache fails
370
+ mock_redis_client.side_effect = Exception("Redis connection failed")
371
+ # Database works
372
+ mock_db.return_value = [sample_merchant_data]
373
+
374
+ response = await async_client.get("/api/v1/merchants/")
375
+
376
+ # Should still work, just without caching
377
+ assert response.status_code == 200
378
+ data = response.json()
379
+ assert len(data) == 1
380
+
381
+ @pytest.mark.asyncio
382
+ async def test_nlp_service_fallback(self, async_client):
383
+ """Test fallback when NLP service fails"""
384
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp, \
385
+ patch('app.services.helper.process_free_text') as mock_fallback:
386
+
387
+ # NLP service fails
388
+ mock_nlp.process_query.side_effect = Exception("NLP service error")
389
+
390
+ # Fallback service works
391
+ mock_fallback.return_value = {
392
+ "query": "hair salon",
393
+ "extracted_keywords": ["hair", "salon"],
394
+ "suggested_category": "salon"
395
+ }
396
+
397
+ # Try NLP endpoint first (should fail)
398
+ nlp_response = await async_client.post("/api/v1/nlp/analyze-query", params={
399
+ "query": "hair salon"
400
+ })
401
+ assert nlp_response.status_code == 500
402
+
403
+ # Use fallback endpoint (should work)
404
+ fallback_response = await async_client.post("/api/v1/helpers/process-text", json={
405
+ "text": "hair salon",
406
+ "latitude": 40.7128,
407
+ "longitude": -74.0060
408
+ })
409
+ assert fallback_response.status_code == 200
410
+
411
+ class TestSecurityIntegration:
412
+ """Test security measures across integrated components"""
413
+
414
+ @pytest.mark.asyncio
415
+ async def test_input_sanitization_flow(self, async_client):
416
+ """Test input sanitization across the request flow"""
417
+ # Test with potentially malicious input
418
+ malicious_query = "<script>alert('xss')</script>find salon"
419
+
420
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp:
421
+ mock_nlp.process_query.return_value = {
422
+ "query": "find salon", # Should be sanitized
423
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.8},
424
+ "entities": {},
425
+ "similar_services": [],
426
+ "search_parameters": {},
427
+ "processing_time": 0.1
428
+ }
429
+
430
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={
431
+ "query": malicious_query
432
+ })
433
+
434
+ assert response.status_code == 200
435
+ data = response.json()
436
+ # Script tags should be removed/sanitized
437
+ assert "<script>" not in data["analysis"]["query"]
438
+
439
+ @pytest.mark.asyncio
440
+ async def test_rate_limiting_integration(self, async_client):
441
+ """Test rate limiting across endpoints"""
442
+ # This would require actual rate limiting implementation
443
+ # For now, test that multiple requests don't crash the system
444
+
445
+ tasks = [
446
+ async_client.get("/health")
447
+ for _ in range(50)
448
+ ]
449
+
450
+ responses = await asyncio.gather(*tasks, return_exceptions=True)
451
+
452
+ # All requests should either succeed or be rate limited (not crash)
453
+ for response in responses:
454
+ if hasattr(response, 'status_code'):
455
+ assert response.status_code in [200, 429] # OK or Too Many Requests
456
+
457
+ class TestPerformanceIntegration:
458
+ """Test performance characteristics of integrated system"""
459
+
460
+ @pytest.mark.asyncio
461
+ async def test_end_to_end_performance(self, async_client, sample_merchant_data):
462
+ """Test end-to-end performance of complete user journey"""
463
+ import time
464
+
465
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp, \
466
+ patch('app.services.merchant.search_merchants') as mock_search:
467
+
468
+ mock_nlp.process_query.return_value = {
469
+ "query": "find salon",
470
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.8},
471
+ "entities": {"service_types": ["haircut"]},
472
+ "similar_services": [("salon", 0.9)],
473
+ "search_parameters": {"merchant_category": "salon"},
474
+ "processing_time": 0.1
475
+ }
476
+
477
+ mock_search.return_value = [sample_merchant_data]
478
+
479
+ start_time = time.time()
480
+
481
+ # Step 1: NLP processing
482
+ nlp_response = await async_client.post("/api/v1/nlp/analyze-query", params={
483
+ "query": "find a hair salon"
484
+ })
485
+
486
+ # Step 2: Merchant search
487
+ search_response = await async_client.get("/api/v1/merchants/search", params={
488
+ "category": "salon"
489
+ })
490
+
491
+ total_time = time.time() - start_time
492
+
493
+ assert nlp_response.status_code == 200
494
+ assert search_response.status_code == 200
495
+ assert total_time < 3.0 # Complete journey within 3 seconds
app/tests/test_performance.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Performance regression tests for the application
3
+ """
4
+
5
+ import pytest
6
+ import time
7
+ import asyncio
8
+ from typing import List, Dict, Any
9
+ from unittest.mock import patch, AsyncMock
10
+ import statistics
11
+
12
+ class TestAPIPerformance:
13
+ """Test API endpoint performance"""
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_health_endpoint_response_time(self, async_client):
17
+ """Test health endpoint response time"""
18
+ start_time = time.time()
19
+ response = await async_client.get("/health")
20
+ response_time = time.time() - start_time
21
+
22
+ assert response.status_code == 200
23
+ assert response_time < 0.1 # Should respond within 100ms
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_merchant_search_performance(self, async_client, sample_merchant_data):
27
+ """Test merchant search endpoint performance"""
28
+ with patch('app.services.merchant.search_merchants') as mock_search:
29
+ mock_search.return_value = [sample_merchant_data] * 10
30
+
31
+ start_time = time.time()
32
+ response = await async_client.get("/api/v1/merchants/search", params={
33
+ "latitude": 40.7128,
34
+ "longitude": -74.0060,
35
+ "radius": 5000,
36
+ "category": "salon"
37
+ })
38
+ response_time = time.time() - start_time
39
+
40
+ assert response.status_code == 200
41
+ assert response_time < 1.0 # Should respond within 1 second
42
+ assert len(response.json()) == 10
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_nlp_processing_performance(self, async_client, mock_nlp_pipeline):
46
+ """Test NLP processing endpoint performance"""
47
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_pipeline:
48
+ mock_pipeline.process_query = mock_nlp_pipeline.process_query
49
+
50
+ start_time = time.time()
51
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={
52
+ "query": "find the best hair salon near me with parking"
53
+ })
54
+ response_time = time.time() - start_time
55
+
56
+ assert response.status_code == 200
57
+ assert response_time < 2.0 # Should respond within 2 seconds
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_concurrent_api_requests(self, async_client, sample_merchant_data):
61
+ """Test concurrent API request handling"""
62
+ with patch('app.services.merchant.get_merchants') as mock_get:
63
+ mock_get.return_value = [sample_merchant_data] * 5
64
+
65
+ # Create 20 concurrent requests
66
+ tasks = [
67
+ async_client.get("/api/v1/merchants/")
68
+ for _ in range(20)
69
+ ]
70
+
71
+ start_time = time.time()
72
+ responses = await asyncio.gather(*tasks)
73
+ total_time = time.time() - start_time
74
+
75
+ # All requests should succeed
76
+ assert all(r.status_code == 200 for r in responses)
77
+ # Should handle concurrent requests efficiently
78
+ assert total_time < 5.0 # Within 5 seconds for 20 requests
79
+
80
+ @pytest.mark.asyncio
81
+ async def test_large_result_set_performance(self, async_client):
82
+ """Test performance with large result sets"""
83
+ # Mock large dataset
84
+ large_dataset = [
85
+ {
86
+ "_id": f"merchant_{i}",
87
+ "name": f"Merchant {i}",
88
+ "category": "salon",
89
+ "location": {"type": "Point", "coordinates": [-74.0060, 40.7128]}
90
+ }
91
+ for i in range(100)
92
+ ]
93
+
94
+ with patch('app.services.merchant.get_merchants') as mock_get:
95
+ mock_get.return_value = large_dataset
96
+
97
+ start_time = time.time()
98
+ response = await async_client.get("/api/v1/merchants/", params={"limit": 100})
99
+ response_time = time.time() - start_time
100
+
101
+ assert response.status_code == 200
102
+ assert len(response.json()) == 100
103
+ assert response_time < 2.0 # Should handle large datasets efficiently
104
+
105
+ class TestDatabasePerformance:
106
+ """Test database operation performance"""
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_single_merchant_query_performance(self, sample_merchant_data):
110
+ """Test single merchant query performance"""
111
+ from app.repositories.db_repository import get_merchant_by_id_from_db
112
+
113
+ with patch('app.nosql.get_mongodb_client') as mock_client:
114
+ mock_collection = AsyncMock()
115
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
116
+ mock_collection.find_one.return_value = sample_merchant_data
117
+
118
+ start_time = time.time()
119
+ result = await get_merchant_by_id_from_db("test_merchant_123")
120
+ query_time = time.time() - start_time
121
+
122
+ assert result is not None
123
+ assert query_time < 0.1 # Should complete within 100ms
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_geospatial_search_performance(self, sample_merchant_data):
127
+ """Test geospatial search performance"""
128
+ from app.repositories.db_repository import search_merchants_in_db
129
+
130
+ with patch('app.nosql.get_mongodb_client') as mock_client:
131
+ mock_collection = AsyncMock()
132
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
133
+ mock_collection.find.return_value.limit.return_value.to_list.return_value = [sample_merchant_data] * 20
134
+
135
+ start_time = time.time()
136
+ result = await search_merchants_in_db(
137
+ latitude=40.7128,
138
+ longitude=-74.0060,
139
+ radius=5000,
140
+ category="salon"
141
+ )
142
+ query_time = time.time() - start_time
143
+
144
+ assert len(result) == 20
145
+ assert query_time < 0.5 # Should complete within 500ms
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_concurrent_database_queries(self, sample_merchant_data):
149
+ """Test concurrent database query performance"""
150
+ from app.repositories.db_repository import get_merchant_by_id_from_db
151
+
152
+ with patch('app.nosql.get_mongodb_client') as mock_client:
153
+ mock_collection = AsyncMock()
154
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
155
+ mock_collection.find_one.return_value = sample_merchant_data
156
+
157
+ # Create 50 concurrent queries
158
+ tasks = [
159
+ get_merchant_by_id_from_db(f"merchant_{i}")
160
+ for i in range(50)
161
+ ]
162
+
163
+ start_time = time.time()
164
+ results = await asyncio.gather(*tasks)
165
+ total_time = time.time() - start_time
166
+
167
+ assert len(results) == 50
168
+ assert all(r is not None for r in results)
169
+ assert total_time < 2.0 # Should handle 50 concurrent queries within 2 seconds
170
+
171
+ @pytest.mark.asyncio
172
+ async def test_cache_performance(self, sample_merchant_data):
173
+ """Test cache operation performance"""
174
+ from app.repositories.cache_repository import cache_merchant_data, get_cached_merchant_data
175
+
176
+ with patch('app.nosql.get_redis_client') as mock_client:
177
+ mock_redis = AsyncMock()
178
+ mock_client.return_value = mock_redis
179
+ mock_redis.setex.return_value = True
180
+ mock_redis.get.return_value = '{"_id": "test_merchant_123", "name": "Test Hair Salon"}'
181
+
182
+ # Test cache write performance
183
+ start_time = time.time()
184
+ await cache_merchant_data("test_merchant_123", sample_merchant_data)
185
+ cache_write_time = time.time() - start_time
186
+
187
+ # Test cache read performance
188
+ start_time = time.time()
189
+ result = await get_cached_merchant_data("test_merchant_123")
190
+ cache_read_time = time.time() - start_time
191
+
192
+ assert cache_write_time < 0.05 # Cache write within 50ms
193
+ assert cache_read_time < 0.05 # Cache read within 50ms
194
+ assert result is not None
195
+
196
+ class TestNLPPerformance:
197
+ """Test NLP processing performance"""
198
+
199
+ @pytest.mark.asyncio
200
+ async def test_intent_classification_performance(self):
201
+ """Test intent classification performance"""
202
+ from app.services.advanced_nlp import IntentClassifier
203
+
204
+ classifier = IntentClassifier()
205
+ test_queries = [
206
+ "find a hair salon",
207
+ "best spa near me",
208
+ "gym with parking",
209
+ "dental clinic open now",
210
+ "massage therapy luxury"
211
+ ]
212
+
213
+ start_time = time.time()
214
+ results = [classifier.get_primary_intent(query) for query in test_queries]
215
+ total_time = time.time() - start_time
216
+
217
+ assert len(results) == 5
218
+ assert all(len(result) == 2 for result in results) # (intent, confidence)
219
+ assert total_time < 1.0 # Should classify 5 queries within 1 second
220
+
221
+ @pytest.mark.asyncio
222
+ async def test_entity_extraction_performance(self):
223
+ """Test entity extraction performance"""
224
+ from app.services.advanced_nlp import BusinessEntityExtractor
225
+
226
+ extractor = BusinessEntityExtractor()
227
+ test_queries = [
228
+ "hair salon with parking near me",
229
+ "luxury spa treatment with wifi",
230
+ "budget-friendly gym open 24/7",
231
+ "pet-friendly grooming service",
232
+ "organic restaurant with outdoor seating"
233
+ ]
234
+
235
+ start_time = time.time()
236
+ results = [extractor.extract_entities(query) for query in test_queries]
237
+ total_time = time.time() - start_time
238
+
239
+ assert len(results) == 5
240
+ assert all(isinstance(result, dict) for result in results)
241
+ assert total_time < 2.0 # Should extract entities from 5 queries within 2 seconds
242
+
243
+ @pytest.mark.asyncio
244
+ async def test_semantic_matching_performance(self):
245
+ """Test semantic matching performance"""
246
+ from app.services.advanced_nlp import SemanticMatcher
247
+
248
+ matcher = SemanticMatcher()
249
+ test_queries = [
250
+ "hair salon",
251
+ "spa treatment",
252
+ "fitness center",
253
+ "dental care",
254
+ "massage therapy"
255
+ ]
256
+
257
+ start_time = time.time()
258
+ results = [matcher.find_similar_services(query) for query in test_queries]
259
+ total_time = time.time() - start_time
260
+
261
+ assert len(results) == 5
262
+ assert all(isinstance(result, list) for result in results)
263
+ assert total_time < 1.5 # Should find matches for 5 queries within 1.5 seconds
264
+
265
+ @pytest.mark.asyncio
266
+ async def test_complete_nlp_pipeline_performance(self):
267
+ """Test complete NLP pipeline performance"""
268
+ from app.services.advanced_nlp import AdvancedNLPPipeline
269
+
270
+ pipeline = AdvancedNLPPipeline()
271
+ test_queries = [
272
+ "find the best hair salon near me with parking",
273
+ "luxury spa treatment open now",
274
+ "budget-friendly gym with pool",
275
+ "pet-friendly grooming service",
276
+ "organic restaurant with delivery"
277
+ ]
278
+
279
+ processing_times = []
280
+
281
+ for query in test_queries:
282
+ start_time = time.time()
283
+ result = await pipeline.process_query(query)
284
+ processing_time = time.time() - start_time
285
+ processing_times.append(processing_time)
286
+
287
+ assert "processing_time" in result
288
+ assert processing_time < 3.0 # Each query within 3 seconds
289
+
290
+ # Calculate statistics
291
+ avg_time = statistics.mean(processing_times)
292
+ max_time = max(processing_times)
293
+
294
+ assert avg_time < 2.0 # Average processing time under 2 seconds
295
+ assert max_time < 3.0 # Maximum processing time under 3 seconds
296
+
297
+ class TestMemoryPerformance:
298
+ """Test memory usage and performance"""
299
+
300
+ @pytest.mark.asyncio
301
+ async def test_memory_usage_during_processing(self):
302
+ """Test memory usage during heavy processing"""
303
+ import psutil
304
+ import os
305
+
306
+ process = psutil.Process(os.getpid())
307
+ initial_memory = process.memory_info().rss / 1024 / 1024 # MB
308
+
309
+ # Simulate heavy processing
310
+ from app.services.advanced_nlp import AdvancedNLPPipeline
311
+ pipeline = AdvancedNLPPipeline()
312
+
313
+ # Process multiple queries
314
+ queries = [f"find service {i}" for i in range(100)]
315
+ tasks = [pipeline.process_query(query) for query in queries]
316
+ await asyncio.gather(*tasks)
317
+
318
+ final_memory = process.memory_info().rss / 1024 / 1024 # MB
319
+ memory_increase = final_memory - initial_memory
320
+
321
+ # Memory increase should be reasonable (less than 100MB for 100 queries)
322
+ assert memory_increase < 100
323
+
324
+ @pytest.mark.asyncio
325
+ async def test_cache_memory_efficiency(self):
326
+ """Test cache memory efficiency"""
327
+ from app.services.advanced_nlp import AsyncNLPProcessor
328
+
329
+ processor = AsyncNLPProcessor(max_cache_size=100)
330
+
331
+ def dummy_processor(text):
332
+ return {"processed": text}
333
+
334
+ # Fill cache beyond limit
335
+ for i in range(150):
336
+ await processor.process_async(f"query_{i}", dummy_processor)
337
+
338
+ # Cache should not exceed max size
339
+ assert len(processor.cache) <= 100
340
+
341
+ @pytest.mark.asyncio
342
+ async def test_garbage_collection_efficiency(self):
343
+ """Test garbage collection during processing"""
344
+ import gc
345
+
346
+ # Force garbage collection
347
+ gc.collect()
348
+ initial_objects = len(gc.get_objects())
349
+
350
+ # Create and process many objects
351
+ from app.services.advanced_nlp import AdvancedNLPPipeline
352
+ pipeline = AdvancedNLPPipeline()
353
+
354
+ for i in range(50):
355
+ await pipeline.process_query(f"test query {i}")
356
+
357
+ # Force garbage collection again
358
+ gc.collect()
359
+ final_objects = len(gc.get_objects())
360
+
361
+ # Object count should not grow excessively
362
+ object_increase = final_objects - initial_objects
363
+ assert object_increase < 1000 # Reasonable object increase
364
+
365
+ class TestLoadTesting:
366
+ """Load testing scenarios"""
367
+
368
+ @pytest.mark.asyncio
369
+ async def test_sustained_load_performance(self, async_client, sample_merchant_data):
370
+ """Test performance under sustained load"""
371
+ with patch('app.services.merchant.get_merchants') as mock_get:
372
+ mock_get.return_value = [sample_merchant_data] * 10
373
+
374
+ # Simulate sustained load for 30 seconds
375
+ end_time = time.time() + 30
376
+ request_count = 0
377
+ response_times = []
378
+
379
+ while time.time() < end_time:
380
+ start_time = time.time()
381
+ response = await async_client.get("/api/v1/merchants/")
382
+ response_time = time.time() - start_time
383
+
384
+ response_times.append(response_time)
385
+ request_count += 1
386
+
387
+ assert response.status_code == 200
388
+
389
+ # Small delay to prevent overwhelming
390
+ await asyncio.sleep(0.1)
391
+
392
+ # Calculate performance metrics
393
+ avg_response_time = statistics.mean(response_times)
394
+ max_response_time = max(response_times)
395
+ requests_per_second = request_count / 30
396
+
397
+ assert avg_response_time < 1.0 # Average response time under 1 second
398
+ assert max_response_time < 3.0 # Max response time under 3 seconds
399
+ assert requests_per_second > 5 # At least 5 requests per second
400
+
401
+ @pytest.mark.asyncio
402
+ async def test_burst_load_handling(self, async_client, sample_merchant_data):
403
+ """Test handling of burst load"""
404
+ with patch('app.services.merchant.search_merchants') as mock_search:
405
+ mock_search.return_value = [sample_merchant_data] * 5
406
+
407
+ # Create burst of 100 concurrent requests
408
+ tasks = [
409
+ async_client.get("/api/v1/merchants/search", params={
410
+ "latitude": 40.7128,
411
+ "longitude": -74.0060,
412
+ "radius": 5000
413
+ })
414
+ for _ in range(100)
415
+ ]
416
+
417
+ start_time = time.time()
418
+ responses = await asyncio.gather(*tasks, return_exceptions=True)
419
+ total_time = time.time() - start_time
420
+
421
+ # Count successful responses
422
+ successful_responses = [r for r in responses if hasattr(r, 'status_code') and r.status_code == 200]
423
+ success_rate = len(successful_responses) / len(responses)
424
+
425
+ assert success_rate > 0.95 # At least 95% success rate
426
+ assert total_time < 10.0 # Complete within 10 seconds
427
+
428
+ class TestPerformanceRegression:
429
+ """Performance regression detection"""
430
+
431
+ @pytest.mark.asyncio
432
+ async def test_api_response_time_regression(self, async_client, performance_test_data):
433
+ """Test for API response time regression"""
434
+ queries = performance_test_data["queries"]
435
+ max_expected_time = performance_test_data["expected_max_response_time"]
436
+
437
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_pipeline:
438
+ mock_pipeline.process_query.return_value = {
439
+ "query": "test",
440
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.8},
441
+ "entities": {},
442
+ "similar_services": [],
443
+ "search_parameters": {},
444
+ "processing_time": 0.1
445
+ }
446
+
447
+ response_times = []
448
+
449
+ for query in queries:
450
+ start_time = time.time()
451
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={"query": query})
452
+ response_time = time.time() - start_time
453
+ response_times.append(response_time)
454
+
455
+ assert response.status_code == 200
456
+
457
+ avg_response_time = statistics.mean(response_times)
458
+ max_response_time = max(response_times)
459
+
460
+ # Check for performance regression
461
+ assert avg_response_time < max_expected_time
462
+ assert max_response_time < max_expected_time * 2 # Allow some variance
463
+
464
+ @pytest.mark.asyncio
465
+ async def test_database_query_performance_regression(self, sample_merchant_data):
466
+ """Test for database query performance regression"""
467
+ from app.repositories.db_repository import search_merchants_in_db
468
+
469
+ with patch('app.nosql.get_mongodb_client') as mock_client:
470
+ mock_collection = AsyncMock()
471
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
472
+ mock_collection.find.return_value.limit.return_value.to_list.return_value = [sample_merchant_data] * 10
473
+
474
+ query_times = []
475
+
476
+ # Test multiple search scenarios
477
+ for i in range(10):
478
+ start_time = time.time()
479
+ await search_merchants_in_db(
480
+ latitude=40.7128 + (i * 0.01),
481
+ longitude=-74.0060 + (i * 0.01),
482
+ radius=5000,
483
+ category="salon"
484
+ )
485
+ query_time = time.time() - start_time
486
+ query_times.append(query_time)
487
+
488
+ avg_query_time = statistics.mean(query_times)
489
+ max_query_time = max(query_times)
490
+
491
+ # Database queries should be fast
492
+ assert avg_query_time < 0.1 # Average under 100ms
493
+ assert max_query_time < 0.2 # Maximum under 200ms
app/tests/test_regression_suite.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main regression test suite runner and comprehensive system tests
3
+ """
4
+
5
+ import pytest
6
+ import asyncio
7
+ import time
8
+ import statistics
9
+ from typing import Dict, Any, List
10
+ from unittest.mock import patch, AsyncMock
11
+
12
+ class TestRegressionSuite:
13
+ """Main regression test suite for critical functionality"""
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_critical_path_regression(self, async_client, sample_merchant_data):
17
+ """Test the most critical user path for regressions"""
18
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp, \
19
+ patch('app.services.merchant.search_merchants') as mock_search:
20
+
21
+ # Setup mocks for critical path
22
+ mock_nlp.process_query.return_value = {
23
+ "query": "find the best hair salon near me",
24
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.9},
25
+ "entities": {"service_types": ["haircut"], "location_modifiers": ["near me"]},
26
+ "similar_services": [("salon", 0.95)],
27
+ "search_parameters": {"merchant_category": "salon", "radius": 5000},
28
+ "processing_time": 0.15
29
+ }
30
+ mock_search.return_value = [sample_merchant_data]
31
+
32
+ # Critical Path: Health Check -> NLP Processing -> Merchant Search
33
+
34
+ # Step 1: System health
35
+ health_response = await async_client.get("/health")
36
+ assert health_response.status_code == 200
37
+ assert health_response.json()["status"] == "healthy"
38
+
39
+ # Step 2: NLP processing
40
+ nlp_response = await async_client.post("/api/v1/nlp/analyze-query", params={
41
+ "query": "find the best hair salon near me",
42
+ "latitude": 40.7128,
43
+ "longitude": -74.0060
44
+ })
45
+ assert nlp_response.status_code == 200
46
+ nlp_data = nlp_response.json()
47
+ assert nlp_data["status"] == "success"
48
+ assert "analysis" in nlp_data
49
+
50
+ # Step 3: Merchant search using NLP results
51
+ search_params = nlp_data["analysis"]["search_parameters"]
52
+ search_response = await async_client.get("/api/v1/merchants/search", params={
53
+ "latitude": 40.7128,
54
+ "longitude": -74.0060,
55
+ **search_params
56
+ })
57
+ assert search_response.status_code == 200
58
+ merchants = search_response.json()
59
+ assert len(merchants) >= 1
60
+ assert merchants[0]["name"] == "Test Hair Salon"
61
+
62
+ @pytest.mark.asyncio
63
+ async def test_api_contract_regression(self, async_client):
64
+ """Test that API contracts haven't changed unexpectedly"""
65
+ # Test health endpoint contract
66
+ health_response = await async_client.get("/health")
67
+ assert health_response.status_code == 200
68
+ health_data = health_response.json()
69
+
70
+ required_health_fields = ["status", "timestamp", "service", "version"]
71
+ for field in required_health_fields:
72
+ assert field in health_data, f"Health endpoint missing required field: {field}"
73
+
74
+ # Test NLP supported intents contract
75
+ intents_response = await async_client.get("/api/v1/nlp/supported-intents")
76
+ assert intents_response.status_code == 200
77
+ intents_data = intents_response.json()
78
+
79
+ assert "status" in intents_data
80
+ assert "supported_intents" in intents_data
81
+ assert intents_data["status"] == "success"
82
+
83
+ # Verify expected intents are still supported
84
+ expected_intents = ["SEARCH_SERVICE", "FILTER_QUALITY", "FILTER_LOCATION"]
85
+ for intent in expected_intents:
86
+ assert intent in intents_data["supported_intents"]
87
+
88
+ @pytest.mark.asyncio
89
+ async def test_performance_regression_baseline(self, async_client, performance_test_data):
90
+ """Test for performance regressions against baseline"""
91
+ queries = performance_test_data["queries"][:5] # Test subset for speed
92
+ max_expected_time = performance_test_data["expected_max_response_time"]
93
+
94
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp:
95
+ mock_nlp.process_query.return_value = {
96
+ "query": "test",
97
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.8},
98
+ "entities": {},
99
+ "similar_services": [],
100
+ "search_parameters": {},
101
+ "processing_time": 0.1
102
+ }
103
+
104
+ response_times = []
105
+
106
+ for query in queries:
107
+ start_time = time.time()
108
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={"query": query})
109
+ response_time = time.time() - start_time
110
+ response_times.append(response_time)
111
+
112
+ assert response.status_code == 200
113
+
114
+ avg_response_time = statistics.mean(response_times)
115
+ p95_response_time = sorted(response_times)[int(len(response_times) * 0.95)]
116
+
117
+ # Performance regression checks
118
+ assert avg_response_time < max_expected_time, f"Average response time {avg_response_time:.3f}s exceeds baseline {max_expected_time}s"
119
+ assert p95_response_time < max_expected_time * 1.5, f"P95 response time {p95_response_time:.3f}s exceeds acceptable threshold"
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_error_handling_regression(self, async_client):
123
+ """Test that error handling hasn't regressed"""
124
+ # Test various error scenarios
125
+ error_scenarios = [
126
+ # Invalid coordinates
127
+ {
128
+ "endpoint": "/api/v1/merchants/search",
129
+ "params": {"latitude": 999, "longitude": 999},
130
+ "expected_codes": [400, 422]
131
+ },
132
+ # Empty NLP query
133
+ {
134
+ "endpoint": "/api/v1/nlp/analyze-query",
135
+ "params": {"query": ""},
136
+ "expected_codes": [400, 422]
137
+ },
138
+ # Non-existent merchant
139
+ {
140
+ "endpoint": "/api/v1/merchants/nonexistent_id",
141
+ "params": {},
142
+ "expected_codes": [404]
143
+ },
144
+ # Invalid endpoint
145
+ {
146
+ "endpoint": "/api/v1/invalid-endpoint",
147
+ "params": {},
148
+ "expected_codes": [404]
149
+ }
150
+ ]
151
+
152
+ for scenario in error_scenarios:
153
+ if scenario["endpoint"].endswith("analyze-query"):
154
+ response = await async_client.post(scenario["endpoint"], params=scenario["params"])
155
+ else:
156
+ response = await async_client.get(scenario["endpoint"], params=scenario["params"])
157
+
158
+ assert response.status_code in scenario["expected_codes"], \
159
+ f"Endpoint {scenario['endpoint']} returned {response.status_code}, expected one of {scenario['expected_codes']}"
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_data_consistency_regression(self, async_client, sample_merchant_data):
163
+ """Test data consistency across different endpoints"""
164
+ with patch('app.services.merchant.get_merchant_by_id') as mock_get_by_id, \
165
+ patch('app.services.merchant.search_merchants') as mock_search:
166
+
167
+ mock_get_by_id.return_value = sample_merchant_data
168
+ mock_search.return_value = [sample_merchant_data]
169
+
170
+ # Get merchant by ID
171
+ merchant_response = await async_client.get(f"/api/v1/merchants/{sample_merchant_data['_id']}")
172
+ assert merchant_response.status_code == 200
173
+ merchant_data = merchant_response.json()
174
+
175
+ # Search for the same merchant
176
+ search_response = await async_client.get("/api/v1/merchants/search", params={
177
+ "category": sample_merchant_data["category"],
178
+ "latitude": 40.7128,
179
+ "longitude": -74.0060
180
+ })
181
+ assert search_response.status_code == 200
182
+ search_results = search_response.json()
183
+
184
+ # Data should be consistent
185
+ assert len(search_results) >= 1
186
+ found_merchant = next((m for m in search_results if m["_id"] == sample_merchant_data["_id"]), None)
187
+ assert found_merchant is not None
188
+
189
+ # Key fields should match
190
+ key_fields = ["_id", "name", "category", "average_rating"]
191
+ for field in key_fields:
192
+ assert merchant_data[field] == found_merchant[field], f"Field {field} inconsistent between endpoints"
193
+
194
+ class TestSystemStability:
195
+ """Test system stability under various conditions"""
196
+
197
+ @pytest.mark.asyncio
198
+ async def test_concurrent_load_stability(self, async_client, sample_merchant_data):
199
+ """Test system stability under concurrent load"""
200
+ with patch('app.services.merchant.get_merchants') as mock_get:
201
+ mock_get.return_value = [sample_merchant_data] * 10
202
+
203
+ # Create concurrent requests
204
+ concurrent_requests = 50
205
+ tasks = [
206
+ async_client.get("/api/v1/merchants/")
207
+ for _ in range(concurrent_requests)
208
+ ]
209
+
210
+ # Execute all requests concurrently
211
+ responses = await asyncio.gather(*tasks, return_exceptions=True)
212
+
213
+ # Analyze results
214
+ successful_responses = 0
215
+ error_responses = 0
216
+ exceptions = 0
217
+
218
+ for response in responses:
219
+ if isinstance(response, Exception):
220
+ exceptions += 1
221
+ elif hasattr(response, 'status_code'):
222
+ if response.status_code == 200:
223
+ successful_responses += 1
224
+ else:
225
+ error_responses += 1
226
+ else:
227
+ exceptions += 1
228
+
229
+ # System should handle concurrent load gracefully
230
+ success_rate = successful_responses / concurrent_requests
231
+ assert success_rate >= 0.9, f"Success rate {success_rate:.2%} below acceptable threshold"
232
+ assert exceptions == 0, f"Got {exceptions} exceptions during concurrent load test"
233
+
234
+ @pytest.mark.asyncio
235
+ async def test_memory_leak_detection(self, async_client):
236
+ """Test for potential memory leaks during repeated operations"""
237
+ import psutil
238
+ import os
239
+
240
+ process = psutil.Process(os.getpid())
241
+ initial_memory = process.memory_info().rss / 1024 / 1024 # MB
242
+
243
+ # Perform many operations
244
+ for i in range(100):
245
+ await async_client.get("/health")
246
+
247
+ # Occasionally check memory growth
248
+ if i % 20 == 0:
249
+ current_memory = process.memory_info().rss / 1024 / 1024 # MB
250
+ memory_growth = current_memory - initial_memory
251
+
252
+ # Memory growth should be reasonable
253
+ assert memory_growth < 50, f"Excessive memory growth: {memory_growth:.2f}MB after {i} requests"
254
+
255
+ final_memory = process.memory_info().rss / 1024 / 1024 # MB
256
+ total_growth = final_memory - initial_memory
257
+
258
+ # Total memory growth should be reasonable
259
+ assert total_growth < 100, f"Total memory growth {total_growth:.2f}MB suggests potential memory leak"
260
+
261
+ @pytest.mark.asyncio
262
+ async def test_database_connection_resilience(self, async_client, sample_merchant_data):
263
+ """Test resilience to database connection issues"""
264
+ with patch('app.nosql.get_mongodb_client') as mock_mongo_client:
265
+ # Simulate intermittent database failures
266
+ call_count = 0
267
+
268
+ def side_effect(*args, **kwargs):
269
+ nonlocal call_count
270
+ call_count += 1
271
+ if call_count % 3 == 0: # Fail every 3rd call
272
+ raise Exception("Database connection timeout")
273
+
274
+ # Return mock for successful calls
275
+ mock_client = AsyncMock()
276
+ mock_collection = AsyncMock()
277
+ mock_client.__getitem__.return_value.__getitem__.return_value = mock_collection
278
+ mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = [sample_merchant_data]
279
+ return mock_client
280
+
281
+ mock_mongo_client.side_effect = side_effect
282
+
283
+ # Make multiple requests
284
+ success_count = 0
285
+ error_count = 0
286
+
287
+ for _ in range(10):
288
+ response = await async_client.get("/api/v1/merchants/")
289
+ if response.status_code == 200:
290
+ success_count += 1
291
+ else:
292
+ error_count += 1
293
+
294
+ # System should handle some database failures gracefully
295
+ assert success_count > 0, "No successful requests despite intermittent failures"
296
+ # Some failures are expected due to simulated database issues
297
+
298
+ class TestBackwardCompatibility:
299
+ """Test backward compatibility of API changes"""
300
+
301
+ @pytest.mark.asyncio
302
+ async def test_api_version_compatibility(self, async_client):
303
+ """Test that API versions remain compatible"""
304
+ # Test v1 API endpoints still work
305
+ v1_endpoints = [
306
+ "/api/v1/merchants/",
307
+ "/api/v1/nlp/supported-intents",
308
+ "/api/v1/nlp/supported-entities"
309
+ ]
310
+
311
+ for endpoint in v1_endpoints:
312
+ response = await async_client.get(endpoint)
313
+ assert response.status_code == 200, f"v1 endpoint {endpoint} is not accessible"
314
+
315
+ @pytest.mark.asyncio
316
+ async def test_response_format_compatibility(self, async_client, sample_merchant_data):
317
+ """Test that response formats haven't broken compatibility"""
318
+ with patch('app.services.merchant.get_merchants') as mock_get:
319
+ mock_get.return_value = [sample_merchant_data]
320
+
321
+ response = await async_client.get("/api/v1/merchants/")
322
+ assert response.status_code == 200
323
+
324
+ merchants = response.json()
325
+ assert isinstance(merchants, list)
326
+
327
+ if merchants:
328
+ merchant = merchants[0]
329
+ # Check that essential fields are still present
330
+ essential_fields = ["_id", "name", "category", "location"]
331
+ for field in essential_fields:
332
+ assert field in merchant, f"Essential field {field} missing from merchant response"
333
+
334
+ @pytest.mark.asyncio
335
+ async def test_parameter_compatibility(self, async_client):
336
+ """Test that API parameters remain compatible"""
337
+ # Test that old parameter names still work
338
+ response = await async_client.get("/api/v1/merchants/search", params={
339
+ "latitude": 40.7128,
340
+ "longitude": -74.0060,
341
+ "radius": 5000,
342
+ "category": "salon"
343
+ })
344
+
345
+ # Should accept these parameters without error
346
+ assert response.status_code in [200, 400] # 400 might be due to mocking, but not 422 (validation error)
347
+
348
+ class TestDataIntegrity:
349
+ """Test data integrity across the system"""
350
+
351
+ @pytest.mark.asyncio
352
+ async def test_search_result_integrity(self, async_client, sample_merchant_data):
353
+ """Test integrity of search results"""
354
+ with patch('app.services.merchant.search_merchants') as mock_search:
355
+ mock_search.return_value = [sample_merchant_data]
356
+
357
+ response = await async_client.get("/api/v1/merchants/search", params={
358
+ "latitude": 40.7128,
359
+ "longitude": -74.0060,
360
+ "radius": 5000,
361
+ "category": "salon"
362
+ })
363
+
364
+ assert response.status_code == 200
365
+ merchants = response.json()
366
+
367
+ for merchant in merchants:
368
+ # Verify data integrity
369
+ assert "_id" in merchant and merchant["_id"]
370
+ assert "name" in merchant and merchant["name"]
371
+ assert "category" in merchant and merchant["category"]
372
+
373
+ # Verify location data integrity
374
+ if "location" in merchant:
375
+ location = merchant["location"]
376
+ assert "type" in location
377
+ assert "coordinates" in location
378
+ assert len(location["coordinates"]) == 2
379
+
380
+ # Coordinates should be valid
381
+ lng, lat = location["coordinates"]
382
+ assert -180 <= lng <= 180, f"Invalid longitude: {lng}"
383
+ assert -90 <= lat <= 90, f"Invalid latitude: {lat}"
384
+
385
+ @pytest.mark.asyncio
386
+ async def test_nlp_result_integrity(self, async_client):
387
+ """Test integrity of NLP processing results"""
388
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp:
389
+ mock_nlp.process_query.return_value = {
390
+ "query": "find a hair salon",
391
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.85},
392
+ "entities": {"service_types": ["haircut"]},
393
+ "similar_services": [("salon", 0.9)],
394
+ "search_parameters": {"merchant_category": "salon"},
395
+ "processing_time": 0.123
396
+ }
397
+
398
+ response = await async_client.post("/api/v1/nlp/analyze-query", params={
399
+ "query": "find a hair salon"
400
+ })
401
+
402
+ assert response.status_code == 200
403
+ data = response.json()
404
+
405
+ # Verify NLP result integrity
406
+ assert data["status"] == "success"
407
+ assert "analysis" in data
408
+
409
+ analysis = data["analysis"]
410
+ assert "query" in analysis
411
+ assert "primary_intent" in analysis
412
+ assert "entities" in analysis
413
+ assert "similar_services" in analysis
414
+ assert "search_parameters" in analysis
415
+ assert "processing_time" in analysis
416
+
417
+ # Verify confidence scores are valid
418
+ if "confidence" in analysis["primary_intent"]:
419
+ confidence = analysis["primary_intent"]["confidence"]
420
+ assert 0.0 <= confidence <= 1.0, f"Invalid confidence score: {confidence}"
421
+
422
+ class TestSystemRecovery:
423
+ """Test system recovery from various failure scenarios"""
424
+
425
+ @pytest.mark.asyncio
426
+ async def test_recovery_from_service_failure(self, async_client):
427
+ """Test recovery when services fail and recover"""
428
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp:
429
+ # First, service fails
430
+ mock_nlp.process_query.side_effect = Exception("Service temporarily unavailable")
431
+
432
+ response1 = await async_client.post("/api/v1/nlp/analyze-query", params={
433
+ "query": "find a salon"
434
+ })
435
+ assert response1.status_code == 500
436
+
437
+ # Then, service recovers
438
+ mock_nlp.process_query.side_effect = None
439
+ mock_nlp.process_query.return_value = {
440
+ "query": "find a salon",
441
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.8},
442
+ "entities": {},
443
+ "similar_services": [],
444
+ "search_parameters": {},
445
+ "processing_time": 0.1
446
+ }
447
+
448
+ response2 = await async_client.post("/api/v1/nlp/analyze-query", params={
449
+ "query": "find a salon"
450
+ })
451
+ assert response2.status_code == 200
452
+
453
+ @pytest.mark.asyncio
454
+ async def test_graceful_degradation(self, async_client, sample_merchant_data):
455
+ """Test graceful degradation when advanced features fail"""
456
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp, \
457
+ patch('app.services.helper.process_free_text') as mock_fallback, \
458
+ patch('app.services.merchant.get_merchants') as mock_get_merchants:
459
+
460
+ # Advanced NLP fails
461
+ mock_nlp.process_query.side_effect = Exception("NLP service down")
462
+
463
+ # Fallback service works
464
+ mock_fallback.return_value = {
465
+ "query": "salon",
466
+ "extracted_keywords": ["salon"],
467
+ "suggested_category": "salon"
468
+ }
469
+
470
+ # Basic merchant service works
471
+ mock_get_merchants.return_value = [sample_merchant_data]
472
+
473
+ # Should still be able to get basic functionality
474
+ fallback_response = await async_client.post("/api/v1/helpers/process-text", json={
475
+ "text": "salon",
476
+ "latitude": 40.7128,
477
+ "longitude": -74.0060
478
+ })
479
+ assert fallback_response.status_code == 200
480
+
481
+ merchants_response = await async_client.get("/api/v1/merchants/")
482
+ assert merchants_response.status_code == 200
483
+
484
+ # Performance benchmarks for regression detection
485
+ class TestPerformanceBenchmarks:
486
+ """Performance benchmarks to detect regressions"""
487
+
488
+ @pytest.mark.asyncio
489
+ async def test_response_time_benchmarks(self, async_client):
490
+ """Benchmark response times for key endpoints"""
491
+ benchmarks = {
492
+ "/health": 0.1, # 100ms
493
+ "/api/v1/nlp/supported-intents": 0.2, # 200ms
494
+ "/api/v1/nlp/supported-entities": 0.2, # 200ms
495
+ }
496
+
497
+ for endpoint, max_time in benchmarks.items():
498
+ start_time = time.time()
499
+ response = await async_client.get(endpoint)
500
+ response_time = time.time() - start_time
501
+
502
+ assert response.status_code == 200
503
+ assert response_time < max_time, f"Endpoint {endpoint} took {response_time:.3f}s, exceeds benchmark {max_time}s"
504
+
505
+ @pytest.mark.asyncio
506
+ async def test_throughput_benchmarks(self, async_client):
507
+ """Benchmark throughput for key operations"""
508
+ # Test health endpoint throughput
509
+ start_time = time.time()
510
+ tasks = [async_client.get("/health") for _ in range(20)]
511
+ responses = await asyncio.gather(*tasks)
512
+ total_time = time.time() - start_time
513
+
514
+ # All should succeed
515
+ assert all(r.status_code == 200 for r in responses)
516
+
517
+ # Should handle 20 requests reasonably fast
518
+ requests_per_second = 20 / total_time
519
+ assert requests_per_second > 10, f"Throughput {requests_per_second:.1f} req/s below benchmark"
app/tests/test_security.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Security regression tests
3
+ """
4
+
5
+ import pytest
6
+ from unittest.mock import patch, MagicMock
7
+ from fastapi.testclient import TestClient
8
+
9
+ class TestInputValidation:
10
+ """Test input validation and sanitization"""
11
+
12
+ def test_sql_injection_prevention(self, client: TestClient):
13
+ """Test SQL injection prevention in merchant search"""
14
+ malicious_input = "'; DROP TABLE merchants; --"
15
+
16
+ response = client.get("/api/v1/merchants/search", params={
17
+ "category": malicious_input,
18
+ "latitude": 40.7128,
19
+ "longitude": -74.0060
20
+ })
21
+
22
+ # Should either sanitize input or return 400
23
+ assert response.status_code in [200, 400]
24
+ if response.status_code == 200:
25
+ # If processed, should not contain malicious SQL
26
+ data = response.json()
27
+ assert isinstance(data, list)
28
+
29
+ def test_xss_prevention_in_nlp_query(self, client: TestClient):
30
+ """Test XSS prevention in NLP query processing"""
31
+ xss_payload = "<script>alert('xss')</script>find salon"
32
+
33
+ with patch('app.services.advanced_nlp.advanced_nlp_pipeline') as mock_nlp:
34
+ mock_nlp.process_query.return_value = {
35
+ "query": "find salon", # Should be sanitized
36
+ "primary_intent": {"intent": "SEARCH_SERVICE", "confidence": 0.8},
37
+ "entities": {},
38
+ "similar_services": [],
39
+ "search_parameters": {},
40
+ "processing_time": 0.1
41
+ }
42
+
43
+ response = client.post("/api/v1/nlp/analyze-query", params={
44
+ "query": xss_payload
45
+ })
46
+
47
+ assert response.status_code == 200
48
+ data = response.json()
49
+ # Script tags should be removed/sanitized
50
+ assert "<script>" not in str(data)
51
+
52
+ def test_command_injection_prevention(self, client: TestClient):
53
+ """Test command injection prevention"""
54
+ command_injection = "; rm -rf /"
55
+
56
+ response = client.post("/api/v1/helpers/process-text", json={
57
+ "text": f"find salon{command_injection}",
58
+ "latitude": 40.7128,
59
+ "longitude": -74.0060
60
+ })
61
+
62
+ # Should handle malicious input safely
63
+ assert response.status_code in [200, 400]
64
+
65
+ def test_path_traversal_prevention(self, client: TestClient):
66
+ """Test path traversal prevention in merchant ID"""
67
+ path_traversal = "../../../etc/passwd"
68
+
69
+ response = client.get(f"/api/v1/merchants/{path_traversal}")
70
+
71
+ # Should not allow path traversal
72
+ assert response.status_code in [400, 404]
73
+
74
+ def test_large_payload_handling(self, client: TestClient):
75
+ """Test handling of excessively large payloads"""
76
+ large_text = "A" * (10 * 1024 * 1024) # 10MB
77
+
78
+ response = client.post("/api/v1/helpers/process-text", json={
79
+ "text": large_text,
80
+ "latitude": 40.7128,
81
+ "longitude": -74.0060
82
+ })
83
+
84
+ # Should reject or handle large payloads appropriately
85
+ assert response.status_code in [400, 413, 422]
86
+
87
+ def test_invalid_coordinates_handling(self, client: TestClient):
88
+ """Test handling of invalid coordinates"""
89
+ invalid_coords = [
90
+ (999, 999), # Out of range
91
+ (-999, -999), # Out of range
92
+ ("abc", "def"), # Non-numeric
93
+ (None, None) # Null values
94
+ ]
95
+
96
+ for lat, lng in invalid_coords:
97
+ response = client.get("/api/v1/merchants/search", params={
98
+ "latitude": lat,
99
+ "longitude": lng,
100
+ "radius": 5000
101
+ })
102
+
103
+ # Should handle invalid coordinates gracefully
104
+ assert response.status_code in [200, 400, 422]
105
+
106
+ class TestAuthentication:
107
+ """Test authentication mechanisms (if implemented)"""
108
+
109
+ def test_unauthenticated_access_to_public_endpoints(self, client: TestClient):
110
+ """Test that public endpoints don't require authentication"""
111
+ public_endpoints = [
112
+ "/health",
113
+ "/api/v1/merchants/",
114
+ "/api/v1/merchants/search"
115
+ ]
116
+
117
+ for endpoint in public_endpoints:
118
+ response = client.get(endpoint)
119
+ # Should not require authentication
120
+ assert response.status_code != 401
121
+
122
+ def test_api_key_validation(self, client: TestClient):
123
+ """Test API key validation if implemented"""
124
+ # This test assumes API key authentication might be implemented
125
+ invalid_api_key = "invalid_key_12345"
126
+
127
+ response = client.get("/api/v1/merchants/", headers={
128
+ "X-API-Key": invalid_api_key
129
+ })
130
+
131
+ # Should either ignore invalid key or reject it
132
+ assert response.status_code in [200, 401, 403]
133
+
134
+ class TestAuthorization:
135
+ """Test authorization and access control"""
136
+
137
+ def test_admin_endpoint_access(self, client: TestClient):
138
+ """Test access to admin endpoints"""
139
+ # Assuming there might be admin endpoints
140
+ admin_endpoints = [
141
+ "/admin/users",
142
+ "/admin/merchants",
143
+ "/admin/system"
144
+ ]
145
+
146
+ for endpoint in admin_endpoints:
147
+ response = client.get(endpoint)
148
+ # Should require proper authorization or not exist
149
+ assert response.status_code in [401, 403, 404]
150
+
151
+ def test_user_data_isolation(self, client: TestClient):
152
+ """Test that users can only access their own data"""
153
+ # This would be relevant if user-specific data exists
154
+ user_id = "user123"
155
+ other_user_id = "user456"
156
+
157
+ # Try to access another user's data
158
+ response = client.get(f"/api/v1/users/{other_user_id}/data", headers={
159
+ "User-ID": user_id
160
+ })
161
+
162
+ # Should not allow access to other user's data
163
+ assert response.status_code in [401, 403, 404]
164
+
165
+ class TestDataProtection:
166
+ """Test data protection and privacy"""
167
+
168
+ def test_sensitive_data_not_exposed(self, client: TestClient, sample_merchant_data):
169
+ """Test that sensitive data is not exposed in API responses"""
170
+ with patch('app.services.merchant.get_merchants') as mock_get:
171
+ # Add sensitive data to mock
172
+ merchant_with_sensitive = sample_merchant_data.copy()
173
+ merchant_with_sensitive.update({
174
+ "internal_id": "INTERNAL_123",
175
+ "api_key": "secret_api_key",
176
+ "database_password": "secret_password",
177
+ "private_notes": "Internal business notes"
178
+ })
179
+ mock_get.return_value = [merchant_with_sensitive]
180
+
181
+ response = client.get("/api/v1/merchants/")
182
+ assert response.status_code == 200
183
+
184
+ data = response.json()
185
+ response_text = str(data)
186
+
187
+ # Sensitive fields should not be in response
188
+ sensitive_fields = ["api_key", "database_password", "internal_id"]
189
+ for field in sensitive_fields:
190
+ assert field not in response_text
191
+
192
+ def test_error_message_information_disclosure(self, client: TestClient):
193
+ """Test that error messages don't disclose sensitive information"""
194
+ with patch('app.services.merchant.get_merchants') as mock_get:
195
+ # Simulate database error with sensitive info
196
+ mock_get.side_effect = Exception("Database connection failed: host=internal-db.company.com, user=admin, password=secret123")
197
+
198
+ response = client.get("/api/v1/merchants/")
199
+ assert response.status_code == 500
200
+
201
+ # Error response should not contain sensitive database info
202
+ error_text = str(response.json())
203
+ sensitive_info = ["password=", "host=internal-db", "user=admin"]
204
+ for info in sensitive_info:
205
+ assert info not in error_text
206
+
207
+ def test_log_sanitization(self, client: TestClient):
208
+ """Test that logs don't contain sensitive information"""
209
+ # This would require checking actual log output
210
+ # For now, test that endpoints handle sensitive data properly
211
+
212
+ sensitive_query = "my credit card is 4111-1111-1111-1111"
213
+
214
+ response = client.post("/api/v1/nlp/analyze-query", params={
215
+ "query": sensitive_query
216
+ })
217
+
218
+ # Should process without exposing sensitive data
219
+ assert response.status_code in [200, 400]
220
+
221
+ class TestCORSAndHeaders:
222
+ """Test CORS and security headers"""
223
+
224
+ def test_cors_configuration(self, client: TestClient):
225
+ """Test CORS configuration"""
226
+ # Test preflight request
227
+ response = client.options("/api/v1/merchants/", headers={
228
+ "Origin": "http://localhost:3000",
229
+ "Access-Control-Request-Method": "GET"
230
+ })
231
+
232
+ # Should handle CORS properly
233
+ assert response.status_code in [200, 204]
234
+
235
+ def test_cors_origin_validation(self, client: TestClient):
236
+ """Test CORS origin validation"""
237
+ # Test with allowed origin
238
+ response = client.get("/api/v1/merchants/", headers={
239
+ "Origin": "http://localhost:3000"
240
+ })
241
+ assert response.status_code == 200
242
+
243
+ # Test with disallowed origin
244
+ response = client.get("/api/v1/merchants/", headers={
245
+ "Origin": "http://malicious-site.com"
246
+ })
247
+ # Should still work but without CORS headers for invalid origin
248
+ assert response.status_code == 200
249
+
250
+ def test_security_headers(self, client: TestClient):
251
+ """Test security headers in responses"""
252
+ response = client.get("/health")
253
+
254
+ # Check for common security headers
255
+ headers = response.headers
256
+
257
+ # These might not be implemented yet, but good to test for
258
+ security_headers = [
259
+ "X-Content-Type-Options",
260
+ "X-Frame-Options",
261
+ "X-XSS-Protection",
262
+ "Strict-Transport-Security"
263
+ ]
264
+
265
+ # At minimum, should not have dangerous headers
266
+ dangerous_headers = [
267
+ "Server", # Should not expose server details
268
+ "X-Powered-By" # Should not expose technology stack
269
+ ]
270
+
271
+ for header in dangerous_headers:
272
+ if header in headers:
273
+ # If present, should not contain sensitive info
274
+ assert "internal" not in headers[header].lower()
275
+ assert "secret" not in headers[header].lower()
276
+
277
+ class TestRateLimiting:
278
+ """Test rate limiting and abuse prevention"""
279
+
280
+ @pytest.mark.asyncio
281
+ async def test_rate_limiting_basic(self, async_client):
282
+ """Test basic rate limiting functionality"""
283
+ # Make many requests quickly
284
+ responses = []
285
+ for _ in range(100):
286
+ response = await async_client.get("/health")
287
+ responses.append(response)
288
+
289
+ # Should either all succeed or some be rate limited
290
+ status_codes = [r.status_code for r in responses]
291
+
292
+ # All should be either 200 (OK) or 429 (Too Many Requests)
293
+ assert all(code in [200, 429] for code in status_codes)
294
+
295
+ def test_rate_limiting_per_endpoint(self, client: TestClient):
296
+ """Test rate limiting per endpoint"""
297
+ endpoints = [
298
+ "/health",
299
+ "/api/v1/merchants/",
300
+ "/api/v1/nlp/supported-intents"
301
+ ]
302
+
303
+ for endpoint in endpoints:
304
+ # Make multiple requests to each endpoint
305
+ responses = []
306
+ for _ in range(20):
307
+ response = client.get(endpoint)
308
+ responses.append(response)
309
+
310
+ # Should handle multiple requests appropriately
311
+ status_codes = [r.status_code for r in responses]
312
+ assert all(code in [200, 429, 500] for code in status_codes)
313
+
314
+ class TestInputSanitization:
315
+ """Test comprehensive input sanitization"""
316
+
317
+ def test_html_sanitization(self, client: TestClient):
318
+ """Test HTML tag sanitization"""
319
+ html_inputs = [
320
+ "<b>bold text</b>",
321
+ "<img src='x' onerror='alert(1)'>",
322
+ "<iframe src='javascript:alert(1)'></iframe>",
323
+ "<<script>alert('xss')</script>script>alert('xss')<</script>/script>"
324
+ ]
325
+
326
+ for html_input in html_inputs:
327
+ response = client.post("/api/v1/helpers/process-text", json={
328
+ "text": html_input,
329
+ "latitude": 40.7128,
330
+ "longitude": -74.0060
331
+ })
332
+
333
+ # Should handle HTML input safely
334
+ assert response.status_code in [200, 400]
335
+ if response.status_code == 200:
336
+ # Response should not contain dangerous HTML
337
+ response_text = str(response.json())
338
+ assert "<script>" not in response_text
339
+ assert "javascript:" not in response_text
340
+
341
+ def test_unicode_handling(self, client: TestClient):
342
+ """Test Unicode and special character handling"""
343
+ unicode_inputs = [
344
+ "cafΓ© rΓ©sumΓ© naΓ―ve", # Accented characters
345
+ "πŸͺπŸ”πŸ’‡β€β™€οΈ", # Emojis
346
+ "桋试中文字符", # Chinese characters
347
+ "тСст ΠΊΠΈΡ€ΠΈΠ»Π»ΠΈΡ†Π°", # Cyrillic
348
+ "\u0000\u0001\u0002", # Control characters
349
+ ]
350
+
351
+ for unicode_input in unicode_inputs:
352
+ response = client.post("/api/v1/nlp/analyze-query", params={
353
+ "query": unicode_input
354
+ })
355
+
356
+ # Should handle Unicode safely
357
+ assert response.status_code in [200, 400]
358
+
359
+ def test_numeric_input_validation(self, client: TestClient):
360
+ """Test numeric input validation"""
361
+ invalid_numeric_inputs = [
362
+ ("latitude", "not_a_number"),
363
+ ("longitude", "infinity"),
364
+ ("radius", "-1000"),
365
+ ("limit", "999999999999999999999"),
366
+ ("skip", "-1")
367
+ ]
368
+
369
+ for param, value in invalid_numeric_inputs:
370
+ response = client.get("/api/v1/merchants/search", params={
371
+ param: value,
372
+ "latitude": 40.7128 if param != "latitude" else value,
373
+ "longitude": -74.0060 if param != "longitude" else value
374
+ })
375
+
376
+ # Should validate numeric inputs
377
+ assert response.status_code in [200, 400, 422]
378
+
379
+ class TestDatabaseSecurity:
380
+ """Test database security measures"""
381
+
382
+ @pytest.mark.asyncio
383
+ async def test_mongodb_injection_prevention(self):
384
+ """Test MongoDB injection prevention"""
385
+ from app.repositories.db_repository import search_merchants_in_db
386
+
387
+ # MongoDB injection attempts
388
+ injection_attempts = [
389
+ {"$where": "this.name == 'test'"},
390
+ {"$regex": ".*"},
391
+ {"$ne": None}
392
+ ]
393
+
394
+ with patch('app.nosql.get_mongodb_client') as mock_client:
395
+ mock_collection = MagicMock()
396
+ mock_client.return_value.__getitem__.return_value.__getitem__.return_value = mock_collection
397
+ mock_collection.find.return_value.limit.return_value.to_list.return_value = []
398
+
399
+ for injection in injection_attempts:
400
+ try:
401
+ # Should sanitize or reject injection attempts
402
+ await search_merchants_in_db(category=injection)
403
+ # If it doesn't raise an exception, check that query was sanitized
404
+ call_args = mock_collection.find.call_args
405
+ if call_args:
406
+ query = call_args[0][0]
407
+ # Should not contain MongoDB operators in user input
408
+ assert "$where" not in str(query.get("category", ""))
409
+ except (ValueError, TypeError):
410
+ # Expected for invalid input
411
+ pass
412
+
413
+ def test_connection_string_security(self):
414
+ """Test that database connection strings don't expose credentials"""
415
+ from app.nosql import get_mongodb_client
416
+
417
+ # This test ensures connection strings are properly configured
418
+ # In a real scenario, you'd check that credentials aren't hardcoded
419
+ client = get_mongodb_client()
420
+
421
+ # Should have a client instance
422
+ assert client is not None
423
+
424
+ # Connection string should not be exposed in error messages
425
+ # This would require triggering a connection error and checking the message
426
+
427
+ class TestAPISecurityBestPractices:
428
+ """Test API security best practices"""
429
+
430
+ def test_http_methods_restriction(self, client: TestClient):
431
+ """Test that endpoints only accept appropriate HTTP methods"""
432
+ # Test that GET endpoints don't accept POST
433
+ response = client.post("/api/v1/merchants/")
434
+ assert response.status_code == 405 # Method Not Allowed
435
+
436
+ # Test that POST endpoints don't accept GET
437
+ response = client.get("/api/v1/helpers/process-text")
438
+ assert response.status_code == 405 # Method Not Allowed
439
+
440
+ def test_content_type_validation(self, client: TestClient):
441
+ """Test content type validation"""
442
+ # Send JSON data with wrong content type
443
+ response = client.post(
444
+ "/api/v1/helpers/process-text",
445
+ data='{"text": "test"}',
446
+ headers={"Content-Type": "text/plain"}
447
+ )
448
+
449
+ # Should reject or handle appropriately
450
+ assert response.status_code in [400, 415, 422]
451
+
452
+ def test_parameter_pollution(self, client: TestClient):
453
+ """Test handling of parameter pollution"""
454
+ # Send duplicate parameters
455
+ response = client.get("/api/v1/merchants/search?category=salon&category=spa")
456
+
457
+ # Should handle duplicate parameters appropriately
458
+ assert response.status_code in [200, 400]
459
+
460
+ def test_null_byte_injection(self, client: TestClient):
461
+ """Test null byte injection prevention"""
462
+ null_byte_input = "test\x00malicious"
463
+
464
+ response = client.post("/api/v1/nlp/analyze-query", params={
465
+ "query": null_byte_input
466
+ })
467
+
468
+ # Should handle null bytes safely
469
+ assert response.status_code in [200, 400]
470
+ if response.status_code == 200:
471
+ # Response should not contain null bytes
472
+ response_text = str(response.json())
473
+ assert "\x00" not in response_text
app/tests/test_services.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Regression tests for service layer components
3
+ """
4
+
5
+ import pytest
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+ from typing import Dict, Any
8
+
9
+ class TestMerchantService:
10
+ """Test merchant service functionality"""
11
+
12
+ @patch('app.nosql.db')
13
+ @pytest.mark.asyncio
14
+ async def test_get_merchants_success(self, mock_db, sample_merchant_data):
15
+ """Test successful merchant retrieval"""
16
+ from app.services.merchant import get_merchants
17
+
18
+ # Mock the MongoDB collection
19
+ mock_collection = AsyncMock()
20
+ mock_db.__getitem__.return_value = mock_collection
21
+ mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = [sample_merchant_data]
22
+
23
+ result = await get_merchants(limit=10, skip=0)
24
+
25
+ assert len(result) == 1
26
+ assert result[0]["name"] == "Test Hair Salon"
27
+
28
+ @patch('app.nosql.db')
29
+ @pytest.mark.asyncio
30
+ async def test_get_merchant_by_id_success(self, mock_db, sample_merchant_data):
31
+ """Test successful merchant retrieval by ID"""
32
+ from app.services.merchant import get_merchant_by_id
33
+
34
+ # Mock the MongoDB collection
35
+ mock_collection = AsyncMock()
36
+ mock_db.__getitem__.return_value = mock_collection
37
+ mock_collection.find_one.return_value = sample_merchant_data
38
+
39
+ result = await get_merchant_by_id("test_merchant_123")
40
+
41
+ assert result["_id"] == "test_merchant_123"
42
+ assert result["name"] == "Test Hair Salon"
43
+
44
+ @patch('app.nosql.db')
45
+ @pytest.mark.asyncio
46
+ async def test_get_merchant_by_id_not_found(self, mock_db):
47
+ """Test merchant not found scenario"""
48
+ from app.services.merchant import get_merchant_by_id
49
+
50
+ # Mock the MongoDB collection
51
+ mock_collection = AsyncMock()
52
+ mock_db.__getitem__.return_value = mock_collection
53
+ mock_collection.find_one.return_value = None
54
+
55
+ result = await get_merchant_by_id("nonexistent_id")
56
+
57
+ assert result is None
58
+
59
+ @patch('app.nosql.db')
60
+ @pytest.mark.asyncio
61
+ async def test_search_merchants_with_location(self, mock_db, sample_merchant_data):
62
+ """Test merchant search with location parameters"""
63
+ from app.services.merchant import search_merchants
64
+
65
+ # Mock the MongoDB collection
66
+ mock_collection = AsyncMock()
67
+ mock_db.__getitem__.return_value = mock_collection
68
+ mock_collection.find.return_value.limit.return_value.to_list.return_value = [sample_merchant_data]
69
+
70
+ result = await search_merchants(
71
+ latitude=40.7128,
72
+ longitude=-74.0060,
73
+ radius=5000,
74
+ category="salon"
75
+ )
76
+
77
+ assert len(result) == 1
78
+ assert result[0]["category"] == "salon"
79
+
80
+ @patch('app.nosql.redis_client')
81
+ @patch('app.nosql.db')
82
+ @pytest.mark.asyncio
83
+ async def test_merchant_caching(self, mock_db, mock_redis, sample_merchant_data):
84
+ """Test merchant data caching"""
85
+ from app.services.merchant import get_merchants
86
+
87
+ # Mock the MongoDB collection
88
+ mock_collection = AsyncMock()
89
+ mock_db.__getitem__.return_value = mock_collection
90
+ mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = [sample_merchant_data]
91
+
92
+ # Mock Redis
93
+ mock_redis.get.return_value = None # Cache miss
94
+ mock_redis.set.return_value = True
95
+
96
+ result = await get_merchants(limit=10, skip=0)
97
+
98
+ assert len(result) == 1
99
+ assert result[0]["name"] == "Test Hair Salon"
100
+
101
+ class TestHelperService:
102
+ """Test helper service functionality"""
103
+
104
+ @pytest.mark.asyncio
105
+ async def test_process_free_text_basic(self):
106
+ """Test basic free text processing"""
107
+ from app.services.helper import process_free_text
108
+
109
+ result = await process_free_text("find a hair salon", 40.7128, -74.0060)
110
+
111
+ assert "query" in result
112
+ assert "extracted_keywords" in result
113
+ assert result["query"] == "find a hair salon"
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_process_free_text_with_location(self):
117
+ """Test free text processing with location"""
118
+ from app.services.helper import process_free_text
119
+
120
+ result = await process_free_text("salon near me", 40.7128, -74.0060)
121
+
122
+ assert "location" in result or "search_parameters" in result
123
+ assert result["query"] == "salon near me"
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_process_free_text_empty_input(self):
127
+ """Test free text processing with empty input"""
128
+ from app.services.helper import process_free_text
129
+
130
+ result = await process_free_text("", 40.7128, -74.0060)
131
+
132
+ # Should handle empty input gracefully
133
+ assert "query" in result
134
+ assert result["query"] == ""
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_extract_keywords(self):
138
+ """Test keyword extraction functionality"""
139
+ from app.services.helper import extract_keywords
140
+
141
+ keywords = extract_keywords("find the best hair salon with parking")
142
+
143
+ assert isinstance(keywords, list)
144
+ assert len(keywords) > 0
145
+ # Should extract relevant keywords
146
+ assert any("hair" in kw.lower() or "salon" in kw.lower() for kw in keywords)
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_suggest_category(self):
150
+ """Test category suggestion functionality"""
151
+ from app.services.helper import suggest_category
152
+
153
+ category = suggest_category("hair salon")
154
+
155
+ assert category is not None
156
+ assert category in ["salon", "beauty", "hair_salon"]
157
+
158
+ class TestSearchHelpers:
159
+ """Test search helper functionality"""
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_build_search_query(self):
163
+ """Test search query building"""
164
+ from app.services.search_helpers import build_search_query
165
+
166
+ params = {
167
+ "category": "salon",
168
+ "latitude": 40.7128,
169
+ "longitude": -74.0060,
170
+ "radius": 5000
171
+ }
172
+
173
+ query = build_search_query(params)
174
+
175
+ assert isinstance(query, dict)
176
+ assert "location" in query or "category" in query
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_apply_filters(self):
180
+ """Test filter application"""
181
+ from app.services.search_helpers import apply_filters
182
+
183
+ merchants = [
184
+ {"name": "Salon A", "average_rating": 4.5, "price_range": "$$"},
185
+ {"name": "Salon B", "average_rating": 3.5, "price_range": "$"},
186
+ {"name": "Salon C", "average_rating": 4.8, "price_range": "$$$"}
187
+ ]
188
+
189
+ filters = {"min_rating": 4.0, "max_price_range": "$$"}
190
+
191
+ filtered = apply_filters(merchants, filters)
192
+
193
+ assert len(filtered) <= len(merchants)
194
+ # Should only include merchants meeting criteria
195
+ for merchant in filtered:
196
+ assert merchant["average_rating"] >= 4.0
197
+
198
+ @pytest.mark.asyncio
199
+ async def test_sort_results(self):
200
+ """Test result sorting"""
201
+ from app.services.search_helpers import sort_results
202
+
203
+ merchants = [
204
+ {"name": "Salon A", "average_rating": 4.5, "distance": 1000},
205
+ {"name": "Salon B", "average_rating": 4.8, "distance": 2000},
206
+ {"name": "Salon C", "average_rating": 4.2, "distance": 500}
207
+ ]
208
+
209
+ # Sort by rating
210
+ sorted_by_rating = sort_results(merchants, "rating")
211
+ assert sorted_by_rating[0]["average_rating"] >= sorted_by_rating[1]["average_rating"]
212
+
213
+ # Sort by distance
214
+ sorted_by_distance = sort_results(merchants, "distance")
215
+ assert sorted_by_distance[0]["distance"] <= sorted_by_distance[1]["distance"]
216
+
217
+ class TestAdvancedNLPService:
218
+ """Test advanced NLP service functionality"""
219
+
220
+ @pytest.mark.asyncio
221
+ async def test_nlp_pipeline_initialization(self):
222
+ """Test NLP pipeline initialization"""
223
+ from app.services.advanced_nlp import AdvancedNLPPipeline
224
+
225
+ pipeline = AdvancedNLPPipeline()
226
+
227
+ assert pipeline.intent_classifier is not None
228
+ assert pipeline.entity_extractor is not None
229
+ assert pipeline.semantic_matcher is not None
230
+ assert pipeline.context_processor is not None
231
+
232
+ @pytest.mark.asyncio
233
+ async def test_intent_classification(self):
234
+ """Test intent classification"""
235
+ from app.services.advanced_nlp import IntentClassifier
236
+
237
+ classifier = IntentClassifier()
238
+
239
+ # Test search intent
240
+ intent, confidence = classifier.get_primary_intent("find a hair salon")
241
+ assert intent == "SEARCH_SERVICE"
242
+ assert confidence > 0.0
243
+
244
+ # Test quality filter intent
245
+ intent, confidence = classifier.get_primary_intent("best salon in town")
246
+ assert intent == "FILTER_QUALITY"
247
+ assert confidence > 0.0
248
+
249
+ @pytest.mark.asyncio
250
+ async def test_entity_extraction(self):
251
+ """Test entity extraction"""
252
+ from app.services.advanced_nlp import BusinessEntityExtractor
253
+
254
+ extractor = BusinessEntityExtractor()
255
+
256
+ entities = extractor.extract_entities("hair salon with parking near me")
257
+
258
+ assert isinstance(entities, dict)
259
+ assert "service_types" in entities or "amenities" in entities
260
+
261
+ @pytest.mark.asyncio
262
+ async def test_semantic_matching(self):
263
+ """Test semantic matching"""
264
+ from app.services.advanced_nlp import SemanticMatcher
265
+
266
+ matcher = SemanticMatcher()
267
+
268
+ matches = matcher.find_similar_services("hair salon")
269
+
270
+ assert isinstance(matches, list)
271
+ assert len(matches) > 0
272
+ # Should return tuples of (service, similarity_score)
273
+ for match in matches:
274
+ assert len(match) == 2
275
+ assert isinstance(match[1], float)
276
+ assert 0.0 <= match[1] <= 1.0
277
+
278
+ @pytest.mark.asyncio
279
+ async def test_context_processing(self):
280
+ """Test context-aware processing"""
281
+ from app.services.advanced_nlp import ContextAwareProcessor
282
+
283
+ processor = ContextAwareProcessor()
284
+
285
+ result = await processor.process_with_context(
286
+ "spa treatment",
287
+ {"service_categories": ["spa"]},
288
+ [("spa", 0.9)]
289
+ )
290
+
291
+ assert isinstance(result, dict)
292
+
293
+ @pytest.mark.asyncio
294
+ async def test_complete_nlp_pipeline(self):
295
+ """Test complete NLP pipeline processing"""
296
+ from app.services.advanced_nlp import AdvancedNLPPipeline
297
+
298
+ pipeline = AdvancedNLPPipeline()
299
+
300
+ result = await pipeline.process_query("find the best hair salon near me")
301
+
302
+ assert "query" in result
303
+ assert "primary_intent" in result
304
+ assert "entities" in result
305
+ assert "similar_services" in result
306
+ assert "search_parameters" in result
307
+ assert "processing_time" in result
308
+
309
+ @pytest.mark.asyncio
310
+ async def test_nlp_caching(self):
311
+ """Test NLP result caching"""
312
+ from app.services.advanced_nlp import AsyncNLPProcessor
313
+
314
+ processor = AsyncNLPProcessor()
315
+
316
+ def dummy_processor(text):
317
+ return {"processed": text.upper()}
318
+
319
+ # First call
320
+ result1 = await processor.process_async("test", dummy_processor)
321
+
322
+ # Second call should use cache
323
+ result2 = await processor.process_async("test", dummy_processor)
324
+
325
+ assert result1 == result2
326
+
327
+ @pytest.mark.asyncio
328
+ async def test_nlp_error_handling(self):
329
+ """Test NLP error handling"""
330
+ from app.services.advanced_nlp import AdvancedNLPPipeline
331
+
332
+ pipeline = AdvancedNLPPipeline()
333
+
334
+ # Test with empty query
335
+ result = await pipeline.process_query("")
336
+
337
+ assert "query" in result
338
+ assert result["query"] == ""
339
+ # Should handle gracefully without crashing
340
+
341
+ class TestServiceIntegration:
342
+ """Test service integration scenarios"""
343
+
344
+ @patch('app.services.merchant.search_merchants')
345
+ @patch('app.services.advanced_nlp.advanced_nlp_pipeline')
346
+ @pytest.mark.asyncio
347
+ async def test_nlp_to_merchant_search_integration(self, mock_nlp, mock_search, sample_merchant_data):
348
+ """Test integration between NLP processing and merchant search"""
349
+ # Mock NLP pipeline response
350
+ mock_nlp.process_query.return_value = {
351
+ "search_parameters": {
352
+ "merchant_category": "salon",
353
+ "radius": 5000,
354
+ "amenities": ["parking"]
355
+ }
356
+ }
357
+
358
+ # Mock merchant search response
359
+ mock_search.return_value = [sample_merchant_data]
360
+
361
+ # Simulate the integration
362
+ nlp_result = await mock_nlp.process_query("salon with parking")
363
+ search_params = nlp_result["search_parameters"]
364
+
365
+ merchants = await mock_search(**search_params)
366
+
367
+ assert len(merchants) == 1
368
+ assert merchants[0]["category"] == "salon"
369
+
370
+ @patch('app.repositories.cache_repository.cache_search_results')
371
+ @patch('app.services.merchant.search_merchants')
372
+ @pytest.mark.asyncio
373
+ async def test_search_result_caching(self, mock_search, mock_cache, sample_merchant_data):
374
+ """Test search result caching integration"""
375
+ mock_search.return_value = [sample_merchant_data]
376
+
377
+ # Perform search
378
+ results = await mock_search(category="salon")
379
+
380
+ # Cache should be called
381
+ mock_cache.assert_called_once()
382
+ assert len(results) == 1
383
+
384
+ @pytest.mark.asyncio
385
+ async def test_error_propagation(self):
386
+ """Test error propagation between services"""
387
+ from app.services.merchant import get_merchant_by_id
388
+
389
+ with patch('app.nosql.db') as mock_db:
390
+ mock_collection = AsyncMock()
391
+ mock_db.__getitem__.return_value = mock_collection
392
+ mock_collection.find_one.side_effect = Exception("Database connection failed")
393
+
394
+ with pytest.raises(Exception) as exc_info:
395
+ await get_merchant_by_id("test_id")
396
+
397
+ assert "Database connection failed" in str(exc_info.value)
398
+
399
+ class TestServicePerformance:
400
+ """Test service performance characteristics"""
401
+
402
+ @pytest.mark.asyncio
403
+ async def test_concurrent_merchant_requests(self, sample_merchant_data):
404
+ """Test concurrent merchant service requests"""
405
+ import asyncio
406
+ from app.services.merchant import get_merchant_by_id
407
+
408
+ with patch('app.nosql.db') as mock_db:
409
+ mock_collection = AsyncMock()
410
+ mock_db.__getitem__.return_value = mock_collection
411
+ mock_collection.find_one.return_value = sample_merchant_data
412
+
413
+ # Create multiple concurrent requests
414
+ tasks = [
415
+ get_merchant_by_id(f"merchant_{i}")
416
+ for i in range(10)
417
+ ]
418
+
419
+ results = await asyncio.gather(*tasks)
420
+
421
+ assert len(results) == 10
422
+ assert all(result["name"] == "Test Hair Salon" for result in results)
423
+
424
+ @pytest.mark.asyncio
425
+ async def test_nlp_processing_performance(self):
426
+ """Test NLP processing performance"""
427
+ import time
428
+ from app.services.advanced_nlp import AdvancedNLPPipeline
429
+
430
+ pipeline = AdvancedNLPPipeline()
431
+
432
+ start_time = time.time()
433
+ result = await pipeline.process_query("find a hair salon")
434
+ processing_time = time.time() - start_time
435
+
436
+ # Should process within reasonable time
437
+ assert processing_time < 2.0 # 2 seconds max
438
+ assert "processing_time" in result
439
+ assert result["processing_time"] > 0