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.
- 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
|