Testing Feedzirra

For one of my side-projects I’m using Feedzirra, a robust feed parsing library. Since we all TATFT I wanted to test parts of my code that depend on feed parsing.

The glitch is that Feedzirra doesn’t work with FakeWeb, since it’s using cURL for remote connections. However cURL supports file:// protocol, so you can fake external requests with local files. I’m using Shoulda in the following examples.

app/models/feed_storage.rb
# Excerpts extracted from model

class FeedStorage < ActiveRecord::Base
  def parse
    if self.marshaled.nil?
      parser = Feedzirra::Feed.fetch_and_parse self.feed.url
      entries = parser.entries
    else
      parser = Marshal.load self.marshaled
      parser = Feedzirra::Feed.update parser
      entries = parser.new_entries
    end

    self.update_attribute :marshaled, (Marshal.dump parser)
    entries
  end
end
test/unit/feed_storage_test.rb
# Excerpts extracted from model's tests
class FeedStorageTest < ActiveSupport::TestCase
  context "a new feed" do
    setup do
      @feed = Factory :feed
      @feed.update_attribute :url, "file://#{URI.escape(File.join(File.dirname(File.expand_path(__FILE__, Dir.getwd)), "..", "fixtures", "full_feed.rss"))}"
    end
    should "setup a new parser and parse all entries" do
      assert_equal 50, @feed.parser.parse.length
    end
  end

  context "a parsed feed" do
    setup do
      @feed_path = "#{URI.escape(File.join(File.dirname(File.expand_path(__FILE__, Dir.getwd)), "..", "fixtures"))}"
      `cp #{File.join @feed_path, "full_feed.rss"} #{File.join @feed_path, "feed.rss"}`

      @feed = Factory :feed
      @feed.update_attribute :url, "file://#{File.join @feed_path, "feed.rss"}"
      @feed.parser.parse

      `cp #{File.join @feed_path, "updated_feed.rss"} #{File.join @feed_path, "feed.rss"}`
    end
    should "parse all new entries" do
      assert_equal 1, @feed.parser.parse.length
    end
    teardown do
      `rm -f #{File.join @feed_path, "feed.rss"}`
    end
  end
end

The logic behing FeedStorage is quite simple - for every feed in the database I store its feed parser (a marshaled Ruby object, might switch to something else if I run into performance issues). Feed class requires valid urls for url field, hence the update_attribute call.

Feel free to drop me a line in the comments - I’m sure there’s some room for improvements!

Fixing smtp_tls.rb for Ruby 1.8.7

I’ve been on-and-off developing a couple of Ruby On Rails applications and since most of them are in the “just for fun” stage I was using my Google Apps account for e-mail delivery. However, I kept on getting argument count error in smtp_tls.rb (a library required for proper e-mail delivery via smtp.gmail.com server) - both on my development and staging machine. I know, I know, I should get a dedicated box and become a real man, but for now my Dreamhost account should be enough (hey, it’s for blogging and fun, not production!).

But back to the problem - I kept on fixing smtp_tls.rb until this morning a lightbulb popped over my head. Yes, check_auth_args in Net::SMTP takes different arguments in Ruby 1.8.7 (which I’m using for development) and in Ruby 1.8.6 (staging).

In order to make things right just do some Ruby version checking (see lines 9 - 13):

require "openssl"
require "net/smtp"

Net::SMTP.class_eval do
  private
  def do_start(helodomain, user, secret, authtype)
    raise IOError, 'SMTP session already started' if @started

    if RUBY_VERSION == "1.8.7"
      check_auth_args user, secret
    else
      check_auth_args user, secret, authtype if user or secret
    end

    sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
    @socket = Net::InternetMessageIO.new(sock)
    @socket.read_timeout = 60 #@read_timeout

    check_response(critical { recv_response() })
    do_helo(helodomain)

    if starttls
      raise 'openssl library not installed' unless defined?(OpenSSL)
      ssl = OpenSSL::SSL::SSLSocket.new(sock)
      ssl.sync_close = true
      ssl.connect
      @socket = Net::InternetMessageIO.new(ssl)
      @socket.read_timeout = 60 #@read_timeout
      do_helo(helodomain)
    end

    authenticate user, secret, authtype if user
    @started = true
  ensure
    unless @started
      # authentication failed, cancel connection.
      @socket.close if not @started and @socket and not @socket.closed?
      @socket = nil
    end
  end

  def do_helo(helodomain)
    begin
      if @esmtp
        ehlo helodomain
      else
        helo helodomain
      end
    rescue Net::ProtocolError
      if @esmtp
        @esmtp = false
        @error_occured = false
        retry
      end
      raise
    end
  end

  def starttls
    getok('STARTTLS') rescue return false
  return true
  end

  def quit
    begin
      getok('QUIT')
    rescue EOFError
    end
  end
end