trocla 0.3.0 → 0.5.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,78 @@
1
+ # the default vault based store
2
+ class Trocla::Stores::Vault < Trocla::Store
3
+ attr_reader :vault, :mount, :destroy
4
+
5
+ def initialize(config, trocla)
6
+ super(config, trocla)
7
+ require 'vault'
8
+ @mount = (config.delete(:mount) || 'kv')
9
+ @destroy = (config.delete(:destroy) || false)
10
+ # load expire support by default
11
+ @vault = Vault::Client.new(config)
12
+ end
13
+
14
+ def close; end
15
+
16
+ def get(key, format)
17
+ read(key)[format.to_sym]
18
+ end
19
+
20
+ def formats(key)
21
+ read(key).keys
22
+ end
23
+
24
+ def search(key)
25
+ arr = key.split('/')
26
+ regexp = Regexp.new(arr.pop(1)[0].to_s)
27
+ path = arr.join('/')
28
+ list = vault.kv(mount).list(path)
29
+ list.map! do |l|
30
+ if regexp.match(l)
31
+ path.empty? ? l : [path, l].join('/')
32
+ end
33
+ end
34
+ list.compact
35
+ end
36
+
37
+ private
38
+
39
+ def read(key)
40
+ k = vault.kv(mount).read(key)
41
+ k.nil? ? {} : k.data
42
+ end
43
+
44
+ def write(key, value, options = {})
45
+ vault.kv(mount).write_metadata(key, convert_metadata(options)) unless options.empty?
46
+ vault.kv(mount).write(key, value)
47
+ end
48
+
49
+ def set_plain(key, value, options)
50
+ set_format(key, 'plain', value, options)
51
+ end
52
+
53
+ def set_format(key, format, value, options)
54
+ write(
55
+ key,
56
+ read(key).merge({ format.to_sym => value }),
57
+ options
58
+ )
59
+ end
60
+
61
+ def delete_all(key)
62
+ destroy ? vault.kv(mount).destroy(key) : vault.kv(mount).delete(key)
63
+ end
64
+
65
+ def delete_format(key, format)
66
+ old = read(key)
67
+ new = old.reject { |k, _| k == format.to_sym }
68
+ new.empty? ? delete_all(key) : write(key, new)
69
+ old[format.to_sym]
70
+ end
71
+
72
+ def convert_metadata(metadatas)
73
+ metadatas.transform_keys!(&:to_sym)
74
+ metadatas[:delete_version_after] = metadatas.delete(:expire) if metadatas[:expire]
75
+ %i[random profiles expires length].each { |k| metadatas.delete(k) }
76
+ metadatas
77
+ end
78
+ end
data/lib/trocla/stores.rb CHANGED
@@ -7,7 +7,7 @@ class Trocla::Stores
7
7
  end
8
8
 
9
9
  def all
10
- @all ||= Dir[ path '*' ].collect do |store|
10
+ @all ||= Dir[path '*'].collect do |store|
11
11
  File.basename(store, '.rb').downcase
12
12
  end
13
13
  end
@@ -17,10 +17,11 @@ class Trocla::Stores
17
17
  end
18
18
 
19
19
  private
20
+
20
21
  def stores
21
22
  @@stores ||= Hash.new do |hash, store|
22
23
  store = store.to_s.downcase
23
- if File.exists?(path(store))
24
+ if File.exist?(path(store))
24
25
  require "trocla/stores/#{store}"
25
26
  class_name = "Trocla::Stores::#{store.capitalize}"
26
27
  hash[store] = (eval class_name)
data/lib/trocla/util.rb CHANGED
@@ -1,30 +1,30 @@
1
1
  require 'securerandom'
2
2
  class Trocla
3
+ # Utils
3
4
  class Util
4
5
  class << self
5
- def random_str(length=12, charset='default')
6
- _charsets = charsets[charset] || charsets['default']
7
- (1..length).collect{|a| _charsets[SecureRandom.random_number(_charsets.size)] }.join.to_s
6
+ def random_str(length = 12, charset = 'default')
7
+ char = charsets[charset] || charsets['default']
8
+ charsets_size = char.size
9
+ (1..length).collect { |_| char[rand_num(charsets_size)] }.join.to_s
8
10
  end
9
11
 
10
- def salt(length=8)
11
- alphanumeric_size = alphanumeric.size
12
- (1..length).collect{|a| alphanumeric[SecureRandom.random_number(alphanumeric_size)] }.join.to_s
12
+ def salt(length = 8)
13
+ random_str(length, 'alphanumeric')
13
14
  end
14
15
 
15
16
  private
16
17
 
18
+ def rand_num(n)
19
+ SecureRandom.random_number(n)
20
+ end
21
+
17
22
  def charsets
18
23
  @charsets ||= begin
19
24
  h = {
20
- 'default' => chars,
21
- 'alphanumeric' => alphanumeric,
22
- 'shellsafe' => shellsafe,
23
- 'windowssafe' => windowssafe,
24
- 'numeric' => numeric,
25
- 'hexadecimal' => hexadecimal,
26
- 'consolesafe' => consolesafe,
27
- 'typesafe' => typesafe,
25
+ 'default' => chars, 'alphanumeric' => alphanumeric, 'shellsafe' => shellsafe,
26
+ 'windowssafe' => windowssafe, 'numeric' => numeric, 'hexadecimal' => hexadecimal,
27
+ 'consolesafe' => consolesafe, 'typesafe' => typesafe
28
28
  }
29
29
  h.each { |k, v| h[k] = v.uniq }
30
30
  end
@@ -33,36 +33,47 @@ class Trocla
33
33
  def chars
34
34
  @chars ||= shellsafe + special_chars
35
35
  end
36
+
36
37
  def shellsafe
37
38
  @shellsafe ||= alphanumeric + shellsafe_chars
38
39
  end
40
+
39
41
  def windowssafe
40
42
  @windowssafe ||= alphanumeric + windowssafe_chars
41
43
  end
44
+
42
45
  def consolesafe
43
46
  @consolesafe ||= alphanumeric + consolesafe_chars
44
47
  end
48
+
45
49
  def hexadecimal
46
50
  @hexadecimal ||= numeric + ('a'..'f').to_a
47
51
  end
52
+
48
53
  def alphanumeric
49
54
  @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + numeric
50
55
  end
56
+
51
57
  def numeric
52
58
  @numeric ||= ('0'..'9').to_a
53
59
  end
60
+
54
61
  def typesafe
55
62
  @typesafe ||= ('a'..'x').to_a - ['i'] - ['l'] + ('A'..'X').to_a - ['I'] - ['L'] + ('1'..'9').to_a
56
63
  end
64
+
57
65
  def special_chars
58
- @special_chars ||= "*()&![]{}-".split(//)
66
+ @special_chars ||= '*()&![]{}-'.split(//)
59
67
  end
68
+
60
69
  def shellsafe_chars
61
- @shellsafe_chars ||= "+%/@=?_.,:".split(//)
70
+ @shellsafe_chars ||= '+%/@=?_.,:'.split(//)
62
71
  end
72
+
63
73
  def windowssafe_chars
64
- @windowssafe_chars ||= "+%/@=?_.,".split(//)
74
+ @windowssafe_chars ||= '+%/@=?_.,'.split(//)
65
75
  end
76
+
66
77
  def consolesafe_chars
67
78
  @consolesafe_chars ||= '+.-,_'.split(//)
68
79
  end
@@ -1,20 +1,23 @@
1
1
  # encoding: utf-8
2
+
2
3
  class Trocla
4
+ # VERSION
3
5
  class VERSION
4
6
  version = {}
5
7
  File.read(File.join(File.dirname(__FILE__), '../', 'VERSION')).each_line do |line|
6
- type, value = line.chomp.split(":")
8
+ type, value = line.chomp.split(':')
7
9
  next if type =~ /^\s+$/ || value =~ /^\s+$/
10
+
8
11
  version[type] = value
9
12
  end
10
-
13
+
11
14
  MAJOR = version['major']
12
15
  MINOR = version['minor']
13
16
  PATCH = version['patch']
14
17
  BUILD = version['build']
15
-
18
+
16
19
  STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
17
-
20
+
18
21
  def self.version
19
22
  STRING
20
23
  end
data/lib/trocla.rb CHANGED
@@ -4,16 +4,17 @@ require 'trocla/formats'
4
4
  require 'trocla/encryptions'
5
5
  require 'trocla/stores'
6
6
 
7
+ # Trocla class
7
8
  class Trocla
8
- def initialize(config_file=nil)
9
+ def initialize(config_file = nil)
9
10
  if config_file
10
11
  @config_file = File.expand_path(config_file)
11
- elsif File.exists?(def_config_file=File.expand_path('~/.troclarc.yaml')) || File.exists?(def_config_file=File.expand_path('/etc/troclarc.yaml'))
12
+ elsif File.exist?(def_config_file = File.expand_path('~/.troclarc.yaml')) || File.exist?(def_config_file = File.expand_path('/etc/troclarc.yaml'))
12
13
  @config_file = def_config_file
13
14
  end
14
15
  end
15
16
 
16
- def self.open(config_file=nil)
17
+ def self.open(config_file = nil)
17
18
  trocla = Trocla.new(config_file)
18
19
 
19
20
  if block_given?
@@ -24,70 +25,76 @@ class Trocla
24
25
  end
25
26
  end
26
27
 
27
- def password(key,format,options={})
28
+ def password(key, format, options={})
28
29
  # respect a default profile, but let the
29
30
  # profiles win over the default options
30
31
  options['profiles'] ||= config['options']['profiles']
31
- if options['profiles']
32
- options = merge_profiles(options['profiles']).merge(options)
33
- end
32
+ options = merge_profiles(options['profiles']).merge(options) if options['profiles']
34
33
  options = config['options'].merge(options)
35
34
 
36
35
  raise "Format #{format} is not supported! Supported formats: #{Trocla::Formats.all.join(', ')}" unless Trocla::Formats::available?(format)
37
36
 
38
- unless (password=get_password(key,format,options)).nil?
37
+ unless (password = get_password(key, format, options)).nil?
39
38
  return password
40
39
  end
41
40
 
42
- plain_pwd = get_password(key,'plain',options)
41
+ plain_pwd = get_password(key, 'plain', options)
43
42
  if options['random'] && plain_pwd.nil?
44
- plain_pwd = Trocla::Util.random_str(options['length'].to_i,options['charset'])
45
- set_password(key,'plain',plain_pwd,options) unless format == 'plain'
43
+ plain_pwd = Trocla::Util.random_str(options['length'].to_i, options['charset'])
44
+ set_password(key, 'plain', plain_pwd, options) unless format == 'plain'
46
45
  elsif !options['random'] && plain_pwd.nil?
47
46
  raise "Password must be present as plaintext if you don't want a random password"
48
47
  end
49
- pwd = self.formats(format).format(plain_pwd,options)
48
+ pwd = self.formats(format).format(plain_pwd, options)
50
49
  # it's possible that meanwhile another thread/process was faster in
51
50
  # formating the password. But we want todo that second lookup
52
51
  # only for expensive formats
53
52
  if self.formats(format).expensive?
54
- get_password(key,format,options) || set_password(key, format, pwd, options)
53
+ get_password(key, format, options) || set_password(key, format, pwd, options)
55
54
  else
56
55
  set_password(key, format, pwd, options)
57
56
  end
58
57
  end
59
58
 
60
- def get_password(key, format, options={})
61
- render(format,decrypt(store.get(key,format)),options)
59
+ def get_password(key, format, options = {})
60
+ render(format, decrypt(store.get(key, format)), options)
62
61
  end
63
62
 
64
- def reset_password(key,format,options={})
65
- set_password(key,format,nil,options)
66
- password(key,format,options)
63
+ def reset_password(key, format, options = {})
64
+ delete_password(key, format)
65
+ password(key, format, options)
67
66
  end
68
67
 
69
- def delete_password(key,format=nil,options={})
70
- v = store.delete(key,format)
68
+ def delete_password(key, format = nil, options = {})
69
+ v = store.delete(key, format)
71
70
  if v.is_a?(Hash)
72
- Hash[*v.map do |f,encrypted_value|
73
- [f,render(format,decrypt(encrypted_value),options)]
71
+ Hash[*v.map do |f, encrypted_value|
72
+ [f, render(format, decrypt(encrypted_value), options)]
74
73
  end.flatten]
75
74
  else
76
- render(format,decrypt(v),options)
75
+ render(format, decrypt(v), options)
77
76
  end
78
77
  end
79
78
 
80
- def set_password(key,format,password,options={})
81
- store.set(key,format,encrypt(password),options)
82
- render(format,password,options)
79
+ def set_password(key, format, password, options = {})
80
+ store.set(key, format, encrypt(password), options)
81
+ render(format, password, options)
82
+ end
83
+
84
+ def available_format(key, options = {})
85
+ render(false, store.formats(key), options)
86
+ end
87
+
88
+ def search_key(key, options = {})
89
+ render(false, store.search(key), options)
83
90
  end
84
91
 
85
92
  def formats(format)
86
- (@format_cache||={})[format] ||= Trocla::Formats[format].new(self)
93
+ (@format_cache ||= {})[format] ||= Trocla::Formats[format].new(self)
87
94
  end
88
95
 
89
96
  def encryption
90
- @encryption ||= Trocla::Encryptions[config['encryption']].new(config['encryption_options'],self)
97
+ @encryption ||= Trocla::Encryptions[config['encryption']].new(config['encryption_options'], self)
91
98
  end
92
99
 
93
100
  def config
@@ -99,6 +106,7 @@ class Trocla
99
106
  end
100
107
 
101
108
  private
109
+
102
110
  def store
103
111
  @store ||= build_store
104
112
  end
@@ -106,19 +114,20 @@ class Trocla
106
114
  def build_store
107
115
  s = config['store']
108
116
  clazz = if s.is_a?(Symbol)
109
- Trocla::Stores[s]
110
- else
111
- require config['store_require'] if config['store_require']
112
- eval(s)
113
- end
114
- clazz.new(config['store_options'],self)
117
+ Trocla::Stores[s]
118
+ else
119
+ require config['store_require'] if config['store_require']
120
+ eval(s)
121
+ end
122
+ clazz.new(config['store_options'], self)
115
123
  end
116
124
 
117
125
  def read_config
118
126
  if @config_file.nil?
119
127
  default_config
120
128
  else
121
- raise "Configfile #{@config_file} does not exist!" unless File.exists?(@config_file)
129
+ raise "Configfile #{@config_file} does not exist!" unless File.exist?(@config_file)
130
+
122
131
  c = default_config.merge(YAML.load(File.read(@config_file)))
123
132
  c['profiles'] = default_config['profiles'].merge(c['profiles'])
124
133
  # migrate all options to new store options
@@ -136,12 +145,13 @@ class Trocla
136
145
 
137
146
  def decrypt(value)
138
147
  return nil if value.nil?
148
+
139
149
  encryption.decrypt(value)
140
150
  end
141
151
 
142
- def render(format,output,options={})
143
- if format && output && f=self.formats(format)
144
- f.render(output,options['render']||{})
152
+ def render(format, output, options = {})
153
+ if format && output && f = self.formats(format)
154
+ f.render(output, options['render'] || {})
145
155
  else
146
156
  output
147
157
  end
@@ -149,14 +159,14 @@ class Trocla
149
159
 
150
160
  def default_config
151
161
  require 'yaml'
152
- YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__),'trocla','default_config.yaml'))))
162
+ YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__), 'trocla', 'default_config.yaml'))))
153
163
  end
154
164
 
155
165
  def merge_profiles(profiles)
156
- Array(profiles).inject({}) do |res,profile|
166
+ Array(profiles).inject({}) do |res, profile|
157
167
  raise "No such profile #{profile} defined" unless profile_hash = config['profiles'][profile]
168
+
158
169
  profile_hash.merge(res)
159
170
  end
160
171
  end
161
-
162
172
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'jruby' if Object.const_defined?(:RUBY_ENGINE) and RUBY_ENGINE == 'jruby'
3
4
  require 'rspec'
4
5
  require 'rspec/pending_for'
5
6
  require 'yaml'
@@ -35,6 +36,11 @@ RSpec.shared_examples "encryption_basics" do
35
36
  expect(@trocla.get_password('some_pass', 'plain')).to eql('super secret')
36
37
  end
37
38
 
39
+ it "resets passwords" do
40
+ @trocla.set_password('some_pass', 'plain', 'super secret')
41
+ expect(@trocla.reset_password('some_pass', 'plain')).not_to eql('super secret')
42
+ end
43
+
38
44
  end
39
45
  describe 'deleting' do
40
46
  it "plain" do
@@ -225,7 +231,13 @@ RSpec.shared_examples 'store_validation' do |store|
225
231
  end
226
232
 
227
233
  def default_config
228
- @default_config ||= YAML.load(File.read(File.expand_path(base_dir+'/lib/trocla/default_config.yaml')))
234
+ @default_config ||= begin
235
+ config_path = [
236
+ File.expand_path(base_dir+'/lib/trocla/default_config.yaml'),
237
+ File.expand_path(File.dirname($LOADED_FEATURES.grep(/trocla.rb/)[0])+'/trocla/default_config.yaml'),
238
+ ].find { |p| File.exists?(p) }
239
+ YAML.load(File.read(config_path))
240
+ end
229
241
  end
230
242
 
231
243
  def test_config
@@ -0,0 +1,25 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe 'Trocla::Format::Pgsql' do
4
+ before(:each) do
5
+ expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
6
+ @trocla = Trocla.new
7
+ end
8
+
9
+ describe 'default pgsql' do
10
+ it 'create a pgsql password keypair without options in sha256' do
11
+ pass = @trocla.password('pgsql_password_sh256', 'pgsql', {})
12
+ expect(pass).to match(/^SCRAM-SHA-256\$(.*):(.*)\$(.*):/)
13
+ end
14
+ end
15
+
16
+ describe 'pgsql in md5 encode' do
17
+ it 'create a pgsql password in md5 encode' do
18
+ pass = @trocla.password(
19
+ 'pgsql_password_md5', 'pgsql',
20
+ { 'username' => 'toto', 'encode' => 'md5' }
21
+ )
22
+ expect(pass).to match(/^md5/)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe "Trocla::Format::Sshkey" do
4
+
5
+ before(:each) do
6
+ expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
7
+ @trocla = Trocla.new
8
+ end
9
+
10
+ let(:sshkey_options) do
11
+ {
12
+ 'type' => 'rsa',
13
+ 'bits' => 4096,
14
+ 'comment' => 'My ssh key'
15
+ }
16
+ end
17
+
18
+ describe "sshkey" do
19
+ it "is able to create an ssh keypair without options" do
20
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', {})
21
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
22
+ expect(sshkey).to match(/ssh-/)
23
+ end
24
+
25
+ it "is able to create an ssh keypair with options" do
26
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', sshkey_options)
27
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
28
+ expect(sshkey).to match(/ssh-/)
29
+ expect(sshkey).to end_with('My ssh key')
30
+ end
31
+
32
+ it 'supports fetching only the priv key' do
33
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'privonly' => true }})
34
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
35
+ expect(sshkey).not_to match(/ssh-/)
36
+ end
37
+
38
+ it 'supports fetching only the pub key' do
39
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'pubonly' => true }})
40
+ expect(sshkey).to start_with('ssh-rsa')
41
+ expect(sshkey).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
42
+ end
43
+
44
+ it "is able to create an ssh keypair with a passphrase" do
45
+ sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'passphrase' => 'spec' })
46
+ expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
47
+ expect(sshkey).to match(/ssh-/)
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -44,7 +44,9 @@ describe "Trocla::Format::X509" do
44
44
  expect(cert.public_key.n.num_bytes * 8).to eq(4096)
45
45
  expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
46
46
  # it's a self signed cert and NOT a CA
47
- expect(verify(cert,cert)).to be false
47
+ # Change of behavior on openssl side: https://github.com/openssl/openssl/issues/15146
48
+ validates_self_even_if_no_ca = Gem::Version.new(%x{openssl version}.split(' ')[1]) > Gem::Version.new('1.1.1g')
49
+ expect(verify(cert,cert)).to be validates_self_even_if_no_ca
48
50
 
49
51
  v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
50
52
  expect(v).to eq('CA:FALSE')
@@ -136,6 +138,11 @@ describe "Trocla::Format::X509" do
136
138
  expect(cert_str).not_to match(/-----BEGIN CERTIFICATE-----/)
137
139
  expect(cert_str).to match(/-----BEGIN RSA PRIVATE KEY-----/)
138
140
  end
141
+ it 'supports fetching only the publickey' do
142
+ pkey_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'publickeyonly' => true }))
143
+ expect(pkey_str).not_to match(/-----BEGIN CERTIFICATE-----/)
144
+ expect(pkey_str).to match(/-----BEGIN PUBLIC KEY-----/)
145
+ end
139
146
  it 'supports fetching only the cert' do
140
147
  cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
141
148
  expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
data/spec/trocla_spec.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # -- encoding : utf-8 --
1
2
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
3
 
3
4
  describe "Trocla" do
@@ -71,6 +72,9 @@ describe "Trocla" do
71
72
  expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
72
73
  end
73
74
  it 'is possible to combine profiles but first profile wins 3' do
75
+ # mysql profile uses a 32 long random pwd with shell safe characters
76
+ # and we want to use a fixed random str here https://github.com/duritong/trocla/issues/55
77
+ allow(Trocla::Util).to receive(:random_str).with(32,'shellsafe') { "jmNi6+7dsUn@H?vfbXCq=ULEGPW,u:hu" }
74
78
  pwd = @trocla.password('some_test3','plain', 'profiles' => ['mysql','login'])
75
79
  expect(pwd).not_to be_empty
76
80
  expect(pwd.length).to eq(32)
@@ -99,6 +103,11 @@ describe "Trocla" do
99
103
  expect(@trocla.get_password('set_test2','md5crypt')).to eq(md5crypt)
100
104
  expect(@trocla.get_password('set_test2','plain')).to eq(plain)
101
105
  end
106
+
107
+ it 'is able to set password with umlauts and other UTF-8 charcters' do
108
+ expect(myumlaut = @trocla.set_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
109
+ expect(@trocla.get_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
110
+ end
102
111
  end
103
112
 
104
113
  describe "reset_password" do
@@ -120,6 +129,40 @@ describe "Trocla" do
120
129
  end
121
130
  end
122
131
 
132
+ describe "search_key" do
133
+ it "search a specific key" do
134
+ keys = ['search_key','search_key1','key_search','key_search2']
135
+ keys.each do |k|
136
+ @trocla.password(k,'plain')
137
+ end
138
+ expect(@trocla.search_key('search_key1').length).to eq(1)
139
+ end
140
+ it "ensure search regex is ok" do
141
+ keys = ['search_key2','search_key3','key_search2','key_search4']
142
+ keys.each do |k|
143
+ @trocla.password(k,'plain')
144
+ end
145
+ expect(@trocla.search_key('key').length).to eq(4)
146
+ expect(@trocla.search_key('^search').length).to eq(2)
147
+ expect(@trocla.search_key('ch.*3').length).to eq(1)
148
+ expect(@trocla.search_key('ch.*[3-4]$').length).to eq(2)
149
+ expect(@trocla.search_key('ch.*1')).to be_nil
150
+ end
151
+ end
152
+
153
+ describe "list_format" do
154
+ it "list available formats for key" do
155
+ formats = ['plain','mysql']
156
+ formats.each do |f|
157
+ @trocla.password('list_key',f)
158
+ end
159
+ expect(@trocla.available_format('list_key')).to eq(formats)
160
+ end
161
+ it "no return if key doesn't exist" do
162
+ expect(@trocla.available_format('list_key1')).to be_nil
163
+ end
164
+ end
165
+
123
166
  describe "delete_password" do
124
167
  it "deletes all passwords if no format is given" do
125
168
  expect(@trocla.password('delete_test1','mysql')).not_to be_nil