sesame-cli 0.1.0 → 0.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.
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