backend.tests.api.test_verification_controller
1import pytest 2import os 3import io 4import numpy as np 5import cv2 6import face_recognition 7from unittest.mock import patch, MagicMock, mock_open 8from datetime import datetime, timedelta 9 10from backend.app import create_app, db 11from backend.components.workers import workerService 12 13 14@pytest.fixture 15def app_context(): 16 """Create app context with in-memory SQLite database for testing.""" 17 app = create_app() 18 app.config.update({ 19 "TESTING": True, 20 "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:" 21 }) 22 23 with app.app_context(): 24 db.create_all() 25 yield app 26 27 28@pytest.fixture 29def test_worker(app_context): 30 """Create a test worker with face embedding from testimg.jpg.""" 31 current_dir = os.path.dirname(os.path.abspath(__file__)) 32 image_path = os.path.normpath(os.path.join(current_dir, "../assets/testimg.jpg")) 33 34 35 36 if not os.path.exists(image_path): 37 pytest.skip(f"Test image not found at {image_path}") 38 39 with app_context.app_context(): 40 # Load and create embedding from test image 41 test_image = face_recognition.load_image_file(image_path) 42 43 # Use workerService.create_worker to create worker properly 44 worker = workerService.create_worker( 45 name="Jacob Czajka", 46 face_image=test_image, 47 expiration_date=datetime(2029, 1, 1) 48 ) 49 50 yield worker 51 52 53@pytest.fixture 54def client(app_context): 55 """Create Flask test client.""" 56 test_client = app_context.test_client() 57 yield test_client 58 test_client.delete() 59 60 61@pytest.fixture 62def test_image_with_qrcode(app_context, client, test_worker): 63 """ 64 Create a composite test image by joining testimg.jpg with the worker's QR code. 65 Places QR code beside the face image without distortion. 66 Returns the composite image as bytes. 67 """ 68 current_dir = os.path.dirname(os.path.abspath(__file__)) 69 testimg_path = os.path.normpath(os.path.join(current_dir, "../assets/testimg.jpg")) 70 71 if not os.path.exists(testimg_path): 72 pytest.skip(f"Test image not found at {testimg_path}") 73 74 with app_context.app_context(): 75 # Get the QR code from the API endpoint 76 response = client.get(f'/api/workers/entrypass/{test_worker.id}') 77 78 if response.status_code != 200: 79 pytest.skip("Failed to generate QR code from API") 80 81 # Read QR code from response 82 qr_code_bytes = response.data 83 qr_code_array = np.frombuffer(qr_code_bytes, np.uint8) 84 qr_code_image = cv2.imdecode(qr_code_array, cv2.IMREAD_COLOR) 85 86 if qr_code_image is None: 87 pytest.skip("Failed to decode QR code image") 88 89 # Load the face image 90 face_image = cv2.imread(testimg_path) 91 92 if face_image is None: 93 pytest.skip("Failed to load test image") 94 95 # Make QR code 70% of face image height (much larger) 96 qr_height = int(face_image.shape[0] * 0.7) 97 qr_width = int(qr_height) # QR codes are square - maintain aspect ratio 98 99 qr_resized = cv2.resize(qr_code_image, (qr_width, qr_height), interpolation=cv2.INTER_AREA) 100 101 # Create composite image by placing QR code beside face image (not on top) 102 # Total width = face_width + qr_width + small padding 103 padding = 20 104 total_width = face_image.shape[1] + qr_width + padding * 2 105 106 # Use the taller of the two images as height 107 total_height = max(face_image.shape[0], qr_height) + padding * 2 108 109 composite_image = np.ones((total_height, total_width, 3), dtype=np.uint8) * 255 110 111 # Place face image on the left 112 face_y_start = (total_height - face_image.shape[0]) // 2 113 face_x_start = padding 114 composite_image[ 115 face_y_start:face_y_start + face_image.shape[0], 116 face_x_start:face_x_start + face_image.shape[1] 117 ] = face_image 118 119 # Place QR code on the right 120 qr_y_start = (total_height - qr_height) // 2 121 qr_x_start = face_x_start + face_image.shape[1] + padding 122 composite_image[ 123 qr_y_start:qr_y_start + qr_height, 124 qr_x_start:qr_x_start + qr_width 125 ] = qr_resized 126 127 # Encode composite image to bytes 128 success, buffer = cv2.imencode('.jpg', composite_image) 129 if not success: 130 pytest.skip("Failed to encode composite image") 131 132 return buffer.tobytes() 133 134 135# ============================================================================ 136# Test POST /api/skan - Success Cases 137# ============================================================================ 138 139def test_post_camera_scan_success(client, app_context, test_worker, test_image_with_qrcode): 140 """ 141 Test successful verification with valid QR code and matching face. 142 QR code detection is iffy so it's attempted 10 times before we assume the endpoint is broken. 143 """ 144 with app_context.app_context(): 145 # Send request with composite image containing QR code and face 146 i = 0 147 response = None 148 while i != 10: 149 response = client.post( 150 '/api/skan', 151 data={'file': (io.BytesIO(test_image_with_qrcode), 'composite.jpg')}, 152 content_type='multipart/form-data' 153 ) 154 i += 1 155 156 if response.status_code == 200: 157 break 158 159 assert response.status_code == 200 160 161 162# ============================================================================ 163# Test POST /api/skan - Error Cases 164# ============================================================================ 165 166def test_post_camera_scan_missing_file(client): 167 """Test endpoint returns 400 when file is missing.""" 168 response = client.post( 169 '/api/skan', 170 data={}, 171 content_type='multipart/form-data' 172 ) 173 174 assert response.status_code == 400 175 assert 'error' in response.json 176 assert 'Brak pliku w żądaniu' in response.json['error'] 177 178 179def test_post_camera_scan_invalid_image(client): 180 """Test endpoint returns 500 when image file is corrupted.""" 181 invalid_image_bytes = b"This is not a valid image" 182 183 response = client.post( 184 '/api/skan', 185 data={'file': (io.BytesIO(invalid_image_bytes), 'invalid.jpg')}, 186 content_type='multipart/form-data' 187 ) 188 189 assert response.status_code == 500 190 191 192def test_post_camera_scan_empty_file(client): 193 """Test endpoint returns 500 when file is empty.""" 194 response = client.post( 195 '/api/skan', 196 data={'file': (io.BytesIO(b''), 'empty.jpg')}, 197 content_type='multipart/form-data' 198 ) 199 200 assert response.status_code == 500 201 202 203def test_post_camera_scan_no_qr_code(client, app_context): 204 """Test endpoint returns 400 when no QR code is found in image.""" 205 current_dir = os.path.dirname(os.path.abspath(__file__)) 206 testimg_path = os.path.normpath(os.path.join(current_dir, "../assets/testimg.jpg")) 207 208 if not os.path.exists(testimg_path): 209 pytest.skip(f"Test image not found at {testimg_path}") 210 211 with app_context.app_context(): 212 # Use plain face image without QR code 213 with open(testimg_path, 'rb') as f: 214 image_bytes = f.read() 215 response = client.post( 216 '/api/skan', 217 data={'file': (io.BytesIO(image_bytes), 'testimg.jpg')}, 218 content_type='multipart/form-data' 219 ) 220 221 assert response.status_code == 400 222 223 224# ============================================================================ 225# Test POST /api/skan - Error Cases - Expired QR Code 226# ============================================================================ 227def test_post_camera_scan_expired_qr_code(client, app_context, test_worker, test_image_with_qrcode): 228 """ 229 Test endpoint returns 403 when QR code is expired. 230 Repeat at most 10 times if fails, QR code detection often fails. 231 """ 232 with app_context.app_context(): 233 with patch('backend.components.workers.workerService.get_worker_from_qr_code') as mock_get_worker: 234 mock_get_worker.return_value = test_worker 235 236 # Store original expiration date 237 original_expiration = test_worker.expiration_date 238 239 # Set worker expiration to the past 240 workerService.extend_worker_expiration(workerService.get_worker_by_id(test_worker.id), 241 datetime.now() - timedelta(days=5)) 242 243 i = 0 244 response = 10 245 while i != 10: 246 # Send request with expired worker's QR code 247 response = client.post( 248 '/api/skan', 249 data={'file': (io.BytesIO(test_image_with_qrcode), 'composite.jpg')}, 250 content_type='multipart/form-data' 251 ) 252 if response.status_code == 403: 253 break 254 i += 1 255 256 # Should return 403 because the QR code is expired 257 assert response.status_code == 403 258 259 # Restore original expiration date 260 workerService.extend_worker_expiration(workerService.get_worker_by_id(test_worker.id), original_expiration) 261 262 263# ============================================================================ 264# Test POST /api/skan - Response Structure 265# ============================================================================ 266 267def test_post_camera_scan_response_structure(client, app_context, test_worker, test_image_with_qrcode): 268 """Test that response always contains 'code' and 'messages' field.""" 269 with app_context.app_context(): 270 with patch('backend.components.workers.workerService.get_worker_from_qr_code') as mock_get_worker: 271 with patch('backend.components.camera_verification.faceid.faceidService.verify_worker_face') as mock_verify: 272 mock_get_worker.return_value = test_worker 273 mock_verify.return_value = [True] 274 275 response = client.post( 276 '/api/skan', 277 data={'file': (io.BytesIO(test_image_with_qrcode), 'composite.jpg')}, 278 content_type='multipart/form-data' 279 ) 280 281 assert 'code' in response.json 282 assert 'message' in response.json
15@pytest.fixture 16def app_context(): 17 """Create app context with in-memory SQLite database for testing.""" 18 app = create_app() 19 app.config.update({ 20 "TESTING": True, 21 "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:" 22 }) 23 24 with app.app_context(): 25 db.create_all() 26 yield app
Create app context with in-memory SQLite database for testing.
29@pytest.fixture 30def test_worker(app_context): 31 """Create a test worker with face embedding from testimg.jpg.""" 32 current_dir = os.path.dirname(os.path.abspath(__file__)) 33 image_path = os.path.normpath(os.path.join(current_dir, "../assets/testimg.jpg")) 34 35 36 37 if not os.path.exists(image_path): 38 pytest.skip(f"Test image not found at {image_path}") 39 40 with app_context.app_context(): 41 # Load and create embedding from test image 42 test_image = face_recognition.load_image_file(image_path) 43 44 # Use workerService.create_worker to create worker properly 45 worker = workerService.create_worker( 46 name="Jacob Czajka", 47 face_image=test_image, 48 expiration_date=datetime(2029, 1, 1) 49 ) 50 51 yield worker
Create a test worker with face embedding from testimg.jpg.
54@pytest.fixture 55def client(app_context): 56 """Create Flask test client.""" 57 test_client = app_context.test_client() 58 yield test_client 59 test_client.delete()
Create Flask test client.
62@pytest.fixture 63def test_image_with_qrcode(app_context, client, test_worker): 64 """ 65 Create a composite test image by joining testimg.jpg with the worker's QR code. 66 Places QR code beside the face image without distortion. 67 Returns the composite image as bytes. 68 """ 69 current_dir = os.path.dirname(os.path.abspath(__file__)) 70 testimg_path = os.path.normpath(os.path.join(current_dir, "../assets/testimg.jpg")) 71 72 if not os.path.exists(testimg_path): 73 pytest.skip(f"Test image not found at {testimg_path}") 74 75 with app_context.app_context(): 76 # Get the QR code from the API endpoint 77 response = client.get(f'/api/workers/entrypass/{test_worker.id}') 78 79 if response.status_code != 200: 80 pytest.skip("Failed to generate QR code from API") 81 82 # Read QR code from response 83 qr_code_bytes = response.data 84 qr_code_array = np.frombuffer(qr_code_bytes, np.uint8) 85 qr_code_image = cv2.imdecode(qr_code_array, cv2.IMREAD_COLOR) 86 87 if qr_code_image is None: 88 pytest.skip("Failed to decode QR code image") 89 90 # Load the face image 91 face_image = cv2.imread(testimg_path) 92 93 if face_image is None: 94 pytest.skip("Failed to load test image") 95 96 # Make QR code 70% of face image height (much larger) 97 qr_height = int(face_image.shape[0] * 0.7) 98 qr_width = int(qr_height) # QR codes are square - maintain aspect ratio 99 100 qr_resized = cv2.resize(qr_code_image, (qr_width, qr_height), interpolation=cv2.INTER_AREA) 101 102 # Create composite image by placing QR code beside face image (not on top) 103 # Total width = face_width + qr_width + small padding 104 padding = 20 105 total_width = face_image.shape[1] + qr_width + padding * 2 106 107 # Use the taller of the two images as height 108 total_height = max(face_image.shape[0], qr_height) + padding * 2 109 110 composite_image = np.ones((total_height, total_width, 3), dtype=np.uint8) * 255 111 112 # Place face image on the left 113 face_y_start = (total_height - face_image.shape[0]) // 2 114 face_x_start = padding 115 composite_image[ 116 face_y_start:face_y_start + face_image.shape[0], 117 face_x_start:face_x_start + face_image.shape[1] 118 ] = face_image 119 120 # Place QR code on the right 121 qr_y_start = (total_height - qr_height) // 2 122 qr_x_start = face_x_start + face_image.shape[1] + padding 123 composite_image[ 124 qr_y_start:qr_y_start + qr_height, 125 qr_x_start:qr_x_start + qr_width 126 ] = qr_resized 127 128 # Encode composite image to bytes 129 success, buffer = cv2.imencode('.jpg', composite_image) 130 if not success: 131 pytest.skip("Failed to encode composite image") 132 133 return buffer.tobytes()
Create a composite test image by joining testimg.jpg with the worker's QR code. Places QR code beside the face image without distortion. Returns the composite image as bytes.
140def test_post_camera_scan_success(client, app_context, test_worker, test_image_with_qrcode): 141 """ 142 Test successful verification with valid QR code and matching face. 143 QR code detection is iffy so it's attempted 10 times before we assume the endpoint is broken. 144 """ 145 with app_context.app_context(): 146 # Send request with composite image containing QR code and face 147 i = 0 148 response = None 149 while i != 10: 150 response = client.post( 151 '/api/skan', 152 data={'file': (io.BytesIO(test_image_with_qrcode), 'composite.jpg')}, 153 content_type='multipart/form-data' 154 ) 155 i += 1 156 157 if response.status_code == 200: 158 break 159 160 assert response.status_code == 200
Test successful verification with valid QR code and matching face. QR code detection is iffy so it's attempted 10 times before we assume the endpoint is broken.
167def test_post_camera_scan_missing_file(client): 168 """Test endpoint returns 400 when file is missing.""" 169 response = client.post( 170 '/api/skan', 171 data={}, 172 content_type='multipart/form-data' 173 ) 174 175 assert response.status_code == 400 176 assert 'error' in response.json 177 assert 'Brak pliku w żądaniu' in response.json['error']
Test endpoint returns 400 when file is missing.
180def test_post_camera_scan_invalid_image(client): 181 """Test endpoint returns 500 when image file is corrupted.""" 182 invalid_image_bytes = b"This is not a valid image" 183 184 response = client.post( 185 '/api/skan', 186 data={'file': (io.BytesIO(invalid_image_bytes), 'invalid.jpg')}, 187 content_type='multipart/form-data' 188 ) 189 190 assert response.status_code == 500
Test endpoint returns 500 when image file is corrupted.
193def test_post_camera_scan_empty_file(client): 194 """Test endpoint returns 500 when file is empty.""" 195 response = client.post( 196 '/api/skan', 197 data={'file': (io.BytesIO(b''), 'empty.jpg')}, 198 content_type='multipart/form-data' 199 ) 200 201 assert response.status_code == 500
Test endpoint returns 500 when file is empty.
204def test_post_camera_scan_no_qr_code(client, app_context): 205 """Test endpoint returns 400 when no QR code is found in image.""" 206 current_dir = os.path.dirname(os.path.abspath(__file__)) 207 testimg_path = os.path.normpath(os.path.join(current_dir, "../assets/testimg.jpg")) 208 209 if not os.path.exists(testimg_path): 210 pytest.skip(f"Test image not found at {testimg_path}") 211 212 with app_context.app_context(): 213 # Use plain face image without QR code 214 with open(testimg_path, 'rb') as f: 215 image_bytes = f.read() 216 response = client.post( 217 '/api/skan', 218 data={'file': (io.BytesIO(image_bytes), 'testimg.jpg')}, 219 content_type='multipart/form-data' 220 ) 221 222 assert response.status_code == 400
Test endpoint returns 400 when no QR code is found in image.
228def test_post_camera_scan_expired_qr_code(client, app_context, test_worker, test_image_with_qrcode): 229 """ 230 Test endpoint returns 403 when QR code is expired. 231 Repeat at most 10 times if fails, QR code detection often fails. 232 """ 233 with app_context.app_context(): 234 with patch('backend.components.workers.workerService.get_worker_from_qr_code') as mock_get_worker: 235 mock_get_worker.return_value = test_worker 236 237 # Store original expiration date 238 original_expiration = test_worker.expiration_date 239 240 # Set worker expiration to the past 241 workerService.extend_worker_expiration(workerService.get_worker_by_id(test_worker.id), 242 datetime.now() - timedelta(days=5)) 243 244 i = 0 245 response = 10 246 while i != 10: 247 # Send request with expired worker's QR code 248 response = client.post( 249 '/api/skan', 250 data={'file': (io.BytesIO(test_image_with_qrcode), 'composite.jpg')}, 251 content_type='multipart/form-data' 252 ) 253 if response.status_code == 403: 254 break 255 i += 1 256 257 # Should return 403 because the QR code is expired 258 assert response.status_code == 403 259 260 # Restore original expiration date 261 workerService.extend_worker_expiration(workerService.get_worker_by_id(test_worker.id), original_expiration)
Test endpoint returns 403 when QR code is expired. Repeat at most 10 times if fails, QR code detection often fails.
268def test_post_camera_scan_response_structure(client, app_context, test_worker, test_image_with_qrcode): 269 """Test that response always contains 'code' and 'messages' field.""" 270 with app_context.app_context(): 271 with patch('backend.components.workers.workerService.get_worker_from_qr_code') as mock_get_worker: 272 with patch('backend.components.camera_verification.faceid.faceidService.verify_worker_face') as mock_verify: 273 mock_get_worker.return_value = test_worker 274 mock_verify.return_value = [True] 275 276 response = client.post( 277 '/api/skan', 278 data={'file': (io.BytesIO(test_image_with_qrcode), 'composite.jpg')}, 279 content_type='multipart/form-data' 280 ) 281 282 assert 'code' in response.json 283 assert 'message' in response.json
Test that response always contains 'code' and 'messages' field.