mirror of
https://github.com/Evolix/chexpire.git
synced 2024-05-25 11:48:48 +02:00
commit
60eb4cb16c
63
README.md
63
README.md
|
@ -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.
|
||||
|
||||
![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
43
app/jobs/ssl_sync_job.rb
Normal 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
|
|
@ -19,4 +19,16 @@ class NotificationsMailer < ApplicationMailer
|
|||
subject = t(".subject", domain: @check.domain)
|
||||
mail subject: subject
|
||||
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
|
||||
|
|
26
app/services/check_domain_processor.rb
Normal file
26
app/services/check_domain_processor.rb
Normal 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
|
|
@ -1,17 +1,12 @@
|
|||
class CheckProcessor
|
||||
module CheckProcessor
|
||||
attr_reader :configuration
|
||||
|
||||
def initialize(configuration = nil)
|
||||
@configuration = configuration || default_configuration
|
||||
end
|
||||
|
||||
def sync_dates # rubocop:disable Metrics/MethodLength
|
||||
%i[
|
||||
resolve_last_run_failed
|
||||
resolve_expire_short_term
|
||||
resolve_expire_long_term
|
||||
resolve_unknown_expiry
|
||||
].each do |resolver|
|
||||
def sync_dates
|
||||
resolvers.each do |resolver|
|
||||
public_send(resolver).find_each(batch_size: 100).each do |check|
|
||||
process(check)
|
||||
|
||||
|
@ -20,6 +15,12 @@ class CheckProcessor
|
|||
end
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
def resolvers
|
||||
fail NotImplementedError, "#{self.class.name} did not implemented method #{__callee__}"
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
def resolve_last_run_failed
|
||||
scope.last_run_failed
|
||||
end
|
||||
|
@ -41,25 +42,36 @@ class CheckProcessor
|
|||
scope.where("domain_expires_at IS NULL")
|
||||
end
|
||||
|
||||
private
|
||||
def resolve_all
|
||||
scope
|
||||
end
|
||||
|
||||
def scope
|
||||
protected
|
||||
|
||||
def base_scope
|
||||
Check
|
||||
.active
|
||||
.where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)")
|
||||
end
|
||||
|
||||
def process(check)
|
||||
case check.kind.to_sym
|
||||
when :domain
|
||||
WhoisSyncJob.perform_now(check.id)
|
||||
else
|
||||
fail ArgumentError, "Unsupported check kind `#{check.kind}`"
|
||||
end
|
||||
# :nocov:
|
||||
def scope
|
||||
fail NotImplementedError, "#{self.class.name} did not implemented method #{__callee__}"
|
||||
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
|
||||
config = Rails.configuration.chexpire.fetch("checks", {})
|
||||
config = Rails.configuration.chexpire.fetch(configuration_key, {})
|
||||
|
||||
OpenStruct.new(
|
||||
interval: config.fetch("interval") { 0.00 },
|
||||
|
|
23
app/services/check_ssl_processor.rb
Normal file
23
app/services/check_ssl_processor.rb
Normal 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
|
|
@ -11,6 +11,10 @@ module Notifier
|
|||
domain_notify_expires_soon(notification)
|
||||
when [:domain, :recurrent_failures]
|
||||
domain_notify_recurrent_failures(notification)
|
||||
when [:ssl, :expires_soon]
|
||||
ssl_notify_expires_soon(notification)
|
||||
when [:ssl, :recurrent_failures]
|
||||
ssl_notify_recurrent_failures(notification)
|
||||
else
|
||||
fail ArgumentError,
|
||||
"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__}"
|
||||
end
|
||||
|
||||
# domain notifications
|
||||
def domain_notify_expires_soon(_notification)
|
||||
fail NotImplementedError,
|
||||
"Channel #{self.class.name} does not implement #{__callee__}"
|
||||
end
|
||||
|
||||
def domain_notify_recurrent_failures(_notification)
|
||||
fail NotImplementedError,
|
||||
"Channel #{self.class.name} does not implement #{__callee__}"
|
||||
%i[
|
||||
domain_notify_expires_soon
|
||||
domain_notify_recurrent_failures
|
||||
ssl_notify_expires_soon
|
||||
ssl_notify_recurrent_failures
|
||||
].each do |method|
|
||||
define_method(method) do
|
||||
fail NotImplementedError,
|
||||
"Channel #{self.class.name} does not implement method #{method}."
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
|
|
|
@ -16,6 +16,14 @@ module Notifier
|
|||
def domain_notify_recurrent_failures(notification)
|
||||
NotificationsMailer.with(notification: notification).domain_recurrent_failures.deliver_now
|
||||
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
|
||||
|
|
66
app/services/ssl.rb
Normal file
66
app/services/ssl.rb
Normal 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
|
10
app/services/ssl/errors.rb
Normal file
10
app/services/ssl/errors.rb
Normal 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
|
52
app/services/ssl/parser.rb
Normal file
52
app/services/ssl/parser.rb
Normal 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
|
13
app/services/ssl/response.rb
Normal file
13
app/services/ssl/response.rb
Normal 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
|
|
@ -3,7 +3,7 @@
|
|||
--
|
||||
|
||||
<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 :
|
||||
<%= link_to nil, edit_check_url(check) %>
|
||||
</p>
|
||||
|
|
|
@ -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.
|
||||
Vous pouvez gérer les notifications pour cette vérification à ce adresse :
|
||||
<%= edit_check_url(check) %>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<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 :
|
||||
<%= link_to nil, edit_check_url(check) %>
|
||||
</p>
|
||||
|
|
|
@ -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.
|
||||
Vous pouvez gérer les notifications pour cette vérification à ce adresse :
|
||||
<%= edit_check_url(check) %>
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
|
||||
<%= render "check_comment_vendor" %>
|
||||
|
||||
<%= render "footer_expires_soon", delay: @notification.delay, check: @check %>
|
||||
<%= render "footer_expires_soon", interval: @notification.interval, check: @check %>
|
||||
|
|
|
@ -6,4 +6,4 @@ le domaine <%= @check.domain %> va expirer le <%= format_utc(@check.domain_expir
|
|||
<%= render "check_comment_vendor" %>
|
||||
|
||||
|
||||
<%= render "footer_expires_soon", delay: @notification.delay, check: @check %>
|
||||
<%= render "footer_expires_soon", interval: @notification.interval, check: @check %>
|
||||
|
|
|
@ -8,9 +8,14 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
Our last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
|
||||
<br />
|
||||
Our last successful check occured <%= format_utc(@check.last_success_at) %>.
|
||||
<%- 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>
|
||||
|
|
|
@ -3,8 +3,12 @@ Hi,
|
|||
We had recurrent failures while checking the whois database for domain
|
||||
<%= @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) %>.
|
||||
<% end %>
|
||||
<%- if @check.last_success_at.present? %>
|
||||
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
|
||||
or delete the check by following this link:
|
||||
|
|
|
@ -8,9 +8,14 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
La dernière date d'expiration connue est le <%= format_utc(@check.domain_expires_at) %>.
|
||||
<br />
|
||||
Notre dernière vérification réussie a eu lieu le <%= format_utc(@check.last_success_at) %>.
|
||||
<%- 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>
|
||||
|
@ -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 "footer_recurrent_failures", delay: @notification.delay, check: @check %>
|
||||
<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>
|
||||
|
|
|
@ -4,10 +4,17 @@ Nous avons rencontré de multiples erreurs pendant l'exécution des vérificatio
|
|||
du domaine <strong><%= @check.domain %></strong>.
|
||||
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é,
|
||||
merci de désactiver la vérification associée, avec ce lien :
|
||||
<%= edit_check_url(@check) %>
|
||||
|
||||
<%= render "check_comment_vendor" %>
|
||||
|
||||
<%= render "footer_recurrent_failures", delay: @notification.delay, check: @check %>
|
||||
<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>
|
||||
|
|
12
app/views/notifications_mailer/ssl_expires_soon.en.html.erb
Normal file
12
app/views/notifications_mailer/ssl_expires_soon.en.html.erb
Normal 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 %>
|
|
@ -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 %>
|
12
app/views/notifications_mailer/ssl_expires_soon.fr.html.erb
Normal file
12
app/views/notifications_mailer/ssl_expires_soon.fr.html.erb
Normal 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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -3,10 +3,13 @@ default: &default
|
|||
notifier:
|
||||
interval: 0.00
|
||||
failure_days: 3
|
||||
checks:
|
||||
checks_domain:
|
||||
interval: 0.5
|
||||
long_term: 60
|
||||
long_term_frequency: 10
|
||||
checks_ssl:
|
||||
check_http_path: ""
|
||||
check_http_args: ""
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
|
|
@ -4,7 +4,10 @@ test:
|
|||
notifier:
|
||||
interval: 0.00
|
||||
failure_days: 3
|
||||
checks:
|
||||
checks_domain:
|
||||
interval: 0.00
|
||||
long_term: 60
|
||||
long_term_frequency: 10
|
||||
checks_ssl:
|
||||
check_http_path: ""
|
||||
check_http_args: ""
|
||||
|
|
|
@ -38,6 +38,15 @@ en:
|
|||
domain_recurrent_failures:
|
||||
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:
|
||||
locales:
|
||||
|
|
|
@ -12,6 +12,14 @@ fr:
|
|||
check:
|
||||
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:
|
||||
registrations:
|
||||
new:
|
||||
|
@ -36,7 +44,16 @@ fr:
|
|||
other: "Le domaine %{domain} expire dans %{count} jours"
|
||||
|
||||
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:
|
||||
|
|
|
@ -19,7 +19,7 @@ set :output, standard: "log/cron.log"
|
|||
# Learn more: http://github.com/javan/whenever
|
||||
|
||||
every 1.day, at: '4:30 am', roles: [:app] do
|
||||
rake "checks:sync_dates"
|
||||
rake "checks:sync_dates:all"
|
||||
end
|
||||
|
||||
every 1.day, at: '10:30 am', roles: [:app] do
|
||||
|
|
42
db/seeds.rb
42
db/seeds.rb
|
@ -1,3 +1,4 @@
|
|||
CheckLog.destroy_all
|
||||
Notification.destroy_all
|
||||
Check.destroy_all
|
||||
User.destroy_all
|
||||
|
@ -33,6 +34,31 @@ check_chexpire_org_error = Check.create!(
|
|||
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!(
|
||||
check: check_chexpire_org,
|
||||
interval: 15,
|
||||
|
@ -49,6 +75,22 @@ Notification.create!(
|
|||
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 " "
|
||||
puts "--------------------"
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
namespace :checks do
|
||||
desc "Refresh expiry dates for checks"
|
||||
task sync_dates: :environment do
|
||||
process = CheckProcessor.new
|
||||
process.sync_dates
|
||||
namespace :sync_dates do
|
||||
task all: [:domain, :ssl]
|
||||
|
||||
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
|
||||
|
|
|
@ -44,6 +44,10 @@ FactoryBot.define do
|
|||
kind :domain
|
||||
end
|
||||
|
||||
trait :ssl do
|
||||
kind :ssl
|
||||
end
|
||||
|
||||
trait :nil_dates do
|
||||
domain_created_at nil
|
||||
domain_updated_at nil
|
||||
|
|
1
test/fixtures/files/ssl/ssl0.domain.org.txt
vendored
Normal file
1
test/fixtures/files/ssl/ssl0.domain.org.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
OK - Certificate 'ssl0.domain.org' will expire on Sat 10 Jun 2028 09:14:18 AM GMT +0000.
|
1
test/fixtures/files/ssl/ssl1.domain.org.txt
vendored
Normal file
1
test/fixtures/files/ssl/ssl1.domain.org.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
SSL OK - Certificate 'ssl1.domain.org' will expire on 2022-08-06 02:57 +0200/CEST.
|
1
test/fixtures/files/ssl/ssl100.invalid.org.txt
vendored
Normal file
1
test/fixtures/files/ssl/ssl100.invalid.org.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
check_http: Invalid hostname/address - ssl100.invalid.org
|
1
test/fixtures/files/ssl/ssl101.invalid.org.txt
vendored
Normal file
1
test/fixtures/files/ssl/ssl101.invalid.org.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
SSL OK - Certificate 'ssl101.invalid.org' will expire on unknown date.
|
68
test/jobs/ssl_sync_job_test.rb
Normal file
68
test/jobs/ssl_sync_job_test.rb
Normal 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
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class NotificationsMailerTest < ActionMailer::TestCase
|
||||
class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics/ClassLength
|
||||
test "domain_expires_soon" 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")
|
||||
|
@ -17,7 +17,7 @@ class NotificationsMailerTest < ActionMailer::TestCase
|
|||
assert_equal ["colin@example.org"], mail.to
|
||||
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|
|
||||
assert_match "domain.fr", part
|
||||
|
@ -30,6 +30,37 @@ class NotificationsMailerTest < ActionMailer::TestCase
|
|||
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
|
||||
check = create(:check,
|
||||
domain_expires_at: 1.week.from_now,
|
||||
|
@ -39,7 +70,7 @@ class NotificationsMailerTest < ActionMailer::TestCase
|
|||
|
||||
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|
|
||||
assert_match "My comment", part
|
||||
|
@ -47,6 +78,25 @@ class NotificationsMailerTest < ActionMailer::TestCase
|
|||
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
|
||||
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")
|
||||
|
@ -64,7 +114,7 @@ class NotificationsMailerTest < ActionMailer::TestCase
|
|||
assert_equal ["recipient@domain.fr"], mail.to
|
||||
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|
|
||||
assert_match "invalid-domain.fr", part
|
||||
|
@ -75,4 +125,159 @@ class NotificationsMailerTest < ActionMailer::TestCase
|
|||
assert_match "/checks/#{check.id}/edit", part
|
||||
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
|
||||
|
|
|
@ -2,12 +2,25 @@
|
|||
class NotificationsMailerPreview < ActionMailer::Preview
|
||||
# Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/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
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/notifications_mailer/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
|
||||
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
|
||||
|
|
34
test/services/check_domain_processor_test.rb
Normal file
34
test/services/check_domain_processor_test.rb
Normal 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
|
|
@ -1,31 +1,26 @@
|
|||
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
|
||||
setup do
|
||||
@processor = CheckProcessor.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
|
||||
@processor = CheckDummyProcessor.new
|
||||
end
|
||||
|
||||
test "resolve_last_run_failed includes already and never succeeded" do
|
||||
|
@ -74,6 +69,14 @@ class CheckProcessorTest < ActiveSupport::TestCase
|
|||
assert_not_includes checks, c4
|
||||
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
|
||||
c1 = create(:check, :expires_next_week)
|
||||
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)
|
||||
end
|
||||
|
||||
processor = CheckProcessor.new(configuration)
|
||||
processor = CheckDummyProcessor.new(configuration)
|
||||
|
||||
mock = Minitest::Mock.new
|
||||
assert_stub = lambda { |actual_time|
|
||||
|
|
35
test/services/check_ssl_processor_test.rb
Normal file
35
test/services/check_ssl_processor_test.rb
Normal 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
|
53
test/services/ssl/parser_test.rb
Normal file
53
test/services/ssl/parser_test.rb
Normal 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
74
test/services/ssl_test.rb
Normal 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
|
|
@ -6,6 +6,7 @@ if !ENV["NO_COVERAGE"] && (ARGV.empty? || ARGV.include?("test/test_helper.rb"))
|
|||
SimpleCov.start "rails" do
|
||||
add_group "Notifier", "app/services/notifier"
|
||||
add_group "Whois", "app/services/whois"
|
||||
add_group "SSL", "app/services/ssl"
|
||||
add_group "Services", "app/services"
|
||||
add_group "Policies", "app/policies"
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue