keyrack 0.2.3 → 0.3.0.pre

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/Gemfile CHANGED
@@ -14,4 +14,5 @@ group :development do
14
14
  gem "rcov", ">= 0"
15
15
  gem "mocha", :require => false
16
16
  gem "cucumber"
17
+ gem 'test-unit'
17
18
  end
data/Gemfile.lock CHANGED
@@ -28,6 +28,7 @@ GEM
28
28
  rake (0.9.2.2)
29
29
  rcov (0.9.11)
30
30
  term-ansicolor (1.0.7)
31
+ test-unit (2.4.3)
31
32
 
32
33
  PLATFORMS
33
34
  ruby
@@ -41,3 +42,4 @@ DEPENDENCIES
41
42
  mocha
42
43
  net-scp
43
44
  rcov
45
+ test-unit
data/README.rdoc CHANGED
@@ -26,16 +26,19 @@ Running keyrack will display a simple menu in your terminal, like this:
26
26
  === Keyrack Main Menu ===
27
27
  1. Twitter [foobar]
28
28
  2. Facebook [foobar@example.com]
29
- n. Add new
30
- d. Delete entry
31
- g. New group
32
- s. Save
33
- q. Quit
29
+ Mode: copy
30
+ Commands: [n]ew [d]elete [g]roup [s]ave [m]ode [q]uit
34
31
 
35
32
  Selecting <b>1</b> in this case will copy the Twitter password for the foobar user
36
33
  to your clipboard.
37
34
 
38
- You can create groups to organize your sites.
35
+ You can create groups (using the 'group' command) to organize your sites.
36
+
37
+ There are two modes, <b>copy</b> (default) and <b>print</b>. In print mode,
38
+ keyrack will print out your password instead of copying it to your clipboard.
39
+ After printing, it will try to erase it after you hit a key (if you're on
40
+ win32 or have either {termios}[https://github.com/arika/ruby-termios]
41
+ or {ffi-ncurses}[http://rubygems.org/gems/ffi-ncurses] installed).
39
42
 
40
43
  == Contributing to keyrack
41
44
 
data/Rakefile CHANGED
@@ -50,7 +50,7 @@ end
50
50
 
51
51
  task :default => :test
52
52
 
53
- require 'rake/rdoctask'
53
+ require 'rdoc/task'
54
54
  Rake::RDocTask.new do |rdoc|
55
55
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
56
56
 
@@ -59,3 +59,5 @@ Rake::RDocTask.new do |rdoc|
59
59
  rdoc.rdoc_files.include('README*')
60
60
  rdoc.rdoc_files.include('lib/**/*.rb')
61
61
  end
62
+
63
+ task :build => :gemspec
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.0.pre
@@ -12,11 +12,11 @@ Feature: Console runner
12
12
  * the output should contain "Choose storage type:"
13
13
  * I type "filesystem"
14
14
 
15
- * the output should contain "g. New group"
15
+ * the output should contain "[g]roup"
16
16
  * I type "g" to add a new group
17
17
  * the output should contain "Group:"
18
18
  * I type "Social"
19
- * the output should contain "n. New entry"
19
+ * the output should contain "[n]ew"
20
20
  * I type "n" to add a new entry
21
21
  * the output should contain "Label:"
22
22
  * I type "Twitter"
@@ -29,13 +29,13 @@ Feature: Console runner
29
29
  * the output should contain "Password (again):"
30
30
  * I type "kittens"
31
31
  * the output should contain "1. Twitter [dudeguy]"
32
- * the output should also contain "s. Save"
32
+ * the output should also contain "[s]ave"
33
33
  * I type "s" to save the database
34
- * the output should contain "t. Top level menu"
34
+ * the output should contain "[t]op"
35
35
  * I type "t"
36
36
  * the output should match /1\. .+Social.+/
37
37
 
38
- * the output should also contain "n. New entry"
38
+ * the output should also contain "[n]ew"
39
39
  * I type "n" to add a new entry
40
40
  * the output should contain "Label:"
41
41
  * I type "Company X"
@@ -46,10 +46,20 @@ Feature: Console runner
46
46
  * the output should contain "Sound good? [yn]"
47
47
  * I type "y" for yes
48
48
  * the output should contain "2. Company X [buddypal]"
49
- * the output should also contain "s. Save"
49
+ * the output should also contain "[s]ave"
50
+ * I type "n" to add a new entry
51
+ * the output should contain "Label:"
52
+ * I type "Company X"
53
+ * the output should contain "Username:"
54
+ * I type "friendguy"
55
+ * the output should contain "Generate password?"
56
+ * I type "y" for yes
57
+ * the output should contain "Sound good? [yn]"
58
+ * I type "y" for yes
59
+ * the output should contain "3. Company X [friendguy]"
50
60
  * I type "s" to save the database
51
61
 
52
- * the output should contain "q. Quit"
62
+ * the output should contain "[q]uit"
53
63
  * I type "q" to quit
54
64
  * I wait a few seconds
55
65
  * I run keyrack interactively again
@@ -59,21 +69,35 @@ Feature: Console runner
59
69
  * I wait a few seconds
60
70
  * the output should match /1\. .+Social.+/
61
71
  * the output should also contain "2. Company X [buddypal]"
72
+ * the output should also contain "3. Company X [friendguy]"
62
73
  * I type "1" for Social
63
74
  * the output should contain "1. Twitter [dudeguy]"
64
75
  * I type "1" for Twitter
65
76
  * my clipboard should contain "kittens"
66
77
 
67
- * the output should contain "d. Delete entry"
78
+ * the output should contain "[d]elete"
68
79
  * I type "d"
69
80
  * the output should contain "1. Twitter [dudeguy]"
70
81
  * I type "1"
71
82
  * the output should contain "Are you sure?"
72
83
  * I type "y"
73
- * the output should contain "t. Top level menu"
84
+ * the output should contain "[t]op"
74
85
  * I type "t"
75
86
 
87
+ * the output should contain "2. Company X [buddypal]"
88
+ * I type "2" for Company X (buddypal)
89
+ * my clipboard should match "^.{8}$"
90
+
91
+ * the output should contain "Main Menu"
92
+ * I type "d"
93
+ * the output should contain "1. Company X [buddypal]"
94
+ * I type "1"
95
+ * the output should contain "Company X [buddypal]"
96
+ * the output should also contain "Are you sure?"
97
+ * I type "y"
98
+
76
99
  * the output should contain "Main Menu"
100
+ * the output should also contain "2. Company X [friendguy]"
77
101
  * I type "q" to quit
78
102
  * the output should contain "Really quit?" (since the database is dirty)
79
103
  * I type "y"
@@ -21,7 +21,7 @@ Then /the output should contain "([^"]+)"/ do |expected|
21
21
  sleep 1
22
22
  end
23
23
  @output = @out.read_nonblock(255)
24
- assert @output.include?(expected)
24
+ assert @output.include?(expected), "Output: #{@output.inspect}"
25
25
  end
26
26
 
27
27
  Then %r{the output should match /([^/]+)/} do |expected|
@@ -53,3 +53,9 @@ Then /my clipboard should contain "([^"]+)"/ do |expected|
53
53
  result = %x{xclip -selection clipboard -o}.chomp
54
54
  assert_equal expected, result
55
55
  end
56
+
57
+ Then /my clipboard should match "([^"]+)"/ do |expected|
58
+ sleep 1
59
+ result = %x{xclip -selection clipboard -o}.chomp
60
+ assert_match Regexp.new(expected), result
61
+ end
data/keyrack.gemspec CHANGED
@@ -4,19 +4,19 @@
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
- s.name = %q{keyrack}
8
- s.version = "0.1.1"
7
+ s.name = "keyrack"
8
+ s.version = "0.3.0.pre"
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jeremy Stephens"]
12
- s.date = %q{2010-12-12}
13
- s.default_executable = %q{keyrack}
14
- s.description = %q{Simple password manager with local/remote database storage and RSA encryption.}
15
- s.email = %q{viking@pillageandplunder.net}
12
+ s.date = "2012-01-11"
13
+ s.description = "Simple password manager with local/remote database storage and RSA encryption."
14
+ s.email = "viking@pillageandplunder.net"
16
15
  s.executables = ["keyrack"]
17
16
  s.extra_rdoc_files = [
18
17
  "LICENSE.txt",
19
- "README.rdoc"
18
+ "README.rdoc",
19
+ "TODO"
20
20
  ]
21
21
  s.files = [
22
22
  ".document",
@@ -26,8 +26,13 @@ Gem::Specification.new do |s|
26
26
  "LICENSE.txt",
27
27
  "README.rdoc",
28
28
  "Rakefile",
29
+ "TODO",
29
30
  "VERSION",
30
31
  "bin/keyrack",
32
+ "features/console.feature",
33
+ "features/step_definitions/keyrack_steps.rb",
34
+ "features/support/env.rb",
35
+ "keyrack.gemspec",
31
36
  "lib/keyrack.rb",
32
37
  "lib/keyrack/database.rb",
33
38
  "lib/keyrack/runner.rb",
@@ -36,23 +41,25 @@ Gem::Specification.new do |s|
36
41
  "lib/keyrack/store/ssh.rb",
37
42
  "lib/keyrack/ui.rb",
38
43
  "lib/keyrack/ui/console.rb",
44
+ "lib/keyrack/utils.rb",
45
+ "test/fixtures/aes",
39
46
  "test/fixtures/config.yml",
40
47
  "test/fixtures/foo.txt",
41
48
  "test/fixtures/id_rsa",
42
- "test/fixtures/id_rsa.pub",
43
49
  "test/helper.rb",
44
50
  "test/keyrack/store/test_filesystem.rb",
45
51
  "test/keyrack/store/test_ssh.rb",
46
52
  "test/keyrack/test_database.rb",
47
53
  "test/keyrack/test_runner.rb",
48
54
  "test/keyrack/test_store.rb",
55
+ "test/keyrack/test_utils.rb",
49
56
  "test/keyrack/ui/test_console.rb"
50
57
  ]
51
- s.homepage = %q{http://github.com/viking/keyrack}
58
+ s.homepage = "http://github.com/viking/keyrack"
52
59
  s.licenses = ["MIT"]
53
60
  s.require_paths = ["lib"]
54
- s.rubygems_version = %q{1.3.7}
55
- s.summary = %q{Simple password manager}
61
+ s.rubygems_version = "1.8.11"
62
+ s.summary = "Simple password manager"
56
63
  s.test_files = [
57
64
  "test/helper.rb",
58
65
  "test/keyrack/store/test_filesystem.rb",
@@ -60,38 +67,44 @@ Gem::Specification.new do |s|
60
67
  "test/keyrack/test_database.rb",
61
68
  "test/keyrack/test_runner.rb",
62
69
  "test/keyrack/test_store.rb",
70
+ "test/keyrack/test_utils.rb",
63
71
  "test/keyrack/ui/test_console.rb"
64
72
  ]
65
73
 
66
74
  if s.respond_to? :specification_version then
67
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
75
  s.specification_version = 3
69
76
 
70
77
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
71
78
  s.add_runtime_dependency(%q<net-scp>, [">= 0"])
72
79
  s.add_runtime_dependency(%q<highline>, [">= 0"])
73
- s.add_runtime_dependency(%q<clipboard>, [">= 0"])
80
+ s.add_runtime_dependency(%q<copier>, [">= 0"])
74
81
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
75
82
  s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
76
83
  s.add_development_dependency(%q<rcov>, [">= 0"])
77
84
  s.add_development_dependency(%q<mocha>, [">= 0"])
85
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
86
+ s.add_development_dependency(%q<test-unit>, [">= 0"])
78
87
  else
79
88
  s.add_dependency(%q<net-scp>, [">= 0"])
80
89
  s.add_dependency(%q<highline>, [">= 0"])
81
- s.add_dependency(%q<clipboard>, [">= 0"])
90
+ s.add_dependency(%q<copier>, [">= 0"])
82
91
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
83
92
  s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
84
93
  s.add_dependency(%q<rcov>, [">= 0"])
85
94
  s.add_dependency(%q<mocha>, [">= 0"])
95
+ s.add_dependency(%q<cucumber>, [">= 0"])
96
+ s.add_dependency(%q<test-unit>, [">= 0"])
86
97
  end
87
98
  else
88
99
  s.add_dependency(%q<net-scp>, [">= 0"])
89
100
  s.add_dependency(%q<highline>, [">= 0"])
90
- s.add_dependency(%q<clipboard>, [">= 0"])
101
+ s.add_dependency(%q<copier>, [">= 0"])
91
102
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
92
103
  s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
93
104
  s.add_dependency(%q<rcov>, [">= 0"])
94
105
  s.add_dependency(%q<mocha>, [">= 0"])
106
+ s.add_dependency(%q<cucumber>, [">= 0"])
107
+ s.add_dependency(%q<test-unit>, [">= 0"])
95
108
  end
96
109
  end
97
110
 
@@ -10,18 +10,56 @@ module Keyrack
10
10
 
11
11
  def add(site, username, password, options = {})
12
12
  hash = options[:group] ? @data[options[:group]] ||= {} : @data
13
- hash[site] = { :username => username, :password => password }
13
+ if hash.has_key?(site)
14
+ site_entry = hash[site]
15
+ if site_entry.is_a?(Array)
16
+ # Multiple entries for this site
17
+ user_entry = site_entry.detect { |e| e[:username] == username }
18
+ if user_entry
19
+ # Update existing entry
20
+ user_entry[:password] = password
21
+ else
22
+ # Add new entry
23
+ site_entry.push({:username => username, :password => password})
24
+ end
25
+ elsif site_entry[:username] == username
26
+ # Update existing entry
27
+ site_entry[:password] = password
28
+ else
29
+ # Convert single entry into an array, then add new entry
30
+ hash[site] = [site_entry, {:username => username, :password => password}]
31
+ end
32
+ else
33
+ hash[site] = { :username => username, :password => password }
34
+ end
14
35
  @dirty = true
15
36
  end
16
37
 
17
- def get(site, options = {})
18
- (options[:group] ? @data[options[:group]] : @data)[site]
38
+ def get(*args)
39
+ options = args.last.is_a?(Hash) ? args.pop : {}
40
+ site, username = args
41
+
42
+ site_entry = (options[:group] ? @data[options[:group]] : @data)[site]
43
+ if username
44
+ if site_entry.is_a?(Array)
45
+ site_entry.find { |e| e[:username] == username }
46
+ elsif site_entry[:username] == username
47
+ site_entry
48
+ else
49
+ nil
50
+ end
51
+ else
52
+ site_entry
53
+ end
19
54
  end
20
55
 
21
56
  def sites(options = {})
22
57
  hash = options[:group] ? @data[options[:group]] : @data
23
58
  if hash
24
- hash.keys.select { |k| hash[k].keys.include?(:username) }.sort
59
+ hash.keys.select do |key|
60
+ val = hash[key]
61
+ val.is_a?(Array) || (val.is_a?(Hash) && val.has_key?(:username))
62
+ end.sort
25
63
  else
26
64
  # new groups are empty
27
65
  []
@@ -29,7 +67,10 @@ module Keyrack
29
67
  end
30
68
 
31
69
  def groups
32
- @data.keys.reject { |k| @data[k].keys.include?(:username) }.sort
70
+ @data.keys.reject do |key|
71
+ val = @data[key]
72
+ val.is_a?(Array) || (val.is_a?(Hash) && val.has_key?(:username))
73
+ end.sort
33
74
  end
34
75
 
35
76
  def dirty?
@@ -43,10 +84,31 @@ module Keyrack
43
84
  @dirty = false
44
85
  end
45
86
 
46
- def delete(site, options = {})
87
+ def delete(site, username, options = {})
47
88
  hash = options[:group] ? @data[options[:group]] : @data
48
- hash.delete(site)
49
- @dirty = true
89
+ site_entry = hash[site]
90
+
91
+ if site_entry.is_a?(Array)
92
+ site_entry.each_with_index do |entry, i|
93
+ if entry[:username] == username
94
+ case site_entry.length
95
+ when 2
96
+ site_entry.delete_at(i)
97
+ hash[site] = site_entry[0]
98
+ when 1
99
+ hash.delete(site)
100
+ else
101
+ site_entry.delete_at(i)
102
+ end
103
+
104
+ @dirty = true
105
+ break
106
+ end
107
+ end
108
+ elsif site_entry[:username] == username
109
+ hash.delete(site)
110
+ @dirty = true
111
+ end
50
112
  end
51
113
 
52
114
  private
@@ -13,8 +13,8 @@ module Keyrack
13
13
  if Dir.exist?(@config_path)
14
14
  @options = YAML.load_file(File.join(@config_path, "config"))
15
15
  password = @ui.get_password
16
- rsa_key = Utils.open_rsa_key(@options['rsa'], password)
17
- aes_data = Utils.open_aes_data(@options['aes'], rsa_key)
16
+ rsa_key = Utils.open_rsa_key(File.expand_path(@options['rsa'], @config_path), password)
17
+ aes_data = Utils.open_aes_data(File.expand_path(@options['aes'], @config_path), rsa_key)
18
18
  else
19
19
  Dir.mkdir(@config_path)
20
20
  @options = {}
@@ -1,9 +1,11 @@
1
1
  module Keyrack
2
2
  module UI
3
3
  class Console
4
- attr_accessor :database
4
+ attr_accessor :database, :mode
5
+
5
6
  def initialize
6
7
  @highline = HighLine.new
8
+ @mode = :copy
7
9
  end
8
10
 
9
11
  def get_password
@@ -11,52 +13,64 @@ module Keyrack
11
13
  end
12
14
 
13
15
  def menu(options = {})
14
- choices = {'n' => :new, 'q' => :quit}
16
+ choices = {'n' => :new, 'q' => :quit, 'm' => :mode}
15
17
  index = 1
16
18
 
19
+ sites = @database.sites(options)
20
+ count = sites.length
21
+ count += @database.groups.length if !options[:group]
22
+ width = count / 10
23
+
17
24
  if !options[:group]
18
25
  # Can't have subgroups (yet?).
19
26
  @highline.say("=== #{@highline.color("Keyrack Main Menu", :yellow)} ===")
20
27
  @database.groups.each do |group|
21
28
  choices[index.to_s] = {:group => group}
22
- @highline.say("% 2d. %s" % [index, @highline.color(group, :green)])
29
+ @highline.say(" %#{width}d. %s" % [index, @highline.color(group, :green)])
23
30
  index += 1
24
31
  end
25
32
  else
26
33
  @highline.say("===== #{@highline.color(options[:group], :green)} =====")
27
34
  end
28
35
 
29
- sites = @database.sites(options)
30
36
  sites.each do |site|
31
- entry = @database.get(site, options)
32
- choices[index.to_s] = entry
33
- @highline.say("% 2d. %s [%s]" % [index, site, entry[:username]])
34
- index += 1
37
+ site_entry = @database.get(site, options)
38
+ site_entry = [site_entry] unless site_entry.is_a?(Array)
39
+ site_entry.each do |entry|
40
+ choices[index.to_s] = entry
41
+ @highline.say(" %#{width}d. %s [%s]" % [index, site, entry[:username]])
42
+ index += 1
43
+ end
35
44
  end
36
45
 
37
- @highline.say(" n. New entry")
46
+ @highline.say("Mode: #{mode}")
47
+ commands = "Commands: [n]ew"
38
48
  if !sites.empty?
39
49
  choices['d'] = :delete
40
- @highline.say(" d. Delete entry")
50
+ commands << " [d]elete"
41
51
  end
42
52
  if !options[:group]
43
53
  choices['g'] = :new_group
44
- @highline.say(" g. New group")
54
+ commands << " [g]roup"
45
55
  else
46
56
  choices['t'] = :top
47
- @highline.say(" t. Top level menu")
57
+ commands << " [t]op"
48
58
  end
49
59
  if @database.dirty?
50
60
  choices['s'] = :save
51
- @highline.say(" s. Save")
61
+ commands << " [s]ave"
52
62
  end
53
- @highline.say(" q. Quit")
54
- answer = @highline.ask(" ? ") { |q| q.in = choices.keys }
63
+ commands << " [m]ode [q]uit"
64
+ @highline.say(commands)
65
+ answer = @highline.ask(" ? ") { |q| q.in = choices.keys }
55
66
  result = choices[answer]
56
67
  case result
57
68
  when Symbol
58
69
  if result == :quit && @database.dirty? && !@highline.agree("Really quit? You have unsaved changes! [yn] ")
59
70
  nil
71
+ elsif result == :mode
72
+ @mode = @mode == :copy ? :print : :copy
73
+ nil
60
74
  else
61
75
  result
62
76
  end
@@ -64,8 +78,19 @@ module Keyrack
64
78
  if result.has_key?(:group)
65
79
  result
66
80
  else
67
- Copier(result[:password])
68
- @highline.say("The password has been copied to your clipboard.")
81
+ if mode == :copy
82
+ Copier(result[:password])
83
+ @highline.say("The password has been copied to your clipboard.")
84
+ elsif mode == :print
85
+ password = @highline.color(result[:password], :cyan)
86
+ @highline.ask("Here you go: #{password}. Done? ") do |question|
87
+ question.echo = false
88
+ if HighLine::SystemExtensions::CHARACTER_MODE != 'stty'
89
+ question.character = true
90
+ question.overwrite = true
91
+ end
92
+ end
93
+ end
69
94
  nil
70
95
  end
71
96
  end
@@ -83,7 +108,7 @@ module Keyrack
83
108
  if @highline.agree("Generate password? [yn] ")
84
109
  loop do
85
110
  password = Utils.generate_password
86
- if @highline.agree("Generated #{@highline.color(password, :blue)}. Sound good? [yn] ")
111
+ if @highline.agree("Generated #{@highline.color(password, :cyan)}. Sound good? [yn] ")
87
112
  result[:password] = password
88
113
  break
89
114
  end
@@ -96,7 +121,7 @@ module Keyrack
96
121
  result[:password] = password
97
122
  break
98
123
  end
99
- @highline.say("Passwords didn't match. Try again!")
124
+ @highline.say("Passwords didn't match. Try again!")
100
125
  end
101
126
  end
102
127
  result
@@ -120,7 +145,7 @@ module Keyrack
120
145
  def store_setup
121
146
  result = {}
122
147
  result['type'] = @highline.choose do |menu|
123
- menu.header = "Choose storage type:"
148
+ menu.header = "Choose storage type"
124
149
  menu.choices("filesystem", "ssh")
125
150
  end
126
151
 
@@ -141,10 +166,13 @@ module Keyrack
141
166
  index = 1
142
167
  @highline.say("Choose entry to delete:")
143
168
  @database.sites(options).each do |site|
144
- entry = @database.get(site, options)
145
- choices[index.to_s] = {:site => site, :username => entry[:username]}
146
- @highline.say("% 2d. %s [%s]" % [index, site, entry[:username]])
147
- index += 1
169
+ site_entry = @database.get(site, options)
170
+ site_entry = [site_entry] unless site_entry.is_a?(Array)
171
+ site_entry.each do |entry|
172
+ choices[index.to_s] = {:site => site, :username => entry[:username]}
173
+ @highline.say("% 2d. %s [%s]" % [index, site, entry[:username]])
174
+ index += 1
175
+ end
148
176
  end
149
177
  @highline.say(" c. Cancel")
150
178
 
@@ -153,7 +181,7 @@ module Keyrack
153
181
  if result != :cancel
154
182
  entry = @highline.color("#{result[:site]} [#{result[:username]}]", :red)
155
183
  if @highline.agree("You're about to delete #{entry}. Are you sure? [yn] ")
156
- @database.delete(result[:site], options)
184
+ @database.delete(result[:site], result[:username], options)
157
185
  end
158
186
  end
159
187
  end
@@ -26,7 +26,7 @@ module Keyrack
26
26
  def test_reading_existing_database
27
27
  database = Keyrack::Database.new(@key, @iv, @store)
28
28
  expected = {:username => 'username', :password => 'password'}
29
- assert_equal(expected, database.get('Twitter'))
29
+ assert_equal(expected, database.get('Twitter', 'username'))
30
30
  end
31
31
 
32
32
  def test_sites
@@ -61,20 +61,51 @@ module Keyrack
61
61
  def test_add_with_top_level_group
62
62
  @database.add('Twitter', 'dudeguy', 'secret', :group => "Social")
63
63
  expected = {:username => 'dudeguy', :password => 'secret'}
64
- assert_equal expected, @database.get('Twitter', :group => "Social")
64
+ assert_equal expected, @database.get('Twitter', 'dudeguy', :group => "Social")
65
65
  end
66
66
 
67
67
  def test_delete
68
- @database.delete('Twitter')
68
+ @database.delete('Twitter', 'username')
69
69
  assert_equal [], @database.sites
70
70
  assert @database.dirty?
71
71
  end
72
72
 
73
+ def test_delete_non_existant_entry
74
+ @database.delete('Twitter', 'foobar')
75
+ assert_equal ['Twitter'], @database.sites
76
+ assert !@database.dirty?
77
+ end
78
+
73
79
  def test_delete_group_entry
74
80
  @database.add('Facebook', 'dudeguy', 'secret', :group => "Social")
75
- @database.delete('Facebook', :group => 'Social')
81
+ @database.delete('Facebook', 'dudeguy', :group => 'Social')
76
82
  assert_equal [], @database.sites(:group => 'Social')
77
83
  assert_equal ['Twitter'], @database.sites
78
84
  end
85
+
86
+ def test_multiple_entries_with_the_same_site
87
+ @database.add('Facebook', 'dudeguy', 'secret')
88
+ @database.add('Facebook', 'foobar', 'secret')
89
+
90
+ expected_1 = {:username => 'dudeguy', :password => 'secret'}
91
+ assert_equal expected_1, @database.get('Facebook', 'dudeguy')
92
+ expected_2 = {:username => 'foobar', :password => 'secret'}
93
+ assert_equal expected_2, @database.get('Facebook', 'foobar')
94
+ assert_equal [expected_1, expected_2], @database.get('Facebook')
95
+ assert_equal ['Facebook', 'Twitter'], @database.sites
96
+ end
97
+
98
+ def test_get_missing_entry_by_site_and_username
99
+ @database.add('Facebook', 'dudeguy', 'secret')
100
+ assert_nil @database.get('Facebook', 'foobar')
101
+ end
102
+
103
+ def test_deleting_one_of_two_entries_with_the_same_site
104
+ @database.add('Facebook', 'dudeguy', 'secret')
105
+ @database.add('Facebook', 'foobar', 'secret')
106
+ @database.delete('Facebook', 'dudeguy')
107
+ assert_nil @database.get('Facebook', 'dudeguy')
108
+ assert_equal({:username => 'foobar', :password => 'secret'}, @database.get('Facebook', 'foobar'))
109
+ end
79
110
  end
80
111
  end
@@ -30,9 +30,9 @@ module Keyrack
30
30
  seq = sequence('ui sequence')
31
31
  @console.expects(:get_password).returns('secret').in_sequence(seq)
32
32
  rsa = mock("rsa key")
33
- Utils.expects(:open_rsa_key).with(rsa_path, 'secret').returns(rsa).in_sequence(seq)
33
+ Utils.expects(:open_rsa_key).with(File.expand_path(rsa_path, keyrack_dir), 'secret').returns(rsa).in_sequence(seq)
34
34
  aes = {'key' => '12345', 'iv' => '54321'}
35
- Utils.expects(:open_aes_data).with(aes_path, rsa).returns(aes).in_sequence(seq)
35
+ Utils.expects(:open_aes_data).with(File.expand_path(aes_path, keyrack_dir), rsa).returns(aes).in_sequence(seq)
36
36
  store = mock('filesystem store')
37
37
  Store::Filesystem.expects(:new).with('path' => store_path).returns(store).in_sequence(seq)
38
38
  Database.expects(:new).with('12345', '54321', store).returns(@database).in_sequence(seq)
@@ -4,7 +4,7 @@ module Keyrack
4
4
  class TestUtils < Test::Unit::TestCase
5
5
  def test_generate_password
6
6
  result = Utils.generate_password
7
- assert_match result, /^[!-~]{8}$/
7
+ assert_match /^[!-~]{8}$/, result
8
8
  end
9
9
 
10
10
  def test_generate_rsa_key
@@ -4,10 +4,14 @@ module Keyrack
4
4
  module UI
5
5
  class TestConsole < Test::Unit::TestCase
6
6
  def setup
7
- @database = stub('database', :sites => %w{Twitter}, :groups => [], :dirty? => false) do
7
+ @database = stub('database', :sites => %w{Twitter Google}, :groups => [], :dirty? => false) do
8
8
  stubs(:get).with('Twitter', {}).returns({
9
9
  :username => 'username', :password => 'password'
10
10
  })
11
+ stubs(:get).with('Google', {}).returns([
12
+ { :username => 'username_1', :password => 'password' },
13
+ { :username => 'username_2', :password => 'password' }
14
+ ])
11
15
  end
12
16
  @highline = stub('highline')
13
17
  @highline.stubs(:color).with("Keyrack Main Menu", :yellow).returns("yellowKeyrack Main Menu")
@@ -17,21 +21,50 @@ module Keyrack
17
21
  end
18
22
 
19
23
  def test_select_entry_from_menu
24
+ seq = sequence('say')
20
25
  @console.database = @database
21
26
  @highline.expects(:say).with("=== yellowKeyrack Main Menu ===")
22
27
  @highline.expects(:say).with(" 1. Twitter [username]")
23
- @highline.expects(:say).with(" n. New entry")
24
- @highline.expects(:say).with(" d. Delete entry")
25
- @highline.expects(:say).with(" g. New group")
26
- @highline.expects(:say).with(" q. Quit")
28
+ @highline.expects(:say).with(" 2. Google [username_1]")
29
+ @highline.expects(:say).with(" 3. Google [username_2]")
30
+ @highline.expects(:say).with("Mode: copy")
31
+ @highline.expects(:say).with("Commands: [n]ew [d]elete [g]roup [m]ode [q]uit")
27
32
 
28
33
  question = mock('question')
29
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 d g}) }).returns('1')
34
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g}) }).returns('1')
30
35
  @console.expects(:Copier).with('password')
31
36
  @highline.expects(:say).with("The password has been copied to your clipboard.")
32
37
  assert_nil @console.menu
33
38
  end
34
39
 
40
+ def test_select_entry_from_menu_in_print_mode
41
+ seq = sequence('say')
42
+ @console.database = @database
43
+ @console.mode = :print
44
+ @highline.expects(:say).with("=== yellowKeyrack Main Menu ===")
45
+ @highline.expects(:say).with(" 1. Twitter [username]")
46
+ @highline.expects(:say).with(" 2. Google [username_1]")
47
+ @highline.expects(:say).with(" 3. Google [username_2]")
48
+ @highline.expects(:say).with("Mode: print")
49
+ @highline.expects(:say).with("Commands: [n]ew [d]elete [g]roup [m]ode [q]uit")
50
+
51
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g}) }).returns('1')
52
+ @highline.expects(:color).with('password', :cyan).returns('cyan[password]').in_sequence(seq)
53
+ question = mock do
54
+ expects(:echo=).with(false)
55
+ if HighLine::SystemExtensions::CHARACTER_MODE != 'stty'
56
+ expects(:character=).with(true)
57
+ expects(:overwrite=).with(true)
58
+ end
59
+ end
60
+ @highline.expects(:ask).
61
+ with('Here you go: cyan[password]. Done? ').
62
+ yields(question).
63
+ returns('')
64
+
65
+ assert_nil @console.menu
66
+ end
67
+
35
68
  def test_select_new_from_menu
36
69
  @console.database = @database
37
70
 
@@ -42,7 +75,7 @@ module Keyrack
42
75
  # g. New group
43
76
  # q. Quit
44
77
 
45
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 d g}) }).returns('n')
78
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g}) }).returns('n')
46
79
  assert_equal :new, @console.menu
47
80
  end
48
81
 
@@ -57,7 +90,7 @@ module Keyrack
57
90
  # q. Quit
58
91
 
59
92
  question = mock('question')
60
- question.expects(:in=).with(%w{n q 1 d g})
93
+ question.expects(:in=).with(%w{n q m 1 2 3 d g})
61
94
  @highline.expects(:ask).yields(question).returns('d')
62
95
  assert_equal :delete, @console.menu
63
96
  end
@@ -72,7 +105,7 @@ module Keyrack
72
105
  # g. New group
73
106
  # q. Quit
74
107
 
75
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 d g}) }).returns('q')
108
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g}) }).returns('q')
76
109
  assert_equal :quit, @console.menu
77
110
  end
78
111
 
@@ -80,7 +113,7 @@ module Keyrack
80
113
  @console.database = @database
81
114
  @database.stubs(:dirty?).returns(true)
82
115
 
83
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 d g s}) }).returns('q')
116
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g s}) }).returns('q')
84
117
  @highline.expects(:agree).with("Really quit? You have unsaved changes! [yn] ").returns(false)
85
118
  assert_equal nil, @console.menu
86
119
  end
@@ -89,8 +122,8 @@ module Keyrack
89
122
  @console.database = @database
90
123
  @database.stubs(:dirty?).returns(true)
91
124
 
92
- @highline.expects(:say).with(" s. Save")
93
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 d g s}) }).returns('s')
125
+ @highline.expects(:say).with { |string| string =~ /\[s\]ave/ }
126
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g s}) }).returns('s')
94
127
  assert_equal :save, @console.menu
95
128
  end
96
129
 
@@ -100,7 +133,7 @@ module Keyrack
100
133
 
101
134
  @highline.expects(:color).with('Blargh', :green).returns('greenBlargh')
102
135
  @highline.expects(:say).with(" 1. greenBlargh")
103
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 2 d g}) }).returns('1')
136
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 4 d g}) }).returns('1')
104
137
  assert_equal({:group => 'Blargh'}, @console.menu)
105
138
  end
106
139
 
@@ -112,12 +145,10 @@ module Keyrack
112
145
  @highline.expects(:color).with("Foo", :green).returns("greenFoo")
113
146
  @highline.expects(:say).with("===== greenFoo =====")
114
147
  @highline.expects(:say).with(" 1. Facebook [username]")
115
- @highline.expects(:say).with(" n. New entry")
116
- @highline.expects(:say).with(" d. Delete entry")
117
- @highline.expects(:say).with(" t. Top level menu")
118
- @highline.expects(:say).with(" q. Quit")
148
+ @highline.expects(:say).with("Mode: copy")
149
+ @highline.expects(:say).with("Commands: [n]ew [d]elete [t]op [m]ode [q]uit")
119
150
 
120
- @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q 1 d t}) }).returns('1')
151
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 d t}) }).returns('1')
121
152
  @console.expects(:Copier).with('password')
122
153
  @highline.expects(:say).with("The password has been copied to your clipboard.")
123
154
  assert_nil @console.menu(:group => 'Foo')
@@ -137,7 +168,7 @@ module Keyrack
137
168
  @highline.expects(:agree).with("Generate password? [yn] ").returns(false).in_sequence(seq)
138
169
  @highline.expects(:ask).with("Password: ").yields(mock { expects(:echo=).with(false) }).returns("baz").in_sequence(seq)
139
170
  @highline.expects(:ask).with("Password (again): ").yields(mock { expects(:echo=).with(false) }).returns("bar").in_sequence(seq)
140
- @highline.expects(:say).with("Passwords didn't match. Try again!").in_sequence(seq)
171
+ @highline.expects(:say).with("Passwords didn't match. Try again!").in_sequence(seq)
141
172
  @highline.expects(:ask).with("Password: ").yields(mock { expects(:echo=).with(false) }).returns("baz").in_sequence(seq)
142
173
  @highline.expects(:ask).with("Password (again): ").yields(mock { expects(:echo=).with(false) }).returns("baz").in_sequence(seq)
143
174
  assert_equal({:site => "Foo", :username => "bar", :password => "baz"}, @console.get_new_entry)
@@ -149,11 +180,11 @@ module Keyrack
149
180
  @highline.expects(:ask).with("Username: ").returns("bar").in_sequence(seq)
150
181
  @highline.expects(:agree).with("Generate password? [yn] ").returns(true).in_sequence(seq)
151
182
  Utils.expects(:generate_password).returns('foobar').in_sequence(seq)
152
- @highline.expects(:color).with('foobar', :blue).returns('bluefoobar').in_sequence(seq)
153
- @highline.expects(:agree).with("Generated bluefoobar. Sound good? [yn] ").returns(false).in_sequence(seq)
183
+ @highline.expects(:color).with('foobar', :cyan).returns('cyanfoobar').in_sequence(seq)
184
+ @highline.expects(:agree).with("Generated cyanfoobar. Sound good? [yn] ").returns(false).in_sequence(seq)
154
185
  Utils.expects(:generate_password).returns('foobar').in_sequence(seq)
155
- @highline.expects(:color).with('foobar', :blue).returns('bluefoobar').in_sequence(seq)
156
- @highline.expects(:agree).with("Generated bluefoobar. Sound good? [yn] ").returns(true).in_sequence(seq)
186
+ @highline.expects(:color).with('foobar', :cyan).returns('cyanfoobar').in_sequence(seq)
187
+ @highline.expects(:agree).with("Generated cyanfoobar. Sound good? [yn] ").returns(true).in_sequence(seq)
157
188
  assert_equal({:site => "Foo", :username => "bar", :password => "foobar"}, @console.get_new_entry)
158
189
  end
159
190
 
@@ -175,7 +206,7 @@ module Keyrack
175
206
 
176
207
  def test_store_setup_for_filesystem
177
208
  @highline.expects(:choose).yields(mock {
178
- expects(:header=).with("Choose storage type:")
209
+ expects(:header=).with("Choose storage type")
179
210
  expects(:choices).with("filesystem", "ssh")
180
211
  }).returns("filesystem")
181
212
 
@@ -186,7 +217,7 @@ module Keyrack
186
217
  def test_store_setup_for_ssh
187
218
  seq = sequence("store setup")
188
219
  @highline.expects(:choose).yields(mock {
189
- expects(:header=).with("Choose storage type:")
220
+ expects(:header=).with("Choose storage type")
190
221
  expects(:choices).with("filesystem", "ssh")
191
222
  }).returns("ssh").in_sequence(seq)
192
223
  @highline.expects(:ask).with("Host: ").returns("example.com").in_sequence(seq)
@@ -221,7 +252,25 @@ module Keyrack
221
252
  }).returns('1').in_sequence(seq)
222
253
  @highline.expects(:color).with("Twitter [username]", :red).returns("redTwitter").in_sequence(seq)
223
254
  @highline.expects(:agree).with("You're about to delete redTwitter. Are you sure? [yn] ").returns(true).in_sequence(seq)
224
- @database.expects(:delete).with("Twitter", {}).in_sequence(seq)
255
+ @database.expects(:delete).with("Twitter", 'username', {}).in_sequence(seq)
256
+ @console.delete_entry
257
+ end
258
+
259
+ def test_delete_one_entry_from_site_with_multiple_entries
260
+ @console.database = @database
261
+
262
+ seq = sequence("deleting")
263
+ @highline.expects(:say).with("Choose entry to delete:").in_sequence(seq)
264
+ @highline.expects(:say).with(" 1. Twitter [username]").in_sequence(seq)
265
+ @highline.expects(:say).with(" 2. Google [username_1]").in_sequence(seq)
266
+ @highline.expects(:say).with(" 3. Google [username_2]").in_sequence(seq)
267
+ @highline.expects(:say).with(" c. Cancel").in_sequence(seq)
268
+ @highline.expects(:ask).yields(mock {
269
+ expects(:in=).with(%w{c 1 2 3})
270
+ }).returns('3').in_sequence(seq)
271
+ @highline.expects(:color).with("Google [username_2]", :red).returns("redGoogle").in_sequence(seq)
272
+ @highline.expects(:agree).with("You're about to delete redGoogle. Are you sure? [yn] ").returns(true).in_sequence(seq)
273
+ @database.expects(:delete).with("Google", 'username_2', {}).in_sequence(seq)
225
274
  @console.delete_entry
226
275
  end
227
276
 
@@ -242,9 +291,18 @@ module Keyrack
242
291
  }).returns('2').in_sequence(seq)
243
292
  @highline.expects(:color).with("Foursquare [username]", :red).returns("redFoursquare").in_sequence(seq)
244
293
  @highline.expects(:agree).with("You're about to delete redFoursquare. Are you sure? [yn] ").returns(true).in_sequence(seq)
245
- @database.expects(:delete).with("Foursquare", :group => 'Social').in_sequence(seq)
294
+ @database.expects(:delete).with("Foursquare", 'username', :group => 'Social').in_sequence(seq)
246
295
  @console.delete_entry(:group => 'Social')
247
296
  end
297
+
298
+ def test_switch_mode_from_menu
299
+ @console.database = @database
300
+ @console.mode = :copy
301
+
302
+ @highline.expects(:ask).yields(mock { expects(:in=).with(%w{n q m 1 2 3 d g}) }).returns('m')
303
+ assert_nil @console.menu
304
+ assert_equal :print, @console.mode
305
+ end
248
306
  end
249
307
  end
250
308
  end
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keyrack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
5
- prerelease:
4
+ version: 0.3.0.pre
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jeremy Stephens
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-01 00:00:00.000000000Z
12
+ date: 2012-01-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-scp
16
- requirement: &28232780 !ruby/object:Gem::Requirement
16
+ requirement: &18759120 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *28232780
24
+ version_requirements: *18759120
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: highline
27
- requirement: &28231960 !ruby/object:Gem::Requirement
27
+ requirement: &18757080 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *28231960
35
+ version_requirements: *18757080
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: copier
38
- requirement: &28231280 !ruby/object:Gem::Requirement
38
+ requirement: &18755860 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *28231280
46
+ version_requirements: *18755860
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
- requirement: &28230680 !ruby/object:Gem::Requirement
49
+ requirement: &18755260 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 1.0.0
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *28230680
57
+ version_requirements: *18755260
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &28230000 !ruby/object:Gem::Requirement
60
+ requirement: &18754600 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 1.5.1
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *28230000
68
+ version_requirements: *18754600
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rcov
71
- requirement: &28229280 !ruby/object:Gem::Requirement
71
+ requirement: &18754000 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *28229280
79
+ version_requirements: *18754000
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: mocha
82
- requirement: &28216480 !ruby/object:Gem::Requirement
82
+ requirement: &18753200 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *28216480
90
+ version_requirements: *18753200
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: cucumber
93
- requirement: &28215980 !ruby/object:Gem::Requirement
93
+ requirement: &18752620 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,7 +98,18 @@ dependencies:
98
98
  version: '0'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *28215980
101
+ version_requirements: *18752620
102
+ - !ruby/object:Gem::Dependency
103
+ name: test-unit
104
+ requirement: &18751900 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *18751900
102
113
  description: Simple password manager with local/remote database storage and RSA encryption.
103
114
  email: viking@pillageandplunder.net
104
115
  executables:
@@ -159,16 +170,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
170
  version: '0'
160
171
  segments:
161
172
  - 0
162
- hash: -3699145682324095706
173
+ hash: -4508723743310181824
163
174
  required_rubygems_version: !ruby/object:Gem::Requirement
164
175
  none: false
165
176
  requirements:
166
- - - ! '>='
177
+ - - ! '>'
167
178
  - !ruby/object:Gem::Version
168
- version: '0'
179
+ version: 1.3.1
169
180
  requirements: []
170
181
  rubyforge_project:
171
- rubygems_version: 1.8.10
182
+ rubygems_version: 1.8.11
172
183
  signing_key:
173
184
  specification_version: 3
174
185
  summary: Simple password manager