ruby-keychain 0.1.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.
@@ -0,0 +1,43 @@
1
+
2
+ # Procotol constants for use when creating/finding a Keychain::Item of type internet password
3
+ #
4
+ #
5
+ module Keychain::Protocols
6
+ # @private
7
+ def self.attach_protocol name
8
+ Sec.send :attach_variable, name, :pointer
9
+ constant_name = name.to_s.gsub('kSecAttrProtocol', '').upcase
10
+ const_set constant_name, CF::Base.typecast(Sec.send(name))
11
+ end
12
+ attach_protocol :kSecAttrProtocolFTP
13
+ attach_protocol :kSecAttrProtocolFTPAccount
14
+ attach_protocol :kSecAttrProtocolHTTP
15
+ attach_protocol :kSecAttrProtocolIRC
16
+ attach_protocol :kSecAttrProtocolNNTP
17
+ attach_protocol :kSecAttrProtocolPOP3
18
+ attach_protocol :kSecAttrProtocolSMTP
19
+ attach_protocol :kSecAttrProtocolSOCKS
20
+ attach_protocol :kSecAttrProtocolIMAP
21
+ attach_protocol :kSecAttrProtocolLDAP
22
+ attach_protocol :kSecAttrProtocolAppleTalk
23
+ attach_protocol :kSecAttrProtocolAFP
24
+ attach_protocol :kSecAttrProtocolTelnet
25
+ attach_protocol :kSecAttrProtocolSSH
26
+ attach_protocol :kSecAttrProtocolFTPS
27
+ attach_protocol :kSecAttrProtocolHTTPS
28
+ attach_protocol :kSecAttrProtocolHTTPProxy
29
+ attach_protocol :kSecAttrProtocolHTTPSProxy
30
+ attach_protocol :kSecAttrProtocolFTPProxy
31
+ attach_protocol :kSecAttrProtocolSMB
32
+ attach_protocol :kSecAttrProtocolRTSP
33
+ attach_protocol :kSecAttrProtocolRTSPProxy
34
+ attach_protocol :kSecAttrProtocolDAAP
35
+ attach_protocol :kSecAttrProtocolEPPC
36
+ attach_protocol :kSecAttrProtocolIPP
37
+ attach_protocol :kSecAttrProtocolNNTPS
38
+ attach_protocol :kSecAttrProtocolLDAPS
39
+ attach_protocol :kSecAttrProtocolTelnetS
40
+ attach_protocol :kSecAttrProtocolIMAPS
41
+ attach_protocol :kSecAttrProtocolIRCS
42
+ attach_protocol :kSecAttrProtocolPOP3S
43
+ end
@@ -0,0 +1,131 @@
1
+ # A scope that represents the search for a keychain item
2
+ #
3
+ #
4
+ class Keychain::Scope
5
+ def initialize(kind, keychain=nil)
6
+ @kind = kind
7
+ @limit = nil
8
+ @keychains = [keychain]
9
+ @conditions = {}
10
+ end
11
+
12
+ # Adds conditions to the scope. Conditions are merged with any previously defined conditions.
13
+ #
14
+ # The set of possible keys for conditions is given by Sec::ATTR_MAP.values. The legal values for the :protocol key are the constants in
15
+ # Keychain::Protocols
16
+ #
17
+ # @param [Hash] conditions options to create the item with
18
+ # @option conditions [String] :account The account (user name)
19
+ # @option conditions [String] :comment A free text comment about the item
20
+ # @option conditions [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code
21
+ # @option conditions [String] :generic generic passwords can have a generic data attribute
22
+ # @option conditions [String] :invisible whether the item should be invisible
23
+ # @option conditions [String] :negative A negative item records that the user decided to never save a password
24
+ # @option conditions [String] :label A label for the item (Shown in keychain access)
25
+ # @option conditions [String] :path The path the password is associated with (internet passwords only)
26
+ # @option conditions [String] :port The path the password is associated with (internet passwords only)
27
+ # @option conditions [String] :port The protocol the password is associated with (internet passwords only)
28
+ # Should be one of the constants at Keychain::Protocols
29
+ # @option conditions [String] :domain the domain the password is associated with (internet passwords only)
30
+ # @option conditions [String] :server the host name the password is associated with (internet passwords only)
31
+ # @option conditions [String] :service the service the password is associated with (generic passwords only)
32
+ # @option conditions [Integer] :type the item's type, as the unsigned integer representation of a 4 char code
33
+ #
34
+ # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
35
+ def where(conditions)
36
+ @conditions.merge! conditions
37
+ self
38
+ end
39
+
40
+ # Sets the number of items returned by the scope
41
+ #
42
+ # @param [Integer] value The maximum number of items to return
43
+ # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
44
+ def limit value
45
+ @limit = value
46
+ self
47
+ end
48
+
49
+ # Set the list of keychains to search
50
+ #
51
+ # @param [Array<Keychain::Keychain>] keychains The maximum number of items to return
52
+ # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
53
+ def in *keychains
54
+ @keychains = keychains.flatten
55
+ self
56
+ end
57
+
58
+ # Returns the first matching item in the scope
59
+ #
60
+ # @return [Keychain::Item, nil]
61
+ def first
62
+ query = to_query
63
+ execute(query).first
64
+ end
65
+
66
+ # Returns an array containing all of the matching items
67
+ #
68
+ # @return [Array] The matching items. May be empty
69
+ def all
70
+ query = to_query
71
+ query[Sec::Search::LIMIT] = @limit ? @limit.to_cf : Sec::Search::ALL
72
+ execute query
73
+ end
74
+
75
+ # Creates a new keychain item
76
+ #
77
+ # @param [Hash] attributes options to create the item with
78
+ # @option attributes [String] :account The account (user name)
79
+ # @option attributes [String] :comment A free text comment about the item
80
+ # @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code
81
+ # @option attributes [String] :generic generic passwords can have a generic data attribute
82
+ # @option attributes [String] :invisible whether the item should be invisible
83
+ # @option attributes [String] :negative A negative item records that the user decided to never save a password
84
+ # @option attributes [String] :label A label for the item (Shown in keychain access)
85
+ # @option attributes [String] :path The path the password is associated with (internet passwords only)
86
+ # @option attributes [String] :port The path the password is associated with (internet passwords only)
87
+ # @option attributes [String] :port The protocol the password is associated with (internet passwords only)
88
+ # Should be one of the constants at Keychain::Protocols
89
+ # @option attributes [String] :domain the domain the password is associated with (internet passwords only)
90
+ # @option attributes [String] :server the host name the password is associated with (internet passwords only)
91
+ # @option attributes [String] :service the service the password is associated with (generic passwords only)
92
+ # @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code
93
+ #
94
+ # @return [Keychain::Item]
95
+ def create(attributes)
96
+ raise "You must specify a password" unless attributes[:password]
97
+
98
+ Keychain::Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first)
99
+ end
100
+
101
+ private
102
+
103
+ def execute query
104
+ result = FFI::MemoryPointer.new :pointer
105
+ status = Sec.SecItemCopyMatching(query, result)
106
+ if status == Sec.enum_value( :errSecItemNotFound)
107
+ return []
108
+ end
109
+ Sec.check_osstatus(status)
110
+ result = CF::Base.typecast(result.read_pointer).release_on_gc
111
+ unless result.is_a?(CF::Array)
112
+ result = CF::Array.immutable([result])
113
+ end
114
+ result.collect {|dictionary_of_attributes| Keychain::Item.from_dictionary_of_attributes(dictionary_of_attributes)}
115
+ end
116
+
117
+
118
+ def to_query
119
+ query = CF::Dictionary.mutable
120
+ @conditions.each do |k,v|
121
+ k = Sec::INVERSE_ATTR_MAP[k]
122
+ query[k] = v.to_cf
123
+ end
124
+
125
+ query[Sec::Query::CLASS] = @kind
126
+ query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any?
127
+ query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE
128
+ query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE
129
+ query
130
+ end
131
+ end
@@ -0,0 +1,165 @@
1
+ # The module to which FFI attaches constants
2
+ module Sec
3
+ extend FFI::Library
4
+ ffi_lib '/System/Library/Frameworks/Security.framework/Security'
5
+
6
+ typedef :int32, :osstatus
7
+ typedef :pointer, :keychainref
8
+
9
+ enum [
10
+ :errSecItemNotFound, -25300,
11
+ :errSecDuplicateItem, -25299,
12
+ :errSecAuthFailed, -25293,
13
+ :errSecNoSuchKeychain, -25294,
14
+ :errCancelled, -128
15
+ ]
16
+
17
+ attach_variable 'kSecClassInternetPassword', :pointer
18
+ attach_variable 'kSecClassGenericPassword', :pointer
19
+
20
+ attach_variable 'kSecClass', :pointer
21
+
22
+ attach_variable 'kSecAttrAccess', :pointer
23
+ attach_variable 'kSecAttrAccount', :pointer
24
+ attach_variable 'kSecAttrAuthenticationType', :pointer
25
+ attach_variable 'kSecAttrComment', :pointer
26
+ attach_variable 'kSecAttrCreationDate', :pointer
27
+ attach_variable 'kSecAttrCreator', :pointer
28
+ attach_variable 'kSecAttrDescription', :pointer
29
+ attach_variable 'kSecAttrGeneric', :pointer
30
+ attach_variable 'kSecAttrIsInvisible', :pointer
31
+ attach_variable 'kSecAttrIsNegative', :pointer
32
+ attach_variable 'kSecAttrLabel', :pointer
33
+ attach_variable 'kSecAttrModificationDate', :pointer
34
+ attach_variable 'kSecAttrPath', :pointer
35
+ attach_variable 'kSecAttrPort', :pointer
36
+ attach_variable 'kSecAttrProtocol', :pointer
37
+ attach_variable 'kSecAttrSecurityDomain', :pointer
38
+ attach_variable 'kSecAttrServer', :pointer
39
+ attach_variable 'kSecAttrService', :pointer
40
+ attach_variable 'kSecAttrType', :pointer
41
+
42
+ attach_variable 'kSecMatchSearchList', :pointer
43
+
44
+ attach_variable 'kSecMatchLimit', :pointer
45
+ attach_variable 'kSecMatchLimitOne', :pointer
46
+ attach_variable 'kSecMatchLimitAll', :pointer
47
+ attach_variable 'kSecMatchItemList', :pointer
48
+ attach_variable 'kSecReturnAttributes', :pointer
49
+ attach_variable 'kSecReturnRef', :pointer
50
+ attach_variable 'kSecReturnData', :pointer
51
+
52
+ attach_variable 'kSecValueRef', :pointer
53
+ attach_variable 'kSecValueData', :pointer
54
+ attach_variable 'kSecUseKeychain', :pointer
55
+
56
+
57
+
58
+ # map of kSecAttr* constants to the corresponding ruby name for the attribute
59
+ # Used in {Keychain::Item#attributes}}
60
+ ATTR_MAP = {
61
+ CF::Base.typecast(kSecAttrAccess) => :access,
62
+ CF::Base.typecast(kSecAttrAccount) => :account,
63
+ CF::Base.typecast(kSecAttrAuthenticationType) => :authentication_type,
64
+ CF::Base.typecast(kSecAttrComment) => :comment,
65
+ CF::Base.typecast(kSecAttrCreationDate) => :created_at,
66
+ CF::Base.typecast(kSecAttrCreator) => :creator,
67
+ CF::Base.typecast(kSecAttrDescription) => :description,
68
+ CF::Base.typecast(kSecAttrGeneric) => :generic,
69
+ CF::Base.typecast(kSecAttrIsInvisible) => :invisible,
70
+ CF::Base.typecast(kSecAttrIsNegative) => :negative,
71
+ CF::Base.typecast(kSecAttrLabel) => :label,
72
+ CF::Base.typecast(kSecAttrModificationDate) => :updated_at,
73
+ CF::Base.typecast(kSecAttrPath) => :path,
74
+ CF::Base.typecast(kSecAttrPort) => :port,
75
+ CF::Base.typecast(kSecAttrProtocol) => :protocol,
76
+ CF::Base.typecast(kSecAttrSecurityDomain) => :security_domain,
77
+ CF::Base.typecast(kSecAttrServer) => :server,
78
+ CF::Base.typecast(kSecAttrService) => :service,
79
+ CF::Base.typecast(kSecAttrType) => :type,
80
+ CF::Base.typecast(kSecClass) => :klass
81
+ }
82
+
83
+ # Inverse of {ATTR_MAP}
84
+ INVERSE_ATTR_MAP = ATTR_MAP.invert
85
+
86
+ # Query options for use with SecCopyMatching, SecItemUpdate
87
+ #
88
+ module Query
89
+ #key identifying the class of an item (kSecClass)
90
+ CLASS = CF::Base.typecast(Sec.kSecClass)
91
+ #key speciying the list of keychains to search (kSecMatchSearchList)
92
+ SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList)
93
+ #key indicating the list of specific keychain items to the scope the search to
94
+ ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList)
95
+ #key indicating whether to return attributes (kSecReturnAttributes)
96
+ RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes)
97
+ #key indicating whether to return the SecKeychainItemRef (kSecReturnRef)
98
+ RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef)
99
+ #key indicating whether to return the password data (kSecReturnData)
100
+ RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData)
101
+ #key indicating which keychain to use for the operation (kSecUseKeychain)
102
+ KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain)
103
+ end
104
+
105
+ # defines constants for use as the class of an item
106
+ module Classes
107
+ # constant identifiying generic passwords (kSecClassGenericPassword)
108
+ GENERIC = CF::Base.typecast(Sec.kSecClassGenericPassword)
109
+ # constant identifying internet passwords (kSecClassInternetPassword)
110
+ INTERNET = CF::Base.typecast(Sec.kSecClassInternetPassword)
111
+ end
112
+
113
+ # Search match types for use with SecCopyMatching
114
+ module Search
115
+ #meta value for {Sec::Search::LIMIT} indicating that all items be returned (kSecMatchLimitAll)
116
+ ALL = CF::Base.typecast(Sec.kSecMatchLimitAll)
117
+ # hash key indicating the maximum number of items (kSecMatchLimit)
118
+ LIMIT = CF::Base.typecast(Sec.kSecMatchLimit)
119
+ end
120
+
121
+ # Constants for use with SecItemAdd/SecItemUpdate
122
+ module Value
123
+ # The hash key for the SecKeychainItemRef (in the dictionary returned by SecCopyMatching) (kSecValueRef)
124
+ REF = CF::Base.typecast(Sec.kSecValueRef)
125
+ # The hash key for the password data (in the dictionary returned by SecCopyMatching) (kSecValueData)
126
+ DATA = CF::Base.typecast(Sec.kSecValueData)
127
+ end
128
+
129
+
130
+ # The base class of all CF types from the security framework
131
+ #
132
+ # @abstract
133
+ class Base < CF::Base
134
+ #@private
135
+ def self.register_type(type_name)
136
+ Sec.attach_function "#{type_name}GetTypeID", [], CF.find_type(:cftypeid)
137
+ @@type_map[Sec.send("#{type_name}GetTypeID")] = self
138
+ end
139
+ end
140
+
141
+ # If the result is non-zero raises an exception.
142
+ #
143
+ # The exception will have the result code as well as a human readable description
144
+ #
145
+ # @param [Integer] result the status code to check
146
+ # @raise [Keychain::Error] is the result is non zero
147
+ def self.check_osstatus result
148
+ if result != 0
149
+ case result
150
+ when Sec.enum_value(:errSecDuplicateItem)
151
+ raise Keychain::DuplicateItemError.new(result)
152
+ when Sec.enum_value(:errCancelled)
153
+ raise Keychain::UserCancelledError.new(result)
154
+ when Sec.enum_value(:errSecAuthFailed)
155
+ raise Keychain::AuthFailedError.new(result)
156
+ when Sec.enum_value(:errSecNoSuchKeychain)
157
+ raise Keychain::NoSuchKeychainError.new(result)
158
+ else
159
+ raise Keychain::Error.new(result)
160
+ end
161
+ end
162
+ end
163
+
164
+
165
+ end
@@ -0,0 +1,4 @@
1
+ module Keychain
2
+ # The current version string
3
+ VERSION = '0.1.0'
4
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe Keychain::Item do
4
+ before(:each) do
5
+ @keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
6
+ @keychain.generic_passwords.create :service => 'some-service', :account => 'some-account', :password => 'some-password'
7
+ end
8
+
9
+ after(:each) do
10
+ @keychain.delete
11
+ end
12
+
13
+ def find_item
14
+ @keychain.generic_passwords.where(:service => 'some-service').first
15
+ end
16
+
17
+ subject {find_item}
18
+
19
+ describe 'keychain' do
20
+ it 'should return the keychain containing the item' do
21
+ subject.keychain.should == @keychain
22
+ end
23
+ end
24
+
25
+ describe 'password' do
26
+ it 'should retrieve the password' do
27
+ subject.password.should == 'some-password'
28
+ end
29
+ end
30
+
31
+ describe 'service' do
32
+ it 'should retrieve the service' do
33
+ subject.service.should == 'some-service'
34
+ end
35
+ end
36
+
37
+ describe 'account' do
38
+ it 'should retrieve the account' do
39
+ subject.account.should == 'some-account'
40
+ end
41
+ end
42
+
43
+ describe 'created_at' do
44
+ it 'should retrieve the item creation date' do
45
+ subject.created_at.should be_within(2).of(Time.now)
46
+ end
47
+ end
48
+
49
+ describe 'save' do
50
+ it 'should update attributes and password' do
51
+ subject.password = 'new-password'
52
+ subject.account = 'new-account'
53
+ subject.save!
54
+
55
+ fresh = find_item
56
+ fresh.password.should == 'new-password'
57
+ fresh.account.should == 'new-account'
58
+ end
59
+ end
60
+
61
+ describe 'delete' do
62
+ it 'should remove the item from the keychain' do
63
+ subject.delete
64
+ find_item.should be_nil
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,220 @@
1
+ require 'spec_helper'
2
+
3
+ describe Keychain do
4
+ describe 'default' do
5
+ it "should return the login keychain" do
6
+ Keychain.default.path.should == File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
7
+ end
8
+ end
9
+
10
+ describe 'open' do
11
+ it 'should create a keychain reference to a path' do
12
+ keychain = Keychain.open(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
13
+ keychain.path.should == Keychain.default.path
14
+ end
15
+ end
16
+
17
+ describe 'new' do
18
+ it 'should create the keychain' do
19
+ begin
20
+ keychain = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"),
21
+ 'password');
22
+ File.exists?(keychain.path).should be_true
23
+ ensure
24
+ keychain.delete
25
+ end
26
+ end
27
+ end
28
+
29
+ describe 'exists?' do
30
+ context 'the keychain exists' do
31
+ it 'should return true' do
32
+ Keychain.default.exists?.should be_true
33
+ end
34
+ end
35
+
36
+ context 'the keychain does not exist' do
37
+ it 'should return false' do
38
+ k = Keychain.open('/some/path/that/does/not/exist')
39
+ k.exists?.should be_false
40
+ end
41
+ end
42
+ end
43
+
44
+ describe 'settings' do
45
+ before(:all) do
46
+ @keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
47
+ end
48
+
49
+ it 'should read/write lock_on_sleep' do
50
+ @keychain.lock_on_sleep = true
51
+ @keychain.lock_on_sleep?.should == true
52
+ @keychain.lock_on_sleep = false
53
+ @keychain.lock_on_sleep?.should == false
54
+ end
55
+
56
+ it 'should read/write lock_interval' do
57
+ @keychain.lock_interval = 12345
58
+ @keychain.lock_interval.should == 12345
59
+ end
60
+
61
+ after(:all) do
62
+ @keychain.delete
63
+ end
64
+ end
65
+
66
+ describe 'locking' do
67
+ context 'with a locked keychain' do
68
+ before :each do
69
+ @keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
70
+ @keychain.lock!
71
+ end
72
+
73
+ it 'should raise on invalid password' do
74
+ expect {@keychain.unlock! 'badpassword'}.to raise_error(Keychain::AuthFailedError)
75
+ end
76
+
77
+ it 'should unlock on valid password' do
78
+ @keychain.unlock! 'pass'
79
+ @keychain.should_not be_locked
80
+ end
81
+ end
82
+ end
83
+
84
+ shared_examples_for 'item collection' do
85
+
86
+ before(:each) do
87
+ @keychain_1 = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
88
+ @keychain_2 = Keychain.create(File.join(Dir.tmpdir, "keychain_2_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
89
+ @keychain_3 = Keychain.create(File.join(Dir.tmpdir, "keychain_3_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
90
+
91
+ add_fixtures
92
+ end
93
+
94
+ after(:each) do
95
+ @keychain_1.delete
96
+ @keychain_2.delete
97
+ @keychain_3.delete
98
+ end
99
+
100
+ describe('create') do
101
+ it 'should add a password' do
102
+ item = @keychain_1.send(subject).create(create_arguments)
103
+ item.should be_a(Keychain::Item)
104
+ item.klass.should == expected_kind
105
+ item.password.should == 'some-password'
106
+ end
107
+
108
+ it 'should be findable' do
109
+ @keychain_1.send(subject).create(create_arguments)
110
+ item = @keychain_1.send(subject).where(search_for_created_arguments).first
111
+ item.password.should == 'some-password'
112
+ end
113
+
114
+ context 'when a duplicate item exists' do
115
+ before(:each) do
116
+ @keychain_1.send(subject).create(create_arguments)
117
+ end
118
+
119
+ it 'should raise Keychain::DuplicateItemError' do
120
+ expect {@keychain_1.send(subject).create(create_arguments)}.to raise_error(Keychain::DuplicateItemError)
121
+ end
122
+ end
123
+ end
124
+
125
+ describe('all') do
126
+
127
+ context 'when the keychain does not contains a matching item' do
128
+ it 'should return []' do
129
+ @keychain_1.send(subject).where(search_arguments_with_no_results).all.should == []
130
+ end
131
+ end
132
+
133
+ it 'should return an array of results' do
134
+ item = @keychain_1.send(subject).where(search_arguments).all.first
135
+ item.should be_a(Keychain::Item)
136
+ item.password.should == 'some-password-1'
137
+ end
138
+
139
+ context 'searching all keychains' do
140
+ context 'when the keychain does contains matching items' do
141
+ it 'should return all of them' do
142
+ Keychain.send(subject).where(search_arguments_with_multiple_results).all.length.should == 3
143
+ end
144
+ end
145
+
146
+ context 'when the limit is option is set' do
147
+ it 'should limit the return set' do
148
+ Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length.should == 1
149
+ end
150
+ end
151
+
152
+ context 'when a subset of keychains is specified' do
153
+ it 'should return items from those keychains' do
154
+ Keychain.send(subject).where(search_arguments_with_multiple_results).in(@keychain_1, @keychain_2).all.length.should == 2
155
+ end
156
+ end
157
+ end
158
+ end
159
+ describe 'first' do
160
+ context 'when the keychain does not contain a matching item' do
161
+ it 'should return nil' do
162
+ item = @keychain_1.send(subject).where(search_arguments_with_no_results).first.should be_nil
163
+ end
164
+ end
165
+
166
+ context 'when the keychain does contain a matching item' do
167
+ it 'should find it' do
168
+ item = @keychain_1.send(subject).where(search_arguments).first
169
+ item.should be_a(Keychain::Item)
170
+ item.password.should == 'some-password-1'
171
+ end
172
+ end
173
+
174
+ context 'when a different keychain contains a matching item' do
175
+ before(:each) do
176
+ item = @keychain_1.send(subject).create(create_arguments)
177
+ end
178
+
179
+ it 'should not find it' do
180
+ @keychain_2.send(subject).where(search_arguments).first.should be_nil
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ describe 'generic_passwords' do
187
+ subject { :generic_passwords }
188
+ let(:create_arguments){{:service => 'aservice', :account => 'anaccount-foo', :password =>'some-password'}}
189
+ let(:search_for_created_arguments){{:service => 'aservice'}}
190
+
191
+ let(:search_arguments){{:service => 'aservice-1'}}
192
+ let(:search_arguments_with_no_results){{:service => 'doesntexist'}}
193
+ let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
194
+ let(:expected_kind) {'genp'}
195
+
196
+ def add_fixtures
197
+ @keychain_1.generic_passwords.create(:service => 'aservice-1', :account => 'anaccount', :password => 'some-password-1')
198
+ @keychain_2.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-2')
199
+ @keychain_3.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-3')
200
+ end
201
+ it_behaves_like 'item collection'
202
+ end
203
+
204
+ describe 'internet_passwords' do
205
+ subject { :internet_passwords }
206
+ let(:create_arguments){{:server => 'dressipi.example.com', :account => 'anaccount-foo', :password =>'some-password', :protocol => Keychain::Protocols::HTTP}}
207
+ let(:search_for_created_arguments){{:server => 'dressipi.example.com', :protocol => Keychain::Protocols::HTTP}}
208
+ let(:search_arguments){{:server => 'dressipi-1.example.com', :protocol => Keychain::Protocols::HTTP}}
209
+ let(:search_arguments_with_no_results){{:server => 'dressipi.example.com'}}
210
+ let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
211
+ let(:expected_kind) {'inet'}
212
+
213
+ def add_fixtures
214
+ @keychain_1.internet_passwords.create(:server => 'dressipi-1.example.com', :account => 'anaccount', :password => 'some-password-1', :protocol => Keychain::Protocols::HTTP)
215
+ @keychain_2.internet_passwords.create(:server => 'dressipi-2.example.com', :account => 'anaccount', :password => 'some-password-2', :protocol => Keychain::Protocols::HTTP)
216
+ @keychain_3.internet_passwords.create(:server => 'dressipi-3.example.com', :account => 'anaccount', :password => 'some-password-3', :protocol => Keychain::Protocols::HTTP)
217
+ end
218
+ it_behaves_like 'item collection'
219
+ end
220
+ end