require File.dirname(__FILE__) + '/../test_helper'
require 'openid_controller'
require 'crypto_methods'

# Re-raise errors caught by the controller.
class OpenidController; def rescue_action(e) raise e end; end

class OpenidControllerTest < Test::Unit::TestCase
  fixtures :assocs

  def setup
    @controller = OpenidController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new

    @trust_root = "http://#{@request.host}/"
  end

  def test_invalid_mode
    post :server, :controller=>'openid', 'openid.mode'=>'invalid'
    assert_response 400
    assert_tag :tag=>'p', :content => 'openid.mode is missing or invalid'
  end

  #
  # server.associate
  #

  def test_associate_cleartext
    post :server, :controller=>'openid', 'openid.mode'=>'associate'
    assert_response :success

    parse_response

    assert_match /^([A-Za-z0-9+\/]+=*)$/, @response.mac_key
    assert_match /^\{HMAC-SHA1\}\d+\/\d+$/, @response.assoc_handle
    assert_equal 7200, @response.expires_in.to_i
    assert_equal 'HMAC-SHA1', @response.assoc_type

    assoc = Assoc.find :first, :conditions=>['handle=?',@response.assoc_handle]
    assert_equal @response.mac_key.unbase64, assoc.secret
  end

  def test_associate_dh_sha1
    dh = DiffieHellman.new

    post :server, :controller=>'openid', 'openid.mode'=>'associate',
      'openid.session_type'=>'DH-SHA1',
      'openid.dh_consumer_public'=>dh.createKeyExchange.btwoc.base64
    assert_response :success

    parse_response

    assert_match /^([A-Za-z0-9+\/]+=*)$/, @response.enc_mac_key
    assert_match /^([A-Za-z0-9+\/]+=*)$/, @response.dh_server_public
    assert_match /^\{HMAC-SHA1\}\d+\/\d+$/, @response.assoc_handle
    assert_equal 7200, @response.expires_in.to_i
    assert_equal 'HMAC-SHA1', @response.assoc_type

    dh_shared = 
      dh.decryptKeyExchange(@response.dh_server_public.unbase64.unbtwoc)
    assoc = Assoc.find :first, :conditions=>['handle=?',@response.assoc_handle]
    assert_equal assoc.secret, 
      @response.enc_mac_key.unbase64 ^ dh_shared.btwoc.sha1
  end

  def test_associate_missing_dh_consumer_public
    post :server, :controller=>'openid', 'openid.mode'=>'associate',
      'openid.session_type'=>'DH-SHA1'
    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.dh_consumer_public is missing or invalid'
  end

  def test_associate_invalid_session_type
    post :server, :controller=>'openid', 'openid.mode'=>'associate',
      'openid.session_type'=>'invalid'
    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.session_type is missing or invalid'
  end

  #
  # server.checkid_setup
  #

  def test_checkid_setup_not_logged_on
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.return_to'=>url_for(:consumer),
      'openid.trust_root'=>@trust_root,
      'openid.identity'=>url_for(:identity, :user=>'rubys')
    assert_redirected_to :action=>'decide'
  end

  def test_checkid_setup_identity_missing
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup'
    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.identity is missing or invalid'
  end

  def test_checkid_setup_identity_invalid
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.return_to'=>url_for(:consumer),
      'openid.trust_root'=>@trust_root,
      'openid.identity'=>url_for(:identity, :user=>'somebodyelse')
    assert_redirected_to :action=>'decide'
  end

  def test_checkid_setup_trust_root_missing
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.return_to'=>url_for(:consumer),
      'openid.identity'=>url_for(:identity, :user=>'rubys')
    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.trust_root is missing or invalid'
  end

  def test_checkid_setup_return_to_missing
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.trust_root'=>'http://evil.host/',
      'openid.return_to'=>'http://evil.host/simple.cgi'
    assert_redirected_to :action=>'decide',
      'openid.identity'=>url_for(:identity, :user=>'rubys')
  end

  def test_checkid_setup_return_to_missing
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.trust_root'=>@trust_root
    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.return_to is missing or invalid'
  end

  def test_checkid_setup_assoc_expired
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.trust_root'=>@trust_root,
      'openid.return_to'=>url_for(:consumer),
      'openid.assoc_handle'=>@expired_assoc.handle
    assert_response :redirect

    parse_response

    assert_equal @expired_assoc.handle, openid.invalidate_handle
  end

  def test_checkid_setup_all_ok
    @request.cookies['user'] = CGI::Cookie.new('user', 'rubys')
    post :server, :controller=>'openid', 'openid.mode'=>'checkid_setup',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.trust_root'=>@trust_root,
      'openid.return_to'=>url_for(:consumer),
      'openid.assoc_handle'=>@live_assoc.handle
    assert_response :redirect
    assert_equal url_for(:consumer), 
      @response.redirect_url.split('?')[0]

    parse_response

    assert_equal url_for(:identity, :user=>'rubys'), openid.identity
    assert_equal @live_assoc.handle, openid.assoc_handle
    assert_equal url_for(:consumer), openid.return_to
    assert_equal 'mode,identity,return_to', openid.signed
    assert_match /^([A-Za-z0-9+\/]+=*)$/, openid.sig

    assert_equal openid.sig, 
      @response.kv.sign(@live_assoc.secret, openid.signed.split(','))

    assert_nil openid.invalidate_handle
  end

  #
  # server.check_authentication
  #

  def test_check_authentication_missing_assoc_handle

    post :server, :controller=>'openid', 'openid.mode'=>'check_authentication'

    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.assoc_handle is missing or invalid'
  end

  def test_check_authentication_missing_signed

    post :server, :controller=>'openid', 'openid.mode'=>'check_authentication',
      'openid.assoc_handle'=>@live_assoc.handle

    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.signed is missing or invalid'
  end

  def test_check_authentication_missing_sig

    post :server, :controller=>'openid', 'openid.mode'=>'check_authentication',
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.signed'=>'mode,assoc_handle'

    assert_response 400
    assert_tag :tag=>'p', 
      :content => 'openid.sig is missing or invalid'
  end

  def test_check_authentication_invalid_sig

    post :server, :controller=>'openid', 'openid.mode'=>'check_authentication',
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.signed'=>'mode,assoc_handle', 'openid.sig'=>'invalid'

    assert_response :success
    parse_response
    assert_equal 'false', @response.is_valid

  end

  def test_check_authentication_all_ok

    reply = {
      'openid.mode'=>'id_res',
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.signed'=>'mode,assoc_handle'
    }

    reply['openid.sig'] = 
      reply.sign(@live_assoc.secret, reply['openid.signed'].split(','))

    request = reply.dup
    request['openid.mode'] = 'check_authentication'

    post :server, request
    assert_response :success
    parse_response
    assert_equal 'true', @response.is_valid

  end

private

  def url_for action, parameters={}
    options = @controller.send(:rewrite_options, parameters)
    options.update(:action => action, :controller=>@controller.controller_name)
    ActionController::UrlRewriter.new(@request, parameters).rewrite(options)
  end

  def parse_response
    @response.instance_eval {
      if response_code == 302
        @kv = URI.parse(redirect_url).get_params
      else
        @kv = body.parsekv
      end

      def kv
        @kv
      end

      def method_missing symbol, *args
        @kv[symbol.to_s]
      end
    }
  end

  def openid
    if ! @openid
      @openid = @response.kv.dup
      def @openid.method_missing symbol, *args
        self['openid.'+symbol.to_s]
      end
    end
    @openid
  end

end
