sesame-cli 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sesame/jinn.rb CHANGED
@@ -1,27 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'highline/import'
2
4
  require 'i18n'
3
5
  require 'clipboard'
4
6
 
5
7
  module Sesame
8
+ # The Jinn class is wholly responsible for interacting with the terminal.
6
9
  class Jinn
10
+ # Pass in parsed command-line options as a Slop instance.
7
11
  def initialize(opts)
12
+ I18n.load_path = Dir[File.join(File.dirname(__FILE__), 'lang', '*yml')]
8
13
  @opts = opts
14
+ _config
9
15
  _parse
10
16
  _welcome
11
17
  @sesame = Cave.new(@opts[:path])
12
- I18n.load_path = Dir[File.join(File.dirname(__FILE__), 'lang', '*yml')]
13
18
  rescue Fail => e
14
19
  _error(e.message)
15
20
  end
16
21
 
22
+ # Process the users request, possibly starting an interactive session.
17
23
  def process!
18
- was_locked = false
24
+ return if @sesame.nil?
25
+ @was_locked = false
26
+ raise Fail, 'Cannot lock and expunge simultaneously' if @opts.lock? && @opts.expunge?
19
27
  if @sesame.exists?
20
- @sesame.forget if @sesame.locked? && @opts.expunge?
28
+ raise Fail, 'Please remove the cave before attempting to reconstruct' if @opts.reconstruct?
29
+ raise Fail, 'Cannot expunge lock; it doesn\'t exist' if @opts.expunge? && !@sesame.locked?
30
+ raise Fail, 'Please specify a command (or use interactive mode)' if !@opts.interactive? && @opts[:command].nil? && !@opts.lock? && !@opts.expunge?
31
+ if @sesame.locked? && @opts.expunge?
32
+ @sesame.forget
33
+ _warn('Lock expunged')
34
+ end
21
35
  if @sesame.locked?
22
36
  _unlock
23
- @sesame.forget
24
- was_locked = true
37
+ @was_locked = true
25
38
  else
26
39
  _open
27
40
  end
@@ -30,7 +43,7 @@ module Sesame
30
43
  _new
31
44
  end
32
45
  _process(@opts[:command])
33
- if @opts[:command].nil? || @opts.interactive?
46
+ if @opts.interactive?
34
47
  loop do
35
48
  say("\n")
36
49
  break if _prompt
@@ -38,32 +51,62 @@ module Sesame
38
51
  if @opts.expunge?
39
52
  @sesame.close
40
53
  else
41
- _lock(was_locked)
54
+ _lock
42
55
  end
43
- elsif was_locked
44
- _lock(true)
45
- else
56
+ elsif @opts.expunge? || (!@was_locked && !@opts.lock?)
46
57
  @sesame.close
58
+ else
59
+ _lock
47
60
  end
48
61
  rescue Fail => e
49
62
  _error(e.message)
63
+ rescue SystemExit, Interrupt
64
+ _error('Stopped by user')
50
65
  end
51
66
 
52
67
  protected
53
68
 
54
69
  def _welcome
55
70
  return if @opts[:quiet]
56
- say(HighLine.color(<<~EOS, :bold, :yellow))
71
+ say(HighLine.color(<<~WELCOME, :bold, :yellow))
57
72
  ╔═════════════════════════════════════╗
58
73
  ║ ┏━━━┓ ┏━━━┓ ┏━━━┓ ┏━┓ ┏┓ ┏┓ ┏━━━┓ ║
59
74
  ║ ┗━╋━┓ ┣━━┫ ┗━╋━┓ ┏┻━┻┓ ┃┗┳┛┃ ┣━━┫ ║
60
75
  ║ ┗━━━┛ ┗━━━┛ ┗━━━┛ ┗ ┛ ┗ ┛ ┗━━━┛ ║
61
76
  ╚═════════════════════════════════════╝
62
- EOS
77
+ WELCOME
78
+ end
79
+
80
+ def _load_config(base = '.', name = '.sesamerc')
81
+ path = File.join(base, name)
82
+ if File.exist?(path)
83
+ JSON.parse(File.read(path), symbolize_names: true)
84
+ elsif name == '.sesamerc'
85
+ _load_config(base, 'sesame.cfg')
86
+ elsif base == '.'
87
+ _load_config(Dir.home)
88
+ else
89
+ {}
90
+ end
91
+ rescue JSON::ParserError
92
+ raise Fail, "#{path} is not valid JSON"
93
+ end
94
+
95
+ def _config
96
+ config = _load_config
97
+ _set_opt(:echo, config[:echo]) unless @opts.echo?
98
+ _set_opt(:interactive, config[:interactive]) unless @opts.interactive?
99
+ _set_opt(:quiet, config[:quiet]) unless @opts.quiet?
100
+ config[:path] ||= ENV.fetch('SESAME_PATH', '.')
101
+ _set_opt(:path, config[:path]) if @opts[:path].nil?
102
+ _set_opt(:path, File.expand_path(@opts[:path]))
63
103
  end
64
104
 
65
105
  def _parse
66
- _set_path
106
+ unless Dir.exist?(@opts[:path])
107
+ _error("No such directory: #{@opts[:path]}")
108
+ exit 2
109
+ end
67
110
  _set_command('list') if @opts.list?
68
111
  _set_command('add') if @opts.add?
69
112
  _set_command('get') if @opts.get?
@@ -71,32 +114,11 @@ module Sesame
71
114
  _set_command('delete') if @opts.delete?
72
115
  end
73
116
 
74
- def _set_path
75
- # set @opts[:path] if nil from .sesamerc and then $SESAME_PATH and then current dir
76
- if @opts[:path].nil?
77
- @opts[:path] = _load_config || ENV.fetch('SESAME_PATH', '.')
78
- end
79
- @opts[:path] = File.expand_path(@opts[:path])
80
- unless Dir.exist?(@opts[:path])
81
- say("No such directory: #{@opts[:path]}")
82
- exit 2
83
- end
84
- end
85
-
86
- def _load_config(base=".")
87
- path = File.join(base, '.sesamerc')
88
- if File.exist?(path)
89
- File.read(path)
90
- elsif base == '.'
91
- _load_config(Dir.home)
92
- end
93
- end
94
-
95
117
  def _set_command(name)
96
118
  if @opts[:command].nil?
97
- @opts[:command] = name
119
+ _set_opt(:command, name)
98
120
  else
99
- say("Cannot execute command #{name}; #{@opts[:command]} already specified!")
121
+ _error("Cannot #{name} and #{@opts[:command]}")
100
122
  exit 2
101
123
  end
102
124
  end
@@ -115,104 +137,115 @@ module Sesame
115
137
  when :delete
116
138
  _delete
117
139
  else
118
- _error
140
+ _error('Command not recognised')
119
141
  end
120
142
  end
121
143
 
122
144
  def _prompt
123
145
  done = false
124
- _info("prompt")
146
+ _info('prompt')
125
147
  choose do |menu|
126
148
  menu.prompt = "\n> "
127
149
  menu.shell = true
128
- menu.choice(:list, "Display all of the services and users in your Sesame store.") do |command, args|
150
+ menu.choice(:list, 'Display all of the services and users in your Sesame store.') do |_, args|
129
151
  _set_opts(args)
130
152
  _list
131
153
  end
132
- menu.choice(:add, "Add a new service and user to your Sesame store.") do |command, args|
154
+ menu.choice(:add, 'Add a new service and user to your Sesame store.') do |_, args|
133
155
  _set_opts(args)
134
156
  _add
135
157
  end
136
- menu.choice(:get, "Retrieve the pass phrase for a service and user.") do |command, args|
158
+ menu.choice(:get, 'Retrieve the pass phrase for a service and user.') do |_, args|
137
159
  _set_opts(args)
138
160
  _get
139
161
  end
140
- menu.choice(:next, "Generate a new passphrase for a service and user.") do |comnmand, args|
162
+ menu.choice(:next, 'Generate a new passphrase for a service and user.') do |_, args|
141
163
  _set_opts(args)
142
164
  _next
143
165
  end
144
- menu.choice(:delete, "Remove a service and user from the Sesame store.") do |command, args|
166
+ menu.choice(:delete, 'Remove a service and user from the Sesame store.') do |_, args|
145
167
  _set_opts(args)
146
168
  _delete
147
169
  end
148
- menu.choice(:exit, "Close Sesame.") do
170
+ menu.choice(:exit, 'Close Sesame.') do
149
171
  done = true
150
172
  end
151
173
  end
174
+ _clear_opts
152
175
  done
153
176
  rescue Fail => e
154
177
  _error(e.message)
155
178
  end
156
179
 
157
- def _error(details="An error occurred")
180
+ def _error(details = 'An error occurred')
158
181
  message =
159
182
  if @opts[:quiet]
160
- _trans("error", details: details)
183
+ _trans('error', details: details)
161
184
  else
162
- _trans("jinn.error", details: details)
185
+ _trans('jinn.error', details: details)
163
186
  end
164
187
  say(HighLine.color(message, :bold, :red))
165
188
  end
166
189
 
190
+ def _warn(details)
191
+ message =
192
+ if @opts[:quiet]
193
+ _trans('warn', details: details)
194
+ else
195
+ _trans('jinn.warn', details: details)
196
+ end
197
+ say(HighLine.color(message, :bold, :yellow))
198
+ end
199
+
167
200
  def _new
168
201
  words = nil
169
202
  if @opts.reconstruct?
170
- _info("reconstruct")
171
- words = ask("🔑 ") { |q| q.echo = "*" }
203
+ _info('reconstruct')
204
+ words = ask('🔑 ') { |q| q.echo = '*' }
172
205
  end
173
206
  phrase = @sesame.create!(words)
174
207
  if words.nil?
175
- _info("new")
208
+ _info('new')
176
209
  _show(phrase)
177
210
  else
178
- _info("reconstructed")
211
+ _info('reconstructed')
179
212
  end
180
213
  end
181
214
 
182
215
  def _open
183
- _info("open")
184
- words = ask("🔑 ") { |q| q.echo = "*" }
216
+ _info('open')
217
+ words = ask('🔑 ') { |q| q.echo = '*' }
185
218
  @sesame.open(words)
186
- _info("path", path: @sesame.path)
219
+ _info('path', path: @sesame.path)
187
220
  end
188
221
 
189
222
  def _unlock
190
- _info("unlock")
191
- key = ask("🔑 ") { |q| q.echo = "*" }
223
+ _info('unlock')
224
+ key = ask('🔑 ') { |q| q.echo = '*' }
192
225
  @sesame.unlock(key)
193
- _info("path", path: @sesame.path)
226
+ _info('path', path: @sesame.path)
194
227
  end
195
228
 
196
229
  def _forget
197
- _info("forgot")
230
+ _info('forgot')
198
231
  @sesame.forget
199
232
  end
200
233
 
201
234
  def _list
202
- _info("list")
203
- if @opts[:service].nil? || @opts[:service].length == 0
235
+ _info('list')
236
+ if @opts[:service].nil? || @opts[:service].length.zero?
204
237
  @sesame.index.each do |service, users|
205
238
  next if service == 'sesame'
206
239
  if users.count > 1
207
240
  say("#{service} (#{users.count})")
208
241
  else
209
- say(service)
242
+ say("#{service} (#{users.first.first})")
210
243
  end
211
244
  end
212
245
  else
213
246
  users = @sesame.index[@opts[:service]]
214
- raise Fail, "No such service found, you must be thinking of some other cave" if users.nil? || @opts[:service] == "sesame"
215
- users.each do |user, index|
247
+ raise Fail, 'No such service found, you must be thinking of some other cave' if users.nil? || @opts[:service] == 'sesame'
248
+ users.sort.each do |user, _|
216
249
  say(user)
217
250
  end
218
251
  end
@@ -220,53 +253,55 @@ module Sesame
220
253
 
221
254
  def _get
222
255
  phrase = @sesame.get(*_question, @opts[:offset])
223
- _info("get", @sesame.item)
256
+ _info('get', @sesame.item)
224
257
  _show(phrase)
225
258
  end
226
259
 
227
260
  def _add
228
261
  phrase = @sesame.insert(*_question(true), @opts[:offset])
229
- _info("add", @sesame.item)
262
+ _info('add', @sesame.item)
230
263
  _show(phrase)
231
264
  end
232
265
 
233
266
  def _next
234
267
  phrase = @sesame.update(*_question, @opts[:offset])
235
268
  if phrase.nil?
236
- _info("next_key")
269
+ _info('next_key')
270
+ @was_locked = false
271
+ _set_opt(:lock, true)
237
272
  else
238
- _info("next", @sesame.item)
273
+ _info('next', @sesame.item)
239
274
  _show(phrase)
240
275
  end
241
276
  end
242
277
 
243
278
  def _delete
244
279
  phrase = @sesame.delete(*_question)
245
- _info("delete")
280
+ _info('delete')
246
281
  _show(phrase)
247
282
  end
248
283
 
249
- def _lock(silent=false)
284
+ def _lock
250
285
  key = @sesame.lock
251
- return if silent
252
- _info("lock")
286
+ return if @was_locked
287
+ _info('lock')
253
288
  _show(key)
254
289
  end
255
290
 
256
- def _info(message, args={})
291
+ def _info(message, args = {})
257
292
  message =
258
293
  if @opts[:quiet]
259
294
  _trans(message, args)
260
295
  else
261
296
  _trans("jinn.#{message}", args)
262
297
  end
263
- return if message.nil? || message.length == 0
298
+ return if message.nil? || message.length.zero?
264
299
  say(HighLine.color(message, :bold, :green))
265
300
  end
266
301
 
267
302
  def _trans(message, args)
268
303
  retval = I18n.t(message, args)
269
- if retval =~ /^translation missing/
304
+ if retval.match?(/^translation missing/)
270
305
  retval =
271
306
  if @opts[:echo]
272
307
  I18n.t("#{message}_echo", args)
@@ -285,22 +320,37 @@ module Sesame
285
320
  end
286
321
  end
287
322
 
323
+ def _set_opt(name, val)
324
+ return unless val
325
+ @opts.option(name).ensure_call(val)
326
+ end
327
+
288
328
  def _set_opts(args)
289
- args = args.split(" ").map(&:strip).map(&:downcase)
290
- return if args.count == 0
291
- @opts[:service] = args.first if args.count < 3
292
- @opts[:user] = args.last if args.count == 2
329
+ args = args.split(' ').map(&:strip).map(&:downcase)
330
+ return if args.count.zero?
331
+ _set_opt(:service, args.first) if args.count < 3
332
+ _set_opt(:user, args.last) if args.count == 2
333
+ end
334
+
335
+ def _clear_opt(name)
336
+ @opts.option(name).reset
337
+ end
338
+
339
+ def _clear_opts
340
+ _clear_opt(:service)
341
+ _clear_opt(:user)
293
342
  end
294
343
 
295
- def _question(user_required=false)
296
- service, user = @opts[:service], @opts[:user]
344
+ def _question(user_required = false)
345
+ service = @opts[:service]
346
+ user = @opts[:user]
297
347
  if service.nil?
298
- _info("service")
299
- service = ask("🏷 ")
348
+ _info('service')
349
+ service = ask('🏷 ')
300
350
  end
301
351
  if user.nil? && (user_required || !@sesame.unique?(service))
302
- _info("user")
303
- user = ask("👤 ")
352
+ _info('user')
353
+ user = ask('👤 ')
304
354
  end
305
355
  [service.downcase, user.nil? ? nil : user.downcase]
306
356
  end
@@ -23,6 +23,7 @@ en:
23
23
  lock_clip: 'Store locked. Unlock code copied to clipboard.'
24
24
  service: 'Enter service name:'
25
25
  user: 'Enter user name:'
26
+ warning: 'Warning: %{details}.'
26
27
  jinn:
27
28
  error: '🧞 - "A thousand apologies! %{details}."'
28
29
  open: '🧞 - "Open sesame! Please incant the magic words..."'
@@ -48,3 +49,4 @@ en:
48
49
  lock_clip: '🧞 - "I have secured your treasure. The key is in your clipboard."'
49
50
  service: '🧞 - "What be the name of the service?"'
50
51
  user: '🧞 - "And what be the name of the user?"'
52
+ warn: '🧞 - "Caution, master! %{details}."'
data/lib/sesame.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sesame/cave'
2
4
  require 'sesame/dict'
3
5
  require 'sesame/fail'
data/sesame-cli.gemspec CHANGED
@@ -2,17 +2,17 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: sesame-cli 0.1.0 ruby lib
5
+ # stub: sesame-cli 0.2.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "sesame-cli".freeze
9
- s.version = "0.1.0"
9
+ s.version = "0.2.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Jason Hutchens".freeze, "Jack Casey".freeze]
14
- s.date = "2018-04-20"
15
- s.description = "Sesame is a simple password manager for the command-line.".freeze
14
+ s.date = "2018-04-24"
15
+ s.description = "\u{1F9DE} - \"Sesame is a simple password manager for the command-line!\"".freeze
16
16
  s.email = "jasonhutchens@gmail.com".freeze
17
17
  s.executables = ["sesame".freeze]
18
18
  s.extra_rdoc_files = [
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  ]
21
21
  s.files = [
22
22
  ".document",
23
+ ".rubocop.yml",
23
24
  "Gemfile",
24
25
  "Gemfile.lock",
25
26
  "README.md",
@@ -34,14 +35,16 @@ Gem::Specification.new do |s|
34
35
  "lib/sesame/jinn.rb",
35
36
  "lib/sesame/lang/en.yml",
36
37
  "sesame-cli.gemspec",
37
- "sesame.gemspec",
38
+ "spec/cave_spec.rb",
39
+ "spec/dict_spec.rb",
40
+ "spec/jinn_spec.rb",
38
41
  "spec/spec_helper.rb"
39
42
  ]
40
43
  s.homepage = "http://github.com/kranzky/sesame-cli".freeze
41
44
  s.licenses = ["UNLICENSE".freeze]
42
45
  s.required_ruby_version = Gem::Requirement.new("~> 2.1".freeze)
43
46
  s.rubygems_version = "2.7.3".freeze
44
- s.summary = "Sesame is a simple password manager for the command-line.".freeze
47
+ s.summary = "\u{1F9DE} - \"Sesame is a simple password manager for the command-line!\"".freeze
45
48
 
46
49
  if s.respond_to? :specification_version then
47
50
  s.specification_version = 4
@@ -60,6 +63,7 @@ Gem::Specification.new do |s|
60
63
  s.add_development_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
61
64
  s.add_development_dependency(%q<rdoc>.freeze, ["~> 6.0"])
62
65
  s.add_development_dependency(%q<rspec>.freeze, ["~> 3.7"])
66
+ s.add_development_dependency(%q<rubocop>.freeze, ["~> 0.55"])
63
67
  s.add_development_dependency(%q<simplecov>.freeze, ["~> 0.15"])
64
68
  s.add_development_dependency(%q<yard>.freeze, ["~> 0.9"])
65
69
  else
@@ -76,6 +80,7 @@ Gem::Specification.new do |s|
76
80
  s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
77
81
  s.add_dependency(%q<rdoc>.freeze, ["~> 6.0"])
78
82
  s.add_dependency(%q<rspec>.freeze, ["~> 3.7"])
83
+ s.add_dependency(%q<rubocop>.freeze, ["~> 0.55"])
79
84
  s.add_dependency(%q<simplecov>.freeze, ["~> 0.15"])
80
85
  s.add_dependency(%q<yard>.freeze, ["~> 0.9"])
81
86
  end
@@ -93,6 +98,7 @@ Gem::Specification.new do |s|
93
98
  s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
94
99
  s.add_dependency(%q<rdoc>.freeze, ["~> 6.0"])
95
100
  s.add_dependency(%q<rspec>.freeze, ["~> 3.7"])
101
+ s.add_dependency(%q<rubocop>.freeze, ["~> 0.55"])
96
102
  s.add_dependency(%q<simplecov>.freeze, ["~> 0.15"])
97
103
  s.add_dependency(%q<yard>.freeze, ["~> 0.9"])
98
104
  end
data/spec/cave_spec.rb ADDED
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+
5
+ describe Sesame::Cave do
6
+ let(:cave) { Sesame::Cave.new(Dir.tmpdir, 20) }
7
+
8
+ after do
9
+ cave.close if cave.open?
10
+ cave.forget if cave.locked?
11
+ File.delete(cave.path) if cave.exists?
12
+ end
13
+
14
+ describe '#path' do
15
+ it 'returns the location of the sesame cave' do
16
+ expect(cave.path).to eq(File.join(Dir.tmpdir, 'sesame.cave'))
17
+ end
18
+ end
19
+
20
+ describe '#item' do
21
+ before do
22
+ phrase = cave.create!
23
+ cave.insert('foo', 'bar')
24
+ cave.close
25
+ cave.open(phrase)
26
+ end
27
+
28
+ it 'returns nothing by default' do
29
+ expect(cave.item).to be_nil
30
+ end
31
+
32
+ context 'after #get' do
33
+ before do
34
+ cave.get('foo')
35
+ end
36
+ it 'returns the item' do
37
+ expect(cave.item[:user]).to eq('bar')
38
+ end
39
+ end
40
+
41
+ context 'after #insert' do
42
+ before do
43
+ cave.insert('xxx', 'yyy')
44
+ end
45
+ it 'returns the item' do
46
+ expect(cave.item[:user]).to eq('yyy')
47
+ end
48
+ end
49
+
50
+ context 'after #update' do
51
+ before do
52
+ cave.update('foo')
53
+ end
54
+ it 'returns the item' do
55
+ expect(cave.item[:user]).to eq('bar')
56
+ end
57
+ end
58
+
59
+ context 'after #delete' do
60
+ before do
61
+ cave.delete('foo')
62
+ end
63
+ it 'returns the item' do
64
+ expect(cave.item[:user]).to eq('bar')
65
+ end
66
+ end
67
+
68
+ context 'after #lock' do
69
+ before do
70
+ cave.insert('xxx', 'yyy')
71
+ cave.lock
72
+ end
73
+ it 'is cleared' do
74
+ expect(cave.item).to be_nil
75
+ end
76
+ end
77
+ end
78
+
79
+ describe '#exists?' do
80
+ context 'by default' do
81
+ it 'returns false' do
82
+ expect(cave.exists?).to be false
83
+ end
84
+ end
85
+
86
+ context 'after #create! and #close' do
87
+ before do
88
+ cave.create!
89
+ cave.close
90
+ end
91
+ it 'returns true' do
92
+ expect(cave.exists?).to be true
93
+ end
94
+ end
95
+ end
96
+
97
+ describe '#locked?' do
98
+ context 'by default' do
99
+ it 'returns false' do
100
+ expect(cave.locked?).to be false
101
+ end
102
+ end
103
+
104
+ context 'after #create! and #close' do
105
+ before do
106
+ cave.create!
107
+ cave.close
108
+ end
109
+ it 'returns false' do
110
+ expect(cave.locked?).to be false
111
+ end
112
+ end
113
+
114
+ context 'after #create! and #lock' do
115
+ before do
116
+ cave.create!
117
+ cave.lock
118
+ end
119
+ it 'returns true' do
120
+ expect(cave.locked?).to be true
121
+ end
122
+ end
123
+
124
+ context 'after #create! and #lock and #forget' do
125
+ before do
126
+ cave.create!
127
+ cave.lock
128
+ cave.forget
129
+ end
130
+ it 'returns false' do
131
+ expect(cave.locked?).to be false
132
+ end
133
+ end
134
+
135
+ context 'after #create! and #lock and #unlock' do
136
+ before do
137
+ cave.create!
138
+ code = cave.lock
139
+ cave.unlock(code)
140
+ end
141
+ it 'returns false' do
142
+ expect(cave.locked?).to be false
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '#open?' do
148
+ end
149
+
150
+ describe '#dirty?' do
151
+ end
152
+
153
+ describe '#create!' do
154
+ end
155
+
156
+ describe '#open' do
157
+ end
158
+
159
+ describe '#close' do
160
+ end
161
+
162
+ describe '#lock' do
163
+ end
164
+
165
+ describe '#unlock' do
166
+ end
167
+
168
+ describe '#forget' do
169
+ end
170
+
171
+ describe '#index' do
172
+ end
173
+
174
+ describe '#unique?' do
175
+ end
176
+
177
+ describe '#get' do
178
+ end
179
+
180
+ describe '#insert' do
181
+ end
182
+
183
+ describe '#update' do
184
+ end
185
+
186
+ describe '#delete' do
187
+ end
188
+ end