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

Infraestrutura

Instalação do Cuda 9 em distribuições Linux baseadas no Ubuntu

Se você deseja trabalhar com algoritmos de Machine Learning, provavelmente precisará usar processamento paralelo para acelerar os resultados dos seus algoritmos. Muitos frameworks como por exemplo, o TensorFlow, já possuem

DevOps

Automatização de Infraestrutura – DevOps e Python

Automatização de Infraestrutura – DevOps e Python Hoje em dia os profissionais de TI estão olhando cada vez mais para DevOps e Python. Se você quer saber por que a

Desenvolvimento

JSON e BSON no MongoDB: para iniciantes

Dando continuidade na série de MongoDB, nesse post farei uma Introdução ao formato “Javascript Object Notation” (JSON), ao BSON e aos primeiros passos com o MongoDB.