From 385b76d31c557d0b050fae813a8753265b809662 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Sat, 6 Dec 2025 12:08:40 -0300 Subject: [PATCH 1/3] feat: add pyproject.toml for project configuration and specify Python version --- .python-version | 1 + pyproject.toml | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .python-version create mode 100644 pyproject.toml diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bb47245 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "certified-builder-py" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "retry==0.9.2", + "annotated-types==0.7.0", + "anyio==4.6.2.post1", + "boto3==1.37.22", + "botocore==1.37.22", + "certifi==2024.8.30", + "dnspython==2.7.0", + "email_validator==2.2.0", + "h11==0.14.0", + "httpcore==1.0.7", + "httpx==0.27.2", + "idna==3.10", + "iniconfig==2.0.0", + "jmespath==1.0.1", + "packaging==24.2", + "pillow==11.0.0", + "pluggy==1.5.0", + "pydantic==2.10.1", + "pydantic-settings==2.8.1", + "pydantic_core==2.27.1", + "pytest==8.3.3", + "python-dateutil==2.9.0.post0", + "python-dotenv==1.1.0", + "s3transfer==0.11.4", + "six==1.17.0", + "sniffio==1.3.1", + "typing_extensions==4.12.2", + "urllib3==2.3.0", + "qrcode==8.2", + "retry==0.9.2" +] From 85aab3f3849dd252be69f4c30c00d6da36bd2e5b Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Sat, 6 Dec 2025 12:11:20 -0300 Subject: [PATCH 2/3] Refactor test cases for improved readability and consistency - Updated formatting and indentation in test files for better clarity. - Consolidated multiple lines into single lines where appropriate. - Added missing commas in dictionary definitions and function calls. - Enhanced comments for better understanding of test logic. - Ensured consistent use of whitespace around operators and after commas. - Improved naming conventions and structure in test functions. --- aws/boto_aws.py | 6 +- aws/s3_service.py | 17 +- aws/sqs_service.py | 11 +- certified_builder/certificates_on_solana.py | 39 +- certified_builder/certified_builder.py | 283 +++++---- certified_builder/make_qrcode.py | 20 +- certified_builder/solana_explorer_url.py | 2 - .../utils/fetch_file_certificate.py | 6 +- lambda_function.py | 197 ++++--- models/certificate.py | 2 +- models/event.py | 2 +- models/participant.py | 117 ++-- tests/conftest.py | 14 +- tests/test_certificates_on_solana.py | 47 +- tests/test_certified_builder.py | 548 +++++++++++++----- tests/test_make_qrcode.py | 2 - tests/test_participant.py | 91 +-- 17 files changed, 913 insertions(+), 491 deletions(-) diff --git a/aws/boto_aws.py b/aws/boto_aws.py index 753d34e..f329ca2 100644 --- a/aws/boto_aws.py +++ b/aws/boto_aws.py @@ -2,9 +2,11 @@ from enum import Enum from config import config + class ServiceNameAWS(Enum): - S3 = 's3' - SQS = 'sqs' + S3 = "s3" + SQS = "sqs" + def get_instance_aws(service_name: ServiceNameAWS): return client( diff --git a/aws/s3_service.py b/aws/s3_service.py index c8f0d2b..e7faf69 100644 --- a/aws/s3_service.py +++ b/aws/s3_service.py @@ -4,21 +4,20 @@ logger = logging.getLogger(__name__) + class S3Service: def __init__(self): - self.aws = get_instance_aws( - ServiceNameAWS.S3 - ) + self.aws = get_instance_aws(ServiceNameAWS.S3) self.bucket_name = config.BUCKET_NAME def upload_file(self, file_path: str, key: str): try: - response = self.aws.upload_file( - file_path, - self.bucket_name, - key + response = self.aws.upload_file(file_path, self.bucket_name, key) + logger.info( + f"Arquivo {file_path} enviado para o bucket {self.bucket_name} com sucesso" ) - logger.info(f"Arquivo {file_path} enviado para o bucket {self.bucket_name} com sucesso") except Exception as e: - logger.error(f"Erro ao enviar o arquivo {file_path} para o bucket {self.bucket_name}: {e}") + logger.error( + f"Erro ao enviar o arquivo {file_path} para o bucket {self.bucket_name}: {e}" + ) raise e diff --git a/aws/sqs_service.py b/aws/sqs_service.py index dc90946..e1d53f8 100644 --- a/aws/sqs_service.py +++ b/aws/sqs_service.py @@ -7,16 +7,19 @@ logger = logging.getLogger(__name__) + class SQSService: def __init__(self): self.aws = get_instance_aws(ServiceNameAWS.SQS) - self.queue_url = config.QUEUE_URL + self.queue_url = config.QUEUE_URL def send_message(self, messagens: List[Dict]): try: # Add MessageGroupId for FIFO queue # Using order_id as MessageGroupId to ensure messages for the same order are processed in order - logger.info(f"Enviando mensagem para a fila {self.queue_url} com {len(messagens)} mensagens") + logger.info( + f"Enviando mensagem para a fila {self.queue_url} com {len(messagens)} mensagens" + ) response = self.aws.send_message( QueueUrl=self.queue_url, MessageBody=json.dumps(messagens), @@ -24,5 +27,7 @@ def send_message(self, messagens: List[Dict]): logger.info(f"Mensagem enviada com sucesso: {response['MessageId']}") return response except ClientError as e: - logger.error(f"Erro ao enviar mensagem para a fila {self.queue_url}: {str(e)}") + logger.error( + f"Erro ao enviar mensagem para a fila {self.queue_url}: {str(e)}" + ) raise diff --git a/certified_builder/certificates_on_solana.py b/certified_builder/certificates_on_solana.py index 6d1ec65..76210c2 100644 --- a/certified_builder/certificates_on_solana.py +++ b/certified_builder/certificates_on_solana.py @@ -9,22 +9,35 @@ class CertificatesOnSolanaException(Exception): """Custom exception for CertificatesOnSolana errors.""" - def __init__(self, message: str = "Error registering certificate on Solana", - details: str = "", - cause: Exception = None): + + def __init__( + self, + message: str = "Error registering certificate on Solana", + details: str = "", + cause: Exception = None, + ): super().__init__(message) self.details = details self.cause = cause -class CertificatesOnSolana: - """ +class CertificatesOnSolana: + """ A class to manage certificates on the Solana blockchain Service.""" @staticmethod - @retry(tries=3, delay=3, backoff=2, exceptions=(httpx.RequestError,CertificatesOnSolanaException, Exception), logger=logger) + @retry( + tries=3, + delay=3, + backoff=2, + exceptions=(httpx.RequestError, CertificatesOnSolanaException, Exception), + logger=logger, + ) def register_certificate_on_solana(certificate_data: dict) -> dict: - logger.info("Registering certificate on Solana blockchain with data: %s", certificate_data) + logger.info( + "Registering certificate on Solana blockchain with data: %s", + certificate_data, + ) """ Registers a certificate on the Solana blockchain. @@ -37,17 +50,17 @@ def register_certificate_on_solana(certificate_data: dict) -> dict: try: with httpx.Client(timeout=60.0) as client: response = client.post( - url= config.SERVICE_URL_REGISTRATION_API_SOLANA, + url=config.SERVICE_URL_REGISTRATION_API_SOLANA, headers={ "x-api-key": config.SERVICE_API_KEY_REGISTRATION_API_SOLANA, - "Content-Type": "application/json" + "Content-Type": "application/json", }, - json=certificate_data + json=certificate_data, ) - logger.info(f"Solana response status code: {response.status_code}") + logger.info(f"Solana response status code: {response.status_code}") response.raise_for_status() solana_response = response.json() - return solana_response + return solana_response except Exception as e: logger.error(f"Error registering certificate on Solana: {str(e)}") - raise CertificatesOnSolanaException(details=str(e), cause=e) \ No newline at end of file + raise CertificatesOnSolanaException(details=str(e), cause=e) diff --git a/certified_builder/certified_builder.py b/certified_builder/certified_builder.py index b7c5fc0..ce5426d 100644 --- a/certified_builder/certified_builder.py +++ b/certified_builder/certified_builder.py @@ -1,5 +1,6 @@ import logging import os +import time from typing import List from PIL import Image, ImageDraw, ImageFont from models.participant import Participant @@ -9,45 +10,61 @@ from certified_builder.make_qrcode import MakeQRCode from certified_builder.solana_explorer_url import extract_solana_explorer_url -FONT_NAME = os.path.join(os.path.dirname(__file__), "fonts/PinyonScript/PinyonScript-Regular.ttf") -VALIDATION_CODE = os.path.join(os.path.dirname(__file__), "fonts/ChakraPetch/ChakraPetch-SemiBold.ttf") -DETAILS_FONT = os.path.join(os.path.dirname(__file__), "fonts/ChakraPetch/ChakraPetch-Regular.ttf") +FONT_NAME = os.path.join( + os.path.dirname(__file__), "fonts/PinyonScript/PinyonScript-Regular.ttf" +) +VALIDATION_CODE = os.path.join( + os.path.dirname(__file__), "fonts/ChakraPetch/ChakraPetch-SemiBold.ttf" +) +DETAILS_FONT = os.path.join( + os.path.dirname(__file__), "fonts/ChakraPetch/ChakraPetch-Regular.ttf" +) TEXT_COLOR = (0, 0, 0) logger = logging.getLogger(__name__) + class CertifiedBuilder: def __init__(self): # Ensure temp directory exists self.temp_dir = "/tmp/certificates" os.makedirs(self.temp_dir, exist_ok=True) - def build_certificates(self, participants: List[Participant]): """Build certificates for all participants.""" try: logger.info(f"Iniciando geração de {len(participants)} certificados") results = [] - + # Cache for background and logo if they are the same for all participants certificate_template = None logo = None logo_tech_floripa = None - + # Check if all participants share the same background and logo if participants: first_participant = participants[0] - all_same_background = all(p.certificate.background == first_participant.certificate.background for p in participants) - all_same_logo = all(p.certificate.logo == first_participant.certificate.logo for p in participants) - + all_same_background = all( + p.certificate.background == first_participant.certificate.background + for p in participants + ) + all_same_logo = all( + p.certificate.logo == first_participant.certificate.logo + for p in participants + ) + # Download shared resources once if they are the same for all if all_same_background: - certificate_template = self._download_image(first_participant.certificate.background) + certificate_template = self._download_image( + first_participant.certificate.background + ) if all_same_logo: logo = self._download_image(first_participant.certificate.logo) - + if not logo_tech_floripa: - logo_tech_floripa = self._download_image(config.TECH_FLORIPA_LOGO_URL) + logo_tech_floripa = self._download_image( + config.TECH_FLORIPA_LOGO_URL + ) for participant in participants: try: @@ -57,42 +74,64 @@ def build_certificates(self, participants: List[Participant]): "name": participant.name_completed(), "event": participant.event.product_name, "email": participant.email, - "certificate_code": participant.formated_validation_code() + "certificate_code": participant.formated_validation_code(), } ) - + time.sleep( + 2 + ) # alteração: espera para evitar problemas de taxa de requisições # alteração: agora usamos a função renomeada que apenas extrai o explorer_url - participant.authenticity_verification_url = extract_solana_explorer_url(solana_response=solana_response) - - if not participant.authenticity_verification_url: - raise RuntimeError("Failed to get authenticity verification URL from Solana response") - logger.info(f"URL de verificação de autenticidade: {participant.authenticity_verification_url}") + participant.authenticity_verification_url = ( + extract_solana_explorer_url(solana_response=solana_response) + ) + + if not participant.authenticity_verification_url: + raise RuntimeError( + "Failed to get authenticity verification URL from Solana response" + ) + logger.info( + f"URL de verificação de autenticidade: {participant.authenticity_verification_url}" + ) # Download template and logo only if they are not shared if not all_same_background: - certificate_template = self._download_image(participant.certificate.background) + certificate_template = self._download_image( + participant.certificate.background + ) if not all_same_logo: logo = self._download_image(participant.certificate.logo) - + # Generate and save certificate - certificate_generated = self.generate_certificate(participant, certificate_template, logo, logo_tech_floripa) - certificate_path = self.save_certificate(certificate_generated, participant) - - results.append({ - "participant": participant.model_dump(), - "certificate_path": certificate_path, - "certificate_key": f"certificates/{participant.event.product_id}/{participant.event.order_id}/{participant.create_name_certificate()}", - "success": True - }) - - logger.info(f"Certificado gerado para {participant.name_completed()} com codigo de validação {participant.formated_validation_code()}") + certificate_generated = self.generate_certificate( + participant, certificate_template, logo, logo_tech_floripa + ) + certificate_path = self.save_certificate( + certificate_generated, participant + ) + + results.append( + { + "participant": participant.model_dump(), + "certificate_path": certificate_path, + "certificate_key": f"certificates/{participant.event.product_id}/{participant.event.order_id}/{participant.create_name_certificate()}", + "success": True, + } + ) + + logger.info( + f"Certificado gerado para {participant.name_completed()} com codigo de validação {participant.formated_validation_code()}" + ) except Exception as e: - logger.error(f"Erro ao gerar certificado para {participant.name_completed()}: {str(e)}") - results.append({ - "participant": participant.model_dump(), - "error": str(e), - "success": False - }) - + logger.error( + f"Erro ao gerar certificado para {participant.name_completed()}: {str(e)}" + ) + results.append( + { + "participant": participant.model_dump(), + "error": str(e), + "success": False, + } + ) + return results except Exception as e: logger.error(f"Erro geral na geração de certificados: {str(e)}") @@ -108,55 +147,71 @@ def _download_image(self, url: str) -> Image: def _ensure_valid_rgba(self, img: Image) -> Image: """Ensure image has a valid RGBA mode with proper transparency channel.""" - if img.mode != 'RGBA': - img = img.convert('RGBA') - + if img.mode != "RGBA": + img = img.convert("RGBA") + # Some PNG images may have problematic transparency channels # Create a new image with proper alpha channel try: - new_img = Image.new('RGBA', img.size, (0, 0, 0, 0)) - new_img.paste(img, (0, 0), img if 'A' in img.mode else None) + new_img = Image.new("RGBA", img.size, (0, 0, 0, 0)) + new_img.paste(img, (0, 0), img if "A" in img.mode else None) return new_img except Exception as e: - logger.warning(f"Erro ao processar transparência, usando método alternativo: {str(e)}") + logger.warning( + f"Erro ao processar transparência, usando método alternativo: {str(e)}" + ) # Fallback method if there's an issue with the alpha channel - new_img = Image.new('RGBA', img.size, (0, 0, 0, 0)) - new_img.paste(img.convert('RGB'), (0, 0)) + new_img = Image.new("RGBA", img.size, (0, 0, 0, 0)) + new_img.paste(img.convert("RGB"), (0, 0)) return new_img - def generate_certificate(self, participant: Participant, certificate_template: Image, logo: Image, logo_tech_floripa: Image): + def generate_certificate( + self, + participant: Participant, + certificate_template: Image, + logo: Image, + logo_tech_floripa: Image, + ): """Generate a certificate for a participant.""" try: # Ensure images have valid transparency channels certificate_template = self._ensure_valid_rgba(certificate_template) logo = self._ensure_valid_rgba(logo) - + # Create transparent layer for text and logo overlay = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0)) - + # Optimize logo size (evita upscaling para reduzir pixelização) logo_max_size = (150, 150) if logo.width > logo_max_size[0] or logo.height > logo_max_size[1]: logo.thumbnail(logo_max_size, Image.Resampling.LANCZOS) - + # Paste logo - handle potential transparency issues try: # Try with mask first overlay.paste(logo, (50, 50), logo) except Exception as e: - logger.warning(f"Erro ao colar logo com máscara, usando método alternativo: {str(e)}") + logger.warning( + f"Erro ao colar logo com máscara, usando método alternativo: {str(e)}" + ) # Fallback without using the logo as its own mask overlay.paste(logo, (50, 50)) - + url_qr_code = f"{config.TECH_FLORIPA_CERTIFICATE_VALIDATE_URL}?validate_code={participant.formated_validation_code()}" - qrcode_size = (150, 150) - logger.info(f"URL do QR code: {url_qr_code} para o certificado de {participant.name_completed()}") - qr_code_image_io = MakeQRCode.generate_qr_code(url_qr_code, logo_tech_floripa=logo_tech_floripa) - qr_code_image = Image.open(qr_code_image_io).convert("RGBA") + qrcode_size = (150, 150) + logger.info( + f"URL do QR code: {url_qr_code} para o certificado de {participant.name_completed()}" + ) + qr_code_image_io = MakeQRCode.generate_qr_code( + url_qr_code, logo_tech_floripa=logo_tech_floripa + ) + qr_code_image = Image.open(qr_code_image_io).convert("RGBA") # comentário: para manter o QR nítido, usamos NEAREST ao redimensionar if qr_code_image.size != qrcode_size: - qr_code_image = qr_code_image.resize(qrcode_size, Image.Resampling.NEAREST) - + qr_code_image = qr_code_image.resize( + qrcode_size, Image.Resampling.NEAREST + ) + # Add QR code to overlay # preciso que a posição do QR code seja abaixo do logo, alinhado à esquerda overlay.paste(qr_code_image, (50, 200), qr_code_image) @@ -165,7 +220,9 @@ def generate_certificate(self, participant: Participant, certificate_template: I # comentário: camada de texto criada para ficar logo abaixo do QR code, centralizada ao QR e com espaçamento justo try: # calcula centralização do texto com base na largura do QR - tmp_img = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0)) + tmp_img = Image.new( + "RGBA", certificate_template.size, (255, 255, 255, 0) + ) tmp_draw = ImageDraw.Draw(tmp_img) tmp_font = ImageFont.truetype(DETAILS_FONT, 16) text_bbox = tmp_draw.textbbox((0, 0), "Scan to Validate", font=tmp_font) @@ -174,67 +231,85 @@ def generate_certificate(self, participant: Participant, certificate_template: I text_y = 185 + qrcode_size[1] # espaçamento curto (quase colado) scan_text_image = self.create_scan_to_validate_image( - size=certificate_template.size, - position=(text_x, text_y) + size=certificate_template.size, position=(text_x, text_y) ) overlay.paste(scan_text_image, (0, 0), scan_text_image) logger.info("Texto 'Scan to Validate' adicionado abaixo do QR code") except Exception as e: logger.warning(f"Falha ao adicionar texto 'Scan to Validate': {str(e)}") - # Add name - name_image = self.create_name_image(participant.name_completed(), certificate_template.size) - + name_image = self.create_name_image( + participant.name_completed(), certificate_template.size + ) + # Paste with error handling try: overlay.paste(name_image, (0, 0), name_image) except Exception as e: - logger.warning(f"Erro ao colar nome com máscara, usando método alternativo: {str(e)}") + logger.warning( + f"Erro ao colar nome com máscara, usando método alternativo: {str(e)}" + ) # Try without mask overlay.paste(name_image, (0, 0)) - + # Add details - details_image = self.create_details_image(participant.certificate.details, certificate_template.size) + details_image = self.create_details_image( + participant.certificate.details, certificate_template.size + ) name_center_y = certificate_template.size[1] // 2 details_y = name_center_y + 50 - - details_with_position = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0)) - + + details_with_position = Image.new( + "RGBA", certificate_template.size, (255, 255, 255, 0) + ) + # Paste with error handling try: - details_with_position.paste(details_image, (0, details_y), details_image) + details_with_position.paste( + details_image, (0, details_y), details_image + ) except Exception as e: - logger.warning(f"Erro ao colar detalhes com máscara, usando método alternativo: {str(e)}") + logger.warning( + f"Erro ao colar detalhes com máscara, usando método alternativo: {str(e)}" + ) details_with_position.paste(details_image, (0, details_y)) - + try: overlay = Image.alpha_composite(overlay, details_with_position) except Exception as e: - logger.warning(f"Erro na composição alpha, usando método alternativo: {str(e)}") + logger.warning( + f"Erro na composição alpha, usando método alternativo: {str(e)}" + ) # Fallback to simple paste if alpha composite fails overlay.paste(details_with_position, (0, 0)) - + # Add validation code - validation_code_image = self.create_validation_code_image(participant.formated_validation_code(), certificate_template.size) - + validation_code_image = self.create_validation_code_image( + participant.formated_validation_code(), certificate_template.size + ) + try: overlay.paste(validation_code_image, (0, 0), validation_code_image) except Exception as e: - logger.warning(f"Erro ao colar código de validação com máscara, usando método alternativo: {str(e)}") + logger.warning( + f"Erro ao colar código de validação com máscara, usando método alternativo: {str(e)}" + ) overlay.paste(validation_code_image, (0, 0)) - + # Merge and optimize final image result = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0)) result.paste(certificate_template, (0, 0)) - + try: result = Image.alpha_composite(result, overlay) except Exception as e: - logger.warning(f"Erro na composição alpha final, usando método alternativo: {str(e)}") + logger.warning( + f"Erro na composição alpha final, usando método alternativo: {str(e)}" + ) # Fallback to simple paste if alpha composite fails result.paste(overlay, (0, 0)) - + return result except Exception as e: logger.error(f"Erro ao gerar certificado: {str(e)}") @@ -263,27 +338,29 @@ def create_details_image(self, details: str, size: tuple) -> Image: words = details.split() total_words = len(words) words_per_line = total_words // 3 - - line1 = ' '.join(words[:words_per_line]) - line2 = ' '.join(words[words_per_line:words_per_line*2]) - line3 = ' '.join(words[words_per_line*2:]) - + + line1 = " ".join(words[:words_per_line]) + line2 = " ".join(words[words_per_line : words_per_line * 2]) + line3 = " ".join(words[words_per_line * 2 :]) + line_height = font.size + 10 - + line1_bbox = draw.textbbox((0, 0), line1, font=font) line2_bbox = draw.textbbox((0, 0), line2, font=font) line3_bbox = draw.textbbox((0, 0), line3, font=font) - + start_y = 0 - + x1 = (size[0] - (line1_bbox[2] - line1_bbox[0])) / 2 x2 = (size[0] - (line2_bbox[2] - line2_bbox[0])) / 2 x3 = (size[0] - (line3_bbox[2] - line3_bbox[0])) / 2 - + draw.text((x1, start_y), line1, fill=TEXT_COLOR, font=font) draw.text((x2, start_y + line_height), line2, fill=TEXT_COLOR, font=font) - draw.text((x3, start_y + line_height * 2), line3, fill=TEXT_COLOR, font=font) - + draw.text( + (x3, start_y + line_height * 2), line3, fill=TEXT_COLOR, font=font + ) + return details_image except Exception as e: logger.error(f"Erro ao criar imagem dos detalhes: {str(e)}") @@ -295,7 +372,9 @@ def create_validation_code_image(self, validation_code: str, size: tuple) -> Ima validation_code_image = Image.new("RGBA", size, (255, 255, 255, 0)) draw = ImageDraw.Draw(validation_code_image) font = ImageFont.truetype(VALIDATION_CODE, 20) - position = self.calculate_validation_code_position(validation_code, font, draw, size) + position = self.calculate_validation_code_position( + validation_code, font, draw, size + ) draw.text(position, validation_code, fill=TEXT_COLOR, font=font) return validation_code_image except Exception as e: @@ -315,14 +394,18 @@ def create_scan_to_validate_image(self, size: tuple, position: tuple) -> Image: logger.error(f"Erro ao criar imagem do texto 'Scan to Validate': {str(e)}") raise - def calculate_text_position(self, text: str, font: ImageFont, draw: ImageDraw, size: tuple) -> tuple: + def calculate_text_position( + self, text: str, font: ImageFont, draw: ImageDraw, size: tuple + ) -> tuple: """Calculate centered position for text.""" text_bbox = draw.textbbox((0, 0), text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] return ((size[0] - text_width) / 2, (size[1] - text_height) / 2) - def calculate_validation_code_position(self, validation_code: str, font: ImageFont, draw: ImageDraw, size: tuple) -> tuple: + def calculate_validation_code_position( + self, validation_code: str, font: ImageFont, draw: ImageDraw, size: tuple + ) -> tuple: """Calculate position for validation code.""" text_bbox = draw.textbbox((0, 0), validation_code, font=font) text_width = text_bbox[2] - text_bbox[0] @@ -335,10 +418,10 @@ def save_certificate(self, certificate: Image, participant: Participant) -> str: name_certificate = participant.create_name_certificate() file_path = os.path.join(self.temp_dir, name_certificate) # Optimize image before saving - certificate = certificate.convert('RGB') - certificate.save(file_path, format="PNG", optimize=True) + certificate = certificate.convert("RGB") + certificate.save(file_path, format="PNG", optimize=True) return file_path - + except Exception as e: logger.error(f"Erro ao salvar certificado: {str(e)}") raise diff --git a/certified_builder/make_qrcode.py b/certified_builder/make_qrcode.py index 35e2cfd..f31ea8c 100644 --- a/certified_builder/make_qrcode.py +++ b/certified_builder/make_qrcode.py @@ -1,4 +1,3 @@ - import qrcode import logging from io import BytesIO @@ -7,9 +6,10 @@ logger = logging.getLogger(__name__) + class MakeQRCode: @staticmethod - def generate_qr_code(data: str, logo_tech_floripa: Image) -> BytesIO: + def generate_qr_code(data: str, logo_tech_floripa: Image) -> BytesIO: try: logger.info(f"Generating QR code for {data}") qr = qrcode.QRCode( @@ -21,9 +21,11 @@ def generate_qr_code(data: str, logo_tech_floripa: Image) -> BytesIO: qr.add_data(data) qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="transparent", image_factory=PilImage) + img = qr.make_image( + fill_color="black", back_color="transparent", image_factory=PilImage + ) img = img.convert("RGBA") - + # Redimensiona o logo para aproximadamente 30% do tamanho do QR code # Tamanho limitado para não interferir nos padrões de detecção nos cantos qr_width, qr_height = img.size @@ -31,20 +33,20 @@ def generate_qr_code(data: str, logo_tech_floripa: Image) -> BytesIO: logo_resized = logo_tech_floripa.copy() logo_resized.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) logo_resized = logo_resized.convert("RGBA") - + # Calcula a posição central para colar o logo logo_width, logo_height = logo_resized.size position = ((qr_width - logo_width) // 2, (qr_height - logo_height) // 2) - + # Cola o logo no centro do QR code mantendo transparência # Com ERROR_CORRECT_H (30% redundância), o QR code permanece legível mesmo com o logo img.paste(logo_resized, position, logo_resized) - + byte_io = BytesIO() - img.save(byte_io, format='PNG') + img.save(byte_io, format="PNG") byte_io.seek(0) logger.info(f"QR code generated successfully for {data}") return byte_io except Exception as e: logging.error(f"Failed to generate QR code: {e}") - raise \ No newline at end of file + raise diff --git a/certified_builder/solana_explorer_url.py b/certified_builder/solana_explorer_url.py index c36cc66..4fca7b9 100644 --- a/certified_builder/solana_explorer_url.py +++ b/certified_builder/solana_explorer_url.py @@ -2,5 +2,3 @@ def extract_solana_explorer_url(solana_response: dict) -> str: # alteração: extrai a URL do explorer do bloco "blockchain" conforme contrato oficial explorer_url = solana_response.get("blockchain", {}).get("explorer_url", "") return explorer_url - - diff --git a/certified_builder/utils/fetch_file_certificate.py b/certified_builder/utils/fetch_file_certificate.py index 836292c..905a81f 100644 --- a/certified_builder/utils/fetch_file_certificate.py +++ b/certified_builder/utils/fetch_file_certificate.py @@ -3,14 +3,15 @@ import httpx import os + def fetch_file_certificate(url_certificate) -> Image: # Configure client for Lambda environment client = httpx.Client( timeout=30.0, verify=False, # Disable SSL verification if needed - follow_redirects=True + follow_redirects=True, ) - + try: response = client.get(url_certificate) response.raise_for_status() # Raise an exception for bad status codes @@ -20,4 +21,3 @@ def fetch_file_certificate(url_certificate) -> Image: raise Exception(f"Error fetching certificate: {str(e)}") finally: client.close() - \ No newline at end of file diff --git a/lambda_function.py b/lambda_function.py index cc4099a..2d5253a 100644 --- a/lambda_function.py +++ b/lambda_function.py @@ -20,155 +20,190 @@ # Add a StreamHandler for CloudWatch handler = logging.StreamHandler() -handler.setFormatter(logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -)) +handler.setFormatter( + logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) +) logger.addHandler(handler) + class DateTimeEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): - return obj.strftime('%Y-%m-%d %H:%M:%S') - if hasattr(obj, 'model_dump'): + return obj.strftime("%Y-%m-%d %H:%M:%S") + if hasattr(obj, "model_dump"): return obj.model_dump() return super().default(obj) + def extract_data_body(event): - try: + try: logger.info("Event recebido : {}".format(event)) - record = event['Records'][0] - if isinstance(record['body'], str): - body = json.loads(record['body']) + record = event["Records"][0] + if isinstance(record["body"], str): + body = json.loads(record["body"]) return body else: - return record['body'] + return record["body"] except Exception as e: logger.error(f"Erro ao extrair dados do body: {str(e)}", exc_info=True) raise + def create_participant_object(participant_data): # Create Certificate object certificate = Certificate( - details=participant_data.get('certificate_details'), - logo=participant_data.get('certificate_logo'), - background=participant_data.get('certificate_background') + details=participant_data.get("certificate_details"), + logo=participant_data.get("certificate_logo"), + background=participant_data.get("certificate_background"), ) - + # Create Event object event = Event( - order_id=participant_data.get('order_id'), - product_id=participant_data.get('product_id'), - product_name=participant_data.get('product_name'), - date=datetime.strptime(participant_data.get('order_date'), "%Y-%m-%d %H:%M:%S"), - time_checkin=datetime.strptime(participant_data.get('time_checkin'), "%Y-%m-%d %H:%M:%S") if participant_data.get('time_checkin') else None, - checkin_latitude=float(participant_data.get('checkin_latitude')) if participant_data.get('checkin_latitude') else None, - checkin_longitude=float(participant_data.get('checkin_longitude')) if participant_data.get('checkin_longitude') else None + order_id=participant_data.get("order_id"), + product_id=participant_data.get("product_id"), + product_name=participant_data.get("product_name"), + date=datetime.strptime(participant_data.get("order_date"), "%Y-%m-%d %H:%M:%S"), + time_checkin=( + datetime.strptime(participant_data.get("time_checkin"), "%Y-%m-%d %H:%M:%S") + if participant_data.get("time_checkin") + else None + ), + checkin_latitude=( + float(participant_data.get("checkin_latitude")) + if participant_data.get("checkin_latitude") + else None + ), + checkin_longitude=( + float(participant_data.get("checkin_longitude")) + if participant_data.get("checkin_longitude") + else None + ), ) - + # Create Participant object participant = Participant( - first_name=participant_data.get('first_name'), - last_name=participant_data.get('last_name'), - email=participant_data.get('email'), - phone=participant_data.get('phone'), - cpf=participant_data.get('cpf', ''), + first_name=participant_data.get("first_name"), + last_name=participant_data.get("last_name"), + email=participant_data.get("email"), + phone=participant_data.get("phone"), + cpf=participant_data.get("cpf", ""), certificate=certificate, - event=event + event=event, ) return participant + def lambda_handler(event, context): # Log the start of the Lambda execution try: - logger.info("Starting Lambda execution") + logger.info("Starting Lambda execution") body = extract_data_body(event) participants_data = body - + if not participants_data: logger.warning("No participants found in message") return { - 'statusCode': 400, - 'body': json.dumps({ - 'error': 'No participants found in message', - 'message': 'Nenhum participante encontrado para processamento' - }) + "statusCode": 400, + "body": json.dumps( + { + "error": "No participants found in message", + "message": "Nenhum participante encontrado para processamento", + } + ), } s3_service = S3Service() sqs_service = SQSService() logger.info(f"Processing {len(participants_data)} participants") - + # Create list of participants participants = [] results = [] - + for participant_data in participants_data: try: participant = create_participant_object(participant_data) participants.append(participant) except Exception as e: logger.error(f"Error creating participant object: {str(e)}") - results.append({ - 'participant_data': participant_data, - 'error': str(e), - 'success': False - }) - + results.append( + { + "participant_data": participant_data, + "error": str(e), + "success": False, + } + ) + # Generate certificates if we have valid participants if participants: builder = CertifiedBuilder() certificates_results = builder.build_certificates(participants) # Format results before adding to response certificates_results_messagens = [] - + for result in certificates_results: - - - if result.get('success'): - s3_service.upload_file(result.get('certificate_path'), result.get('certificate_key')) - - - certificates_results_messagens.append({ - "order_id": result.get('participant', {}).get('event', {}).get('order_id', ""), - "validation_code": result.get('participant', {}).get('validation_code', ""), - "authenticity_verification_url": result.get('participant', {}).get('authenticity_verification_url', ""), - "product_id": result.get('participant', {}).get('event', {}).get('product_id', ""), - "product_name": result.get('participant', {}).get('event', {}).get('product_name', ""), - "email": result.get('participant', {}).get('email', ""), - "certificate_key": result.get('certificate_key', ""), - "success": result.get('success', False) - }) - + + if result.get("success"): + s3_service.upload_file( + result.get("certificate_path"), result.get("certificate_key") + ) + + certificates_results_messagens.append( + { + "order_id": result.get("participant", {}) + .get("event", {}) + .get("order_id", ""), + "validation_code": result.get("participant", {}).get( + "validation_code", "" + ), + "authenticity_verification_url": result.get( + "participant", {} + ).get("authenticity_verification_url", ""), + "product_id": result.get("participant", {}) + .get("event", {}) + .get("product_id", ""), + "product_name": result.get("participant", {}) + .get("event", {}) + .get("product_name", ""), + "email": result.get("participant", {}).get("email", ""), + "certificate_key": result.get("certificate_key", ""), + "success": result.get("success", False), + } + ) + sqs_service.send_message(certificates_results_messagens) - + logger.info("Certificados gerados com sucesso") return { - 'statusCode': 200, - 'body': json.dumps({ - 'message': 'Processamento concluído', - 'results': results - }, cls=DateTimeEncoder) + "statusCode": 200, + "body": json.dumps( + {"message": "Processamento concluído", "results": results}, + cls=DateTimeEncoder, + ), } else: logger.warning("No valid participants to process") return { - 'statusCode': 400, - 'body': json.dumps({ - 'error': 'No valid participants to process', - 'message': 'Nenhum participante válido para processamento', - 'results': results - }, cls=DateTimeEncoder) + "statusCode": 400, + "body": json.dumps( + { + "error": "No valid participants to process", + "message": "Nenhum participante válido para processamento", + "results": results, + }, + cls=DateTimeEncoder, + ), } - + except Exception as e: logger.error(f"Error in lambda handler: {str(e)}") return { - 'statusCode': 500, - 'body': json.dumps({ - 'error': str(e), - 'message': 'Erro ao gerar certificados' - }) + "statusCode": 500, + "body": json.dumps( + {"error": str(e), "message": "Erro ao gerar certificados"} + ), } - diff --git a/models/certificate.py b/models/certificate.py index 9ebc0fa..8de212c 100644 --- a/models/certificate.py +++ b/models/certificate.py @@ -4,4 +4,4 @@ class Certificate(BaseModel): details: str logo: str - background: str \ No newline at end of file + background: str diff --git a/models/event.py b/models/event.py index cbd26a9..16a0818 100644 --- a/models/event.py +++ b/models/event.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import Optional + class Event(BaseModel): order_id: int product_id: int @@ -10,4 +11,3 @@ class Event(BaseModel): time_checkin: Optional[datetime] = None checkin_latitude: Optional[float] = None checkin_longitude: Optional[float] = None - \ No newline at end of file diff --git a/models/participant.py b/models/participant.py index b051990..c98cce9 100644 --- a/models/participant.py +++ b/models/participant.py @@ -12,110 +12,121 @@ # Configure logger for this module logger = logging.getLogger(__name__) -class Participant(BaseModel): + +class Participant(BaseModel): first_name: str last_name: str email: EmailStr phone: str - cpf: str - validation_code: Optional[str] = Field(default_factory= lambda: ''.join(random.choices(string.hexdigits, k=9)), init=False) + cpf: str + validation_code: Optional[str] = Field( + default_factory=lambda: "".join(random.choices(string.hexdigits, k=9)), + init=False, + ) certificate: Optional[Certificate] = None event: Optional[Event] = None authenticity_verification_url: Optional[str] = None def __str__(self): - return f"Participant: {self.first_name} {self.last_name} - {self.email}" + return f"Participant: {self.first_name} {self.last_name} - {self.email}" # criar metodo name_completed def name_completed(self): self.first_name = self.first_name.lower() self.last_name = self.last_name.lower() - + name_completed = self.first_name + " " + self.last_name if len(name_completed.split(" ")) > 3: self.first_name = name_completed.split()[0] - self.last_name = name_completed.split()[1] + " " + name_completed.split()[-1] + self.last_name = ( + name_completed.split()[1] + " " + name_completed.split()[-1] + ) name_completed = self.first_name + " " + self.last_name - + return name_completed.title() - + def _sanitize_filename(self, text: str) -> str: """ Sanitiza uma string para ser usada como nome de arquivo no S3. Remove ou substitui caracteres especiais que podem causar problemas em URLs. - + Args: text (str): Texto a ser sanitizado - + Returns: str: Texto sanitizado adequado para nomes de arquivo S3 """ # Normaliza caracteres unicode (remove acentos) - text = unicodedata.normalize('NFD', text) - text = ''.join(char for char in text if unicodedata.category(char) != 'Mn') - + text = unicodedata.normalize("NFD", text) + text = "".join(char for char in text if unicodedata.category(char) != "Mn") + # Substitui caracteres especiais comuns por versões seguras special_chars = { - 'º': 'o', - 'ª': 'a', - '×': 'x', - '@': '_at_', - '&': '_and_', - '+': '_plus_', - '=': '_equals_', - '%': '_percent_', - '#': '_hash_', - '?': '_question_', - '/': '_slash_', - '\\': '_backslash_', - ':': '_colon_', - ';': '_semicolon_', - '<': '_lt_', - '>': '_gt_', - '|': '_pipe_', - '*': '_star_', - '"': '_quote_', - "'": '_apostrophe_' + "º": "o", + "ª": "a", + "×": "x", + "@": "_at_", + "&": "_and_", + "+": "_plus_", + "=": "_equals_", + "%": "_percent_", + "#": "_hash_", + "?": "_question_", + "/": "_slash_", + "\\": "_backslash_", + ":": "_colon_", + ";": "_semicolon_", + "<": "_lt_", + ">": "_gt_", + "|": "_pipe_", + "*": "_star_", + '"': "_quote_", + "'": "_apostrophe_", } - + # Aplica as substituições de caracteres especiais for char, replacement in special_chars.items(): text = text.replace(char, replacement) - + # Remove caracteres que não são alfanuméricos, espaços, hífens ou underscores - text = re.sub(r'[^\w\s\-_]', '', text) - + text = re.sub(r"[^\w\s\-_]", "", text) + # Substitui espaços múltiplos por um único espaço - text = re.sub(r'\s+', ' ', text) - + text = re.sub(r"\s+", " ", text) + # Substitui espaços por underscores - text = text.replace(' ', '_') - + text = text.replace(" ", "_") + # Remove underscores múltiplos consecutivos - text = re.sub(r'_+', '_', text) - + text = re.sub(r"_+", "_", text) + # Remove underscores do início e fim - text = text.strip('_') - + text = text.strip("_") + return text - def formated_validation_code(self): self.validation_code = self.validation_code.upper() return f"{self.validation_code[0:3]}-{self.validation_code[3:6]}-{self.validation_code[6:9]}" - def create_name_certificate(self): + def create_name_certificate(self): # Sanitiza o nome do participante e o nome do produto separadamente sanitized_name = self._sanitize_filename(self.name_completed()) sanitized_product = self._sanitize_filename(self.event.product_name) sanitized_validation = self._sanitize_filename(self.formated_validation_code()) - + # Combina os componentes sanitizados - name_certificate = f"{sanitized_name}{sanitized_product}_{sanitized_validation}.png" - - logger.info(f"Nome do participante antes da sanitização: {self.name_completed()}") + name_certificate = ( + f"{sanitized_name}{sanitized_product}_{sanitized_validation}.png" + ) + + logger.info( + f"Nome do participante antes da sanitização: {self.name_completed()}" + ) logger.info(f"Nome do produto antes da sanitização: {self.event.product_name}") - logger.info(f"Código de validação antes da sanitização: {self.formated_validation_code()}") + logger.info( + f"Código de validação antes da sanitização: {self.formated_validation_code()}" + ) logger.info(f"Nome do certificado após a sanitização: {name_certificate}") - + return name_certificate diff --git a/tests/conftest.py b/tests/conftest.py index 93e9b91..40621c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,9 +21,11 @@ class MockConfig: SERVICE_URL_REGISTRATION_API_SOLANA = "https://example.test/solana/register" SERVICE_API_KEY_REGISTRATION_API_SOLANA = "test-api-key" # comentário: URL de validação mockada para o Tech Floripa usada nos testes - TECH_FLORIPA_CERTIFICATE_VALIDATE_URL = "https://example.test/certificate-validate/" + TECH_FLORIPA_CERTIFICATE_VALIDATE_URL = ( + "https://example.test/certificate-validate/" + ) TECH_FLORIPA_LOGO_URL = "https://example.test/logo.png" - + # comentário: expõe tanto a classe quanto a instância, como o módulo real faria mock_module.Config = MockConfig mock_module.config = MockConfig() @@ -57,9 +59,7 @@ def _mock_solana_registration(monkeypatch, request): # comentário: resposta estável usada nos demais testes fake_response = { - "blockchain": { - "verificacao_url": "https://example.test/verify/abc123" - } + "blockchain": {"verificacao_url": "https://example.test/verify/abc123"} } # comentário: função fake substitui o método estático @@ -70,12 +70,10 @@ def _fake_register_certificate_on_solana(certificate_data: dict) -> dict: # comentário: injeta o mock no alvo correto from certified_builder.certificates_on_solana import CertificatesOnSolana + monkeypatch.setattr( CertificatesOnSolana, "register_certificate_on_solana", staticmethod(_fake_register_certificate_on_solana), raising=False, ) - - - diff --git a/tests/test_certificates_on_solana.py b/tests/test_certificates_on_solana.py index df93f7d..f7fd706 100644 --- a/tests/test_certificates_on_solana.py +++ b/tests/test_certificates_on_solana.py @@ -1,6 +1,9 @@ import pytest from unittest.mock import patch, MagicMock -from certified_builder.certificates_on_solana import CertificatesOnSolana, CertificatesOnSolanaException +from certified_builder.certificates_on_solana import ( + CertificatesOnSolana, + CertificatesOnSolanaException, +) from certified_builder import certificates_on_solana as module_under_test @@ -16,13 +19,24 @@ def sample_payload(): def test_register_certificate_success(sample_payload, monkeypatch): # Configura URLs/chaves do módulo - monkeypatch.setattr(module_under_test.config, "SERVICE_URL_REGISTRATION_API_SOLANA", "https://api.test/solana") - monkeypatch.setattr(module_under_test.config, "SERVICE_API_KEY_REGISTRATION_API_SOLANA", "secret-key") + monkeypatch.setattr( + module_under_test.config, + "SERVICE_URL_REGISTRATION_API_SOLANA", + "https://api.test/solana", + ) + monkeypatch.setattr( + module_under_test.config, + "SERVICE_API_KEY_REGISTRATION_API_SOLANA", + "secret-key", + ) # Mock do client httpx mock_response = MagicMock() mock_response.status_code = 200 - mock_response.json.return_value = {"ok": True, "blockchain": {"verificacao_url": "https://verify"}} + mock_response.json.return_value = { + "ok": True, + "blockchain": {"verificacao_url": "https://verify"}, + } mock_response.raise_for_status.return_value = None mock_client_instance = MagicMock() @@ -30,7 +44,10 @@ def test_register_certificate_success(sample_payload, monkeypatch): mock_client_instance.__enter__.return_value = mock_client_instance mock_client_instance.__exit__.return_value = False - with patch("certified_builder.certificates_on_solana.httpx.Client", return_value=mock_client_instance) as mock_client_cls: + with patch( + "certified_builder.certificates_on_solana.httpx.Client", + return_value=mock_client_instance, + ) as mock_client_cls: result = CertificatesOnSolana.register_certificate_on_solana(sample_payload) # Retorno @@ -48,8 +65,16 @@ def test_register_certificate_success(sample_payload, monkeypatch): def test_register_certificate_http_error_raises(sample_payload, monkeypatch): - monkeypatch.setattr(module_under_test.config, "SERVICE_URL_REGISTRATION_API_SOLANA", "https://api.test/solana") - monkeypatch.setattr(module_under_test.config, "SERVICE_API_KEY_REGISTRATION_API_SOLANA", "secret-key") + monkeypatch.setattr( + module_under_test.config, + "SERVICE_URL_REGISTRATION_API_SOLANA", + "https://api.test/solana", + ) + monkeypatch.setattr( + module_under_test.config, + "SERVICE_API_KEY_REGISTRATION_API_SOLANA", + "secret-key", + ) mock_response = MagicMock() mock_response.status_code = 500 @@ -58,6 +83,7 @@ def test_register_certificate_http_error_raises(sample_payload, monkeypatch): # raise_for_status levanta erro def _raise(): raise Exception("boom") + mock_response.raise_for_status.side_effect = _raise mock_client_instance = MagicMock() @@ -65,10 +91,11 @@ def _raise(): mock_client_instance.__enter__.return_value = mock_client_instance mock_client_instance.__exit__.return_value = False - with patch("certified_builder.certificates_on_solana.httpx.Client", return_value=mock_client_instance): + with patch( + "certified_builder.certificates_on_solana.httpx.Client", + return_value=mock_client_instance, + ): with pytest.raises(CertificatesOnSolanaException) as exc: CertificatesOnSolana.register_certificate_on_solana(sample_payload) assert "boom" in str(exc.value.details) - - diff --git a/tests/test_certified_builder.py b/tests/test_certified_builder.py index de7fbb2..e18c3c4 100644 --- a/tests/test_certified_builder.py +++ b/tests/test_certified_builder.py @@ -9,14 +9,16 @@ from datetime import datetime from unittest.mock import patch + @pytest.fixture def mock_certificate(): return Certificate( details="In recognition of their participation in the 84st edition of the Python Floripa Community Meeting, held on March 29, 2025, in Florianópolis, Brazil.", logo="https://tech.floripa.br/wp-content/uploads/2025/03/84o-Python-Floripa-e1741729144453.png", - background="https://tech.floripa.br/wp-content/uploads/2025/03/Background.png" + background="https://tech.floripa.br/wp-content/uploads/2025/03/Background.png", ) + @pytest.fixture def mock_event(): return Event( @@ -26,9 +28,10 @@ def mock_event(): date=datetime.strptime("2025-03-26 20:55:25", "%Y-%m-%d %H:%M:%S"), time_checkin=datetime.strptime("2025-03-26 20:55:44", "%Y-%m-%d %H:%M:%S"), checkin_latitude=-27.5460492, - checkin_longitude=-48.6227075 + checkin_longitude=-48.6227075, ) + @pytest.fixture def mock_participant(mock_certificate, mock_event): return Participant( @@ -38,53 +41,94 @@ def mock_participant(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) + @pytest.fixture def mock_certificate_template(): return Image.new("RGBA", (1920, 1080), (255, 255, 255, 0)) + @pytest.fixture def mock_logo(): return Image.new("RGBA", (150, 150), (255, 255, 255, 0)) + @pytest.fixture def mock_logo_tech_floripa(): return Image.new("RGBA", (100, 100), (255, 255, 255, 255)) + @pytest.fixture def certified_builder(): return CertifiedBuilder() -def test_generate_certificate(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - certificate = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) + +def test_generate_certificate( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + certificate = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) assert isinstance(certificate, Image.Image) assert certificate.size == mock_certificate_template.size assert certificate.mode == "RGBA" -def test_create_name_image(certified_builder, mock_participant, mock_certificate_template): - name_image = certified_builder.create_name_image(mock_participant.name_completed(), mock_certificate_template.size) + +def test_create_name_image( + certified_builder, mock_participant, mock_certificate_template +): + name_image = certified_builder.create_name_image( + mock_participant.name_completed(), mock_certificate_template.size + ) assert isinstance(name_image, Image.Image) assert name_image.size == mock_certificate_template.size assert name_image.mode == "RGBA" -def test_create_details_image(certified_builder, mock_participant, mock_certificate_template): - details_image = certified_builder.create_details_image(mock_participant.certificate.details, mock_certificate_template.size) + +def test_create_details_image( + certified_builder, mock_participant, mock_certificate_template +): + details_image = certified_builder.create_details_image( + mock_participant.certificate.details, mock_certificate_template.size + ) assert isinstance(details_image, Image.Image) assert details_image.size == mock_certificate_template.size assert details_image.mode == "RGBA" -def test_create_validation_code_image(certified_builder, mock_participant, mock_certificate_template): - validation_code_image = certified_builder.create_validation_code_image(mock_participant.formated_validation_code(), mock_certificate_template.size) + +def test_create_validation_code_image( + certified_builder, mock_participant, mock_certificate_template +): + validation_code_image = certified_builder.create_validation_code_image( + mock_participant.formated_validation_code(), mock_certificate_template.size + ) assert isinstance(validation_code_image, Image.Image) assert validation_code_image.size == mock_certificate_template.size assert validation_code_image.mode == "RGBA" -def test_build_certificates(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): + +def test_build_certificates( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): participants = [mock_participant] - + # comentário: mock do download de imagens e da resposta do serviço Solana para evitar chamada externa # Função que retorna o mock baseado na URL chamada def mock_download_image(url): @@ -92,49 +136,58 @@ def mock_download_image(url): return mock_certificate_template elif url == mock_participant.certificate.logo: return mock_logo - elif url == "https://example.test/logo.png": # TECH_FLORIPA_LOGO_URL do config mock + elif ( + url == "https://example.test/logo.png" + ): # TECH_FLORIPA_LOGO_URL do config mock return mock_logo_tech_floripa raise ValueError(f"URL não mockada: {url}") - - with patch.object(certified_builder, '_download_image', side_effect=mock_download_image), \ - patch('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana', return_value={ - # comentário: mock alinhado ao contrato atual do serviço - "status": "sucesso", - "certificado": { - "event": "evento de teste", - "name": "user test", - "email": "user@test.com", - "uuid": "uuid-123", - "time": "2025-10-31 12:05:38", - "json_canonico": {"fake": "data"}, - "hash_sha256": "deadbeef", - "txid_solana": "fake_txid_abc123", - "network": "devnet", - "timestamp": "2025-10-31 12:05:39", - "timestamp_unix": 1730366739 - }, - "blockchain": { - "rede": "Solana Devnet", - "explorer_url": "https://explorer.solana.com/tx/fake_txid_abc123?cluster=devnet", - "verificacao_url": "http://localhost:8000/certificados/verify/fake_txid_abc123", - "memo_program": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" - }, - "validacao": { - "como_validar": "Recrie o JSON canonizado e compare o hash SHA-256", - "json_canonico_string": "{}", - "hash_esperado": "deadbeef", - "comando_validacao": "printf '{}' | shasum -a 256" - } - }), \ - patch.object(certified_builder, 'save_certificate') as mock_save: - + + with ( + patch.object( + certified_builder, "_download_image", side_effect=mock_download_image + ), + patch( + "certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana", + return_value={ + # comentário: mock alinhado ao contrato atual do serviço + "status": "sucesso", + "certificado": { + "event": "evento de teste", + "name": "user test", + "email": "user@test.com", + "uuid": "uuid-123", + "time": "2025-10-31 12:05:38", + "json_canonico": {"fake": "data"}, + "hash_sha256": "deadbeef", + "txid_solana": "fake_txid_abc123", + "network": "devnet", + "timestamp": "2025-10-31 12:05:39", + "timestamp_unix": 1730366739, + }, + "blockchain": { + "rede": "Solana Devnet", + "explorer_url": "https://explorer.solana.com/tx/fake_txid_abc123?cluster=devnet", + "verificacao_url": "http://localhost:8000/certificados/verify/fake_txid_abc123", + "memo_program": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", + }, + "validacao": { + "como_validar": "Recrie o JSON canonizado e compare o hash SHA-256", + "json_canonico_string": "{}", + "hash_esperado": "deadbeef", + "comando_validacao": "printf '{}' | shasum -a 256", + }, + }, + ), + patch.object(certified_builder, "save_certificate") as mock_save, + ): + certified_builder.build_certificates(participants) - + mock_save.assert_called_once() args = mock_save.call_args[0] assert isinstance(args[0], Image.Image) assert args[1] == mock_participant - + def _count_non_transparent_pixels(img: Image.Image) -> int: # util simples para checar presença de conteúdo desenhado @@ -142,18 +195,34 @@ def _count_non_transparent_pixels(img: Image.Image) -> int: return sum(1 for p in alpha.getdata() if p != 0) -def test_scan_to_validate_is_centered_and_below_qr(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_scan_to_validate_is_centered_and_below_qr( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): # Garante url para QR mock_participant.authenticity_verification_url = "https://example.com/verify" - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) # Recalcula posição esperada do texto seguindo a mesma lógica do código qrcode_size = (150, 150) qr_left = 50 qr_top = 200 - draw_tmp = ImageDraw.Draw(Image.new("RGBA", mock_certificate_template.size, (255, 255, 255, 0))) + draw_tmp = ImageDraw.Draw( + Image.new("RGBA", mock_certificate_template.size, (255, 255, 255, 0)) + ) font = ImageFont.truetype(DETAILS_FONT, 16) text = "Scan to Validate" bbox = draw_tmp.textbbox((0, 0), text, font=font) @@ -174,99 +243,209 @@ def test_scan_to_validate_is_centered_and_below_qr(certified_builder, mock_parti ) cropped = result.crop(crop_box) - assert _count_non_transparent_pixels(cropped) > 0, "Texto 'Scan to Validate' não encontrado na área esperada" + assert ( + _count_non_transparent_pixels(cropped) > 0 + ), "Texto 'Scan to Validate' não encontrado na área esperada" -def test_qr_is_placed_at_expected_region(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_qr_is_placed_at_expected_region( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): mock_participant.authenticity_verification_url = "https://example.com/verify" - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) # QR é esperado em (50,200) com 150x150 qr_left, qr_top = 50, 200 qr_right, qr_bottom = qr_left + 150, qr_top + 150 qr_region = result.crop((qr_left, qr_top, qr_right, qr_bottom)) - assert _count_non_transparent_pixels(qr_region) > 0, "QR não encontrado na região esperada" + assert ( + _count_non_transparent_pixels(qr_region) > 0 + ), "QR não encontrado na região esperada" # Região logo abaixo do QR deve conter o texto (algum conteúdo) - below_region = result.crop((qr_left, qr_bottom, qr_right, min(qr_bottom + 30, result.height))) - assert _count_non_transparent_pixels(below_region) > 0, "Nenhum conteúdo encontrado abaixo do QR onde o texto deveria estar" - - -def test_name_is_centered_on_certificate(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): + below_region = result.crop( + (qr_left, qr_bottom, qr_right, min(qr_bottom + 30, result.height)) + ) + assert ( + _count_non_transparent_pixels(below_region) > 0 + ), "Nenhum conteúdo encontrado abaixo do QR onde o texto deveria estar" + + +def test_name_is_centered_on_certificate( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Verifica que o nome do participante está centralizado no certificado.""" mock_participant.authenticity_verification_url = "https://example.com/verify" - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) - + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) + # Verifica que há conteúdo na região central onde o nome deveria estar center_x = result.width // 2 center_y = result.height // 2 - name_region = result.crop((center_x - 200, center_y - 50, center_x + 200, center_y + 50)) - - assert _count_non_transparent_pixels(name_region) > 0, "Nome não encontrado na região central do certificado" + name_region = result.crop( + (center_x - 200, center_y - 50, center_x + 200, center_y + 50) + ) + + assert ( + _count_non_transparent_pixels(name_region) > 0 + ), "Nome não encontrado na região central do certificado" -def test_validation_code_is_at_bottom_right(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_validation_code_is_at_bottom_right( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Verifica que o código de validação está posicionado no canto inferior direito.""" mock_participant.authenticity_verification_url = "https://example.com/verify" - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) - + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) + # Verifica região inferior direita onde o código de validação deveria estar - validation_region = result.crop((result.width - 250, result.height - 80, result.width, result.height)) - - assert _count_non_transparent_pixels(validation_region) > 0, "Código de validação não encontrado no canto inferior direito" + validation_region = result.crop( + (result.width - 250, result.height - 80, result.width, result.height) + ) + + assert ( + _count_non_transparent_pixels(validation_region) > 0 + ), "Código de validação não encontrado no canto inferior direito" -def test_logo_is_positioned_at_top_left(certified_builder, mock_participant, mock_certificate_template, mock_logo_tech_floripa): +def test_logo_is_positioned_at_top_left( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo_tech_floripa, +): """Verifica que o logo está posicionado no canto superior esquerdo.""" mock_participant.authenticity_verification_url = "https://example.com/verify" # Usa um logo visível (não transparente) para o teste visible_logo = Image.new("RGBA", (150, 150), (255, 255, 255, 255)) - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, visible_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, visible_logo, mock_logo_tech_floripa) - + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, visible_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + visible_logo, + mock_logo_tech_floripa, + ) + # Logo é esperado em (50, 50) com tamanho máximo de 150x150 logo_region = result.crop((50, 50, 200, 200)) - - assert _count_non_transparent_pixels(logo_region) > 0, "Logo não encontrado na região esperada" + + assert ( + _count_non_transparent_pixels(logo_region) > 0 + ), "Logo não encontrado na região esperada" -def test_large_logo_is_resized(certified_builder, mock_participant, mock_certificate_template, mock_logo_tech_floripa): +def test_large_logo_is_resized( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo_tech_floripa, +): """Verifica que logos grandes são redimensionados para o tamanho máximo.""" mock_participant.authenticity_verification_url = "https://example.com/verify" large_logo = Image.new("RGBA", (300, 300), (255, 255, 255, 255)) - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, large_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, large_logo, mock_logo_tech_floripa) - + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, large_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + large_logo, + mock_logo_tech_floripa, + ) + # Verifica que o logo foi redimensionado (não deve ocupar toda a região de 300x300) logo_region = result.crop((50, 50, 200, 200)) - - assert _count_non_transparent_pixels(logo_region) > 0, "Logo redimensionado não encontrado" + assert ( + _count_non_transparent_pixels(logo_region) > 0 + ), "Logo redimensionado não encontrado" -def test_details_are_split_into_three_lines(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): + +def test_details_are_split_into_three_lines( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Verifica que os detalhes do certificado são divididos em 3 linhas.""" mock_participant.authenticity_verification_url = "https://example.com/verify" - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - result = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) - + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + result = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) + # Verifica região onde os detalhes deveriam estar (abaixo do centro) center_y = result.height // 2 details_region = result.crop((0, center_y + 50, result.width, center_y + 150)) - - assert _count_non_transparent_pixels(details_region) > 0, "Detalhes não encontrados na região esperada" + + assert ( + _count_non_transparent_pixels(details_region) > 0 + ), "Detalhes não encontrados na região esperada" -def test_build_certificates_with_multiple_participants_same_resources(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_build_certificates_with_multiple_participants_same_resources( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Testa geração de certificados para múltiplos participantes com mesmo background e logo.""" second_participant = Participant( first_name="Maria", @@ -275,11 +454,11 @@ def test_build_certificates_with_multiple_participants_same_resources(certified_ phone="(48) 99999-9999", cpf="111.111.111-11", certificate=mock_participant.certificate, - event=mock_participant.event + event=mock_participant.event, ) - + participants = [mock_participant, second_participant] - + def mock_download_image(url): if url == mock_participant.certificate.background: return mock_certificate_template @@ -288,33 +467,42 @@ def mock_download_image(url): elif url == "https://example.test/logo.png": return mock_logo_tech_floripa raise ValueError(f"URL não mockada: {url}") - - with patch.object(certified_builder, '_download_image', side_effect=mock_download_image), \ - patch('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana', return_value={ - "blockchain": { - "explorer_url": "https://explorer.solana.com/tx/test123?cluster=devnet" - } - }), \ - patch.object(certified_builder, 'save_certificate') as mock_save: - + + with ( + patch.object( + certified_builder, "_download_image", side_effect=mock_download_image + ), + patch( + "certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana", + return_value={ + "blockchain": { + "explorer_url": "https://explorer.solana.com/tx/test123?cluster=devnet" + } + }, + ), + patch.object(certified_builder, "save_certificate") as mock_save, + ): + results = certified_builder.build_certificates(participants) - + assert len(results) == 2 assert all(r["success"] for r in results) assert mock_save.call_count == 2 -def test_build_certificates_with_different_backgrounds(certified_builder, mock_participant, mock_logo, mock_logo_tech_floripa): +def test_build_certificates_with_different_backgrounds( + certified_builder, mock_participant, mock_logo, mock_logo_tech_floripa +): """Testa geração de certificados para participantes com backgrounds diferentes.""" different_background = "https://example.com/different-background.png" different_template = Image.new("RGBA", (1920, 1080), (200, 200, 200, 255)) - + second_certificate = Certificate( details=mock_participant.certificate.details, logo=mock_participant.certificate.logo, - background=different_background + background=different_background, ) - + second_participant = Participant( first_name="João", last_name="Santos", @@ -322,11 +510,11 @@ def test_build_certificates_with_different_backgrounds(certified_builder, mock_p phone="(48) 88888-8888", cpf="222.222.222-22", certificate=second_certificate, - event=mock_participant.event + event=mock_participant.event, ) - + participants = [mock_participant, second_participant] - + def mock_download_image(url): if url == mock_participant.certificate.background: return Image.new("RGBA", (1920, 1080), (255, 255, 255, 0)) @@ -337,25 +525,38 @@ def mock_download_image(url): elif url == "https://example.test/logo.png": return mock_logo_tech_floripa raise ValueError(f"URL não mockada: {url}") - - with patch.object(certified_builder, '_download_image', side_effect=mock_download_image), \ - patch('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana', return_value={ - "blockchain": { - "explorer_url": "https://explorer.solana.com/tx/test123?cluster=devnet" - } - }), \ - patch.object(certified_builder, 'save_certificate') as mock_save: - + + with ( + patch.object( + certified_builder, "_download_image", side_effect=mock_download_image + ), + patch( + "certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana", + return_value={ + "blockchain": { + "explorer_url": "https://explorer.solana.com/tx/test123?cluster=devnet" + } + }, + ), + patch.object(certified_builder, "save_certificate") as mock_save, + ): + results = certified_builder.build_certificates(participants) - + assert len(results) == 2 assert all(r["success"] for r in results) -def test_build_certificates_handles_solana_registration_error(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_build_certificates_handles_solana_registration_error( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Testa tratamento de erro quando o registro na Solana falha.""" participants = [mock_participant] - + def mock_download_image(url): if url == mock_participant.certificate.background: return mock_certificate_template @@ -364,35 +565,61 @@ def mock_download_image(url): elif url == "https://example.test/logo.png": return mock_logo_tech_floripa raise ValueError(f"URL não mockada: {url}") - - with patch.object(certified_builder, '_download_image', side_effect=mock_download_image), \ - patch('certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana', return_value={ - "blockchain": {} # Sem explorer_url - simula erro - }): - + + with ( + patch.object( + certified_builder, "_download_image", side_effect=mock_download_image + ), + patch( + "certified_builder.certified_builder.CertificatesOnSolana.register_certificate_on_solana", + return_value={"blockchain": {}}, # Sem explorer_url - simula erro + ), + ): + results = certified_builder.build_certificates(participants) - + assert len(results) == 1 assert results[0]["success"] == False assert "Failed to get authenticity verification URL" in results[0]["error"] -def test_save_certificate_saves_to_temp_directory(certified_builder, mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_save_certificate_saves_to_temp_directory( + certified_builder, + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Testa que o certificado é salvo no diretório temporário.""" mock_participant.authenticity_verification_url = "https://example.com/verify" - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - certificate = certified_builder.generate_certificate(mock_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + certificate = certified_builder.generate_certificate( + mock_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) file_path = certified_builder.save_certificate(certificate, mock_participant) - + assert file_path.startswith("/tmp/certificates/") assert file_path.endswith(".png") - + import os + assert os.path.exists(file_path), "Arquivo do certificado não foi criado" -def test_generate_certificate_with_long_name(certified_builder, mock_certificate, mock_certificate_template, mock_logo, mock_logo_tech_floripa): +def test_generate_certificate_with_long_name( + certified_builder, + mock_certificate, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, +): """Testa geração de certificado com nome muito longo.""" mock_event = Event( order_id=1, @@ -401,9 +628,9 @@ def test_generate_certificate_with_long_name(certified_builder, mock_certificate date=datetime.now(), time_checkin=datetime.now(), checkin_latitude=0.0, - checkin_longitude=0.0 + checkin_longitude=0.0, ) - + long_name_participant = Participant( first_name="João Pedro", last_name="da Silva Santos Oliveira", @@ -411,13 +638,21 @@ def test_generate_certificate_with_long_name(certified_builder, mock_certificate phone="(48) 99999-9999", cpf="123.456.789-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) long_name_participant.authenticity_verification_url = "https://example.com/verify" - - with patch('certified_builder.utils.fetch_file_certificate.fetch_file_certificate', side_effect=[mock_certificate_template, mock_logo]): - result = certified_builder.generate_certificate(long_name_participant, mock_certificate_template, mock_logo, mock_logo_tech_floripa) - + + with patch( + "certified_builder.utils.fetch_file_certificate.fetch_file_certificate", + side_effect=[mock_certificate_template, mock_logo], + ): + result = certified_builder.generate_certificate( + long_name_participant, + mock_certificate_template, + mock_logo, + mock_logo_tech_floripa, + ) + assert isinstance(result, Image.Image) assert result.size == mock_certificate_template.size @@ -426,7 +661,7 @@ def test_ensure_valid_rgba_converts_rgb_image(certified_builder): """Testa que _ensure_valid_rgba converte imagens RGB para RGBA.""" rgb_image = Image.new("RGB", (100, 100), (255, 0, 0)) rgba_image = certified_builder._ensure_valid_rgba(rgb_image) - + assert rgba_image.mode == "RGBA" assert rgba_image.size == rgb_image.size @@ -435,7 +670,6 @@ def test_ensure_valid_rgba_preserves_rgba_image(certified_builder): """Testa que _ensure_valid_rgba preserva imagens já em RGBA.""" rgba_image = Image.new("RGBA", (100, 100), (255, 0, 0, 128)) result = certified_builder._ensure_valid_rgba(rgba_image) - + assert result.mode == "RGBA" assert result.size == rgba_image.size - diff --git a/tests/test_make_qrcode.py b/tests/test_make_qrcode.py index 2ea7efa..023ad28 100644 --- a/tests/test_make_qrcode.py +++ b/tests/test_make_qrcode.py @@ -36,5 +36,3 @@ def test_qr_has_transparent_background_and_visible_foreground(mock_logo_tech_flo # Deve haver pixels transparentes (fundo) e opacos (padrão do QR) assert zeros > 0, "Esperava-se pixels transparentes no QR" assert nonzeros > 0, "Esperava-se pixels opacos no QR" - - diff --git a/tests/test_participant.py b/tests/test_participant.py index 0cc7e72..2c9bd34 100644 --- a/tests/test_participant.py +++ b/tests/test_participant.py @@ -5,14 +5,16 @@ from datetime import datetime from pydantic import ValidationError + @pytest.fixture def mock_certificate(): return Certificate( details="In recognition of their participation in the 84st edition of the Python Floripa Community Meeting, held on March 29, 2025, in Florianópolis, Brazil.", logo="https://tech.floripa.br/wp-content/uploads/2025/03/84o-Python-Floripa-e1741729144453.png", - background="https://tech.floripa.br/wp-content/uploads/2025/03/Background.png" + background="https://tech.floripa.br/wp-content/uploads/2025/03/Background.png", ) + @pytest.fixture def mock_event(): return Event( @@ -22,9 +24,10 @@ def mock_event(): date=datetime.strptime("2025-03-26 20:55:25", "%Y-%m-%d %H:%M:%S"), time_checkin=datetime.strptime("2025-03-26 20:55:44", "%Y-%m-%d %H:%M:%S"), checkin_latitude=-27.5460492, - checkin_longitude=-48.6227075 + checkin_longitude=-48.6227075, ) + def test_participant_initialization(mock_certificate, mock_event): participant = Participant( first_name="Jardel", @@ -33,7 +36,7 @@ def test_participant_initialization(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) assert participant.first_name == "Jardel" @@ -45,6 +48,7 @@ def test_participant_initialization(mock_certificate, mock_event): assert participant.event == mock_event assert len(participant.validation_code) == 9 + def test_name_completed(mock_certificate, mock_event): participant = Participant( first_name="Jardel", @@ -53,10 +57,11 @@ def test_name_completed(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) assert participant.name_completed() == "Jardel Godinho" + def test_formated_validation_code(mock_certificate, mock_event): participant = Participant( first_name="Jardel", @@ -65,12 +70,13 @@ def test_formated_validation_code(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) formatted_code = participant.formated_validation_code() assert len(formatted_code) == 11 # XXX-XXX-XXX - assert formatted_code[3] == '-' - assert formatted_code[7] == '-' + assert formatted_code[3] == "-" + assert formatted_code[7] == "-" + def test_create_name_certificate(mock_certificate, mock_event): participant = Participant( @@ -80,7 +86,7 @@ def test_create_name_certificate(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) name_certificate = participant.create_name_certificate() assert name_certificate.endswith(".png") @@ -88,6 +94,7 @@ def test_create_name_certificate(mock_certificate, mock_event): assert "Evento_de_Teste" in name_certificate assert participant.formated_validation_code() in name_certificate + def test_invalid_email(mock_certificate, mock_event): with pytest.raises(ValidationError) as exc_info: Participant( @@ -97,10 +104,11 @@ def test_invalid_email(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) assert "value is not a valid email address" in str(exc_info.value) + def test_missing_required_field(mock_certificate, mock_event): with pytest.raises(ValidationError): Participant( @@ -109,9 +117,10 @@ def test_missing_required_field(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) + def test_name_completed_with_multiple_names(mock_certificate, mock_event): participant = Participant( first_name="Jardel Silva", @@ -120,12 +129,13 @@ def test_name_completed_with_multiple_names(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) completed_name = participant.name_completed() assert len(completed_name.split()) == 3 assert completed_name == "Jardel Silva Santos" + def test_sanitize_filename_special_characters(mock_certificate): """Testa se a função de sanitização remove adequadamente caracteres especiais""" # Cria um evento com nome contendo caracteres especiais @@ -136,9 +146,9 @@ def test_sanitize_filename_special_characters(mock_certificate): date=datetime.strptime("2025-03-26 20:55:25", "%Y-%m-%d %H:%M:%S"), time_checkin=datetime.strptime("2025-03-26 20:55:44", "%Y-%m-%d %H:%M:%S"), checkin_latitude=-27.5460492, - checkin_longitude=-48.6227075 + checkin_longitude=-48.6227075, ) - + participant = Participant( first_name="Rodrigo", last_name="Farah", @@ -146,18 +156,21 @@ def test_sanitize_filename_special_characters(mock_certificate): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=event_with_special_chars + event=event_with_special_chars, ) - + # Testa a função de sanitização diretamente - sanitized = participant._sanitize_filename("87º Python Floripa × CODECON @ UNICESUSC") - + sanitized = participant._sanitize_filename( + "87º Python Floripa × CODECON @ UNICESUSC" + ) + # Verifica se caracteres especiais foram sanitizados - assert 'º' not in sanitized - assert '×' not in sanitized - assert '@' not in sanitized + assert "º" not in sanitized + assert "×" not in sanitized + assert "@" not in sanitized assert sanitized == "87o_Python_Floripa_x_CODECON_at_UNICESUSC" + def test_create_name_certificate_with_special_characters(mock_certificate): """Testa se o nome do certificado é gerado corretamente com caracteres especiais""" # Cria um evento com nome contendo caracteres especiais (igual ao exemplo do usuário) @@ -168,9 +181,9 @@ def test_create_name_certificate_with_special_characters(mock_certificate): date=datetime.strptime("2025-06-19 11:15:31", "%Y-%m-%d %H:%M:%S"), time_checkin=datetime.strptime("2025-06-19 11:15:31", "%Y-%m-%d %H:%M:%S"), checkin_latitude=-27.5460492, - checkin_longitude=-48.6227075 + checkin_longitude=-48.6227075, ) - + participant = Participant( first_name="Rodrigo", last_name="Farah", @@ -178,25 +191,29 @@ def test_create_name_certificate_with_special_characters(mock_certificate): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=event_with_special_chars + event=event_with_special_chars, ) - + name_certificate = participant.create_name_certificate() - + # Verifica se o nome do certificado não contém caracteres especiais problemáticos - assert 'º' not in name_certificate - assert '×' not in name_certificate - assert '@' not in name_certificate + assert "º" not in name_certificate + assert "×" not in name_certificate + assert "@" not in name_certificate assert name_certificate.endswith(".png") - + # Verifica se contém elementos esperados (sanitizados) assert "Rodrigo_Farah" in name_certificate assert "87o_Python_Floripa_x_CODECON_at_UNICESUSC" in name_certificate - + # Verifica se o nome é válido para URL (não contém caracteres que precisam ser encoded) import re + # Permite apenas caracteres alfanuméricos, hífens, underscores e pontos - assert re.match(r'^[\w\-_.]+$', name_certificate), f"Nome do certificado contém caracteres inválidos: {name_certificate}" + assert re.match( + r"^[\w\-_.]+$", name_certificate + ), f"Nome do certificado contém caracteres inválidos: {name_certificate}" + def test_sanitize_filename_edge_cases(mock_certificate, mock_event): """Testa casos extremos da função de sanitização""" @@ -207,9 +224,9 @@ def test_sanitize_filename_edge_cases(mock_certificate, mock_event): phone="(48) 98866-7447", cpf="000.000.000-00", certificate=mock_certificate, - event=mock_event + event=mock_event, ) - + # Testa strings com múltiplos caracteres especiais test_cases = { "": "", # String vazia @@ -221,9 +238,9 @@ def test_sanitize_filename_edge_cases(mock_certificate, mock_event): "C++ Programming": "C_plus_plus_Programming", # Símbolos de programação "file/path\\name": "file_slash_path_backslash_name", # Separadores de caminho } - + for input_text, expected_output in test_cases.items(): result = participant._sanitize_filename(input_text) - assert result == expected_output, f"Para '{input_text}', esperado '{expected_output}', obtido '{result}'" - - \ No newline at end of file + assert ( + result == expected_output + ), f"Para '{input_text}', esperado '{expected_output}', obtido '{result}'" From 63a4c635941b006872b1addb880b6b7a065b5f54 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Sat, 6 Dec 2025 12:11:26 -0300 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20ajustar=20formata=C3=A7=C3=A3o?= =?UTF-8?q?=20e=20espa=C3=A7amento=20no=20arquivo=20de=20configura=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.py b/config.py index 1d46f3b..428369f 100644 --- a/config.py +++ b/config.py @@ -3,19 +3,19 @@ logger = logging.getLogger(__name__) + class Config(BaseSettings): REGION: str - BUCKET_NAME: str + BUCKET_NAME: str QUEUE_URL: str SERVICE_URL_REGISTRATION_API_SOLANA: str SERVICE_API_KEY_REGISTRATION_API_SOLANA: str TECH_FLORIPA_CERTIFICATE_VALIDATE_URL: str TECH_FLORIPA_LOGO_URL: str + class Config: env_file = ".env" - env_file_encoding = "utf-8" + env_file_encoding = "utf-8" config = Config() - -