Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Allow user to auth their account with readability. #11

Merged
merged 9 commits into from
This page is out of date. Refresh to see the latest.
3  Gemfile
View
@@ -7,6 +7,7 @@ gem 'grape-entity', '~> 0.2.0'
gem 'i18n', '~> 0.6.4'
gem 'activesupport', '~> 3.2.12'
gem 'json', '~> 1.7.7'
+gem 'hashie', '~> 1.2.0'
gem 'multi_json', '~> 1.7.0'
gem 'will_paginate', '~> 3.0.3'
gem 'sanitize'
@@ -18,6 +19,7 @@ gem 'feedzirra', '~> 0.0.31'
# Readability
gem 'readit', '~> 0.0.9'
+gem 'omniauth-readability'
# Database
gem 'pg', '~> 0.14.1'
@@ -25,7 +27,6 @@ gem 'activerecord', '~> 3.2.12'
group :development do
gem 'shotgun', '~> 0.9'
- gem 'heroku_san', '~> 4.2'
end
group :development, :test do
21 Gemfile.lock
View
@@ -28,7 +28,6 @@ GEM
descendants_tracker (0.0.1)
diff-lcs (1.2.1)
eventmachine (1.0.3)
- excon (0.16.10)
factory_girl (4.2.0)
activesupport (>= 3.0.0)
faker (1.1.2)
@@ -72,12 +71,7 @@ GEM
childprocess (>= 0.2.3)
guard (>= 1.1)
spork (>= 0.8.4)
- hashie (2.0.3)
- heroku-api (0.3.8)
- excon (~> 0.16.10)
- heroku_san (4.2.5)
- heroku-api (>= 0.1.2)
- rake
+ hashie (1.2.0)
i18n (0.6.4)
json (1.7.7)
listen (0.7.3)
@@ -92,6 +86,16 @@ GEM
multi_xml (0.5.3)
nokogiri (1.4.7)
oauth (0.4.7)
+ omniauth (1.0.3)
+ hashie (~> 1.2)
+ rack
+ omniauth-oauth (1.0.1)
+ oauth
+ omniauth (~> 1.0)
+ omniauth-readability (0.0.1)
+ multi_json
+ omniauth (~> 1.0.0.rc2)
+ omniauth-oauth (~> 1.0.0.rc2)
pg (0.14.1)
pry (0.9.12)
coderay (~> 1.0.5)
@@ -163,11 +167,12 @@ DEPENDENCIES
grape-entity (~> 0.2.0)
guard-rspec (~> 2.5.1)
guard-spork (~> 1.5.0)
- heroku_san (~> 4.2)
+ hashie (~> 1.2.0)
i18n (~> 0.6.4)
json (~> 1.7.7)
multi_json (~> 1.7.0)
nokogiri
+ omniauth-readability
pg (~> 0.14.1)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
16 Rakefile
View
@@ -1,6 +1,13 @@
#!/usr/bin/env rake
require File.expand_path('../config/environment', __FILE__)
+desc 'Deploy'
+task :deploy do
+ app = 'rss-reader'
+ sh "git push git@heroku.com:#{app}.git master:HEAD"
+ sh "heroku run rake db:migrate -a #{app}"
+end
+
desc 'Start an irb session'
task :console do
require 'irb'
@@ -22,15 +29,6 @@ namespace :updater do
end
end
-begin
- require 'heroku_san'
- config_file = File.join(File.expand_path(File.dirname(__FILE__)), 'config', 'heroku.yml')
- HerokuSan.project = HerokuSan::Project.new(config_file, :deploy => HerokuSan::Deploy::Sinatra)
- load 'heroku_san/tasks.rb'
-rescue LoadError
- # The gem shouldn't be installed in a production environment
-end
-
task :default => [:spec]
begin
59 api/api.rb
View
@@ -25,14 +25,20 @@ def filtered_items
items.filtered(params).paginate(page: params[:page])
end
+ def session
+ env['rack.session']
+ end
+
def current_user
@user ||= User.authenticate(params[:apikey])
end
+ def current_user=(user)
+ @user = user
+ end
+
def authenticate!
- unless ENV['RACK_ENV'] == 'test'
- error!('401 Unauthorized', 401) unless current_user
- end
+ error!('401 Unauthorized', 401) unless current_user
end
end
@@ -141,9 +147,52 @@ def authenticate!
if user.save
present user
else
- status 400
- { error: user.errors }
+ error!(user.errors, 400)
end
end
end
+
+ namespace :user do
+ namespace :readability do
+ desc 'Enable posting to readability.'
+ put do
+ authenticate!
+ if current_user.readability.authorized?
+ current_user.readability.enable
+ current_user.save!
+ present current_user
+ else
+ error!('You need to authorize readability first.', 400)
+ end
+ end
+
+ desc 'Disable posting to readability.'
+ delete do
+ authenticate!
+ current_user.readability.disable
+ current_user.save!
+ present current_user
+ end
+
+ desc 'Authorize this account to post bookmarks to readability.'
+ get :authorize do
+ authenticate!
+ session[:user_id] = current_user.id
+ redirect '/auth/readability'
+ end
+ end
+ end
+
+ get '/auth/readability/callback' do
+ current_user = User.find(session.delete(:user_id))
+ auth_hash = request.env['omniauth.auth']
+ current_user.readability.token = auth_hash.credentials.token
+ current_user.readability.secret = auth_hash.credentials.secret
+ current_user.save
+ 'Ok'
+ end
+
+ get '/auth/failure' do
+ { error: "Authorization failed: #{params[:message]}" }
+ end
end
13 app/app.rb
View
@@ -1,10 +1,15 @@
require 'models'
class App
- def initialize
- end
+ def self.app
+ @app ||= Rack::Builder.new {
+ use Rack::Session::Cookie, secret: ENV['COOKIE_SECRET'] || 'iebie5oKneequ1Ae'
+
+ use OmniAuth::Builder do
+ provider :readability, ENV['READABILITY_KEY'], ENV['READABILITY_SECRET']
+ end
- def call(env)
- API.call(env)
+ run API
+ }
end
end
7 app/models/entry_processor.rb
View
@@ -19,12 +19,17 @@ def update
end
def create
- items.create(attributes.merge(guid: guid))
+ item = items.create(attributes.merge(guid: guid))
+ bookmark item.link if readability.enabled?
+ item
end
private
delegate :url, :published, to: :entry
+ delegate :user, to: :feed
+ delegate :readability, to: :user
+ delegate :bookmark, to: :readability
def existing
@existing ||= items.where(guid: guid).first
14 app/models/user.rb
View
@@ -1,6 +1,10 @@
require 'securerandom'
class User < ActiveRecord::Base
+ include Grape::Entity::DSL
+
+ autoload :Readability, 'models/user/readability'
+
attr_accessible :email
# Validations
@@ -11,11 +15,14 @@ class User < ActiveRecord::Base
has_many :feeds
has_many :items, through: :feeds
- before_validation do
+ serialize :readability, Readability
+
+ before_validation on: :create do
self.token = SecureRandom.hex
end
def self.authenticate(token)
+ return nil unless token.present?
User.where(token: token).first
end
@@ -27,8 +34,7 @@ def entity
Entity.new(self)
end
- class Entity < Grape::Entity
- expose :email
- expose :token
+ entity :email, :token do
+ expose :readability, using: User::Readability::Entity
end
end
68 app/models/user/readability.rb
View
@@ -0,0 +1,68 @@
+class User::Readability < Hash
+ include Grape::Entity::DSL
+
+ def token
+ credentials[:token]
+ end
+
+ def token=(token)
+ credentials[:token] = token
+ end
+
+ def secret
+ credentials[:secret]
+ end
+
+ def secret=(secret)
+ credentials[:secret] = secret
+ end
+
+ def authorized?
+ token.present? && secret.present?
+ end
+
+ def enabled?
+ authorized? && enabled
+ end
+
+ def enabled
+ self[:enabled]
+ end
+
+ def enable
+ self[:enabled] = true
+ end
+
+ def disable
+ self[:enabled] = false
+ end
+
+ def client
+ @client ||= Readit::API.new token, secret
+ end
+
+ def bookmark(url)
+ client.bookmark url: url
+ end
+
+ def dump(obj)
+ obj
+ end
+
+ def load(hash)
+ self.class.new.tap { |o| o.replace(hash) }
+ end
+
+ def entity(options = {})
+ Entity.new(self, options)
+ end
+
+ entity :enabled
+
+private
+
+ def credentials
+ self[:credentials] ||= Hash.new
+ end
+
+end
6 app/updater.rb
View
@@ -12,7 +12,11 @@ def run
feed = Feed.next
if feed
puts "Updating #{feed.title}"
- feed.refresh!
+ begin
+ feed.refresh!
+ rescue
+ # Whatever
+ end
end
end
end
2  config.ru
View
@@ -2,4 +2,4 @@ require File.expand_path('../config/environment', __FILE__)
ActiveRecord::Base.logger = Logger.new(STDOUT) unless ENV['RACK_ENV'] == 'test'
-run App.new
+run App.app
3  config/application.rb
View
@@ -32,5 +32,8 @@ def database_config
ActiveRecord::Base.establish_connection(database_config)
+Readit::Config.consumer_key = ENV['READABILITY_KEY']
+Readit::Config.consumer_secret = ENV['READABILITY_SECRET']
+
require 'api'
require 'app'
3  config/heroku.yml
View
@@ -1,3 +0,0 @@
-production:
- stack: cedar
- app: rss-reader
5 db/migrate/20130320071316_add_readability_fields_to_users.rb
View
@@ -0,0 +1,5 @@
+class AddReadabilityFieldsToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :readability, :string
+ end
+end
9 db/schema.rb
View
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20130320055139) do
+ActiveRecord::Schema.define(:version => 20130320071316) do
create_table "feeds", :force => true do |t|
t.string "xml_url"
@@ -38,10 +38,11 @@
end
create_table "users", :force => true do |t|
- t.string "email", :default => "", :null => false
+ t.string "email", :default => "", :null => false
t.string "token"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ t.string "readability"
end
add_index "users", ["email"], :name => "index_users_on_email", :unique => true
70 spec/api_spec.rb
View
@@ -4,17 +4,18 @@
include Rack::Test::Methods
def app
- API
+ App.app
end
let(:current_user) { create :user }
before do
Subscription.any_instance.stub(:feed).and_return(Feedzirra::Feed.parse(atom_feed(:github)))
- User.stub(:authenticate).and_return(current_user)
end
describe 'Items' do
+ with_authenticated_user
+
let(:feed) { create :feed, user: current_user}
describe 'GET /items' do
@@ -96,6 +97,8 @@ def app
end
describe 'Subscriptions' do
+ with_authenticated_user
+
describe 'GET /subscriptions' do
it 'responds with a list of items' do
subscription = create :feed, user: current_user
@@ -123,6 +126,8 @@ def app
end
describe 'Import' do
+ with_authenticated_user
+
describe 'POST /import/google_reader' do
it 'imports all subscriptions' do
Importer::GoogleReader.should_receive(:new).and_return(stub(import: nil))
@@ -153,5 +158,66 @@ def app
end
end
end
+
+ describe 'Readability' do
+ with_authenticated_user
+
+ describe 'Authorize' do
+ before do
+ OmniAuth.config.mock_auth[:readability] = OmniAuth::AuthHash.new(
+ provider: 'readability',
+ credentials: {
+ token: 'token',
+ secret: 'secret'
+ }
+ )
+ end
+
+ describe 'GET /users/readability/authorize' do
+ it 'authorizes readability for the current user' do
+ get '/user/readability/authorize'
+ follow_redirect!
+ follow_redirect!
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to eq 'Ok'.to_json
+ end
+ end
+ end
+
+ describe 'PUT /user/readability' do
+ context 'when the user has already authorized readability' do
+ before do
+ current_user.readability.stub(:authorized?).and_return(true)
+ end
+
+ it 'enables readability' do
+ put '/user/readability'
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to eq current_user.entity.to_json
+ end
+ end
+
+ context 'when the user has not authorized readability' do
+ before do
+ current_user.readability.stub(:authorized?).and_return(false)
+ end
+
+ it 'returns an error' do
+ put '/user/readability'
+ expect(last_response.status).to eq 400
+ expect(last_response.body).to eq({ error: 'You need to authorize readability first.'}.to_json)
+ end
+ end
+ end
+
+ describe 'DELETE /user/readability' do
+ it 'disables readability' do
+ delete '/user/readability'
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to eq current_user.entity.to_json
+ end
+ end
+ end
end
+
end
46 spec/models/entry_processor.rb
View
@@ -1,46 +0,0 @@
-require 'spec_helper'
-
-describe EntryProcessor do
- let(:feed) { double('feed') }
- let(:feed_items) { double('feed items') }
- let(:item) { double('item').as_null_object }
- let(:processor) { described_class.new(feed, item) }
-
- before do
- feed.stub(:items).and_return(feed_items)
- end
-
- describe '.create' do
- it 'creates a feed item' do
- feed_items.should_receive(:create)
- processor.create
- end
- end
-
- describe '.update' do
- it 'updates the feed item' do
- item.stub(:guid).and_return(stub(:content => 'http://guid.org'))
- feed_items.stub(:where).with(guid: 'http://guid.org').and_return(stub(:first => item))
- item.should_receive(:update_attributes)
- processor.update
- end
- end
-
- describe '.process' do
- context 'when the item exists' do
- it 'calls update' do
- processor.stub(:existing).and_return(true)
- processor.should_receive(:update)
- processor.process
- end
- end
-
- context 'when the item does not exist' do
- it 'calls create' do
- processor.stub(:existing).and_return(nil)
- processor.should_receive(:create)
- processor.process
- end
- end
- end
-end
69 spec/models/entry_processor_spec.rb
View
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe EntryProcessor do
+ let(:feed) { double('feed') }
+ let(:feed_items) { double('feed items') }
+ let(:entry) { double('entry').as_null_object }
+ let(:processor) { described_class.new(feed, entry) }
+
+ before do
+ feed.stub(:items).and_return(feed_items)
+ end
+
+ describe '.create' do
+ let(:readability) { double('readability') }
+
+ before do
+ feed.stub_chain(:user, :readability).and_return(readability)
+ feed_items.should_receive(:create).and_return(stub(link: 'http://www.google.com'))
+ end
+
+ context 'when readability is disabled' do
+ before do
+ readability.stub(:enabled?).and_return(false)
+ end
+
+ it 'creates a feed item' do
+ processor.create
+ end
+ end
+
+ context 'when readability is enabled' do
+ before do
+ readability.stub(:enabled?).and_return(true)
+ processor.should_receive(:bookmark).with('http://www.google.com')
+ end
+
+ it 'creates a feed item and bookmarks the link on readability' do
+ processor.create
+ end
+ end
+ end
+
+ describe '.update' do
+ it 'updates the feed entry' do
+ entry.stub(:entry_id).and_return('http://guid.org')
+ feed_items.stub(:where).with(guid: 'http://guid.org').and_return(stub(:first => entry))
+ entry.should_receive(:update_attributes)
+ processor.update
+ end
+ end
+
+ describe '.process' do
+ context 'when the item exists' do
+ it 'calls update' do
+ processor.stub(:existing).and_return(true)
+ processor.should_receive(:update)
+ processor.process
+ end
+ end
+
+ context 'when the item does not exist' do
+ it 'calls create' do
+ processor.stub(:existing).and_return(nil)
+ processor.should_receive(:create)
+ processor.process
+ end
+ end
+ end
+end
113 spec/models/user/readability_spec.rb
View
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe User::Readability do
+ let(:hash) { Hash.new }
+ let(:readability) { described_class.new.tap { |o| o.replace(hash) } }
+ subject { readability }
+
+ describe '.token' do
+ subject { readability.token }
+
+ context 'when not present' do
+ it { should be_nil }
+ end
+
+ context 'when present' do
+ let(:hash) { { credentials: { token: 'foobar' } } }
+ it { should eq 'foobar' }
+ end
+ end
+
+ describe '.secret' do
+ subject { readability.secret }
+
+ context 'when not present' do
+ it { should be_nil }
+ end
+
+ context 'when present' do
+ let(:hash) { { credentials: { secret: 'foobar' } } }
+ it { should eq 'foobar' }
+ end
+ end
+
+ describe '.authorized?' do
+ context 'when the token and secret are not present' do
+ it { should_not be_authorized }
+ end
+
+ context 'when token and secret are present' do
+ before do
+ readability.token = 'foo'
+ readability.secret = 'bar'
+ end
+
+ it { should be_authorized }
+ end
+ end
+
+ describe '.enabled?' do
+ context 'when authorized' do
+ before do
+ readability.stub(:authorized?).and_return(true)
+ end
+
+ context 'and enabled' do
+ let(:hash) { { enabled: true } }
+ it { should be_enabled }
+ end
+
+ context 'and enabled' do
+ let(:hash) { { enabled: false } }
+ it { should_not be_enabled }
+ end
+ end
+
+ context 'when not authorized' do
+ before do
+ readability.stub(:authorized?).and_return(false)
+ end
+
+ context 'and enabled' do
+ let(:hash) { { enabled: true } }
+ it { should_not be_enabled }
+ end
+
+ context 'and enabled' do
+ let(:hash) { { enabled: false } }
+ it { should_not be_enabled }
+ end
+ end
+ end
+
+ describe '.enable' do
+ it 'enables readability' do
+ readability.stub(:authorized?).and_return(true)
+ readability.enable
+ expect(readability).to be_enabled
+ end
+ end
+
+ describe '.disable' do
+ let(:hash) { { enabled: true }}
+ it 'disables readability' do
+ readability.stub(:authorized?).and_return(true)
+ readability.disable
+ expect(readability).to_not be_enabled
+ end
+ end
+
+ describe '.client' do
+ subject { readability.client }
+ it { should be_a Readit::API }
+ end
+
+ describe '.bookmark' do
+ let(:url) { 'http://www.google.com' }
+
+ it 'creates a bookmark' do
+ readability.client.should_receive(:bookmark).with(url: url)
+ readability.bookmark(url)
+ end
+ end
+end
7 spec/models/user_spec.rb
View
@@ -9,6 +9,8 @@
it { should have_many(:feeds) }
it { should have_many(:items).through(:feeds) }
+ it { should serialize(:readability).as(User::Readability) }
+
describe '#new' do
it 'initializes a token' do
user = described_class.new(email: 'foo@example.com')
@@ -39,4 +41,9 @@
user.subscribe_to(url)
end
end
+
+ describe '.readability' do
+ subject { user.readability }
+ it { should be_a User::Readability }
+ end
end
11 spec/spec_helper.rb
View
@@ -19,6 +19,14 @@ def atom_feed(feed)
end
end
+module AuthenticationMacros
+ def with_authenticated_user
+ before do
+ User.stub(:authenticate).and_return(current_user)
+ end
+ end
+end
+
Spork.prefork do
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV['RACK_ENV'] = 'test'
@@ -35,6 +43,8 @@ def atom_feed(feed)
WebMock.disable_net_connect! allow_localhost: true
+ OmniAuth.config.test_mode = true
+
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
@@ -78,6 +88,7 @@ def atom_feed(feed)
end
config.include FixtureHelpers
+ config.extend AuthenticationMacros
config.include FactoryGirl::Syntax::Methods
if defined?(::ActiveRecord)
Something went wrong with that request. Please try again.