Making Rails 4 and 3 share signed cookies

Rails 4 by default wants to upgrade all cookies, which makes┬árails 3 unable to read them. But we want that to work since we let rails 3 and 4 run in parallel to test performance (which is terrible on rails 4 … )

# While we run servers with rails 3 and rails 4 we don't want to encrypt our cookie
# once everything is on rails 4 we can by using the upgrade signed to encrypted strategy
# tested via test/integration/rails_compatibility_test.rb
if RAILS4
  ActionDispatch::Cookies::ChainedCookieJars.class_eval do
    def signed_or_encrypted
      signed
    end
  end

  # do not update ... compare to action_dispatch/middleware/cookies.rb:184
  ActionDispatch::Cookies::UpgradeLegacySignedCookieJar.class_eval do
    def initialize(*args)
      super
      @verifier = @legacy_verifier
    end

    def verify_and_upgrade_legacy_signed_message(name, signed_message)
      deserialize(name, @legacy_verifier.verify(signed_message))
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      nil
    end
  end
end
nested-errors-wordpress

Render nested Rails object errors

  • No looping
  • Infinitely deep
  • Html safe

nested-errors-wordpress

  # application_helper.rb
  def render_nested_errors(object, seen=Set.new)
    return "" if seen.include?(object)
    seen << object
    return "" if object.errors.empty?

    content_tag :ul do
      lis = object.errors.map do |attribute, message|
        content_tag(:li) do
          content = "".html_safe
          content << object.errors.full_message(attribute, message)
          values = (object.respond_to?(attribute) ? Array.wrap(object.send(attribute)) : [])
          if values.first.is_a?(ActiveRecord::Base)
            values.each do |value|
              content << render_nested_errors(value, seen)
            end
          end
          content
        end
      end
      safe_join lis
    end
  end

  # application_helper_test.rb
  
  describe "#render_nested_errors" do
    # simulate what erb will do so we can see html_safe issues
    def render
      ERB::Util.html_escape(render_nested_errors(stage))
    end

    let(:stage) { stages(:test_staging) }

    it "renders nothing for valid" do
      render.must_equal ""
    end

    it "renders simple errors" do
      stage.errors.add(:base, "Kaboom")
      render.must_equal "<ul><li>Kaboom</li></ul>"
    end

    it "renders nested errors" do
      stage.errors.add(:deploy_groups, "Invalid") # happens on save normally .. not a helpful message for our users
      stage.errors.add(:base, "BASE") # other error to make sure nesting is correct
      stage.deploy_groups.to_a.first.errors.add(:base, "Kaboom")
      render.must_equal "<ul><li>Deploy groups Invalid<ul><li>Kaboom</li></ul></li><li>BASE</li></ul>"
    end

    it "does not loop" do
      stage.errors.add(:project, "Invalid")
      stage.project.stubs(stages: [stage])
      stage.project.errors.add(:stages, "Invalid")
      render.must_equal "<ul><li>Project Invalid<ul><li>Stages Invalid</li></ul></li></ul>"
    end

    it "cannot inject html" do
      stage.errors.add(:deploy_groups, "<foo>")
      stage.errors.add(:base, "<bar>")
      stage.deploy_groups.to_a.first.errors.add(:base, "<baz>")
      render.must_equal "<ul><li>Deploy groups &lt;foo&gt;<ul><li>&lt;baz&gt;</li></ul></li><li>&lt;bar&gt;</li></ul>"
    end
  end

Ad-hoc Rack Test Servers for Integration Tests

Boots up test servers so integration tests can connect to them (when running in the same process try webmock) … works for RSpec and Minitest (with minitest-around/maxitest or similar gem)

The server boot takes about 5 seconds, might be better with a different web-server, but WEBrick is simplest.

require 'rack'
require 'base64'
require 'json'
require 'open-uri'

describe 'CLI' do
  class FakeServer
    TEST_ENDPOINT = "/__ping__".freeze

    def self.open(port, replies)
      server = new(port, replies)
      server.boot
      yield server
    ensure
      server.shutdown
    end

    def initialize(port, replies)
      @port = port
      @replies = replies.merge(TEST_ENDPOINT => 'PONG')
    end

    def boot
      Thread.new do
        Rack::Handler::WEBrick.run(
          self,
          Port: @port,
          Logger: WEBrick::Log.new("/dev/null"),
          AccessLog: []
        ) { |s| @server = s }
      end
    end

    def wait
      loop do
        break if URI.parse("http://localhost:#{@port}#{TEST_ENDPOINT}").read rescue nil
      end
    end

    def call(env)
      path = env.fetch("PATH_INFO")
      unless reply = @replies[path]
        puts "Missing reply for path #{path}"
        raise
      end
      [200, {}, [reply.to_json]]
    end

    def shutdown
      @server.shutdown if @server
    end
  end

  let(:service_a_replies) {{'/v1/catalog/services' => {}}}
  let(:service_b_replies) {{'/api/v1/nodes' => {}}}

  around do |test|
    FakeServer.open(8500, service_a_replies) do |a|
      FakeServer.open(9000, service_b_replies) do |b|
        a.wait
        b.wait
        test.call
      end
    end
  end

  it "works" do
    ...
  end
end

Bundler / Docker / alpine for a super small container

Took me a while to figure out how to get this running without installing ruby-dev or tools to compile native extensions, keeping my container nice and small.

Updated: using builtin packages for bundler + io-console now since they are smaller then installing manually and the logic is simpler
(apk add –update ruby ruby-io-console ruby-bundler)
… if latest bundler is needed below Dockerfile might still be useful.

FROM alpine

RUN apk add --update ruby && rm -rf /var/cache/apk/*

ENV BUNDLER_VERSION 1.12.3
RUN gem install bundler -v $BUNDLER_VERSION --no-ri --no-rdoc

# bundler wants some library that needs core extensions ... but it won't compile
RUN mkdir /usr/lib/ruby/gems/2.2.0/gems/bundler-$BUNDLER_VERSION/lib/io
RUN touch /usr/lib/ruby/gems/2.2.0/gems/bundler-$BUNDLER_VERSION/lib/io/console.rb

# bundler does not want to install as root
RUN bundle config --global silence_root_warning 1

RUN mkdir /app
WORKDIR /app

ADD Gemfile .
ADD Gemfile.lock .
RUN bundle

Testing SuckerPunch/Celluloid vs ActiveRecord Transactions

Celluloid runs in a new thread, so it runs on a new transaction. Therefore we cannot test what was done in this transaction.

If you are not using any Celluloid callbacks then a simple unthreaded baseclass can help:

class BackgroundJobTestable
  def perform
    ...
end

class BackgroundJob < BackgroundJobTestable
  include SuckerPunch::Job
  workers 2
end

An alternative way to fix this is to disable AR multithreading support via a mocking library like mocha … this will break any code that truly runs in parallel, but if you only run 1 code path at a time this should work fine.

ActiveRecord::Base.stubs(connection: ActiveRecord::Base.connection)