ActionController::Routing での不思議な挙動

前振り

Rails 2.3.4 では,URL にピリオド(.)があると,それ以降は :format だと思うらしく,例えば /mails/show/hogehoge@domomo.ne.jp なんて言う URL は使えません.
まあそれはいいのですが,かといって link_to でその補正はしてくれないので,絶えず URL.encode などする必要があります.

link_to "メール", :controller => "mails", :action => "show", :id => URI.encode(@user.mail, ".") # => /mails/show/hogehoge@domomo%2ene%2ejp

これで /mails/show/hogehoge@domomo%2ene%2ejp にアクセスすると

/mails/show/hogehoge@domomo%2ene%2ejp -> {:controller => "mails", :aciton => "show", :id => "hogehoge@domomo.ne.jp"

となります.

問題発生

さて,ここまではいいのですが,問題はテスト.
そもそもなぜこれが問題になったかというと,RSpec で route_for を使っていて,以下の spec がありました.

route_for(:controller => "mails", :action => "show", :cid => "hogehoge@domomo.ne.jp").should == "/mails/show/hogehoge@domomo%2ene%2ejp"

これを rspec-rails 1.2.7.1 だと pass して,rspec-rails 1.2.9 だと failure になるという現象に出くわしました.
で,この調査をした結果,以下の部分が変わっているのが原因とわかりました.

  • 1.2.7.1
            @example.assert_recognizes(@options, path, params)
  • 1.2.9
              @example.assert_routing(path, @options, {}, params)

調査

調査開始は以下の通り.RSpec も結局は ActionController::TestCase を使っているので,テストアプリを使って検証.

require 'test_helper'

class SampleControllerTest < ActionController::TestCase
  test "routing 1" do
    assert_recognizes({:controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"}, "/sample/show/hogehoge@domomo%2ene%2ejp")

    assert_routing "/sample/show/hogehoge@domomo%2ene%2ejp", :controller => "sample", :action => "show", :cid => "hoeghoge@domomo%2ene%2ejp"
    # assert_routing "/sample/show/hogehoge@domomo%2ene%2ejp", :controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"
    # assert_routing "/sample/show/hogehoge@domomo.ne.jp", :controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"
  end
end

これの結果は

% rake test
(in /home/user/rails)
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/unit/helpers/sample_helper_test.rb"
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started

Finished in 0.000144 seconds.

0 tests, 0 assertions, 0 failures, 0 errors
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/functional/sample_controller_test.rb"
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
F
Finished in 0.036886 seconds.

  1) Failure:
test_routing_1(SampleControllerTest) [/test/functional/sample_controller_test.rb:7]:
The recognized options <{"action"=>"show", "controller"=>"sample", "cid"=>"hogehoge@domomo.ne.jp"}> did not match <{"action"=>"show", "controller"=>"sample", "cid"=>"hoeghoge@domomo%2ene%2ejp"}>, difference: <{"cid"=>"hoeghoge@domomo%2ene%2ejp"}>

1 tests, 2 assertions, 1 failures, 0 errors
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb"
Errors running test:functionals!

となり,テストが通りません.かといって,

require 'test_helper'

class SampleControllerTest < ActionController::TestCase
  test "routing 1" do
    assert_recognizes({:controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"}, "/sample/show/hogehoge@domomo%2ene%2ejp")

    # assert_routing "/sample/show/hogehoge@domomo%2ene%2ejp", :controller => "sample", :action => "show", :cid => "hoeghoge@domomo%2ene%2ejp"
    assert_routing "/sample/show/hogehoge@domomo%2ene%2ejp", :controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"
    # assert_routing "/sample/show/hogehoge@domomo.ne.jp", :controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"
  end
end

だと

% rake test
(in /home/user/rails)
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/unit/helpers/sample_helper_test.rb"
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started

Finished in 0.000157 seconds.

0 tests, 0 assertions, 0 failures, 0 errors
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/functional/sample_controller_test.rb"
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
F
Finished in 0.039399 seconds.

  1) Failure:
test_routing_1(SampleControllerTest) [/test/functional/sample_controller_test.rb:8]:
The generated path <"/sample/show/hogehoge@domomo.ne.jp"> did not match <"/sample/show/hogehoge@domomo%2ene%2ejp">

1 tests, 4 assertions, 1 failures, 0 errors
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb"
Errors running test:functionals!

今度はリンク生成のときに考慮されていないのでまた失敗.そして

require 'test_helper'

class SampleControllerTest < ActionController::TestCase
  test "routing 1" do
    assert_recognizes({:controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"}, "/sample/show/hogehoge@domomo%2ene%2ejp")

    # assert_routing "/sample/show/hogehoge@domomo%2ene%2ejp", :controller => "sample", :action => "show", :cid => "hoeghoge@domomo%2ene%2ejp"
    # assert_routing "/sample/show/hogehoge@domomo%2ene%2ejp", :controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"
    assert_routing "/sample/show/hogehoge@domomo.ne.jp", :controller => "sample", :action => "show", :cid => "hogehoge@domomo.ne.jp"
  end
end

とすると

% rake test
(in /home/user/rails)
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/unit/helpers/sample_helper_test.rb"
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started

Finished in 0.000166 seconds.

0 tests, 0 assertions, 0 failures, 0 errors
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/functional/sample_controller_test.rb"
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
E
Finished in 0.040792 seconds.

  1) Error:
test_routing_1(SampleControllerTest):
ActionController::RoutingError: No route matches "/sample/show/hogehoge@domomo.ne.jp" with {:method=>:get}
    /test/functional/sample_controller_test.rb:9:in `test_routing_1'

1 tests, 1 assertions, 0 failures, 1 errors
/opt/ruby-enterprise/bin/ruby -I"lib:test" "/opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb"
Errors running test:functionals!

となり,今度は ActionController::RoutingError という八方ふさがり.

要点

これは URL の生成処理とディスパッチ処理が一致していないのが問題だと思われます.さらに assert_* については,ルーティング処理に 3 つもあり,その互換性がないのも問題じゃないかなと思われます.
さらにこれ(assert_* の挙動)が仕様なのかバグなのかが判然としないのも問題.Rails らしいと言えばそうなんですが.

結論

ピリオドを含んだ URL に対する Routing の Spec 書くときには気をつけましょう.