gwong-apn_on_rails 0.4.2

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