@@ -49,13 +49,17 @@ def mock_certificate_template():
4949def mock_logo ():
5050 return Image .new ("RGBA" , (150 , 150 ), (255 , 255 , 255 , 0 ))
5151
52+ @pytest .fixture
53+ def mock_logo_tech_floripa ():
54+ return Image .new ("RGBA" , (100 , 100 ), (255 , 255 , 255 , 255 ))
55+
5256@pytest .fixture
5357def certified_builder ():
5458 return CertifiedBuilder ()
5559
56- def test_generate_certificate (certified_builder , mock_participant , mock_certificate_template , mock_logo ):
60+ def test_generate_certificate (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
5761 with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
58- certificate = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo )
62+ certificate = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
5963 assert isinstance (certificate , Image .Image )
6064 assert certificate .size == mock_certificate_template .size
6165 assert certificate .mode == "RGBA"
@@ -78,11 +82,21 @@ def test_create_validation_code_image(certified_builder, mock_participant, mock_
7882 assert validation_code_image .size == mock_certificate_template .size
7983 assert validation_code_image .mode == "RGBA"
8084
81- def test_build_certificates (certified_builder , mock_participant , mock_certificate_template , mock_logo ):
85+ def test_build_certificates (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
8286 participants = [mock_participant ]
8387
8488 # comentário: mock do download de imagens e da resposta do serviço Solana para evitar chamada externa
85- with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]), \
89+ # Função que retorna o mock baseado na URL chamada
90+ def mock_download_image (url ):
91+ if url == mock_participant .certificate .background :
92+ return mock_certificate_template
93+ elif url == mock_participant .certificate .logo :
94+ return mock_logo
95+ elif url == "https://example.test/logo.png" : # TECH_FLORIPA_LOGO_URL do config mock
96+ return mock_logo_tech_floripa
97+ raise ValueError (f"URL não mockada: { url } " )
98+
99+ with patch .object (certified_builder , '_download_image' , side_effect = mock_download_image ), \
86100 patch ('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana' , return_value = {
87101 # comentário: mock alinhado ao contrato atual do serviço
88102 "status" : "sucesso" ,
@@ -128,12 +142,12 @@ def _count_non_transparent_pixels(img: Image.Image) -> int:
128142 return sum (1 for p in alpha .getdata () if p != 0 )
129143
130144
131- def test_scan_to_validate_is_centered_and_below_qr (certified_builder , mock_participant , mock_certificate_template , mock_logo ):
145+ def test_scan_to_validate_is_centered_and_below_qr (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
132146 # Garante url para QR
133147 mock_participant .authenticity_verification_url = "https://example.com/verify"
134148
135149 with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
136- result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo )
150+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
137151
138152 # Recalcula posição esperada do texto seguindo a mesma lógica do código
139153 qrcode_size = (150 , 150 )
@@ -163,11 +177,11 @@ def test_scan_to_validate_is_centered_and_below_qr(certified_builder, mock_parti
163177 assert _count_non_transparent_pixels (cropped ) > 0 , "Texto 'Scan to Validate' não encontrado na área esperada"
164178
165179
166- def test_qr_is_placed_at_expected_region (certified_builder , mock_participant , mock_certificate_template , mock_logo ):
180+ def test_qr_is_placed_at_expected_region (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
167181 mock_participant .authenticity_verification_url = "https://example.com/verify"
168182
169183 with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
170- result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo )
184+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
171185
172186 # QR é esperado em (50,200) com 150x150
173187 qr_left , qr_top = 50 , 200
@@ -180,3 +194,248 @@ def test_qr_is_placed_at_expected_region(certified_builder, mock_participant, mo
180194 below_region = result .crop ((qr_left , qr_bottom , qr_right , min (qr_bottom + 30 , result .height )))
181195 assert _count_non_transparent_pixels (below_region ) > 0 , "Nenhum conteúdo encontrado abaixo do QR onde o texto deveria estar"
182196
197+
198+ def test_name_is_centered_on_certificate (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
199+ """Verifica que o nome do participante está centralizado no certificado."""
200+ mock_participant .authenticity_verification_url = "https://example.com/verify"
201+
202+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
203+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
204+
205+ # Verifica que há conteúdo na região central onde o nome deveria estar
206+ center_x = result .width // 2
207+ center_y = result .height // 2
208+ name_region = result .crop ((center_x - 200 , center_y - 50 , center_x + 200 , center_y + 50 ))
209+
210+ assert _count_non_transparent_pixels (name_region ) > 0 , "Nome não encontrado na região central do certificado"
211+
212+
213+ def test_validation_code_is_at_bottom_right (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
214+ """Verifica que o código de validação está posicionado no canto inferior direito."""
215+ mock_participant .authenticity_verification_url = "https://example.com/verify"
216+
217+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
218+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
219+
220+ # Verifica região inferior direita onde o código de validação deveria estar
221+ validation_region = result .crop ((result .width - 250 , result .height - 80 , result .width , result .height ))
222+
223+ assert _count_non_transparent_pixels (validation_region ) > 0 , "Código de validação não encontrado no canto inferior direito"
224+
225+
226+ def test_logo_is_positioned_at_top_left (certified_builder , mock_participant , mock_certificate_template , mock_logo_tech_floripa ):
227+ """Verifica que o logo está posicionado no canto superior esquerdo."""
228+ mock_participant .authenticity_verification_url = "https://example.com/verify"
229+ # Usa um logo visível (não transparente) para o teste
230+ visible_logo = Image .new ("RGBA" , (150 , 150 ), (255 , 255 , 255 , 255 ))
231+
232+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , visible_logo ]):
233+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , visible_logo , mock_logo_tech_floripa )
234+
235+ # Logo é esperado em (50, 50) com tamanho máximo de 150x150
236+ logo_region = result .crop ((50 , 50 , 200 , 200 ))
237+
238+ assert _count_non_transparent_pixels (logo_region ) > 0 , "Logo não encontrado na região esperada"
239+
240+
241+ def test_large_logo_is_resized (certified_builder , mock_participant , mock_certificate_template , mock_logo_tech_floripa ):
242+ """Verifica que logos grandes são redimensionados para o tamanho máximo."""
243+ mock_participant .authenticity_verification_url = "https://example.com/verify"
244+ large_logo = Image .new ("RGBA" , (300 , 300 ), (255 , 255 , 255 , 255 ))
245+
246+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , large_logo ]):
247+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , large_logo , mock_logo_tech_floripa )
248+
249+ # Verifica que o logo foi redimensionado (não deve ocupar toda a região de 300x300)
250+ logo_region = result .crop ((50 , 50 , 200 , 200 ))
251+
252+ assert _count_non_transparent_pixels (logo_region ) > 0 , "Logo redimensionado não encontrado"
253+
254+
255+ def test_details_are_split_into_three_lines (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
256+ """Verifica que os detalhes do certificado são divididos em 3 linhas."""
257+ mock_participant .authenticity_verification_url = "https://example.com/verify"
258+
259+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
260+ result = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
261+
262+ # Verifica região onde os detalhes deveriam estar (abaixo do centro)
263+ center_y = result .height // 2
264+ details_region = result .crop ((0 , center_y + 50 , result .width , center_y + 150 ))
265+
266+ assert _count_non_transparent_pixels (details_region ) > 0 , "Detalhes não encontrados na região esperada"
267+
268+
269+ def test_build_certificates_with_multiple_participants_same_resources (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
270+ """Testa geração de certificados para múltiplos participantes com mesmo background e logo."""
271+ second_participant = Participant (
272+ first_name = "Maria" ,
273+ last_name = "Silva" ,
274+ email = "maria@example.com" ,
275+ phone = "(48) 99999-9999" ,
276+ cpf = "111.111.111-11" ,
277+ certificate = mock_participant .certificate ,
278+ event = mock_participant .event
279+ )
280+
281+ participants = [mock_participant , second_participant ]
282+
283+ def mock_download_image (url ):
284+ if url == mock_participant .certificate .background :
285+ return mock_certificate_template
286+ elif url == mock_participant .certificate .logo :
287+ return mock_logo
288+ elif url == "https://example.test/logo.png" :
289+ return mock_logo_tech_floripa
290+ raise ValueError (f"URL não mockada: { url } " )
291+
292+ with patch .object (certified_builder , '_download_image' , side_effect = mock_download_image ), \
293+ patch ('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana' , return_value = {
294+ "blockchain" : {
295+ "explorer_url" : "https://explorer.solana.com/tx/test123?cluster=devnet"
296+ }
297+ }), \
298+ patch .object (certified_builder , 'save_certificate' ) as mock_save :
299+
300+ results = certified_builder .build_certificates (participants )
301+
302+ assert len (results ) == 2
303+ assert all (r ["success" ] for r in results )
304+ assert mock_save .call_count == 2
305+
306+
307+ def test_build_certificates_with_different_backgrounds (certified_builder , mock_participant , mock_logo , mock_logo_tech_floripa ):
308+ """Testa geração de certificados para participantes com backgrounds diferentes."""
309+ different_background = "https://example.com/different-background.png"
310+ different_template = Image .new ("RGBA" , (1920 , 1080 ), (200 , 200 , 200 , 255 ))
311+
312+ second_certificate = Certificate (
313+ details = mock_participant .certificate .details ,
314+ logo = mock_participant .certificate .logo ,
315+ background = different_background
316+ )
317+
318+ second_participant = Participant (
319+ first_name = "João" ,
320+ last_name = "Santos" ,
321+ email = "joao@example.com" ,
322+ phone = "(48) 88888-8888" ,
323+ cpf = "222.222.222-22" ,
324+ certificate = second_certificate ,
325+ event = mock_participant .event
326+ )
327+
328+ participants = [mock_participant , second_participant ]
329+
330+ def mock_download_image (url ):
331+ if url == mock_participant .certificate .background :
332+ return Image .new ("RGBA" , (1920 , 1080 ), (255 , 255 , 255 , 0 ))
333+ elif url == different_background :
334+ return different_template
335+ elif url == mock_participant .certificate .logo :
336+ return mock_logo
337+ elif url == "https://example.test/logo.png" :
338+ return mock_logo_tech_floripa
339+ raise ValueError (f"URL não mockada: { url } " )
340+
341+ with patch .object (certified_builder , '_download_image' , side_effect = mock_download_image ), \
342+ patch ('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana' , return_value = {
343+ "blockchain" : {
344+ "explorer_url" : "https://explorer.solana.com/tx/test123?cluster=devnet"
345+ }
346+ }), \
347+ patch .object (certified_builder , 'save_certificate' ) as mock_save :
348+
349+ results = certified_builder .build_certificates (participants )
350+
351+ assert len (results ) == 2
352+ assert all (r ["success" ] for r in results )
353+
354+
355+ def test_build_certificates_handles_solana_registration_error (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
356+ """Testa tratamento de erro quando o registro na Solana falha."""
357+ participants = [mock_participant ]
358+
359+ def mock_download_image (url ):
360+ if url == mock_participant .certificate .background :
361+ return mock_certificate_template
362+ elif url == mock_participant .certificate .logo :
363+ return mock_logo
364+ elif url == "https://example.test/logo.png" :
365+ return mock_logo_tech_floripa
366+ raise ValueError (f"URL não mockada: { url } " )
367+
368+ with patch .object (certified_builder , '_download_image' , side_effect = mock_download_image ), \
369+ patch ('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana' , return_value = {
370+ "blockchain" : {} # Sem explorer_url - simula erro
371+ }):
372+
373+ results = certified_builder .build_certificates (participants )
374+
375+ assert len (results ) == 1
376+ assert results [0 ]["success" ] == False
377+ assert "Failed to get authenticity verification URL" in results [0 ]["error" ]
378+
379+
380+ def test_save_certificate_saves_to_temp_directory (certified_builder , mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
381+ """Testa que o certificado é salvo no diretório temporário."""
382+ mock_participant .authenticity_verification_url = "https://example.com/verify"
383+
384+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
385+ certificate = certified_builder .generate_certificate (mock_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
386+ file_path = certified_builder .save_certificate (certificate , mock_participant )
387+
388+ assert file_path .startswith ("/tmp/certificates/" )
389+ assert file_path .endswith (".png" )
390+
391+ import os
392+ assert os .path .exists (file_path ), "Arquivo do certificado não foi criado"
393+
394+
395+ def test_generate_certificate_with_long_name (certified_builder , mock_certificate , mock_certificate_template , mock_logo , mock_logo_tech_floripa ):
396+ """Testa geração de certificado com nome muito longo."""
397+ mock_event = Event (
398+ order_id = 1 ,
399+ product_id = 1 ,
400+ product_name = "Evento Teste" ,
401+ date = datetime .now (),
402+ time_checkin = datetime .now (),
403+ checkin_latitude = 0.0 ,
404+ checkin_longitude = 0.0
405+ )
406+
407+ long_name_participant = Participant (
408+ first_name = "João Pedro" ,
409+ last_name = "da Silva Santos Oliveira" ,
410+ email = "joao@example.com" ,
411+ phone = "(48) 99999-9999" ,
412+ cpf = "123.456.789-00" ,
413+ certificate = mock_certificate ,
414+ event = mock_event
415+ )
416+ long_name_participant .authenticity_verification_url = "https://example.com/verify"
417+
418+ with patch ('certified_builder.utils.fetch_file_certificate.fetch_file_certificate' , side_effect = [mock_certificate_template , mock_logo ]):
419+ result = certified_builder .generate_certificate (long_name_participant , mock_certificate_template , mock_logo , mock_logo_tech_floripa )
420+
421+ assert isinstance (result , Image .Image )
422+ assert result .size == mock_certificate_template .size
423+
424+
425+ def test_ensure_valid_rgba_converts_rgb_image (certified_builder ):
426+ """Testa que _ensure_valid_rgba converte imagens RGB para RGBA."""
427+ rgb_image = Image .new ("RGB" , (100 , 100 ), (255 , 0 , 0 ))
428+ rgba_image = certified_builder ._ensure_valid_rgba (rgb_image )
429+
430+ assert rgba_image .mode == "RGBA"
431+ assert rgba_image .size == rgb_image .size
432+
433+
434+ def test_ensure_valid_rgba_preserves_rgba_image (certified_builder ):
435+ """Testa que _ensure_valid_rgba preserva imagens já em RGBA."""
436+ rgba_image = Image .new ("RGBA" , (100 , 100 ), (255 , 0 , 0 , 128 ))
437+ result = certified_builder ._ensure_valid_rgba (rgba_image )
438+
439+ assert result .mode == "RGBA"
440+ assert result .size == rgba_image .size
441+
0 commit comments