communicator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ namespace :communicator do
2
+ desc "Copies all communicator gem migrations to your rails app"
3
+ task :update_migrations do
4
+ fail "Only available in Rails context!" unless defined?(Rails)
5
+ require 'fileutils'
6
+ Dir[File.join(File.dirname(__FILE__), '..', '..', 'db/migrate/*.rb')].each do |migration|
7
+ target = File.join(Rails.root, 'db/migrate', File.basename(migration))
8
+ if File.exist?(target)
9
+ puts "Skipped #{File.basename(migration)}, already present!"
10
+ else
11
+ FileUtils.cp(migration, target)
12
+ puts "Copied #{File.basename(migration)} to your Rails apps' db/migrate folder. Please do rake db:migrate"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup(:default)
4
+ require 'json'
5
+ require 'active_record'
6
+
7
+ module Communicator
8
+ # Error to be raised when no receiver can be found for a message that is to be processed
9
+ class ReceiverUnknown < StandardError; end;
10
+
11
+ # Error to be raised when no credentials were given
12
+ class MissingCredentials < StandardError; end;
13
+
14
+ class << self
15
+ # Hash containing all receivers
16
+ def receivers
17
+ @recevers ||= {}.with_indifferent_access
18
+ end
19
+
20
+ # Register a given class as a receiver from source (underscored name). Will then
21
+ # mix in the instance methods from Communicator::ActiveRecord::InstanceMethods so
22
+ # message processing and publishing functionality is included
23
+ def register_receiver(target, source)
24
+ receivers[source] = target
25
+ target.send(:include, Communicator::ActiveRecordIntegration::InstanceMethods)
26
+ end
27
+
28
+ # Tries to find the receiver for given source, raising Communicator::ReceiverUnknown
29
+ # on failure
30
+ def receiver_for(source)
31
+ return receivers[source] if receivers[source]
32
+ raise Communicator::ReceiverUnknown.new("No receiver registered for '#{source}'")
33
+ end
34
+ end
35
+ end
36
+
37
+ require 'communicator/server'
38
+ require 'communicator/client'
39
+ require 'communicator/active_record_integration'
40
+ require 'communicator/outbound_message'
41
+ require 'communicator/inbound_message'
data/test/config.ru ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler'
2
+ Bundler.setup(:default, :development)
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+
7
+ require 'active_record'
8
+ require 'communicator'
9
+
10
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "db/test_server.sqlite3")
11
+ require 'lib/post'
12
+
13
+ Communicator::Server.username = 'testuser'
14
+ Communicator::Server.password = 'pwd'
15
+
16
+ run Communicator::Server
data/test/factories.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'factory_girl'
2
+ Factory.sequence(:inbound_record_id) {|i| i}
3
+ Factory.sequence(:outbound_record_id) {|i| i}
4
+
5
+ Factory.define(:inbound_message, :class => Communicator::InboundMessage) do |f|
6
+ f.body { {:post => {:id => Factory.next(:inbound_record_id), :title => 'foo', :body => 'bar'}}.to_json }
7
+ end
8
+
9
+ Factory.define(:outbound_message, :class => Communicator::OutboundMessage) do |f|
10
+ f.body { {:post => {:id => Factory.next(:outbound_record_id), :title => 'foo', :body => 'bar'}}.to_json }
11
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup(:default, :development)
4
+ ENV['RACK_ENV'] = 'test'
5
+ require 'test/unit'
6
+ require 'rack/test'
7
+ require 'active_record'
8
+ require 'shoulda'
9
+ require 'shoulda/active_record'
10
+
11
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
12
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
13
+ require 'communicator'
14
+ require 'factories'
15
+
16
+ # Connect to client test database
17
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "db/test_client.sqlite3")
18
+ require 'lib/post'
19
+
20
+ # Connect to server database too so we can peek into what's happening over there
21
+ class TestServerDatabase < ActiveRecord::Base
22
+ establish_connection(:adapter => 'sqlite3', :database => "db/test_server.sqlite3")
23
+ end
24
+ require 'lib/test_server_database/post'
25
+ require 'lib/test_server_database/inbound_message'
26
+ require 'lib/test_server_database/outbound_message'
27
+
28
+ class Test::Unit::TestCase
29
+ def setup
30
+ # Reset communicator credentials every time
31
+ Communicator::Client.username = nil
32
+ Communicator::Client.password = nil
33
+ Communicator::Client.base_uri nil
34
+
35
+ Communicator::Server.username = nil
36
+ Communicator::Server.password = nil
37
+
38
+ # Purge the databases every time...
39
+ Communicator::InboundMessage.delete_all
40
+ Communicator::OutboundMessage.delete_all
41
+ Post.delete_all
42
+
43
+ TestServerDatabase::InboundMessage.delete_all
44
+ TestServerDatabase::OutboundMessage.delete_all
45
+ TestServerDatabase::Post.delete_all
46
+ end
47
+ end
data/test/lib/post.rb ADDED
@@ -0,0 +1,7 @@
1
+ # A simple class for publishing and receiving messages
2
+ class Post < ActiveRecord::Base
3
+ receives_from :post
4
+ after_save do |p|
5
+ p.publish unless p.updated_from_message
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # Basic class for introspection of what is happening on the server side
2
+ class TestServerDatabase::InboundMessage < TestServerDatabase
3
+ set_table_name "inbound_messages"
4
+ end
@@ -0,0 +1,4 @@
1
+ # Basic class for introspection of what is happening on the server side
2
+ class TestServerDatabase::OutboundMessage < TestServerDatabase
3
+ set_table_name "outbound_messages"
4
+ end
@@ -0,0 +1,4 @@
1
+ # Basic class for introspection of what is happening on the server side
2
+ class TestServerDatabase::Post < TestServerDatabase
3
+ set_table_name "posts"
4
+ end
@@ -0,0 +1,13 @@
1
+ class CreatePosts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :posts do |t|
4
+ t.string :title, :null => false
5
+ t.text :body, :null => false
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :posts
12
+ end
13
+ end
@@ -0,0 +1,146 @@
1
+ require 'helper'
2
+
3
+ #
4
+ # Unit tests for client and it's interaction with the real server running in background
5
+ # on port 20359. For introspection purposes, a simple second database connection
6
+ # has been established as TestServerDatabase
7
+ #
8
+ class TestClient < Test::Unit::TestCase
9
+ context "Without auth credentials configured" do
10
+ context "PUSH" do
11
+ should "raise an exception" do
12
+ assert_raise Communicator::MissingCredentials do
13
+ Communicator::Client.push
14
+ end
15
+ end
16
+ end
17
+
18
+ context "PULL" do
19
+ should "raise an exception" do
20
+ assert_raise Communicator::MissingCredentials do
21
+ Communicator::Client.pull
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ context "with server configured" do
28
+ setup do
29
+ Communicator::Client.username = 'testuser'
30
+ Communicator::Client.password = 'pwd'
31
+ Communicator::Client.base_uri 'localhost:20359'
32
+ end
33
+
34
+ context "PUSH" do
35
+ setup { Communicator::Client.push }
36
+
37
+ should "not have any inbound messages in server" do
38
+ assert_equal 0, TestServerDatabase::InboundMessage.count
39
+ end
40
+ end
41
+
42
+ context "PULL" do
43
+ setup { Communicator::Client.pull }
44
+
45
+ should "not have any inbound messages locally" do
46
+ assert_equal 0, Communicator::InboundMessage.count
47
+ end
48
+ end
49
+
50
+ context "after creating a local Post" do
51
+ setup do
52
+ @post = Post.create!(:title => 'foo', :body => 'local post')
53
+ end
54
+
55
+ should "have created an outbound message locally" do
56
+ assert_equal 'local post', Communicator::OutboundMessage.first.message_content["post"]["body"]
57
+ assert_nil Communicator::OutboundMessage.first.delivered_at
58
+ end
59
+
60
+ context "after PUSH" do
61
+ setup { Communicator::Client.push }
62
+
63
+ should "have flagged the outbound message as delivered" do
64
+ assert_equal 0, Communicator::OutboundMessage.undelivered.count
65
+ end
66
+
67
+ should "have created the corresponding inbound message at remote" do
68
+ assert_equal 1, TestServerDatabase::InboundMessage.count
69
+ assert msg = TestServerDatabase::InboundMessage.first
70
+ assert_equal 'local post', JSON.parse(msg.body)["post"]["body"]
71
+ assert_equal 'foo', JSON.parse(msg.body)["post"]["title"]
72
+ assert msg.processed_at > 3.seconds.ago
73
+ end
74
+
75
+ should "have created the corresponding Post at remote" do
76
+ assert_equal 1, TestServerDatabase::Post.count
77
+ assert post = TestServerDatabase::Post.first
78
+ assert_equal 'local post', post.body
79
+ assert_equal 'foo', post.title
80
+ end
81
+
82
+ should "not have created outbound message at remote" do
83
+ assert_equal 0, TestServerDatabase::OutboundMessage.count
84
+ end
85
+
86
+ context "when i try to PUSH the same message again" do
87
+ setup do
88
+ @post.title = 'changed'
89
+ Communicator::OutboundMessage.first.update_attribute(:body, @post.to_json)
90
+ Communicator::OutboundMessage.first.update_attribute(:delivered_at, nil)
91
+ Communicator::Client.push
92
+ end
93
+
94
+ should "not have updated the remote message" do
95
+ assert_equal 1, TestServerDatabase::InboundMessage.count
96
+ assert_equal 'foo', JSON.parse(TestServerDatabase::InboundMessage.first.body)["post"]["title"]
97
+ end
98
+ end
99
+
100
+ context "when an update message is created at remote" do
101
+ setup do
102
+ @remote_msg = TestServerDatabase::OutboundMessage.create!(:body => {:post => {:id => @post.id, :title => 'new title', :body => 'remote body'}}.to_json)
103
+ end
104
+
105
+ context "a PULL" do
106
+ setup do
107
+ Communicator::Client.pull
108
+ end
109
+
110
+ should "result in the local post getting updated" do
111
+ @post.reload
112
+ assert_equal 'new title', @post.title
113
+ assert_equal 'remote body', @post.body
114
+ end
115
+
116
+ should "have created the corresponding inbound message" do
117
+ assert_equal 1, Communicator::InboundMessage.count
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ context "when an update message is created at remote" do
125
+ setup do
126
+ @remote_msg = TestServerDatabase::OutboundMessage.create!(:body => {:post => {:id => 25, :title => 'new title', :body => 'remote body'}}.to_json)
127
+ end
128
+
129
+ context "a PULL" do
130
+ setup do
131
+ Communicator::Client.pull
132
+ end
133
+
134
+ should "result in a local post getting created" do
135
+ post = Post.find(25)
136
+ assert_equal 'new title', post.title
137
+ assert_equal 'remote body', post.body
138
+ end
139
+
140
+ should "have created the corresponding inbound message" do
141
+ assert_equal 1, Communicator::InboundMessage.count
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,68 @@
1
+ require 'helper'
2
+
3
+ class TestMessageModels < Test::Unit::TestCase
4
+ context "A new inbound message" do
5
+ subject { Communicator::InboundMessage.new }
6
+ should_validate_presence_of :body
7
+ end
8
+
9
+ context "A new outbound message" do
10
+ subject { Communicator::OutboundMessage.new }
11
+ should_validate_presence_of :body
12
+ end
13
+
14
+ context "With a couple inbound messages existing" do
15
+ setup { 5.times { Factory.create(:inbound_message) } }
16
+
17
+ should "return an appropriate last_id" do
18
+ assert_equal Communicator::InboundMessage.last_id, Communicator::InboundMessage.all.map(&:id).sort.last
19
+ end
20
+ end
21
+
22
+ context "A inbound message for a post that does not exist locally yet" do
23
+ setup do
24
+ @message = Factory.create(:inbound_message)
25
+ @post = ::Post.find(JSON.parse(@message.body)["post"]["id"])
26
+ end
27
+
28
+ should "have all attributes of the post matching those in the json body" do
29
+ JSON.parse(@message.body)["post"].each do |attr_name, value|
30
+ assert_equal @post.send(attr_name), value
31
+ end
32
+ end
33
+
34
+ context "after another Inbound Message for the same Post" do
35
+ setup do
36
+ @message = Factory.create(:inbound_message, :body => {
37
+ :post => { :id => @post.id,
38
+ :title => 'new title',
39
+ :body => 'new malarkey'} }.to_json)
40
+ @post.reload
41
+ end
42
+
43
+ should "have updated the posts title and body" do
44
+ assert_equal @post.title, 'new title'
45
+ assert_equal @post.body, 'new malarkey'
46
+ end
47
+
48
+ should "not have created any Outbound messages" do
49
+ assert_equal 0, Communicator::OutboundMessage.count
50
+ end
51
+
52
+ context "after an update to the post" do
53
+ setup do
54
+ @post.title = 'changed locally'
55
+ @post.save!
56
+ end
57
+
58
+ should "Have created an outbound message" do
59
+ assert_equal 1, Communicator::OutboundMessage.count
60
+ end
61
+
62
+ should "have an outbound message with correct title" do
63
+ assert_equal 'changed locally', JSON.parse(Communicator::OutboundMessage.first.body)["post"]["title"]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,133 @@
1
+ require 'helper'
2
+ require 'base64'
3
+
4
+ # Unit tests for the server component using rack test in isolation
5
+ # Please note that this is using the CLIENT database, since we are testing locally
6
+ # as opposed to the tests of client/server interaction, which use the Client and Server
7
+ # DB
8
+ #
9
+ class TestServer < Test::Unit::TestCase
10
+ include Rack::Test::Methods
11
+
12
+ def app
13
+ Communicator::Server.username = 'someguy'
14
+ Communicator::Server.password = 'password'
15
+ Communicator::Server
16
+ end
17
+
18
+ context "Unauthorized" do
19
+ context "GET /messages.json" do
20
+ setup { get '/messages.json' }
21
+ should("return forbidden status") { assert_equal 401, last_response.status}
22
+ end
23
+
24
+ context "POST /messages.json" do
25
+ setup { post '/messages.json' }
26
+ should("return forbidden status") { assert_equal 401, last_response.status}
27
+ end
28
+ end
29
+
30
+ context "Authorized" do
31
+ context "GET /messages.json" do
32
+ context "without from_id" do
33
+ setup { get '/messages.json', {}, auth_header('someguy', 'password') }
34
+ should("return rejected status") { assert_equal 409, last_response.status}
35
+ end
36
+
37
+ context "with from_id" do
38
+ setup { get '/messages.json', {:from_id => 1}, auth_header('someguy', 'password') }
39
+ should("return success status") { assert_equal 200, last_response.status }
40
+ should("return empty json array") { assert_equal "[]", last_response.body }
41
+ end
42
+
43
+ context "with existing OutboundMessages" do
44
+ setup { 5.times { Factory.create(:outbound_message) } }
45
+
46
+ should "have 5 items in undelivered named_scope" do
47
+ assert_equal 5, Communicator::OutboundMessage.undelivered.count
48
+ end
49
+
50
+ context "and proper from_id" do
51
+ setup do
52
+ get '/messages.json', {:from_id => 1}, auth_header('someguy', 'password')
53
+ @json = JSON.parse(last_response.body)
54
+ end
55
+
56
+ should "have rendered successfully" do
57
+ assert_equal 200, last_response.status
58
+ end
59
+
60
+ should "have returned 5 messages" do
61
+ assert_equal 5, @json.length
62
+ end
63
+
64
+ should "have proper representations of all messages" do
65
+ @json.each do |json|
66
+ message = Communicator::OutboundMessage.find(json["id"])
67
+ assert_equal message.body, json["body"]
68
+ end
69
+ end
70
+
71
+ should "have flagged all outbound messages as delivered" do
72
+ Communicator::OutboundMessage.all.each do |msg|
73
+ assert msg.delivered_at > 5.seconds.ago
74
+ end
75
+ end
76
+
77
+ should "have no outbound in undelivered named_scope" do
78
+ assert_equal 0, Communicator::OutboundMessage.undelivered.count
79
+ end
80
+
81
+ context "another GET with updated from_id" do
82
+ setup do
83
+ get '/messages.json', {:from_id => Communicator::OutboundMessage.first(:order => 'id DESC').id+1}, auth_header('someguy', 'password')
84
+ end
85
+
86
+ should "have rendered empty json array" do
87
+ assert_equal "[]", last_response.body
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ context "POST /messages.json" do
95
+ context "without body" do
96
+ setup { post '/messages.json', {}, auth_header('someguy', 'password') }
97
+ should("return rejected status") { assert_equal 409, last_response.status}
98
+ end
99
+
100
+ context "with empty array in body" do
101
+ setup { post '/messages.json', "[]", auth_header('someguy', 'password') }
102
+ should("return accepted status") { assert_equal 202, last_response.status}
103
+ end
104
+
105
+ context "with 1 post in body" do
106
+ setup do
107
+ assert Communicator::InboundMessage.count == 0, "Should have no inbound"
108
+ post '/messages.json', [{:id => 1, :body => {:post => {:id => 1, :title => 'foo', :body => 'bar'}}.to_json}].to_json, auth_header('someguy', 'password')
109
+ end
110
+ should("return accepted status") { assert_equal 202, last_response.status }
111
+ should "have created the inbound message" do
112
+ assert_equal 1, Communicator::InboundMessage.count
113
+ end
114
+ should "have created the corresponding post" do
115
+ assert post = Post.find(1)
116
+ assert_equal 'foo', post.title
117
+ assert_equal 'bar', post.body
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def auth_header(username, password)
126
+ {'HTTP_AUTHORIZATION' => encode_credentials(username, password)}
127
+ end
128
+
129
+ def encode_credentials(username, password)
130
+ "Basic " + Base64.encode64("#{username}:#{password}")
131
+ end
132
+
133
+ end