gwong-apn_on_rails 0.4.2

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.
Files changed (60) hide show
  1. data/.rspec +2 -0
  2. data/.specification +80 -0
  3. data/Gemfile +19 -0
  4. data/Gemfile.lock +47 -0
  5. data/LICENSE +21 -0
  6. data/README +179 -0
  7. data/README.textile +209 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/apn_on_rails.gemspec +144 -0
  11. data/autotest/discover.rb +1 -0
  12. data/generators/apn_migrations_generator.rb +31 -0
  13. data/generators/templates/apn_migrations/001_create_apn_devices.rb +13 -0
  14. data/generators/templates/apn_migrations/002_create_apn_notifications.rb +23 -0
  15. data/generators/templates/apn_migrations/003_alter_apn_devices.rb +25 -0
  16. data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
  17. data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
  18. data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
  19. data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
  20. data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
  21. data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
  22. data/generators/templates/apn_migrations/010_alter_apn_notifications.rb +21 -0
  23. data/generators/templates/apn_migrations/011_make_device_token_index_nonunique.rb +11 -0
  24. data/generators/templates/apn_migrations/012_add_launch_notification_to_apn_pull_notifications.rb +9 -0
  25. data/lib/apn_on_rails.rb +4 -0
  26. data/lib/apn_on_rails/apn_on_rails.rb +81 -0
  27. data/lib/apn_on_rails/app/models/apn/app.rb +150 -0
  28. data/lib/apn_on_rails/app/models/apn/base.rb +9 -0
  29. data/lib/apn_on_rails/app/models/apn/device.rb +49 -0
  30. data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
  31. data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
  32. data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
  33. data/lib/apn_on_rails/app/models/apn/notification.rb +93 -0
  34. data/lib/apn_on_rails/app/models/apn/pull_notification.rb +28 -0
  35. data/lib/apn_on_rails/libs/connection.rb +70 -0
  36. data/lib/apn_on_rails/libs/feedback.rb +39 -0
  37. data/lib/apn_on_rails/tasks/apn.rake +30 -0
  38. data/lib/apn_on_rails/tasks/db.rake +19 -0
  39. data/lib/apn_on_rails_tasks.rb +3 -0
  40. data/spec/active_record/setup_ar.rb +19 -0
  41. data/spec/apn_on_rails/app/models/apn/app_spec.rb +226 -0
  42. data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
  43. data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
  44. data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
  45. data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +100 -0
  46. data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
  47. data/spec/apn_on_rails/libs/feedback_spec.rb +43 -0
  48. data/spec/extensions/string.rb +10 -0
  49. data/spec/factories/app_factory.rb +27 -0
  50. data/spec/factories/device_factory.rb +29 -0
  51. data/spec/factories/device_grouping_factory.rb +22 -0
  52. data/spec/factories/group_factory.rb +27 -0
  53. data/spec/factories/group_notification_factory.rb +22 -0
  54. data/spec/factories/notification_factory.rb +22 -0
  55. data/spec/factories/pull_notification_factory.rb +22 -0
  56. data/spec/fixtures/hexa.bin +1 -0
  57. data/spec/fixtures/message_for_sending.bin +0 -0
  58. data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
  59. data/spec/spec_helper.rb +55 -0
  60. metadata +282 -0
@@ -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,100 @@
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
+ latest = APN::PullNotification.latest_since(app.id,Time.now - 1.week)
25
+ puts "latest is #{latest}"
26
+ latest.should == noty1
27
+ end
28
+
29
+ end
30
+
31
+ describe 'latest_since_with_no_date_when_there_is_no_launch_notification' do
32
+ it 'should return the most recent pull notification because no date is given' do
33
+ app = APN::App.first
34
+ noty1 = APN::PullNotification.find(:first, :order => "created_at DESC")
35
+ APN::PullNotification.latest_since(app.id).should == noty1
36
+ end
37
+ end
38
+
39
+ describe 'latest_since_with_no_date_when_there_is_a_launch_notification' do
40
+ it 'should return the launch notification even though there is a more recent notification' do
41
+ app = APN::App.first
42
+ noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true})
43
+ noty_launch.created_at = Time.now - 1.week
44
+ noty_launch.save
45
+ noty_nonlaunch = PullNotificationFactory.create({:app_id => app.id})
46
+ APN::PullNotification.latest_since(app.id).should == noty_launch
47
+ end
48
+ end
49
+
50
+ describe 'older_non_launch_noty_with_newer_launch_noty' do
51
+ it 'should return the older non launch notification even though a newer launch notification exists' do
52
+ APN::PullNotification.all.each { |n| n.destroy }
53
+ app = APN::App.first
54
+ noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true})
55
+ puts "noty_launch id is #{noty_launch.id}"
56
+ noty_nonlaunch = PullNotificationFactory.create({:app_id => app.id})
57
+ noty_nonlaunch.created_at = Time.now - 1.week
58
+ noty_nonlaunch.save
59
+ puts "noty_nonlaunch id is #{noty_nonlaunch.id}"
60
+ APN::PullNotification.latest_since(app.id, Time.now - 2.weeks).should == noty_nonlaunch
61
+ end
62
+ end
63
+
64
+ describe 'all_since_date_with_date_given' do
65
+ it 'should return all the non-launch notifications after the given date but not the ones before it' do
66
+ APN::PullNotification.all.each { |n| n.destroy }
67
+ app = APN::App.first
68
+ noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true})
69
+ noty_launch.created_at = Time.now - 2.weeks
70
+ noty_launch.save
71
+ old_noty = PullNotificationFactory.create({:app_id => app.id})
72
+ old_noty.created_at = Time.now - 2.weeks
73
+ old_noty.save
74
+ new_noty_one = PullNotificationFactory.create({:app_id => app.id})
75
+ new_noty_one.created_at = Time.now - 1.day
76
+ new_noty_one.save
77
+ new_noty_two = PullNotificationFactory.create({:app_id => app.id})
78
+ APN::PullNotification.all_since(app.id, Time.now - 1.week).should == [new_noty_two,new_noty_one]
79
+ end
80
+ end
81
+
82
+ describe 'all_since_with_no_since_date_given' do
83
+ it 'should return all of the non-launch notifications' do
84
+ APN::PullNotification.all.each { |n| n.destroy }
85
+ app = APN::App.first
86
+ noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true})
87
+ noty_launch.created_at = Time.now - 2.weeks
88
+ noty_launch.save
89
+ old_noty = PullNotificationFactory.create({:app_id => app.id})
90
+ old_noty.created_at = Time.now - 2.weeks
91
+ old_noty.save
92
+ new_noty_one = PullNotificationFactory.create({:app_id => app.id})
93
+ new_noty_one.created_at = Time.now - 1.day
94
+ new_noty_one.save
95
+ new_noty_two = PullNotificationFactory.create({:app_id => app.id})
96
+ APN::PullNotification.all_since(app.id, Time.now - 3.weeks).should == [new_noty_two,new_noty_one,old_noty]
97
+ end
98
+ end
99
+
100
+ 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,43 @@
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(:unpack).with('N1n1H140').and_return([@time.to_i, 12388, @device.token.delete(' ')])
14
+
15
+ @ssl_mock = mock('ssl_mock')
16
+ @ssl_mock.should_receive(:read).with(38).twice.and_return(@data_mock, nil)
17
+ @sock_mock = mock('sock_mock')
18
+ end
19
+
20
+ it 'should an Array of devices that need to be processed' do
21
+ APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
22
+
23
+ devices = APN::Feedback.devices(@cert)
24
+ devices.size.should == 1
25
+ r_device = devices.first
26
+ r_device.token.should == @device.token
27
+ r_device.feedback_at.to_s.should == @time.to_s
28
+ end
29
+
30
+ it 'should yield up each device' do
31
+ APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
32
+ lambda {
33
+ APN::Feedback.devices(@cert) do |r_device|
34
+ r_device.token.should == @device.token
35
+ r_device.feedback_at.to_s.should == @time.to_s
36
+ raise BlockRan.new
37
+ end
38
+ }.should raise_error(BlockRan)
39
+ end
40
+
41
+ end
42
+
43
+ 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
+ app = AppFactory.new(options)
13
+ app.save
14
+ return app
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