Utilizando FakeWeb + Cucumber para testar autenticação por Oauth no Twitter
Continuando a série de posts sobre testes, neste artigo será mostrada uma técnica para simular o retorno das requisições HTTP realizadas durante o processo de autenticação por Oauth. Será utilizado como exemplo uma autenticação ao Twitter, mas a técnica é a mesma para qualquer esquema de autenticação que utilize Oauth.
O que é Oauth?
Se você ainda não sabe o que é o Oauth, dê uma olhada aqui. De forma resumida, o Oauth é um modelo de autenticação que pode ser utilizado quando uma aplicação A (consumer) precisa acessar dados de seus usuários em uma aplicação B (provider). Um exemplo que vem se tornando bem conhecido nos últimos tempos é a autenticação de aplicações que acessam o Twitter para ler e alterar dados de seus usuários. O fluxo é mais ou menos assim:
- A aplicação A deve ser registrada na aplicação B. Ao final desse processo, será gerado um token e um secret, que serão utilizados para a criação de um consumer na aplicação A. A criação deste consumer é feita através deste par token/secret e da URL para a aplicação B.
- Na aplicação A, quando quisermos autenticar um usuário na aplicação B, criamos um request token, a partir do consumer do passo anterior. Este request token também possui um par token/secret, que deve ser guardado de alguma forma. Em aplicações web ele geralmente é guardado na sessão do usuário.
- O request token fornece também uma URL para autorização, para a qual o usuário da aplicação A deverá ser redirecionado. Essa URL pode também receber como argumento (é um request GET) uma URL de callback para alguma área da aplicação A ṕara o qual o usuário será retornado após ter se autenticado com sucesso na aplicação B.
- Ao ser enviado para a aplicação B, o usuário informa seus dados de autenticação e, se estes estiverem corretos, é enviado de volta para a aplicação A. Ao retornar o usuário, a aplicação B também devolve o par token/secret previamente enviado.
- Na aplicação A, o par token/secret recebido é comparado com os dados armazenados previamente na sessão do usuário. Se estes forem iguais, significa que o usuário foi corretamente autenticado na aplicação B. Isso também pode ser mecanismo de segurança.
- Com o par token/secret recebidos, pode-se criar um novo request token e, a partir deste, conseguimos gerar um access token. Com o access token é possível realizar chamadas diretas à API da aplicação B, lendo e alterando dados ali armazenados para o usuário recém autenticado.
Porque testar isso é difícil
O problema em realizar testes funcionais em um processo de autenticação como esse é que o fluxo de execução não fica contido somente na nossa aplicação. Uma vez que o usuário é redirecionado para a aplicação provider, perdemos o controle do que está sendo executado. Considerando-se que precisamos recuperar o fluxo de execução quando o usuário for redirecionado de volta para nossa aplicação, pode-se ter idéia do tamanho do problema.
Além disso, se nossos testes forem fortemente acoplados à aplicação provider e realmente percorrerem o ciclo completo para a autenticação, alguns problemas poderão ocorrer:
- Nossos testes serão lentos.
- Não será possível testar caso a aplicação provider esteja indisponível.
O que precisamos fazer é enganar nossa aplicação. Precisamos fazer com que ela acredite estar enviando requisições para a aplicação provider, sem que isso aconteça de verdade. A primeira idéia que vem à mente é:
Se podemos usar o Rspec junto ao Cucumber, vamos criar mocks e stubs para simular os requests!
Hum… péssima idéia meu amigo. O motivo é simples:
Testes funcionais devem realmente testar sua aplicação. De ponta a ponta. Se você usar mocks e stubs, isso não será muito verdade… O propósito de ferramentas como o Cucumber é testar as coisas como elas realmente são.
Chegamos então a um paradoxo: Não podemos usar mocks e stubs, mas também não temos como testar se nossa aplicação realizar requests reais ao provider. A solução é uma gem muito interessante chamada FakeWeb.
FakeWeb
De forma resumida, o que o FakeWeb faz é acabar com o Net::HTTP. Destruir tudo o que ele faz, matar sua capacidade de realizar requests. Para quem não sabe, toda vez que fazemos um request HTTP a partir de nossa aplicação Rails, quem cuida de tudo é o Net::HTTP.
Como a coisa vai funcionar então? O FakeWeb permite registrar URLS e respectivos retornos. Todo o request feito pelo Net::HTTP será interceptado e a resposta “falsa” será retornada. Nossa aplicação nunca saberá a verdade!
Como saber o que retornar a partir dos requests registrados com o FakeWeb?
Boa pergunta! E a resposta é bastante interessante! Na verdade eu não inventei isso, me baseei no que li no blog Technical Pickles, e funciona muito bem!
Tudo o que temos a fazer é usar o curl e realizar um request a cada URL que nossa aplicação normalmente tentaria acessar durante o processo que pretendemos testar, direcionando a saída para um arquivo.
curl -is http://some.url > output-file
Com essa técnica podemos fazer com que o FakeWeb retorne o conteúdo deste arquivo quando for realizado um request para a URL registrada:
FakeWeb.register_uri('http://some.url', :response => File.join('path', 'to', 'output-file'))
Caso você não saiba exatamente quais são as URLs que deverão ser registradas, o FakeWeb possui uma configuração que faz com que qualquer request a uma URL não registrada (e que acessaria a rede) gere uma exceção contendo a URL solicitada. É uma forma suja de descobrir as URLs, mas resolveu meu problema :)
FakeWeb.allow_net_connect = false
Aplicando isso para testar o processo de autenticação ao Twitter com OAuth
Precisamos criar o código capaz de executar o seguinte step:
Given I am logged in
Estou considerando como exemplo a utilização da gem twitter-auth, a qual fornece mecanismos para autenticação ao Twitter usando OAuth ou autenticação básica.
No arquivo features/support/env.rb registramos as URLs no FakeWeb, para as quais já temos os retornos esperados dentro de arquivo gerados com o auxílio do curl:
require 'fake_web' FakeWeb.allow_net_connect = false FakeWeb.register_uri(:post, 'https://twitter.com:443/oauth/request_token', :string => 'oauth_token=faketoken&oauth_token_secret=faketokensecret') FakeWeb.register_uri(:post, 'https://twitter.com:443/oauth/access_token', :string => 'oauth_token=fakeaccesstoken&oauth_token_secret=fakeaccesstokensecret') FakeWeb.register_uri(:get, 'https://twitter.com:443/account/verify_credentials.json', :response => File.join(RAILS_ROOT, 'features', 'fixtures', 'credentials.json'))
Além disso, precisamos simular o redirecionamento realizado pelo provider de volta à nossa aplicação. Tomando como exemplo o código do controller utilizado no twitter-auth para criar as sessões:
class SessionsController < ApplicationController def new if TwitterAuth.oauth? @request_token = TwitterAuth.consumer.get_request_token session[:request_token] = @request_token.token session[:request_token_secret] = @request_token.secret url = @request_token.authorize_url url << "&oauth_callback=#{CGI.escape(TwitterAuth.oauth_callback)}" if TwitterAuth.oauth_callback? redirect_to url else # we don't have to do anything, it's just a simple form for HTTP basic! end end def oauth_callback unless session[:request_token] && session[:request_token_secret] authentication_failed('No authentication information was found in the session. Please try again.') and return end unless params[:oauth_token].blank? || session[:request_token] == params[:oauth_token] authentication_failed('Authentication information does not match session information. Please try again.') and return end @request_token = OAuth::RequestToken.new(TwitterAuth.consumer, session[:request_token], session[:request_token_secret]) @access_token = @request_token.get_access_token #... [/source] pode-se perceber que a primeira action acessada é <strong>new</strong>, a qual redireciona o usuário para realizar autenticação no Twitter. Quando o usuário é autenticado, ele é redirecionado de volta para a action <strong>oauth_callback</strong> do controlle SessionsController. Na action <strong>new</strong> são colocados na sessão o par <em>token/secret</em> de request, os quais serão confirmados quando o usuário voltar para a action <strong>oauth_callback</strong>. Será necessário realizar dois requests distintos na implementação do nosso step: um para colocar os dados na sessão e um segundo para simular o redirecionamento para a action <strong>oauth_callback</strong>. O implementação do step ficaria mais ou menos assim: Given /^I am logged in$/ do visit login_url visit oauth_callback_url end
Tudo isso pra que duas linhas de código funcionem como esperado!
Era o que eu estava precisando. Estavamos pensando em levantar um serviço utilizando Sinatra para simular o serviço que utilizamos, mas com esse FakeWeb ficará bem mais fácil.
Cara, muito legal o artigo. Já venho há algum tempo querendo usar o Fakeweb pra testar chamadas http! Vou ver se começo logo, hehehe
Ola,
“Testes funcionais devem realmente testar sua aplicação. De ponta a ponta. Se você usar mocks e stubs, isso não será muito verdade… O propósito de ferramentas como o Cucumber é testar as coisas como elas realmente são.”
Permita-me discordar, mas essa é a definicao de testes de integracao, nao seria cucumber um framework BDD?
Testes funcionais testam as regras de negocio “pra baixo”. Algumas pessoas incluem a UI nos testes de aceitacao, na minha opiniao, um botao que é incluido no teste é suficiente pra fugir do proposito inicial que é testar regras de negocio. Me parece que voce foi ao extremo do que considero um anti-pattern, testar login distribuido como regra de negocio.
Mas achei interessante o FakeWeb, principalmente pra integrar com o twitter quando estiver fora do ar. :)
@Capixaba,
Eu acredito que as regras de negócio devem ser testadas através de testes unitários. O Cucumber vai testar as diversas partes da aplicação em conjunto, desde a interação do usuário com a aplicação até as regras de negócio e os resultados da execução das mesmas.
Eu não testei login como regra de negócio. Se para realizar uma determinada operação na aplicação o usuário precisar estar autenticado, a autenticação deve ser testada, primeiro com testes unitários e depois com uma ferramenta que integre todas as partes da aplicação que criam a funcionalidade de autenticação, pois é assim que o usuário utilizará o sistema.
Autenticação ‘per si’ não é regra de negócio, é regra de autorização, ou seja, segurança. É um conceito inerente a qualquer aplicação e não a um negócio específico.
Sobre o Twitter fora do ar, essa semana mesmo eles desabilitaram a parte de OAuth, disseram que voltariam até ontem… Nestes casos seria bom que as aplicações ainda mantivessem suporte a basic authentication :)
Abraço!
Muito bom a sequencia de posts sobre o Cucumber. Também adoro isso.
Abraços