passbookpgh 0.4.5

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +7 -0
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +130 -0
  5. data/LICENSE +22 -0
  6. data/README.md +294 -0
  7. data/Rakefile +36 -0
  8. data/VERSION +1 -0
  9. data/bin/pk +22 -0
  10. data/lib/commands/build.rb +62 -0
  11. data/lib/commands/commands.rb +31 -0
  12. data/lib/commands/generate.rb +44 -0
  13. data/lib/commands/templates/boarding-pass.json +56 -0
  14. data/lib/commands/templates/coupon.json +33 -0
  15. data/lib/commands/templates/event-ticket.json +33 -0
  16. data/lib/commands/templates/generic.json +33 -0
  17. data/lib/commands/templates/store-card.json +33 -0
  18. data/lib/passbook/pkpass.rb +121 -0
  19. data/lib/passbook/push_notification.rb +19 -0
  20. data/lib/passbook/signer.rb +40 -0
  21. data/lib/passbook/version.rb +3 -0
  22. data/lib/passbook.rb +15 -0
  23. data/lib/rack/passbook_rack.rb +98 -0
  24. data/lib/rails/generators/passbook/config/config_generator.rb +16 -0
  25. data/lib/rails/generators/passbook/config/templates/initializer.rb +13 -0
  26. data/lib/utils/command_utils.rb +12 -0
  27. data/passbookpgh.gemspec +110 -0
  28. data/spec/data/icon.png +0 -0
  29. data/spec/data/icon@2x.png +0 -0
  30. data/spec/data/logo.png +0 -0
  31. data/spec/data/logo@2x.png +0 -0
  32. data/spec/lib/commands/build_spec.rb +92 -0
  33. data/spec/lib/commands/commands_spec.rb +102 -0
  34. data/spec/lib/commands/commands_spec_helper.rb +69 -0
  35. data/spec/lib/commands/generate_spec.rb +72 -0
  36. data/spec/lib/passbook/pkpass_spec.rb +108 -0
  37. data/spec/lib/passbook/push_notification_spec.rb +23 -0
  38. data/spec/lib/passbook/signer_spec.rb +84 -0
  39. data/spec/lib/rack/passbook_rack_spec.rb +233 -0
  40. data/spec/spec_helper.rb +9 -0
  41. metadata +244 -0
@@ -0,0 +1,92 @@
1
+ require 'lib/commands/commands_spec_helper'
2
+ require 'passbook'
3
+
4
+ describe 'Build' do
5
+
6
+ before :each do
7
+ $stderr = StringIO.new
8
+ mock_terminal
9
+ end
10
+
11
+ context 'command' do
12
+ specify 'missing directory' do
13
+ run_command 'build' do
14
+ @output.string.should eq "\e[31mMissing argument\e[0m\n"
15
+ end
16
+ end
17
+
18
+ context 'good directory' do
19
+
20
+ before :each do
21
+ File.should_receive(:directory?).with('scraps').and_return true
22
+ File.should_receive(:exist?).once.with('scraps/pass.json').and_return true
23
+ File.should_receive(:exist?).once.with('scraps.pkpass').and_return false
24
+ end
25
+
26
+ specify 'no certificate file' do
27
+ run_command 'build', 'scraps' do
28
+ @output.string.should eq "\e[31mMissing or invalid certificate file\e[0m\n"
29
+ end
30
+ end
31
+
32
+ context 'good certificate file' do
33
+ before :each do
34
+ File.should_receive(:exist?).with('jackels').and_return true
35
+ end
36
+
37
+ specify 'no certificate password entered' do
38
+ run_command 'build', 'scraps', '-w', 'jackels' do
39
+ @output.string.should eq "Enter certificate password:\n\n"
40
+ end
41
+ end
42
+
43
+ context 'all required values' do
44
+
45
+ let(:pass_json) {'{"this":"is awesome json"}'}
46
+ let(:pass_assets) {['pass.json', 'something.jpeg']}
47
+
48
+ before :each do
49
+ Passbook.should_receive(:wwdc_cert=).with 'jackels'
50
+ Passbook.should_receive(:p12_key=).with 'badger_key'
51
+ Passbook.should_receive(:p12_certificate=).with 'badger_cert'
52
+ Passbook.should_receive(:p12_password=).with 'bees'
53
+ CommandUtils.should_receive(:get_assets).with('scraps').and_return pass_assets
54
+ @pk_pass = double 'pk pass'
55
+ File.should_receive(:read).with('pass.json').and_return pass_json
56
+ Passbook::PKPass.should_receive(:new).with(pass_json).and_return @pk_pass
57
+ @pk_pass.should_receive(:addFiles).with pass_assets
58
+ end
59
+
60
+ specify 'are set from command line' do
61
+ pass_stream = double 'passbook stream'
62
+ pass_stream.should_receive(:string).and_return 'my badass pass'
63
+ @pk_pass.should_receive(:stream).and_return pass_stream
64
+ File.should_receive(:open).with('scraps.pkpass', 'w')
65
+
66
+ run_command 'build', 'scraps', '-w', 'jackels',
67
+ '-p', 'bees', '-k', 'badger_key', '-c', 'badger_cert' do
68
+ @output.string.should eq ""
69
+ end
70
+ end
71
+
72
+ specify 'should catch a general error' do
73
+ @pk_pass.should_receive(:stream).and_raise(StandardError.new('I have failed'))
74
+ run_command 'build', 'scraps', '-w', 'jackels',
75
+ '-p', 'bees', '-k', 'badger_key', '-c', 'badger_cert' do
76
+ @output.string.should eq "\e[31mError: I have failed\e[0m\n"
77
+ end
78
+ end
79
+
80
+ specify 'should catch a general error' do
81
+ @pk_pass.should_receive(:stream).and_raise(OpenSSL::PKCS12::PKCS12Error.new('I am a failure'))
82
+ run_command 'build', 'scraps', '-w', 'jackels',
83
+ '-p', 'bees', '-k', 'badger_key', '-c', 'badger_cert' do
84
+ @output.string.should eq "\e[31mError: I am a failure\e[0m\n\e[33mYou may be getting this error because the certificate password is either incorrect or missing\e[0m\n"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ end
@@ -0,0 +1,102 @@
1
+ require 'lib/commands/commands_spec_helper'
2
+
3
+ describe 'Commands' do
4
+
5
+ before :each do
6
+ program :version, '1.2.3'
7
+ program :description, "Honey Badger Don't Care"
8
+ end
9
+
10
+ context 'determine directory' do
11
+
12
+ specify 'no directories' do
13
+ Dir.should_receive(:[]).and_return []
14
+ determine_directory!
15
+ @directory.should eq nil
16
+ end
17
+
18
+ specify 'one directory present' do
19
+ Dir.should_receive(:[]).and_return ['ass']
20
+ File.should_receive(:dirname).with('ass').and_return('/honey/badger/is/bad/ass')
21
+ determine_directory!
22
+ @directory.should eq '/honey/badger/is/bad/ass'
23
+ end
24
+
25
+ specify 'multiple directories' do
26
+ Dir.should_receive(:[]).and_return ['cobras', 'bee_larvae']
27
+ File.should_receive(:dirname).with('cobras').and_return('/yummy/cobras')
28
+ File.should_receive(:dirname).with('bee_larvae').and_return('/disgusting/bee_larvae')
29
+ self.should_receive(:choose).with('Select a directory:',
30
+ '/yummy/cobras', '/disgusting/bee_larvae').and_return '/yummy/cobras'
31
+ determine_directory!
32
+ @directory.should eq '/yummy/cobras'
33
+ end
34
+
35
+ end
36
+
37
+ context 'validate directory' do
38
+
39
+ specify 'missing directory' do
40
+ self.should_receive(:say_error).with('Missing argument').and_return true
41
+ lambda {
42
+ @directory = nil
43
+ validate_directory!
44
+ }.should exit_with_code(1)
45
+ end
46
+
47
+ specify 'directory does not exist' do
48
+ self.should_receive(:say_error).with("Directory scraps does not exist").and_return true
49
+ File.should_receive(:directory?).with('scraps').and_return false
50
+ lambda {
51
+ @directory = 'scraps'
52
+ validate_directory!
53
+ }.should exit_with_code(1)
54
+ end
55
+
56
+ specify 'directory does not have a valid pass' do
57
+ self.should_receive(:say_error).with("Directory scraps is not a valid pass").and_return true
58
+ File.should_receive(:directory?).with('scraps').and_return true
59
+ File.should_receive(:exist?).with('scraps/pass.json').and_return false
60
+ lambda {
61
+ @directory = 'scraps'
62
+ validate_directory!
63
+ }.should exit_with_code(1)
64
+ end
65
+
66
+ specify 'directory has valid pass' do
67
+ File.should_receive(:directory?).with('scraps').and_return true
68
+ File.should_receive(:exist?).with('scraps/pass.json').and_return true
69
+ lambda {
70
+ @directory = 'scraps'
71
+ validate_directory!
72
+ }.should_not exit_with_code(1)
73
+ end
74
+ end
75
+
76
+ context 'validate certificate' do
77
+ specify 'nil certificate' do
78
+ self.should_receive(:say_error).with("Missing or invalid certificate file").and_return true
79
+ lambda {
80
+ @certificate = nil
81
+ validate_certificate!
82
+ }.should exit_with_code(1)
83
+ end
84
+
85
+ specify 'certificate file does not exist' do
86
+ self.should_receive(:say_error).with("Missing or invalid certificate file").and_return true
87
+ File.should_receive(:exist?).with('jackels').and_return false
88
+ lambda {
89
+ @certificate = 'jackels'
90
+ validate_certificate!
91
+ }.should exit_with_code(1)
92
+ end
93
+
94
+ specify 'certificate file exists' do
95
+ File.should_receive(:exist?).with('jackels').and_return true
96
+ lambda {
97
+ @certificate = 'jackels'
98
+ validate_certificate!
99
+ }.should_not exit_with_code(1)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+ require 'terminal-table'
3
+ require 'commander/import'
4
+ require 'utils/command_utils'
5
+
6
+ def load_commands
7
+ Dir['lib/commands/**/*.rb'].each {|f|
8
+ load File.join(File.dirname(__FILE__), '../../..', f)
9
+ require File.join(File.dirname(__FILE__), '../../..', f.gsub(/.rb/, ''))
10
+ }
11
+ end
12
+
13
+ def mock_terminal
14
+ @input = StringIO.new
15
+ @output = StringIO.new
16
+ $terminal = HighLine.new @input, @output
17
+ end
18
+
19
+ def new_command_runner *args, &block
20
+ Commander::Runner.instance_variable_set :"@singleton", Commander::Runner.new(args)
21
+ program :version, '1.2.3'
22
+ program :description, "Honey Badger Don't Care"
23
+ yield if block
24
+ Commander::Runner.instance
25
+ end
26
+
27
+ def run *args
28
+ runner = new_command_runner(*args) do
29
+ load_commands
30
+ end
31
+ runner.run!
32
+ @output.string
33
+ end
34
+
35
+ RSpec::Matchers.define :exit_with_code do |exp_code|
36
+ actual = nil
37
+ match do |block|
38
+ begin
39
+ block.call
40
+ rescue SystemExit => e
41
+ actual = e.status
42
+ end
43
+ actual and actual == exp_code
44
+ end
45
+ failure_message_for_should do |block|
46
+ "expected block to call exit(#{exp_code}) but exit" +
47
+ (actual.nil? ? " not called" : "(#{actual}) was called")
48
+ end
49
+ failure_message_for_should_not do |block|
50
+ "expected block not to call exit(#{exp_code})"
51
+ end
52
+ description do
53
+ "expect block to call exit(#{exp_code})"
54
+ end
55
+ end
56
+
57
+ def run_raw_command(*args)
58
+ lambda{
59
+ run(*args)
60
+ }.should raise_error(SystemExit, ' ')
61
+ end
62
+
63
+ def run_command(*args, &block)
64
+ begin
65
+ run_raw_command *args
66
+ rescue
67
+ yield
68
+ end
69
+ end
@@ -0,0 +1,72 @@
1
+ require 'lib/commands/commands_spec_helper'
2
+ require 'passbook'
3
+
4
+ describe 'Generate' do
5
+
6
+ before :each do
7
+ $stderr = StringIO.new
8
+ mock_terminal
9
+ end
10
+
11
+ context 'command' do
12
+ specify 'no options' do
13
+ run_command 'generate' do
14
+ @output.string.should eq 'Enter a passbook name: '
15
+ end
16
+
17
+ end
18
+
19
+ specify 'passbook entered directory already exists' do
20
+ @input << "my_awesome_passbook\n"
21
+ @input.rewind
22
+ File.should_receive(:directory?).with('my_awesome_passbook').and_return true
23
+ run_command 'generate' do
24
+ @output.string.should eq "Enter a passbook name: \e[31mDirectory my_awesome_passbook already exists\e[0m\n"
25
+ end
26
+ end
27
+
28
+ specify 'passbook entered file already exists' do
29
+ @input << "my_awesome_passbook\n"
30
+ @input.rewind
31
+ File.should_receive(:directory?).with('my_awesome_passbook').and_return false
32
+ File.should_receive(:exist?).with('my_awesome_passbook').and_return true
33
+ run_command 'generate' do
34
+ @output.string.should eq "Enter a passbook name: \e[31mFile exists at my_awesome_passbook\e[0m\n"
35
+ end
36
+ end
37
+
38
+ context 'valid pass directory' do
39
+
40
+ before :each do
41
+ File.should_receive(:directory?).with('my_awesome_passbook').and_return false
42
+ File.should_receive(:exist?).with('my_awesome_passbook').and_return false
43
+ end
44
+
45
+ specify 'invalid type' do
46
+ @input << "my_awesome_passbook\n"
47
+ @input.rewind
48
+ run_command 'generate', '-T', 'honey_badger' do
49
+ @output.string.should eq "Enter a passbook name: \e[31mInvalid type: \"honey_badger\", expected one of: [boarding-pass, coupon, event-ticket, store-card, generic]\e[0m\n"
50
+ end
51
+ end
52
+
53
+ specify 'valid type' do
54
+ CommandUtils.should_receive(:get_current_directory).and_return('')
55
+ FileUtils.should_receive(:mkdir_p).with('my_awesome_passbook')
56
+ FileUtils.should_receive(:cp).with("/../commands/templates/boarding-pass.json", "my_awesome_passbook/pass.json")
57
+ FileUtils.should_receive(:touch).with("my_awesome_passbook/icon.png")
58
+ FileUtils.should_receive(:touch).with("my_awesome_passbook/icon@2x.png")
59
+ @input << "1\n"
60
+ @input.rewind
61
+ run_command 'generate', 'my_awesome_passbook' do
62
+ @output.string.should eq "Select a pass type\n1. boarding-pass\n2. coupon\n3. event-ticket\n4. store-card\n5. generic\n? \e[32mPass generated in my_awesome_passbook\e[0m\n"
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+ end
69
+
70
+
71
+ end
72
+
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe Passbook do
4
+
5
+ let (:content) {{
6
+ :formatVersion => 1,
7
+ :passTypeIdentifier => "pass.passbook.test",
8
+ :serialNumber => "001",
9
+ :teamIdentifier => ENV['APPLE_TEAM_ID'],
10
+ :relevantDate => "2012-10-02",
11
+ :locations => [ #TODO
12
+ {
13
+ :longitude => 2.35403,
14
+ :latitude => 48.893855
15
+ }
16
+ ],
17
+ :organizationName => "WorldCo",
18
+ :description => "description",
19
+ :foregroundColor => "rgb(227,210,18)",
20
+ :backgroundColor => "rgb(60, 65, 76)",
21
+ :logoText => "Event",
22
+ :eventTicket => {
23
+ :primaryFields => [
24
+ {
25
+ :key => "date",
26
+ :label => "DATE",
27
+ :value => "date"
28
+ }
29
+ ],
30
+ :backFields => [
31
+ {
32
+ :key => "description",
33
+ :label => "DESCRIPTION",
34
+ :value => "description"
35
+ },
36
+ {
37
+ :key => "aboutUs",
38
+ :label => "MORE",
39
+ :value => "about us"
40
+ }
41
+ ]
42
+ }
43
+ }}
44
+
45
+ let (:signer) {double 'signer'}
46
+ let (:pass) {Passbook::PKPass.new content.to_json, signer}
47
+
48
+ context 'outputs' do
49
+ let (:base_path) {'spec/data'}
50
+ let (:entries) {["pass.json", "manifest.json", "signature", "icon.png", "icon@2x.png", "logo.png", "logo@2x.png"]}
51
+
52
+ before :each do
53
+ pass.addFiles ["#{base_path}/icon.png","#{base_path}/icon@2x.png","#{base_path}/logo.png","#{base_path}/logo@2x.png"]
54
+ signer.should_receive(:sign).and_return('Signed by the Honey Badger')
55
+ @file_entries = []
56
+ Zip::InputStream::open(zip_path) {|io|
57
+ while (entry = io.get_next_entry)
58
+ @file_entries << entry.name
59
+ end
60
+ }
61
+ end
62
+
63
+ context 'zip file' do
64
+ let(:zip_path) {pass.file.path}
65
+
66
+ subject {entries}
67
+ it {should eq @file_entries}
68
+ end
69
+
70
+ context 'StringIO' do
71
+ let (:temp_file) {Tempfile.new("pass.pkpass")}
72
+ let (:zip_path) {
73
+ zip_out = pass.stream
74
+ zip_out.class.should eq(Class::StringIO)
75
+ #creating file, re-reading zip to see if correctly formed
76
+ temp_file.write zip_out.string
77
+ temp_file.close
78
+ temp_file.path
79
+ }
80
+
81
+ subject {entries}
82
+ it {should eq @file_entries}
83
+
84
+ after do
85
+ temp_file.delete
86
+ end
87
+ end
88
+ end
89
+
90
+ # TODO: find a proper way to do this
91
+ context 'Error catcher' do
92
+ context 'formatVersion' do
93
+ let (:base_path) {'spec/data'}
94
+
95
+ before :each do
96
+ pass.addFiles ["#{base_path}/icon.png","#{base_path}/icon@2x.png","#{base_path}/logo.png","#{base_path}/logo@2x.png"]
97
+ tpass = JSON.parse(pass.pass)
98
+ tpass['formatVersion'] = 'It should be a numeric'
99
+ pass.pass = tpass.to_json
100
+ end
101
+
102
+ it "raise an error" do
103
+ expect { pass.build }.to raise_error('Format Version should be a numeric')
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'grocer'
3
+
4
+ describe Passbook::PushNotification do
5
+
6
+ context 'send notification' do
7
+ let(:grocer_pusher) {double 'Grocer'}
8
+ let(:notification) {double 'Grocer::Notification'}
9
+ let(:notification_settings) {{:certificate => './notification_cert.pem', :gateway => 'honeybadger.apple.com', :passphrase => 'ah@rdvintAge'}}
10
+
11
+ before :each do
12
+ Passbook.should_receive(:notification_cert).and_return './notification_cert.pem'
13
+ Grocer::PassbookNotification.should_receive(:new).with(:device_token => 'my token').and_return notification
14
+ grocer_pusher.should_receive(:push).with(notification).and_return 55
15
+ Grocer.should_receive(:pusher).with(notification_settings).and_return grocer_pusher
16
+ Passbook.should_receive(:notification_gateway).and_return 'honeybadger.apple.com'
17
+ Passbook.should_receive(:notification_passphrase).and_return 'ah@rdvintAge'
18
+ end
19
+
20
+ subject {Passbook::PushNotification.send_notification('my token')}
21
+ it {should eq 55}
22
+ end
23
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Signer' do
4
+ context 'signatures' do
5
+
6
+ context 'p12_cert_and_key' do
7
+ context 'pem p12 certs' do
8
+ context 'using config file certificates' do
9
+ before do
10
+ Passbook.should_receive(:p12_password).and_return 'password'
11
+ Passbook.should_receive(:p12_key).and_return 'my_p12_key'
12
+ Passbook.should_receive(:p12_certificate).and_return 'my_p12_certificate'
13
+ Passbook.should_receive(:wwdc_cert).and_return 'i_love_robots'
14
+ File.should_receive(:read).with('my_p12_key').and_return 'my_p12_key_file'
15
+ File.should_receive(:read).with('my_p12_certificate').and_return 'my_p12_certificate_file'
16
+ OpenSSL::PKey::RSA.should_receive(:new).with('my_p12_key_file', 'password').and_return 'my_rsa_key'
17
+ OpenSSL::X509::Certificate.should_receive(:new).with('my_p12_certificate_file').and_return 'my_ssl_p12_cert'
18
+ end
19
+
20
+ subject {Passbook::Signer.new.key_hash}
21
+ its([:key]) {should eq 'my_rsa_key'}
22
+ its([:cert]) {should eq 'my_ssl_p12_cert'}
23
+ end
24
+
25
+ context 'using passed in certificates' do
26
+ before do
27
+ Passbook.should_receive(:p12_password).never
28
+ Passbook.should_receive(:p12_key).never
29
+ Passbook.should_receive(:p12_certificate).never
30
+ Passbook.should_receive(:wwdc_cert).never
31
+ File.should_receive(:read).with('my_p12_key').and_return 'my_p12_key_file'
32
+ File.should_receive(:read).with('my_p12_certificate').and_return 'my_p12_certificate_file'
33
+ OpenSSL::PKey::RSA.should_receive(:new).with('my_p12_key_file', 'password').and_return 'my_rsa_key'
34
+ OpenSSL::X509::Certificate.should_receive(:new).with('my_p12_certificate_file').and_return 'my_ssl_p12_cert'
35
+ end
36
+
37
+ subject {Passbook::Signer.new(certificate: 'my_p12_certificate', password: 'password',
38
+ key: 'my_p12_key', wwdc_cert: 'i_love_robots').key_hash}
39
+ its([:key]) {should eq 'my_rsa_key'}
40
+ its([:cert]) {should eq 'my_ssl_p12_cert'}
41
+ end
42
+ end
43
+
44
+ context 'p12 files' do
45
+ let (:p12) { double('OpenSSL::PKCS12') }
46
+ let (:final_hash) {{:key => 'my_final_p12_key', :cert => 'my_final_p12_cert'}}
47
+ context 'using config file certificates' do
48
+ before do
49
+ p12.should_receive(:key).and_return final_hash[:key]
50
+ p12.should_receive(:certificate).and_return final_hash[:cert]
51
+ Passbook.should_receive(:p12_password).and_return 'password'
52
+ Passbook.should_receive(:wwdc_cert).and_return 'i_love_robots'
53
+ Passbook.should_receive(:p12_certificate).and_return 'my_p12_cert'
54
+ Passbook.should_receive(:p12_key).and_return nil
55
+ File.should_receive(:read).with('my_p12_cert').and_return 'my_p12_cert_file'
56
+ OpenSSL::PKCS12.should_receive(:new).with('my_p12_cert_file', 'password').and_return p12
57
+ end
58
+
59
+ subject {Passbook::Signer.new.key_hash}
60
+ its([:key]) {should eq final_hash[:key]}
61
+ its([:cert]) {should eq final_hash[:cert]}
62
+ end
63
+
64
+ context 'using passed in certificates' do
65
+ before do
66
+ p12.should_receive(:key).and_return final_hash[:key]
67
+ p12.should_receive(:certificate).and_return final_hash[:cert]
68
+ Passbook.should_receive(:p12_password).never
69
+ Passbook.should_receive(:p12_key).never
70
+ Passbook.should_receive(:p12_certificate).never
71
+ Passbook.should_receive(:wwdc_cert).never
72
+ File.should_receive(:read).with('my_p12_cert').and_return 'my_p12_cert_file'
73
+ OpenSSL::PKCS12.should_receive(:new).with('my_p12_cert_file', 'password').and_return p12
74
+ end
75
+
76
+ subject {Passbook::Signer.new(certificate: 'my_p12_cert', password: 'password',
77
+ wwdc_cert: 'i_love_robots').key_hash}
78
+ its([:key]) {should eq final_hash[:key]}
79
+ its([:cert]) {should eq final_hash[:cert]}
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end