Apartment::TenantNotFoundをrescueするElevatorを使う

井原(@ihara2525)です。

apartmentを使ってサブドメインでテナントを切り替える場合、存在しないテナントのサブドメインにアクセスするとApartment::TenantNotFoundが投げられるのですが、これはApplicationController等のレベルではrescueできません。

というわけで、Rackのミドルウェアを追加してそこで吸収するのですが、apartmentでいうところのElevatorを継承して、Apartment::Elevators::Subdomainを親とするミドルウェアをつくってみます。

lib/middlewares/rescued_tenant_elevator.rb

require 'apartment/elevators/subdomain'

class RescuedTenantElevator < ::Apartment::Elevators::Subdomain
  def call(env)
    super
  rescue Apartment::TenantNotFound
    request = Rack::Request.new(env)
    Rails.logger.error "Apartment Tenant not found: #{subdomain(request.host)}"
    [302, { 'Content-Type' => 'text/hml', 'Location' => Rails.application.routes.url_helpers.root_url }, self]
  end

  # needed to work
  def each
  end
end

こんな感じで、superを呼び出してまずApartment::Elevators::Subdomainに処理を行ってもらい、例外が発生した場合は上記の例だとトップページに飛ばす、みたいな対応をしています。

さらに、これのテストも書いたので、それも載せておきます。

spec/lib/middlewares/rescued_tenant_elevator_spec.rb

require 'rails_helper'

RSpec.describe RescuedTenantElevator do
  include Rack::Test::Methods

  let(:test_app) { -> (env) { [200, env, 'app'] } }
  let(:app) { described_class.new(test_app) }

  context 'when the tenant is public' do
    it 'returns 200' do
      get('http://example.org')
      expect(last_response.status).to eq 200
    end
  end

  context 'when the tenant exists' do
    it 'returns 200' do
      get("http://#{SUBDOMAIN}.example.org")
      expect(last_response.status).to eq 200
    end
  end

  context 'when the tenant does not exist' do
    it 'redirects to service top page' do
      get('http://foo.example.org')
      expect(last_response.status).to eq 302
      expect(last_response.header['Location']).to eq 'http://localhost:3000/'
    end
  end
end

テナントがない(サブドメインがない)場合や、テナントがある場合は200を返しますが、テナントがない場合は上記のミドルウェアの実装に従って302を返し、所定のページにリダイレクトされることをテストしています。

何かのご参考になれば幸いです!

CircleCI上のCapybara::Poltergeist::JavascriptErrorを回避する

井原(@ihara2525)です。

CircleCIでPoltergeistを使ってテストを回しているのですが、PhantomJSのバージョンが古いため(2015/7/27現在1.9.8)、以下のようなエラーが出てしまいました。

Capybara::Poltergeist::JavascriptError:
  One or more errors were raised in the Javascript code on the page. If you don't care about these errors, you can ignore them by setting js_errors: false in your Poltergeist configuration (see documentation for details).

  TypeError: 'undefined' is not a function (evaluating 'ReactElementValidator.createElement.bind(
        null,
        type
      )')

バージョン2系になれば必要なくなるのですが、それまでの間、Gemfileにbind-polyfillを追加することでエラーを回避することができました。

source 'https://rails-assets.org' do
  gem 'rails-assets-bind-polyfill', '~> 1.0'
end

簡単ですが、以上です!

ActiveJob#perform_laterにモデルのインスタンスを渡すテストを書く

井原(@ihara2525)です。

以下のような、インスタンスが変更されるとActivejobでElasticsearchのインデックスを更新する処理があり、そのテストを書きたい!ということでやってみました。

after_commit -> { ElasticsearchIndexerJob.perform_later('index', self) if published? }, on: :create

ActiveJob::TestHelperという便利なヘルパが用意されているので、まずはそれをinclude。

spec/support/active_job.rb

RSpec.configure do |config|
  config.include ActiveJob::TestHelper
end

ジョブが保存される場合はその内容をチェック、保存されない場合はキューが空であることをテストしています。

require 'rails_helper'

RSpec.shared_examples_for 'indexable' do
  shared_examples_for 'indexed' do
    before { clear_enqueued_jobs }

    it 'is in indexing queue' do
      subject.save
      expect(enqueued_jobs.size).to eq 1
      expect(enqueued_jobs.first[:job]).to eq ElasticsearchIndexerJob
      expect(enqueued_jobs.first[:args]).to eq [operation, { '_aj_globalid' => subject.to_global_id.to_s }]
      expect(enqueued_jobs.first[:queue]).to eq 'default'
    end
  end

  shared_examples_for 'not indexed' do
    before { clear_enqueued_jobs }

    it 'is not in indexing queue' do
      subject.save
      expect(enqueued_jobs.size).to eq 0
    end
  end

  context 'when the source is created' do
    let(:operation) { 'index' }

    context 'when the source is published' do
      before { subject.published_at = Time.now }

      it_behaves_like 'indexed'
    end

    context 'when the source is not published' do
      it_behaves_like 'not indexed'
    end
  end
end

実際perform_laterには'index', selfを渡しているので(selfはincludeされているモデルのインスタンス)、ActiveJob::TestHelperに用意されているassert_enqueued_withを使って、

assert_enqueued_with(job: ElasticsearchIndexerJob, args['index', subject], queue: 'default') { subject.save }

のように書きたかったのですが、キューにはGlobal IDが入っているため、上記のようにGlobal IDと比較する、若干内部実装によったテストになってしまっています。

これがベストなのかわかりませんが、perform_laterが呼び出されることをチェックするよりは良くなっているように思います!

Gyazoでキャプチャした画像をS3に保存するSinatraサーバを書いてみた

井原(@ihara2525)です。

会社はできましたが画像を共有する仕組みがない!というわけで、

  • 画像はS3に保存する
  • 鍵等の共有を各クライアントでやりたくないのでHerokuでサーバを動かす
  • サーバは画像の一覧表示等できなくて良いので、とにかくS3に上げてくれればOK

な感じのサーバをSinatraでさくっと書いてみました。

github.com

バケット名、AWSの鍵を環境変数に設定してHerokuにpushすればサーバ設置は完了、あとはクライアントをいただいてきてHOSTCGIを書き換えればクライアントも完了です。

あとはS3でIP制限かけたりしないとですね!

Enjoy capturing!

Hound CIを自前でホスティングする

エンジニアの出口 (@dex1t)です。

プルリクエスト毎に、コードの品質をチェックしてくれるHound CIですが、

ということで、自前でホスティングしてみました。Hound自体は、こちらで公開されています。

Houndを手元で動かすにあたって、基本はこちらのドキュメントに沿って進めていけばいいのですが、いくつかハマりどころがありました。

template0を明示する

Houndに限ったことではなくPostgresqlの環境によりますが、template1がUTF8以外で作られている環境では、 rake db:createに失敗することがありました。 config/database.ymltemplate: template0を明示してあげる必要があります。

Stripeのアカウントを用意する

PrivateリポジトリをHoundに登録する際に、決済サービスのStripeAPIを叩くため、API Keyが必要になります。

f:id:dex1t:20150618212945p:plain

アカウントを用意した後で、Stripeのtest環境用API Keyを、Houndの.envにセットしましょう。

StripeでPrivateプランを作る

ここが最大のハマりどころですが、HoundにPrivateリポジトリを登録する際には、 Stirpe上でPrivateプラン (本番だとこれが月額$12) を購入することになります。

手元の環境でも同様に、プランをStripe上に用意してあげる必要があります。

f:id:dex1t:20150618210534p:plain

Stripeのダッシュボードからプランが作成できるので、ID: privateという名前で適当に作成しましょう。

ここまでをクリアすれば、あとは普通のRailsアプリの感覚で手元で動かすことができると思われます。

自前のカスタムCopを使う

Hound CIは、Rubyのコードを静的解析するためにRubocopを使っています。 そのため、Rubocop用のrubocop-rspecや、自前のカスタムCopを、Gemfileに追加するだけでHoundでも利用できます。

手元のRubocopとHound CIのコーディング規約を揃えたい

Hound CIがある場合でも、手元でRubocopを回したい場合もあります。 ただ、両者のコーディング規約を揃えておかないと、いまいちです。

これはHoundのドキュメントにもありますが、 各リポジトリに、.hound.ymlを置いて、

ruby:
    config_file: .rubocop.yml

のようにコーディング規約にどのファイルを使うのかを明示してあげられます。

これによって、Houndに手元のrubocopの規約を流し込むことができます。

おわり

f:id:dex1t:20150618211901p:plain

これで弊社のBot、ジャーニーくんがコメントをくれるようになりました!

RuboCopのカスタムCopをつくるときの注意点

井原(@ihara2525)です。

自社の文法チェックのためにRuboCopのカスタムCopをつくりたいと思い調べていたところ、RuboCopのCustom Copを作るという良い記事があるのですが、2015/6/16時点で若干気をつけないといけない点がありました。

rubocop-0.29.1までを使う

rubocop-0.30.0からspec以下が含まれなくなったようで、テストの書き方を変えないといけません。具体的にはinspect_sourceを使えないので、その対応をどうするかというところです。 僕は0.29.1を使うことでとりあえず回避しました :+1:

Gem::Specification.find_by_nameではなくBundler.load.specs.findを使う

上記のinspect_sourceを使うためにrubocopのヘルパを読み込むのですが、rubocopの複数バージョンがインストールされていた場合、

require File.join(Gem::Specification.find_by_name('rubocop').gem_dir, 'spec', 'spec_helper.rb')

だと最新のものを見つけてしまい(0.29.1以降があればそれ)、そうすると読み込みたいヘルパがないバージョンをロードしようとしてしまいます。

そのため、

rubocop_gem_path = Bundler.load.specs.find { |s| s.name == 'rubocop' }.full_gem_path
Dir["#{rubocop_gem_path}/spec/support/**/*.rb"].each { |f| require f }

のように、Bundler.load.specs.findを使ってBundler環境でrubocopを探すようにしました。gemspecに

spec.add_development_dependency 'rubocop', '0.29.1' # fix the version, see https://github.com/nevir/rubocop-rspec/issues/38

のように書いておけばバージョン固定で大丈夫だと思います。

というわけでとりあえずつくってみた弊社用のrubocop拡張はこちら。

github.com

it { should be_valid }

みたいなものは

it { is_expected.to be_valid }

のように指摘して欲しかったのです。

RuboCop、頑張ればコーディング規約のような文章をつくるよりも機械にチェックさせることができて断然楽そうです。 もっとやりたいことがあるのですが、とりあえずまずはここまで!

予約語を管理するgemをつくりました

井原(@ihara2525)です。

先日apartmentを使ってマルチテナントを実装するというエントリを書いたのですが、取得されたくないサブドメインをどうやって管理しようかということで、予約語をためておくだけのgemをつくってみました。

github.com

デフォルトでは数個の単語だけ(%w(admin api image rss www))を登録しているので、例えばRailsを使っているのであれば、config/initializers/reserved_words.rb等の中で、

ReservedWords.add(%w(bitjourney public))

等として単語を追加して、

validates :subdomain, exclusion: { in: ReservedWords.list }

みたいにバリデーションを設定してあげれば良いと思います。

なかなかニッチですね :+1: