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 assoc.secret, @response.mac_key.unbase64
  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 url_for(:consumer), @response.redirect_url.split('?')[0]
    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

    parse_response

    assert_equal url_for(:consumer), @response.redirect_url.split('?')[0]
    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

  ######################################################################
  #                          consumer request                          #
  ######################################################################

  def test_autodiscovery
    server, delegate = 
      @controller.send :autodiscover, url_for(:identity, :user=>'rubys')

    assert_equal url_for(:server), server
    assert_nil delegate
  end

  def test_consumer_form
    get :consumer
    assert_response :success
    assert_no_tag :tag=>'div', :attributes => {:id => 'alert'}
    assert_tag :tag=>'input', :attributes => {:name => 'identity'}
  end

  def test_consumer_invalid_post
    post :consumer
    assert_response :success
    assert_tag :tag=>'input', :attributes => {:name => 'identity'}
    assert_tag :tag=>'div', :attributes => {:id => 'alert'},
      :content=>'Please enter an Identity URL'
  end

  def test_consumer_post_new
    post :consumer, :identity=>url_for(:identity, :user=>'rubys')
    assert_response :redirect

    parse_response

    assert_equal url_for(:server), @response.redirect_url.split('?')[0]
    assert_equal 'checkid_setup', openid.mode
    assert_equal url_for(:identity, :user=>'rubys'), openid.identity
    assert_match /^\{HMAC-SHA1\}\d+\/\d+$/, openid.assoc_handle
    assert_equal url_for(:consumer), openid.return_to
    assert_equal url_for(:consumer), openid.trust_root
  end

  def test_consumer_post_live
    @live_assoc.identity=url_for(:identity, :user=>'rubys')
    @live_assoc.save!

    post :consumer, :identity=>url_for(:identity, :user=>'rubys')
    assert_response :redirect

    parse_response

    assert_equal url_for(:server), @response.redirect_url.split('?')[0]
    assert_equal 'checkid_setup', openid.mode
    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 url_for(:consumer), openid.trust_root
  end

  def test_consumer_post_expired
    @expired_assoc.identity=url_for(:identity, :user=>'rubys')
    @expired_assoc.save!

    post :consumer, :identity=>url_for(:identity, :user=>'rubys')
    assert_response :redirect

    parse_response

    assert_equal url_for(:server), @response.redirect_url.split('?')[0]
    assert_equal 'checkid_setup', openid.mode
    assert_equal url_for(:identity, :user=>'rubys'), openid.identity
    assert_not_equal @expired_assoc.handle, openid.assoc_handle
    assert_equal url_for(:consumer), openid.return_to
    assert_equal url_for(:consumer), openid.trust_root
  end

  ######################################################################
  #                         consumer response                          #
  ######################################################################

  def test_consumer_cancel
    get :consumer, 'openid.mode'=>'cancel'
    assert_response :success
    assert_tag :tag=>'input', :attributes => {:name => 'identity'}
    assert_tag :tag=>'div', :attributes => {:id => 'alert'},
      :content=>'Cancelled by user'
  end

  def test_consumer_valid_smart
    @live_assoc.identity=url_for(:identity, :user=>'rubys')
    @live_assoc.save!

    reply = {
      'openid.mode'=>'id_res',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.return_to'=>url_for(:consumer),
      'openid.signed'=>'mode,identity,return_to',
    }

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

    get :consumer, reply
    assert_response :success
    assert_tag :tag=>'div', :attributes => {:id => 'alert'},
      :content=>'Identity verified'
  end

  def test_consumer_invalid_smart
    @live_assoc.identity=url_for(:identity, :user=>'rubys')
    @live_assoc.save!

    reply = {
      'openid.mode'=>'id_res',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.return_to'=>url_for(:consumer),
      'openid.signed'=>'mode,identity,return_to',
    }

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

    get :consumer, reply
    assert_response :success
    assert_tag :tag=>'div', :attributes => {:id => 'alert'},
      :content=>'Signature not valid'
  end

  def test_consumer_valid_degrade_to_dumb
    @expired_assoc.identity=url_for(:identity, :user=>'rubys')
    @expired_assoc.save!

    reply = {
      'openid.mode'=>'id_res',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.return_to'=>url_for(:consumer),
      'openid.signed'=>'mode,identity,return_to',
      'openid.invalidate_handle'=>@expired_assoc.handle
    }

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

    get :consumer, reply
    assert_response :success
    assert_tag :tag=>'div', :attributes => {:id => 'alert'},
      :content=>'Identity verified'
  end

  def test_consumer_valid_degrade_to_dumb
    @expired_assoc.identity=url_for(:identity, :user=>'rubys')
    @expired_assoc.save!

    reply = {
      'openid.mode'=>'id_res',
      'openid.identity'=>url_for(:identity, :user=>'rubys'),
      'openid.assoc_handle'=>@live_assoc.handle,
      'openid.return_to'=>url_for(:consumer),
      'openid.signed'=>'mode,identity,return_to',
      'openid.invalidate_handle'=>@expired_assoc.handle
    }

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

    get :consumer, reply
    assert_response :success
    assert_tag :tag=>'div', :attributes => {:id => 'alert'},
      :content=>'Identity NOT verified'
  end

private

  ###################################################################### 
  #                          Utility Methods                           #
  ###################################################################### 

  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

######################################################################## 
#                               Mock-ups                               #
######################################################################## 

module URI
  @@testhost = ActionController::TestRequest.new.host
  instance_eval "alias uriparse parse"

  def self.parse string
    uri = uriparse string
    if uri.host == @@testhost
      uri.instance_eval {
        @request    = ActionController::TestRequest.new
        @response   = ActionController::TestResponse.new
        @request.request_uri = request_uri

        def get
          controller = ActionController::Routing::Routes.recognize!(@request)
          controller.process(@request,@response)
        end

        def post parameters
          @request.query_parameters.update(parameters)
          @request.env['REQUEST_METHOD'] = "POST"
          get
        end
      }
    end
    return uri
  end

end

module ActionController
  class TestResponse
    def code
      headers['Status'].split(' ')[0]
    end
    def message
      headers['Status'].split(' ',2)[1]
    end
  end
end
