Rails開発でリポジトリには入れたくないんだけどローカルではやっておきたいテストがある場合の取りあつかい
自明だったり、すごく重かったり、細かすぎるテストたちがいます。
重いテストは短期的にも長期的にもコストになり、細かすぎるテストは実装変更時のコストが必要以上に高く、よくありません。
しかし、たまにやっときたいんですよというテストがありますので、本体に影響がないようにやっていきます。
Railsのディレクトリの外にもう一層つくる
以下のような構成になりました。
- /my_test - /.git - /rails_application # 本体 - /seed # テスト用の - /spec # 自分専用テスト - /models - rails_helper.rb # 自分専用のテストのrails_helper.rb - .gitignore # 本体を含めないため - Gemfile # 自分専用テスト用のGemfile
自分だけが使うGemを用意する
テストで使いたいが、該当アプリのGemfileに入っていないので使えないGemというのがありますので、どうにかします。(今回のはテストで使う!というGemではないですが)
むりくりrequire
しても、コンテキストが変わってしまい、ダメなようですので、本体のGemfileの内容をinstance_eval
します。
instance_eval(File.read(File.expand_path('../rails_application/Gemfile', __FILE__))) # 好きなGemを入れる gem 'seed-fu'
seed-fu
ところで、ダミーデータもまた、大量だったり詳細だったりすると、コストになります。
しかしさまざまなレコード状態表示の調整をする場合など、ある局面では役に立つので、テスト以外でも使えるように用意しておきます。
#!/usr/bin/env ruby require File.expand_path('../../rails_application/config/application', __FILE__) abort("The Rails environment is running in production mode!") if Rails.env.production? Rails.application.require_environment! require 'seed-fu' 10000.times do |n| ModelA.seed( id: n, name: SecureRandom.hex(4) ) end
$ bundle exec ./seed/seed.rb
これで外側から、自分のデベロップメントや、テストに、大量のダミーデータを投入できるようになりました。
テスト
rails_helper.rb
をコピペして編集する
environment
とspec_helper
に対するパス部分を本体のファイルに当たるように変更します。
require File.expand_path('../../rails_application/config/environment', __FILE__) require File.expand_path('../../rails_application/spec/spec_helper', __FILE__)
さきのseed-fu
を利用する
seed-fu
はconfig.before :all
内などに書いておけば安泰ではないでしょうか。DatabaseCleaner
などを使っている場合は、それが動いた後である必要があります。
require File.expand_path('../seed/seed', __FILE__)
実行
require 'rails_helper' RSpec.describe ModelA, type: :model do it { expect(ModelA.create!).to be_a(ModelA) } it { expect(ModelA.count).to eq(10000) } end
$ rspec -cfd # 略 should be a kind of ModelA(id: integer, name: string, created_at: datetime, updated_at: datetime) should eq 10000 Finished in 1 minute 41.39 seconds (files took 3.8 seconds to load) 2 examples, 0 failures
できました。
これはジョークのようなテストですが、たとえばCapybara
でスクリーンショットを撮りまくるといった、半分趣味のようなテストも、本体に影響なくどんどんやっていくことができます。
Authlogicに関するメモ
認証機能再発明するべからずは有名な鉄則ですが、まるで知らないままというのもマズイので実装を読むことでお茶を濁していきたい。
salt
やSCrypt
の運用、persistence_token
の更新などで比較的安全にいけるのでは。
パスワード
salt
Authlogic::Random.friendly_token
def friendly_token # use base64url as defined by RFC4648 SecureRandom.base64(15).tr('+/=', '').strip.delete("\n") end
暗号化
authlogic/lib/authlogic/crypto_providers
以下に、各種Cryptorをラップしたアダプターがある。
SCrypt
ならAuthlogic::CryptoProviders::SCrypt#encrypt(*tokens)
で暗号化パスワードを得られる。
*tokens
には[password, salt]
が入り、salt
は保存されてログイン時のパスワード一致を見る時に使われる。
生パスワードは勿論破棄される。
パスワード確認
各種Cryptorの比較演算子を使ったりする。
# ユーザーのレコードから得る salt = user.password_salt encrypted = user.encryped_password # 入力 password = 'aaaaaaaa' # 比較 SCrypt::Password.new(encrypted) == Authlogic::CryptoProviders::SCrypt.encrypt(password, salt)
セッション
cookie
などの値を得るために、Controller
インスタンスを持つアダプターが必要になる。
Controller
経由では、初期化時に自動的に付与されるAuthlogic::ControllerAdapters::RailsAdapter
が設定する。
Authlogic::Session::Base.controller = RailsAdapter.new(self)
それ以外で使いたい場合は、必要な値を返すなにかを用意する。
class ControllerLike def initialize(request) @request = request end def cookies @request.cookies end def params @request.params end def session @request.session end def responds_to_last_request_update_allowed? true end def last_request_update_allowed? false end def method_missing(*args) false end end Authlogic::Session::Base.controller = ControllerLike.new(request)
開始
create
時にユーザーレコードにpersistence_token
を保存する。
クッキーにはそのpersistence_token
とユーザーのid
を、後にsplit
可能な形で保存する。[persistence_token, id].join('::')
など。
復元
保存しておいたクッキーの値のid
からユーザーを検索、persistence_token
と一致するか見る。
AWS Lambdaで使う関数をローカルでテストするサーバーを建てる。
AWS Lambda + AWS Api Gateway + AWS DynamoDBでなにかをつくることにはまっています。
AWS Lambdaで使う関数自体はmochaなどでテストできますが、実際にブラウザから叩くテストをローカルでしたいと思いました。
そこでNode.jsで簡単なサーバーを建てます。
まず、AWS Lambdaはこのような関数です
"use strict"; const Workbook = require('./src/workbook').Workbook; const Marker = require('./src/marker').Marker; exports.handler = (data, context, callback) => { let workbook = new Workbook(data); let marker = new Marker(workbook, data); marker.mark((result) => { if (result) { context.succeed({result: 'correct', data: marker.explain()}); } else { context.fail(JSON.stringify({result: 'incorrect', data: marker.explain()})); } }); };
Workbook
やMarker
がDynamoDBにアクセスします。環境変数を使って、内部で参照ファイルを切りかえ、ローカルのDynamoDBにアクセスするようにしているので、そちらは大丈夫です。
テストサーバーです
サーバー側の言語をまともに触ったのはNode.jsがはじめてで、はじめて書いたのもこのような簡易なサーバーでした。非常に懐かしいですね。
import * as http from 'http'; import * as qs from 'querystring'; import * as marker from '../marker/index'; http.createServer(function (req, res) { if (req.method !== 'POST' && req.method !== 'OPTIONS') { res.end(''); return; } res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); res.setHeader('Content-Type', 'application/json'); if(req.method === 'OPTIONS'){ res.end(``); return; } let body = ''; req .on('data', (data) => { console.log('data', data); body += data }) .on('end', () => { qs.parse(body); marker.handler(JSON.parse(body), { succeed: (data)=> { res.end(JSON.stringify(data)); }, fail: (stringifiedData)=> { res.end(stringifiedData); } }); }); }).listen(process.env.PORT || 3080, '127.0.0.1');
OPTIONS
によるプリフライト対応処理を追加しました。(昨日は素のPOST
だけ飛んでたのにな……)
まずPOST
とOPTIONS
以外はハネます。
つぎに、データの整形ですね。こういうのは自動でやってくれないんでしょうか。
marker.handler
がAWS Lambdaでの関数本体なので、そこへデータを投げます。
context.succees
とcontext.fail
で関数を終了するつくりなので、そこに関数を妖異します。その関数呼び出しをもってJSONレスポンスを作成します。
実際のfail
では{errorMessage: 引数}
というレスポンスになりますが、これはAWS Api Gatewayで整形します。ので、それを踏まえたJSONとします。
$ TEST=true babel-node test-server/index.js
テストできた
これでブラウザ側からSuperagentなどを使いPOST
すると、実際のレスポンスが得られるようになりました。
クリッククリックで行うテストがはかどりますね。
`Regexp`などActiveRecordでメソッドが用意されていない標準SQLの演算子を清く正しく使う。
清く正しくとは、生SQL文字列を書かない程度の意味です。
下の方の長いやつはRails 4
までの話です
REGEXP
はRails 5
もしくはArel 7
からmatches_regexp
としてメソッドが用意されました。
Writer.where(Writer.arel_table[:email].matches_regexp('.*@gmail.com')) # SELECT "writers".* FROM "writers" WHERE ("writers"."email" ~ '.*@gmail.com')
Rails 5
になってなお下のような書き方をしていると殴られる可能性があります。
このようにどんどん便利になっていってるので、たまに知識更新するのも大切ですね。
たとえばRegexp
でwhere
する
グーグル先生にたずねるとStack Overflowが出てきて以下のようなコードが出てきます。
Writer.where("email REGEXP ?", '.*@gmail.com') # SELECT "writers".* FROM "writers" WHERE (email ~ '.*@gmail.com')
動きますが、せっかくのORMですから、ORMらしく書きましょう。
だいたいの演算子や句はArel
に用意されている
ドキュメントやグーグル先生より、ソース内を検索したほうが早いのですが、大体の演算子のビルダーが用意されています。
REGEXP
なら以下のように書けます。
Writer .where( Arel::Nodes::Regexp.new( Writer.arel_table[:email], Arel::Nodes::Casted.new('.*@gmail.com', Writer.arel_table[:email]) ) ) # SELECT "writers".* FROM "writers" WHERE "writers"."email" ~ '.*@gmail.com'
WHERE
句のカラム指定にテーブル名が入りました。
より具体的な記述になっていますが、テーブル名が欲しくない場合もあります('AS'した場合とか)。
ActiveRecord#arel_table
で得られるArel::Table
はその名の通りテーブルを前提とした値しか返してくれませんので、自分で組みたてる必要があります。
Writer .where( Arel::Nodes::Regexp.new( Arel.sql(Writer.connection.quote_column_name(:email)), Arel::Nodes::Quoted.new('.*@gmail.com') ) ) # SELECT "writers".* FROM "writers" WHERE "email" ~ '.*@gmail.com'
カラムのクォーティングのルールはデータベースによって違うので、quote_column_name
で万全を期すと良いでしょう。
値は値用にクォーティングします。
WHERE
句の()
が欲しい場合はさらにこうします。
Writer .where( Arel::Nodes::Grouping.new( Arel::Nodes::Regexp.new( Arel.sql(Writer.connection.quote_column_name(:email)), Arel::Nodes::Quoted.new('.*@gmail.com') ) ) ) # SELECT "writers".* FROM "writers" WHERE ("email" ~ '.*@gmail.com')
大変な騒ぎになってきましたね。
Arel
のルール
Arel
は安全性を維持するために、生のString
は受けつけてくれず、なんらかのArel
クラスでラップする必要があります。
ところでArel.sql
はArel::Nodes::SqlLiteral.new
のショートカットで、これは何の評価も加工もされず、SQLに渡ります。
カジュアルに使うと生SQLをいじくりまわしているのと大して変わらないので気をつけましょう。
長い
記述が長くなってうれしみが少ないですか?
最初はウッとなりましたが、自分で文字列を組みたてるのと比べると、これが別に気にならなくなったりします。
フシギですね。
AWS Lambdaを使って、ブラウザ側とサーバー側で同じバリデーションをするということをやった。
日記です。今日の日記コードはこれ。
qiitaにはさすがにもうAWS Lambdaでフォームなんていう記事は山盛りあったのでこっちで。
日記
Node.jsはサーバーとして動かせるので、ブラウザ側とサーバー側で同じスクリプトを用いることが可能です。(アイソモーフィック?)
しかし動かすとなるとサーバーを用意して〜、など色々と難関がありました。その難関を軽く解消してくれたのがAWS Lambdaであることはみなさんご存知のとおりですね。
バリデーションの準備
まずバリデーターライブラリを用意します。
これにはvalidator
を使いました。
これをラップしたRecord
というクラスをつくり、以下のようなコンフィグ(config.jsに入っています)をあたえ、validate
メソッドで確認できるようにしました。
exports.config = { attributes: ['name', 'email', 'age', 'gender'], validation: { name: { isLength: {min: 1, max: 20, message: '1-20文字で入力してください'} }, email: { isEmail: {message: 'メールアドレスを入力してください'} }, age: { isNumeric: {message: '数字を入力してください'} }, gender: { isIn: ['female', 'male', 'other'] } } };
ブラウザ側バリデーション
あとはこのバリデーターを、ブラウザ側で使えるようにします。
ライブラリやコンフィグファイルが別ファイルになっているので、毎度おなじみのbrowserify
で連結します。
const Record = require('../lib/src/record').Record; const config = require('../lib/src/config').config; const record = new Record(config); const Validator = window.Validator = {}; Validator.validate = (data) => { if (record.assign(data).validate()) { return {result: 'success', data: record.parameters}; } else { return {result: 'failure', errors: record.errors}; } };
あとはこのバリデーションをサーバーに送信する前に行い、validであれば送信します。
サーバー側バリデーション
送信されてくるデータは全く安全でないのは常識ですから、サーバー側でもバリデーションする必要があります。
ブラウザ側とほぼ同じコードですが、AWS LambdaではNode.jsが動いていますから、browserify
は必要ありません。
今回はバリデーションの成否で終了していますが、そこらへんにdynamoDBやメール送信の処理を書けばいいんじゃないでしょうか。
"use strict"; const Record = require('./src/record').Record; const config = require('./src/config').config; exports.handler = (event, context, callback) => { let record = new Record(config); if (record.assign(event).validate()) { context.succeed({result: 'success', data: record.parameters}); } else { context.fail(JSON.stringify({result: 'failure', errors: record.errors})); } };
エラー時の戻り値がブラウザ側と違ってしまうのが気になるところですが(fail
の引数は{"errorMessage": 引数}
なJSON
として飛んでいく)、これはAWS Api Gateway側でfailureをキャッチして、同じ形にします。
.*"result":"failure".*
で失敗ステータスコードに振り、本文マッピングテンプレートでエラー部を取り出したものをレスポンスとします。
$input.path('$.errorMessage')
ブラウザ側送信時処理
送信せずとも同じバリデーションが行えるわけですから、送信前にバリデーションし、invalidであればdeployErrors
というメソッドが、各項目のinput
の上や下にエラーを表示します。
もし何らかの都合でinvalidなデータが送信されても、サーバー側でinvalid判定されて、同じエラーが返ってきてdeployErrors
というメソッドが、各項目のinput
の上や下にエラーを表示します。
function send($form, $thanks) { var data = traceValue($form); var validated = Validator.validate(data); if(validated.result == 'failure'){ deployErrors($form, validated.errors); return; } disablizeButton($form, true); superagent .post(endPoint) .set('Accept', 'application/json') .send(data) .end(function (err, res) { if (err) { deployErrors($form, res.body.errors); disablizeButton($form, false); } else { $form.hide(); $thanks.show(); } }); }
わりと長年の夢であった、サーバーとクライアントで同じコードでバリデーションするというのが、比較的簡単にできてしまいました。しかも課金の心配が結構軽い。
フォーム作成というこまごまとした仕事は、前職前前職ともによくあって、cgiの、メンテされてるんだがされてないんだかわからないスクリプトを使用していました。
Rails大好きなわたしもさすがに一個の問い合わせフォームにRailsというわけにもいかなくて難儀していましたが、これならヤバイ脆弱性も自分の責任のうちで作成できるのでいいかもしれませんね。
HerokuにSymbolic Link入りを投げるとき気をつけること
リソースを再利用しようとしました。
そこでディレクトリに対してln -s
で作成しますが、例えば同ディレクトリ内の何がしかへ張る場合、
$ ln -s development staging
とする必要があり
$ ln -s ./development staging
これだと辿れなくなります。
セキュリティかなにかの都合でスラッシュアクセスがブロックされているのでは。同じ理由で上には登れないんじゃないかな(試してない)。
おうちでLinuxOS使っててもお外に発射したら動かないコードあるし世の中は厳しい。
— おふくろさま (@o296sm) 2016年7月3日
レールズにプルリクエストがマージされてハッピーだった
ので、思いつく限りのSNSやブログに書いています。
ここにも書きます。
どれ
以下の記事のやつを作ってる時に「バグかしら?」と思ってモンキーパッチで回避してて、せっかくだからとpull requestを飛ばしてみたら忘れた頃にマージされたという話です。
Rails5.0.0がつい先日リリースされましたが、その時には自分でも忘れていて、ほん今日、なんか通知が出てるなと思って見てみたらマージされていました。
無職してた甲斐があった
仕事をしないで自分で思うがままにコードを書きつづけていたところで出会ったバグ的なものでしたから、無職でなかったら出会わなかった可能性があります。
これがここのコードが原因で起こってる、っていうのも結局p
メソッドとクリッククリックで追いつめて見つけたみたいなもんですから、よっぽど時間に余裕がないと無理だったでしょう。
無職時代のいい思い出になったな〜〜と言いたいところですが、まだ就職先は決まっておりません。
大変ですね。
まぁとにかく嬉しい
ここ3年ぐらいで一番うれしいんじゃないの。
— おふくろさま (@o296sm) 2016年7月4日
好きなフレームワークに貢献できたということで、本当に嬉しいですね。