File size: 3,311 Bytes
210db8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import pytest
import os
import json
from unittest.mock import patch, MagicMock

def test_api_feedback_unauthorized(client):
    """Test feedback endpoint requires session authentication or API key."""
    response = client.post('/api/feedback', json={'image_path': 'test.jpg', 'label': 'morph'})
    assert response.status_code == 401

def test_api_feedback_missing_payload(client):
    """Test feedback endpoint rejects missing parameters."""
    with client.session_transaction() as sess:
        sess['logged_in'] = True
        sess['role'] = 'admin'

    response = client.post('/api/feedback', json={})
    assert response.status_code == 400
    assert 'Missing image_path or label' in response.get_json()['error']

def test_api_feedback_persistence_and_threshold_trigger(client, tmp_path):
    """Test feedback persists entries and triggers background training when reaching threshold."""
    with client.session_transaction() as sess:
        sess['logged_in'] = True
        sess['role'] = 'admin'

    test_feedback_file = str(tmp_path / "test_feedback.json")

    # Mock FEEDBACK_FILE in training route
    with patch('src.routes.training.log_model_interaction'), \
         patch('src.routes.training.Thread') as mock_thread, \
         patch('os.path.exists', return_value=False), \
         patch('builtins.open', create=True) as mock_open:

        # Test single feedback submission
        response = client.post(
            '/api/feedback',
            json={
                'image_path': 'static/uploads/sample_morph.jpg',
                'label': 'morph',
                'prediction': 'real'
            }
        )
        assert response.status_code == 200
        json_data = response.get_json()
        assert json_data['status'] == 'flagged'
        assert json_data['feedback_status'] == 'feedback_received'
        assert json_data['count'] == 1

def test_api_feedback_triggers_retrain_at_threshold(client):
    """Test background training thread is spawned when count reaches RETRAIN_FLAG_THRESHOLD (10)."""
    with client.session_transaction() as sess:
        sess['logged_in'] = True
        sess['role'] = 'admin'

    # Pre-populate 9 feedbacks
    existing_feedbacks = [
        {'timestamp': '2026-08-30T00:00:00', 'image_path': f'img_{i}.jpg', 'user_label': 'morph'}
        for i in range(9)
    ]

    with patch('src.routes.training.log_model_interaction'), \
         patch('os.path.exists', return_value=True), \
         patch('builtins.open') as mock_file, \
         patch('json.load', return_value=existing_feedbacks), \
         patch('json.dump'), \
         patch('src.routes.training.Thread') as mock_thread:

        mock_instance = MagicMock()
        mock_thread.return_value = mock_instance

        # 10th feedback submission reaches threshold
        response = client.post(
            '/api/feedback',
            json={
                'image_path': 'img_9.jpg',
                'label': 'morph'
            }
        )
        assert response.status_code == 200
        json_data = response.get_json()
        assert json_data['count'] == 10

        # Verify background daemon thread was started
        mock_thread.assert_called_once()
        assert mock_thread.call_args.kwargs['daemon'] is True
        mock_instance.start.assert_called_once()