cert_watch 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +151 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/cert_watch/application.js +13 -0
- data/app/assets/stylesheets/cert_watch/application.css +15 -0
- data/app/controllers/cert_watch/application_controller.rb +5 -0
- data/app/jobs/cert_watch/install_certificate_job.rb +16 -0
- data/app/jobs/cert_watch/renew_certificate_job.rb +16 -0
- data/app/jobs/cert_watch/renew_expiring_certificates_job.rb +13 -0
- data/app/models/cert_watch/certificate.rb +42 -0
- data/app/views/layouts/cert_watch/application.html.erb +14 -0
- data/config/locales/de.yml +47 -0
- data/config/locales/en.yml +47 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20160711193700_create_certificates.rb +18 -0
- data/lib/cert_watch/CHANGELOG.md +5 -0
- data/lib/cert_watch/certbot_client.rb +32 -0
- data/lib/cert_watch/client.rb +7 -0
- data/lib/cert_watch/configuration.rb +37 -0
- data/lib/cert_watch/domain_owner.rb +23 -0
- data/lib/cert_watch/engine.rb +10 -0
- data/lib/cert_watch/error.rb +4 -0
- data/lib/cert_watch/install_error.rb +4 -0
- data/lib/cert_watch/installer.rb +7 -0
- data/lib/cert_watch/pem_directory_installer.rb +55 -0
- data/lib/cert_watch/renew_error.rb +4 -0
- data/lib/cert_watch/sanitize.rb +13 -0
- data/lib/cert_watch/shell.rb +20 -0
- data/lib/cert_watch/version.rb +3 -0
- data/lib/cert_watch/views/all.rb +3 -0
- data/lib/cert_watch/views/certificate_state.rb +42 -0
- data/lib/cert_watch.rb +32 -0
- data/spec/cert_watch/certbot_client_spec.rb +53 -0
- data/spec/cert_watch/domain_owner_spec.rb +62 -0
- data/spec/cert_watch/pem_directory_installer_spec.rb +75 -0
- data/spec/cert_watch/sanitize_spec.rb +19 -0
- data/spec/cert_watch/shell_spec.rb +19 -0
- data/spec/cert_watch/views/certificate_state_spec.rb +33 -0
- data/spec/examples.txt +40 -0
- data/spec/factories/certificates.rb +6 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +7 -0
- data/spec/internal/log/jobs/test/cert_watch.log +3793 -0
- data/spec/internal/log/test.log +24187 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb +43 -0
- data/spec/models/cert_watch/certificate_spec.rb +102 -0
- data/spec/rails_helper.rb +45 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/config/cert_watch.rb +7 -0
- data/spec/support/config/factory_girl.rb +11 -0
- data/spec/support/config/resque_logger.rb +16 -0
- data/spec/support/config/timecop.rb +21 -0
- data/spec/support/helpers/doubles.rb +28 -0
- data/spec/support/helpers/fixtures.rb +25 -0
- data/spec/support/helpers/inline_resque.rb +9 -0
- data/spec/support/helpers/view_component_example_group.rb +31 -0
- metadata +298 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module CertWatch
|
2
|
+
module Sanitize
|
3
|
+
extend self
|
4
|
+
|
5
|
+
class ForbiddenCharacters < Error; end
|
6
|
+
|
7
|
+
def check_domain!(name)
|
8
|
+
if name =~ /[^0-9a-zA-Z.-]/
|
9
|
+
fail(ForbiddenCharacters, "Domain '#{name}' contains forbidden characters.")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CertWatch
|
2
|
+
module Shell
|
3
|
+
extend self
|
4
|
+
|
5
|
+
class CommandFailed < Error; end
|
6
|
+
|
7
|
+
def sudo(command)
|
8
|
+
output, input = IO.pipe
|
9
|
+
prefix = !Rails.env.test? ? 'sudo ' : ''
|
10
|
+
full_command = [prefix, command].join
|
11
|
+
|
12
|
+
result = system(full_command, [:out, :err] => input)
|
13
|
+
input.close
|
14
|
+
|
15
|
+
unless result
|
16
|
+
fail(CommandFailed, "Command '#{full_command}' failed with output:\n\n#{output.read}\n")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'arbre'
|
2
|
+
require 'active_admin'
|
3
|
+
|
4
|
+
module CertWatch
|
5
|
+
module Views
|
6
|
+
class CertificateState < Arbre::Component
|
7
|
+
builder_method :cert_watch_certificate_state
|
8
|
+
|
9
|
+
STATE_MAPPING = {
|
10
|
+
'installed' => 'ok',
|
11
|
+
'installing' => 'warn',
|
12
|
+
'renewing' => 'warn',
|
13
|
+
'installing_failed' => 'error',
|
14
|
+
'renewing_failed' => 'error'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def build(certificate_or_domain, options = {})
|
18
|
+
state = get_state(certificate_or_domain)
|
19
|
+
format = options.fetch(:format, 'short')
|
20
|
+
|
21
|
+
add_class 'cert_watch_certificate_state'
|
22
|
+
|
23
|
+
status_tag(t(state, scope: "cert_watch.states.#{format}"),
|
24
|
+
[state, STATE_MAPPING[state]].compact.join(' '))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def get_state(certificate_or_domain)
|
30
|
+
get_certificate(certificate_or_domain).try(:state) || 'not_found'
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_certificate(certificate_or_domain)
|
34
|
+
if certificate_or_domain.is_a?(String)
|
35
|
+
Certificate.find_by_domain(certificate_or_domain)
|
36
|
+
else
|
37
|
+
certificate_or_domain
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/cert_watch.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'cert_watch/engine'
|
2
|
+
|
3
|
+
module CertWatch
|
4
|
+
def self.config
|
5
|
+
fail('Call CertWatch.setup before accessing CertWatch.config') unless @config
|
6
|
+
@config
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.setup
|
10
|
+
@config = Configuration.new
|
11
|
+
yield @config if block_given?
|
12
|
+
|
13
|
+
self.client = CertbotClient.new(executable: config.certbot_executable,
|
14
|
+
port: config.certbot_port)
|
15
|
+
|
16
|
+
self.installer = PemDirectoryInstaller.new(pem_directory: config.pem_directory,
|
17
|
+
input_directory: config.certbot_output_directory,
|
18
|
+
reload_command: config.server_reload_command)
|
19
|
+
end
|
20
|
+
|
21
|
+
mattr_accessor :client
|
22
|
+
|
23
|
+
mattr_accessor :installer
|
24
|
+
|
25
|
+
def self.active_admin_load_path
|
26
|
+
Dir[CertWatch::Engine.root.join('admin')].first
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.domain_owner(options)
|
30
|
+
DomainOwner.define(options)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module CertWatch
|
4
|
+
RSpec.describe CertbotClient do
|
5
|
+
let(:shell) do
|
6
|
+
instance_double('Shell', sudo: nil)
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:client) do
|
10
|
+
CertbotClient.new(executable: '/usr/bin/certbot',
|
11
|
+
port: 99,
|
12
|
+
shell: shell)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'invokes given executable' do
|
16
|
+
client.renew('some.example.com')
|
17
|
+
|
18
|
+
expect(shell).to have_received(:sudo).with(a_string_including('/usr/bin/certbot'))
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'passes given port' do
|
22
|
+
client.renew('some.example.com')
|
23
|
+
|
24
|
+
expect(shell).to have_received(:sudo).with(a_string_including('--http-01-port 99'))
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'passes domain' do
|
28
|
+
client.renew('some.example.com')
|
29
|
+
|
30
|
+
expect(shell).to have_received(:sudo).with(a_string_including('-d some.example.com'))
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'fails if domain contains forbidden characters' do
|
34
|
+
expect do
|
35
|
+
client.renew('some.example.com;" rm *')
|
36
|
+
end.to raise_error(Sanitize::ForbiddenCharacters)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'passes --renew-by-default flag' do
|
40
|
+
client.renew('some.example.com')
|
41
|
+
|
42
|
+
expect(shell).to have_received(:sudo).with(a_string_including('--renew-by-default'))
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'fails with RenewError if shell command fails' do
|
46
|
+
allow(shell).to receive(:sudo).and_raise(Shell::CommandFailed)
|
47
|
+
|
48
|
+
expect do
|
49
|
+
client.renew('some.example.com')
|
50
|
+
end.to raise_error(RenewError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
require 'support/helpers/doubles'
|
4
|
+
|
5
|
+
module CertWatch
|
6
|
+
RSpec.describe DomainOwner do
|
7
|
+
let(:test_domain_owner_model) do
|
8
|
+
Class.new(ActiveRecord::Base) do
|
9
|
+
self.table_name = :test_domain_owner
|
10
|
+
include CertWatch.domain_owner(attribute: :cname)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'renews certificate on create' do
|
15
|
+
test_domain_owner_model.create!(cname: 'new.example.com')
|
16
|
+
|
17
|
+
certificate = Certificate.find_by_domain('new.example.com')
|
18
|
+
|
19
|
+
expect(certificate).to be_present
|
20
|
+
expect(certificate.state).to eq('renewing')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'renews certificate on update' do
|
24
|
+
domain_owner = test_domain_owner_model.create!(cname: 'old.example.com')
|
25
|
+
|
26
|
+
domain_owner.update!(cname: 'new.example.com')
|
27
|
+
certificate = Certificate.find_by_domain('new.example.com')
|
28
|
+
|
29
|
+
expect(certificate).to be_present
|
30
|
+
expect(certificate.state).to eq('renewing')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'does not renew certificate when cname is unchanged' do
|
34
|
+
domain_owner = test_domain_owner_model.create!(name: 'Old', cname: 'some.example.com')
|
35
|
+
certificate = Certificate.find_by_domain!('some.example.com')
|
36
|
+
certificate.update!(state: 'installed')
|
37
|
+
|
38
|
+
domain_owner.update!(name: 'New')
|
39
|
+
|
40
|
+
expect(certificate.reload.state).to eq('installed')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'does not create certificate when new cname is blank' do
|
44
|
+
domain_owner = test_domain_owner_model.create!(name: 'Old', cname: 'some.example.com')
|
45
|
+
certificate = Certificate.find_by_domain!('some.example.com')
|
46
|
+
certificate.update!(state: 'installed')
|
47
|
+
|
48
|
+
expect do
|
49
|
+
domain_owner.update!(cname: '')
|
50
|
+
end.not_to change { Certificate.count }
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'abandons old certificate on update' do
|
54
|
+
domain_owner = test_domain_owner_model.create!(cname: 'old.example.com')
|
55
|
+
|
56
|
+
domain_owner.update!(cname: 'new.example.com')
|
57
|
+
certificate = Certificate.find_by_domain('old.example.com')
|
58
|
+
|
59
|
+
expect(certificate.state).to eq('abandoned')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module CertWatch
|
4
|
+
RSpec.describe PemDirectoryInstaller, fixture_files: true do
|
5
|
+
before do
|
6
|
+
Fixtures.file('live/some.example.com/fullchain.pem', "FULL CHAIN\n")
|
7
|
+
Fixtures.file('live/some.example.com/privkey.pem', "PRIVATE KEY\n")
|
8
|
+
Fixtures.directory('ssl')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'concatenates full chain and private key files' do
|
12
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
13
|
+
pem_directory: 'ssl',
|
14
|
+
reload_command: 'touch reload.txt')
|
15
|
+
installer.install('some.example.com')
|
16
|
+
|
17
|
+
expect(File.read('ssl/some.example.com.pem')).to eq("FULL CHAIN\nPRIVATE KEY\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'invokes reload command' do
|
21
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
22
|
+
pem_directory: 'ssl',
|
23
|
+
reload_command: 'touch reload.txt')
|
24
|
+
installer.install('some.example.com')
|
25
|
+
|
26
|
+
expect(File.exist?('reload.txt')).to eq(true)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'fails with InstallError if reload command fails' do
|
30
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
31
|
+
pem_directory: 'ssl',
|
32
|
+
reload_command: './not_there')
|
33
|
+
expect do
|
34
|
+
installer.install('some.example.com')
|
35
|
+
end.to raise_error(InstallError)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'fails with InstallError if input files not found' do
|
39
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
40
|
+
pem_directory: 'ssl')
|
41
|
+
expect do
|
42
|
+
installer.install('not-there.example.com')
|
43
|
+
end.to raise_error(InstallError)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'does not create output file if input files not found' do
|
47
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
48
|
+
pem_directory: 'ssl')
|
49
|
+
|
50
|
+
begin
|
51
|
+
installer.install('not-there.example.com')
|
52
|
+
rescue InstallError
|
53
|
+
end
|
54
|
+
|
55
|
+
expect(File.exist?('ssl/not-there.example.com.pem')).to eq(false)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'fails with InstallError if output directory does not exist' do
|
59
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
60
|
+
pem_directory: 'not-there')
|
61
|
+
expect do
|
62
|
+
installer.install('some.example.com')
|
63
|
+
end.to raise_error(InstallError)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'fails if domain contains forbidden characters' do
|
67
|
+
installer = PemDirectoryInstaller.new(input_directory: 'live',
|
68
|
+
pem_directory: 'ssl')
|
69
|
+
|
70
|
+
expect do
|
71
|
+
installer.install('some.*example ".com')
|
72
|
+
end.to raise_error(Sanitize::ForbiddenCharacters)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module CertWatch
|
4
|
+
RSpec.describe Sanitize do
|
5
|
+
describe '.check_domain!' do
|
6
|
+
it 'fails if string contains forbidden characters' do
|
7
|
+
expect do
|
8
|
+
Sanitize.check_domain!('some.12-; rm *"')
|
9
|
+
end.to raise_error(Sanitize::ForbiddenCharacters)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'handles multi line strings' do
|
13
|
+
expect do
|
14
|
+
Sanitize.check_domain!("valid\n rm *")
|
15
|
+
end.to raise_error(Sanitize::ForbiddenCharacters)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module CertWatch
|
4
|
+
RSpec.describe Shell, fixture_files: true do
|
5
|
+
describe '.sudo' do
|
6
|
+
it 'runs given command' do
|
7
|
+
Shell.sudo('echo "test" > foo')
|
8
|
+
|
9
|
+
expect(File.read('foo')).to eq("test\n")
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'raises CommandFailed with output if command fails' do
|
13
|
+
expect do
|
14
|
+
Shell.sudo('LANG=en touch not/there')
|
15
|
+
end.to raise_error(Shell::CommandFailed, /cannot touch/)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module CertWatch
|
4
|
+
module Views
|
5
|
+
RSpec.describe CertificateState, type: :view_component do
|
6
|
+
context 'with string argument' do
|
7
|
+
it 'renders status tag for matching certificate' do
|
8
|
+
create(:certificate, state: 'installed', domain: 'my.example.com')
|
9
|
+
|
10
|
+
render(:cert_watch_certificate_state, 'my.example.com')
|
11
|
+
|
12
|
+
expect(rendered).to have_selector('.status_tag.installed')
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'renders status tag if certificate is not found' do
|
16
|
+
render(:cert_watch_certificate_state, 'not.there.com')
|
17
|
+
|
18
|
+
expect(rendered).to have_selector('.status_tag.not_found')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'with certificate argument' do
|
23
|
+
it 'renders status tag for certificate' do
|
24
|
+
certificate = create(:certificate, state: 'installed', domain: 'my.example.com')
|
25
|
+
|
26
|
+
render(:cert_watch_certificate_state, certificate)
|
27
|
+
|
28
|
+
expect(rendered).to have_selector('.status_tag.installed')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/examples.txt
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
------------------------------------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/cert_watch/certbot_client_spec.rb[1:1] | passed | 0.00682 seconds |
|
4
|
+
./spec/cert_watch/certbot_client_spec.rb[1:2] | passed | 0.00204 seconds |
|
5
|
+
./spec/cert_watch/certbot_client_spec.rb[1:3] | passed | 0.00203 seconds |
|
6
|
+
./spec/cert_watch/certbot_client_spec.rb[1:4] | passed | 0.00201 seconds |
|
7
|
+
./spec/cert_watch/certbot_client_spec.rb[1:5] | passed | 0.00223 seconds |
|
8
|
+
./spec/cert_watch/certbot_client_spec.rb[1:6] | passed | 0.0029 seconds |
|
9
|
+
./spec/cert_watch/domain_owner_spec.rb[1:1] | passed | 0.04831 seconds |
|
10
|
+
./spec/cert_watch/domain_owner_spec.rb[1:2] | passed | 0.07414 seconds |
|
11
|
+
./spec/cert_watch/domain_owner_spec.rb[1:3] | passed | 0.06987 seconds |
|
12
|
+
./spec/cert_watch/domain_owner_spec.rb[1:4] | passed | 0.08508 seconds |
|
13
|
+
./spec/cert_watch/domain_owner_spec.rb[1:5] | passed | 0.08109 seconds |
|
14
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:1] | passed | 0.03524 seconds |
|
15
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:2] | passed | 0.05051 seconds |
|
16
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:3] | passed | 0.03625 seconds |
|
17
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:4] | passed | 0.01778 seconds |
|
18
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:5] | passed | 0.02905 seconds |
|
19
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:6] | passed | 0.02549 seconds |
|
20
|
+
./spec/cert_watch/pem_directory_installer_spec.rb[1:7] | passed | 0.00437 seconds |
|
21
|
+
./spec/cert_watch/sanitize_spec.rb[1:1:1] | passed | 0.00192 seconds |
|
22
|
+
./spec/cert_watch/sanitize_spec.rb[1:1:2] | passed | 0.00219 seconds |
|
23
|
+
./spec/cert_watch/shell_spec.rb[1:1:1] | passed | 0.01215 seconds |
|
24
|
+
./spec/cert_watch/shell_spec.rb[1:1:2] | passed | 0.01372 seconds |
|
25
|
+
./spec/cert_watch/views/certificate_state_spec.rb[1:1:1] | passed | 0.0096 seconds |
|
26
|
+
./spec/cert_watch/views/certificate_state_spec.rb[1:1:2] | passed | 0.00646 seconds |
|
27
|
+
./spec/cert_watch/views/certificate_state_spec.rb[1:2:1] | passed | 0.03833 seconds |
|
28
|
+
./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:1] | passed | 0.01287 seconds |
|
29
|
+
./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:2] | passed | 0.0086 seconds |
|
30
|
+
./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:3] | passed | 0.0154 seconds |
|
31
|
+
./spec/jobs/cert_watch/renew_expiring_certificates_job_spec.rb[1:4] | passed | 0.01655 seconds |
|
32
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:1] | passed | 0.01814 seconds |
|
33
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:2] | passed | 0.03732 seconds |
|
34
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:3] | passed | 0.01962 seconds |
|
35
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:4] | passed | 0.02037 seconds |
|
36
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:5] | passed | 0.01938 seconds |
|
37
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:6:1] | passed | 0.01556 seconds |
|
38
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:6:2] | passed | 0.01882 seconds |
|
39
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:7:1] | passed | 0.02004 seconds |
|
40
|
+
./spec/models/cert_watch/certificate_spec.rb[1:1:7:2] | passed | 0.02153 seconds |
|
Binary file
|