mirror of
https://github.com/Evolix/chexpire.git
synced 2024-05-05 02:05:09 +02:00
commit
822da5c752
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -35,3 +35,9 @@ yarn-debug.log*
|
|||
.yarn-integrity
|
||||
|
||||
/config/deploy/config.yml
|
||||
|
||||
# SimpleCov coverage output
|
||||
/coverage
|
||||
|
||||
# OS file
|
||||
.DS_Store
|
||||
|
|
|
@ -19,9 +19,9 @@ before_install:
|
|||
install:
|
||||
- bundle install
|
||||
- yarn install
|
||||
- rails db:setup
|
||||
- rails db:create db:migrate
|
||||
|
||||
script:
|
||||
- bundle exec rubocop
|
||||
- bundle exec rails test
|
||||
- bundle exec rails test:system
|
||||
- bundle exec rails test NO_COVERAGE=1
|
||||
- bundle exec rails test:system NO_COVERAGE=1
|
||||
|
|
9
Gemfile
9
Gemfile
|
@ -39,6 +39,9 @@ gem 'bcrypt', '~> 3.1.7'
|
|||
gem 'open4'
|
||||
gem 'naught'
|
||||
|
||||
|
||||
gem 'octicons'
|
||||
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', '>= 1.1.0', require: false
|
||||
|
||||
|
@ -47,6 +50,8 @@ group :development, :test do
|
|||
gem 'binding_of_caller'
|
||||
gem 'pry-byebug'
|
||||
gem 'pry-rails'
|
||||
|
||||
gem "factory_bot_rails"
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
@ -61,7 +66,6 @@ group :development do
|
|||
gem 'annotate', require: false
|
||||
gem 'letter_opener_web'
|
||||
|
||||
|
||||
gem "guard"
|
||||
gem "guard-minitest"
|
||||
|
||||
|
@ -78,6 +82,9 @@ group :test do
|
|||
# Easy installation and use of chromedriver to run system tests with Chrome
|
||||
gem 'chromedriver-helper'
|
||||
gem 'launchy'
|
||||
|
||||
gem "database_cleaner"
|
||||
gem "simplecov", require: false
|
||||
end
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
|
|
19
Gemfile.lock
19
Gemfile.lock
|
@ -94,6 +94,7 @@ GEM
|
|||
coderay (1.1.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
crass (1.0.4)
|
||||
database_cleaner (1.7.0)
|
||||
debug_inspector (0.0.3)
|
||||
devise (4.4.3)
|
||||
bcrypt (~> 3.0)
|
||||
|
@ -103,8 +104,14 @@ GEM
|
|||
warden (~> 1.2.3)
|
||||
devise-i18n (1.6.2)
|
||||
devise (>= 4.4)
|
||||
docile (1.3.1)
|
||||
erubi (1.7.1)
|
||||
execjs (2.7.0)
|
||||
factory_bot (4.10.0)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_bot_rails (4.10.0)
|
||||
factory_bot (~> 4.10.0)
|
||||
railties (>= 3.0.0)
|
||||
ffi (1.9.23)
|
||||
formatador (0.2.5)
|
||||
globalid (0.4.1)
|
||||
|
@ -128,6 +135,7 @@ GEM
|
|||
jbuilder (2.7.0)
|
||||
activesupport (>= 4.2.0)
|
||||
multi_json (>= 1.2)
|
||||
json (2.1.0)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
letter_opener (1.6.0)
|
||||
|
@ -167,6 +175,8 @@ GEM
|
|||
notiffany (0.1.1)
|
||||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
octicons (7.3.0)
|
||||
nokogiri (>= 1.6.3.1)
|
||||
open4 (1.3.4)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.12.1)
|
||||
|
@ -253,6 +263,11 @@ GEM
|
|||
simple_form (4.0.1)
|
||||
actionpack (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
simplecov (0.16.1)
|
||||
docile (~> 1.1)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
spring (2.0.2)
|
||||
activesupport (>= 4.2)
|
||||
spring-watcher-listen (2.0.1)
|
||||
|
@ -310,8 +325,10 @@ DEPENDENCIES
|
|||
capistrano3-puma
|
||||
capybara (>= 2.15, < 4.0)
|
||||
chromedriver-helper
|
||||
database_cleaner
|
||||
devise (~> 4.4)
|
||||
devise-i18n (~> 1.6)
|
||||
factory_bot_rails
|
||||
guard
|
||||
guard-minitest
|
||||
jbuilder (~> 2.5)
|
||||
|
@ -320,6 +337,7 @@ DEPENDENCIES
|
|||
listen (>= 3.0.5, < 3.2)
|
||||
mysql2 (>= 0.4.4, < 0.6.0)
|
||||
naught
|
||||
octicons
|
||||
open4
|
||||
pry-byebug
|
||||
pry-rails
|
||||
|
@ -331,6 +349,7 @@ DEPENDENCIES
|
|||
sass-rails (~> 5.0)
|
||||
selenium-webdriver
|
||||
simple_form (~> 4.0)
|
||||
simplecov
|
||||
spring
|
||||
spring-watcher-listen (~> 2.0.0)
|
||||
turbolinks (~> 5)
|
||||
|
|
|
@ -21,9 +21,11 @@ guard "minitest", spring: "bin/rails test" do
|
|||
watch(%r{^app/controllers/application_controller\.rb$}) { "test/controllers" }
|
||||
watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" }
|
||||
watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" }
|
||||
watch(%r{^app/services/notifier/.+\.rb}) { |_m| "test/services/notifier" }
|
||||
watch(%r{^app/services/whois/.+\.rb}) { |_m| "test/services/whois" }
|
||||
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
||||
watch(%r{^test/.+_test\.rb$})
|
||||
watch(%r{^test/test_helper\.rb$}) { "test" }
|
||||
watch(%r{^test/fixtures/.+\.yml$}) { "test" }
|
||||
watch(%r{^test/factories/.+\.rb$}) { "test" }
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class ChecksController < ApplicationController
|
|||
|
||||
def new
|
||||
@check = Check.new
|
||||
build_empty_notification
|
||||
authorize @check
|
||||
end
|
||||
|
||||
|
@ -27,7 +28,9 @@ class ChecksController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def edit; end
|
||||
def edit
|
||||
build_empty_notification
|
||||
end
|
||||
|
||||
def update
|
||||
if @check.update(update_check_params)
|
||||
|
@ -35,6 +38,7 @@ class ChecksController < ApplicationController
|
|||
redirect_to checks_path
|
||||
else
|
||||
flash.now[:alert] = "An error occured."
|
||||
build_empty_notification
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
@ -62,6 +66,11 @@ class ChecksController < ApplicationController
|
|||
end
|
||||
|
||||
def check_params(*others)
|
||||
params.require(:check).permit(:domain, :domain_created_at, :comment, :vendor, *others)
|
||||
params.require(:check).permit(:domain, :domain_created_at, :comment, :vendor, *others,
|
||||
notifications_attributes: [:id, :channel, :recipient, :delay])
|
||||
end
|
||||
|
||||
def build_empty_notification
|
||||
@check.notifications.build
|
||||
end
|
||||
end
|
||||
|
|
44
app/controllers/notifications_controller.rb
Normal file
44
app/controllers/notifications_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class NotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_notification, except: [:create]
|
||||
|
||||
def create
|
||||
check = Check.find(params[:check_id])
|
||||
@notification = check.notifications.build(notification_params)
|
||||
authorize @notification
|
||||
|
||||
if @notification.save
|
||||
flash[:notice] = "Your notification has been saved."
|
||||
redirect_to check_path
|
||||
else
|
||||
flash.now[:alert] = "An error occured."
|
||||
render "checks/edit"
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_notification
|
||||
# joins the check because policy use the check relation
|
||||
@notification = Notification
|
||||
.joins(:check)
|
||||
.find_by!(id: params[:id], check_id: params[:check_id])
|
||||
authorize @notification
|
||||
end
|
||||
|
||||
def notification_params
|
||||
params.require(:notification).permit(:channel, :recipient, :delay)
|
||||
end
|
||||
|
||||
def check_path
|
||||
edit_check_path(check_id: params[:check_id])
|
||||
end
|
||||
end
|
5
app/frontend/scss/icons.scss
Normal file
5
app/frontend/scss/icons.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.octicon {
|
||||
fill: currentColor;
|
||||
vertical-align: text-top;
|
||||
display: inline-block;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
@import '~bootstrap/scss/bootstrap';
|
||||
@import 'layout';
|
||||
@import 'icons';
|
||||
@import 'components/users';
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
module ApplicationHelper
|
||||
def format_utc(time, format: :default)
|
||||
l(time.utc, format: format)
|
||||
end
|
||||
end
|
||||
|
|
9
app/helpers/notifications_helper.rb
Normal file
9
app/helpers/notifications_helper.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module NotificationsHelper
|
||||
def many_channels_available?
|
||||
Notification.channels.many?
|
||||
end
|
||||
|
||||
def recipient_col_class
|
||||
many_channels_available? ? "col-md-7" : "col-md-9"
|
||||
end
|
||||
end
|
|
@ -7,8 +7,9 @@ class WhoisSyncJob < ApplicationJob
|
|||
|
||||
def perform(check_id)
|
||||
@check = Check.find(check_id)
|
||||
response = Whois.ask(check.domain)
|
||||
check.update_attribute(:last_run_at, Time.now)
|
||||
|
||||
response = Whois.ask(check.domain)
|
||||
return unless response.valid?
|
||||
|
||||
update_from_response(response)
|
||||
|
@ -17,11 +18,14 @@ class WhoisSyncJob < ApplicationJob
|
|||
rescue Whois::DomainNotFoundError
|
||||
check.active = false
|
||||
check.save!
|
||||
rescue Whois::ParserError # rubocop:disable Lint/HandleExceptions
|
||||
# already logged
|
||||
end
|
||||
|
||||
def update_from_response(response)
|
||||
check.domain_created_at = response.created_at
|
||||
check.domain_updated_at = response.updated_at
|
||||
check.domain_expire_at = response.expire_at
|
||||
check.domain_expires_at = response.expire_at
|
||||
check.last_success_at = Time.now
|
||||
end
|
||||
end
|
||||
|
|
22
app/mailers/notifications_mailer.rb
Normal file
22
app/mailers/notifications_mailer.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
class NotificationsMailer < ApplicationMailer
|
||||
helper :application
|
||||
|
||||
before_action do
|
||||
@notification = params.fetch(:notification)
|
||||
@check = @notification.check
|
||||
end
|
||||
|
||||
default to: -> { @notification.recipient }
|
||||
|
||||
def domain_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 domain_recurrent_failures
|
||||
subject = t(".subject", domain: @check.domain)
|
||||
mail subject: subject
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@
|
|||
# comment :string(255)
|
||||
# domain :string(255) not null
|
||||
# domain_created_at :datetime
|
||||
# domain_expire_at :datetime
|
||||
# domain_expires_at :datetime
|
||||
# domain_updated_at :datetime
|
||||
# kind :integer not null
|
||||
# last_run_at :datetime
|
||||
|
@ -29,13 +29,17 @@
|
|||
class Check < ApplicationRecord
|
||||
belongs_to :user
|
||||
has_many :logs, class_name: "CheckLog"
|
||||
has_many :notifications, validate: true, dependent: :destroy
|
||||
accepts_nested_attributes_for :notifications,
|
||||
allow_destroy: true,
|
||||
reject_if: lambda { |at| at["recipient"].blank? && at["delay"].blank? }
|
||||
|
||||
enum kind: [:domain, :ssl]
|
||||
|
||||
self.skip_time_zone_conversion_for_attributes = [
|
||||
:domain_created_at,
|
||||
:domain_updated_at,
|
||||
:domain_expire_at,
|
||||
:domain_expires_at,
|
||||
]
|
||||
|
||||
validates :kind, presence: true
|
||||
|
@ -45,9 +49,10 @@ class Check < ApplicationRecord
|
|||
validates :comment, length: { maximum: 255 }
|
||||
validates :vendor, length: { maximum: 255 }
|
||||
|
||||
after_update :reset_notifications
|
||||
after_save :enqueue_sync
|
||||
|
||||
protected
|
||||
private
|
||||
|
||||
def domain_created_at_past
|
||||
errors.add(:domain_created_at, :past) if domain_created_at.present? && domain_created_at.future?
|
||||
|
@ -63,4 +68,10 @@ class Check < ApplicationRecord
|
|||
|
||||
WhoisSyncJob.perform_later(id) if domain?
|
||||
end
|
||||
|
||||
def reset_notifications
|
||||
return unless (saved_changes.keys & %w[domain domain_expires_at]).present?
|
||||
|
||||
notifications.each(&:reset!)
|
||||
end
|
||||
end
|
||||
|
|
44
app/models/notification.rb
Normal file
44
app/models/notification.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: notifications
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# channel :integer default("email"), not null
|
||||
# delay :integer not null
|
||||
# recipient :string(255) not null
|
||||
# sent_at :datetime
|
||||
# status :integer default("pending"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# check_id :bigint(8)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_notifications_on_check_id (check_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (check_id => checks.id)
|
||||
#
|
||||
|
||||
class Notification < ApplicationRecord
|
||||
belongs_to :check
|
||||
|
||||
enum channel: [:email]
|
||||
enum status: [:pending, :ongoing, :succeed, :failed]
|
||||
|
||||
validates :channel, presence: true
|
||||
validates :delay, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
|
||||
validates :recipient, presence: true
|
||||
|
||||
def pending!
|
||||
self.sent_at = nil
|
||||
super
|
||||
end
|
||||
alias reset! pending!
|
||||
|
||||
def ongoing!
|
||||
self.sent_at = Time.now
|
||||
super
|
||||
end
|
||||
end
|
|
@ -37,4 +37,6 @@ class User < ApplicationRecord
|
|||
|
||||
has_many :checks
|
||||
validates :tos_accepted, acceptance: true
|
||||
|
||||
scope :notifications_disabled, -> { where(notifications_enabled: false) }
|
||||
end
|
||||
|
|
21
app/policies/notification_policy.rb
Normal file
21
app/policies/notification_policy.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class NotificationPolicy < ApplicationPolicy
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
scope.joins(:check).where(checks: { user: user })
|
||||
end
|
||||
end
|
||||
|
||||
def destroy?
|
||||
check_owner?
|
||||
end
|
||||
|
||||
def show?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_owner?
|
||||
record.check.user == user
|
||||
end
|
||||
end
|
9
app/services/notifier.rb
Normal file
9
app/services/notifier.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Notifier
|
||||
class << self
|
||||
def process_all(configuration = nil)
|
||||
processor = Processor.new(configuration)
|
||||
processor.process_expires_soon
|
||||
processor.process_recurrent_failures
|
||||
end
|
||||
end
|
||||
end
|
41
app/services/notifier/channels/base.rb
Normal file
41
app/services/notifier/channels/base.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
module Notifier
|
||||
module Channels
|
||||
class Base
|
||||
def notify(reason, notification) # rubocop:disable Metrics/MethodLength
|
||||
return unless supports?(reason, notification)
|
||||
|
||||
notification.ongoing!
|
||||
|
||||
case [notification.check.kind.to_sym, reason]
|
||||
when [:domain, :expires_soon]
|
||||
domain_notify_expires_soon(notification)
|
||||
when [:domain, :recurrent_failures]
|
||||
domain_notify_recurrent_failures(notification)
|
||||
else
|
||||
fail ArgumentError,
|
||||
"Invalid notification reason `#{reason}` for check kind `#{notification.check.kind}`."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# :nocov:
|
||||
def supports?(_reason, _notification)
|
||||
fail NotImplementedError,
|
||||
"#{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__}"
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
end
|
21
app/services/notifier/channels/email.rb
Normal file
21
app/services/notifier/channels/email.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
module Notifier
|
||||
module Channels
|
||||
class Email < Base
|
||||
REASONS = %i[expires_soon recurrent_failures].freeze
|
||||
|
||||
protected
|
||||
|
||||
def supports?(reason, _notification)
|
||||
REASONS.include?(reason)
|
||||
end
|
||||
|
||||
def domain_notify_expires_soon(notification)
|
||||
NotificationsMailer.with(notification: notification).domain_expires_soon.deliver_now
|
||||
end
|
||||
|
||||
def domain_notify_recurrent_failures(notification)
|
||||
NotificationsMailer.with(notification: notification).domain_recurrent_failures.deliver_now
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
56
app/services/notifier/processor.rb
Normal file
56
app/services/notifier/processor.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
module Notifier
|
||||
Configuration = Struct.new(:interval, :failure_days)
|
||||
|
||||
class Processor
|
||||
attr_reader :configuration
|
||||
attr_reader :channels
|
||||
attr_reader :resolver
|
||||
|
||||
def initialize(configuration = nil)
|
||||
@configuration = configuration || default_configuration
|
||||
|
||||
@resolver = Resolver.new
|
||||
@channels = {
|
||||
email: Channels::Email.new,
|
||||
}
|
||||
end
|
||||
|
||||
def process_expires_soon
|
||||
resolver.resolve_expires_soon.find_each do |notification|
|
||||
notifier_channel_for(notification).notify(:expires_soon, notification)
|
||||
|
||||
sleep configuration.interval
|
||||
end
|
||||
end
|
||||
|
||||
def process_recurrent_failures
|
||||
resolver.resolve_check_failed.find_each do |notification|
|
||||
next unless should_notify_for_recurrent_failures?(notification)
|
||||
|
||||
notifier_channel_for(notification).notify(:recurrent_failures, notification)
|
||||
|
||||
sleep configuration.interval
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_configuration
|
||||
config = Rails.configuration.chexpire.fetch("notifier", {})
|
||||
|
||||
Configuration.new(
|
||||
config.fetch("interval") { 0.00 },
|
||||
config.fetch("failures_days") { 3 },
|
||||
)
|
||||
end
|
||||
|
||||
def notifier_channel_for(notification)
|
||||
channels.fetch(notification.channel.to_sym)
|
||||
end
|
||||
|
||||
def should_notify_for_recurrent_failures?(_notification)
|
||||
true
|
||||
# TODO: dependent of logs consecutive failures
|
||||
end
|
||||
end
|
||||
end
|
31
app/services/notifier/resolver.rb
Normal file
31
app/services/notifier/resolver.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
module Notifier
|
||||
class Resolver
|
||||
def resolve_expires_soon
|
||||
scope
|
||||
.where("checks.domain_expires_at >= CURDATE()")
|
||||
.where("DATE(checks.domain_expires_at)
|
||||
<= DATE_ADD(CURDATE(), INTERVAL notifications.delay DAY)")
|
||||
end
|
||||
|
||||
def resolve_check_failed
|
||||
# Only gets here the checks having its last run in error
|
||||
# Logical rules are in plain ruby inside processor
|
||||
scope
|
||||
.includes(check: :logs)
|
||||
.where("checks.last_success_at <= DATE_SUB(checks.last_run_at, INTERVAL 5 MINUTE)")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope
|
||||
Notification
|
||||
.includes(:check)
|
||||
.where(status: [:pending, :failed], checks: { active: true })
|
||||
.where.not(checks: { user: ignore_users })
|
||||
end
|
||||
|
||||
def ignore_users
|
||||
@ignore_users ||= User.notifications_disabled.pluck(:id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,6 +34,7 @@ class SystemCommand
|
|||
|
||||
private
|
||||
|
||||
# :nocov:
|
||||
def call(cmd)
|
||||
pid, _, stdout, stderr = Open4.popen4 cmd
|
||||
_, status = Process.waitpid2 pid
|
||||
|
@ -45,6 +46,7 @@ class SystemCommand
|
|||
stderr.read.strip,
|
||||
)
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
def escape_arg(arg)
|
||||
arg.to_s.gsub('"') { '\"' }
|
||||
|
|
|
@ -12,5 +12,18 @@
|
|||
<%= f.input :active %>
|
||||
<% end %>
|
||||
|
||||
<%= f.button :submit, "Validate", class: "btn-primary" %>
|
||||
<h2 class="mt-5"><%= t(".notifications") %></h2>
|
||||
<p class="alert alert-light"><%= t(".notifications_hint") %></p>
|
||||
|
||||
<%- check.notifications.each_with_index do |notification, index| %>
|
||||
<div data-notification-id="<%= notification.id %>">
|
||||
<%= f.fields_for :notifications, notification do |nf| %>
|
||||
<%= render "notifications/nested_form_headers", f: nf if index.zero? %>
|
||||
<%= render "notifications/nested_form", f: nf, check: check %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<%= f.button :submit, class: "btn-primary mt-5" %>
|
||||
<% end %>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div>Kind: <%= check.kind %></div>
|
||||
<div>Created date: <%= l(check.domain_created_at.to_date) if check.domain_created_at.present? %></div>
|
||||
<div>Update date: <%= l(check.domain_updated_at.to_date) if check.domain_updated_at.present? %></div>
|
||||
<div>Expire date: <%= l(check.domain_expire_at.to_date) if check.domain_expire_at.present? %></div>
|
||||
<div>Expire date: <%= l(check.domain_expires_at.to_date) if check.domain_expires_at.present? %></div>
|
||||
|
||||
<% if check.comment? %>
|
||||
<div>Comment: <%= check.comment %></div>
|
||||
|
|
29
app/views/notifications/_nested_form.html.erb
Normal file
29
app/views/notifications/_nested_form.html.erb
Normal file
|
@ -0,0 +1,29 @@
|
|||
<fieldset class="form-group">
|
||||
<div class="form-row">
|
||||
<%- if many_channels_available? %>
|
||||
<div class="form-group col-md-2">
|
||||
<%- if f.object.new_record? -%>
|
||||
<%= f.input :channel, collection: Notification.channels.keys, label: false %>
|
||||
<% else -%>
|
||||
<%= f.input_field :channel, as: :string, readonly: true, class: "form-control-plaintext" %>
|
||||
<%- end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-group <%= recipient_col_class %>">
|
||||
<%= f.input :recipient, as: :email, label: false %>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<%= f.input :delay, as: :integer, label: false %>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-1">
|
||||
<% if f.object.persisted? %>
|
||||
<%= link_to check_notification_path(check, f.object), method: :delete, remote: true, class: "btn btn-danger" do %>
|
||||
<%== Octicons::Octicon.new("x", width: 15, height: 20).to_svg %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
15
app/views/notifications/_nested_form_headers.html.erb
Normal file
15
app/views/notifications/_nested_form_headers.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<div class="form-row">
|
||||
<%- if many_channels_available? %>
|
||||
<div class="col-md-2">
|
||||
<%= f.label :channel %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="<%= recipient_col_class %>">
|
||||
<%= f.label :recipient %>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<%= f.label :delay %>
|
||||
</div>
|
||||
</div>
|
1
app/views/notifications/destroy.js.erb
Normal file
1
app/views/notifications/destroy.js.erb
Normal file
|
@ -0,0 +1 @@
|
|||
document.querySelector("[data-notification-id='<%= @notification.id %>']").remove();
|
|
@ -0,0 +1,13 @@
|
|||
<%- if @check.comment.present? -%>
|
||||
<p>
|
||||
You wrote the following comment with this domain:
|
||||
|
||||
<blockquote>
|
||||
<%= @check.comment -%>
|
||||
</blockquote>
|
||||
</p>
|
||||
<%- end -%>
|
||||
|
||||
<%- if @check.vendor.present? -%>
|
||||
Vendor: <%= @check.vendor %>
|
||||
<% end %>
|
|
@ -0,0 +1,11 @@
|
|||
<%- if @check.comment.present? -%>
|
||||
You wrote the following comment with this domain:
|
||||
|
||||
<%= @check.comment -%>
|
||||
<%- end -%>
|
||||
|
||||
|
||||
|
||||
<%- if @check.vendor.present? -%>
|
||||
Vendor: <%= @check.vendor %>
|
||||
<% end %>
|
|
@ -0,0 +1,11 @@
|
|||
<br />
|
||||
<br />
|
||||
--
|
||||
|
||||
<p>
|
||||
You received this email because of the notification <%= delay %> days before the expiry date.<br />
|
||||
You can handle the notifications for this check by following this link:<br />
|
||||
<%= link_to nil, edit_check_url(check) %>
|
||||
</p>
|
||||
|
||||
<p>The Chexpire Team</p>
|
|
@ -0,0 +1,7 @@
|
|||
--
|
||||
|
||||
You received this email because of the notification <%= delay %> days before the expiry date.
|
||||
You can handle the notifications for this check by following this link:
|
||||
<%= edit_check_url(check) %>
|
||||
|
||||
The Chexpire Team
|
|
@ -0,0 +1,11 @@
|
|||
<br />
|
||||
<br />
|
||||
--
|
||||
|
||||
<p>You received this email because of the notification <%= delay %> days before
|
||||
the last known expiry date.<br />
|
||||
You can handle the check by following this link: <br />
|
||||
<%= link_to nil, edit_check_url(check) %>
|
||||
</p>
|
||||
|
||||
<p>The Chexpire Team</p>
|
|
@ -0,0 +1,8 @@
|
|||
--
|
||||
|
||||
You received this email because of the notification <%= delay %> days before
|
||||
the last known expiry date.
|
||||
You can handle the check by following this link:
|
||||
<%= edit_check_url(check) %>
|
||||
|
||||
The Chexpire Team
|
|
@ -0,0 +1,12 @@
|
|||
<p>
|
||||
Hi,
|
||||
<br />
|
||||
<br />
|
||||
the domain <strong><%= @check.domain %></strong> will expire
|
||||
<strong><%= format_utc(@check.domain_expires_at) %></strong>.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<%= render "check_comment_vendor" %>
|
||||
|
||||
<%= render "footer_expires_soon", delay: @notification.delay, check: @check %>
|
|
@ -0,0 +1,9 @@
|
|||
Hi,
|
||||
|
||||
the domain <%= @check.domain %> will expire <%= format_utc(@check.domain_expires_at) %>.
|
||||
|
||||
|
||||
<%= render "check_comment_vendor" %>
|
||||
|
||||
|
||||
<%= render "footer_expires_soon", delay: @notification.delay, check: @check %>
|
|
@ -0,0 +1,25 @@
|
|||
<p>
|
||||
Hi,
|
||||
<br />
|
||||
<br />
|
||||
|
||||
We had <strong>recurrent failures</strong> while checking the whois database for domain
|
||||
<strong><%= @check.domain %></strong>. As of today, we can't anymore verify the expiry date.
|
||||
</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) %>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have deleted the domain or have not renewed it, 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", delay: @notification.delay, check: @check %>
|
|
@ -0,0 +1,28 @@
|
|||
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.
|
||||
|
||||
The last known expiry date is <%= format_utc(@check.domain_expires_at) %>.
|
||||
The last successful check occured <%= format_utc(@check.last_success_at) %>.
|
||||
|
||||
If you have deleted your domain or have not renewed it, please disable
|
||||
or delete the check by following this link:
|
||||
|
||||
<%= edit_check_url(@check) %>
|
||||
|
||||
|
||||
<%- if @check.comment.present? -%>
|
||||
You wrote the following comment with this domain:
|
||||
|
||||
<%= @check.comment -%>
|
||||
<%- end -%>
|
||||
|
||||
|
||||
|
||||
<%- if @check.vendor.present? -%>
|
||||
Vendor: <%= @check.vendor %>
|
||||
<% end %>
|
||||
|
||||
|
||||
<%= render "footer_recurrent_failures", delay: @notification.delay, check: @check %>
|
|
@ -1,5 +1,8 @@
|
|||
default: &default
|
||||
mailer_default_from: "from@example.org"
|
||||
notifier:
|
||||
interval: 0.00
|
||||
failure_days: 3
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
test:
|
||||
mailer_default_from: "contact@chexpire.org"
|
||||
host: "localhost"
|
||||
notifier:
|
||||
interval: 0.00
|
||||
failure_days: 3
|
||||
|
|
|
@ -67,6 +67,4 @@ Rails.application.configure do
|
|||
# Use an evented file watcher to asynchronously detect changes in source code,
|
||||
# routes, locales, etc. This feature depends on the listen gem.
|
||||
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||
|
||||
config.active_job.queue_adapter = :inline
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ Rails.application.config.content_security_policy do |policy|
|
|||
end
|
||||
|
||||
# If you are using UJS then enable automatic nonce generation
|
||||
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
|
||||
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
|
||||
|
||||
# Report CSP violations to a specified URI
|
||||
# For further information see the following documentation:
|
||||
|
|
|
@ -17,8 +17,25 @@ en:
|
|||
new:
|
||||
tos_acceptance_html: "You must accept our Terms of service"
|
||||
|
||||
simple_form:
|
||||
placeholders:
|
||||
notifications:
|
||||
recipient: john@example.org
|
||||
|
||||
flashes:
|
||||
user_not_authorized: "You are not authorized to access to this resource."
|
||||
|
||||
notifications_mailer:
|
||||
domain_expires_soon:
|
||||
subject:
|
||||
zero: "Domain %{domain} expires TODAY !"
|
||||
one: "Domain %{domain} expires TOMORROW !"
|
||||
other: "Domain %{domain} expires in %{count} days"
|
||||
|
||||
domain_recurrent_failures:
|
||||
subject: "Recurrent failures in %{domain} domain expiry check"
|
||||
|
||||
|
||||
shared:
|
||||
navbar:
|
||||
sign_up: "Sign up"
|
||||
|
@ -32,3 +49,7 @@ en:
|
|||
You have not set up a check yet.
|
||||
Please add a <a href="%{new_domain_path}">domain</a>
|
||||
or a <a href="%{new_ssl_path}">ssl</a> !
|
||||
form:
|
||||
notifications_hint: |
|
||||
Receive notifications to warn you when our system detects that the
|
||||
expiration date is coming. The delay is set in number of days.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# == Route Map
|
||||
#
|
||||
# Prefix Verb URI Pattern Controller#Action
|
||||
# check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy
|
||||
# checks GET /checks(.:format) checks#index
|
||||
# POST /checks(.:format) checks#create
|
||||
# new_check GET /checks/new(.:format) checks#new
|
||||
|
@ -46,7 +47,9 @@
|
|||
Rails.application.routes.draw do
|
||||
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
|
||||
|
||||
resources :checks, except: [:show]
|
||||
resources :checks, except: [:show] do
|
||||
resources :notifications, only: [:destroy]
|
||||
end
|
||||
|
||||
devise_for :users
|
||||
root to: "pages#home"
|
||||
|
|
14
db/migrate/20180531101412_create_notifications.rb
Normal file
14
db/migrate/20180531101412_create_notifications.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class CreateNotifications < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :notifications do |t|
|
||||
t.references :check, foreign_key: true
|
||||
t.integer :channel, null: false, default: 0
|
||||
t.string :recipient, null: false
|
||||
t.integer :delay, null: false
|
||||
t.integer :status, null: false, default: 0
|
||||
t.datetime :sent_at
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class RenameChecksDomainExpireAtToDomainExpiresAt < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
rename_column :checks, :domain_expire_at, :domain_expires_at
|
||||
end
|
||||
end
|
17
db/schema.rb
17
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2018_05_30_123611) do
|
||||
ActiveRecord::Schema.define(version: 2018_06_02_154319) do
|
||||
|
||||
create_table "check_logs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
|
||||
t.bigint "check_id"
|
||||
|
@ -30,7 +30,7 @@ ActiveRecord::Schema.define(version: 2018_05_30_123611) do
|
|||
t.string "domain", null: false
|
||||
t.datetime "domain_created_at"
|
||||
t.datetime "domain_updated_at"
|
||||
t.datetime "domain_expire_at"
|
||||
t.datetime "domain_expires_at"
|
||||
t.datetime "last_run_at"
|
||||
t.datetime "last_success_at"
|
||||
t.string "vendor"
|
||||
|
@ -41,6 +41,18 @@ ActiveRecord::Schema.define(version: 2018_05_30_123611) do
|
|||
t.index ["user_id"], name: "index_checks_on_user_id"
|
||||
end
|
||||
|
||||
create_table "notifications", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
|
||||
t.bigint "check_id"
|
||||
t.integer "channel", default: 0, null: false
|
||||
t.string "recipient", null: false
|
||||
t.integer "delay", null: false
|
||||
t.integer "status", default: 0, null: false
|
||||
t.datetime "sent_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["check_id"], name: "index_notifications_on_check_id"
|
||||
end
|
||||
|
||||
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
|
@ -67,4 +79,5 @@ ActiveRecord::Schema.define(version: 2018_05_30_123611) do
|
|||
|
||||
add_foreign_key "check_logs", "checks"
|
||||
add_foreign_key "checks", "users"
|
||||
add_foreign_key "notifications", "checks"
|
||||
end
|
||||
|
|
64
db/seeds.rb
64
db/seeds.rb
|
@ -1,7 +1,57 @@
|
|||
# This file should contain all the record creation needed to seed the database with its default values.
|
||||
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
|
||||
# Character.create(name: 'Luke', movie: movies.first)
|
||||
Notification.destroy_all
|
||||
Check.destroy_all
|
||||
User.destroy_all
|
||||
|
||||
user1 = User.create!(
|
||||
email: "colin@example.org",
|
||||
password: "password",
|
||||
tos_accepted: true,
|
||||
confirmed_at: Time.now
|
||||
)
|
||||
|
||||
check_chexpire_org = Check.create!(
|
||||
user: user1,
|
||||
kind: :domain,
|
||||
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",
|
||||
)
|
||||
|
||||
check_chexpire_org_error = Check.create!(
|
||||
user: user1,
|
||||
kind: :domain,
|
||||
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,
|
||||
delay: 15,
|
||||
channel: :email,
|
||||
recipient: "colin@example.org",
|
||||
status: :pending,
|
||||
)
|
||||
|
||||
Notification.create!(
|
||||
check: check_chexpire_org_error,
|
||||
delay: 15,
|
||||
channel: :email,
|
||||
recipient: "colin@example.org",
|
||||
status: :pending,
|
||||
)
|
||||
|
||||
puts "\e[0;32mDone 👌\e[0m"
|
||||
puts " "
|
||||
puts "--------------------"
|
||||
puts "Users: #{User.count}"
|
||||
puts "Checks: #{Check.count}"
|
||||
puts "Notifications: #{Notification.count}"
|
||||
|
|
1
lib/errors.rb
Normal file
1
lib/errors.rb
Normal file
|
@ -0,0 +1 @@
|
|||
class SystemCommandNotAllowedError < StandardError; end
|
13
lib/tasks/factory_bot.rake
Normal file
13
lib/tasks/factory_bot.rake
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace :factory_bot do
|
||||
desc "Verify that all FactoryBot factories are valid"
|
||||
task lint: :environment do
|
||||
if Rails.env.test?
|
||||
DatabaseCleaner.cleaning do
|
||||
FactoryBot.lint
|
||||
end
|
||||
else
|
||||
system("bundle exec rake factory_bot:lint RAILS_ENV='test'")
|
||||
fail if $CHILD_STATUS.exitstatus.nonzero?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,4 +2,9 @@ require "test_helper"
|
|||
|
||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
driven_by :headless_chrome
|
||||
|
||||
def teardown
|
||||
Capybara.reset_sessions!
|
||||
Warden.test_reset!
|
||||
end
|
||||
end
|
||||
|
|
23
test/chexpire_assertions.rb
Normal file
23
test/chexpire_assertions.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
module ChexpireAssertions
|
||||
def assert_just_now(expected)
|
||||
assert_in_delta expected.to_i, Time.now.to_i, 1.0
|
||||
end
|
||||
|
||||
def assert_permit(user, record, action)
|
||||
msg = "User #{user.inspect} should be permitted to #{action} #{record}, but isn't permitted"
|
||||
assert policy_permit(user, record, action), msg
|
||||
end
|
||||
|
||||
def refute_permit(user, record, action)
|
||||
msg = "User #{user.inspect} should NOT be permitted to #{action} #{record}, but is permitted"
|
||||
refute policy_permit(user, record, action), msg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def policy_permit(user, record, action)
|
||||
test_name = self.class.ancestors.select { |a| a.to_s.match(/PolicyTest/) }.first
|
||||
klass = test_name.to_s.gsub(/Test/, "")
|
||||
klass.constantize.new(user, record).public_send("#{action}?")
|
||||
end
|
||||
end
|
7
test/controllers/notifications_controller_test.rb
Normal file
7
test/controllers/notifications_controller_test.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class NotificationsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
7
test/factories/.rubocop.yml
Normal file
7
test/factories/.rubocop.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
inherit_from: ../../.rubocop.yml
|
||||
|
||||
Style/BlockDelimiters:
|
||||
EnforcedStyle: line_count_based
|
||||
|
||||
Metrics/BlockLength:
|
||||
Enabled: false
|
|
@ -21,4 +21,12 @@
|
|||
# fk_rails_... (check_id => checks.id)
|
||||
#
|
||||
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
FactoryBot.define do
|
||||
factory :check_log do
|
||||
check
|
||||
status :pending
|
||||
exit_status nil
|
||||
parsed_response nil
|
||||
raw_response nil
|
||||
end
|
||||
end
|
77
test/factories/checks.rb
Normal file
77
test/factories/checks.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: checks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# active :boolean default(TRUE), not null
|
||||
# comment :string(255)
|
||||
# domain :string(255) not null
|
||||
# domain_created_at :datetime
|
||||
# domain_expires_at :datetime
|
||||
# domain_updated_at :datetime
|
||||
# kind :integer not null
|
||||
# last_run_at :datetime
|
||||
# last_success_at :datetime
|
||||
# vendor :string(255)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :bigint(8)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_checks_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
|
||||
FactoryBot.define do
|
||||
factory :check do
|
||||
user
|
||||
kind :domain
|
||||
domain "domain.fr"
|
||||
domain_created_at Time.new(2016, 4, 1, 12, 0, 0, "+02:00")
|
||||
domain_updated_at Time.new(2017, 3, 1, 12, 0, 0, "+02:00")
|
||||
domain_expires_at Time.new(2019, 4, 1, 12, 0, 0, "+02:00")
|
||||
active true
|
||||
vendor nil
|
||||
comment nil
|
||||
last_run_at nil
|
||||
last_success_at nil
|
||||
|
||||
trait :domain do
|
||||
kind :domain
|
||||
end
|
||||
|
||||
trait :nil_dates do
|
||||
domain_created_at nil
|
||||
domain_updated_at nil
|
||||
domain_expires_at nil
|
||||
end
|
||||
|
||||
trait :expires_next_week do
|
||||
domain_expires_at 1.week.from_now
|
||||
end
|
||||
|
||||
trait :last_runs_failed do
|
||||
last_run_at Time.now - 90.minutes
|
||||
last_success_at 1.week.ago - 2.hours
|
||||
end
|
||||
|
||||
trait :last_run_succeed do
|
||||
last_run_at 1.hour.ago
|
||||
last_success_at 1.hour.ago
|
||||
end
|
||||
|
||||
trait :inactive do
|
||||
active false
|
||||
end
|
||||
|
||||
trait :with_notifications do
|
||||
after :create do |check|
|
||||
create_list :notification, 2, check: check
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
50
test/factories/notifications.rb
Normal file
50
test/factories/notifications.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: notifications
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# channel :integer default("email"), not null
|
||||
# delay :integer not null
|
||||
# recipient :string(255) not null
|
||||
# sent_at :datetime
|
||||
# status :integer default("pending"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# check_id :bigint(8)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_notifications_on_check_id (check_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (check_id => checks.id)
|
||||
#
|
||||
|
||||
FactoryBot.define do
|
||||
factory :notification do
|
||||
check
|
||||
delay 30
|
||||
channel :email
|
||||
recipient "recipient@domain.fr"
|
||||
status :pending
|
||||
sent_at nil
|
||||
|
||||
trait :email do
|
||||
channel :email
|
||||
end
|
||||
|
||||
trait :ongoing do
|
||||
status :ongoing
|
||||
end
|
||||
|
||||
trait :succeed do
|
||||
status :succeed
|
||||
sent_at { 1.day.ago }
|
||||
end
|
||||
|
||||
trait :failed do
|
||||
status :failed
|
||||
end
|
||||
end
|
||||
end
|
|
@ -28,15 +28,14 @@
|
|||
# index_users_on_email (email) UNIQUE
|
||||
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
||||
#
|
||||
require "securerandom"
|
||||
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# This model initially had no columns defined. If you add columns to the
|
||||
# model remove the '{}' from the fixture names and add the columns immediately
|
||||
# below each fixture, per the syntax in the comments below
|
||||
#
|
||||
user1:
|
||||
email: user@chexpire.org
|
||||
encrypted_password: <%= User.new.send(:password_digest, 'password') %>
|
||||
confirmed_at: <%= 1.minute.ago %>
|
||||
tos_accepted: true
|
||||
FactoryBot.define do
|
||||
factory :user do
|
||||
sequence(:email) { |n| "user-#{n}@chexpire.org" }
|
||||
password "password"
|
||||
confirmed_at Time.new(2018, 4, 1, 12, 0, 0, "+02:00")
|
||||
notifications_enabled true
|
||||
tos_accepted true
|
||||
end
|
||||
end
|
55
test/fixtures/checks.yml
vendored
55
test/fixtures/checks.yml
vendored
|
@ -1,55 +0,0 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: checks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# active :boolean default(TRUE), not null
|
||||
# comment :string(255)
|
||||
# domain :string(255) not null
|
||||
# domain_created_at :datetime
|
||||
# domain_expire_at :datetime
|
||||
# domain_updated_at :datetime
|
||||
# kind :integer not null
|
||||
# last_run_at :datetime
|
||||
# last_success_at :datetime
|
||||
# vendor :string(255)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :bigint(8)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_checks_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
domain_example_org:
|
||||
user: user1
|
||||
kind: domain
|
||||
domain: example.org
|
||||
domain_created_at: 2017-03-01 17:29:50
|
||||
domain_updated_at: 2018-02-15 12:10:00
|
||||
domain_expire_at: 2019-03-01 17:29:49
|
||||
last_run_at: 2018-05-24 17:29:50
|
||||
last_success_at: 2018-05-24 17:29:50
|
||||
vendor: ""
|
||||
comment: ""
|
||||
active: true
|
||||
|
||||
ssl_www_example_org:
|
||||
user: user1
|
||||
kind: ssl
|
||||
domain: www.example.org
|
||||
domain_created_at: 2018-05-24 17:29:50
|
||||
domain_updated_at: 2018-05-24 17:29:50
|
||||
domain_expire_at: 2019-05-24 17:29:49
|
||||
last_run_at: 2018-05-24 17:29:50
|
||||
last_success_at: 2018-05-24 17:29:50
|
||||
vendor: ""
|
||||
comment: "MyString"
|
||||
active: true
|
|
@ -1,7 +1,58 @@
|
|||
require "test_helper"
|
||||
|
||||
class WhoisSyncJobTest < ActiveJob::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
test "calls whois database and update check with the response (domain.fr)" do
|
||||
domain = "domain.fr"
|
||||
check = create(:check, :nil_dates, domain: domain)
|
||||
|
||||
mock_system_command("whois", domain, stdout: whois_response(domain)) do
|
||||
WhoisSyncJob.new.perform(check.id)
|
||||
end
|
||||
|
||||
check.reload
|
||||
|
||||
assert_just_now check.last_run_at
|
||||
assert_just_now check.last_success_at
|
||||
assert_equal Time.new(2019, 2, 17, 0, 0, 0, 0), check.domain_expires_at
|
||||
assert_equal Time.new(2017, 1, 28, 0, 0, 0, 0), check.domain_updated_at
|
||||
assert_equal Time.new(2004, 2, 18, 0, 0, 0, 0), check.domain_created_at
|
||||
assert check.active?
|
||||
end
|
||||
|
||||
test "ignore invalid response (domain.fr)" do
|
||||
check = create(:check, :nil_dates, domain: "domain.fr")
|
||||
original_updated_at = check.updated_at
|
||||
|
||||
mock_system_command("whois", "domain.fr", stdout: "not a response") do
|
||||
WhoisSyncJob.new.perform(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 "Disable check when whois responds domain not found" do
|
||||
domain = "willneverexist.fr"
|
||||
check = create(:check, :nil_dates, domain: domain)
|
||||
|
||||
mock_system_command("whois", domain, stdout: whois_response(domain)) do
|
||||
WhoisSyncJob.new.perform(check.id)
|
||||
end
|
||||
|
||||
check.reload
|
||||
|
||||
refute check.active?
|
||||
assert_just_now check.last_run_at
|
||||
assert_nil check.last_success_at
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def whois_response(domain)
|
||||
file_fixture("whois/#{domain}.txt").read
|
||||
end
|
||||
end
|
||||
|
|
78
test/mailers/notifications_mailer_test.rb
Normal file
78
test/mailers/notifications_mailer_test.rb
Normal file
|
@ -0,0 +1,78 @@
|
|||
require "test_helper"
|
||||
|
||||
class NotificationsMailerTest < ActionMailer::TestCase
|
||||
test "domain_expires_soon" do
|
||||
check = create(:check, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
|
||||
notification = build(:notification, delay: 10, check: check, recipient: "colin@example.org")
|
||||
|
||||
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 "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.body.to_s, mail.html_part.to_s]
|
||||
|
||||
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 "domain_expires_soon include comment & vendor" do
|
||||
check = create(:check,
|
||||
domain_expires_at: 1.week.from_now,
|
||||
comment: "My comment",
|
||||
vendor: "The vendor")
|
||||
notification = build(:notification, check: check)
|
||||
|
||||
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
|
||||
|
||||
parts = [mail.text_part.body.to_s, mail.html_part.to_s]
|
||||
|
||||
parts.each do |part|
|
||||
assert_match "My comment", part
|
||||
assert_match "The vendor", part
|
||||
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")
|
||||
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)
|
||||
|
||||
mail = NotificationsMailer.with(notification: notification).domain_recurrent_failures
|
||||
assert_match "failures", 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.body.to_s, mail.html_part.to_s]
|
||||
|
||||
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
|
||||
end
|
13
test/mailers/previews/notifications_mailer_preview.rb
Normal file
13
test/mailers/previews/notifications_mailer_preview.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Preview all emails at http://localhost:3000/rails/mailers/notifications_mailer
|
||||
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
|
||||
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
|
||||
NotificationsMailer.with(notification: check.notifications.first).domain_recurrent_failures
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@
|
|||
# comment :string(255)
|
||||
# domain :string(255) not null
|
||||
# domain_created_at :datetime
|
||||
# domain_expire_at :datetime
|
||||
# domain_expires_at :datetime
|
||||
# domain_updated_at :datetime
|
||||
# kind :integer not null
|
||||
# last_run_at :datetime
|
||||
|
@ -29,7 +29,24 @@
|
|||
require "test_helper"
|
||||
|
||||
class CheckTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
test "notifications are resetted when domain expiration date has changed" do
|
||||
check = create(:check)
|
||||
notification = create(:notification, :succeed, check: check)
|
||||
|
||||
check.comment = "Will not reset because of this attribute"
|
||||
check.save!
|
||||
|
||||
notification.reload
|
||||
|
||||
assert notification.succeed?
|
||||
assert_not_nil notification.sent_at
|
||||
|
||||
check.domain_expires_at = 1.year.from_now
|
||||
check.save!
|
||||
|
||||
notification.reload
|
||||
|
||||
assert notification.pending?
|
||||
assert_nil notification.sent_at
|
||||
end
|
||||
end
|
||||
|
|
30
test/models/notification_test.rb
Normal file
30
test/models/notification_test.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: notifications
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# channel :integer default("email"), not null
|
||||
# delay :integer not null
|
||||
# recipient :string(255) not null
|
||||
# sent_at :datetime
|
||||
# status :integer default("pending"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# check_id :bigint(8)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_notifications_on_check_id (check_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (check_id => checks.id)
|
||||
#
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class NotificationTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,13 +1,40 @@
|
|||
require "test_helper"
|
||||
|
||||
class CheckPolicyTest < ActiveSupport::TestCase
|
||||
def test_scope; end
|
||||
setup do
|
||||
@owner, @other = create_list(:user, 2)
|
||||
@check = create(:check, user: @owner)
|
||||
end
|
||||
|
||||
def test_show; end
|
||||
test "create" do
|
||||
assert_permit @other, Check, :create
|
||||
assert_permit @other, Check, :new
|
||||
end
|
||||
|
||||
def test_create; end
|
||||
test "check owner" do
|
||||
assert_permit @owner, @check, :update
|
||||
assert_permit @owner, @check, :edit
|
||||
assert_permit @owner, @check, :destroy
|
||||
assert_permit @owner, @check, :show
|
||||
end
|
||||
|
||||
def test_update; end
|
||||
test "anonymous and other user" do
|
||||
refute_permit @other, @check, :update
|
||||
refute_permit @other, @check, :edit
|
||||
refute_permit @other, @check, :destroy
|
||||
refute_permit @other, @check, :show
|
||||
|
||||
def test_destroy; end
|
||||
refute_permit nil, @check, :update
|
||||
refute_permit nil, @check, :edit
|
||||
refute_permit nil, @check, :destroy
|
||||
refute_permit nil, @check, :show
|
||||
end
|
||||
|
||||
test "scope only to owner" do
|
||||
others = create_list(:check, 2, user: @other)
|
||||
|
||||
assert_empty Pundit.policy_scope!(nil, Check)
|
||||
assert_equal [@check], Pundit.policy_scope!(@owner, Check)
|
||||
assert_equal others, Pundit.policy_scope!(@other, Check)
|
||||
end
|
||||
end
|
||||
|
|
32
test/policies/notification_policy_test.rb
Normal file
32
test/policies/notification_policy_test.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
require "test_helper"
|
||||
|
||||
class NotificationPolicyTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@owner, @other = create_list(:user, 2)
|
||||
@notification = create(:notification, check: build(:check, user: @owner))
|
||||
end
|
||||
|
||||
test "permit to check user" do
|
||||
assert_permit @owner, @notification, :destroy
|
||||
end
|
||||
|
||||
test "disallow to anonymous and other user" do
|
||||
refute_permit @other, @notification, :destroy
|
||||
refute_permit nil, @notification, :destroy
|
||||
end
|
||||
|
||||
test "scope only to user checks" do
|
||||
other_notifications = create_list(:notification, 2, check: build(:check, user: @other))
|
||||
|
||||
assert_empty Pundit.policy_scope!(nil, Notification)
|
||||
assert_equal [@notification], Pundit.policy_scope!(@owner, Notification)
|
||||
assert_equal other_notifications, Pundit.policy_scope!(@other, Notification)
|
||||
end
|
||||
|
||||
test "disabled actions" do
|
||||
refute_permit @owner, @notification, :update
|
||||
refute_permit @owner, @notification, :edit
|
||||
refute_permit @owner, @notification, :create
|
||||
refute_permit @owner, @notification, :index
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ require "system_command"
|
|||
|
||||
class CheckLoggerTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@check = checks(:domain_example_org)
|
||||
@check = create(:check)
|
||||
@logger = CheckLogger.new(@check)
|
||||
end
|
||||
|
||||
|
|
49
test/services/notifier/channels/base_test.rb
Normal file
49
test/services/notifier/channels/base_test.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
require "test_helper"
|
||||
|
||||
module Notifier
|
||||
module Channels
|
||||
class BaseTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
class FakeChannel < Base
|
||||
def supports?(reason, _notification)
|
||||
reason != :unsupported
|
||||
end
|
||||
|
||||
def domain_notify_expires_soon(*); end
|
||||
end
|
||||
|
||||
@channel = FakeChannel.new
|
||||
end
|
||||
|
||||
test "#notify change the status of the notification" do
|
||||
notification = create(:notification)
|
||||
|
||||
@channel.notify(:expires_soon, notification)
|
||||
|
||||
notification.reload
|
||||
|
||||
assert notification.ongoing?
|
||||
assert_just_now notification.sent_at
|
||||
end
|
||||
|
||||
test "#notify raises an exception for a invalid reason" do
|
||||
notification = build(:notification)
|
||||
|
||||
assert_raises ArgumentError do
|
||||
@channel.notify(:unknown, notification)
|
||||
end
|
||||
end
|
||||
|
||||
test "#notify does nothing when channel does not support a reason" do
|
||||
notification = create(:notification)
|
||||
|
||||
@channel.notify(:unsupported, notification)
|
||||
|
||||
notification.reload
|
||||
|
||||
assert notification.pending?
|
||||
assert_nil notification.sent_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
54
test/services/notifier/processor_test.rb
Normal file
54
test/services/notifier/processor_test.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
require "test_helper"
|
||||
|
||||
module Notifier
|
||||
class ProcessorTest < ActiveSupport::TestCase
|
||||
test "#process_expires_soon sends an email for checks expiring soon" do
|
||||
create_list(:notification, 3, :email, check: build(:check, :expires_next_week))
|
||||
create(:notification, :email, check: build(:check, :nil_dates))
|
||||
create(:notification, :email, check: build(:check, :inactive))
|
||||
|
||||
processor = Processor.new
|
||||
assert_difference "ActionMailer::Base.deliveries.size", +3 do
|
||||
processor.process_expires_soon
|
||||
end
|
||||
|
||||
last_email = ActionMailer::Base.deliveries.last
|
||||
assert_match "expires in 7 days", last_email.subject
|
||||
end
|
||||
|
||||
test "#process_expires_soon respects the interval configuration between sends" do
|
||||
create_list(:notification, 3, :email, check: build(:check, :expires_next_week))
|
||||
test_interval_respected(:process_expires_soon, 3)
|
||||
end
|
||||
|
||||
test "#process_recurrent_failures respects the interval configuration between sends" do
|
||||
create_list(:notification, 3, :email, check: build(:check, :last_runs_failed))
|
||||
test_interval_respected(:process_recurrent_failures, 3)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def test_interval_respected(process_method, count_expected)
|
||||
configuration = Minitest::Mock.new
|
||||
count_expected.times do
|
||||
configuration.expect(:interval, 0.000001)
|
||||
end
|
||||
processor = Processor.new(configuration)
|
||||
|
||||
mock = Minitest::Mock.new
|
||||
assert_stub = lambda { |actual_time|
|
||||
assert_equal 0.000001, actual_time
|
||||
mock
|
||||
}
|
||||
|
||||
processor.stub :sleep, assert_stub do
|
||||
processor.public_send(process_method)
|
||||
end
|
||||
|
||||
configuration.verify
|
||||
mock.verify
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
end
|
113
test/services/notifier/resolver_test.rb
Normal file
113
test/services/notifier/resolver_test.rb
Normal file
|
@ -0,0 +1,113 @@
|
|||
require "test_helper"
|
||||
|
||||
module Notifier
|
||||
class ResolverTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@resolver = Notifier::Resolver.new
|
||||
end
|
||||
|
||||
test "#resolve_expires_soon ignores user having notification disabled" do
|
||||
n1 = create(:notification, check: build(:check, :expires_next_week))
|
||||
n1.check.user.update_attribute(:notifications_enabled, false)
|
||||
n2 = create(:notification, check: build(:check, :expires_next_week))
|
||||
|
||||
notifications = @resolver.resolve_expires_soon
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
end
|
||||
|
||||
test "#resolve_expires_soon ignores inactive checks" do
|
||||
n1 = create(:notification, check: build(:check, :expires_next_week, :inactive))
|
||||
n2 = create(:notification, check: build(:check, :expires_next_week))
|
||||
|
||||
notifications = @resolver.resolve_expires_soon
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
end
|
||||
|
||||
test "#resolve_expires_soon gets only checks inside delay" do
|
||||
n1 = create(:notification, check: build(:check, :expires_next_week), delay: 6)
|
||||
n2 = create(:notification, check: build(:check, :expires_next_week), delay: 7)
|
||||
|
||||
notifications = @resolver.resolve_expires_soon
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
end
|
||||
|
||||
test "#resolve_expires_soon can gets several notifications for a same check" do
|
||||
check = create(:check, :expires_next_week)
|
||||
n1 = create(:notification, check: check, delay: 3)
|
||||
n2 = create(:notification, check: check, delay: 10)
|
||||
n3 = create(:notification, check: check, delay: 30)
|
||||
|
||||
notifications = @resolver.resolve_expires_soon
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
assert_includes notifications, n3
|
||||
end
|
||||
|
||||
test "#resolve_expires_soon takes care of the status" do
|
||||
check = create(:check, :expires_next_week)
|
||||
n1 = create(:notification, check: check)
|
||||
n2 = create(:notification, :failed, check: check)
|
||||
n3 = create(:notification, :ongoing, check: check)
|
||||
n4 = create(:notification, :succeed, check: check)
|
||||
|
||||
notifications = @resolver.resolve_expires_soon
|
||||
|
||||
assert_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
assert_not_includes notifications, n3
|
||||
assert_not_includes notifications, n4
|
||||
end
|
||||
|
||||
test "#resolve_expires_soon ignores checks expired and without date" do
|
||||
n1 = create(:notification, check: build(:check, :expires_next_week))
|
||||
n2 = create(:notification, check: build(:check, domain_expires_at: 1.week.ago))
|
||||
n3 = create(:notification, check: build(:check, :nil_dates))
|
||||
|
||||
notifications = @resolver.resolve_expires_soon
|
||||
|
||||
assert_includes notifications, n1
|
||||
assert_not_includes notifications, n2
|
||||
assert_not_includes notifications, n3
|
||||
end
|
||||
|
||||
test "#resolve_check_failed ignores inactive checks" do
|
||||
n1 = create(:notification, check: build(:check, :last_runs_failed, :inactive))
|
||||
n2 = create(:notification, check: build(:check, :last_runs_failed))
|
||||
|
||||
notifications = @resolver.resolve_check_failed
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
end
|
||||
|
||||
test "#resolve_check_failed ignores user having notification disabled" do
|
||||
n1 = create(:notification, check: build(:check, :last_runs_failed))
|
||||
n1.check.user.update_attribute(:notifications_enabled, false)
|
||||
n2 = create(:notification, check: build(:check, :last_runs_failed))
|
||||
|
||||
notifications = @resolver.resolve_check_failed
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_includes notifications, n2
|
||||
end
|
||||
|
||||
test "#resolve_check_failed gets only checks having last run in error" do
|
||||
n1 = create(:notification, check: build(:check, :nil_dates))
|
||||
n2 = create(:notification, check: build(:check, :last_run_succeed))
|
||||
n3 = create(:notification, check: build(:check, :last_runs_failed))
|
||||
|
||||
notifications = @resolver.resolve_check_failed
|
||||
|
||||
assert_not_includes notifications, n1
|
||||
assert_not_includes notifications, n2
|
||||
assert_includes notifications, n3
|
||||
end
|
||||
end
|
||||
end
|
25
test/services/notifier_test.rb
Normal file
25
test/services/notifier_test.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
require "test_helper"
|
||||
require "notifier"
|
||||
|
||||
class NotifierTest < ActiveSupport::TestCase
|
||||
test "#process_all process expirable & failures notifications" do
|
||||
mock = Minitest::Mock.new
|
||||
mock.expect(:process_expires_soon, nil)
|
||||
mock.expect(:process_recurrent_failures, nil)
|
||||
|
||||
myconfig = "test config"
|
||||
|
||||
assert_stub = lambda { |actual_config|
|
||||
assert_equal myconfig, actual_config,
|
||||
"Processor was not initialized with the expected configuration"
|
||||
|
||||
mock
|
||||
}
|
||||
|
||||
Notifier::Processor.stub :new, assert_stub do
|
||||
Notifier.process_all(myconfig)
|
||||
end
|
||||
|
||||
mock.verify
|
||||
end
|
||||
end
|
85
test/system/checks_test.rb
Normal file
85
test/system/checks_test.rb
Normal file
|
@ -0,0 +1,85 @@
|
|||
require "application_system_test_case"
|
||||
|
||||
class ChecksTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
@user = create(:user)
|
||||
login_as(@user)
|
||||
|
||||
@check = create(:check, :with_notifications, user: @user)
|
||||
end
|
||||
|
||||
test "create a check and a notification" do
|
||||
visit new_check_path
|
||||
|
||||
domain = "domain-test.fr"
|
||||
fill_in("check[domain]", with: domain)
|
||||
choose "domain"
|
||||
|
||||
recipient = "recipient@example.org"
|
||||
fill_in("check[notifications_attributes][0][recipient]", with: recipient)
|
||||
fill_in("check[notifications_attributes][0][delay]", with: 30)
|
||||
|
||||
click_button
|
||||
|
||||
assert_equal checks_path, page.current_path
|
||||
|
||||
assert page.has_css?(".alert-success")
|
||||
assert page.has_content?(domain)
|
||||
|
||||
notification = Notification.last
|
||||
assert_equal recipient, notification.recipient
|
||||
assert_equal 30, notification.delay
|
||||
assert notification.email?
|
||||
assert notification.pending?
|
||||
end
|
||||
|
||||
test "remove a notification" do
|
||||
visit edit_check_path(@check)
|
||||
notification = @check.notifications.first
|
||||
|
||||
selector = "[data-notification-id=\"#{notification.id}\"]"
|
||||
|
||||
assert_difference "Notification.where(check_id: #{@check.id}).count", -1 do
|
||||
within selector do
|
||||
find(".btn-danger").click
|
||||
end
|
||||
|
||||
page.has_no_content?(selector)
|
||||
end
|
||||
end
|
||||
|
||||
test "update a check" do
|
||||
visit edit_check_path(@check)
|
||||
|
||||
fill_in "check[comment]", with: "My comment"
|
||||
|
||||
click_button "Update Check"
|
||||
|
||||
assert_equal checks_path, page.current_path
|
||||
|
||||
assert page.has_css?(".alert-success")
|
||||
assert page.has_content?("My comment")
|
||||
end
|
||||
|
||||
test "add a notification" do
|
||||
visit edit_check_path(@check)
|
||||
|
||||
recipient = "recipient2@example.org"
|
||||
fill_in("check[notifications_attributes][2][recipient]", with: recipient)
|
||||
fill_in("check[notifications_attributes][2][delay]", with: 55)
|
||||
|
||||
assert_difference "Notification.where(check_id: #{@check.id}).count", +1 do
|
||||
click_button "Update Check"
|
||||
|
||||
assert_equal checks_path, page.current_path
|
||||
end
|
||||
|
||||
assert page.has_css?(".alert-success")
|
||||
|
||||
notification = Notification.last
|
||||
assert_equal recipient, notification.recipient
|
||||
assert_equal 55, notification.delay
|
||||
assert notification.email?
|
||||
assert notification.pending?
|
||||
end
|
||||
end
|
|
@ -1,6 +1,10 @@
|
|||
require "application_system_test_case"
|
||||
|
||||
class UsersTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
@user = create(:user)
|
||||
end
|
||||
|
||||
test "an user can signup from the homepage and confirm its account" do
|
||||
visit root_path
|
||||
|
||||
|
@ -31,28 +35,25 @@ class UsersTest < ApplicationSystemTestCase
|
|||
end
|
||||
|
||||
test "an user can signin from the homepage" do
|
||||
user = users(:user1)
|
||||
visit root_path
|
||||
|
||||
click_on I18n.t("shared.navbar.sign_in")
|
||||
|
||||
fill_in "user[email]", with: user.email
|
||||
fill_in "user[email]", with: @user.email
|
||||
fill_in "user[password]", with: "password"
|
||||
|
||||
click_button I18n.t("devise.sessions.new.sign_in")
|
||||
|
||||
assert_equal root_path, page.current_path
|
||||
assert page.has_content?(user.email)
|
||||
assert page.has_content?(@user.email)
|
||||
end
|
||||
|
||||
test "an user can signout from the homepage" do
|
||||
user = users(:user1)
|
||||
|
||||
login_as user
|
||||
login_as @user
|
||||
visit root_path
|
||||
|
||||
find ".navbar" do
|
||||
click_on user.email
|
||||
click_on @user.email
|
||||
click_on I18n.t("shared.navbar.sign_out")
|
||||
end
|
||||
|
||||
|
@ -90,12 +91,11 @@ class UsersTest < ApplicationSystemTestCase
|
|||
end
|
||||
|
||||
test "an user can globally disable its notifications" do
|
||||
user = users(:user1)
|
||||
login_as user
|
||||
login_as @user
|
||||
|
||||
visit edit_user_registration_path
|
||||
|
||||
assert_equal user.email, find_field("user[email]").value
|
||||
assert_equal @user.email, find_field("user[email]").value
|
||||
|
||||
assert find_field("user[notifications_enabled]").value
|
||||
uncheck "user[notifications_enabled]"
|
||||
|
@ -104,7 +104,7 @@ class UsersTest < ApplicationSystemTestCase
|
|||
|
||||
click_button I18n.t("devise.registrations.edit.update")
|
||||
|
||||
user.reload
|
||||
refute user.notifications_enabled
|
||||
@user.reload
|
||||
refute @user.notifications_enabled
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
ENV["RAILS_ENV"] ||= "test"
|
||||
require "pry"
|
||||
|
||||
if !ENV["NO_COVERAGE"] && (ARGV.empty? || ARGV.include?("test/test_helper.rb"))
|
||||
require "simplecov"
|
||||
SimpleCov.start "rails" do
|
||||
add_group "Notifier", "app/services/notifier"
|
||||
add_group "Whois", "app/services/whois"
|
||||
add_group "Services", "app/services"
|
||||
add_group "Policies", "app/policies"
|
||||
end
|
||||
end
|
||||
|
||||
require_relative "../config/environment"
|
||||
require "rails/test_help"
|
||||
|
||||
require "minitest/mock"
|
||||
require_relative "test_mocks_helper"
|
||||
require_relative "chexpire_assertions"
|
||||
|
||||
class ActiveSupport::TestCase
|
||||
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
||||
|
@ -12,8 +26,13 @@ class ActiveSupport::TestCase
|
|||
Warden.test_mode!
|
||||
|
||||
# Add more helper methods to be used by all tests here...
|
||||
include ActiveJob::TestHelper
|
||||
include FactoryBot::Syntax::Methods
|
||||
include TestMocksHelper
|
||||
include ChexpireAssertions
|
||||
end
|
||||
|
||||
# Capybara configuration
|
||||
Capybara.register_driver :headless_chrome do |app|
|
||||
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
||||
"chromeOptions" => { args: %w[headless disable-gpu] + ["window-size=1280,800"] },
|
||||
|
@ -22,3 +41,21 @@ Capybara.register_driver :headless_chrome do |app|
|
|||
end
|
||||
Capybara.save_path = Rails.root.join("tmp/capybara")
|
||||
Capybara.javascript_driver = :headless_chrome
|
||||
|
||||
# Disable Open4 real system calls
|
||||
require "open4"
|
||||
require "errors"
|
||||
module Open4
|
||||
def popen4(*)
|
||||
fail SystemCommandNotAllowedError,
|
||||
"Real Open4 calls are disabled in test env. Use mock_system_command helper instead."
|
||||
end
|
||||
alias open4 popen4
|
||||
alias pfork4 popen4
|
||||
alias popen4ext popen4
|
||||
|
||||
module_function :open4
|
||||
module_function :popen4
|
||||
module_function :pfork4
|
||||
module_function :popen4ext
|
||||
end
|
||||
|
|
29
test/test_mocks_helper.rb
Normal file
29
test/test_mocks_helper.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
require "system_command"
|
||||
|
||||
module TestMocksHelper
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def mock_system_command(program, args, exit_status: 0, stdout: "", stderr: "")
|
||||
syscmd = "#{program} #{Array.wrap(args).join(' ')}"
|
||||
result = SystemCommandResult.new(syscmd, exit_status, stdout, stderr)
|
||||
|
||||
mock = Minitest::Mock.new
|
||||
mock.expect :execute, result
|
||||
|
||||
assert_stub = lambda { |actual_program, actual_args, _logger|
|
||||
assert_equal program, actual_program,
|
||||
"SystemCommand was not initialized with the expected program name"
|
||||
|
||||
assert_equal args, actual_args,
|
||||
"SystemCommand was not initialized with the expected arguments"
|
||||
|
||||
mock
|
||||
}
|
||||
|
||||
SystemCommand.stub :new, assert_stub do
|
||||
yield
|
||||
end
|
||||
|
||||
mock.verify
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
Loading…
Reference in a new issue