Construindo uma API em Python com: Flask, Decorators e Pytest para validação de cartão de crédito

Construindo uma API em Python com: Flask, Decorators e Pytest para validação de cartão de crédito

Recentemente precisei desenvolver uma API que realizava transações de cartões de crédito passando por diversas validações, como por exemplo, se o valor solicitado pelo vendedor no momento da compra é maior que o limite existente no cartão ou ainda, se o cartão está ou não bloqueado.

Levando esses pontos em consideração, temos que efetuar pelo menos 3 testes:

  • Verificar se o valor da conta ultrapassa o limite disponível;
  • Verificar se o cartão está ativo;
  • Verificar se a transação foi aprovada e se as verificações acima retornaram em falso.

O JSON que será recebido pela nossa API será conforme abaixo:

{
  "status": true,
  "number":123456,
  "limit":1000,
  "transaction":{
     "amount":500
   }
}

Então vamos ao código:

Primeira coisa é instalar as dependências:

python3 -m pip install pytest flask

Agora vamos escrever os testes, para isso vou utilizar uma ferramenta chamada Pytest.

Arquivo: test_app.py

#!/usr/bin/python3

import os
import tempfile

import pytest

from app import app

@pytest.fixture
def client():
    app.config['TESTING'] = True
    client = app.test_client()

    yield client

def test_valid_transaction(client):
    card = {
            "status": True,
            "number":123456,
            "limit":1000,
            "transaction":{
                "amount":500
            }
        }
    rv = client.post("/api/transaction",json=card)    
    assert  True == rv.get_json().get("aprovado")    
    assert  500 == rv.get_json().get("novoLimite")

def test_above_limit(client):
    card = {
            "status": True,
            "number":123456,
            "limit":1000,
            "transaction":{
                "amount":1500
            }
        }
    rv = client.post("/api/transaction",json=card)    
    assert  False == rv.get_json().get("aprovado")
    assert  "Compra acima do limite" in rv.get_json().get("motivo")

def test_blocked_card(client):
    card = {
            "status": False,
            "number":123456,
            "limit":1000,
            "transaction":{
                "amount":500
            }
        }
    rv = client.post("/api/transaction",json=card)    
    assert  False == rv.get_json().get("aprovado")
    assert  "Cartao bloqueado" in rv.get_json().get("motivo")

Neste momento, vamos criar um arquivo chamado app.py que será a nossa API de fato, no entanto esse arquivo ainda não está completo e servirá apenas para ver se os testes estão funcionando.

#!/usr/bin/python3

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/api/transaction",methods=["POST"])
def transacao():
    response = {"aprovado":True,"novoLimite":10}
    return jsonify(response)

if __name__ == '__main__':
    app.run(debug=True)

Para executar os testes execute o seguinte comando:

pytest

Obviamente todos os testes vão falhar, porém nosso objetivo é fazer eles darem certo.

Com os testes falhando a saída será parecida com essa:

============================================== test session starts ==============================================
platform linux -- Python 3.6.5, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: /home/alisson, inifile:
collected 3 items

blog/test_app.py FFF                                                                                      [100%]

=================================================== FAILURES ====================================================
____________________________________________ test_valid_transaction _____________________________________________
client = <FlaskClient >
    def test_valid_transaction(client):        card = {
                "status": True,
                "number":123456,
                "limit":1000,
                "transaction":{
                    "amount":500
                }
            }
        rv = client.post("/api/transaction",json=card)
        assert  True == rv.get_json().get("aprovado")
>       assert  500 == rv.get_json().get("novoLimite")
E       AssertionError: assert 500 == 10
E        +  where 10 = ('novoLimite')
E        +    where  = {'aprovado': True, 'novoLimite': 10}.get
E        +      where {'aprovado': True, 'novoLimite': 10} = <bound method JSONMixin.get_json of >()
E        +        where <bound method JSONMixin.get_json of > = .get_json

blog/test_app.py:28: AssertionError
_______________________________________________ test_above_limit ________________________________________________

client = <FlaskClient >

    def test_above_limit(client):
        card = {
                "status": True,
                "number":123456,
                "limit":1000,
                "transaction":{
                    "amount":1500
                }
            }
        rv = client.post("/api/transaction",json=card)
>       assert  False == rv.get_json().get("approved")
E       AssertionError: assert False == None
E        +  where None = ('approved')
E        +    where  = {'aprovado': True, 'novoLimite': 10}.get
E        +      where {'aprovado': True, 'novoLimite': 10} = <bound method JSONMixin.get_json of >()
E        +        where <bound method JSONMixin.get_json of > = .get_json

blog/test_app.py:40: AssertionError
_______________________________________________ test_blocked_card _______________________________________________

client = <FlaskClient >

    def test_blocked_card(client):
        card = {
                "status": False,
                "number":123456,
                "limit":1000,
                "transaction":{
                    "amount":500
                }
            }
        rv = client.post("/api/transaction",json=card)
>       assert  False == rv.get_json().get("approved")
E       AssertionError: assert False == None
E        +  where None = ('approved')
E        +    where  = {'aprovado': True, 'novoLimite': 10}.get
E        +      where {'aprovado': True, 'novoLimite': 10} = <bound method JSONMixin.get_json of >()
E        +        where <bound method JSONMixin.get_json of > = .get_json

blog/test_app.py:53: AssertionError
=========================================== 3 failed in 3.06 seconds ============================================

Note que falharam no total 3 testes:

  • test_valid_transaction
>       assert  500 == rv.get_json().get("novoLimite")
E       AssertionError: assert 500 == 10

Nesse primeiro teste, era esperado que o novo limite do cartão fosse 500 e foi retornado 10.

  • test_above_limit
>       assert  False == rv.get_json().get("aprovado")
E       AssertionError: assert False == None

Nesse segundo teste, era esperado que o valor de aprovado fosse igual a False

  • test_blocked_card
>       assert  False == rv.get_json().get("aprovado")
E       AssertionError: assert False == None

Nesse último teste, também era esperado que o valor de aprovado fosse igual a False, pois as transações não podem ser permitidas.

Vamos agora para aplicação principal.

Para fazer a validação das transações vou criar um decorator chamado checar_cartao, ele ficará da seguinte forma:

def checar_cartao(f):
    wraps(f)
    def validacoes(*args, **kwargs):
        dados = request.get_json()
        if not dados.get("status"):
            response = {"aprovado":False,
            "novoLimite":dados.get("limit"),
            "motivo":"Cartao bloqueado"}
            return jsonify(response)

        if dados.get("limit") < dados.get("transaction").get("amount"):
            response = {"aprovado":False,
            "novoLimite":dados.get("limit"),
            "motivo":"Compra acima do limite"}
            return jsonify(response)
        return f(*args, **kwargs)

    return(validacoes)

Vamos chamá-lo antes da requisição ser respondida:

@app.route("/api/transaction",methods=["POST"])
@checar_cartao
def transacao():
    // codigo da funcao

Agora explicando o código acima.

O Decorator em Python é uma função que retorna uma função, então qual a lógica desse decorator?

Ao invés de fazer uma série de IFs dentro do código principal da função da API, antes mesmo de uma requisição chegar, ela é enviada para o decorator – que tem a função validações – onde será verificado o limite do cartão de crédito e o seu status, caso as condições sejam verdadeiras é retornada uma função jsonify que devolve a transação como negada.

Caso todas as condições sejam falsas, no final temos o return f(*args, **kwargs), que devolve a função original, neste caso a função transação e o fluxo do programa segue normalmente.

Assim, o código do app.py ficou da seguinte maneira:

#!/usr/bin/python3

from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

def checar_cartao(f):
    wraps(f)
    def validacoes(*args, **kwargs):
        dados = request.get_json()
        if not dados.get("status"):
            response = {"aprovado":False,
            "novoLimite":dados.get("limit"),
            "motivo":"Cartao bloqueado"}
            return jsonify(response)

        if dados.get("limit") < dados.get("transaction").get("amount"):
            response = {"aprovado":False,
            "novoLimite":dados.get("limit"),
            "motivo":"Compra acima do limite"}
            return jsonify(response)
        return f(*args, **kwargs)

    return(validacoes)

@app.route("/api/transaction",methods=["POST"])
@checar_cartao
def transacao():
    card = request.get_json()   
    novo_limite = card.get("limit") - card.get("transaction").get("amount")
    response = {"aprovado":True,"novoLimite":novo_limite}
    return jsonify(response)

if __name__ == '__main__':
    app.run(debug=True)

Faça as alterações no seu código e rode os testes novamente, a saída agora deve ser parecida com a saída abaixo:

============================================== test session starts ==============================================
platform linux -- Python 3.6.5, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: /home/alisson/blog, inifile:
collected 3 items

test_app.py ...                                                                                           [100%]

=========================================== 3 passed in 0.14 seconds ============================================

Isso significa que todos os testes passaram.

É nóis valeu!

CURSOSCONSULTORIA CONTATO

Anterior 4Linux palestra na 4ª edição da CAOS, abordando DevSecOps - segurança em DevOps.
Próxima Replicação assíncrona em PostgreSQL 9.6

About author

Alisson Machado
Alisson Machado 19 posts

Alisson Menezes, atua como Gerente de T.I, 9 anos de experiência em projetos FOSS (Free and Open Source Software) e Python. Formação em Análise de Sistemas pela FMU e cursando MBA em BigData pela FIA, possui certificações LPI1, LPI2 e SUSE CLA, LPI DevOps e Exim - DevOps Professional. Autor dos cursos Python Fundamentals, Python for Sysadmins, MongoDB for Developers/DBAs, DevSecOps, Co-Autor do Infraestrutura Ágil e Docker da 4Linux e palestrantes em eventos como FISL, TDC e Python Brasil. É entusiasta das mais diversas áreas em T.I como Segurança, Bancos de dados NoSQL, DataScience mas tem como foco DevOps e Automação.

View all posts by this author →

Você pode gostar também

Desenvolvimento

Torne-se um Desenvolvedor Front-end com o Curso de HTML5 e CSS3 Online

Você já olhou para algum site e se perguntou como toda aquela estrutura funciona? Já parou para pensar como tudo é definido em um site, e quis saber como é

Destaques

PHP, como aprender? Por onde começar?

No ano passado, em 2017, não foram poucas as vezes em que fui abordada por pessoas, em sua maioria mulheres, que me perguntaram: Como faço pra aprender PHP? Em início

Desenvolvimento

Socket em Python

Sockets são usados para enviar dados através da rede, um exemplo seria enviar um arquivo pelo Rocket.chat /  Skype / Whats App, ou ainda podemos considerar até mesmo as próprias