keyrack 0.2.3 → 0.3.0.pre

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