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

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))

import pytest
from unittest.mock import Mock, patch
from anonymizer import NERAnonimizer

class TestNERAnonimizer:
    """Test classe NERAnonimizer"""
    
    def test_init(self):
        """Test inizializzazione"""
        anonymizer = NERAnonimizer()
        assert anonymizer.regex_patterns is not None
        assert anonymizer._ner_pipe is None
    
    @patch('anonymizer.pipeline')
    def test_ner_pipe_lazy_loading(self, mock_pipeline, mock_streamlit):
        """Test lazy loading del modello NER"""
        anonymizer = NERAnonimizer()
        
        # Prima chiamata - dovrebbe caricare il modello
        pipe = anonymizer.ner_pipe
        assert mock_pipeline.called
        
        # Seconda chiamata - dovrebbe usare cache
        mock_pipeline.reset_mock()
        pipe2 = anonymizer.ner_pipe
        assert not mock_pipeline.called
        assert pipe == pipe2
    
    def test_mask_with_regex_basic(self, sample_text):
        """Test mascheramento regex base"""
        anonymizer = NERAnonimizer()
        
        masked_text, entities = anonymizer.mask_with_regex(sample_text)
        
        # Verifica che abbia trovato entità
        assert len(entities) > 0
        
        # Verifica che le entità siano nel formato corretto
        for placeholder, original in entities.items():
            assert placeholder.startswith('[')
            assert placeholder.endswith(']')
            assert '_' in placeholder
            assert original in sample_text
            assert placeholder in masked_text
    
    def test_mask_with_regex_iban(self):
        """Test mascheramento IBAN specifico"""
        anonymizer = NERAnonimizer()
        text = "Il mio IBAN è IT60 X054 2811 1010 0000 0123 456 per i pagamenti"
        
        masked_text, entities = anonymizer.mask_with_regex(text)
        
        # Dovrebbe trovare l'IBAN
        iban_entities = [k for k in entities.keys() if k.startswith('[IBAN_')]
        assert len(iban_entities) == 1
        
        iban_placeholder = iban_entities[0]
        assert entities[iban_placeholder] == "IT60 X054 2811 1010 0000 0123 456"
        assert iban_placeholder in masked_text
    
    def test_mask_with_regex_email(self):
        """Test mascheramento email"""
        anonymizer = NERAnonimizer()
        text = "Contattami su mario.rossi@example.com o test@domain.co.uk"
        
        masked_text, entities = anonymizer.mask_with_regex(text)
        
        # Dovrebbe trovare 2 email
        email_entities = [k for k in entities.keys() if k.startswith('[EMAIL_')]
        assert len(email_entities) == 2
        
        email_values = [entities[k] for k in email_entities]
        assert "mario.rossi@example.com" in email_values
        assert "test@domain.co.uk" in email_values
    
    def test_mask_with_regex_cf(self):
        """Test mascheramento codice fiscale"""
        anonymizer = NERAnonimizer()
        text = "Il codice fiscale è RSSMRA80A01H501Z"
        
        masked_text, entities = anonymizer.mask_with_regex(text)
        
        cf_entities = [k for k in entities.keys() if k.startswith('[CF_')]
        assert len(cf_entities) == 1
        assert entities[cf_entities[0]] == "RSSMRA80A01H501Z"
    
    def test_mask_with_regex_empty_text(self, sample_empty_text):
        """Test con testo vuoto"""
        anonymizer = NERAnonimizer()
        
        masked_text, entities = anonymizer.mask_with_regex(sample_empty_text)
        
        assert masked_text == sample_empty_text
        assert len(entities) == 0
    
    def test_mask_with_regex_no_entities(self, sample_text_no_entities):
        """Test con testo senza entità"""
        anonymizer = NERAnonimizer()
        
        masked_text, entities = anonymizer.mask_with_regex(sample_text_no_entities)
        
        assert masked_text == sample_text_no_entities
        assert len(entities) == 0
    
    def test_mask_with_ner_success(self, mock_ner_pipeline, mock_streamlit):
        """Test mascheramento NER con successo"""
        anonymizer = NERAnonimizer()
        anonymizer._ner_pipe = mock_ner_pipeline
        
        text = "Mario Rossi lavora in ACME SpA"
        
        masked_text, entities = anonymizer.mask_with_ner(text)
        
        # Verifica chiamata al modello
        assert mock_ner_pipeline.called
        
        # Verifica entità trovate
        assert len(entities) == 2
        per_entities = [k for k in entities.keys() if k.startswith('[PER_')]
        org_entities = [k for k in entities.keys() if k.startswith('[ORG_')]
        
        assert len(per_entities) == 1
        assert len(org_entities) == 1
    
    def test_mask_with_ner_no_model(self, mock_streamlit):
        """Test NER senza modello caricato"""
        anonymizer = NERAnonimizer()
        anonymizer._ner_pipe = None
        
        text = "Mario Rossi lavora in ACME SpA"
        
        masked_text, entities = anonymizer.mask_with_ner(text)
        
        # Dovrebbe ritornare testo invariato
        assert masked_text == text
        assert len(entities) == 0
    
    def test_mask_with_ner_low_confidence(self, mock_streamlit):
        """Test NER con confidence bassa"""
        anonymizer = NERAnonimizer()
        
        # Mock con score basso
        mock_pipe = Mock()
        mock_pipe.return_value = [
            {
                'entity_group': 'PER',
                'score': 0.3,  # Sotto threshold (0.5)
                'start': 0,
                'end': 11,
                'word': 'Mario Rossi'
            }
        ]
        anonymizer._ner_pipe = mock_pipe
        
        text = "Mario Rossi"
        masked_text, entities = anonymizer.mask_with_ner(text)
        
        # Non dovrebbe mascherare con confidence bassa
        assert masked_text == text
        assert len(entities) == 0
    
    def test_anonymize_complete_pipeline(self, sample_text, mock_ner_pipeline, mock_streamlit):
        """Test pipeline completa di anonimizzazione"""
        anonymizer = NERAnonimizer()
        anonymizer._ner_pipe = mock_ner_pipeline
        
        anonymized_text, all_entities = anonymizer.anonymize(sample_text)
        
        # Verifica che sia diverso dall'originale
        assert anonymized_text != sample_text
        
        # Verifica che contenga placeholder
        assert '[' in anonymized_text and ']' in anonymized_text
        
        # Verifica che abbia trovato entità da entrambi i sistemi
        assert len(all_entities) > 0
        
        # Verifica mix di entità regex e NER
        regex_entities = [k for k in all_entities.keys() 
                         if any(k.startswith(f'[{t}_') for t in ['IBAN', 'EMAIL', 'CF', 'CARD', 'PHONE'])]
        ner_entities = [k for k in all_entities.keys() 
                       if any(k.startswith(f'[{t}_') for t in ['PER', 'ORG'])]
        
        assert len(regex_entities) > 0  # Dovrebbe trovare entità regex
        assert len(ner_entities) > 0    # Dovrebbe trovare entità NER
    
    def test_anonymize_empty_text(self, sample_empty_text):
        """Test anonimizzazione testo vuoto"""
        anonymizer = NERAnonimizer()
        
        anonymized_text, entities = anonymizer.anonymize(sample_empty_text)
        
        assert anonymized_text == sample_empty_text
        assert len(entities) == 0
    
    def test_anonymize_preserves_structure(self, mock_streamlit):
        """Test che l'anonimizzazione preservi la struttura del testo"""
        anonymizer = NERAnonimizer()
        
        text = """Documento importante
        
        Dati cliente:
        - Nome: Mario Rossi
        - Email: mario@test.com
        
        Fine documento."""
        
        anonymized_text, entities = anonymizer.anonymize(text)
        
        # Dovrebbe preservare newline e struttura
        assert '\n' in anonymized_text
        assert 'Documento importante' in anonymized_text
        assert 'Fine documento.' in anonymized_text
    
    def test_placeholder_uniqueness(self, sample_text, mock_ner_pipeline, mock_streamlit):
        """Test che i placeholder siano unici"""
        anonymizer = NERAnonimizer()
        anonymizer._ner_pipe = mock_ner_pipeline
        
        anonymized_text, entities = anonymizer.anonymize(sample_text)
        
        # Tutti i placeholder dovrebbero essere unici
        placeholders = list(entities.keys())
        assert len(placeholders) == len(set(placeholders))
        
        # Ogni placeholder dovrebbe apparire nel testo
        for placeholder in placeholders:
            assert placeholder in anonymized_text

class TestAnonymizerEdgeCases:
    """Test casi limite"""
    
    def test_already_masked_text(self, mock_streamlit):
        """Test testo già parzialmente mascherato"""
        anonymizer = NERAnonimizer()
        
        text = "Contatta [EMAIL_0] per info su [CF_0]"
        masked_text, entities = anonymizer.mask_with_regex(text)
        
        # Non dovrebbe ri-mascherare placeholder esistenti
        assert masked_text == text
        assert len(entities) == 0
    
    def test_overlapping_patterns(self, mock_streamlit):
        """Test pattern che si sovrappongono"""
        anonymizer = NERAnonimizer()
        
        # Testo con potenziali sovrapposizioni
        text = "Email test@domain.com nel sito https://test@domain.com"
        
        masked_text, entities = anonymizer.mask_with_regex(text)
        
        # Dovrebbe gestire correttamente le sovrapposizioni
        assert len(entities) >= 1
        assert all(placeholder in masked_text for placeholder in entities.keys())
    
    def test_special_characters(self, mock_streamlit):
        """Test caratteri speciali"""
        anonymizer = NERAnonimizer()
        
        text = "Email: test@domain.com; IBAN: IT60X05428111010000001234567!"
        
        masked_text, entities = anonymizer.mask_with_regex(text)
        
        # Dovrebbe trovare entità anche con caratteri speciali intorno
        email_found = any('EMAIL' in k for k in entities.keys())
        iban_found = any('IBAN' in k for k in entities.keys())
        
        assert email_found
        assert iban_found