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.
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.specification +80 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +45 -0
- data/README +51 -9
- data/README.textile +198 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/apn_on_rails.gemspec +110 -0
- data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
- data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
- data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
- data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
- data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
- data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
- data/lib/apn_on_rails/apn_on_rails.rb +22 -3
- data/lib/apn_on_rails/app/models/apn/app.rb +115 -0
- data/lib/apn_on_rails/app/models/apn/device.rb +3 -1
- data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
- data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
- data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
- data/lib/apn_on_rails/app/models/apn/notification.rb +6 -30
- data/lib/apn_on_rails/app/models/apn/pull_notification.rb +15 -0
- data/lib/apn_on_rails/libs/connection.rb +2 -1
- data/lib/apn_on_rails/libs/feedback.rb +6 -18
- data/lib/apn_on_rails/tasks/apn.rake +13 -4
- data/spec/active_record/setup_ar.rb +19 -0
- data/spec/apn_on_rails/app/models/apn/app_spec.rb +178 -0
- data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
- data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
- data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
- data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +37 -0
- data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
- data/spec/apn_on_rails/libs/feedback_spec.rb +45 -0
- data/spec/extensions/string.rb +10 -0
- data/spec/factories/app_factory.rb +27 -0
- data/spec/factories/device_factory.rb +29 -0
- data/spec/factories/device_grouping_factory.rb +22 -0
- data/spec/factories/group_factory.rb +27 -0
- data/spec/factories/group_notification_factory.rb +22 -0
- data/spec/factories/notification_factory.rb +22 -0
- data/spec/factories/pull_notification_factory.rb +22 -0
- data/spec/fixtures/hexa.bin +1 -0
- data/spec/fixtures/message_for_sending.bin +0 -0
- data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
- data/spec/spec_helper.rb +55 -0
- 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
|