communicator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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