flores 0.0.3 → 0.0.4
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.
- checksums.yaml +4 -4
- data/Gemfile +5 -0
- data/Gemfile.lock +8 -0
- data/README.md +70 -65
- data/examples/analyze_number.rb +8 -4
- data/examples/socket_acceptance_spec.rb +27 -19
- data/examples/socket_stress_spec.rb +24 -16
- data/flores.gemspec +1 -1
- data/lib/flores/pki.rb +254 -0
- data/lib/flores/random.rb +96 -2
- data/lib/flores/rspec/analyze.rb +21 -6
- data/lib/flores/rspec/formatters/analyze.rb +51 -5
- data/spec/flores/pki_spec.rb +74 -0
- data/spec/flores/random_spec.rb +116 -0
- data/spec/spec_init.rb +2 -0
- metadata +14 -12
- data/examples/socket_analyze_spec.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5628419d75d7725ac7a85dfb625142a1607b162e
|
4
|
+
data.tar.gz: 79027be03b9a83aca2573c3f05b17838bfb218f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3898ab4b5e26cdd2cd8b1bb01284b0d525f0a141cc211dcfa0804c20f217b06c8e9a2ef37429bede123feb439d6a69a3b38a9e77956aae164776b42dd3e2a885
|
7
|
+
data.tar.gz: 058ddd9a151b86d5728eddcafb880144e7578d673b007b2f8389fc8511aee1f45ffe47620f85beb07251af68c5a80e6f960792bd0bf04f4bd62e5ebcc636bdc0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -8,6 +8,7 @@ GEM
|
|
8
8
|
rspec (~> 3.0)
|
9
9
|
ruby-progressbar (~> 1.4)
|
10
10
|
method_source (0.8.2)
|
11
|
+
multi_json (1.11.1)
|
11
12
|
pry (0.10.1)
|
12
13
|
coderay (~> 1.1.0)
|
13
14
|
method_source (~> 0.8.1)
|
@@ -31,9 +32,14 @@ GEM
|
|
31
32
|
rspec-support (~> 3.2.0)
|
32
33
|
rspec-support (3.2.1)
|
33
34
|
ruby-progressbar (1.7.1)
|
35
|
+
simplecov (0.6.4)
|
36
|
+
multi_json (~> 1.0)
|
37
|
+
simplecov-html (~> 0.5.3)
|
38
|
+
simplecov-html (0.5.3)
|
34
39
|
slop (3.6.0)
|
35
40
|
spoon (0.0.4)
|
36
41
|
ffi
|
42
|
+
stud (0.0.19)
|
37
43
|
|
38
44
|
PLATFORMS
|
39
45
|
java
|
@@ -43,3 +49,5 @@ DEPENDENCIES
|
|
43
49
|
fuubar
|
44
50
|
pry
|
45
51
|
rspec (>= 3.0.0)
|
52
|
+
simplecov
|
53
|
+
stud
|
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Flores - a stress testing library
|
2
2
|
|
3
|
+
This library is named in loving memory of Carlo Flores.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
3
7
|
When writing tests, it is often good to test a wide variety of inputs to ensure
|
4
8
|
your entire input range behaves correctly.
|
5
9
|
|
@@ -14,12 +18,17 @@ to find common patterns in failures!
|
|
14
18
|
Let's look at a sample situation. Ruby's TCPServer. Let's write a spec to cover a spec covering port binding:
|
15
19
|
|
16
20
|
```ruby
|
21
|
+
require "flores/rspec"
|
22
|
+
RSpec.configure do |config|
|
23
|
+
Flores::RSpec.configure(config)
|
24
|
+
end
|
25
|
+
|
17
26
|
describe TCPServer do
|
18
27
|
subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) }
|
19
28
|
let(:port) { 5000 }
|
20
29
|
let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
|
21
30
|
|
22
|
-
after { socket.close}
|
31
|
+
after { socket.close }
|
23
32
|
|
24
33
|
it "should bind successfully" do
|
25
34
|
socket.bind(sockaddr)
|
@@ -46,7 +55,7 @@ Let's assume I don't know anything about tcp port ranges and test randomly in th
|
|
46
55
|
|
47
56
|
```ruby
|
48
57
|
describe TCPServer do
|
49
|
-
let(:port) {
|
58
|
+
let(:port) { Flores::Random.integer(-100_000..100_000) }
|
50
59
|
...
|
51
60
|
end
|
52
61
|
```
|
@@ -61,15 +70,19 @@ Failures:
|
|
61
70
|
|
62
71
|
1) TCPServer should bind successfully
|
63
72
|
Failure/Error: expect(socket.local_address.ip_port).to(be == port)
|
64
|
-
expected: ==
|
65
|
-
got:
|
66
|
-
# ./tcpserver_spec.rb:
|
73
|
+
expected: == 70144
|
74
|
+
got: 4608
|
75
|
+
# ./tcpserver_spec.rb:18:in `block (2 levels) in <top (required)>'
|
67
76
|
|
68
|
-
Finished in 0.
|
77
|
+
Finished in 0.00163 seconds (files took 0.09982 seconds to load)
|
69
78
|
1 example, 1 failure
|
79
|
+
|
80
|
+
Failed examples:
|
81
|
+
|
82
|
+
rspec ./tcpserver_spec.rb:16 # TCPServer should bind successfully
|
70
83
|
```
|
71
84
|
|
72
|
-
Well that's weird. Binding port
|
85
|
+
Well that's weird. Binding port 70144 actually made it bind on port 4608!
|
73
86
|
|
74
87
|
If we run it more times, we'll see all kinds of different results:
|
75
88
|
|
@@ -104,74 +117,66 @@ we introduced randomness to our test inputs.
|
|
104
117
|
We can go further and run a given spec example many times and group the
|
105
118
|
failures by similarity and include context (what the inputs were, etc)
|
106
119
|
|
107
|
-
This library provides an `
|
108
|
-
`it` except that
|
109
|
-
each time. This lets you run a given test many times with many random inputs!
|
120
|
+
This library provides an `stress_it` helper which behaves similarly to rspec's
|
121
|
+
`it` except that the spec is copied (and run) many times.
|
110
122
|
|
111
|
-
The result is grouped by failure and includes context
|
123
|
+
The result is grouped by failure and includes context (`let` and `subject`).
|
124
|
+
Let's see how it works:
|
112
125
|
|
113
|
-
We'll change `it` to use `
|
126
|
+
We'll change `it` to use `stress_it` instead, and also add `analyze_results`:
|
114
127
|
|
115
128
|
```diff
|
116
129
|
- it "should bind successfully" do
|
117
|
-
+
|
130
|
+
+ analyze_results # track the `let` and `subject` values in our tests.
|
131
|
+
+ stress_it "should bind successfully" do
|
118
132
|
```
|
119
133
|
|
134
|
+
The `analyze_results` method just adds an `after` hook to capture the `let` and
|
135
|
+
`subject` values used in each example.
|
136
|
+
|
137
|
+
The final step is to use a custom formatter provided with this library to do the analysis.
|
138
|
+
|
120
139
|
Now rerunning the test. With barely any spec changes from the original, we have
|
121
140
|
now enough randomness and stress testing to identify many different failure cases
|
122
141
|
and input ranges for those failures.
|
123
142
|
|
124
143
|
```
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
Sample exception for {:port=>34402}
|
162
|
-
Address already in use - bind(2) for 127.0.0.1:34402
|
163
|
-
Samples causing Errno::EADDRINUSE:
|
164
|
-
{:port=>50905}
|
165
|
-
{:port=>71202}
|
166
|
-
{:port=>34402}
|
167
|
-
{:port=>28235}
|
168
|
-
{:port=>85641}
|
169
|
-
# ./lib/rspec/stress_it.rb:103:in `block in analyze_it'
|
170
|
-
|
171
|
-
Finished in 0.0735 seconds (files took 0.10247 seconds to load)
|
172
|
-
1 example, 1 failure
|
173
|
-
|
174
|
-
Failed examples:
|
175
|
-
|
176
|
-
rspec ./tcpserver_spec.rb:8 # TCPServer should bind successfully
|
144
|
+
% rspec -f Flores::RSpec::Formatters::Analyze tcpserver_spec.rb
|
145
|
+
|
146
|
+
TCPServer should bind successfully
|
147
|
+
33.96% (of 742 total) tests are successful
|
148
|
+
Failure analysis:
|
149
|
+
46.90% -> [348] SocketError
|
150
|
+
Sample exception for {:socket=>#<Socket:(closed)>, :port=>-74235}
|
151
|
+
getaddrinfo: nodename nor servname provided, or not known
|
152
|
+
Samples causing SocketError:
|
153
|
+
{:socket=>#<Socket:(closed)>, :port=>-60170}
|
154
|
+
{:socket=>#<Socket:(closed)>, :port=>-73159}
|
155
|
+
{:socket=>#<Socket:(closed)>, :port=>-84648}
|
156
|
+
{:socket=>#<Socket:(closed)>, :port=>-5936}
|
157
|
+
{:socket=>#<Socket:(closed)>, :port=>-78195}
|
158
|
+
18.33% -> [136] RSpec::Expectations::ExpectationNotMetError
|
159
|
+
Sample exception for {:socket=>#<Socket:(closed)>, :port=>72849, :sockaddr=>"\x10\x02\x1C\x91\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
160
|
+
expected: == 72849
|
161
|
+
got: 7313
|
162
|
+
Samples causing RSpec::Expectations::ExpectationNotMetError:
|
163
|
+
{:socket=>#<Socket:(closed)>, :port=>74072, :sockaddr=>"\x10\x02!X\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
164
|
+
{:socket=>#<Socket:(closed)>, :port=>77973, :sockaddr=>"\x10\x020\x95\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
165
|
+
{:socket=>#<Socket:(closed)>, :port=>88867, :sockaddr=>"\x10\x02[#\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
166
|
+
{:socket=>#<Socket:(closed)>, :port=>87710, :sockaddr=>"\x10\x02V\x9E\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
167
|
+
{:socket=>#<Socket:(closed)>, :port=>95690, :sockaddr=>"\x10\x02u\xCA\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
168
|
+
0.81% -> [6] Errno::EACCES
|
169
|
+
Sample exception for {:socket=>#<Socket:(closed)>, :port=>65897, :sockaddr=>"\x10\x02\x01i\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
170
|
+
Permission denied - bind(2) for 127.0.0.1:361
|
171
|
+
Samples causing Errno::EACCES:
|
172
|
+
{:socket=>#<Socket:(closed)>, :port=>879, :sockaddr=>"\x10\x02\x03o\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
173
|
+
{:socket=>#<Socket:(closed)>, :port=>66258, :sockaddr=>"\x10\x02\x02\xD2\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
174
|
+
{:socket=>#<Socket:(closed)>, :port=>65829, :sockaddr=>"\x10\x02\x01%\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
175
|
+
{:socket=>#<Socket:(closed)>, :port=>66044, :sockaddr=>"\x10\x02\x01\xFC\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
176
|
+
{:socket=>#<Socket:(closed)>, :port=>65897, :sockaddr=>"\x10\x02\x01i\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"}
|
177
|
+
|
178
|
+
Finished in 0.10509 seconds
|
179
|
+
742 examples, 490 failures
|
177
180
|
```
|
181
|
+
|
182
|
+
Now we can see a wide variety of failure cases all found through randomization. Nice!
|
data/examples/analyze_number.rb
CHANGED
@@ -17,15 +17,19 @@
|
|
17
17
|
require "flores/rspec"
|
18
18
|
require "flores/random"
|
19
19
|
|
20
|
-
RSpec.configure do |
|
21
|
-
Flores::RSpec.configure(
|
22
|
-
|
20
|
+
RSpec.configure do |config|
|
21
|
+
Flores::RSpec.configure(config)
|
22
|
+
Kernel.srand config.seed
|
23
|
+
|
24
|
+
# Demonstrate the wonderful Analyze formatter
|
25
|
+
config.add_formatter("Flores::RSpec::Formatters::Analyze")
|
23
26
|
end
|
24
27
|
|
25
28
|
describe "a random number" do
|
29
|
+
analyze_results
|
30
|
+
|
26
31
|
context "between 0 and 200 inclusive" do
|
27
32
|
let(:number) { Flores::Random.number(0..200) }
|
28
|
-
analyze_results
|
29
33
|
stress_it "should be less than 100" do
|
30
34
|
expect(number).to(be < 100)
|
31
35
|
end
|
@@ -14,12 +14,16 @@
|
|
14
14
|
#
|
15
15
|
# You should have received a copy of the GNU Affero General Public License
|
16
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
require "flores/rspec"
|
17
18
|
require "flores/random"
|
18
19
|
require "socket"
|
19
|
-
require "flores/rspec"
|
20
20
|
|
21
|
-
RSpec.configure do |
|
22
|
-
|
21
|
+
RSpec.configure do |config|
|
22
|
+
Kernel.srand config.seed
|
23
|
+
Flores::RSpec.configure(config)
|
24
|
+
|
25
|
+
# Demonstrate the wonderful Analyze formatter
|
26
|
+
config.add_formatter("Flores::RSpec::Formatters::Analyze")
|
23
27
|
end
|
24
28
|
|
25
29
|
# A factory for encapsulating behavior of a tcp server and client for the
|
@@ -53,7 +57,9 @@ class TCPIntegrationTestFactory
|
|
53
57
|
|
54
58
|
@client.syswrite(text)
|
55
59
|
@client.close
|
56
|
-
server.read
|
60
|
+
data = server.read
|
61
|
+
data.force_encoding(Encoding.default_external).encoding
|
62
|
+
data
|
57
63
|
ensure
|
58
64
|
@client.close unless @client.closed?
|
59
65
|
server.close unless server.nil? || server.closed?
|
@@ -61,25 +67,27 @@ class TCPIntegrationTestFactory
|
|
61
67
|
end
|
62
68
|
|
63
69
|
describe "TCPServer+TCPSocket" do
|
70
|
+
analyze_results
|
71
|
+
|
64
72
|
let(:port) { Flores::Random.integer(1024..65535) }
|
65
73
|
let(:text) { Flores::Random.text(1..2000) }
|
66
74
|
subject { TCPIntegrationTestFactory.new(port) }
|
67
|
-
|
68
|
-
describe "using stress_it" do
|
69
|
-
stress_it2 "should send data correctly", [:port, :text] do
|
70
|
-
begin
|
71
|
-
subject.setup
|
72
|
-
rescue Errno::EADDRINUSE
|
73
|
-
next # Skip port bindings that are in use
|
74
|
-
end
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
before do
|
77
|
+
begin
|
78
|
+
subject.setup
|
79
|
+
rescue Errno::EADDRINUSE
|
80
|
+
skip "Port #{port} was in use. Skipping!"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
stress_it "should send data correctly", [:port, :text] do
|
85
|
+
begin
|
86
|
+
received = subject.send_and_receive(text)
|
87
|
+
expect(received.encoding).to(be == text.encoding)
|
88
|
+
expect(received).to(be == text)
|
89
|
+
ensure
|
90
|
+
subject.teardown
|
83
91
|
end
|
84
92
|
end
|
85
93
|
end
|
@@ -14,31 +14,33 @@
|
|
14
14
|
#
|
15
15
|
# You should have received a copy of the GNU Affero General Public License
|
16
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
require "flores/rspec"
|
17
18
|
require "flores/random"
|
18
19
|
require "socket"
|
19
|
-
require "flores/rspec"
|
20
20
|
|
21
|
-
RSpec.configure do |
|
22
|
-
Flores::RSpec.configure(
|
21
|
+
RSpec.configure do |config|
|
22
|
+
Flores::RSpec.configure(config)
|
23
|
+
Kernel.srand(config.seed)
|
24
|
+
|
25
|
+
# Demonstrate the wonderful Analyze formatter
|
26
|
+
config.add_formatter("Flores::RSpec::Formatters::Analyze")
|
23
27
|
end
|
24
28
|
|
25
29
|
describe TCPServer do
|
26
|
-
|
27
|
-
|
30
|
+
analyze_results
|
28
31
|
subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) }
|
29
32
|
let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
|
30
|
-
let(:ignore_eaddrinuse) do
|
31
|
-
proc do |m, *args|
|
32
|
-
begin
|
33
|
-
m.call(*args)
|
34
|
-
rescue Errno::EADDRINUSE # rubocop:disable Lint/HandleExceptions
|
35
|
-
# ignore
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
33
|
|
40
34
|
after do
|
41
|
-
socket.close
|
35
|
+
socket.close unless socket.closed?
|
36
|
+
end
|
37
|
+
|
38
|
+
context "on a random port" do
|
39
|
+
let(:port) { Flores::Random.integer(-100_000..100_000) }
|
40
|
+
stress_it "should bind successfully", [:port] do
|
41
|
+
socket.bind(sockaddr)
|
42
|
+
expect(socket.local_address.ip_port).to(be == port)
|
43
|
+
end
|
42
44
|
end
|
43
45
|
|
44
46
|
context "on privileged ports" do
|
@@ -53,7 +55,13 @@ describe TCPServer do
|
|
53
55
|
stress_it "should bind on a port" do
|
54
56
|
# EADDRINUSE is expected since we are picking ports at random
|
55
57
|
# Let's ignore this specific exception
|
56
|
-
allow(socket).to(receive(:bind).and_wrap_original
|
58
|
+
allow(socket).to(receive(:bind).and_wrap_original do |original, *args|
|
59
|
+
begin
|
60
|
+
original.call(*args)
|
61
|
+
rescue Errno::EADDRINUSE # rubocop:disable Lint/HandleExceptions
|
62
|
+
# Ignore
|
63
|
+
end
|
64
|
+
end)
|
57
65
|
expect { socket.bind(sockaddr) }.to_not(raise_error)
|
58
66
|
end
|
59
67
|
end
|
data/flores.gemspec
CHANGED
@@ -2,7 +2,7 @@ Gem::Specification.new do |spec|
|
|
2
2
|
files = %x(git ls-files).split("\n")
|
3
3
|
|
4
4
|
spec.name = "flores"
|
5
|
-
spec.version = "0.0.
|
5
|
+
spec.version = "0.0.4"
|
6
6
|
spec.summary = "Fuzz, randomize, and stress your tests"
|
7
7
|
spec.description = <<-DESCRIPTION
|
8
8
|
Add fuzzing, randomization, and stress to your tests.
|
data/lib/flores/pki.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# This file is part of ruby-flores.
|
3
|
+
# Copyright (C) 2015 Jordan Sissel
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU Affero General Public License as
|
7
|
+
# published by the Free Software Foundation, either version 3 of the
|
8
|
+
# License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU Affero General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require "flores/namespace"
|
19
|
+
require "flores/random"
|
20
|
+
require "English"
|
21
|
+
require "openssl"
|
22
|
+
|
23
|
+
module Flores::PKI
|
24
|
+
# Generate a random serial number for a certificate.
|
25
|
+
def self.random_serial
|
26
|
+
# RFC5280 (X509) says:
|
27
|
+
# > 4.1.2.2. Serial Number
|
28
|
+
# > Certificate users MUST be able to handle serialNumber values up to 20 octets
|
29
|
+
Flores::Random.integer(1..9).to_s + Flores::Random.iterations(0..19).collect { Flores::Random.integer(0..9) }.join
|
30
|
+
end
|
31
|
+
|
32
|
+
# A certificate signing request.
|
33
|
+
#
|
34
|
+
# From here, you can configure a certificate to be created based on your
|
35
|
+
# desired configuration.
|
36
|
+
#
|
37
|
+
# Example making a root CA:
|
38
|
+
#
|
39
|
+
# key = OpenSSL::PKey::RSA.generate(4096, 65537)
|
40
|
+
# csr = Flores::PKI::CertificateSigningRequest.new
|
41
|
+
# csr.subject = "OU=Fancy Pants Inc."
|
42
|
+
# certificate = csr.create_root(key)
|
43
|
+
#
|
44
|
+
# Example making an intermediate CA:
|
45
|
+
#
|
46
|
+
# root_key = OpenSSL::PKey::RSA.generate(4096, 65537)
|
47
|
+
# root_csr = Flores::PKI::CertificateSigningRequest.new
|
48
|
+
# root_csr.subject = "OU=Fancy Pants Inc."
|
49
|
+
# root_csr.public_key = root_key.public
|
50
|
+
# root_certificate = csr.create_root(root_key)
|
51
|
+
#
|
52
|
+
# intermediate_key = OpenSSL::PKey::RSA.generate(4096, 65537)
|
53
|
+
# intermediate_csr = Flores::PKI::CertificateSigningRequest.new
|
54
|
+
# intermediate_csr.public_key = intermediate_key.public
|
55
|
+
# intermediate_csr.subject = "OU=Fancy Pants Inc. Intermediate 1"
|
56
|
+
# intermediate_certificate = csr.create_intermediate(root_certificate, root_key)
|
57
|
+
class CertificateSigningRequest
|
58
|
+
# raised when an invalid signing configuration is given
|
59
|
+
class InvalidRequest < StandardError; end
|
60
|
+
|
61
|
+
# raised when invalid data is present in a certificate request
|
62
|
+
class InvalidData < StandardError; end
|
63
|
+
|
64
|
+
# raised when an invalid subject (format, or whatever) is given in a certificate request
|
65
|
+
class InvalidSubject < InvalidData; end
|
66
|
+
|
67
|
+
# raised when an invalid time value is given for a certificate request
|
68
|
+
class InvalidTime < InvalidData; end
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
self.serial = Flores::PKI.random_serial
|
72
|
+
self.digest_method = default_digest_method
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def validate_subject(value)
|
78
|
+
OpenSSL::X509::Name.parse(value)
|
79
|
+
rescue OpenSSL::X509::NameError => e
|
80
|
+
raise InvalidSubject, "Invalid subject '#{value}'. (#{e})"
|
81
|
+
rescue TypeError => e
|
82
|
+
# Bug(?) in MRI 2.1.6(?)
|
83
|
+
raise InvalidSubject, "Invalid subject '#{value}'. (#{e})"
|
84
|
+
end
|
85
|
+
|
86
|
+
def subject=(value)
|
87
|
+
@subject = validate_subject(value)
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :subject
|
91
|
+
|
92
|
+
def subject_alternates=(values)
|
93
|
+
@subject_alternates = values
|
94
|
+
end
|
95
|
+
|
96
|
+
attr_reader :subject_alternates
|
97
|
+
|
98
|
+
def public_key=(value)
|
99
|
+
@public_key = validate_public_key(value)
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_public_key(value)
|
103
|
+
raise InvalidData, "public key must be a OpenSSL::PKey::PKey" unless value.is_a? OpenSSL::PKey::PKey
|
104
|
+
value
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :public_key
|
108
|
+
|
109
|
+
def start_time=(value)
|
110
|
+
@start_time = validate_time(value)
|
111
|
+
end
|
112
|
+
|
113
|
+
attr_reader :start_time
|
114
|
+
|
115
|
+
def expire_time=(value)
|
116
|
+
@expire_time = validate_time(value)
|
117
|
+
end
|
118
|
+
|
119
|
+
attr_reader :expire_time
|
120
|
+
|
121
|
+
def validate_time(value)
|
122
|
+
raise InvalidTime, "#{value.inspect} (class #{value.class.name})" unless value.is_a?(Time)
|
123
|
+
value
|
124
|
+
end
|
125
|
+
|
126
|
+
def certificate
|
127
|
+
return @certificate if @certificate
|
128
|
+
@certificate = OpenSSL::X509::Certificate.new
|
129
|
+
|
130
|
+
# RFC5280
|
131
|
+
# > 4.1.2.1. Version
|
132
|
+
# > version MUST be 3 (value is 2).
|
133
|
+
#
|
134
|
+
# Version value of '2' means a v3 certificate.
|
135
|
+
@certificate.version = 2
|
136
|
+
|
137
|
+
@certificate.subject = subject
|
138
|
+
@certificate.not_before = start_time
|
139
|
+
@certificate.not_after = expire_time
|
140
|
+
@certificate.public_key = public_key
|
141
|
+
@certificate
|
142
|
+
end
|
143
|
+
|
144
|
+
def default_digest_method
|
145
|
+
OpenSSL::Digest::SHA256.new
|
146
|
+
end
|
147
|
+
|
148
|
+
def self_signed?
|
149
|
+
@signing_certificate.nil?
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate!
|
153
|
+
if self_signed?
|
154
|
+
if @signing_key.nil?
|
155
|
+
raise InvalidRequest, "No signing_key given. Cannot sign key."
|
156
|
+
end
|
157
|
+
elsif @signing_certificate.nil? && @signing_key
|
158
|
+
raise InvalidRequest, "signing_key given, but no signing_certificate is set"
|
159
|
+
elsif @signing_certificate && @signing_key.nil?
|
160
|
+
raise InvalidRequest, "signing_certificate given, but no signing_key is set"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def create
|
165
|
+
validate!
|
166
|
+
extensions = OpenSSL::X509::ExtensionFactory.new
|
167
|
+
extensions.subject_certificate = certificate
|
168
|
+
extensions.issuer_certificate = self_signed? ? certificate : signing_certificate
|
169
|
+
|
170
|
+
certificate.issuer = extensions.issuer_certificate.subject
|
171
|
+
certificate.add_extension(extensions.create_extension("subjectKeyIdentifier", "hash", true))
|
172
|
+
|
173
|
+
# RFC 5280 4.2.1.1. Authority Key Identifier
|
174
|
+
# This is "who signed this key"
|
175
|
+
certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always,issuer", true))
|
176
|
+
|
177
|
+
if want_signature_ability?
|
178
|
+
# Create a CA.
|
179
|
+
certificate.add_extension(extensions.create_extension("basicConstraints", "CA:TRUE", true))
|
180
|
+
# Rough googling seems to indicate at least keyCertSign is required for CA and intermediate certs.
|
181
|
+
certificate.add_extension(extensions.create_extension("keyUsage", "keyCertSign, cRLSign, digitalSignature", true))
|
182
|
+
else
|
183
|
+
# Create a client+server certificate
|
184
|
+
#
|
185
|
+
# It feels weird to create a certificate that's valid as both server and client, but a brief inspection of major
|
186
|
+
# web properties (apple.com, google.com, yahoo.com, github.com, fastly.com, mozilla.com, amazon.com) reveals that
|
187
|
+
# major web properties have certificates with both clientAuth and serverAuth extended key usages. Further,
|
188
|
+
# these major server certificates all have digitalSignature and keyEncipherment for key usage.
|
189
|
+
#
|
190
|
+
# Here's the command I used to check this:
|
191
|
+
# echo mozilla.com apple.com github.com google.com yahoo.com fastly.com elastic.co amazon.com \
|
192
|
+
# | xargs -n1 sh -c 'openssl s_client -connect $1:443 \
|
193
|
+
# | sed -ne "/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p" \
|
194
|
+
# | openssl x509 -text -noout | sed -ne "/X509v3 extensions/,/Signature Algorithm/p" | sed -e "s/^/$1 /"' - \
|
195
|
+
# | grep -A2 'Key Usage'
|
196
|
+
certificate.add_extension(extensions.create_extension("keyUsage", "digitalSignature, keyEncipherment", true))
|
197
|
+
certificate.add_extension(extensions.create_extension("extendedKeyUsage", "clientAuth, serverAuth", false))
|
198
|
+
end
|
199
|
+
certificate.serial = OpenSSL::BN.new(serial)
|
200
|
+
certificate.sign(signing_key, digest_method)
|
201
|
+
certificate
|
202
|
+
end
|
203
|
+
|
204
|
+
# Set the certificate which is going to be signing this request.
|
205
|
+
def signing_certificate=(certificate)
|
206
|
+
raise InvalidData, "signing_certificate must be an OpenSSL::X509::Certificate" unless certificate.is_a?(OpenSSL::X509::Certificate)
|
207
|
+
@signing_certificate = certificate
|
208
|
+
end
|
209
|
+
attr_reader :signing_certificate
|
210
|
+
|
211
|
+
attr_reader :signing_key
|
212
|
+
def signing_key=(private_key)
|
213
|
+
raise InvalidData, "signing_key must be an OpenSSL::PKey::PKey (or a subclass)" unless private_key.is_a?(OpenSSL::PKey::PKey)
|
214
|
+
@signing_key = private_key
|
215
|
+
end
|
216
|
+
|
217
|
+
def want_signature_ability=(value)
|
218
|
+
raise InvalidData, "want_signature_ability must be a boolean" unless value == true || value == false
|
219
|
+
@want_signature_ability = value
|
220
|
+
end
|
221
|
+
|
222
|
+
def want_signature_ability?
|
223
|
+
@want_signature_ability == true
|
224
|
+
end
|
225
|
+
|
226
|
+
attr_reader :digest_method
|
227
|
+
def digest_method=(value)
|
228
|
+
raise InvalidData, "digest_method must be a OpenSSL::Digest (or a subclass)" unless value.is_a?(OpenSSL::Digest)
|
229
|
+
@digest_method = value
|
230
|
+
end
|
231
|
+
|
232
|
+
attr_reader :serial
|
233
|
+
def serial=(value)
|
234
|
+
begin
|
235
|
+
Integer(value)
|
236
|
+
rescue
|
237
|
+
raise InvalidData, "Invalid serial value. Must be a number (or a String containing only nubers)"
|
238
|
+
end
|
239
|
+
@serial = value
|
240
|
+
end
|
241
|
+
|
242
|
+
public(:serial, :serial=)
|
243
|
+
public(:subject, :subject=)
|
244
|
+
public(:subject_alternates, :subject_alternates=)
|
245
|
+
public(:public_key, :public_key=)
|
246
|
+
public(:start_time, :start_time=)
|
247
|
+
public(:expire_time, :expire_time=)
|
248
|
+
public(:digest_method, :digest_method=)
|
249
|
+
public(:want_signature_ability?, :want_signature_ability=)
|
250
|
+
public(:signing_key, :signing_key=)
|
251
|
+
public(:signing_certificate, :signing_certificate=)
|
252
|
+
public(:create)
|
253
|
+
end # class CertificateSigningRequest
|
254
|
+
end # Flores::PKI
|
data/lib/flores/random.rb
CHANGED
@@ -16,6 +16,7 @@
|
|
16
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
17
|
|
18
18
|
require "flores/namespace"
|
19
|
+
autoload :Socket, "socket"
|
19
20
|
|
20
21
|
# A collection of methods intended for use in randomized testing.
|
21
22
|
module Flores::Random
|
@@ -26,7 +27,7 @@ module Flores::Random
|
|
26
27
|
# characters.
|
27
28
|
CHARACTERS = [
|
28
29
|
# Basic Latin
|
29
|
-
*(32..126).map(&:chr),
|
30
|
+
*(32..126).map(&:chr).map { |c| c.force_encoding(Encoding.default_external) },
|
30
31
|
|
31
32
|
# hand-selected CJK Unified Ideographs Extension A
|
32
33
|
"㐤", "㐨", "㐻", "㑐",
|
@@ -99,4 +100,97 @@ module Flores::Random
|
|
99
100
|
integer(range).times
|
100
101
|
end
|
101
102
|
end # def iterations
|
102
|
-
|
103
|
+
|
104
|
+
# Return a random element from an array
|
105
|
+
def self.item(array)
|
106
|
+
array[integer(0...array.size)]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Return a random IPv4 address as a string
|
110
|
+
def self.ipv4
|
111
|
+
# TODO(sissel): Support CIDR range restriction?
|
112
|
+
# TODO(sissel): Support netmask restriction?
|
113
|
+
[integer(0..IPV4_MAX)].pack("N").unpack("C4").join(".")
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return a random IPv6 address as a string
|
117
|
+
#
|
118
|
+
# The address may be in abbreviated form (ABCD::01EF):w
|
119
|
+
def self.ipv6
|
120
|
+
# TODO(sissel): Support CIDR range restriction?
|
121
|
+
# TODO(sissel): Support netmask restriction?
|
122
|
+
length = integer(2..8)
|
123
|
+
if length == 8
|
124
|
+
# Full address; nothing to abbreviate
|
125
|
+
ipv6_pack(length)
|
126
|
+
else
|
127
|
+
abbreviation = ipv6_abbreviation(length)
|
128
|
+
if length == 2
|
129
|
+
first = 1
|
130
|
+
second = 1
|
131
|
+
else
|
132
|
+
first = integer(2...length)
|
133
|
+
second = length - first
|
134
|
+
end
|
135
|
+
ipv6_pack(first) + abbreviation + ipv6_pack(second)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Get a TCP socket bound and listening on a random port.
|
140
|
+
#
|
141
|
+
# You are responsible for closing the socket.
|
142
|
+
#
|
143
|
+
# Returns [socket, address, port]
|
144
|
+
def self.tcp_listener(host = "127.0.0.1")
|
145
|
+
socket_listener(Socket::SOCK_STREAM, host)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get a UDP socket bound and listening on a random port.
|
149
|
+
#
|
150
|
+
# You are responsible for closing the socket.
|
151
|
+
#
|
152
|
+
# Returns [socket, address, port]
|
153
|
+
def self.udp_listener(host = "127.0.0.1")
|
154
|
+
socket_listener(Socket::SOCK_DGRAM, host)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
IPV4_MAX = 1 << 32
|
160
|
+
IPV6_SEGMENT = 1 << 16
|
161
|
+
|
162
|
+
def self.ipv6_pack(length)
|
163
|
+
length.times.collect { integer(0..IPV6_SEGMENT).to_s(16) }.join(":")
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.ipv6_abbreviation(length)
|
167
|
+
abbreviate = (integer(0..1) == 0)
|
168
|
+
if abbreviate
|
169
|
+
"::"
|
170
|
+
else
|
171
|
+
":" + (8 - length).times.collect { "0" }.join(":") + ":"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
LISTEN_BACKLOG = 5
|
176
|
+
def self.socket_listener(type, host)
|
177
|
+
socket = server_socket_class.new(Socket::AF_INET, type)
|
178
|
+
socket.bind(Socket.pack_sockaddr_in(0, host))
|
179
|
+
if type == Socket::SOCK_STREAM || type == Socket::SOCK_SEQPACKET
|
180
|
+
socket.listen(LISTEN_BACKLOG)
|
181
|
+
end
|
182
|
+
|
183
|
+
port = socket.local_address.ip_port
|
184
|
+
address = socket.local_address.ip_address
|
185
|
+
[socket, address, port]
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.server_socket_class
|
189
|
+
if RUBY_ENGINE == 'jruby'
|
190
|
+
# https://github.com/jruby/jruby/wiki/ServerSocket
|
191
|
+
ServerSocket
|
192
|
+
else
|
193
|
+
Socket
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end # module Flores::Random
|
data/lib/flores/rspec/analyze.rb
CHANGED
@@ -85,13 +85,21 @@ module Flores::RSpec::Analyze
|
|
85
85
|
end # def total
|
86
86
|
|
87
87
|
def success_count
|
88
|
-
if @results.include?(:
|
89
|
-
@results[:
|
88
|
+
if @results.include?(:passed)
|
89
|
+
@results[:passed].length
|
90
90
|
else
|
91
91
|
0
|
92
92
|
end
|
93
93
|
end # def success_count
|
94
94
|
|
95
|
+
def success_and_pending_count
|
96
|
+
count = 0
|
97
|
+
[:passed, :pending].each do |group|
|
98
|
+
count += @results[group].length
|
99
|
+
end
|
100
|
+
count
|
101
|
+
end # def success_count
|
102
|
+
|
95
103
|
def percent(count)
|
96
104
|
return (count + 0.0) / total
|
97
105
|
end # def percent
|
@@ -100,10 +108,16 @@ module Flores::RSpec::Analyze
|
|
100
108
|
return format("%.2f%%", percent(count) * 100)
|
101
109
|
end # def percent_s
|
102
110
|
|
103
|
-
def to_s
|
111
|
+
def to_s # rubocop:disable Metrics/AbcSize
|
104
112
|
# This method is crazy complex for a formatter. Should refactor this significantly.
|
105
|
-
report = [
|
106
|
-
|
113
|
+
report = []
|
114
|
+
if @results[:pending].any?
|
115
|
+
# We have pending examples, put a clear message.
|
116
|
+
report << "#{percent_s(success_and_pending_count)} (of #{total} total) tests are successful or pending"
|
117
|
+
else
|
118
|
+
report << "#{percent_s(success_count)} (of #{total} total) tests are successful"
|
119
|
+
end
|
120
|
+
report += failure_summary if success_and_pending_count < total
|
107
121
|
report.join("\n")
|
108
122
|
end # def to_s
|
109
123
|
|
@@ -112,7 +126,8 @@ module Flores::RSpec::Analyze
|
|
112
126
|
def failure_summary
|
113
127
|
report = ["Failure analysis:"]
|
114
128
|
report += @results.sort_by { |_, v| -v.length }.collect do |group, instances|
|
115
|
-
next if group == :
|
129
|
+
next if group == :passed
|
130
|
+
next if group == :pending
|
116
131
|
error_report(group, instances)
|
117
132
|
end.reject(&:nil?).flatten
|
118
133
|
report
|
@@ -18,18 +18,62 @@ require "flores/namespace"
|
|
18
18
|
require "rspec/core/formatters/base_text_formatter"
|
19
19
|
|
20
20
|
Flores::RSpec::Formatters::Analyze = Class.new(RSpec::Core::Formatters::BaseTextFormatter) do
|
21
|
-
RSpec::Core::Formatters.register self, :dump_failures, :dump_summary
|
21
|
+
RSpec::Core::Formatters.register self, :dump_failures, :dump_summary, :start, :example_passed, :example_failed, :example_pending
|
22
|
+
|
23
|
+
SPINNER = %w(▘ ▝ ▗ ▖)
|
24
|
+
|
25
|
+
def example_passed(_event)
|
26
|
+
increment(:pass)
|
27
|
+
end
|
28
|
+
|
29
|
+
def example_failed(_event)
|
30
|
+
increment(:failed)
|
31
|
+
end
|
32
|
+
|
33
|
+
def example_pending(_event)
|
34
|
+
increment(:pending)
|
35
|
+
end
|
36
|
+
|
37
|
+
def increment(status)
|
38
|
+
return unless output.tty?
|
39
|
+
now = Time.new
|
40
|
+
if status == :failed
|
41
|
+
output.write("F")
|
42
|
+
elsif status == :pending
|
43
|
+
output.write("P")
|
44
|
+
end
|
45
|
+
|
46
|
+
update_status if now - @last_update > 0.200
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_status
|
50
|
+
glyph = SPINNER[@count]
|
51
|
+
output.write("[2D#{glyph} ")
|
52
|
+
@last_update = Time.new
|
53
|
+
@count += 1
|
54
|
+
@count = 0 if @count >= SPINNER.size
|
55
|
+
end
|
56
|
+
|
57
|
+
def start(event)
|
58
|
+
@last_update = Time.now
|
59
|
+
@total = event.count
|
60
|
+
@count = 0
|
61
|
+
end
|
22
62
|
|
23
63
|
def dump_summary(event)
|
64
|
+
output.write("\r") if output.tty?
|
24
65
|
# The event is an RSpec::Core::Notifications::SummaryNotification
|
25
66
|
# Let's mimic the BaseTextFormatter but without the failing test report
|
26
|
-
output.puts
|
27
67
|
output.puts "Finished in #{event.formatted_duration}"
|
28
68
|
output.puts "#{event.colorized_totals_line}"
|
29
69
|
end
|
30
70
|
|
71
|
+
def failures?(examples)
|
72
|
+
return examples.select { |e| e.metadata[:execution_result].status == :failed }.any?
|
73
|
+
end
|
74
|
+
|
31
75
|
def dump_failures(event)
|
32
|
-
|
76
|
+
return unless failures?(event.examples)
|
33
77
|
group = event.examples.each_with_object(Hash.new { |h, k| h[k] = [] }) do |e, m|
|
34
78
|
m[e.metadata[:full_description]] << e
|
35
79
|
m
|
@@ -45,8 +89,10 @@ Flores::RSpec::Formatters::Analyze = Class.new(RSpec::Core::Formatters::BaseText
|
|
45
89
|
|
46
90
|
def group_by_result(examples) # rubocop:disable Metrics/AbcSize
|
47
91
|
examples.each_with_object(Hash.new { |h, k| h[k] = [] }) do |example, results|
|
48
|
-
|
49
|
-
|
92
|
+
status = example.metadata[:execution_result].status
|
93
|
+
case status
|
94
|
+
when :passed, :pending
|
95
|
+
results[status] << [example.metadata[:values], nil]
|
50
96
|
else
|
51
97
|
exception = example.metadata[:execution_result].exception
|
52
98
|
results[exception.class] << [example.metadata[:values], exception]
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# This file is part of ruby-flores.
|
3
|
+
# Copyright (C) 2015 Jordan Sissel
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU Affero General Public License as
|
7
|
+
# published by the Free Software Foundation, either version 3 of the
|
8
|
+
# License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU Affero General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
require "spec_init"
|
18
|
+
require "flores/pki"
|
19
|
+
|
20
|
+
describe Flores::PKI::CertificateSigningRequest do
|
21
|
+
let(:csr) { Flores::PKI::CertificateSigningRequest.new }
|
22
|
+
|
23
|
+
# Here, I use a 512-bit key for faster tests.
|
24
|
+
# Please do not use 512-bit keys in production.
|
25
|
+
let(:key_bits) { 512 }
|
26
|
+
|
27
|
+
let(:key) { OpenSSL::PKey::RSA.generate(key_bits, 65537) }
|
28
|
+
let(:certificate_duration) { Flores::Random.number(1..86400) }
|
29
|
+
|
30
|
+
#before do
|
31
|
+
#csr.subject = "OU=Fancy Pants Co."
|
32
|
+
#csr.public_key = root_key.public_key
|
33
|
+
#csr.start_time = Time.now
|
34
|
+
#csr.expire_time = csr.start_time + certificate_duration
|
35
|
+
#end
|
36
|
+
|
37
|
+
shared_examples_for "a certificate" do
|
38
|
+
it "returns a valid certificate" do
|
39
|
+
expect(certificate).to(be_a(OpenSSL::X509::Certificate))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "#subject=" do
|
44
|
+
context "with an invalid subject" do
|
45
|
+
let(:certificate_subject) { Flores::Random.text(1..20) }
|
46
|
+
it "fails" do
|
47
|
+
expect { csr.subject = certificate_subject }.to(raise_error(Flores::PKI::CertificateSigningRequest::InvalidSubject))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "a self-signed client/server certificate" do
|
53
|
+
let(:certificate_subject) { "CN=server.example.com" }
|
54
|
+
before do
|
55
|
+
csr.subject = certificate_subject
|
56
|
+
csr.public_key = key.public_key
|
57
|
+
csr.start_time = Time.now
|
58
|
+
csr.expire_time = csr.start_time + certificate_duration
|
59
|
+
csr.signing_key = key
|
60
|
+
end
|
61
|
+
let(:certificate) { csr.create }
|
62
|
+
it_behaves_like "a certificate"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe Flores::PKI do
|
67
|
+
context "#random_serial" do
|
68
|
+
let(:serial) { Flores::PKI.random_serial }
|
69
|
+
stress_it "generates a valid OpenSSL::BN value" do
|
70
|
+
OpenSSL::BN.new(serial)
|
71
|
+
Integer(serial)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/spec/flores/random_spec.rb
CHANGED
@@ -20,11 +20,27 @@ shared_examples_for String do
|
|
20
20
|
stress_it "should be a String" do
|
21
21
|
expect(subject).to(be_a(String))
|
22
22
|
end
|
23
|
+
stress_it "have expected encoding" do
|
24
|
+
expect(subject.encoding).to(be == Encoding.default_external)
|
25
|
+
end
|
23
26
|
stress_it "have valid encoding" do
|
24
27
|
expect(subject).to(be_valid_encoding)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
31
|
+
shared_examples_for "network address" do
|
32
|
+
before { require "socket" }
|
33
|
+
stress_it "should be a valid ipv6 address according to Socket.pack_sockaddr_in" do
|
34
|
+
expect { Socket.pack_sockaddr_in(0, subject) }.not_to(raise_error)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
shared_examples_for Socket do
|
39
|
+
stress_it "should be a Socket" do
|
40
|
+
expect(socket).to(be_a(Socket))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
28
44
|
describe Flores::Random do
|
29
45
|
analyze_results
|
30
46
|
|
@@ -44,6 +60,10 @@ describe Flores::Random do
|
|
44
60
|
stress_it "has correct length" do
|
45
61
|
expect(subject.length).to(eq(length))
|
46
62
|
end
|
63
|
+
|
64
|
+
stress_it "has correct encoding" do
|
65
|
+
expect(subject.encoding).to(be == Encoding.default_external)
|
66
|
+
end
|
47
67
|
end
|
48
68
|
|
49
69
|
context "that is negative" do
|
@@ -128,5 +148,101 @@ describe Flores::Random do
|
|
128
148
|
# counts (via iteration) and is slow on large numbers.
|
129
149
|
expect(range).to(include(subject.count))
|
130
150
|
end
|
151
|
+
|
152
|
+
context "{ ... }" do
|
153
|
+
stress_it "should invoke a given block for each iteration" do
|
154
|
+
count = 0
|
155
|
+
Flores::Random.iterations(range) do
|
156
|
+
count += 1
|
157
|
+
end
|
158
|
+
expect(count).to(be > 0)
|
159
|
+
expect(range).to(include(count))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "#items" do
|
165
|
+
let(:start) { Flores::Random.integer(1..1000) }
|
166
|
+
let(:length) { Flores::Random.integer(1..1000) }
|
167
|
+
let(:range) { start..(start + length) }
|
168
|
+
let(:items) { Flores::Random.iterations(range).collect { Flores::Random.number(1..1000) } }
|
169
|
+
subject { Flores::Random.item(items) }
|
170
|
+
|
171
|
+
stress_it "should choose a random item from the list" do
|
172
|
+
expect(items).to(include(subject))
|
173
|
+
end
|
174
|
+
|
175
|
+
context "with a list of numbers" do
|
176
|
+
stress_it "should be return a number" do
|
177
|
+
expect(subject).to(be_a(Numeric))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#ipv6" do
|
183
|
+
subject { Flores::Random.ipv6 }
|
184
|
+
it_behaves_like "network address"
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "#ipv4" do
|
188
|
+
subject { Flores::Random.ipv4 }
|
189
|
+
it_behaves_like "network address"
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "networking" do
|
193
|
+
let(:socket) { subject[0] }
|
194
|
+
let(:host) { subject[1] }
|
195
|
+
let(:port) { subject[2] }
|
196
|
+
after do
|
197
|
+
socket.close
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "#udp_listener" do
|
201
|
+
let(:text) { Flores::Random.text(1..100) }
|
202
|
+
subject { Flores::Random.udp_listener }
|
203
|
+
it_behaves_like Socket
|
204
|
+
|
205
|
+
context "#recvfrom" do
|
206
|
+
let(:payload) do
|
207
|
+
data, _ = socket.recvfrom(65536)
|
208
|
+
data.force_encoding(text.encoding)
|
209
|
+
end
|
210
|
+
let(:client) { UDPSocket.new }
|
211
|
+
|
212
|
+
before do
|
213
|
+
client.send(text, 0, host, port)
|
214
|
+
end
|
215
|
+
|
216
|
+
after do
|
217
|
+
client.close
|
218
|
+
end
|
219
|
+
|
220
|
+
it "receives udp packets" do
|
221
|
+
expect(payload).to(be == text)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "#tcp_listener" do
|
227
|
+
subject { Flores::Random.tcp_listener }
|
228
|
+
it_behaves_like Socket
|
229
|
+
|
230
|
+
context "#accept" do
|
231
|
+
let(:client) { TCPSocket.new(host, port) }
|
232
|
+
|
233
|
+
before do
|
234
|
+
client
|
235
|
+
end
|
236
|
+
|
237
|
+
after do
|
238
|
+
client.close
|
239
|
+
end
|
240
|
+
|
241
|
+
it "returns a socket" do
|
242
|
+
connection, _address = socket.accept
|
243
|
+
expect(connection).to(be_a(Socket))
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
131
247
|
end
|
132
248
|
end
|
data/spec/spec_init.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flores
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Sissel
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Add fuzzing, randomization, and stress to your tests.
|
@@ -23,7 +23,7 @@ executables: []
|
|
23
23
|
extensions: []
|
24
24
|
extra_rdoc_files: []
|
25
25
|
files:
|
26
|
-
- .rubocop.yml
|
26
|
+
- ".rubocop.yml"
|
27
27
|
- Gemfile
|
28
28
|
- Gemfile.lock
|
29
29
|
- LICENSE.txt
|
@@ -31,41 +31,43 @@ files:
|
|
31
31
|
- README.md
|
32
32
|
- examples/analyze_number.rb
|
33
33
|
- examples/socket_acceptance_spec.rb
|
34
|
-
- examples/socket_analyze_spec.rb
|
35
34
|
- examples/socket_stress_spec.rb
|
36
35
|
- flores.gemspec
|
37
36
|
- lib/flores/namespace.rb
|
37
|
+
- lib/flores/pki.rb
|
38
38
|
- lib/flores/random.rb
|
39
39
|
- lib/flores/rspec.rb
|
40
40
|
- lib/flores/rspec/analyze.rb
|
41
41
|
- lib/flores/rspec/formatters/analyze.rb
|
42
42
|
- lib/flores/rspec/stress.rb
|
43
|
+
- spec/flores/pki_spec.rb
|
43
44
|
- spec/flores/random_spec.rb
|
44
45
|
- spec/flores/rspec/stress_spec.rb
|
45
46
|
- spec/spec_init.rb
|
46
|
-
homepage:
|
47
|
+
homepage:
|
47
48
|
licenses:
|
48
49
|
- AGPL 3.0 - http://www.gnu.org/licenses/agpl-3.0.html
|
49
50
|
metadata: {}
|
50
|
-
post_install_message:
|
51
|
+
post_install_message:
|
51
52
|
rdoc_options: []
|
52
53
|
require_paths:
|
53
54
|
- lib
|
54
55
|
- lib
|
55
56
|
required_ruby_version: !ruby/object:Gem::Requirement
|
56
57
|
requirements:
|
57
|
-
- -
|
58
|
+
- - ">="
|
58
59
|
- !ruby/object:Gem::Version
|
59
60
|
version: '0'
|
60
61
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
62
|
requirements:
|
62
|
-
- -
|
63
|
+
- - ">="
|
63
64
|
- !ruby/object:Gem::Version
|
64
65
|
version: '0'
|
65
66
|
requirements: []
|
66
|
-
rubyforge_project:
|
67
|
-
rubygems_version: 2.4.
|
68
|
-
signing_key:
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.4.6
|
69
|
+
signing_key:
|
69
70
|
specification_version: 4
|
70
71
|
summary: Fuzz, randomize, and stress your tests
|
71
72
|
test_files: []
|
73
|
+
has_rdoc:
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
# This file is part of ruby-flores.
|
3
|
-
# Copyright (C) 2015 Jordan Sissel
|
4
|
-
#
|
5
|
-
# This program is free software: you can redistribute it and/or modify
|
6
|
-
# it under the terms of the GNU Affero General Public License as
|
7
|
-
# published by the Free Software Foundation, either version 3 of the
|
8
|
-
# License, or (at your option) any later version.
|
9
|
-
#
|
10
|
-
# This program is distributed in the hope that it will be useful,
|
11
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
-
# GNU Affero General Public License for more details.
|
14
|
-
#
|
15
|
-
# You should have received a copy of the GNU Affero General Public License
|
16
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
-
require "flores/random"
|
18
|
-
require "socket"
|
19
|
-
require "flores/rspec"
|
20
|
-
|
21
|
-
RSpec.configure do |c|
|
22
|
-
Flores::RSpec.configure(c)
|
23
|
-
end
|
24
|
-
|
25
|
-
describe TCPServer do
|
26
|
-
subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) }
|
27
|
-
let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
|
28
|
-
|
29
|
-
context "on a random port" do
|
30
|
-
let(:port) { Flores::Random.integer(-100_000..100_000) }
|
31
|
-
stress_it2 "should bind successfully", [:port] do
|
32
|
-
begin
|
33
|
-
socket.bind(sockaddr)
|
34
|
-
expect(socket.local_address.ip_port).to(be == port)
|
35
|
-
ensure
|
36
|
-
socket.close
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|