apn_on_rails 0.3.1 → 0.4.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.
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