passphrase 0.1.0 → 1.0.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.
@@ -0,0 +1,26 @@
1
+ module Passphrase
2
+ # This class encapsulates the {#count} and {#[]} queries against the
3
+ # "languages" table in the "words" SQLite 3 database.
4
+ class LanguageQuery
5
+ def initialize(db)
6
+ @db = db
7
+ end
8
+
9
+ # @return [Integer] the number of rows in the languages table
10
+ def count
11
+ sql = "SELECT COUNT(*) AS count FROM languages"
12
+ @db.get_first_value(sql)
13
+ end
14
+
15
+ # @param index [Integer] selects a specific row in the languages table
16
+ # @return [String] the language corresponding to the given index
17
+ def [](index)
18
+ sql = "SELECT language FROM languages"
19
+ unless @languages
20
+ @languages = []
21
+ @db.execute(sql).each { |lang| @languages << lang.first }
22
+ end
23
+ @languages[index]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,80 @@
1
+ module Passphrase
2
+ # This is the main class of the Passphrase library for generating
3
+ # passphrases. It's initialized with a two-element hash that specifies the
4
+ # number of words in the resulting passphrase and a flag to use the
5
+ # RANDOM.ORG site as a source of random numbers.
6
+ # @example
7
+ # require "passphrase"
8
+ # p = Passphrase::Passphrase.new(number_of_words: 4, use_random_org: nil)
9
+ # puts p.generate
10
+ class Passphrase
11
+
12
+ # @return [String] the passphrase string
13
+ attr_reader :passphrase
14
+
15
+ # @param options [Hash] characteristics for a new Passphrase object
16
+ # @yieldparam obj [self] the Passphrase object
17
+ def initialize(options={})
18
+ @options = Default.options.merge(options)
19
+ @passphrase = ""
20
+ @languages = []
21
+ @die_rolls = []
22
+ @words = []
23
+ yield self if block_given?
24
+ end
25
+
26
+ # Invokes the Diceware method by running {DicewareMethod.run}. The three
27
+ # resulting arrays are accumulated into instance variables. The words
28
+ # array is formatted into a single passphrase string and stored in another
29
+ # instance variable. The method returns itself to allow method chaining.
30
+ # @return [self] a Passphrase object
31
+ def generate
32
+ @languages, @die_rolls, @words = DicewareMethod.run(@options)
33
+ @passphrase = @words.join(" ")
34
+ self
35
+ end
36
+
37
+ # This virtual attribute accessor returns the number of words in the
38
+ # generated passphrase.
39
+ # @return [Integer] the number of words in the passphrase
40
+ def number_of_words
41
+ @words.size
42
+ end
43
+
44
+ # A predicate method that returns true if the Passphrase object is
45
+ # initialized to use RANDOM.ORG
46
+ # @return [Boolean] returns true of RANDOM.ORG is being used
47
+ def using_random_org?
48
+ @options[:use_random_org]
49
+ end
50
+
51
+ # String representation of a Passphrase object
52
+ # @return [String] the passphrase string
53
+ def to_s
54
+ @passphrase
55
+ end
56
+
57
+ # Returns details for the Passphrase object as a hash.
58
+ # @return [Hash] the details of a Passphrase object
59
+ def inspect
60
+ {
61
+ passphrase: @passphrase,
62
+ number_of_words: @words.size,
63
+ word_origins: word_origins
64
+ }
65
+ end
66
+
67
+ private
68
+
69
+ def word_origins
70
+ word_origins = {}
71
+ @words.each_index do |index|
72
+ word_origins[@words[index]] = {
73
+ language: @languages[index],
74
+ die_rolls: @die_rolls[index]
75
+ }
76
+ end
77
+ word_origins
78
+ end
79
+ end
80
+ end
@@ -1,14 +1,5 @@
1
- #! /usr/bin/env ruby
2
-
3
1
  module Passphrase
4
- module Version
5
-
6
- # rubygems rational versioning policy
7
- # http://docs.rubygems.org/read/chapter/7
8
- MAJOR = 0
9
- MINOR = 1
10
- BUILD = 0
11
-
12
- STRING = [MAJOR, MINOR, BUILD].compact.join('.')
13
- end
2
+ # Version numbers are bumped according to {http://semver.org Semantic
3
+ # Versioning}.
4
+ VERSION = "1.0.0"
14
5
  end
@@ -0,0 +1,21 @@
1
+ module Passphrase
2
+ # This class encapsulates the {#where} query against the "words" table in
3
+ # the "words" SQLite 3 database. The filter parameter must be a hash that
4
+ # specifies a language and sequence of die rolls.
5
+ # @example
6
+ # {language: "afrikaans", die_rolls: "11111"}
7
+ class WordQuery
8
+ def initialize(db)
9
+ @db = db
10
+ end
11
+
12
+ # @param filter [Hash] specifies the language/die_roll combination
13
+ # @return [String] the selected words from the wordlist
14
+ def where(filter)
15
+ sql = "SELECT words " +
16
+ "FROM words " +
17
+ "WHERE language = :language AND die_rolls = :die_rolls"
18
+ @db.get_first_value(sql, filter)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ require "passphrase"
2
+ require "optparse"
3
+
4
+ module Passphrase
5
+ RSpec.describe CLI, "implements the command line interface" do
6
+ it "responds to class method parse() with one argument" do
7
+ expect(CLI).to respond_to(:parse).with(1).argument
8
+ end
9
+
10
+ it "does not respond to class method validate_number_of_words() (private)" do
11
+ expect(CLI).not_to respond_to(:validate_number_of_words)
12
+ end
13
+
14
+ it "does not respond to class method handle_error() (private)" do
15
+ expect(CLI).not_to respond_to(:handle_error)
16
+ end
17
+
18
+ it "emits an error when an invalid option is supplied" do
19
+ # For some reason, :handle_error doesn't need to be stubbed
20
+ expect(CLI).to receive(:handle_error)
21
+ .with(kind_of(OptionParser::InvalidOption))
22
+ bad_option = ["-x"]
23
+ CLI.parse(bad_option)
24
+ end
25
+
26
+ it "emits an error when a mandatory argument is missing" do
27
+ # For some reason, :handle_error doesn't need to be stubbed
28
+ expect(CLI).to receive(:handle_error).twice
29
+ .with(kind_of(OptionParser::MissingArgument))
30
+ [["-n"], ["--num-words"]].each { |bad_option| CLI.parse(bad_option) }
31
+ end
32
+
33
+ it "emits an error when the number of words is out of range" do
34
+ under_range = (Default.number_range.min - 1).to_s
35
+ over_range = (Default.number_range.max + 1).to_s
36
+ # For some reason, :handle_error doesn't need to be stubbed
37
+ expect(CLI).to receive(:handle_error).exactly(4).times
38
+ .with(kind_of(RuntimeError))
39
+ ["-n", "--num-words"].each do |opt|
40
+ [under_range, over_range].each do |arg|
41
+ bad_option = [opt, arg]
42
+ CLI.parse(bad_option)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,61 @@
1
+ require "passphrase"
2
+
3
+ module Passphrase
4
+ RSpec.describe DicewareMethod, "implements the Diceware method" do
5
+ it "responds to the class method run() with one argument" do
6
+ expect(DicewareMethod).to respond_to(:run).with(1).argument
7
+ end
8
+
9
+ context "initialized with given options" do
10
+ before do
11
+ @number_of_words = 5
12
+ options = { number_of_words: @number_of_words, use_random_org: nil }
13
+ @diceware = DicewareMethod.new(options)
14
+ end
15
+
16
+ it "responds to the run() method with no arguments" do
17
+ expect(@diceware).to respond_to(:run)
18
+ end
19
+
20
+ it "does not respond to the setup_database() method (private)" do
21
+ expect(@diceware).not_to respond_to(:setup_database)
22
+ end
23
+
24
+ it "does not respond to the setup_queries() method (private)" do
25
+ expect(@diceware).not_to respond_to(:setup_queries)
26
+ end
27
+
28
+ it "does not respond to the get_random_languages() method (private)" do
29
+ expect(@diceware).not_to respond_to(:get_random_languages)
30
+ end
31
+
32
+ it "does not respond to the get_random_random_die_rolls() method (private)" do
33
+ expect(@diceware).not_to respond_to(:get_random_random_die_rolls)
34
+ end
35
+
36
+ it "does not respond to the select_words_from_wordlist() method (private)" do
37
+ expect(@diceware).not_to respond_to(:select_words_from_wordlist)
38
+ end
39
+
40
+ describe "#run" do
41
+ before do
42
+ @result = @diceware.run
43
+ end
44
+
45
+ it "returns an array of three arrays" do
46
+ expect(@result).to contain_exactly(
47
+ an_instance_of(Array),
48
+ an_instance_of(Array),
49
+ an_instance_of(Array)
50
+ )
51
+ end
52
+
53
+ it "each array is of size given by the number of words specified" do
54
+ @result.each do |result|
55
+ expect(result.size).to eq(@number_of_words)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,124 @@
1
+ require "passphrase"
2
+
3
+ module Passphrase
4
+ RSpec.shared_examples "DicewareRandom object" do
5
+ it "responds to the indices() method with 2 args" do
6
+ expect(@random).to respond_to(:indices).with(2).arguments
7
+ end
8
+
9
+ it "responds to the die_rolls() method with 1 arg" do
10
+ expect(@random).to respond_to(:die_rolls).with(1).arguments
11
+ end
12
+
13
+ it "does not respond to the setup_remote_generator() method (private)" do
14
+ expect(@random).not_to respond_to(:setup_remote_generator)
15
+ end
16
+
17
+ it "does not respond to the setup_local_generator() method (private)" do
18
+ expect(@random).not_to respond_to(:setup_local_generator)
19
+ end
20
+
21
+ it "does not respond to the generate_random_numbers() method (private)" do
22
+ expect(@random).not_to respond_to(:generate_random_numbers)
23
+ end
24
+
25
+ it "does not respond to the group_die_rolls() method (private)" do
26
+ expect(@random).not_to respond_to(:group_die_rolls)
27
+ end
28
+
29
+ describe "#indices(4, 15)" do
30
+ before do
31
+ @result = @random.indices(4, 15)
32
+ end
33
+
34
+ it "returns an array" do
35
+ expect(@result).to be_an_instance_of(Array)
36
+ end
37
+
38
+ it "of size 4" do
39
+ expect(@result.size).to eq(4)
40
+ end
41
+
42
+ it "all Fixnum items" do
43
+ expect(@result).to all be_an_instance_of(Fixnum)
44
+ end
45
+
46
+ it "each one in the range 0...15" do
47
+ expect(@result).to all be_between(0, 14)
48
+ end
49
+ end
50
+
51
+ describe "#die_rolls(6)" do
52
+ before do
53
+ @result = @random.die_rolls(6)
54
+ end
55
+
56
+ it "returns an array" do
57
+ expect(@result).to be_an_instance_of(Array)
58
+ end
59
+
60
+ it "of size 6" do
61
+ expect(@result.size).to eq(6)
62
+ end
63
+
64
+ it "all String items" do
65
+ expect(@result).to all be_an_instance_of(String)
66
+ end
67
+
68
+ it "each one five characters long" do
69
+ expect(@result.map(&:length)).to all eq(5)
70
+ end
71
+
72
+ it "that match the pattern /^[123456]+$/" do
73
+ expect(@result).to all match(/^[123456]+$/)
74
+ end
75
+ end
76
+ end
77
+
78
+ RSpec.describe DicewareRandom, "generates and formats arrays of random numbers" do
79
+ it "responds to the attribute reader method random_org_requests()" do
80
+ expect(DicewareRandom).to respond_to(:random_org_requests)
81
+ end
82
+
83
+ context "initialized by default to use the local Ruby random number generator" do
84
+ before do
85
+ @random = DicewareRandom.new
86
+ end
87
+
88
+ include_examples "DicewareRandom object"
89
+ end
90
+
91
+ context "initialized to use random numbers from RANDOM.ORG" do
92
+ before do
93
+ @random = DicewareRandom.new(:use_random_org)
94
+ end
95
+
96
+ it "initially has a zero random.org request count" do
97
+ expect(DicewareRandom.random_org_requests).to eq(0)
98
+ end
99
+
100
+ it "increments the random.org request count on indices()" do
101
+ expect {
102
+ @random.indices(4, 15)
103
+ }.to change(DicewareRandom, :random_org_requests).by(1)
104
+ end
105
+
106
+ it "increments the RANDOM.ORG request count on die_rolls()" do
107
+ expect {
108
+ @random.die_rolls(4)
109
+ }.to change(DicewareRandom, :random_org_requests).by(1)
110
+ end
111
+
112
+ it "does not respond to the check_random_org_quota() method (private)" do
113
+ expect(@random).not_to respond_to(:check_random_org_quota)
114
+ end
115
+
116
+ it "raises an exception when a network error occurs (e.g. wrong address)" do
117
+ @random.instance_eval { @random_org_uri = "https://www.randomx.org" }
118
+ expect { @random.indices(4, 15) }.to raise_error
119
+ end
120
+
121
+ include_examples "DicewareRandom object"
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,48 @@
1
+ require "passphrase"
2
+
3
+ module Passphrase
4
+ RSpec.describe LanguageQuery, "queries the languages table in a valid wordlist database" do
5
+ before do
6
+ wordlist_file = "../../lib/passphrase/wordlist/words.sqlite3"
7
+ wordlist_path = File.join(File.dirname(__FILE__), wordlist_file)
8
+ db = SQLite3::Database.new(wordlist_path, readonly: true)
9
+ @languages = LanguageQuery.new(db)
10
+ end
11
+
12
+ it "responds to the count() method with no arguments" do
13
+ expect(@languages).to respond_to(:count).with(0).arguments
14
+ end
15
+
16
+ it "responds to the []() method with one argument" do
17
+ expect(@languages).to respond_to(:[]).with(1).argument
18
+ end
19
+
20
+ context "#count" do
21
+ before do
22
+ @count = @languages.count
23
+ end
24
+
25
+ it "returns an integer" do
26
+ expect(@count).to be_an_instance_of(Fixnum)
27
+ end
28
+
29
+ it "greater than zero" do
30
+ expect(@count).to be > 0
31
+ end
32
+ end
33
+
34
+ context "#[](0)" do
35
+ before do
36
+ @language = @languages[0]
37
+ end
38
+
39
+ it "returns a string" do
40
+ expect(@language).to be_an_instance_of(String)
41
+ end
42
+
43
+ it "that is not empty" do
44
+ expect(@language).not_to be_empty
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,118 @@
1
+ require "passphrase"
2
+
3
+ module Passphrase
4
+ RSpec.describe Passphrase, "for generating passphrase objects" do
5
+ it "yields itself during instantiation" do
6
+ expect { |b| Passphrase.new(Default.options, &b) }.to yield_with_args(Passphrase)
7
+ end
8
+
9
+ # Dependence on RANDOM.ORG only affects the DicewareRandom class.
10
+ # Therefore, only need to test the case where RANDOM.ORG is not used.
11
+ context "initialized to generate a passphrase with 1 word, not using RANDOM.ORG" do
12
+ before do
13
+ @passphrase = Passphrase.new(number_of_words: 1, use_random_org: nil)
14
+ end
15
+
16
+ it "responds to the generate() method with zero arguments" do
17
+ expect(@passphrase).to respond_to(:generate).with(0).arguments
18
+ end
19
+
20
+ it "responds to the to_s() method" do
21
+ expect(@passphrase).to respond_to(:to_s)
22
+ end
23
+
24
+ it "responds to the inspect() method with zero arguments" do
25
+ expect(@passphrase).to respond_to(:inspect).with(0).arguments
26
+ end
27
+
28
+ it "responds to attribute reader method passphrase()" do
29
+ expect(@passphrase).to respond_to(:passphrase)
30
+ end
31
+
32
+ it "responds to predicate method using_random_org?()" do
33
+ expect(@passphrase).to respond_to(:using_random_org?)
34
+ end
35
+
36
+ it "responds to virtual attribute reader method number_of_words()" do
37
+ expect(@passphrase).to respond_to(:number_of_words).with(0).arguments
38
+ end
39
+
40
+ it "does not respond to the word_origins() method (private)" do
41
+ expect(@passphrase).not_to respond_to(:word_origins)
42
+ end
43
+
44
+ it "initially contains an empty passphrase string" do
45
+ expect(@passphrase.passphrase).to be_empty
46
+ end
47
+
48
+ it "initially contains zero words" do
49
+ expect(@passphrase.number_of_words).to eq(0)
50
+ end
51
+
52
+ it "should not be using RANDOM.ORG" do
53
+ expect(@passphrase).not_to be_using_random_org
54
+ end
55
+
56
+ context "after executing the generate() method" do
57
+ before do
58
+ dwr = [["language"], ["11111"], ["passphrase"]]
59
+ allow(DicewareMethod).to receive(:run).and_return(dwr)
60
+ @result = @passphrase.generate
61
+ end
62
+
63
+ it "returns itself" do
64
+ expect(@result).to equal(@passphrase)
65
+ end
66
+
67
+ it "contains the passphrase string" do
68
+ expect(@result.passphrase).to eq("passphrase")
69
+ end
70
+
71
+ it "contains a number of words equal to one" do
72
+ expect(@result.number_of_words).to eq(1)
73
+ end
74
+
75
+ it "returns the passphrase when printed" do
76
+ sio = StringIO.new("")
77
+ $stdout = sio
78
+ expect { puts @result }.to change { sio.string.chomp }
79
+ .from("").to(@result.passphrase)
80
+ end
81
+
82
+ it "can be inspected" do
83
+ expect(@result.inspect).to match(
84
+ passphrase: "passphrase",
85
+ number_of_words: 1,
86
+ word_origins: { "passphrase" => {
87
+ language: "language",
88
+ die_rolls: "11111"
89
+ }
90
+ }
91
+ )
92
+ end
93
+ end
94
+ end
95
+
96
+ context "initialized to use RANDOM.ORG" do
97
+ it "the predicate method should confirm it is using RANDOM.ORG" do
98
+ passphrase = Passphrase.new(number_of_words: 3, use_random_org: true)
99
+ expect(passphrase).to be_using_random_org
100
+ end
101
+ end
102
+
103
+ context "initialized with default options" do
104
+ before do
105
+ @passphrase = Passphrase.new
106
+ @passphrase.generate
107
+ end
108
+
109
+ it "return a passphrase with the default number of words" do
110
+ expect(@passphrase.number_of_words).to eq(Default.options[:number_of_words])
111
+ end
112
+
113
+ it "should not be using RANDOM.ORG" do
114
+ expect(@passphrase).not_to be_using_random_org
115
+ end
116
+ end
117
+ end
118
+ end