mpw 4.1.1 → 4.2.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +0 -1
- data/.travis.yml +10 -6
- data/CHANGELOG.md +32 -11
- data/Gemfile +6 -0
- data/README.md +32 -31
- data/VERSION +1 -1
- data/bin/mpw-config +1 -1
- data/bin/mpw-update +2 -0
- data/bin/mpw-wallet +4 -4
- data/i18n/en.yml +50 -48
- data/i18n/fr.yml +2 -0
- data/lib/mpw/cli.rb +81 -56
- data/lib/mpw/config.rb +17 -18
- data/lib/mpw/item.rb +14 -5
- data/lib/mpw/mpw.rb +34 -31
- data/test/files/fixtures-import.yml +19 -0
- data/test/files/fixtures.yml +23 -23
- data/test/init.rb +6 -2
- data/test/test_cli.rb +265 -0
- data/test/test_config.rb +13 -11
- data/test/test_item.rb +75 -73
- data/test/test_mpw.rb +24 -28
- data/test/test_translate.rb +1 -1
- metadata +7 -5
- data/test/tests.rb +0 -7
data/lib/mpw/mpw.rb
CHANGED
@@ -25,7 +25,11 @@ require 'mpw/item'
|
|
25
25
|
|
26
26
|
module MPW
|
27
27
|
class MPW
|
28
|
-
#
|
28
|
+
# @param key [String] gpg key name
|
29
|
+
# @param wallet_file [String] path of the wallet file
|
30
|
+
# @param gpg_pass [String] password of the gpg key
|
31
|
+
# @param gpg_exe [String] path of the gpg executable
|
32
|
+
# @param pinmode [Boolean] enable the gpg pinmode
|
29
33
|
def initialize(key, wallet_file, gpg_pass = nil, gpg_exe = nil, pinmode = false)
|
30
34
|
@key = key
|
31
35
|
@gpg_pass = gpg_pass
|
@@ -98,7 +102,7 @@ module MPW
|
|
98
102
|
raise "#{I18n.t('error.mpw_file.read_data')}\n#{e}"
|
99
103
|
end
|
100
104
|
|
101
|
-
# Encrypt
|
105
|
+
# Encrypt all data in tarball
|
102
106
|
def write_data
|
103
107
|
data = {}
|
104
108
|
tmp_file = "#{@wallet_file}.tmp"
|
@@ -154,7 +158,7 @@ module MPW
|
|
154
158
|
end
|
155
159
|
|
156
160
|
# Get a password
|
157
|
-
#
|
161
|
+
# @param id [String] the item id
|
158
162
|
def get_password(id)
|
159
163
|
password = decrypt(@passwords[id])
|
160
164
|
|
@@ -165,9 +169,9 @@ module MPW
|
|
165
169
|
end
|
166
170
|
end
|
167
171
|
|
168
|
-
# Set a password
|
169
|
-
#
|
170
|
-
#
|
172
|
+
# Set a new password for an item
|
173
|
+
# @param id [String] the item id
|
174
|
+
# @param password [String] the new password
|
171
175
|
def set_password(id, password)
|
172
176
|
salt = MPW.password(length: Random.rand(4..9))
|
173
177
|
password = "$#{salt}::#{password}"
|
@@ -176,13 +180,13 @@ module MPW
|
|
176
180
|
end
|
177
181
|
|
178
182
|
# Return the list of all gpg keys
|
179
|
-
#
|
183
|
+
# @return [Array] the gpg keys name
|
180
184
|
def list_keys
|
181
185
|
@keys.keys
|
182
186
|
end
|
183
187
|
|
184
188
|
# Add a public key
|
185
|
-
#
|
189
|
+
# @param key [String] new public key file or name
|
186
190
|
def add_key(key)
|
187
191
|
if File.exist?(key)
|
188
192
|
data = File.open(key).read
|
@@ -200,7 +204,7 @@ module MPW
|
|
200
204
|
end
|
201
205
|
|
202
206
|
# Delete a public key
|
203
|
-
#
|
207
|
+
# @param key [String] public key to delete
|
204
208
|
def delete_key(key)
|
205
209
|
@keys.delete(key)
|
206
210
|
@passwords.each_key { |id| set_password(id, get_password(id)) }
|
@@ -208,7 +212,7 @@ module MPW
|
|
208
212
|
end
|
209
213
|
|
210
214
|
# Add a new item
|
211
|
-
# @
|
215
|
+
# @param item [Item]
|
212
216
|
def add(item)
|
213
217
|
raise I18n.t('error.bad_class') unless item.instance_of?(Item)
|
214
218
|
raise I18n.t('error.empty') if item.empty?
|
@@ -217,8 +221,8 @@ module MPW
|
|
217
221
|
end
|
218
222
|
|
219
223
|
# Search in some csv data
|
220
|
-
# @
|
221
|
-
# @
|
224
|
+
# @param options [Hash]
|
225
|
+
# @return [Array] a list with the resultat of the search
|
222
226
|
def list(**options)
|
223
227
|
result = []
|
224
228
|
|
@@ -240,9 +244,9 @@ module MPW
|
|
240
244
|
result
|
241
245
|
end
|
242
246
|
|
243
|
-
# Search
|
244
|
-
# @
|
245
|
-
# @
|
247
|
+
# Search an item with an id
|
248
|
+
# @param id [String]the id item
|
249
|
+
# @return [Item] an item or nil
|
246
250
|
def search_by_id(id)
|
247
251
|
@data.each do |item|
|
248
252
|
return item if item.id == id
|
@@ -251,36 +255,35 @@ module MPW
|
|
251
255
|
nil
|
252
256
|
end
|
253
257
|
|
254
|
-
# Set
|
255
|
-
#
|
256
|
-
#
|
258
|
+
# Set a new opt key
|
259
|
+
# @param id [String] the item id
|
260
|
+
# @param key [String] the new key
|
257
261
|
def set_otp_key(id, key)
|
258
262
|
@otp_keys[id] = encrypt(key.to_s) unless key.to_s.empty?
|
259
263
|
end
|
260
264
|
|
261
265
|
# Get an opt key
|
262
|
-
#
|
263
|
-
# key -> the new key
|
266
|
+
# @param id [String] the item id
|
264
267
|
def get_otp_key(id)
|
265
268
|
@otp_keys.key?(id) ? decrypt(@otp_keys[id]) : nil
|
266
269
|
end
|
267
270
|
|
268
271
|
# Get an otp code
|
269
|
-
# @
|
270
|
-
# @
|
272
|
+
# @param id [String] the item id
|
273
|
+
# @return [String] an otp code
|
271
274
|
def get_otp_code(id)
|
272
275
|
@otp_keys.key?(id) ? 0 : ROTP::TOTP.new(decrypt(@otp_keys[id])).now
|
273
276
|
end
|
274
277
|
|
275
278
|
# Get remaining time before expire otp code
|
276
|
-
# @
|
279
|
+
# @return [Integer] time in seconde
|
277
280
|
def get_otp_remaining_time
|
278
281
|
(Time.now.utc.to_i / 30 + 1) * 30 - Time.now.utc.to_i
|
279
282
|
end
|
280
283
|
|
281
284
|
# Generate a random password
|
282
|
-
# @
|
283
|
-
# @
|
285
|
+
# @param options [Hash] :length, :special, :alpha, :numeric
|
286
|
+
# @return [String] a random string
|
284
287
|
def self.password(**options)
|
285
288
|
length =
|
286
289
|
if !options.include?(:length) || options[:length].to_i <= 0
|
@@ -298,11 +301,9 @@ module MPW
|
|
298
301
|
chars = [*('A'..'Z'), *('a'..'z'), *('0'..'9')] if chars.empty?
|
299
302
|
|
300
303
|
result = ''
|
301
|
-
|
302
|
-
result << chars.sample
|
303
|
-
length -= 62
|
304
|
+
length.times do
|
305
|
+
result << chars.sample
|
304
306
|
end
|
305
|
-
result << chars.sample(length).join
|
306
307
|
|
307
308
|
result
|
308
309
|
end
|
@@ -310,7 +311,8 @@ module MPW
|
|
310
311
|
private
|
311
312
|
|
312
313
|
# Decrypt a gpg file
|
313
|
-
# @
|
314
|
+
# @param data [String] data to decrypt
|
315
|
+
# @return [String] data decrypted
|
314
316
|
def decrypt(data)
|
315
317
|
return nil if data.to_s.empty?
|
316
318
|
|
@@ -331,7 +333,8 @@ module MPW
|
|
331
333
|
end
|
332
334
|
|
333
335
|
# Encrypt a file
|
334
|
-
#
|
336
|
+
# @param data [String] data to encrypt
|
337
|
+
# @return [String] data encrypted
|
335
338
|
def encrypt(data)
|
336
339
|
recipients = []
|
337
340
|
crypto = GPGME::Crypto.new(armor: true, always_trust: true)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
---
|
2
|
+
1:
|
3
|
+
host: fric.com
|
4
|
+
user: 230403
|
5
|
+
group: Bank
|
6
|
+
password: 5XdiTQOubRDw9B0aJoMlcEyL
|
7
|
+
protocol: https
|
8
|
+
port:
|
9
|
+
otp_key: 330223432
|
10
|
+
comment: I love my bank
|
11
|
+
2:
|
12
|
+
host: assurance.com
|
13
|
+
user: user_2132
|
14
|
+
group: Assurance
|
15
|
+
password: DMyK6B3v4bWO52VzU7aTHIem
|
16
|
+
protocol: https
|
17
|
+
port: 443
|
18
|
+
otp_key:
|
19
|
+
comment:
|
data/test/files/fixtures.yml
CHANGED
@@ -1,28 +1,28 @@
|
|
1
|
-
|
2
|
-
group: '
|
3
|
-
host: '
|
4
|
-
protocol: '
|
5
|
-
user: '
|
6
|
-
password: '
|
7
|
-
port: '
|
8
|
-
comment: '
|
1
|
+
add:
|
2
|
+
group: 'Bank'
|
3
|
+
host: 'example.com'
|
4
|
+
protocol: 'https'
|
5
|
+
user: 'admin'
|
6
|
+
password: 'VmfnCN6pPIqgRIbc'
|
7
|
+
port: '8080'
|
8
|
+
comment: 'the website'
|
9
9
|
|
10
|
-
|
10
|
+
import:
|
11
11
|
id: 'TEST-ID-XXXXX'
|
12
|
-
group: '
|
13
|
-
host: '
|
14
|
-
protocol: '
|
15
|
-
user: '
|
16
|
-
password: '
|
17
|
-
port: '
|
18
|
-
comment: '
|
12
|
+
group: 'Cloud'
|
13
|
+
host: 'gogole.com'
|
14
|
+
protocol: 'https'
|
15
|
+
user: 'gg-2304'
|
16
|
+
password: 'TITl0kV9CDDa9sVK'
|
17
|
+
port: '8081'
|
18
|
+
comment: 'My little servers'
|
19
19
|
created: 1386752948
|
20
20
|
|
21
21
|
update:
|
22
|
-
group: '
|
23
|
-
host: '
|
24
|
-
protocol: '
|
25
|
-
user: '
|
26
|
-
password: '
|
27
|
-
port: '
|
28
|
-
comment: '
|
22
|
+
group: 'Assurance'
|
23
|
+
host: 'example2.com'
|
24
|
+
protocol: 'ssh'
|
25
|
+
user: 'root'
|
26
|
+
password: 'kbSrbv4WlMaVxaZ7'
|
27
|
+
port: '2222'
|
28
|
+
comment: 'i love ssh'
|
data/test/init.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
|
3
|
+
require 'fileutils'
|
3
4
|
require 'gpgme'
|
4
5
|
|
6
|
+
FileUtils.rm_rf("#{Dir.home}/.config/mpw")
|
7
|
+
FileUtils.rm_rf("#{Dir.home}/.gnupg")
|
8
|
+
|
5
9
|
param = ''
|
6
10
|
param << '<GnupgKeyParms format="internal">' + "\n"
|
7
11
|
param << "Key-Type: RSA\n"
|
8
|
-
param << "Key-Length:
|
12
|
+
param << "Key-Length: 512\n"
|
9
13
|
param << "Subkey-Type: ELG-E\n"
|
10
|
-
param << "Subkey-Length:
|
14
|
+
param << "Subkey-Length: 512\n"
|
11
15
|
param << "Name-Real: test\n"
|
12
16
|
param << "Name-Comment: test\n"
|
13
17
|
param << "Name-Email: test2@example.com\n"
|
data/test/test_cli.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'i18n'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class TestConfig < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
if defined?(I18n.enforce_available_locales)
|
9
|
+
I18n.enforce_available_locales = true
|
10
|
+
end
|
11
|
+
|
12
|
+
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
13
|
+
I18n.load_path = ["#{File.expand_path('../../i18n', __FILE__)}/en.yml"]
|
14
|
+
I18n.locale = :en
|
15
|
+
|
16
|
+
@password = 'password'
|
17
|
+
@fixtures = YAML.load_file('./test/files/fixtures.yml')
|
18
|
+
@gpg_key = 'test@example.com'
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_00_init_config
|
22
|
+
output = %x(echo "#{@password}\n#{@password}" | mpw config --init #{@gpg_key})
|
23
|
+
assert_match(I18n.t('form.setup_config.valid'), output)
|
24
|
+
assert_match(I18n.t('form.setup_gpg_key.valid'), output)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_01_add_item
|
28
|
+
data = @fixtures['add']
|
29
|
+
|
30
|
+
output = %x(
|
31
|
+
echo #{@password} | mpw add \
|
32
|
+
--host #{data['host']} \
|
33
|
+
--port #{data['port']} \
|
34
|
+
--protocol #{data['protocol']} \
|
35
|
+
--user #{data['user']} \
|
36
|
+
--comment '#{data['comment']}' \
|
37
|
+
--group #{data['group']} \
|
38
|
+
--random
|
39
|
+
)
|
40
|
+
puts output
|
41
|
+
assert_match(I18n.t('form.add_item.valid'), output)
|
42
|
+
|
43
|
+
output = %x(echo #{@password} | mpw list)
|
44
|
+
puts output
|
45
|
+
assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
|
46
|
+
assert_match(data['user'], output)
|
47
|
+
assert_match(data['comment'], output)
|
48
|
+
assert_match(data['group'], output)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_02_search
|
52
|
+
data = @fixtures['add']
|
53
|
+
|
54
|
+
output = %x(echo #{@password} | mpw list --group #{data['group']})
|
55
|
+
assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
|
56
|
+
|
57
|
+
output = %x(echo #{@password} | mpw list --pattern #{data['host']})
|
58
|
+
assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
|
59
|
+
|
60
|
+
output = %x(echo #{@password} | mpw list --pattern #{data['comment']})
|
61
|
+
assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
|
62
|
+
|
63
|
+
output = %x(echo #{@password} | mpw list --group R1Pmfbp626TFpjlr)
|
64
|
+
assert_match(I18n.t('display.nothing'), output)
|
65
|
+
|
66
|
+
output = %x(echo #{@password} | mpw list --pattern h1IfnKqamaGM9oEX)
|
67
|
+
assert_match(I18n.t('display.nothing'), output)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_03_update_item
|
71
|
+
data = @fixtures['update']
|
72
|
+
|
73
|
+
output = %x(
|
74
|
+
echo #{@password} | mpw update \
|
75
|
+
-p #{@fixtures['add']['host']} \
|
76
|
+
--host #{data['host']} \
|
77
|
+
--port #{data['port']} \
|
78
|
+
--protocol #{data['protocol']} \
|
79
|
+
--user #{data['user']} \
|
80
|
+
--comment '#{data['comment']}' \
|
81
|
+
--new-group #{data['group']}
|
82
|
+
)
|
83
|
+
puts output
|
84
|
+
assert_match(I18n.t('form.update_item.valid'), output)
|
85
|
+
|
86
|
+
output = %x(echo #{@password} | mpw list)
|
87
|
+
puts output
|
88
|
+
assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
|
89
|
+
assert_match(data['user'], output)
|
90
|
+
assert_match(data['comment'], output)
|
91
|
+
assert_match(data['group'], output)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_04_delete_item
|
95
|
+
output = %x(echo "#{@password}\ny" | mpw delete -p #{@fixtures['update']['host']})
|
96
|
+
puts output
|
97
|
+
assert_match(I18n.t('form.delete_item.valid'), output)
|
98
|
+
|
99
|
+
output = %x(echo #{@password} | mpw list)
|
100
|
+
puts output
|
101
|
+
assert_match(I18n.t('display.nothing'), output)
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_05_import_export
|
105
|
+
file_import = './test/files/fixtures-import.yml'
|
106
|
+
file_export = '/tmp/test-mpw.yml'
|
107
|
+
|
108
|
+
output = %x(echo #{@password} | mpw import --file #{file_import})
|
109
|
+
assert_match(I18n.t('form.import.valid', file: file_import), output)
|
110
|
+
|
111
|
+
output = %x(echo #{@password} | mpw export --file #{file_export})
|
112
|
+
assert_match(I18n.t('form.export.valid', file: file_export), output)
|
113
|
+
assert(File.exist?(file_export))
|
114
|
+
assert_equal(YAML.load_file(file_export).length, 2)
|
115
|
+
|
116
|
+
YAML.load_file(file_import).each_value do |import|
|
117
|
+
error = true
|
118
|
+
|
119
|
+
YAML.load_file(file_export).each_value do |export|
|
120
|
+
next if import['host'] != export['host']
|
121
|
+
|
122
|
+
%w[user group password protocol port otp_key comment].each do |key|
|
123
|
+
assert_equal(import[key].to_s, export[key].to_s)
|
124
|
+
end
|
125
|
+
|
126
|
+
error = false
|
127
|
+
end
|
128
|
+
assert(!error)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_06_copy
|
133
|
+
data = YAML.load_file('./test/files/fixtures-import.yml')[1]
|
134
|
+
|
135
|
+
output = %x(
|
136
|
+
echo "#{@password}\np\nq" | mpw copy \
|
137
|
+
--disable-clipboard \
|
138
|
+
-p #{data['host']}
|
139
|
+
)
|
140
|
+
puts output
|
141
|
+
assert_match(data['password'], output)
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_07_setup_wallet
|
145
|
+
path = '/tmp/'
|
146
|
+
gpg_key = 'test2@example.com'
|
147
|
+
|
148
|
+
output = %x(echo #{@password} | mpw wallet --add-gpg-key #{gpg_key})
|
149
|
+
puts output
|
150
|
+
assert_match(I18n.t('form.add_key.valid'), output)
|
151
|
+
|
152
|
+
output = %x(echo #{@password} | mpw wallet --list-keys)
|
153
|
+
puts output
|
154
|
+
assert_match("| #{@gpg_key}", output)
|
155
|
+
assert_match("| #{gpg_key}", output)
|
156
|
+
|
157
|
+
output = %x(echo #{@password} | mpw wallet --delete-gpg-key #{gpg_key})
|
158
|
+
puts output
|
159
|
+
assert_match(I18n.t('form.delete_key.valid'), output)
|
160
|
+
|
161
|
+
output = %x(echo #{@password} | mpw wallet --list-keys)
|
162
|
+
puts output
|
163
|
+
assert_match("| #{@gpg_key}", output)
|
164
|
+
assert_no_match(/\| #{gpg_key}/, output)
|
165
|
+
|
166
|
+
output = %x(mpw wallet)
|
167
|
+
puts output
|
168
|
+
assert_match('| default', output)
|
169
|
+
|
170
|
+
output = %x(mpw wallet --path #{path})
|
171
|
+
puts output
|
172
|
+
assert_match(I18n.t('form.set_wallet_path.valid'), output)
|
173
|
+
|
174
|
+
output = %x(mpw config)
|
175
|
+
puts output
|
176
|
+
assert_match(%r{path_wallet_default.+\| #{path}/default.mpw}, output)
|
177
|
+
assert(File.exist?("#{path}/default.mpw"))
|
178
|
+
|
179
|
+
output = %x(mpw wallet --default-path)
|
180
|
+
puts output
|
181
|
+
assert_match(I18n.t('form.set_wallet_path.valid'), output)
|
182
|
+
|
183
|
+
output = %x(mpw config)
|
184
|
+
puts output
|
185
|
+
assert_no_match(/path_wallet_default/, output)
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_08_setup_config
|
189
|
+
gpg_key = 'user@example2.com'
|
190
|
+
gpg_exe = '/usr/bin/gpg2'
|
191
|
+
wallet_dir = '/tmp/mpw'
|
192
|
+
length = 24
|
193
|
+
wallet = 'work'
|
194
|
+
|
195
|
+
output = %x(
|
196
|
+
mpw config \
|
197
|
+
--gpg-exe #{gpg_exe} \
|
198
|
+
--enable-pinmode \
|
199
|
+
--disable-alpha \
|
200
|
+
--disable-special-chars \
|
201
|
+
--disable-numeric \
|
202
|
+
--length #{length} \
|
203
|
+
--wallet-dir #{wallet_dir} \
|
204
|
+
--default-wallet #{wallet}
|
205
|
+
)
|
206
|
+
puts output
|
207
|
+
assert_match(I18n.t('form.set_config.valid'), output)
|
208
|
+
|
209
|
+
output = %x(mpw config)
|
210
|
+
puts output
|
211
|
+
assert_match(/gpg_key.+\| #{@gpg_key}/, output)
|
212
|
+
assert_match(/gpg_exe.+\| #{gpg_exe}/, output)
|
213
|
+
assert_match(/pinmode.+\| true/, output)
|
214
|
+
assert_match(/default_wallet.+\| #{wallet}/, output)
|
215
|
+
assert_match(/wallet_dir.+\| #{wallet_dir}/, output)
|
216
|
+
assert_match(/password_length.+\| #{length}/, output)
|
217
|
+
%w[numeric alpha special].each do |k|
|
218
|
+
assert_match(/password_#{k}.+\| false/, output)
|
219
|
+
end
|
220
|
+
|
221
|
+
output = %x(
|
222
|
+
mpw config \
|
223
|
+
--key #{gpg_key} \
|
224
|
+
--alpha \
|
225
|
+
--special-chars \
|
226
|
+
--numeric \
|
227
|
+
--disable-pinmode
|
228
|
+
)
|
229
|
+
puts output
|
230
|
+
assert_match(I18n.t('form.set_config.valid'), output)
|
231
|
+
|
232
|
+
output = %x(mpw config)
|
233
|
+
puts output
|
234
|
+
assert_match(/gpg_key.+\| #{gpg_key}/, output)
|
235
|
+
assert_match(/pinmode.+\| false/, output)
|
236
|
+
%w[numeric alpha special].each do |k|
|
237
|
+
assert_match(/password_#{k}.+\| true/, output)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def test_09_generate_password
|
242
|
+
length = 24
|
243
|
+
|
244
|
+
output = %x(
|
245
|
+
mpw genpwd \
|
246
|
+
--length #{length} \
|
247
|
+
--alpha
|
248
|
+
)
|
249
|
+
assert_match(/[a-zA-Z]{#{length}}/, output)
|
250
|
+
|
251
|
+
output = %x(
|
252
|
+
mpw genpwd \
|
253
|
+
--length #{length} \
|
254
|
+
--numeric
|
255
|
+
)
|
256
|
+
assert_match(/[0-9]{#{length}}/, output)
|
257
|
+
|
258
|
+
output = %x(
|
259
|
+
mpw genpwd \
|
260
|
+
--length #{length} \
|
261
|
+
--special-chars
|
262
|
+
)
|
263
|
+
assert_no_match(/[a-zA-Z0-9]/, output)
|
264
|
+
end
|
265
|
+
end
|