トップ 最新 追記

2019-06-01 [長年日記]

_ Rails + rack-corsの設定をRSpecだけである程度お手軽にテストする

やりたいこと

  • CORSの設定ミスやエラーをできるだけ早期に発見する
  • CORSの設定のテストをできるだけブラウザを使った手動テスト以外の方法で実現する
  • 以上をできるだけ手軽に実現する

CORSのテストはとてもやっかい

CORS は非常にやっかいで、そもそも用語が request 側と response 側で分かりにくいというのもあるんだけど、何よりテストが面倒くさい。JavaScript からの CORS リクエストの動作を完全にテストしようとすると、例えば Rack アプリの場合、

  • cross-origin リクエストを生成するために JavaScript 向けに HTTP サーバを立てる
  • rack サーバを立てる
  • JavaScript のテストコードを書く
  • 以上を連動させる自動テストを作る

ということになってしまう。これはかなり面倒くさい。もちろん rack サーバ側は Rails アプリをそのまま使うとなると E2E になる。

rspec-railsだけでどこまでいけるのか?

ということで、どこまでだったら RSpec だけで頑張れるかをちょっと試してみた。環境は以下の通り。

  • rails 5.2.3
  • rspec-rails 3.8.2
  • rack-cors 1.0.3
rack-corsのdebugを有効に
config.middleware.insert_before 0, Rack::Cors do

で書いていく部分に debug 設定を追加。こんな感じ。

config.middleware.insert_before 0, Rack::Cors, debug: !Rails.env.production?

こうしておくと production の時以外は X-Rack-CORS* なヘッダが追加されるので、test 時にも利用できる。

ここで大事なのは

X-Rack-CORS: miss

というヘッダが現れるかどうか。これが現れたら少なくともサーバ側の動作としては CORS の要件を満たさないので目的の response を返してくれることはない。

request specを書く

これには以下のような理由がある。

  • Rails アプリが E2E で動作するように
    • rack-cors は rack レイヤーであって Rails の Controller, Routing などのテストでは不十分
  • ある程度自由に HTTP を喋れるように
    • Capybara のボキャブラリに縛られると自由が効かない

注意点。OPTIONS を直接呼べない。

RSpec::Rails::RequestExampleGroup の利用している ActionDispatch::Itegration::Runner で define_method で決まっているっぽい。

     %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
       define_method(method) do |*args|

こゆことです。つまり Preflight リクエストを直接テストできないことを意味している。ということで書けるのは Origin ヘッダ付きの GET とか POST とかそういうものになる。

get '/api/v1/posts.json', headers: { 'Origin' => 'http://example.net' }

みたいな感じ。例えば production で動く Origin をここで指定してあげると deploy する前にテストすることができる。

※ Rails 5.1 でオプションの与え方が変わっているらしいので注意が必要。ここは RSpec ではなく ActionDispatch の方に引っぱられているので、Rails のバージョンによって変わってしまう。

cf. Request tests fail after upgrading to Rails 5

この時、

response.headers

の中身は以下のようになる。

{ "Content-Type" => "application/json; charset=utf-8",
  "Vary"         => "Origin",
  "X-Rack-CORS"  => "miss; no-origin",
..

この "X-Rack-CORS" => "miss" がポイント。これがあったら Rack のレベルで弾かれている。つまり Controller に処理が渡ることはない。

理由については rack-cors の中で以下のように定義されている。

module Rack
  class Cors

    protected
     class Result
       HEADER_KEY = 'X-Rack-CORS'.freeze

       MISS_NO_ORIGIN = 'no-origin'.freeze
       MISS_NO_PATH   = 'no-path'.freeze

       MISS_NO_METHOD   = 'no-method'.freeze
       MISS_DENY_METHOD = 'deny-method'.freeze
       MISS_DENY_HEADER = 'deny-header'.freeze
stub outなどは適宜ご自由に

E2E なので Model 側の準備も必要になってしまうが、CORS の部分だけをチェックしたいのであれば適当に Stub Out してあげればなんとかなる。これはいつもの通り。

RR.stub(Post).recents { [] }

みたいなのでたぶん十分。

まとめ - できること、できないこと

  • 少なくとも Origin をまたいだ HTTP リクエストのフリはできる
    • XHR のフリもできる
  • Origin が食い違っていることで動かない様子や Origin をオウム返しして動く様子など Rack サーバ側のテストはできる
  • JavaScript が動いているわけではないので、XHR の mode 設定はできない
    • つまり withCredentials 周りで引っかかるか否かはテストできない

欲を言えば XHR のモード設定とかしたいけれども、少なくとも今回の方法で 単純な HTTP のレベルと XHR や Fetch などの API レベルの切り分けはできる ので、そういう使い方になるのかなと思っている。

おまけ

今回 RSpec でどこまでやれるのかを確かめるきっかけになったのは、実は rack-cors が 1.0 で default の動作が変わったこと、および XHR に利用していたライブラリがなぜか withCredentials = true で動いていたことのコンボで CORS で取得していたコンテンツが表示できなくなっていたという問題だった。こうした問題に対してあれこれ試して分かったのは

  • CORS で意図通りにコンテンツを扱えないという問題を取得できる堅牢な API はない
    • XHR および Fetch API では通信の失敗とコンテンツが利用できないエラーは区別できない
  • ブラウザのconsole上には何か出ている
  • つまり production のモニタリングで失敗を検知するのは難しい

ということだった。*1 少なくとも本気でやるなら最初に書いたようなテストの自動化を用意してあげる必要はありそうなんだけど、ちょっと今の環境でそこまでやる手間を掛けるのはアレだなと思って別な方法を探していた次第。

はー、思ったより疲れたよ。CORS ってほんとに狭間にあるので、意外と知見がまとまってないんだよなー。

参考

Tags: Ruby

*1 もしかしたらイマドキの賢い Fetch クライアント的なものではうまく扱える可能性もないではないけど。


2019-06-05 [長年日記]

_ Hashie::Dashでお手軽validationあるいはリソースを信用できるかどうか問題

intridea/hashie: Hashie is a collection of classes and mixins that make hashes more powerful.

まとめ

  • Ruby で nil を避ける方法は事前に validate する方法と取得時、利用時に気をつける方法がある
  • 事前に validate できる方が安心だしテストも楽(影響範囲が小さい)
  • いくつか validator はあるが Hashie::Dash は validate 機能内蔵していてかなりお手軽でいい感じ
    • 独立した validator を使うのは大袈裟だなと感じるようなカジュアルなシーンにハマりそう

Rubyでnilを避ける方法百選

nil が入るかもしれないところのコードをどうするかにはいくつか方法がある。

  • ActiveSupport の try
  • ぼっち演算子 (&. )
  • 文字列と分かっているなら .to_s
  • 逆に Hash から取り出す時に死んでほしい Hash#fetch
  • 素朴に nil?

などなど。

逆に DBMS 前提なら例えば ActiveRecord で

create_table do |t|
  t.string  :name, null: false
end

とすると name に nil を入れて save することはできない。

ま、扱うオブジェクトによっていろいろある。

事前に弾くのか、取得時に考慮するのか

上に挙げた例はアプローチが正反対である。

  • 前者は値の取得時、利用する際に nil を避ける方法
  • 後者はそもそも nil が入らないようにする方法

今回は後者の nil が入らないようにするということを validation と呼ぶことにしている。

Hashie::Dashの使い方

Hashie::Dash はみんな大好き Hashie::Mash に事前の property 定義が書けるもの。以下のような感じ。

class Model < Hashie::Dash
  property :foo, required: true
end

これで

Model.new(foo: nil)

とすると

ArgumentError: The property 'foo' is required for Model.

というエラーになる。おぉ、これならふいに nil を踏むことはなくなるじゃん。逆に

Model.new(foo: 'nil', bar: 1)

になると

NoMethodError: The property 'bar' is not defined for Model.

で、余計な property を突っ込むこともできない。

m = Model.new(foo: 'nil')
m.bar = 1

だと

NoMethodError: undefined method `bar=' for #<Model foo="nil">

おおおお。初期化時だろうと動的な変更だろうと弾いてくれる。

ということで Dash は以下のようなものらしい。

  • Hashie::Dash は事前に property を定義することができる Mash のようなもの
  • property の validation が自動で行われる
  • required: true の property は必須でかつ nil を与えることはできない
  • 余計な property を追加することもできない

intridea/hashie: Hashie is a collection of classes and mixins that make hashes more powerful.

の例を見ると他の property を参照することもできる。けっこう便利。

まぁ Rails 使ってるなら ActiveModel できっちり固めるという方法もあると思うし、Hanami みたいに dry-validation で schema を書くという方法もあると思うけど、もっとカジュアルに使いたいよねって時に Hashie::Dash で validate しておくと、単なる Hash より

  • いい具合にオブジェクトっぽく振舞ってくれる
  • 事前の定義で比較的安全に扱えるようにできる

ということが分かった。

なるほどな。

Tags: Ruby

2019-06-15 [長年日記]

_ 今さら改めてRack middleware

Understanding Rack Middleware

がとても分かりやすかったのでこれをもとにしたいと思う。

Rackの基本構造

  • Rack middleware はたまねぎ構造で rack app が他の rack app をくるんだ形
  • 受け取った request を外側の app が内側の app へバケツリレーし、response を内側の app から外側の app へリレーする
  • この組み立てのために Rack::Builder の use と run を使う

Rackアプリの基本形

  • callメソッドを持っていること

だけ。

call メソッドを持っていれば class だろうが Proc オブジェクトだろうが Method オブジェクトだろうがなんでもよい。

まぁ本当は call メソッドの引数の型と戻り値の型がどうってのはあって、Hash を受け取って Rack::Response を finish して返しなさいってことなんだけど、それは置いておく。

Rack middlewareは二種類ある

  1. バケツリレーする middleware
  2. response を直接返す middleware

どちらも Lint 的には valid なので少しややこしいのと、直接 response を返してるのは middleware と呼ぶのか?という気はするけど、まぁそういうものらしい。

Rack middlewareはclassで

説明は以下に続くんだけど、とにかく

middlewareはアプリと違ってcallメソッドを持っているだけではダメで、classにする

と覚えておくこと。

前者のバケツリレーを実現するたまねぎ構造のために

rack app のところでは call があればよいという話だったんだけど、たまねぎ構造のためには app を受け取ってリレーする必要がある。これをどう実現するのかというと、ちょうどいいサンプルが rack 2.0.7 の

Rack::Builder#use

のコメントに例が書かれているので貼っておく。

#   class Middleware
#     def initialize(app)
#       @app = app
#     end
#
#     def call(env)
#       env["rack.some_header"] = "setting an example"
#       @app.call(env)
#     end
#   end
  • initialize で app を受け取ってどこかのインスタンス変数に保存しておく
  • call で env を受け取って、call の中で保存しておいた app を取り出してそいつの call を呼ぶ

という形になっている。どこでこれが決まっているかというと、use の中のこれ。

@use << proc { |app| middleware.new(app, *args, &block) }

つまり、

use Middleware, *args, &block

と書くと

Middleware.new(app, *args, &block)

で他の app をくるむので、initialize で受け取る処理を書きましょう、となる。

直接返す

直接返すっていうか、直接ではないんだけど、要はこの @app.call(env) の実行がなければ内側の middleware, app は呼び出されないので、外側の middleware だけで処理が完結するという形。

例えば Rack::Attack という middleware があるんだけど、この 6.0.0 の call は以下のようになっている。

 def call(env)
   env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
   request = Rack::Attack::Request.new(env)

   if safelisted?(request)
     @app.call(env)
   elsif blocklisted?(request)
     self.class.blocklisted_response.call(env)
   elsif throttled?(request)
     self.class.throttled_response.call(env)
   else
     tracked?(request)
     @app.call(env)
   end
 end

アクセスを block するのが目的なので、 blocklist に引っかかった場合は中の app は呼ばれず、自分で用意している response を返す形になっている。

順番をあとから制御したい場合はどうしたらいいの?

基本的に rack middleware は use で並べた順番にだんだんと内側に middleware をくるんでいくので、この順番で実行される。

しかし例えば Rails では

middleware.insert 0, Middleware

という書き方で順番を後から制御できる。しかしこの記法、これは Rack の機能じゃないのです。Rails::Configuration::MiddlewareStackProxy ってやつがいい具合にやってくれている。

逆にこの機能に依存せずに request を受け取ってすぐに受け取る何かを作ろうと思ったらどうするか?

そう、Rack は app を middleware でくるめばいいだけなので、config.rb で

run Middleware.new(Rails::Application)

みたいなことをやればよい。なるほどなぁ。

middlewareの中身はこんな感じ

class Middleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    res = Rack::Response.new(body, status, headers)

    res.xxxxx  # なんかやる

    res.finish
  end
end

参考


2019-06-30 [長年日記]

_ Emacs 21時代の設定を捨ててEmacs 26に移行した

やっと時代に追いついた。なお、環境は macOS ( Terminal and Window ) なので、Windows や X Window のことは知らんです。

設定を全部捨てて作り直した

これまでの設定は Emacs 21 から設定を継ぎ足し継ぎ足した Emacs 21 - 24 まで対応するもので、mode 定義もそれぞれのバージョンでいい具合に動くように切り替えできるようにしてあった。

これは remote ( vagrant の向こうとかネットワークの向こうとか ) でも使えて、かつ手元の新し目の環境でも動くようにするために編み出した苦肉の策だったんだけど、もはや時代は Infra as Code で DevOps なので、これらの設定は完全に足枷でしかなくなっていた。そこで一気に捨てる機会を伺っていたんだけど、やっと実現できたというわけ。

予想はしていたけど、ほとんどコードを書かずに設定を終えることができた。いい時代になったもんだ。byte compile やら lazy load やら特に考えなくても起動速度も速くなってるみたいだし、よい環境だ。満足満足。

M-x customizeできなかったもの

今回はほとんどの設定を手書きの elisp ではなく M-x customize から行えた。例外は

  • keybind
  • font

くらい。それも以下くらいしかない。あれー Emacs のフォントの設定ってクソ面倒くさいイメージあったんだけど、あっさりしたもんだな。

;; keybindings
; buffer-menu -> bs-show
(global-set-key "\C-x\C-b" 'bs-show)
(global-set-key "\C-h" (quote delete-backward-char))

(when window-system
  ;; font
  (setq default-frame-alist
        (append (list
                 '(font . "Migu 1M 13")
                 )
                default-frame-alist)
        )
     (define-key global-map [?\] "\\")
     )

Terminal では相変わらず Monaco + ヒラギノなんだけど、一部の文字の大きさが揃わないので Window System くらいは完全な等幅にしておこうと思って Migu の設定を足してみた。

Miguフォント : M+とIPAの合成フォント

package.el便利

Emacs 23 も 24 も使っていたけど、ELPA についてはバージョン切り替え用の設定が悪さするのが分かっていたので使っていなかった。これで知らない elisp とか探しやすくなったし、簡単便利。

一応標準だと物足りなかったので melpa を足して、

(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)

mode を少々。追加したのは

  • csv-mode
  • yaml-mode
  • markdown-mode
  • php-mode
  • rhtml-mode
  • feature-mode
  • vue-mode
  • editorconfig
  • company

くらいかな。apache-mode とか昔は使ってたけど、必要になってから入れればいいや。

editorconfig

やっと時代に(ry

t にしないと動かないので

(custom-set-variables
 '(editorconfig-mode t)

が必要ですね。

mmm-mode

もともとあんまり好きじゃなくて使ってなかったんだけど、Single File Component な .vue とかあると対応できなきゃいけないので向き合うことにした。

inverse-videoが邪魔
(custom-set-faces
 '(menu ((t (:inverse-video t))))
 '(mmm-comment-submode-face ((t nil)))
 '(mmm-declaration-submode-face ((t nil)))
 '(mmm-default-submode-face ((t nil))))
editorconfigの設定が反映されない

editorconfig 側で追加設定が必要だった。今回できた .emacs ではこの部分がいちばんでかい。

.vue でいい具合に適用させるためには

(custom-set-variables
 '(editorconfig-indentation-alist
   (quote
    ((vue-mode js-indent-level css-indent-offset)
    ..

みたいな感じの設定が必要だった。なるほどなぁ。ちょっと不便。

Tags: Emacs