diff --git a/README.md b/README.md
index 9a6399a..5800ae6 100644
--- a/README.md
+++ b/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.
diff --git a/app/jobs/ssl_sync_job.rb b/app/jobs/ssl_sync_job.rb
new file mode 100644
index 0000000..6c47679
--- /dev/null
+++ b/app/jobs/ssl_sync_job.rb
@@ -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
diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb
index 814d526..31a4677 100644
--- a/app/mailers/notifications_mailer.rb
+++ b/app/mailers/notifications_mailer.rb
@@ -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
diff --git a/app/services/check_domain_processor.rb b/app/services/check_domain_processor.rb
new file mode 100644
index 0000000..183fc03
--- /dev/null
+++ b/app/services/check_domain_processor.rb
@@ -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
diff --git a/app/services/check_processor.rb b/app/services/check_processor.rb
index 31737c3..abe087c 100644
--- a/app/services/check_processor.rb
+++ b/app/services/check_processor.rb
@@ -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 },
diff --git a/app/services/check_ssl_processor.rb b/app/services/check_ssl_processor.rb
new file mode 100644
index 0000000..f36e7c5
--- /dev/null
+++ b/app/services/check_ssl_processor.rb
@@ -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
diff --git a/app/services/notifier/channels/base.rb b/app/services/notifier/channels/base.rb
index b63a52d..a3e9586 100644
--- a/app/services/notifier/channels/base.rb
+++ b/app/services/notifier/channels/base.rb
@@ -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
diff --git a/app/services/notifier/channels/email.rb b/app/services/notifier/channels/email.rb
index 17bbdab..9243b7d 100644
--- a/app/services/notifier/channels/email.rb
+++ b/app/services/notifier/channels/email.rb
@@ -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
diff --git a/app/services/ssl.rb b/app/services/ssl.rb
new file mode 100644
index 0000000..95af40f
--- /dev/null
+++ b/app/services/ssl.rb
@@ -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
diff --git a/app/services/ssl/errors.rb b/app/services/ssl/errors.rb
new file mode 100644
index 0000000..6982c62
--- /dev/null
+++ b/app/services/ssl/errors.rb
@@ -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
diff --git a/app/services/ssl/parser.rb b/app/services/ssl/parser.rb
new file mode 100644
index 0000000..68dffe8
--- /dev/null
+++ b/app/services/ssl/parser.rb
@@ -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
diff --git a/app/services/ssl/response.rb b/app/services/ssl/response.rb
new file mode 100644
index 0000000..762c498
--- /dev/null
+++ b/app/services/ssl/response.rb
@@ -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
diff --git a/app/views/notifications_mailer/_footer_expires_soon.fr.html.erb b/app/views/notifications_mailer/_footer_expires_soon.fr.html.erb
index d6f13b9..2389402 100644
--- a/app/views/notifications_mailer/_footer_expires_soon.fr.html.erb
+++ b/app/views/notifications_mailer/_footer_expires_soon.fr.html.erb
@@ -3,7 +3,7 @@
--
- Vous avez reçu ce courriel à <%= pluralize(delay, "jour", "jours") %> avant la date d'expiration.
+ 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 :
<%= link_to nil, edit_check_url(check) %>
diff --git a/app/views/notifications_mailer/_footer_expires_soon.fr.text.erb b/app/views/notifications_mailer/_footer_expires_soon.fr.text.erb
index 35d29f2..6c3880b 100644
--- a/app/views/notifications_mailer/_footer_expires_soon.fr.text.erb
+++ b/app/views/notifications_mailer/_footer_expires_soon.fr.text.erb
@@ -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) %>
diff --git a/app/views/notifications_mailer/_footer_recurrent_failures.fr.html.erb b/app/views/notifications_mailer/_footer_recurrent_failures.fr.html.erb
index 8d68f9d..bb35480 100644
--- a/app/views/notifications_mailer/_footer_recurrent_failures.fr.html.erb
+++ b/app/views/notifications_mailer/_footer_recurrent_failures.fr.html.erb
@@ -2,7 +2,7 @@
--
-Vous avez reçu ce courriel à <%= pluralize(delay, "jour", "jours") %> avant la dernière date d'expiration connue.
+
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 :
<%= link_to nil, edit_check_url(check) %>
diff --git a/app/views/notifications_mailer/_footer_recurrent_failures.fr.text.erb b/app/views/notifications_mailer/_footer_recurrent_failures.fr.text.erb
index ca48eea..a77dd21 100644
--- a/app/views/notifications_mailer/_footer_recurrent_failures.fr.text.erb
+++ b/app/views/notifications_mailer/_footer_recurrent_failures.fr.text.erb
@@ -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) %>
diff --git a/app/views/notifications_mailer/domain_expires_soon.fr.html.erb b/app/views/notifications_mailer/domain_expires_soon.fr.html.erb
index 365f986..9b39910 100644
--- a/app/views/notifications_mailer/domain_expires_soon.fr.html.erb
+++ b/app/views/notifications_mailer/domain_expires_soon.fr.html.erb
@@ -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 %>
diff --git a/app/views/notifications_mailer/domain_expires_soon.fr.text.erb b/app/views/notifications_mailer/domain_expires_soon.fr.text.erb
index 7d11568..2caad62 100644
--- a/app/views/notifications_mailer/domain_expires_soon.fr.text.erb
+++ b/app/views/notifications_mailer/domain_expires_soon.fr.text.erb
@@ -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 %>
diff --git a/app/views/notifications_mailer/domain_recurrent_failures.en.html.erb b/app/views/notifications_mailer/domain_recurrent_failures.en.html.erb
index 5584f9b..2146dbc 100644
--- a/app/views/notifications_mailer/domain_recurrent_failures.en.html.erb
+++ b/app/views/notifications_mailer/domain_recurrent_failures.en.html.erb
@@ -8,9 +8,14 @@
- Our last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
-
- 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) %>.
+
+ <% end %>
+
+ <%- if @check.last_success_at.present? %>
+ Our last successful check occured <%= format_utc(@check.last_success_at) %>.
+ <% end %>
diff --git a/app/views/notifications_mailer/domain_recurrent_failures.en.text.erb b/app/views/notifications_mailer/domain_recurrent_failures.en.text.erb
index a54d658..b5ce466 100644
--- a/app/views/notifications_mailer/domain_recurrent_failures.en.text.erb
+++ b/app/views/notifications_mailer/domain_recurrent_failures.en.text.erb
@@ -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:
diff --git a/app/views/notifications_mailer/domain_recurrent_failures.fr.html.erb b/app/views/notifications_mailer/domain_recurrent_failures.fr.html.erb
index 94e15ba..7da5aed 100644
--- a/app/views/notifications_mailer/domain_recurrent_failures.fr.html.erb
+++ b/app/views/notifications_mailer/domain_recurrent_failures.fr.html.erb
@@ -8,9 +8,14 @@
- 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) %>.
+ <%- 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 %>
@@ -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 %>
diff --git a/app/views/notifications_mailer/domain_recurrent_failures.fr.text.erb b/app/views/notifications_mailer/domain_recurrent_failures.fr.text.erb
index 64b76c5..cb8673e 100644
--- a/app/views/notifications_mailer/domain_recurrent_failures.fr.text.erb
+++ b/app/views/notifications_mailer/domain_recurrent_failures.fr.text.erb
@@ -4,10 +4,17 @@ Nous avons rencontré de multiples erreurs pendant l'exécution des vérificatio
du domaine <%= @check.domain %>.
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 %>
diff --git a/app/views/notifications_mailer/ssl_expires_soon.en.html.erb b/app/views/notifications_mailer/ssl_expires_soon.en.html.erb
new file mode 100644
index 0000000..401df10
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_expires_soon.en.html.erb
@@ -0,0 +1,12 @@
+
+ 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 %>
diff --git a/app/views/notifications_mailer/ssl_expires_soon.en.text.erb b/app/views/notifications_mailer/ssl_expires_soon.en.text.erb
new file mode 100644
index 0000000..a1d1c56
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_expires_soon.en.text.erb
@@ -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 %>
diff --git a/app/views/notifications_mailer/ssl_expires_soon.fr.html.erb b/app/views/notifications_mailer/ssl_expires_soon.fr.html.erb
new file mode 100644
index 0000000..dfc1c3f
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_expires_soon.fr.html.erb
@@ -0,0 +1,12 @@
+
+ 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 %>
diff --git a/app/views/notifications_mailer/ssl_expires_soon.fr.text.erb b/app/views/notifications_mailer/ssl_expires_soon.fr.text.erb
new file mode 100644
index 0000000..0664fb5
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_expires_soon.fr.text.erb
@@ -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 %>
diff --git a/app/views/notifications_mailer/ssl_recurrent_failures.en.html.erb b/app/views/notifications_mailer/ssl_recurrent_failures.en.html.erb
new file mode 100644
index 0000000..602c32d
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_recurrent_failures.en.html.erb
@@ -0,0 +1,31 @@
+
+ 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? %>
+ Our last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
+
+ <%- end %>
+
+ <%- if @check.last_success_at.present? %>
+ Our 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:
+ <%= link_to nil, edit_check_url(@check) %>
+
+
+<%= render "check_comment_vendor" %>
+
+
+<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>
diff --git a/app/views/notifications_mailer/ssl_recurrent_failures.en.text.erb b/app/views/notifications_mailer/ssl_recurrent_failures.en.text.erb
new file mode 100644
index 0000000..2bb6dd3
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_recurrent_failures.en.text.erb
@@ -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 %>
diff --git a/app/views/notifications_mailer/ssl_recurrent_failures.fr.html.erb b/app/views/notifications_mailer/ssl_recurrent_failures.fr.html.erb
new file mode 100644
index 0000000..ea05abc
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_recurrent_failures.fr.html.erb
@@ -0,0 +1,35 @@
+
+ Salut,
+
+
+
+ Nous avons rencontré de multiples erreurs
+ pendant l'exécution des vérifications du certificat SSL
+ du site <%= @check.domain %>.
+ 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 :
+
+
+ <%= link_to nil, edit_check_url(@check) %>
+
+
+<%= render "check_comment_vendor" %>
+
+
+<%= render "footer_recurrent_failures", interval: @notification.interval, check: @check %>
diff --git a/app/views/notifications_mailer/ssl_recurrent_failures.fr.text.erb b/app/views/notifications_mailer/ssl_recurrent_failures.fr.text.erb
new file mode 100644
index 0000000..fe15787
--- /dev/null
+++ b/app/views/notifications_mailer/ssl_recurrent_failures.fr.text.erb
@@ -0,0 +1,22 @@
+Salut,
+
+Nous avons rencontré de multiples erreurs pendant l'exécution des vérifications
+du certificat SSL pour le site <%= @check.domain %>.
+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 %>
diff --git a/config/chexpire.example.yml b/config/chexpire.example.yml
index ffb7daa..b322b96 100644
--- a/config/chexpire.example.yml
+++ b/config/chexpire.example.yml
@@ -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
diff --git a/config/chexpire.test.yml b/config/chexpire.test.yml
index db88e9a..2a72a7b 100644
--- a/config/chexpire.test.yml
+++ b/config/chexpire.test.yml
@@ -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: ""
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d93597e..9a34ad9 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 4e4b71c..ed5926f 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -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:
diff --git a/config/schedule.rb b/config/schedule.rb
index 709286e..27553b1 100644
--- a/config/schedule.rb
+++ b/config/schedule.rb
@@ -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
diff --git a/db/seeds.rb b/db/seeds.rb
index c89deb6..2bf199b 100644
--- a/db/seeds.rb
+++ b/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 "--------------------"
diff --git a/lib/tasks/checks.rake b/lib/tasks/checks.rake
index 5d82236..6703e73 100644
--- a/lib/tasks/checks.rake
+++ b/lib/tasks/checks.rake
@@ -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
diff --git a/test/factories/checks.rb b/test/factories/checks.rb
index 62d95ec..f940eb2 100644
--- a/test/factories/checks.rb
+++ b/test/factories/checks.rb
@@ -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
diff --git a/test/fixtures/files/ssl/ssl0.domain.org.txt b/test/fixtures/files/ssl/ssl0.domain.org.txt
new file mode 100644
index 0000000..8663a1d
--- /dev/null
+++ b/test/fixtures/files/ssl/ssl0.domain.org.txt
@@ -0,0 +1 @@
+OK - Certificate 'ssl0.domain.org' will expire on Sat 10 Jun 2028 09:14:18 AM GMT +0000.
diff --git a/test/fixtures/files/ssl/ssl1.domain.org.txt b/test/fixtures/files/ssl/ssl1.domain.org.txt
new file mode 100644
index 0000000..91b39ee
--- /dev/null
+++ b/test/fixtures/files/ssl/ssl1.domain.org.txt
@@ -0,0 +1 @@
+SSL OK - Certificate 'ssl1.domain.org' will expire on 2022-08-06 02:57 +0200/CEST.
diff --git a/test/fixtures/files/ssl/ssl100.invalid.org.txt b/test/fixtures/files/ssl/ssl100.invalid.org.txt
new file mode 100644
index 0000000..3153b3e
--- /dev/null
+++ b/test/fixtures/files/ssl/ssl100.invalid.org.txt
@@ -0,0 +1 @@
+check_http: Invalid hostname/address - ssl100.invalid.org
diff --git a/test/fixtures/files/ssl/ssl101.invalid.org.txt b/test/fixtures/files/ssl/ssl101.invalid.org.txt
new file mode 100644
index 0000000..1698540
--- /dev/null
+++ b/test/fixtures/files/ssl/ssl101.invalid.org.txt
@@ -0,0 +1 @@
+SSL OK - Certificate 'ssl101.invalid.org' will expire on unknown date.
diff --git a/test/jobs/ssl_sync_job_test.rb b/test/jobs/ssl_sync_job_test.rb
new file mode 100644
index 0000000..0a55cdb
--- /dev/null
+++ b/test/jobs/ssl_sync_job_test.rb
@@ -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
diff --git a/test/mailers/notifications_mailer_test.rb b/test/mailers/notifications_mailer_test.rb
index adce75c..c261131 100644
--- a/test/mailers/notifications_mailer_test.rb
+++ b/test/mailers/notifications_mailer_test.rb
@@ -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
diff --git a/test/mailers/previews/notifications_mailer_preview.rb b/test/mailers/previews/notifications_mailer_preview.rb
index f9c2898..01f5085 100644
--- a/test/mailers/previews/notifications_mailer_preview.rb
+++ b/test/mailers/previews/notifications_mailer_preview.rb
@@ -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
diff --git a/test/services/check_domain_processor_test.rb b/test/services/check_domain_processor_test.rb
new file mode 100644
index 0000000..1ea3023
--- /dev/null
+++ b/test/services/check_domain_processor_test.rb
@@ -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
diff --git a/test/services/check_processor_test.rb b/test/services/check_processor_test.rb
index 66e1e53..d5c1387 100644
--- a/test/services/check_processor_test.rb
+++ b/test/services/check_processor_test.rb
@@ -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|
diff --git a/test/services/check_ssl_processor_test.rb b/test/services/check_ssl_processor_test.rb
new file mode 100644
index 0000000..db6fa3c
--- /dev/null
+++ b/test/services/check_ssl_processor_test.rb
@@ -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
diff --git a/test/services/ssl/parser_test.rb b/test/services/ssl/parser_test.rb
new file mode 100644
index 0000000..05566d6
--- /dev/null
+++ b/test/services/ssl/parser_test.rb
@@ -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
diff --git a/test/services/ssl_test.rb b/test/services/ssl_test.rb
new file mode 100644
index 0000000..5b98396
--- /dev/null
+++ b/test/services/ssl_test.rb
@@ -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
diff --git a/test/test_helper.rb b/test/test_helper.rb
index fda0a43..91ae325 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -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