No lançamento do pix os aplicativos estavam ignorando esse campo e até mesmo 99 | * os BRCodes gerados em aplicativos de alguns bancos não apresentavam esse campo. 100 | * Porém, recentemente identificou-se que algumas instituições já não estão processando os pix na ausência 101 | * desse campo. Ver https://github.com/renatomb/php_qrcode_pix/issues/2
102 | * 103 | *O conteúdo desse campo é gerado pelo recebedor do pix. Devendo ser um valor único para cada transação, ou *** 104 | * quando não for usado, pois esse passa a ser gerado automaticamente. Dada a necessidade de identificador único, 105 | * caso haja a opção pelo uso do mesmo, recomendo a utilização de um UUID vinculado ao sistema do recebedor, 106 | * o que permitirá a conciliação dos pagamentos que foram recebidos.
107 | * 108 | *Entretanto, conforme discutido na issue https://github.com/bacen/pix-api/issues/214, o Banco Itaú bloqueia 109 | * qualquer código de transação que não tenha sido gerado previamente no aplicativo da instituição. 110 | * Desta forma, é necessário solicitar ao gerente da conta a liberação para que a conta do recebedor possa 111 | * gerar qrcode do pix fora do aplicativo do banco. 112 | * É possível que outras instituições passem a adotar esse posicionamento no futuro.
113 | * 114 | *Com o uso de QR Code dinâmicos pode-se inclusive definir um WebHook onde o cliente final seja notificado 115 | * automaticamente quando determinada transação for recebida. Consulte os detalhes da API da sua instituição.
116 | * @see QRCodePix#QRCodePix(DadosEnvioPix) 117 | * @throws IllegalArgumentException quando o ID da transação é inválido 118 | */ 119 | public QRCodePix(final DadosEnvioPix dadosPix, final String idTransacao) { 120 | if(idTransacao.length() > 25) { 121 | final var msg = "idTransacao deve ter no máximo 25 caracteres. Valor %s tem %d caracteres.".formatted(idTransacao, idTransacao.length()); 122 | throw new IllegalArgumentException(msg); 123 | } 124 | 125 | this.idTransacao = idTransacao; 126 | this.dadosPix = dadosPix; 127 | } 128 | 129 | /** 130 | * {@return um nome de arquivo PNG temporário} que pode ser usado para 131 | * {@link #save(Path) salvar} a imagem do QRCode {@link #generate() gerado}. 132 | * @throws UncheckedIOException se não for possível gerar um nome de arquivo temporário 133 | */ 134 | static Path tempImgFilePath() { 135 | try { 136 | return Path.of(File.createTempFile("qrcode-pix-", ".png").getAbsoluteFile().getPath()); 137 | } catch (IOException e) { 138 | throw new UncheckedIOException(e); 139 | } 140 | } 141 | 142 | /** 143 | * Cria um objeto JSON contendo os dados completos para gerar o QRCode. 144 | * 145 | * @return o objeto JSON criado 146 | * @see #generate() 147 | */ 148 | private JSONObject newJSONObject() { 149 | final var jsonTemplate = 150 | """ 151 | { 152 | '00': '%s', 153 | '26': { 154 | '00': '%s', 155 | '01': '%s', 156 | '02': '%s' 157 | }, 158 | '52': '%s', 159 | '53': '%s', 160 | '%s': '%s', 161 | '58': '%s', 162 | '59': '%s', 163 | '60': '%s', 164 | '62': { 165 | '05': '%s' 166 | } 167 | } 168 | """; 169 | 170 | final var json = 171 | jsonTemplate 172 | .formatted( 173 | PFI, ARRANJO_PAGAMENTO, dadosPix.chaveDestinatario(), dadosPix.descricao(), 174 | MCC, COD_MOEDA, COD_CAMPO_VALOR, dadosPix.valorStr(), COD_PAIS, 175 | dadosPix.nomeDestinatario(), dadosPix.cidadeRemetente(), idTransacao); 176 | return new JSONObject(json); 177 | } 178 | 179 | /** 180 | * Gera o QRCode PIX "Copia e Cola" para os dados informados. 181 | * @return o código gerado 182 | * @see #save(Path) 183 | * @see #toString() 184 | */ 185 | public String generate() { 186 | final String partialCode = generateInternal(newJSONObject()) + COD_CRC; 187 | final String checksum = crcChecksum(partialCode); 188 | return setCode(partialCode + checksum); 189 | } 190 | 191 | /** 192 | * Armazena o último QRCode gerado. 193 | * @param code QR Code gerado 194 | * @return 195 | */ 196 | private String setCode(final String code) { 197 | this.code = code; 198 | return code; 199 | } 200 | 201 | private String generateInternal(final JSONObject jsonObj) { 202 | final var sb = new StringBuilder(); 203 | jsonObj.keySet().stream().sorted().forEach(key -> { 204 | final Object val = jsonObj.get(key); 205 | final String str = encodeValue(key, val); 206 | sb.append(leftPad(key)).append(strLenLeftPadded(str)).append(str); 207 | }); 208 | 209 | return sb.toString(); 210 | } 211 | 212 | /** 213 | * Codifica o valor de uma chave do objeto JSON com configurações 214 | * para geração do QRCode, conforme as especificações do PIX. 215 | * @param key nome da chave no objeto JSON contendo parte dos dados 216 | * @param val valor para a chave correspondente no objeto JSON 217 | * @return o valor da chave codificado 218 | */ 219 | private String encodeValue(final String key, final Object val) { 220 | //Se o valor para a chave contém outro objeto, processa seus atributos recursivamente 221 | if(val instanceof JSONObject jsonObjValue) 222 | return generateInternal(jsonObjValue); 223 | 224 | //Se o valor é String ou um tipo primitivo 225 | return key.equals(COD_CAMPO_VALOR) ? val.toString() : removeSpecialChars(val); 226 | } 227 | 228 | /** 229 | * Calcula o checksum CRC16 a partir de um código parcial do PIX. 230 | * @param partialCode código parcial do QRCode 231 | * @return o checksum em hexadecimal 232 | */ 233 | private String crcChecksum(final String partialCode){ 234 | int crc = 0xFFFF; 235 | final var byteArray = partialCode.getBytes(); 236 | for (final byte b : byteArray) { 237 | crc ^= b << 8; 238 | for (int i = 0; i < 8; i++) { 239 | if ((crc & 0x8000) == 0) 240 | crc = crc << 1; 241 | else crc = (crc << 1) ^ 0x1021; 242 | } 243 | } 244 | 245 | final int decimal = crc & 0xFFFF; 246 | return leftPad(toHexString(decimal), 4).toUpperCase(); 247 | } 248 | 249 | private String removeSpecialChars(final Object value) { 250 | return value.toString().replaceAll("[^a-zA-Z0-9\\-@\\.\\*\\s]", ""); 251 | } 252 | 253 | /** 254 | * Obtém o total de caracteres de uma String incluindo zero a esquerda se necessário. 255 | * @return o total como uma String de dois dígitos (incluindo zero à esquerda se necessário). 256 | * @throws IllegalArgumentException se a quantidade de caracteres do valor é maior que o permitido 257 | */ 258 | static String strLenLeftPadded(final String value) { 259 | if (value.length() > 99) { 260 | final var msg = "Tamanho máximo dos valores dos campos deve ser 99. '%s' tem %d caracteres.".formatted(value, value.length()); 261 | throw new IllegalArgumentException(msg); 262 | } 263 | 264 | final String len = String.valueOf(value.length()); 265 | return leftPad(len); 266 | } 267 | 268 | /** 269 | * Inclui zero à esquerda de um código de um campo do QRCode PIX (se necessário), 270 | * pois todos os códigos devem ter 2 dígitos. 271 | * @param code código de um campo do QRCode PIX 272 | * @return o código com um possível zero à esquerda 273 | */ 274 | private static String leftPad(final String code) { 275 | return leftPad(code, 2); 276 | } 277 | 278 | /** 279 | * Inclui uma determinada quantidade de zeros à esquerda de um valor. 280 | * @param code código pra incluir zeros à esquerda 281 | * @param len tamanho máximo da String retornada 282 | * @return o código com possíveis zeros à esquerda 283 | */ 284 | private static String leftPad(final String code, final int len) { 285 | final var format = "%1$" + len + "s"; 286 | return format.formatted(code).replace(' ', '0'); 287 | } 288 | 289 | /** 290 | * Salva o QRCode gerado com {@link #generate()} 291 | * em um arquivo de imagem. 292 | * Se o código não foi gerado ainda, chama automaticamente o {@link #generate()}. 293 | * @param imagePath caminho para o arquivo de imagem a ser gerado 294 | * @see #save() 295 | * @see #saveAndGetBytes(Path) 296 | */ 297 | public void save(final Path imagePath) { 298 | saveAndGetBytes(imagePath); 299 | } 300 | 301 | /** 302 | * Salva o QRCode gerado com {@link #generate()} em um arquivo de imagem temporário com nome aleatório. 303 | * Se o código não foi gerado ainda, chama automaticamente o {@link #generate()}. 304 | * @see #save(Path) 305 | * @return o caminho do arquivo gerado 306 | * @see #saveAndGetBytes(Path) 307 | */ 308 | public Path save() { 309 | final Path imagePath = tempImgFilePath(); 310 | saveAndGetBytes(imagePath); 311 | return imagePath; 312 | } 313 | 314 | /** 315 | * Salva o QRCode gerado com {@link #generate()} em um arquivo de imagem. 316 | * Se o código não foi gerado ainda, chama automaticamente o {@link #generate()}. 317 | * @param imagePath caminho para o arquivo de imagem a ser gerado 318 | * @return um vetor de bytes representando a imagem gerada 319 | * @throws IOException se não for possível acessar o arquivo para gravação 320 | * @throws WriterException se ocorrer erro durante a gravação de dados no arquivo 321 | * @see #save(Path) 322 | * @see #save() 323 | */ 324 | public byte[] saveAndGetBytes(final Path imagePath) { 325 | //Obtém a extensão do arquivo 326 | final var fileFormat = FilenameUtils.getExtension(imagePath.toString()); 327 | if(fileFormat.isEmpty()) 328 | throw new IllegalArgumentException("Nome do arquivo deve conter a extensão para indicar o formato da imagem"); 329 | 330 | final var hintsMap = new EnumMap<>(EncodeHintType.class); 331 | hintsMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); 332 | hintsMap.put(EncodeHintType.CHARACTER_SET, "UTF-8"); 333 | final int tamanho = 300; // Tamanho da imagem do QRCode em pixels 334 | 335 | final var writer = new QRCodeWriter(); 336 | try { 337 | if(code == null || code.isBlank()) 338 | generate(); 339 | 340 | final var bitMatrix = writer.encode(code, BarcodeFormat.QR_CODE, tamanho, tamanho, hintsMap); 341 | final var image = new BufferedImage(tamanho, tamanho, BufferedImage.TYPE_INT_RGB); 342 | for (int y = 0; y < tamanho; y++) { 343 | for (int x = 0; x < tamanho; x++) { 344 | final var isBlack = bitMatrix.get(x, y); 345 | final int color = isBlack ? 0 : 0xFFFFFF; //black or white 346 | image.setRGB(x, y, color); 347 | } 348 | } 349 | 350 | final var baos = new ByteArrayOutputStream(); 351 | ImageIO.write(image, fileFormat, baos); 352 | final var byteArray = baos.toByteArray(); 353 | try(final var fos = new FileOutputStream(imagePath.toFile())) { 354 | fos.write(byteArray); 355 | } 356 | 357 | return byteArray; 358 | } catch (IOException | WriterException e) { 359 | throw new RuntimeException(e); 360 | } 361 | } 362 | 363 | /** 364 | * {@return o último QRCode gerado.} 365 | * @see #generate() 366 | */ 367 | @Override 368 | public String toString() { 369 | return code; 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/test/java/br/com/competeaqui/pix/DadosEnvioPixCamposObrigatoriosTest.java: -------------------------------------------------------------------------------- 1 | package br.com.competeaqui.pix; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.math.BigDecimal; 6 | 7 | import static br.com.competeaqui.pix.DadosEnvioPixInvalidosTest.BLANK; 8 | import static br.com.competeaqui.pix.DadosEnvioPixInvalidosTest.EMPTY; 9 | import static br.com.competeaqui.pix.DadosEnvioPixValorTest.*; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | /** 13 | * Testes para verificar se os campos obrigatórios da classe {@link DadosEnvioPix} 14 | * estão sendo verificados. 15 | * @author Manoel Campos da Silva Filho 16 | */ 17 | class DadosEnvioPixCamposObrigatoriosTest { 18 | private static final BigDecimal V = BigDecimal.ONE; 19 | 20 | @Test 21 | void nomeEmpty() { 22 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(EMPTY, CD, V, CR)); 23 | } 24 | 25 | @Test 26 | void nomeBlank() { 27 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(BLANK, CD, V, CR)); 28 | } 29 | 30 | @Test 31 | void nomeNull() { 32 | assertThrows(NullPointerException.class, () -> new DadosEnvioPix(null, CD, V, CR)); 33 | } 34 | 35 | @Test 36 | void chaveEmpty() { 37 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, EMPTY, V, CR)); 38 | } 39 | 40 | @Test 41 | void chaveBlank() { 42 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, BLANK, V, CR)); 43 | } 44 | 45 | @Test 46 | void chaveNull() { 47 | assertThrows(NullPointerException.class, () -> new DadosEnvioPix(ND, null, V, CR)); 48 | } 49 | 50 | @Test 51 | void cidadeEmpty() { 52 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, CD, V, EMPTY)); 53 | } 54 | 55 | @Test 56 | void cidadeBlank() { 57 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, CD, V, BLANK)); 58 | } 59 | 60 | @Test 61 | void cidadeNull() { 62 | assertThrows(NullPointerException.class, () -> new DadosEnvioPix(ND, CD, V, null)); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/br/com/competeaqui/pix/DadosEnvioPixConstrutoresTest.java: -------------------------------------------------------------------------------- 1 | package br.com.competeaqui.pix; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.math.BigDecimal; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertSame; 10 | 11 | /** 12 | * Testes para os construtores de {@link DadosEnvioPix}. 13 | * @author Manoel Campos da Silva Filho 14 | */ 15 | class DadosEnvioPixConstrutoresTest { 16 | private static final String NOME_DESTINATARIO = "Manoel"; 17 | private static final String CHAVE_DESTINATARIO = "11111111111"; 18 | private static final BigDecimal VALOR = new BigDecimal(1); 19 | private static final String CIDADE_REMETENTE = "Palmas"; 20 | private static final String DESCRICAO = "PIX em Java"; 21 | private DadosEnvioPix instance; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | instance = new DadosEnvioPix( 26 | NOME_DESTINATARIO, CHAVE_DESTINATARIO, 27 | VALOR, CIDADE_REMETENTE, DESCRICAO); 28 | } 29 | 30 | @Test 31 | void valor() { 32 | assertEquals(new BigDecimal(1), instance.valor()); 33 | assertSame(VALOR, instance.valor()); 34 | } 35 | 36 | @Test 37 | void valorStr() { 38 | assertEquals("1.00", instance.valorStr()); 39 | } 40 | 41 | @Test 42 | void nomeDestinatario() { 43 | assertEquals(NOME_DESTINATARIO, instance.nomeDestinatario()); 44 | } 45 | 46 | @Test 47 | void chaveDestinatario() { 48 | assertEquals(CHAVE_DESTINATARIO, instance.chaveDestinatario()); 49 | } 50 | 51 | @Test 52 | void cidadeRemetente() { 53 | assertEquals(CIDADE_REMETENTE, instance.cidadeRemetente()); 54 | } 55 | 56 | @Test 57 | void descricao() { 58 | assertEquals(DESCRICAO, instance.descricao()); 59 | } 60 | 61 | @Test 62 | void descricaoVazia() { 63 | final var instance = new DadosEnvioPix( 64 | NOME_DESTINATARIO, CHAVE_DESTINATARIO, 65 | VALOR, CIDADE_REMETENTE); 66 | assertEquals("", instance.descricao()); 67 | } 68 | } -------------------------------------------------------------------------------- /src/test/java/br/com/competeaqui/pix/DadosEnvioPixInvalidosTest.java: -------------------------------------------------------------------------------- 1 | package br.com.competeaqui.pix; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.math.BigDecimal; 6 | 7 | import static br.com.competeaqui.pix.DadosEnvioPixValorTest.*; 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | /** 11 | * Testes de validação de parâmetros dos construtores de {@link DadosEnvioPix}. 12 | * 13 | *As constantes com nomes abreviados são irrelevantes para os testes. 14 | * Cada teste considera apenas o valor inválido passado por meio de uma variável 15 | * (estas constantes são valores válidos).
16 | * 17 | * @author Manoel Campos da Silva Filho 18 | */ 19 | class DadosEnvioPixInvalidosTest { 20 | static final String EMPTY = ""; 21 | static final String BLANK = " "; 22 | private static final BigDecimal V = new BigDecimal(1); 23 | 24 | /** Nome no limite do tamanho máximo. */ 25 | @Test 26 | void nomeDestinatarioNoLimite() { 27 | final var nomeInvalido = "a".repeat(25); 28 | assertDoesNotThrow(() -> new DadosEnvioPix(nomeInvalido, CD, V, CR)); 29 | } 30 | 31 | @Test 32 | void nomeDestinatarioMuitoGrande() { 33 | final var nomeInvalido = "a".repeat(26); 34 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(nomeInvalido, CD, V, CR)); 35 | } 36 | 37 | @Test 38 | void chaveMuitoGrande() { 39 | final var chaveInvalido = "a".repeat(78); 40 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, chaveInvalido, V, CR)); 41 | } 42 | 43 | /** Valor no limite do tamanho máximo. */ 44 | @Test 45 | void valorNoLimite() { 46 | final var valorInvalido = new BigDecimal("1234567890.00"); // 13 caracteres (com o ponto) 47 | assertDoesNotThrow(() -> new DadosEnvioPix(ND, CD, valorInvalido, CR)); 48 | } 49 | 50 | /** Quando o total de caracteres do valor (incluíndo o ponto) é maior do que o suportado. */ 51 | @Test 52 | void valorDoubleMuitoGrande() { 53 | final var valorInvalido = new BigDecimal("12345678901.00"); // 14 caracteres (com o ponto) 54 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, CD, valorInvalido, CR)); 55 | } 56 | 57 | /** Quando o total de caracteres do valor (incluíndo o ponto) é maior do que o suportado. */ 58 | @Test 59 | void valorIntMuitoGrande() { 60 | // 11 caracteres, mas neste caso será incluído .00 ficando com 14 (além do limite) 61 | final var valorInvalido = new BigDecimal("12345678901"); 62 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, CD, valorInvalido, CR)); 63 | } 64 | 65 | /** Cidade no limite do tamanho máximo. */ 66 | @Test 67 | void cidadeRemetenteNoLimite() { 68 | final var cidadeInvalida = "a".repeat(15); 69 | assertDoesNotThrow(() -> new DadosEnvioPix(ND, CD, V, cidadeInvalida)); 70 | } 71 | 72 | @Test 73 | void cidadeRemetenteMuitoGrande() { 74 | final var cidadeInvalida = "a".repeat(16); 75 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, CD, V, cidadeInvalida)); 76 | } 77 | 78 | @Test 79 | void descricaoBlankAlteradaPraEmpty() { 80 | final var instance = new DadosEnvioPix(ND, CD, V, CR, BLANK); 81 | assertTrue(instance.descricao().isEmpty()); 82 | } 83 | 84 | @Test 85 | void descricaoNull() { 86 | assertThrows(NullPointerException.class, () -> new DadosEnvioPix(ND, CD, V, CR, null)); 87 | } 88 | 89 | @Test 90 | void descricaoMuitoGrande() { 91 | final var descricaoInvalida = "a".repeat(73); 92 | assertThrows(IllegalArgumentException.class, () -> new DadosEnvioPix(ND, CD, V, CR, descricaoInvalida)); 93 | } 94 | } -------------------------------------------------------------------------------- /src/test/java/br/com/competeaqui/pix/DadosEnvioPixValorTest.java: -------------------------------------------------------------------------------- 1 | package br.com.competeaqui.pix; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.math.BigDecimal; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | 10 | /** 11 | * Testes para o campo {@link DadosEnvioPix#valor()}. 12 | * 13 | *As constantes com nomes abreviados são irrelevantes para os testes. 14 | * Cada teste considera apenas o valor inválido passado por meio de uma variável 15 | * (estas constantes são valores válidos).
16 | * 17 | * @author Manoel Campos da Silva Filho 18 | */ 19 | class DadosEnvioPixValorTest { 20 | /** Nome do Destinatário. */ 21 | static final String ND = "Manoel"; 22 | 23 | /** Chave PIX do Destinatário. */ 24 | static final String CD = "11111111111"; 25 | 26 | /** Cidade do Remetente. */ 27 | static final String CR = "Palmas"; 28 | 29 | @Test 30 | void valorZero() { 31 | assertThrows(IllegalArgumentException.class, () -> newInstance(BigDecimal.ZERO)); 32 | } 33 | 34 | @Test 35 | void valorNegativo() { 36 | assertThrows(IllegalArgumentException.class, () -> newInstance(new BigDecimal(-1))); 37 | } 38 | 39 | @Test 40 | void valorStr1Casa() { 41 | final var instance = newInstance(new BigDecimal("1.0")); 42 | assertEquals("1.00", instance.valorStr()); 43 | } 44 | 45 | @Test 46 | void valorStr2Casas() { 47 | final var instance = newInstance(new BigDecimal("1.00")); 48 | assertEquals("1.00", instance.valorStr()); 49 | } 50 | 51 | @Test 52 | void valorStr3Casas() { 53 | final var instance = newInstance(new BigDecimal("1.234")); 54 | assertEquals("1.23", instance.valorStr()); 55 | } 56 | 57 | @Test 58 | void valorStr3CasasZerosNoMeio() { 59 | final var instance = newInstance(new BigDecimal("1.001")); 60 | assertEquals("1.00", instance.valorStr()); 61 | } 62 | 63 | @Test 64 | void valorStr3CasasZeroFinal() { 65 | final var instance = newInstance(new BigDecimal("1.230")); 66 | assertEquals("1.23", instance.valorStr()); 67 | } 68 | 69 | /** Teste de arredondamento pra cima. */ 70 | @Test 71 | void valorStrMaisDe2CasasCeil() { 72 | final var instance = newInstance(new BigDecimal("1.229")); 73 | assertEquals("1.23", instance.valorStr()); 74 | } 75 | 76 | @Test 77 | void valorStrMaisDe2CasasZeros() { 78 | final var instance = newInstance(new BigDecimal("1.000")); 79 | assertEquals("1.00", instance.valorStr()); 80 | } 81 | 82 | private static DadosEnvioPix newInstance(final BigDecimal valor) { 83 | return new DadosEnvioPix(ND, CD, valor, CR); 84 | } 85 | } -------------------------------------------------------------------------------- /src/test/java/br/com/competeaqui/pix/QRCodePixTest.java: -------------------------------------------------------------------------------- 1 | package br.com.competeaqui.pix; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.TestInfo; 6 | import java.io.IOException; 7 | import java.math.BigDecimal; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | 12 | import static br.com.competeaqui.pix.QRCodePix.tempImgFilePath; 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | /** 16 | * Testes gerais para a classe {@link QRCodePix}. 17 | * @author Manoel Campos da Silva Filho 18 | */ 19 | class QRCodePixTest { 20 | /** 21 | * Caminho da imagem cujo conteúdo é esperado que seja igual ao da imagem gerada nos testes. 22 | */ 23 | private static final String QRCODE_FILENAME = "src/test/resources/qrcode-test.png"; 24 | 25 | private static final DadosEnvioPix DADOS = new DadosEnvioPix("Manoel", "11111111111", new BigDecimal("1.0"), "Palmas"); 26 | 27 | /** 28 | * QRCode que deve ser gerado para os {@link #DADOS} definidos anteriormente. 29 | */ 30 | private static final String QRCODE = "00020126370014BR.GOV.BCB.PIX011111111111111020052040000530398654041.005802BR5906Manoel6006Palmas62070503***630477F1"; 31 | 32 | private QRCodePix instance; 33 | 34 | /** 35 | * Cria uma instância de {@link QRCodePix} a partir de dados pré-definidos. 36 | *37 | * AVISO: Se estes dados forem alterados, o arquivo {@link #QRCODE_FILENAME} precisa ser atualizado. 38 | *
39 | */ 40 | @BeforeEach 41 | void setUp() { 42 | instance = new QRCodePix(DADOS); 43 | } 44 | 45 | /** 46 | * Ao chamar o método {@link QRCodePix#generate()}, 47 | * ele deve armazenar o resultado em um atributo retornado pelo toString(). 48 | */ 49 | @Test 50 | void toStringEmptyBeforeGenerate() { 51 | assertTrue(instance.toString().isEmpty()); 52 | instance.generate(); 53 | assertFalse(instance.toString().isEmpty()); 54 | } 55 | 56 | /** 57 | * Ao chamar o método {@link QRCodePix#save(Path)} antes do {@link QRCodePix#generate()}, 58 | * ele deve chamar o segundo, e então armazenar o resultado em um atributo retornado pelo toString(). 59 | */ 60 | @Test 61 | void toStringEmptyBeforeSave() { 62 | assertTrue(instance.toString().isEmpty()); 63 | instance.save(tempImgFilePath()); 64 | assertFalse(instance.toString().isEmpty()); 65 | } 66 | 67 | /** 68 | * Verifica se o QRCode foi gerado corretamente e se o toString tá retornando o mesmo 69 | * resultado de generate. 70 | */ 71 | @Test 72 | void generateAndToString() { 73 | assertEquals(QRCODE, instance.generate()); 74 | assertEquals(QRCODE, instance.toString()); 75 | } 76 | 77 | @Test 78 | void saveAndCheckFileContent(final TestInfo info) throws IOException { 79 | final var testName = getTestName(info); 80 | final Path caminhoImgGerada = Paths.get("target/test-classes/%s.png".formatted(testName)); 81 | System.out.printf("Gerando arquivo temporário com QRCode em %s%n", caminhoImgGerada); 82 | final byte[] bytesArqImgGerado = instance.saveAndGetBytes(caminhoImgGerada); 83 | 84 | final byte[] bytesArqImgEsperado = Files.readAllBytes(Paths.get(QRCODE_FILENAME)); 85 | assertArrayEquals(bytesArqImgEsperado, bytesArqImgGerado); 86 | } 87 | 88 | private static String getTestName(final TestInfo info) { 89 | return info.getTestMethod() 90 | .map(m -> m.getDeclaringClass().getSimpleName() + "." + m.getName()) 91 | .orElse(String.valueOf(System.currentTimeMillis())); 92 | } 93 | 94 | @Test 95 | void saveRandomFileCheckExists() { 96 | final Path caminhoImgGerada = instance.save(); 97 | System.out.printf("Gerado arquivo temporário com QRCode em %s%n", caminhoImgGerada); 98 | assertTrue(Files.exists(caminhoImgGerada)); 99 | } 100 | 101 | @Test 102 | void saveInvalidFile() { 103 | final Path invalidFileName = Path.of("\\///&&&.png"); 104 | final var exception = assertThrows(RuntimeException.class, () -> instance.save(invalidFileName)); 105 | assertInstanceOf(IOException.class, exception.getCause()); 106 | } 107 | 108 | @Test 109 | void saveFilenameWithoutExtension() { 110 | assertThrows(IllegalArgumentException.class, () -> instance.save(Path.of("nome-do-arquivo-sem-extensao"))); 111 | } 112 | 113 | @Test 114 | void constructorIdTransacaoMuitoGrande() { 115 | final var idInvalido = "i".repeat(26); 116 | assertThrows(IllegalArgumentException.class, () -> new QRCodePix(DADOS, idInvalido)); 117 | } 118 | 119 | @Test 120 | void strLenLeftPadded() { 121 | final var strInvalidLen = "a".repeat(100); 122 | assertThrows(IllegalArgumentException.class, () -> QRCodePix.strLenLeftPadded(strInvalidLen)); 123 | } 124 | } -------------------------------------------------------------------------------- /src/test/resources/qrcode-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/competeaqui/qrcode-pix-java/9cbaac918c9eae1240c5857353d722322b5fe69e/src/test/resources/qrcode-test.png --------------------------------------------------------------------------------