Pular para o conteúdo
abril 23, 2009 / cassiomarques

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.

oauth-test

oauth-test

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&#91;:request_token&#93; = @request_token.token
      session&#91;:request_token_secret&#93; = @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&#91;:request_token&#93; && session&#91;:request_token_secret&#93; 
      authentication_failed('No authentication information was found in the session. Please try again.') and return
    end

   unless params&#91;:oauth_token&#93;.blank? || session&#91;:request_token&#93; ==  params&#91;:oauth_token&#93;
     authentication_failed('Authentication information does not match session information. Please try again.') and return
   end

   @request_token = OAuth::RequestToken.new(TwitterAuth.consumer, session&#91;:request_token&#93;, session&#91;:request_token_secret&#93;)

   @access_token = @request_token.get_access_token

   #...
&#91;/source&#93;

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!

5 Comentários

Deixe um comentário
  1. Tiago Albineli Motta / abr 24 2009 2:05 am

    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.

  2. bueno / abr 24 2009 4:12 am

    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

  3. Capixaba / abr 24 2009 3:22 pm

    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. :)

  4. cassiomarques / abr 24 2009 3:33 pm

    @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!

  5. Ricardo Almeida / abr 29 2009 6:31 pm

    Muito bom a sequencia de posts sobre o Cucumber. Também adoro isso.

    Abraços

Deixar mensagem para cassiomarques Cancelar resposta