21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-04-27 22:40:49 +02:00

Merge pull request #35 from Evolix/checks-ssl

Checks SSL
This commit is contained in:
Colin Darie 2018-07-03 10:22:09 +02:00 committed by GitHub
commit 60eb4cb16c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1163 additions and 81 deletions

View file

@ -1,5 +1,66 @@
# chexpire [![Build Status](https://travis-ci.org/Evolix/chexpire.svg?branch=master)](https://travis-ci.org/Evolix/chexpire) # Chexpire [![Build Status](https://travis-ci.org/Evolix/chexpire.svg?branch=master)](https://travis-ci.org/Evolix/chexpire)
A web application to help check for domain or SSL/TLS certificate expirations. A web application to help check for domain or SSL/TLS certificate expirations.
![Shakespeare quote: « An SSL error has occured and a secure connection to the server cannot be made. »](app/assets/images/shakespeare_quote_ssl.png) ![Shakespeare quote: « An SSL error has occured and a secure connection to the server cannot be made. »](app/assets/images/shakespeare_quote_ssl.png)
## How-To Add a new check kind
TL;DR:
- write a job for executing and updating a check instance of your kind and use `CheckLogger` to log important event during a check execution.
- Write a `CheckProcessor` class to perform theses jobs at regular interval.
- For each notification channel, implement the code that notify the user when the date will expires soon or when there are recurrent failures.
Start by appending a new kind into the `Check` model at the `enum :kind` line.
### Write a job and the check execution
The job takes a `Check` instance as an argument and is responsible for performing the check and updating the following dates fields for the check instance:
- mandatory: `last_run_at`, immediately when the execution is starting
- mandatory: `domain_expires_at`
- mandatory: `last_success_at`, when no problems have occured during the verification and fields have been updated
- optional: `domain_created_at`, `domain_updated_at`, when the kind of check allows them
The code architecture is up to you and can vary depending on the check, but most of time you should write a service which will execute something and parse the response. Then you'll return a simple response object which responds to methods used for updating check fields in the job.
Look at the SSL and Domain implementations for examples.
### CheckLogger
This is not currently required, but you should use the `CheckLogger` class to emit log events at important steps of a check execution (especially when error occurs): this can help in case of problems. A single `CheckLogger` instance is attached to a check execution and fill a `CheckLog` model instance with various contents. Looks into the code to see supported events.
### CheckProcessor
A `processor` is responsible for executing checks at regular interval, selectively or not, depending of your needs.
Basically, it performs your job for all relevant checks.
Create a processor dedicated for your check kind. The class should include the `CheckProcessor` module and respond to the following methods :
- `configuration_key`: returns the key name of configuration in chexpire configuration file. For instance: `checks_domain`
- `scope`: returns the checks scope used by each resolver. Generally, this should be the base scope defined into the `CheckProcessor` module, filtered by your check kind (ie: `base_scope.your_kind`)
- `resolvers`: returns an array of methods which returns checks to execute. Allows conditional checks depending of various checks criterias such as far known expiry date, failing checks etc…
Looks into the `CheckProcessor` for available resolvers methods or write your own. use `resolve_all` resolver if you want execute all of your check at each execution.
- `process`: must execute or enqueue your job for a given check as argument. Note: because the processor is called into a task, and to take profit of a interval configuration parameter, it's better to execute the job synchronously in this method.
### Configuration
Each check kind can have configuration. Looks for other checks configuration for examples, such as `checks_domain` configuration in `config/chexpire.example.yml` file.
### Schedule a task
Write a task in `lib/tasks/checks.rake` under the namespaces `checks:sync_dates` and invoke the `sync_dates` method of your processor. Schedule to run it periodically in `config/schedule.rb`, or list the task in the `all` alias which is the default processor scheduler.
### Notifier
Finally, you have to write the way the checks will be notified to theirs users. For each notifier channel (email, …) you need to write two notifications :
- expires_soon
- recurrent_failures
First, add your checks kinds and these notifications definitions in the base class for notifier: `app/services/notifier/base.rb` : in the notify method, for your new check kind, a specific method will be called in each notifier. For example, in the email channel, a specific mailer action is called for each couple (check kin, notification kind).
Then, in each notifier class, implements the details of this method. If you want to ignore a notification for a given channel, simply write the method and do nothing.

43
app/jobs/ssl_sync_job.rb Normal file
View file

@ -0,0 +1,43 @@
class SSLSyncJob < ApplicationJob
queue_as :default
rescue_from StandardError do |exception|
check_logger.log(:standard_error, exception) if check.present?
raise # rubocop:disable Style/SignalException
end
rescue_from ActiveRecord::RecordNotFound do; end
# parser error are already logged
rescue_from SSL::Error do; end
attr_reader :check
def perform(check_id)
prepare_check(check_id)
response = SSL.ask(check.domain, logger: check_logger)
return unless response.valid?
update_from_response(response)
end
def update_from_response(response)
check.domain_expires_at = response.expire_at
check.last_success_at = Time.now
check.save!
end
private
def prepare_check(check_id)
@check = Check.find(check_id)
check.update_attribute(:last_run_at, Time.now)
end
# logger is a reserved ActiveJob method
def check_logger
@check_logger ||= CheckLogger.new(check)
end
end

View file

@ -19,4 +19,16 @@ class NotificationsMailer < ApplicationMailer
subject = t(".subject", domain: @check.domain) subject = t(".subject", domain: @check.domain)
mail subject: subject mail subject: subject
end end
def ssl_expires_soon
@expire_in_days = Integer(@check.domain_expires_at.to_date - Date.today)
subject = t(".subject", domain: @check.domain, count: @expire_in_days)
mail subject: subject
end
def ssl_recurrent_failures
subject = t(".subject", domain: @check.domain)
mail subject: subject
end
end end

View file

@ -0,0 +1,26 @@
class CheckDomainProcessor
include CheckProcessor
protected
def configuration_key
"checks_domain"
end
def resolvers
%i[
resolve_last_run_failed
resolve_expire_short_term
resolve_expire_long_term
resolve_unknown_expiry
]
end
def scope
base_scope.domain
end
def process(check)
WhoisSyncJob.perform_now(check.id)
end
end

View file

@ -1,17 +1,12 @@
class CheckProcessor module CheckProcessor
attr_reader :configuration attr_reader :configuration
def initialize(configuration = nil) def initialize(configuration = nil)
@configuration = configuration || default_configuration @configuration = configuration || default_configuration
end end
def sync_dates # rubocop:disable Metrics/MethodLength def sync_dates
%i[ resolvers.each do |resolver|
resolve_last_run_failed
resolve_expire_short_term
resolve_expire_long_term
resolve_unknown_expiry
].each do |resolver|
public_send(resolver).find_each(batch_size: 100).each do |check| public_send(resolver).find_each(batch_size: 100).each do |check|
process(check) process(check)
@ -20,6 +15,12 @@ class CheckProcessor
end end
end end
# :nocov:
def resolvers
fail NotImplementedError, "#{self.class.name} did not implemented method #{__callee__}"
end
# :nocov:
def resolve_last_run_failed def resolve_last_run_failed
scope.last_run_failed scope.last_run_failed
end end
@ -41,25 +42,36 @@ class CheckProcessor
scope.where("domain_expires_at IS NULL") scope.where("domain_expires_at IS NULL")
end end
private def resolve_all
scope
end
def scope protected
def base_scope
Check Check
.active .active
.where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)") .where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)")
end end
def process(check) # :nocov:
case check.kind.to_sym def scope
when :domain fail NotImplementedError, "#{self.class.name} did not implemented method #{__callee__}"
WhoisSyncJob.perform_now(check.id)
else
fail ArgumentError, "Unsupported check kind `#{check.kind}`"
end
end end
def process(_check)
fail NotImplementedError, "#{self.class.name} did not implemented method #{__callee__}"
end
def configuration_key
fail NotImplementedError, "#{self.class.name} did not implemented method #{__callee__}"
end
# :nocov:
private
def default_configuration def default_configuration
config = Rails.configuration.chexpire.fetch("checks", {}) config = Rails.configuration.chexpire.fetch(configuration_key, {})
OpenStruct.new( OpenStruct.new(
interval: config.fetch("interval") { 0.00 }, interval: config.fetch("interval") { 0.00 },

View file

@ -0,0 +1,23 @@
class CheckSSLProcessor
include CheckProcessor
protected
def configuration_key
"checks_ssl"
end
def resolvers
%i[
resolve_all
]
end
def scope
base_scope.ssl
end
def process(check)
SSLSyncJob.perform_now(check.id)
end
end

View file

@ -11,6 +11,10 @@ module Notifier
domain_notify_expires_soon(notification) domain_notify_expires_soon(notification)
when [:domain, :recurrent_failures] when [:domain, :recurrent_failures]
domain_notify_recurrent_failures(notification) domain_notify_recurrent_failures(notification)
when [:ssl, :expires_soon]
ssl_notify_expires_soon(notification)
when [:ssl, :recurrent_failures]
ssl_notify_recurrent_failures(notification)
else else
fail ArgumentError, fail ArgumentError,
"Invalid notification reason `#{reason}` for check kind `#{notification.check.kind}`." "Invalid notification reason `#{reason}` for check kind `#{notification.check.kind}`."
@ -25,15 +29,16 @@ module Notifier
"#{self.class.name} channel did not implemented method #{__callee__}" "#{self.class.name} channel did not implemented method #{__callee__}"
end end
# domain notifications %i[
def domain_notify_expires_soon(_notification) domain_notify_expires_soon
fail NotImplementedError, domain_notify_recurrent_failures
"Channel #{self.class.name} does not implement #{__callee__}" ssl_notify_expires_soon
end ssl_notify_recurrent_failures
].each do |method|
def domain_notify_recurrent_failures(_notification) define_method(method) do
fail NotImplementedError, fail NotImplementedError,
"Channel #{self.class.name} does not implement #{__callee__}" "Channel #{self.class.name} does not implement method #{method}."
end
end end
# :nocov: # :nocov:
end end

View file

@ -16,6 +16,14 @@ module Notifier
def domain_notify_recurrent_failures(notification) def domain_notify_recurrent_failures(notification)
NotificationsMailer.with(notification: notification).domain_recurrent_failures.deliver_now NotificationsMailer.with(notification: notification).domain_recurrent_failures.deliver_now
end end
def ssl_notify_expires_soon(notification)
NotificationsMailer.with(notification: notification).ssl_expires_soon.deliver_now
end
def ssl_notify_recurrent_failures(notification)
NotificationsMailer.with(notification: notification).ssl_recurrent_failures.deliver_now
end
end end
end end
end end

66
app/services/ssl.rb Normal file
View file

@ -0,0 +1,66 @@
require "null_logger"
require "system_command"
require_relative "ssl/parser"
require_relative "ssl/response"
require_relative "ssl/errors"
module SSL
class << self
def ask(domain, system_klass: SystemCommand, logger: NullLogger.new)
Service.new(domain, system_klass: system_klass, logger: logger).call
end
end
class Service
attr_reader :domain
attr_reader :logger
attr_reader :system_klass
attr_reader :configuration
def initialize(domain, system_klass: SystemCommand, configuration: nil, logger: NullLogger.new)
@domain = domain
@logger = logger
@system_klass = system_klass
@configuration = configuration || default_configuration
end
def call
result = run_command
parse(result)
rescue StandardError => ex
logger.log :service_error, ex
raise
end
def run_command
command = system_klass.new(check_http_path, check_http_args, logger: logger)
result = command.execute
unless result.exit_status.zero?
fail SSLCommandError, "SSL command failed with status #{result.exit_status}"
end
result
end
def parse(result)
parser = Parser.new(domain, logger: logger)
parser.parse(result.stdout)
end
def check_http_path
configuration.check_http_path.presence || "check_http"
end
def check_http_args
[
configuration.check_http_args.presence,
"-H '#{domain}'",
].compact
end
def default_configuration
OpenStruct.new(Rails.configuration.chexpire.fetch("checks_ssl") { {} })
end
end
end

View file

@ -0,0 +1,10 @@
module SSL
class Error < StandardError; end
class SSLCommandError < Error; end
class ParserError < Error; end
class DomainNotMatchError < ParserError; end
class InvalidResponseError < ParserError; end
class InvalidDateError < ParserError; end
end

View file

@ -0,0 +1,52 @@
require "null_logger"
require "ssl/errors"
module SSL
class Parser
DATE_REGEX = /will expire on (.+)\./
# Several date formats possible:
# OK - Certificate 'domain.net' will expire on Sat 10 Jun 2028 09:14:18 AM GMT +0000.
# OK - Certificate 'domain.net' will expire on 2018-08-06 02:57 +0200/CEST.
attr_reader :logger
attr_reader :domain
def initialize(domain, logger: NullLogger.new)
@logger = logger
@domain = domain
end
def parse(raw)
fail DomainNotMatchError unless match_domain?(raw)
match = raw.match(DATE_REGEX)
fail InvalidResponseError unless match.present?
response = build_response(match)
logger.log :parsed_response, response
response
rescue ParserError => ex
logger.log :parser_error, ex
raise
end
def match_domain?(raw)
raw.match(/\b#{domain}\b/).present?
end
def build_response(match)
Response.new(domain).tap do |response|
response.expire_at = parse_datetime(match[1])
end
end
def parse_datetime(date_str)
Time.parse(date_str).utc
rescue StandardError => ex
raise InvalidDateError, ex.message
end
end
end

View file

@ -0,0 +1,13 @@
module SSL
class Response
attr_accessor :expire_at
def initialize(domain)
@domain = domain
end
def valid?
expire_at.present?
end
end
end

View file

@ -3,7 +3,7 @@
-- --
<p> <p>
Vous avez reçu ce courriel à <%= pluralize(delay, "jour", "jours") %> avant la date d'expiration.</br> Vous avez reçu ce courriel à <%= pluralize(interval, "jour", "jours") %> avant la date d'expiration.</br>
Vous pouvez gérer les notifications pour cette vérification à ce adresse : Vous pouvez gérer les notifications pour cette vérification à ce adresse :
<%= link_to nil, edit_check_url(check) %> <%= link_to nil, edit_check_url(check) %>
</p> </p>

View file

@ -1,6 +1,6 @@
-- --
Vous avez reçu ce courriel à <%= pluralize(delay, "jour", "jours") %> avant Vous avez reçu ce courriel à <%= pluralize(interval, "jour", "jours") %> avant
la date d'expiration. la date d'expiration.
Vous pouvez gérer les notifications pour cette vérification à ce adresse : Vous pouvez gérer les notifications pour cette vérification à ce adresse :
<%= edit_check_url(check) %> <%= edit_check_url(check) %>

View file

@ -2,7 +2,7 @@
<br /> <br />
-- --
<p>Vous avez reçu ce courriel à <%= pluralize(delay, "jour", "jours") %> avant la dernière date d'expiration connue.<br /> <p>Vous avez reçu ce courriel à <%= pluralize(interval, "jour", "jours") %> avant la dernière date d'expiration connue.<br />
Vous pouvez gérer les notifications pour cette vérification à ce adresse : Vous pouvez gérer les notifications pour cette vérification à ce adresse :
<%= link_to nil, edit_check_url(check) %> <%= link_to nil, edit_check_url(check) %>
</p> </p>

View file

@ -1,6 +1,6 @@
-- --
Vous avez reçu ce courriel à <%= pluralize(delay, "jour", "jours") %> Vous avez reçu ce courriel à <%= pluralize(interval, "jour", "jours") %>
avant la dernière date d'expiration connue. avant la dernière date d'expiration connue.
Vous pouvez gérer les notifications pour cette vérification à ce adresse : Vous pouvez gérer les notifications pour cette vérification à ce adresse :
<%= edit_check_url(check) %> <%= edit_check_url(check) %>

View file

@ -9,4 +9,4 @@
<%= render "check_comment_vendor" %> <%= render "check_comment_vendor" %>
<%= render "footer_expires_soon", delay: @notification.delay, check: @check %> <%= render "footer_expires_soon", interval: @notification.interval, check: @check %>

View file

@ -6,4 +6,4 @@ le domaine <%= @check.domain %> va expirer le <%= format_utc(@check.domain_expir
<%= render "check_comment_vendor" %> <%= render "check_comment_vendor" %>
<%= render "footer_expires_soon", delay: @notification.delay, check: @check %> <%= render "footer_expires_soon", interval: @notification.interval, check: @check %>

View file

@ -8,9 +8,14 @@
</p> </p>
<p> <p>
Our last known expiry date is <%= format_utc(@check.domain_expires_at) %>. <%- if @check.domain_expires_at.present? %>
<br /> Our last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
Our last successful check occured <%= format_utc(@check.last_success_at) %>. <br />
<% end %>
<%- if @check.last_success_at.present? %>
Our last successful check occured <%= format_utc(@check.last_success_at) %>.
<% end %>
</p> </p>
<p> <p>

View file

@ -3,8 +3,12 @@ Hi,
We had recurrent failures while checking the whois database for domain We had recurrent failures while checking the whois database for domain
<%= @check.domain %>. As of today, we can't anymore verify the expiry date. <%= @check.domain %>. As of today, we can't anymore verify the expiry date.
<%- if @check.domain_expires_at.present? %>
The last known expiry date is <%= format_utc(@check.domain_expires_at) %>. The last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
<% end %>
<%- if @check.last_success_at.present? %>
The last successful check occured <%= format_utc(@check.last_success_at) %>. The last successful check occured <%= format_utc(@check.last_success_at) %>.
<% end %>
If you have deleted your domain or have not renewed it, please disable If you have deleted your domain or have not renewed it, please disable
or delete the check by following this link: or delete the check by following this link:

View file

@ -8,9 +8,14 @@
</p> </p>
<p> <p>
La dernière date d'expiration connue est le <%= format_utc(@check.domain_expires_at) %>. <%- if @check.domain_expires_at.present? %>
<br /> La dernière date d'expiration connue est le <%= format_utc(@check.domain_expires_at) %>.
Notre dernière vérification réussie a eu lieu le <%= format_utc(@check.last_success_at) %>. <br />
<% end %>
<%- if @check.last_success_at.present? %>
Notre dernière vérification réussie a eu lieu le <%= format_utc(@check.last_success_at) %>.
<% end %>
</p> </p>
<p> <p>
@ -21,4 +26,4 @@ Si vous avez supprimé le domaine ou ne l'avez pas renouvellé, merci de désact
<%= render "check_comment_vendor" %> <%= render "check_comment_vendor" %>
<%= render "footer_recurrent_failures", delay: @notification.delay, check: @check %> <%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>

View file

@ -4,10 +4,17 @@ Nous avons rencontré de multiples erreurs pendant l'exécution des vérificatio
du domaine <strong><%= @check.domain %></strong>. du domaine <strong><%= @check.domain %></strong>.
Nous ne pouvons plus interroger la base Whois pour vérifier la date d'expiration. Nous ne pouvons plus interroger la base Whois pour vérifier la date d'expiration.
<%- if @check.domain_expires_at.present? %>
La dernière date d'expiration connue est le <%= format_utc(@check.domain_expires_at) %>.
<% end %>
<%- if @check.last_success_at.present? %>
Notre dernière vérification réussie a eu lieu le <%= format_utc(@check.last_success_at) %>.
<% end %>
Si vous avez supprimé le domaine ou ne l'avez pas renouvellé, Si vous avez supprimé le domaine ou ne l'avez pas renouvellé,
merci de désactiver la vérification associée, avec ce lien : merci de désactiver la vérification associée, avec ce lien :
<%= edit_check_url(@check) %> <%= edit_check_url(@check) %>
<%= render "check_comment_vendor" %> <%= render "check_comment_vendor" %>
<%= render "footer_recurrent_failures", delay: @notification.delay, check: @check %> <%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,12 @@
<p>
Hi,
<br />
<br />
the SSL certificate for <strong><%= @check.domain %></strong> will expire
<strong><%= format_utc(@check.domain_expires_at) %></strong>.
</p>
<br />
<%= render "check_comment_vendor" %>
<%= render "footer_expires_soon", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,9 @@
Hi,
the SSL certificate for <%= @check.domain %> will expire <%= format_utc(@check.domain_expires_at) %>.
<%= render "check_comment_vendor" %>
<%= render "footer_expires_soon", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,12 @@
<p>
Salut,
<br />
<br />
le certificat SSL pour <strong><%= @check.domain %></strong> va expirer le
<strong><%= format_utc(@check.domain_expires_at) %></strong>.
</p>
<br />
<%= render "check_comment_vendor" %>
<%= render "footer_expires_soon", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,9 @@
Salut,
le certificat SSL pour <%= @check.domain %> va expirer le <%= format_utc(@check.domain_expires_at) %>.
<%= render "check_comment_vendor" %>
<%= render "footer_expires_soon", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,31 @@
<p>
Hi,
<br />
<br />
We had <strong>recurrent failures</strong> while checking the SSL certificate for
<strong><%= @check.domain %></strong>. As of today, we can no longer verify the certificate
expiry date.
</p>
<p>
<%- if @check.domain_expires_at.present? %>
Our last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
<br />
<%- end %>
<%- if @check.last_success_at.present? %>
Our last successful check occured <%= format_utc(@check.last_success_at) %>.
<%- end %>
</p>
<p>
If there is no more SSL endpoint for this domain, please disable
or delete the check by following this link: <br /><br />
<%= link_to nil, edit_check_url(@check) %>
</p>
<br />
<%= render "check_comment_vendor" %>
<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,22 @@
Hi,
We had recurrent failures while checking the SSL certificate for
<%= @check.domain %>. As of today, we can no longer verify the certificate
expiry date.
<%- if @check.domain_expires_at.present? %>
The last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
<%- end %>
<%- if @check.last_success_at.present? %>
The last successful check occured <%= format_utc(@check.last_success_at) %>.
<%- end %>
If there is no more SSL endpoint for this domain, please disable
or delete the check by following this link:
<%= edit_check_url(@check) %>
<%= render "check_comment_vendor" %>
<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,35 @@
<p>
Salut,
<br />
<br />
Nous avons rencontré de <strong>multiples erreurs</strong>
pendant l'exécution des vérifications du certificat SSL
du site <strong><%= @check.domain %></strong>.
Nous ne pouvons plus vérifier la date d'expiration du certificat
en nous connectant au site.
</p>
<p>
<%- if @check.domain_expires_at.present? %>
La dernière date d'expiration connue est le <%= format_utc(@check.domain_expires_at) %>.
<br />
<% end %>
<%- if @check.last_success_at.present? %>
Notre dernière vérification réussie a eu lieu le <%= format_utc(@check.last_success_at) %>.
<% end %>
</p>
<p>
S'il n'y a plus de terminaison SSL pour ce site ou s'il n'existe plus,
merci de désactiver la vérification associée, avec ce lien :
<br />
<br />
<%= link_to nil, edit_check_url(@check) %>
</p>
<br />
<%= render "check_comment_vendor" %>
<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>

View file

@ -0,0 +1,22 @@
Salut,
Nous avons rencontré de multiples erreurs pendant l'exécution des vérifications
du certificat SSL pour le site <strong><%= @check.domain %></strong>.
Nous ne pouvons plus vérifier la date d'expiration du certificat
en nous connectant au site.
<%- if @check.domain_expires_at.present? %>
La dernière date d'expiration connue est le <%= format_utc(@check.domain_expires_at) %>.
<% end %>
<%- if @check.last_success_at.present? %>
Notre dernière vérification réussie a eu lieu le <%= format_utc(@check.last_success_at) %>.
<% end %>
S'il n'y a plus de terminaison SSL pour ce site ou s'il n'existe plus,
merci de désactiver la vérification associée, avec ce lien :
<%= edit_check_url(@check) %>
<%= render "check_comment_vendor" %>
<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>

View file

@ -3,10 +3,13 @@ default: &default
notifier: notifier:
interval: 0.00 interval: 0.00
failure_days: 3 failure_days: 3
checks: checks_domain:
interval: 0.5 interval: 0.5
long_term: 60 long_term: 60
long_term_frequency: 10 long_term_frequency: 10
checks_ssl:
check_http_path: ""
check_http_args: ""
development: development:
<<: *default <<: *default

View file

@ -4,7 +4,10 @@ test:
notifier: notifier:
interval: 0.00 interval: 0.00
failure_days: 3 failure_days: 3
checks: checks_domain:
interval: 0.00 interval: 0.00
long_term: 60 long_term: 60
long_term_frequency: 10 long_term_frequency: 10
checks_ssl:
check_http_path: ""
check_http_args: ""

View file

@ -38,6 +38,15 @@ en:
domain_recurrent_failures: domain_recurrent_failures:
subject: "Recurrent failures in %{domain} domain expiry check" subject: "Recurrent failures in %{domain} domain expiry check"
ssl_expires_soon:
subject:
zero: "SSL certificate for %{domain} expires TODAY!"
one: "SSL certificate for %{domain} expires TOMORROW!"
other: "SSL certificate for %{domain} expires in %{count} days"
ssl_recurrent_failures:
subject: "Recurrent failures in %{domain} SSL certificate expiry check"
shared: shared:
locales: locales:

View file

@ -12,6 +12,14 @@ fr:
check: check:
past: "ne peut être dans le futur" past: "ne peut être dans le futur"
time:
am: am
formats:
default: "%a %d %b %Y %H:%M:%S %z"
long: "%A %d %B %Y %H:%M"
short: "%d %b %H:%M"
pm: pm
devise: devise:
registrations: registrations:
new: new:
@ -36,7 +44,16 @@ fr:
other: "Le domaine %{domain} expire dans %{count} jours" other: "Le domaine %{domain} expire dans %{count} jours"
domain_recurrent_failures: domain_recurrent_failures:
subject: "Multiples erreur dans la vérification d'expiration du domaine %{domain}" subject: "Erreurs dans la vérification d'expiration du domaine %{domain}"
ssl_expires_soon:
subject:
zero: "Le certificat SSL pour %{domain} expire AUJOURD'HUI !"
one: "Le certificat SSL pour %{domain} expire DEMAIN !"
other: "Le certificat SSL pour %{domain} expire dans %{count} jours"
ssl_recurrent_failures:
subject: "Erreurs dans la vérification d'expiration du certificat SSL %{domain}"
shared: shared:

View file

@ -19,7 +19,7 @@ set :output, standard: "log/cron.log"
# Learn more: http://github.com/javan/whenever # Learn more: http://github.com/javan/whenever
every 1.day, at: '4:30 am', roles: [:app] do every 1.day, at: '4:30 am', roles: [:app] do
rake "checks:sync_dates" rake "checks:sync_dates:all"
end end
every 1.day, at: '10:30 am', roles: [:app] do every 1.day, at: '10:30 am', roles: [:app] do

View file

@ -1,3 +1,4 @@
CheckLog.destroy_all
Notification.destroy_all Notification.destroy_all
Check.destroy_all Check.destroy_all
User.destroy_all User.destroy_all
@ -33,6 +34,31 @@ check_chexpire_org_error = Check.create!(
last_success_at: 4.days.ago, last_success_at: 4.days.ago,
) )
ssl_check_chexpire_org = Check.create!(
user: user1,
kind: :ssl,
domain: "www.chexpire.org",
domain_expires_at: 1.week.from_now,
domain_updated_at: 6.months.ago,
domain_created_at: Time.new(2016, 8, 4, 12, 15, 1),
comment: "The date are fake, this is a seed !",
vendor: "Some random registrar",
)
ssl_check_chexpire_org_error = Check.create!(
user: user1,
kind: :ssl,
domain: "chexpire.org",
domain_expires_at: 1.week.from_now,
domain_updated_at: 6.months.ago,
domain_created_at: Time.new(2016, 8, 4, 12, 15, 1),
comment: "The date are fake, this is a seed !",
vendor: "Some random registrar",
last_run_at: 20.minutes.ago,
last_success_at: 4.days.ago,
)
Notification.create!( Notification.create!(
check: check_chexpire_org, check: check_chexpire_org,
interval: 15, interval: 15,
@ -49,6 +75,22 @@ Notification.create!(
status: :pending, status: :pending,
) )
Notification.create!(
check: ssl_check_chexpire_org,
interval: 15,
channel: :email,
recipient: "colin@example.org",
status: :pending,
)
Notification.create!(
check: ssl_check_chexpire_org_error,
interval: 15,
channel: :email,
recipient: "colin@example.org",
status: :pending,
)
puts "\e[0;32mDone 👌\e[0m" puts "\e[0;32mDone 👌\e[0m"
puts " " puts " "
puts "--------------------" puts "--------------------"

View file

@ -1,7 +1,17 @@
namespace :checks do namespace :checks do
desc "Refresh expiry dates for checks" namespace :sync_dates do
task sync_dates: :environment do task all: [:domain, :ssl]
process = CheckProcessor.new
process.sync_dates desc "Refresh domains expiry dates"
task domain: :environment do
process = CheckDomainProcessor.new
process.sync_dates
end
desc "Refresh SSL expiry dates"
task domain: :environment do
process = CheckSSLProcessor.new
process.sync_dates
end
end end
end end

View file

@ -44,6 +44,10 @@ FactoryBot.define do
kind :domain kind :domain
end end
trait :ssl do
kind :ssl
end
trait :nil_dates do trait :nil_dates do
domain_created_at nil domain_created_at nil
domain_updated_at nil domain_updated_at nil

View file

@ -0,0 +1 @@
OK - Certificate 'ssl0.domain.org' will expire on Sat 10 Jun 2028 09:14:18 AM GMT +0000.

View file

@ -0,0 +1 @@
SSL OK - Certificate 'ssl1.domain.org' will expire on 2022-08-06 02:57 +0200/CEST.

View file

@ -0,0 +1 @@
check_http: Invalid hostname/address - ssl100.invalid.org

View file

@ -0,0 +1 @@
SSL OK - Certificate 'ssl101.invalid.org' will expire on unknown date.

View file

@ -0,0 +1,68 @@
require "test_helper"
class SSLSyncJobTest < ActiveJob::TestCase
test "calls whois database and update check with the response (domain.fr)" do
domain = "ssl0.domain.org"
check = create(:check, :nil_dates, domain: domain)
mock_system_command("check_http", expected_command_arg(domain), stdout: ssl_response(domain)) do
SSLSyncJob.perform_now(check.id)
end
check.reload
assert_just_now check.last_run_at
assert_just_now check.last_success_at
assert_equal Time.new(2028, 6, 10, 9, 14, 18, 0), check.domain_expires_at
assert_nil check.domain_updated_at
assert_nil check.domain_created_at
assert check.active?
end
test "ignore invalid response" do
domain = "domain.fr"
check = create(:check, :nil_dates, domain: domain)
original_updated_at = check.updated_at
mock_system_command("check_http", expected_command_arg(domain), stdout: "not a response") do
SSLSyncJob.perform_now(check.id)
end
check.reload
assert_just_now check.last_run_at
assert_nil check.last_success_at
assert_equal original_updated_at, check.updated_at
assert check.active?
end
test "should ignore not found (removed) checks" do
assert_nothing_raised do
SSLSyncJob.perform_now("9999999")
end
end
test "should log and re-raise StandardError" do
check = create(:check)
assert_raise StandardError do
SSL.stub :ask, nil do
SSLSyncJob.perform_now(check.id)
end
end
assert_equal 1, check.logs.count
assert_match(/undefined method \W+valid\?/, check.logs.last.error)
assert check.logs.last.failed?
end
private
def ssl_response(domain)
file_fixture("ssl/#{domain}.txt").read
end
def expected_command_arg(domain)
["-H '#{domain}'"]
end
end

View file

@ -1,6 +1,6 @@
require "test_helper" require "test_helper"
class NotificationsMailerTest < ActionMailer::TestCase class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics/ClassLength
test "domain_expires_soon" do test "domain_expires_soon" do
check = create(:check, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00")) check = create(:check, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org") notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
@ -17,7 +17,7 @@ class NotificationsMailerTest < ActionMailer::TestCase
assert_equal ["colin@example.org"], mail.to assert_equal ["colin@example.org"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.body.to_s, mail.html_part.to_s] parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part| parts.each do |part|
assert_match "domain.fr", part assert_match "domain.fr", part
@ -30,6 +30,37 @@ class NotificationsMailerTest < ActionMailer::TestCase
end end
end end
test "domain_expires_soon FR" do
check = create(:check, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
I18n.with_locale :fr do
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
assert_emails 1 do
mail.deliver_now
end
assert_match "domain.fr", mail.subject
assert_match "dans 8 jours", mail.subject
assert_equal ["colin@example.org"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "domain.fr", part
assert_match "dim 10 juin 2018 10:00:05 +0000", part
assert_match "10 jours", part
assert_match "/checks/#{check.id}/edit", part
assert_no_match "commentaire", part
assert_no_match "fournisseur", part
end
end
end
end
test "domain_expires_soon include comment & vendor" do test "domain_expires_soon include comment & vendor" do
check = create(:check, check = create(:check,
domain_expires_at: 1.week.from_now, domain_expires_at: 1.week.from_now,
@ -39,7 +70,7 @@ class NotificationsMailerTest < ActionMailer::TestCase
mail = NotificationsMailer.with(notification: notification).domain_expires_soon mail = NotificationsMailer.with(notification: notification).domain_expires_soon
parts = [mail.text_part.body.to_s, mail.html_part.to_s] parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part| parts.each do |part|
assert_match "My comment", part assert_match "My comment", part
@ -47,6 +78,25 @@ class NotificationsMailerTest < ActionMailer::TestCase
end end
end end
test "domain_expires_soon include comment & vendor - FR" do
check = create(:check,
domain_expires_at: 1.week.from_now,
comment: "My comment",
vendor: "The vendor")
notification = build(:notification, check: check)
I18n.with_locale :fr do
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "commentaire", part
assert_match "Fournisseur", part
end
end
end
test "domain_recurrent_failures" do test "domain_recurrent_failures" do
last_success_at = Time.new(2018, 5, 30, 6, 10, 0, "+00:00") last_success_at = Time.new(2018, 5, 30, 6, 10, 0, "+00:00")
domain_expires_at = Time.new(2018, 10, 10, 7, 20, 0, "+04:00") domain_expires_at = Time.new(2018, 10, 10, 7, 20, 0, "+04:00")
@ -64,7 +114,7 @@ class NotificationsMailerTest < ActionMailer::TestCase
assert_equal ["recipient@domain.fr"], mail.to assert_equal ["recipient@domain.fr"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.body.to_s, mail.html_part.to_s] parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part| parts.each do |part|
assert_match "invalid-domain.fr", part assert_match "invalid-domain.fr", part
@ -75,4 +125,159 @@ class NotificationsMailerTest < ActionMailer::TestCase
assert_match "/checks/#{check.id}/edit", part assert_match "/checks/#{check.id}/edit", part
end end
end end
test "domain_recurrent_failures - FR" do
last_success_at = Time.new(2018, 5, 30, 6, 10, 0, "+00:00")
domain_expires_at = Time.new(2018, 10, 10, 7, 20, 0, "+04:00")
check = build(:check, :last_runs_failed,
domain: "invalid-domain.fr",
last_success_at: last_success_at,
domain_expires_at: domain_expires_at,
comment: "My comment")
notification = create(:notification, check: check)
I18n.with_locale :fr do
mail = NotificationsMailer.with(notification: notification).domain_recurrent_failures
assert_match "Erreurs", mail.subject
assert_match "invalid-domain.fr", mail.subject
assert_equal ["recipient@domain.fr"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "invalid-domain.fr", part
assert_match "erreurs", part
assert_match(/réussie[a-z ]+ mer 30 mai 2018 06:10:00 \+0000/, part)
assert_match(/expiration[a-z ]+ mer 10 oct. 2018 03:20:00 \+0000/, part)
assert_match "commentaire", part
assert_match "/checks/#{check.id}/edit", part
end
end
end
test "ssl_expires_soon" do
check = create(:check, :ssl, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).ssl_expires_soon
assert_emails 1 do
mail.deliver_now
end
assert_match "domain.fr", mail.subject
assert_match "SSL", mail.subject
assert_match "in 8 days", mail.subject
assert_equal ["colin@example.org"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "domain.fr", part
assert_match "Sun, 10 Jun 2018 10:00:05 +0000", part
assert_match "10 days", part
assert_match "/checks/#{check.id}/edit", part
assert_no_match "comment", part
assert_no_match "vendor", part
end
end
end
test "ssl_expires_soon - FR" do
check = create(:check, :ssl, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
I18n.with_locale :fr do
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).ssl_expires_soon
assert_emails 1 do
mail.deliver_now
end
assert_match "domain.fr", mail.subject
assert_match "SSL", mail.subject
assert_match "dans 8 jours", mail.subject
assert_equal ["colin@example.org"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "domain.fr", part
assert_match "dim 10 juin 2018 10:00:05 +0000", part
assert_match "10 jours", part
assert_match "/checks/#{check.id}/edit", part
assert_no_match "commentaire", part
assert_no_match "fournisseur", part
end
end
end
end
test "ssl_recurrent_failures" do
last_success_at = Time.new(2018, 5, 30, 6, 10, 0, "+00:00")
domain_expires_at = Time.new(2018, 10, 10, 7, 20, 0, "+04:00")
check = build(:check, :ssl, :last_runs_failed,
domain: "invalid-domain.fr",
last_success_at: last_success_at,
domain_expires_at: domain_expires_at,
comment: "My comment")
notification = create(:notification, check: check)
mail = NotificationsMailer.with(notification: notification).ssl_recurrent_failures
assert_match "failures", mail.subject
assert_match "invalid-domain.fr", mail.subject
assert_match "SSL", mail.subject
assert_equal ["recipient@domain.fr"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "invalid-domain.fr", part
assert_match "recurrent failures", part
assert_match(/success[a-z ]+ Wed, 30 May 2018 06:10:00 \+0000/, part)
assert_match(/expiry[a-z ]+ Wed, 10 Oct 2018 03:20:00 \+0000/, part)
assert_match "My comment", part
assert_match "/checks/#{check.id}/edit", part
end
end
test "ssl_recurrent_failures - FR" do
last_success_at = Time.new(2018, 5, 30, 6, 10, 0, "+00:00")
domain_expires_at = Time.new(2018, 10, 10, 7, 20, 0, "+04:00")
check = build(:check, :ssl, :last_runs_failed,
domain: "invalid-domain.fr",
last_success_at: last_success_at,
domain_expires_at: domain_expires_at,
comment: "My comment")
notification = create(:notification, check: check)
I18n.with_locale :fr do
mail = NotificationsMailer.with(notification: notification).ssl_recurrent_failures
assert_match "Erreurs", mail.subject
assert_match "invalid-domain.fr", mail.subject
assert_match "SSL", mail.subject
assert_equal ["recipient@domain.fr"], mail.to
assert_equal [Rails.configuration.chexpire.fetch("mailer_default_from")], mail.from
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
parts.each do |part|
assert_match "invalid-domain.fr", part
assert_match "erreurs", part
assert_match(/réussie[a-z ]+ mer 30 mai 2018 06:10:00 \+0000/, part)
assert_match(/expiration[a-z ]+ mer 10 oct. 2018 03:20:00 \+0000/, part)
assert_match "commentaire", part
assert_match "/checks/#{check.id}/edit", part
end
end
end
end end

View file

@ -2,12 +2,25 @@
class NotificationsMailerPreview < ActionMailer::Preview class NotificationsMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/domain_expires_soon # Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/domain_expires_soon
def domain_expires_soon def domain_expires_soon
NotificationsMailer.with(notification: Notification.first).domain_expires_soon check = Check.domain.first
NotificationsMailer.with(notification: check.notifications.first).domain_expires_soon
end end
# Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/domain_recurrent_failures # Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/domain_recurrent_failures
def domain_recurrent_failures def domain_recurrent_failures
check = Check.where("last_run_at != last_success_at").limit(1).first check = Check.domain.where("last_run_at != last_success_at").first
NotificationsMailer.with(notification: check.notifications.first).domain_recurrent_failures NotificationsMailer.with(notification: check.notifications.first).domain_recurrent_failures
end end
# Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/ssl_expires_soon
def ssl_expires_soon
check = Check.ssl.first
NotificationsMailer.with(notification: check.notifications.first).ssl_expires_soon
end
# Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/ssl_recurrent_failures
def ssl_recurrent_failures
check = Check.ssl.where("last_run_at != last_success_at").first
NotificationsMailer.with(notification: check.notifications.first).ssl_recurrent_failures
end
end end

View file

@ -0,0 +1,34 @@
require "test_helper"
class CheckDomainProcessorTest < ActiveSupport::TestCase
setup do
@processor = CheckDomainProcessor.new
end
test "process WhoisSyncJob for domain checks" do
domain = "domain.fr"
check = create(:check, :domain, :nil_dates, domain: domain)
mock_system_command("whois", domain, stdout: file_fixture("whois/domain.fr.txt").read) do
@processor.send(:process, check)
end
check.reload
assert_equal Time.new(2019, 2, 17, 0, 0, 0, 0), check.domain_expires_at
end
test "scope concerns only checks of kind 'domain'" do
domains = create_list(:check, 2, :domain)
create_list(:check, 2, :ssl)
assert_equal domains, @processor.send(:scope)
end
test "resolvers returns an array of methods returning a scope" do
assert_not_empty @processor.send(:resolvers)
@processor.send(:resolvers).each do |method|
assert_kind_of ActiveRecord::Relation, @processor.public_send(method)
end
end
end

View file

@ -1,31 +1,26 @@
require "test_helper" require "test_helper"
class CheckDummyProcessor
include CheckProcessor
def scope
base_scope
end
def configuration_key
"checks_dummy"
end
def resolvers
%i[
resolve_expire_short_term
resolve_expire_long_term
]
end
end
class CheckProcessorTest < ActiveSupport::TestCase class CheckProcessorTest < ActiveSupport::TestCase
setup do setup do
@processor = CheckProcessor.new @processor = CheckDummyProcessor.new
end
test "process WhoisSyncJob for domain checks" do
domain = "domain.fr"
check = create(:check, :domain, :nil_dates, domain: domain)
mock_system_command("whois", domain, stdout: file_fixture("whois/domain.fr.txt").read) do
@processor.send(:process, check)
end
check.reload
assert_equal Time.new(2019, 2, 17, 0, 0, 0, 0), check.domain_expires_at
end
test "raises an error for an unsupported check kind" do
check = build(:check)
check.stub :kind, :unknown do
assert_raises ArgumentError do
@processor.send(:process, check)
end
end
end end
test "resolve_last_run_failed includes already and never succeeded" do test "resolve_last_run_failed includes already and never succeeded" do
@ -74,6 +69,14 @@ class CheckProcessorTest < ActiveSupport::TestCase
assert_not_includes checks, c4 assert_not_includes checks, c4
end end
test "resolve_all include all eligible checks" do
create(:check, :expires_next_week)
create(:check, :expires_next_week, last_run_at: 4.hours.ago)
create(:check, :last_runs_failed)
assert_equal @processor.send(:scope), @processor.resolve_all
end
test "resolvers does not include checks recently executed" do test "resolvers does not include checks recently executed" do
c1 = create(:check, :expires_next_week) c1 = create(:check, :expires_next_week)
c2 = create(:check, :expires_next_week, last_run_at: 4.hours.ago) c2 = create(:check, :expires_next_week, last_run_at: 4.hours.ago)
@ -113,7 +116,7 @@ class CheckProcessorTest < ActiveSupport::TestCase
configuration.expect(:interval, 0.000001) configuration.expect(:interval, 0.000001)
end end
processor = CheckProcessor.new(configuration) processor = CheckDummyProcessor.new(configuration)
mock = Minitest::Mock.new mock = Minitest::Mock.new
assert_stub = lambda { |actual_time| assert_stub = lambda { |actual_time|

View file

@ -0,0 +1,35 @@
require "test_helper"
class CheckSSLProcessorTest < ActiveSupport::TestCase
setup do
@processor = CheckSSLProcessor.new
end
test "process SSLSyncJob for ssl checks" do
domain = "ssl0.domain.org"
check = create(:check, :ssl, :nil_dates, domain: domain)
response = file_fixture("ssl/ssl0.domain.org.txt").read
mock_system_command("check_http", ["-H '#{domain}'"], stdout: response) do
@processor.send(:process, check)
end
check.reload
assert_equal Time.new(2028, 6, 10, 9, 14, 18, 0), check.domain_expires_at
end
test "scope concerns only checks of kind 'ssl'" do
domains = create_list(:check, 2, :ssl)
create_list(:check, 2, :domain)
assert_equal domains, @processor.send(:scope)
end
test "resolvers returns an array of methods returning a scope" do
assert_not_empty @processor.send(:resolvers)
@processor.send(:resolvers).each do |method|
assert_kind_of ActiveRecord::Relation, @processor.public_send(method)
end
end
end

View file

@ -0,0 +1,53 @@
require "test_helper"
require "ssl/parser"
require "ssl/errors"
module SSL
class ParserTest < ActiveSupport::TestCase
test "should parse a SSL check response" do
parser = Parser.new("ssl0.domain.org")
domain = file_fixture("ssl/ssl0.domain.org.txt").read
response = parser.parse(domain)
assert_kind_of Response, response
assert_equal Time.new(2028, 6, 10, 9, 14, 18, 0), response.expire_at
assert response.expire_at.utc?
end
test "should parse a SSL check response in another format and convert it in UTC" do
parser = Parser.new("ssl1.domain.org")
domain = file_fixture("ssl/ssl1.domain.org.txt").read
response = parser.parse(domain)
assert_kind_of Response, response
assert_equal Time.new(2022, 8, 6, 0, 57, 0, 0), response.expire_at
assert response.expire_at.utc?
end
test "should raises DomainNotMatchError when parsed text does not match the domain" do
parser = Parser.new("anotherdomain.fr")
output = file_fixture("ssl/ssl1.domain.org.txt").read
assert_raises DomainNotMatchError do
parser.parse(output)
end
end
test "should raises InvalidResponseError when check response is not matched" do
parser = Parser.new("ssl100.invalid.org")
output = file_fixture("ssl/ssl100.invalid.org.txt").read
assert_raises InvalidResponseError do
parser.parse(output)
end
end
test "should raises InvalidDateError when a date is not in the expected format" do
parser = Parser.new("ssl101.invalid.org")
output = file_fixture("ssl/ssl101.invalid.org.txt").read
assert_raises InvalidDateError do
parser.parse(output)
end
end
end
end

74
test/services/ssl_test.rb Normal file
View file

@ -0,0 +1,74 @@
require "test_helper"
require "ssl"
require "system_command"
module SSL
class ServiceTest < ActiveSupport::TestCase
test "should run the command, return the result" do
result = OpenStruct.new(exit_status: 0)
mock_system_klass("check_http", ["-H 'example.org'"], result) do |system_klass|
service = Service.new("example.org", system_klass: system_klass)
assert_equal result, service.run_command
end
end
test "should raise an exception if exit status > 0" do
result = OpenStruct.new(exit_status: 1)
mock_system_klass("check_http", ["-H 'example.org'"], result) do |system_klass|
service = Service.new("example.org", system_klass: system_klass)
assert_raises SSLCommandError do
service.run_command
end
end
end
test "should parse from a command result" do
result = OpenStruct.new(
exit_status: 0,
stdout: file_fixture("ssl/ssl0.domain.org.txt").read,
)
service = Service.new("ssl0.domain.org")
assert_kind_of Response, service.parse(result)
end
test "should uses the command line arguments of the configuration" do
result = OpenStruct.new(exit_status: 0)
config = OpenStruct.new(check_http_args: "-f follow -I 127.0.0.1")
expected_args = ["-f follow -I 127.0.0.1", "-H 'example.org'"]
mock_system_klass("check_http", expected_args, result) do |system_klass|
service = Service.new("example.org", configuration: config, system_klass: system_klass)
assert_equal result, service.run_command
end
end
test "should uses the program path from the configuration" do
result = OpenStruct.new(exit_status: 0)
config = OpenStruct.new(check_http_path: "/usr/local/custom/path")
mock_system_klass("/usr/local/custom/path", ["-H 'example.org'"], result) do |system_klass|
service = Service.new("example.org", configuration: config, system_klass: system_klass)
assert_equal result, service.run_command
end
end
def mock_system_klass(program, command_args, result)
system_klass = Minitest::Mock.new
system_command = Minitest::Mock.new.expect(:execute, result)
system_klass.expect(:new, system_command) do |arg1, arg2, logger:|
arg1 == program &&
arg2 == command_args &&
logger.class == NullLogger
end
yield system_klass
system_klass.verify
system_command.verify
end
end
end

View file

@ -6,6 +6,7 @@ if !ENV["NO_COVERAGE"] && (ARGV.empty? || ARGV.include?("test/test_helper.rb"))
SimpleCov.start "rails" do SimpleCov.start "rails" do
add_group "Notifier", "app/services/notifier" add_group "Notifier", "app/services/notifier"
add_group "Whois", "app/services/whois" add_group "Whois", "app/services/whois"
add_group "SSL", "app/services/ssl"
add_group "Services", "app/services" add_group "Services", "app/services"
add_group "Policies", "app/policies" add_group "Policies", "app/policies"
end end