trocla 0.3.0 → 0.5.0

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