communicator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +24 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +44 -0
- data/LICENSE +20 -0
- data/README.rdoc +83 -0
- data/Rakefile +100 -0
- data/VERSION +1 -0
- data/communicator.gemspec +106 -0
- data/db/migrate/20101101075419_create_inbound_messages.rb +13 -0
- data/db/migrate/20101101075719_create_outbound_messages.rb +13 -0
- data/lib/communicator/active_record_integration.rb +42 -0
- data/lib/communicator/client.rb +63 -0
- data/lib/communicator/inbound_message.rb +50 -0
- data/lib/communicator/outbound_message.rb +30 -0
- data/lib/communicator/server.rb +62 -0
- data/lib/communicator/tasks.rb +16 -0
- data/lib/communicator.rb +41 -0
- data/test/config.ru +16 -0
- data/test/factories.rb +11 -0
- data/test/helper.rb +47 -0
- data/test/lib/post.rb +7 -0
- data/test/lib/test_server_database/inbound_message.rb +4 -0
- data/test/lib/test_server_database/outbound_message.rb +4 -0
- data/test/lib/test_server_database/post.rb +4 -0
- data/test/migrate/20101101093519_create_posts.rb +13 -0
- data/test/test_client.rb +146 -0
- data/test/test_message_models.rb +68 -0
- data/test/test_server.rb +133 -0
- metadata +249 -0
@@ -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
|
data/lib/communicator.rb
ADDED
@@ -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
data/test/test_client.rb
ADDED
@@ -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
|
data/test/test_server.rb
ADDED
@@ -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
|