apn_on_rails 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +16 -0
  2. data/.rspec +2 -0
  3. data/.specification +80 -0
  4. data/Gemfile +18 -0
  5. data/Gemfile.lock +45 -0
  6. data/README +51 -9
  7. data/README.textile +198 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/apn_on_rails.gemspec +110 -0
  11. data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
  12. data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
  13. data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
  14. data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
  15. data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
  16. data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
  17. data/lib/apn_on_rails/apn_on_rails.rb +22 -3
  18. data/lib/apn_on_rails/app/models/apn/app.rb +115 -0
  19. data/lib/apn_on_rails/app/models/apn/device.rb +3 -1
  20. data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
  21. data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
  22. data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
  23. data/lib/apn_on_rails/app/models/apn/notification.rb +6 -30
  24. data/lib/apn_on_rails/app/models/apn/pull_notification.rb +15 -0
  25. data/lib/apn_on_rails/libs/connection.rb +2 -1
  26. data/lib/apn_on_rails/libs/feedback.rb +6 -18
  27. data/lib/apn_on_rails/tasks/apn.rake +13 -4
  28. data/spec/active_record/setup_ar.rb +19 -0
  29. data/spec/apn_on_rails/app/models/apn/app_spec.rb +178 -0
  30. data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
  31. data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
  32. data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
  33. data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +37 -0
  34. data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
  35. data/spec/apn_on_rails/libs/feedback_spec.rb +45 -0
  36. data/spec/extensions/string.rb +10 -0
  37. data/spec/factories/app_factory.rb +27 -0
  38. data/spec/factories/device_factory.rb +29 -0
  39. data/spec/factories/device_grouping_factory.rb +22 -0
  40. data/spec/factories/group_factory.rb +27 -0
  41. data/spec/factories/group_notification_factory.rb +22 -0
  42. data/spec/factories/notification_factory.rb +22 -0
  43. data/spec/factories/pull_notification_factory.rb +22 -0
  44. data/spec/fixtures/hexa.bin +1 -0
  45. data/spec/fixtures/message_for_sending.bin +0 -0
  46. data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
  47. data/spec/spec_helper.rb +55 -0
  48. metadata +214 -24
@@ -0,0 +1,60 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
2
+
3
+ describe APN::Device do
4
+
5
+ describe 'token' do
6
+
7
+ it 'should be unique' do
8
+ device = DeviceFactory.new(:token => APN::Device.first.token)
9
+ device.should_not be_valid
10
+ device.errors['token'].should include('has already been taken')
11
+
12
+ device = DeviceFactory.new(:token => device.token.succ)
13
+ device.should be_valid
14
+ end
15
+
16
+ it 'should get cleansed if it contains brackets' do
17
+ token = DeviceFactory.random_token
18
+ device = DeviceFactory.new(:token => "<#{token}>")
19
+ device.token.should == token
20
+ device.token.should_not == "<#{token}>"
21
+ end
22
+
23
+ it 'should be in the correct pattern' do
24
+ device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
25
+ device.should be_valid
26
+ device.token = '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6'
27
+ device.should_not be_valid
28
+ device.token = '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7!!'
29
+ device.should_not be_valid
30
+ end
31
+
32
+ end
33
+
34
+ describe 'to_hexa' do
35
+
36
+ it 'should convert the text string to hexadecimal' do
37
+ device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
38
+ device.to_hexa.should == fixture_value('hexa.bin')
39
+ end
40
+
41
+ end
42
+
43
+ describe 'before_save' do
44
+
45
+ it 'should set the last_registered_at date to Time.now if nil' do
46
+ time = Time.now
47
+ Time.stub(:now).and_return(time)
48
+ device = DeviceFactory.create
49
+ device.last_registered_at.should_not be_nil
50
+ device.last_registered_at.to_s.should == time.to_s
51
+
52
+ ago = 1.week.ago
53
+ device = DeviceFactory.create(:last_registered_at => ago)
54
+ device.last_registered_at.should_not be_nil
55
+ device.last_registered_at.to_s.should == ago.to_s
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,66 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
2
+
3
+ describe APN::GroupNotification do
4
+
5
+ describe 'alert' do
6
+
7
+ it 'should trim the message to 150 characters' do
8
+ noty = APN::GroupNotification.new
9
+ noty.alert = 'a' * 200
10
+ noty.alert.should == ('a' * 147) + '...'
11
+ end
12
+
13
+ end
14
+
15
+ describe 'apple_hash' do
16
+
17
+ it 'should return a hash of the appropriate params for Apple' do
18
+ noty = APN::GroupNotification.first
19
+ noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"},"typ" => "1"}
20
+ noty.custom_properties = nil
21
+ noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}}
22
+ noty.badge = nil
23
+ noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff", "alert" => "Hello!"}}
24
+ noty.alert = nil
25
+ noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff"}}
26
+ noty.sound = nil
27
+ noty.apple_hash.should == {"aps" => {}}
28
+ noty.sound = true
29
+ noty.apple_hash.should == {"aps" => {"sound" => "1.aiff"}}
30
+ end
31
+
32
+ end
33
+
34
+ describe 'to_apple_json' do
35
+
36
+ it 'should return the necessary JSON for Apple' do
37
+ noty = APN::GroupNotification.first
38
+ noty.to_apple_json.should == %{{"typ":"1","aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}}
39
+ end
40
+
41
+ end
42
+
43
+ describe 'message_for_sending' do
44
+
45
+ it 'should create a binary message to be sent to Apple' do
46
+ noty = APN::GroupNotification.first
47
+ noty.custom_properties = nil
48
+ device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
49
+ noty.message_for_sending(device).should == fixture_value('message_for_sending.bin')
50
+ end
51
+
52
+ it 'should raise an APN::Errors::ExceededMessageSizeError if the message is too big' do
53
+ app = AppFactory.create
54
+ device = DeviceFactory.create({:app_id => app.id})
55
+ group = GroupFactory.create({:app_id => app.id})
56
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
57
+ noty = GroupNotificationFactory.new(:group_id => group.id, :sound => true, :badge => nil)
58
+ noty.send(:write_attribute, 'alert', 'a' * 183)
59
+ lambda {
60
+ noty.message_for_sending(device)
61
+ }.should raise_error(APN::Errors::ExceededMessageSizeError)
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
2
+
3
+ describe APN::Notification do
4
+
5
+ describe 'alert' do
6
+
7
+ it 'should trim the message to 150 characters' do
8
+ noty = APN::Notification.new
9
+ noty.alert = 'a' * 200
10
+ noty.alert.should == ('a' * 147) + '...'
11
+ end
12
+
13
+ end
14
+
15
+ describe 'apple_hash' do
16
+
17
+ it 'should return a hash of the appropriate params for Apple' do
18
+ noty = APN::Notification.first
19
+ noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"},"typ" => "1"}
20
+ noty.custom_properties = nil
21
+ noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}}
22
+ noty.badge = nil
23
+ noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff", "alert" => "Hello!"}}
24
+ noty.alert = nil
25
+ noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff"}}
26
+ noty.sound = nil
27
+ noty.apple_hash.should == {"aps" => {}}
28
+ noty.sound = true
29
+ noty.apple_hash.should == {"aps" => {"sound" => "1.aiff"}}
30
+ end
31
+
32
+ end
33
+
34
+ describe 'to_apple_json' do
35
+
36
+ it 'should return the necessary JSON for Apple' do
37
+ noty = APN::Notification.first
38
+ noty.to_apple_json.should == %{{"typ":"1","aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}}
39
+ end
40
+
41
+ end
42
+
43
+ describe 'message_for_sending' do
44
+
45
+ it 'should create a binary message to be sent to Apple' do
46
+ noty = APN::Notification.first
47
+ noty.custom_properties = nil
48
+ noty.device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
49
+ noty.message_for_sending.should == fixture_value('message_for_sending.bin')
50
+ end
51
+
52
+ it 'should raise an APN::Errors::ExceededMessageSizeError if the message is too big' do
53
+ noty = NotificationFactory.new(:device_id => DeviceFactory.create, :sound => true, :badge => nil)
54
+ noty.send(:write_attribute, 'alert', 'a' * 183)
55
+ lambda {
56
+ noty.message_for_sending
57
+ }.should raise_error(APN::Errors::ExceededMessageSizeError)
58
+ end
59
+
60
+ end
61
+
62
+ describe 'send_notifications' do
63
+
64
+ it 'should warn the user the method is deprecated and call the corresponding method on APN::App' do
65
+ ActiveSupport::Deprecation.should_receive(:warn)
66
+ APN::App.should_receive(:send_notifications)
67
+ APN::Notification.send_notifications
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
2
+
3
+ describe APN::PullNotification do
4
+
5
+ describe 'latest_since_when_already_seen_latest' do
6
+
7
+ it 'should return nothing because since date is after the latest pull notification' do
8
+ app = APN::App.first
9
+ noty1 = PullNotificationFactory.create({:app_id => app.id})
10
+ noty1.created_at = Time.now - 1.week
11
+ noty1.save
12
+ APN::PullNotification.latest_since(app.id,Time.now).should == nil
13
+ end
14
+
15
+ end
16
+
17
+ describe 'latest_since_when_have_not_seen_latest' do
18
+
19
+ it 'should return the most recent pull notification because it has not yet been seen' do
20
+ app = APN::App.first
21
+ noty1 = PullNotificationFactory.create({:app_id => app.id})
22
+ noty1.created_at = Time.now + 1.week
23
+ noty1.save
24
+ APN::PullNotification.latest_since(app.id,Time.now - 1.week).should == noty1
25
+ end
26
+
27
+ end
28
+
29
+ describe 'latest_since_with_no_date' do
30
+ it 'should return the most recent pull notification because no date is given' do
31
+ app = APN::App.first
32
+ noty1 = APN::PullNotification.find(:first, :order => "created_at DESC")
33
+ APN::PullNotification.latest_since(app.id).should == noty1
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe APN::Connection do
4
+
5
+ describe 'open_for_delivery' do
6
+
7
+ it 'should create a connection to Apple, yield it, and then close' do
8
+ rsa_mock = mock('rsa_mock')
9
+ OpenSSL::PKey::RSA.should_receive(:new).and_return(rsa_mock)
10
+
11
+ cert_mock = mock('cert_mock')
12
+ OpenSSL::X509::Certificate.should_receive(:new).and_return(cert_mock)
13
+
14
+ ctx_mock = mock('ctx_mock')
15
+ ctx_mock.should_receive(:key=).with(rsa_mock)
16
+ ctx_mock.should_receive(:cert=).with(cert_mock)
17
+ OpenSSL::SSL::SSLContext.should_receive(:new).and_return(ctx_mock)
18
+
19
+ tcp_mock = mock('tcp_mock')
20
+ tcp_mock.should_receive(:close)
21
+ TCPSocket.should_receive(:new).with('gateway.sandbox.push.apple.com', 2195).and_return(tcp_mock)
22
+
23
+ ssl_mock = mock('ssl_mock')
24
+ ssl_mock.should_receive(:sync=).with(true)
25
+ ssl_mock.should_receive(:connect)
26
+ ssl_mock.should_receive(:write).with('message-0')
27
+ ssl_mock.should_receive(:write).with('message-1')
28
+ ssl_mock.should_receive(:close)
29
+ OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_mock, ctx_mock).and_return(ssl_mock)
30
+
31
+ APN::Connection.open_for_delivery do |conn, sock|
32
+ conn.write('message-0')
33
+ conn.write('message-1')
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe APN::Feedback do
4
+
5
+ describe 'devices' do
6
+
7
+ before(:each) do
8
+ @time = Time.now
9
+ @device = DeviceFactory.create
10
+ @cert = mock('cert_mock')
11
+
12
+ @data_mock = mock('data_mock')
13
+ @data_mock.should_receive(:strip!)
14
+ @data_mock.should_receive(:unpack).with('N1n1H140').and_return([@time.to_i, 12388, @device.token.delete(' ')])
15
+
16
+ @ssl_mock = mock('ssl_mock')
17
+ @sock_mock = mock('sock_mock')
18
+ @sock_mock.should_receive(:gets).twice.and_return(@data_mock, nil)
19
+
20
+ end
21
+
22
+ it 'should an Array of devices that need to be processed' do
23
+ APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
24
+
25
+ devices = APN::Feedback.devices(@cert)
26
+ devices.size.should == 1
27
+ r_device = devices.first
28
+ r_device.token.should == @device.token
29
+ r_device.feedback_at.to_s.should == @time.to_s
30
+ end
31
+
32
+ it 'should yield up each device' do
33
+ APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
34
+ lambda {
35
+ APN::Feedback.devices(@cert) do |r_device|
36
+ r_device.token.should == @device.token
37
+ r_device.feedback_at.to_s.should == @time.to_s
38
+ raise BlockRan.new
39
+ end
40
+ }.should raise_error(BlockRan)
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,10 @@
1
+ class String
2
+
3
+ def self.randomize(length = 10)
4
+ chars = ("A".."H").to_a + ("J".."N").to_a + ("P".."T").to_a + ("W".."Z").to_a + ("3".."9").to_a
5
+ newpass = ""
6
+ 1.upto(length) { |i| newpass << chars[rand(chars.size-1)] }
7
+ return newpass.upcase
8
+ end
9
+
10
+ end
@@ -0,0 +1,27 @@
1
+ module AppFactory
2
+
3
+ class << self
4
+
5
+ def new(options = {})
6
+ options = {:apn_dev_cert => AppFactory.random_cert,
7
+ :apn_prod_cert => AppFactory.random_cert}.merge(options)
8
+ return APN::App.new(options)
9
+ end
10
+
11
+ def create(options = {})
12
+ device = AppFactory.new(options)
13
+ device.save
14
+ return device
15
+ end
16
+
17
+ def random_cert
18
+ tok = []
19
+ tok << String.randomize(50)
20
+ tok.join('').downcase
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ AppFactory.create
@@ -0,0 +1,29 @@
1
+ module DeviceFactory
2
+
3
+ class << self
4
+
5
+ def new(options = {})
6
+ app = APN::App.first
7
+ options = {:token => DeviceFactory.random_token, :app_id => app.id}.merge(options)
8
+ return APN::Device.new(options)
9
+ end
10
+
11
+ def create(options = {})
12
+ device = DeviceFactory.new(options)
13
+ device.save
14
+ return device
15
+ end
16
+
17
+ def random_token
18
+ tok = []
19
+ 8.times do
20
+ tok << String.randomize(8)
21
+ end
22
+ tok.join(' ').downcase
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ DeviceFactory.create
@@ -0,0 +1,22 @@
1
+ module DeviceGroupingFactory
2
+
3
+ class << self
4
+
5
+ def new(options = {})
6
+ device = APN::Device.first
7
+ group = APN::Group.first
8
+ options = {:device_id => device.id, :group_id => group.id}.merge(options)
9
+ return APN::DeviceGrouping.new(options)
10
+ end
11
+
12
+ def create(options = {})
13
+ device_grouping = DeviceGroupingFactory.new(options)
14
+ device_grouping.save
15
+ return device_grouping
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ DeviceGroupingFactory.create
@@ -0,0 +1,27 @@
1
+ module GroupFactory
2
+
3
+ class << self
4
+
5
+ def new(options = {})
6
+ app = APN::App.first
7
+ options = {:app_id => app.id, :name => GroupFactory.random_name}.merge(options)
8
+ return APN::Group.new(options)
9
+ end
10
+
11
+ def create(options = {})
12
+ group = GroupFactory.new(options)
13
+ group.save
14
+ return group
15
+ end
16
+
17
+ def random_name
18
+ tok = []
19
+ tok << String.randomize(8)
20
+ tok.join(' ').downcase
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ GroupFactory.create