ruby-keychain 0.1.0

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