Saturday, May 19, 2012

環境変数の設定 OSX bash

dev.twitterから発行されるconsumer keyやconsumer secret keyがgithubに上がっていってしまわないように環境変数に設定する。プログラムの中から環境変数を呼び出して使用。

設定
export 変数名=変数

確認
echo $変数名

例)
export TWITTER_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
echo $TWITTER_SECRET


プログラムからの呼び出し(omniauthの設定ファイル)

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
  #provider :facebook,"App ID","App Secret"
end


アプリが1つ以上になるとちょっと不便になってくる気がするけどとりあえずこれで。

Tuesday, May 15, 2012

devise認証やってみる(4/12)モデルの関連付け、バリデーション、ユニットテスト、セキュリティ [rails3]


devise認証やってみる(4/12)モデルの関連付け、バリデーション、ユニットテスト、セキュリティ [rails3]
CommunityGuides 4/12 - Relations, Validations, Unit Tests, Security

(回を重ねるごとにキレイになるどころか、より自分メモ的になってしまって読みづらくなってきてます。。。)


モデルの関連付け

migratationファイルの修正

DBに空白が入らないように、いくつかの項目に :null => false で制約をかける。

db/migrate/…create_users.rb

class DeviseCreateUsers < ActiveRecord::Migration
  def self.up
    create_table(:users) do |t|
      t.database_authenticatable :null => false
      t.recoverable
      t.rememberable
      t.trackable
      t.confirmable
      t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :time
      # t.token_authenticatable
     
+     # author information
+     t.string :fullname
+     t.text :shortbio
+     t.string :weburl
+     t.integer :country_id, :null => false, :default => 1                # foreign key to country table
     
      t.timestamps
    end

+   add_index :users, :fullname                                          
+   add_index :users, :country_id                                      
    add_index :users, :email,                :unique => true
    add_index :users, :reset_password_token, :unique => true
    add_index :users, :confirmation_token,   :unique => true   
    # add_index :users, :unlock_token,         :unique => true
  end

  def self.down
    drop_table :users
  end
end


 ( +の行を追加。バージョン差異で吐き出されているコードが多少違うので不安を感じつつ。。。 )


 class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.integer :user_id, :null => false # + foreign key
      t.string :title, :null => false # +
      t.text :teaser, :null => false # +
      t.text :body, :null => false # +
      t.string :version
      t.text :changelog
      t.string :message # 却下時のユーザへのメッセージ
      t.text :freezebody # 記事を受け取ると同時にtextbodyをこのフィールドにコピー
      t.integer :state, :null => false, :default => 0 # + 0...下書き, 1...送信済み, 2...却下, 3...全記事, 4...オススメ記事
      t.date :submitted
      t.date :accepted

      t.timestamps
    end
   
    add_index :articles, :user_id # +
  end
end




  def self.down
    drop_table :articles
  end
はとりあえず入れないでおく。。。

% rake db:drop
% rake db:migrate



モデルの関連づけ ユーザと記事

ユーザは複数の記事を持っている。
ひとつの記事はひとりのユーザに属する。

user.rb
has_many :articles, :dependent => :destroy
attr_accessibleの行に :fullname, :shortbio, :weburl
を追加


article.rb

空だったのでまるまる追加。

class Article < ActiveRecord::Base
  belongs_to :user

  validates :user_id, :presence => true
  validates :title, :presence => true, :length => { :maximum => 80 }
  validates :teaser, :presence => true, :length => { :maximum => 500 }
  validates :body, :presence => true
  validates :version, :length => { :maximum => 120 }
  validates :changelog, :length => { :maximum => 2000 }
  validates :message, :length => { :maximum => 5000 }
  validates :state, :presence => true, :numericality => true, :inclusion => { :in => 0..4 }
end


:dependent => :destroy
関連するユーザが削除されると記事も削除される

その他ヴァリデーションはこちらを見てね、と。



ここまでのテストではarticle controllerに対してfunctional testをやってきたが、ヴァリデーションのテストはunit testを使用。

test/unit/article_test.rb

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase

  test "should not have empty title teaser body" do
    article = Article.new
    article.user_id = 1
    assert article.invalid?
    assert article.errors[:title].any?
    assert article.errors[:teaser].any?
    assert article.errors[:body].any?
    assert !article.save
  end

  test "must belong to a user" do
    article = Article.new :title => "Title", :teaser => "Teaser", :body => "Body"
    assert article.invalid?
    assert !article.save
  end

  test "should not have a state outside boundaries" do
    article = Article.new :title => "Title", :teaser => "Teaser", :body => "Body"
    article.user_id = 1
   
    article.state = -1
    assert !article.save
    article.state = 'a'
    assert !article.save
    article.state = 5
    assert !article.save
   
    article.state = 0  
    assert article.save
    article.state = 2
    assert article.save
    article.state = 4  
    assert article.save
  end
end


Rescue

などのように存在しない記事を呼び出された場合。

通常はエラー画面や public/404.html などだがflash messageをページ内に表示させて対応したほうが親切。ということで。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  ...
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
  ...
  protected
    def record_not_found
      flash[:error] = 'The article you requested could not be found.'
      redirect_to root_url
    end 
end                     # end of controller file

ActiveRecordからnotfoundのエラーをもらってrecord_not_foundメソッドに渡して、rootのflash message部分に表示させる、のような感じ?

rescue_fromの詳細は、



もっとテストする

ちなににユニットテストも rake ?
% rake
9 tests, 13 assertions, 0 failures, 0 errors, 0 skips

テストが通るということは「適切なテストがテストスートにない」ということを表している。

他ユーザによる記事編集、ログインしていない状況、など。。。

未登録ユーザのリダイレクト

test/functional/articles_controller_test.rb
require 'test_helper'

class ArticlesControllerTest < ActionController::TestCase

  setup do
    @article_user1 = articles(:one)
    @article_user2 = articles(:three)
  end

  # index and all
  test "should get index and all anonymous" do
    get :index
    assert_response :success
    assert_not_nil assigns(:articles)
    get :all
    assert_response :success
    assert_not_nil assigns(:articles)
  end 
  test "should get index and all signed in" do
    sign_in users(:user2)
    get :index
    assert_response :success
    assert_not_nil assigns(:articles)
    get :all
    assert_response :success
    assert_not_nil assigns(:articles)
  end 

  # about 
  test "should get about anonymous" do
    get :about
    assert_response :success       
  end
  test "should get about signed in" do
    sign_in users(:user2)
    get :about
    assert_response :success       
  end

  # show
  test "should show article anonymous" do
    get :show, :id => @article_user1.to_param
    assert_response :success
  end
  test "should show article signed in" do
    sign_in users(:user2)
    get :show, :id => @article_user1.to_param
    assert_response :success
  end

  # new and edit
  test "should not get new and edit anonymous" do
    get :new
    assert_redirected_to new_user_session_path
    get :edit, :id => @article_user1.to_param
    assert_redirected_to new_user_session_path
  end
  test "should get new signed in" do
    sign_in users(:user2)
    get :new
    assert_response :success
  end
  test "new article has to belong to current user" do
    sign_in users(:user2)
    get :new
    assert assigns(:article).user_id == users(:user2).id, "Article does not belong to current user"
  end
  test "should get edit to own article signed in" do
    sign_in users(:user2)
    get :edit, :id => @article_user2.to_param
    assert_response :success
  end
  test "edited article has to belong to current user" do
    sign_in users(:user2)
    get :edit, :id => @article_user1.to_param
    assert_redirected_to root_url, "Should be redirected to root url if article of other user is requested"
    assert_equal 'The article you requested could not be found.', flash[:error]
  end

  # create
  test "should not create article anonymous" do
    assert_no_difference('Article.count') do
      post :create, :article => @article_user1.attributes
    end
  end
  test "should not create article linked to other user" do
    sign_in users(:user2)
    post :create, :article => { :user_id => users(:user1).id, :title => 'Title', :teaser => 'Teaser', :body => 'Body' }
    assert assigns(:article).user_id == users(:user2).id, "Article does not belong to current user"  
  end
  test "should create article signed in" do
    sign_in users(:user2)
    assert_difference('Article.count', 1, "Article count has not changed") do
       post :create, :article => { :user_id => users(:user2).id, :title => 'Title', :teaser => 'Teaser', :body => 'Body' }
    end
    assert_redirected_to article_path(assigns(:article))
    assert_equal 'Article was successfully created.', flash[:notice]
  end

  # update
  test "should not update article anonymous" do 
    put :update, :id => @article_user1.to_param, :article => @article_user1.attributes
    assert_redirected_to new_user_session_path
  end
  test "should update article signed in" do
    sign_in users(:user2)
    put :update, :id => @article_user2.to_param, :article => @article_user2.attributes
    assert_redirected_to article_path(assigns(:article))
  end
  test "should not update article linked to other user" do
    sign_in users(:user2)
    put :update, :id => @article_user1.to_param, :article => @article_user1.attributes
    assert_redirected_to root_url, "Should be redirected to root url if article of other user is requested"
    assert_equal 'The article you requested could not be found.', flash[:error]
  end

  # destroy
  test "should not destroy article anonymous" do
    assert_no_difference('Article.count') do
      delete :destroy, :id => @article_user1.to_param
    end
    assert_redirected_to new_user_session_path
  end
  test "should destroy article signed in" do
    sign_in users(:user2)
    assert_difference('Article.count', -1) do
      delete :destroy, :id => @article_user2.to_param
    end
    assert_redirected_to articles_path
  end
  test "should not destroy article linked to other user" do
    sign_in users(:user2)
    assert_no_difference('Article.count', "Article count has changed") do
      delete :destroy, :id => @article_user1.to_param
    end
    assert_redirected_to root_url, "Should be redirected to root url if article of other user is requested"
    assert_equal 'The article you requested could not be found.', flash[:error]
  end 
end


test/fixtures/articles.yml
one:
  id: 1
  user_id: 1
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 0
  submitted: 2011-02-11
  accepted: 2011-02-11

two:
  id: 2
  user_id: 1
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 3
  submitted: 2011-02-11
  accepted: 2011-02-11

three:
  id: 3
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 0
  submitted: 2011-02-11
  accepted: 2011-02-11

four:
  id: 4
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 1
  submitted: 2011-02-11
  accepted: 2011-02-11

five:
  id: 5
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 2
  submitted: 2011-02-11
  accepted: 2011-02-11

six:
  id: 6
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 3
  submitted: 2011-02-11
  accepted: 2011-02-11 

seven:
  id: 7
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 4
  submitted: 2011-02-11
  accepted: 2011-02-11

% rake

  1) Failure:
test_edited_article_has_to_belong_to_current_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:76]:
Expected response to be a <:redirect>, but was <200>

  2) Failure:
test_new_article_has_to_belong_to_current_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:66]:
Article does not belong to current user

  3) Failure:
test_should_not_create_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:89]:
Article does not belong to current user

  4) Failure:
test_should_not_destroy_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:133]:
Article count has changed.
"Article.count" didn't change by 0.
<7> expected but was
<6>.

  5) Failure:
test_should_not_update_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:113]:
Expected response to be a redirect to <http://test.host/> but was a redirect to <http://test.host/articles/1>

20 tests, 32 assertions, 5 failures, 0 errors, 0 skips

失敗5つ。

articles controller の修正

@article = Article.find(params[:id])
をすべて以下に変更
@article = current_user.articles.find(params[:id])

現在ログイン中のユーザにひもづく記事だけを呼び出すようになる。

% rake
errorが出た。

  6) Error:
test_should_show_article_anonymous(ArticlesControllerTest):
NoMethodError: undefined method `articles' for nil:NilClass

結局showだけ
@article = Article.find(params[:id])
のままにしておいて、 edit, update, destroy だけを変更したら通った。showは普通に考えたらcurrent_userに限定する必要はない気がするけど違うのかな?

20 tests, 35 assertions, 2 failures, 0 errors, 0 skips


new
@article = current_user.articles.new

 and create
@article = current_user.articles.new(params[:article])


% rake
あとひとつ

  1) Failure:
test_should_not_create_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:89]:
Article does not belong to current user

20 tests, 35 assertions, 1 failures, 0 errors, 0 skips

なぜか?

test "should not create article linked to other user" do
   sign_in users(:user2)
   post :create, :article => { :user_id => users(:user1).id, :title => 'Title', :teaser => 'Teaser', :body => 'Body' }
+  puts assigns(:article).user_id
   assert assigns(:article).user_id == users(:user2).id, "Article does not belong to current user" 
end

puts assigns(:article).user_idから出力されるのは「1」。つまりこの記事のuser_idはtestで指示した通りに1を持ってしまっている。もしuserがuser_idをparamに入力したらRailsはそのidをアサインする。ようになっている。mass asignment。

Railsはこのマスアサインメントの管理のために attr_accessible を使う。

article model:
app/models/article.rb
class Article < ActiveRecord::Base
  belongs_to :user

+  attr_accessible :title, :teaser, :body, :version, :changelog

  validates :user_id, :presence => true
  validates :title, :presence => true, :length => { :maximum => 80 }
  validates :teaser, :presence => true, :length => { :maximum => 500 }
  validates :body, :presence => true
  validates :version, :length => { :maximum => 120 }
  validates :changelog, :length => { :maximum => 2000 }
  validates :message, :length => { :maximum => 5000 }
  validates :state, :presence => true, :numericality => true, :inclusion => { :in => 0..4 }
end

attr_accessibleではmass assignmentで有効なすべての属性?を定義する。もしパラメータがその他の属性を含む場合は、assignされない。

attr_protected という有効ではない属性を明示的に宣言するメソッドもある。がブラックリストよりもホワイトリストの方がより安全なのでお勧めしない。ホワイトは漏れがあった場合、それを追加すればいいが、ブラックの場合はすべてを追加する必要があり、なおかつ漏れがあるとセキュリティリスクが発生する。


エラー出るけどfailure 0 なら意図通りオッケー。(エラー3つ出るけどほんとにいいのか?)

  1) Error:
test_should_create_article_signed_in(ArticlesControllerTest):
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: user_id

20 tests, 30 assertions, 0 failures, 3 errors, 0 skips


疑問点:
逆にmass assignさせてもいいケースはどんなケースなのか?
mass assignmentはどう便利なのか?
user_idとかはできるようにしとくと危ないのはわかるけど、全部できないようにしといたらどんな不便があるのか?

回答:
 mass assignmentできるということはユーザが値をコントロールできるということ。

ユーザ入力を許すフィールドはattr_accessibleしてオッケーだけど、システムが振る番号や関連付けのためのuser_idなど、ユーザに任意で入力させたくない項目はmass assignの対象から外しておく、ということ。