mirror of
https://github.com/Evolix/chexpire.git
synced 2024-05-18 16:38:38 +02:00
commit
822da5c752
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -35,3 +35,9 @@ yarn-debug.log*
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
/config/deploy/config.yml
|
/config/deploy/config.yml
|
||||||
|
|
||||||
|
# SimpleCov coverage output
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# OS file
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -19,9 +19,9 @@ before_install:
|
||||||
install:
|
install:
|
||||||
- bundle install
|
- bundle install
|
||||||
- yarn install
|
- yarn install
|
||||||
- rails db:setup
|
- rails db:create db:migrate
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- bundle exec rubocop
|
- bundle exec rubocop
|
||||||
- bundle exec rails test
|
- bundle exec rails test NO_COVERAGE=1
|
||||||
- bundle exec rails test:system
|
- bundle exec rails test:system NO_COVERAGE=1
|
||||||
|
|
9
Gemfile
9
Gemfile
|
@ -39,6 +39,9 @@ gem 'bcrypt', '~> 3.1.7'
|
||||||
gem 'open4'
|
gem 'open4'
|
||||||
gem 'naught'
|
gem 'naught'
|
||||||
|
|
||||||
|
|
||||||
|
gem 'octicons'
|
||||||
|
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', '>= 1.1.0', require: false
|
gem 'bootsnap', '>= 1.1.0', require: false
|
||||||
|
|
||||||
|
@ -47,6 +50,8 @@ group :development, :test do
|
||||||
gem 'binding_of_caller'
|
gem 'binding_of_caller'
|
||||||
gem 'pry-byebug'
|
gem 'pry-byebug'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
|
|
||||||
|
gem "factory_bot_rails"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
@ -61,7 +66,6 @@ group :development do
|
||||||
gem 'annotate', require: false
|
gem 'annotate', require: false
|
||||||
gem 'letter_opener_web'
|
gem 'letter_opener_web'
|
||||||
|
|
||||||
|
|
||||||
gem "guard"
|
gem "guard"
|
||||||
gem "guard-minitest"
|
gem "guard-minitest"
|
||||||
|
|
||||||
|
@ -78,6 +82,9 @@ group :test do
|
||||||
# Easy installation and use of chromedriver to run system tests with Chrome
|
# Easy installation and use of chromedriver to run system tests with Chrome
|
||||||
gem 'chromedriver-helper'
|
gem 'chromedriver-helper'
|
||||||
gem 'launchy'
|
gem 'launchy'
|
||||||
|
|
||||||
|
gem "database_cleaner"
|
||||||
|
gem "simplecov", require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
# 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)
|
coderay (1.1.2)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
crass (1.0.4)
|
crass (1.0.4)
|
||||||
|
database_cleaner (1.7.0)
|
||||||
debug_inspector (0.0.3)
|
debug_inspector (0.0.3)
|
||||||
devise (4.4.3)
|
devise (4.4.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
|
@ -103,8 +104,14 @@ GEM
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-i18n (1.6.2)
|
devise-i18n (1.6.2)
|
||||||
devise (>= 4.4)
|
devise (>= 4.4)
|
||||||
|
docile (1.3.1)
|
||||||
erubi (1.7.1)
|
erubi (1.7.1)
|
||||||
execjs (2.7.0)
|
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)
|
ffi (1.9.23)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
globalid (0.4.1)
|
globalid (0.4.1)
|
||||||
|
@ -128,6 +135,7 @@ GEM
|
||||||
jbuilder (2.7.0)
|
jbuilder (2.7.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
multi_json (>= 1.2)
|
multi_json (>= 1.2)
|
||||||
|
json (2.1.0)
|
||||||
launchy (2.4.3)
|
launchy (2.4.3)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
letter_opener (1.6.0)
|
letter_opener (1.6.0)
|
||||||
|
@ -167,6 +175,8 @@ GEM
|
||||||
notiffany (0.1.1)
|
notiffany (0.1.1)
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
|
octicons (7.3.0)
|
||||||
|
nokogiri (>= 1.6.3.1)
|
||||||
open4 (1.3.4)
|
open4 (1.3.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
parallel (1.12.1)
|
parallel (1.12.1)
|
||||||
|
@ -253,6 +263,11 @@ GEM
|
||||||
simple_form (4.0.1)
|
simple_form (4.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
activemodel (>= 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)
|
spring (2.0.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
spring-watcher-listen (2.0.1)
|
spring-watcher-listen (2.0.1)
|
||||||
|
@ -310,8 +325,10 @@ DEPENDENCIES
|
||||||
capistrano3-puma
|
capistrano3-puma
|
||||||
capybara (>= 2.15, < 4.0)
|
capybara (>= 2.15, < 4.0)
|
||||||
chromedriver-helper
|
chromedriver-helper
|
||||||
|
database_cleaner
|
||||||
devise (~> 4.4)
|
devise (~> 4.4)
|
||||||
devise-i18n (~> 1.6)
|
devise-i18n (~> 1.6)
|
||||||
|
factory_bot_rails
|
||||||
guard
|
guard
|
||||||
guard-minitest
|
guard-minitest
|
||||||
jbuilder (~> 2.5)
|
jbuilder (~> 2.5)
|
||||||
|
@ -320,6 +337,7 @@ DEPENDENCIES
|
||||||
listen (>= 3.0.5, < 3.2)
|
listen (>= 3.0.5, < 3.2)
|
||||||
mysql2 (>= 0.4.4, < 0.6.0)
|
mysql2 (>= 0.4.4, < 0.6.0)
|
||||||
naught
|
naught
|
||||||
|
octicons
|
||||||
open4
|
open4
|
||||||
pry-byebug
|
pry-byebug
|
||||||
pry-rails
|
pry-rails
|
||||||
|
@ -331,6 +349,7 @@ DEPENDENCIES
|
||||||
sass-rails (~> 5.0)
|
sass-rails (~> 5.0)
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
simple_form (~> 4.0)
|
simple_form (~> 4.0)
|
||||||
|
simplecov
|
||||||
spring
|
spring
|
||||||
spring-watcher-listen (~> 2.0.0)
|
spring-watcher-listen (~> 2.0.0)
|
||||||
turbolinks (~> 5)
|
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/application_controller\.rb$}) { "test/controllers" }
|
||||||
watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" }
|
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/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{^app/services/whois/.+\.rb}) { |_m| "test/services/whois" }
|
||||||
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
||||||
watch(%r{^test/.+_test\.rb$})
|
watch(%r{^test/.+_test\.rb$})
|
||||||
watch(%r{^test/test_helper\.rb$}) { "test" }
|
watch(%r{^test/test_helper\.rb$}) { "test" }
|
||||||
watch(%r{^test/fixtures/.+\.yml$}) { "test" }
|
watch(%r{^test/fixtures/.+\.yml$}) { "test" }
|
||||||
|
watch(%r{^test/factories/.+\.rb$}) { "test" }
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ class ChecksController < ApplicationController
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@check = Check.new
|
@check = Check.new
|
||||||
|
build_empty_notification
|
||||||
authorize @check
|
authorize @check
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -27,7 +28,9 @@ class ChecksController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit
|
||||||
|
build_empty_notification
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @check.update(update_check_params)
|
if @check.update(update_check_params)
|
||||||
|
@ -35,6 +38,7 @@ class ChecksController < ApplicationController
|
||||||
redirect_to checks_path
|
redirect_to checks_path
|
||||||
else
|
else
|
||||||
flash.now[:alert] = "An error occured."
|
flash.now[:alert] = "An error occured."
|
||||||
|
build_empty_notification
|
||||||
render :edit
|
render :edit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -62,6 +66,11 @@ class ChecksController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_params(*others)
|
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
|
||||||
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 '~bootstrap/scss/bootstrap';
|
||||||
@import 'layout';
|
@import 'layout';
|
||||||
|
@import 'icons';
|
||||||
@import 'components/users';
|
@import 'components/users';
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
|
def format_utc(time, format: :default)
|
||||||
|
l(time.utc, format: format)
|
||||||
|
end
|
||||||
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)
|
def perform(check_id)
|
||||||
@check = Check.find(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?
|
return unless response.valid?
|
||||||
|
|
||||||
update_from_response(response)
|
update_from_response(response)
|
||||||
|
@ -17,11 +18,14 @@ class WhoisSyncJob < ApplicationJob
|
||||||
rescue Whois::DomainNotFoundError
|
rescue Whois::DomainNotFoundError
|
||||||
check.active = false
|
check.active = false
|
||||||
check.save!
|
check.save!
|
||||||
|
rescue Whois::ParserError # rubocop:disable Lint/HandleExceptions
|
||||||
|
# already logged
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_from_response(response)
|
def update_from_response(response)
|
||||||
check.domain_created_at = response.created_at
|
check.domain_created_at = response.created_at
|
||||||
check.domain_updated_at = response.updated_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
|
||||||
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)
|
# comment :string(255)
|
||||||
# domain :string(255) not null
|
# domain :string(255) not null
|
||||||
# domain_created_at :datetime
|
# domain_created_at :datetime
|
||||||
# domain_expire_at :datetime
|
# domain_expires_at :datetime
|
||||||
# domain_updated_at :datetime
|
# domain_updated_at :datetime
|
||||||
# kind :integer not null
|
# kind :integer not null
|
||||||
# last_run_at :datetime
|
# last_run_at :datetime
|
||||||
|
@ -29,13 +29,17 @@
|
||||||
class Check < ApplicationRecord
|
class Check < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :logs, class_name: "CheckLog"
|
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]
|
enum kind: [:domain, :ssl]
|
||||||
|
|
||||||
self.skip_time_zone_conversion_for_attributes = [
|
self.skip_time_zone_conversion_for_attributes = [
|
||||||
:domain_created_at,
|
:domain_created_at,
|
||||||
:domain_updated_at,
|
:domain_updated_at,
|
||||||
:domain_expire_at,
|
:domain_expires_at,
|
||||||
]
|
]
|
||||||
|
|
||||||
validates :kind, presence: true
|
validates :kind, presence: true
|
||||||
|
@ -45,9 +49,10 @@ class Check < ApplicationRecord
|
||||||
validates :comment, length: { maximum: 255 }
|
validates :comment, length: { maximum: 255 }
|
||||||
validates :vendor, length: { maximum: 255 }
|
validates :vendor, length: { maximum: 255 }
|
||||||
|
|
||||||
|
after_update :reset_notifications
|
||||||
after_save :enqueue_sync
|
after_save :enqueue_sync
|
||||||
|
|
||||||
protected
|
private
|
||||||
|
|
||||||
def domain_created_at_past
|
def domain_created_at_past
|
||||||
errors.add(:domain_created_at, :past) if domain_created_at.present? && domain_created_at.future?
|
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?
|
WhoisSyncJob.perform_later(id) if domain?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_notifications
|
||||||
|
return unless (saved_changes.keys & %w[domain domain_expires_at]).present?
|
||||||
|
|
||||||
|
notifications.each(&:reset!)
|
||||||
|
end
|
||||||
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
|
has_many :checks
|
||||||
validates :tos_accepted, acceptance: true
|
validates :tos_accepted, acceptance: true
|
||||||
|
|
||||||
|
scope :notifications_disabled, -> { where(notifications_enabled: false) }
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
def call(cmd)
|
def call(cmd)
|
||||||
pid, _, stdout, stderr = Open4.popen4 cmd
|
pid, _, stdout, stderr = Open4.popen4 cmd
|
||||||
_, status = Process.waitpid2 pid
|
_, status = Process.waitpid2 pid
|
||||||
|
@ -45,6 +46,7 @@ class SystemCommand
|
||||||
stderr.read.strip,
|
stderr.read.strip,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
def escape_arg(arg)
|
def escape_arg(arg)
|
||||||
arg.to_s.gsub('"') { '\"' }
|
arg.to_s.gsub('"') { '\"' }
|
||||||
|
|
|
@ -12,5 +12,18 @@
|
||||||
<%= f.input :active %>
|
<%= f.input :active %>
|
||||||
<% end %>
|
<% 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 %>
|
<% end %>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div>Kind: <%= check.kind %></div>
|
<div>Kind: <%= check.kind %></div>
|
||||||
<div>Created date: <%= l(check.domain_created_at.to_date) if check.domain_created_at.present? %></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>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? %>
|
<% if check.comment? %>
|
||||||
<div>Comment: <%= check.comment %></div>
|
<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
|
default: &default
|
||||||
mailer_default_from: "from@example.org"
|
mailer_default_from: "from@example.org"
|
||||||
|
notifier:
|
||||||
|
interval: 0.00
|
||||||
|
failure_days: 3
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
test:
|
test:
|
||||||
mailer_default_from: "contact@chexpire.org"
|
mailer_default_from: "contact@chexpire.org"
|
||||||
host: "localhost"
|
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,
|
# Use an evented file watcher to asynchronously detect changes in source code,
|
||||||
# routes, locales, etc. This feature depends on the listen gem.
|
# routes, locales, etc. This feature depends on the listen gem.
|
||||||
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||||
|
|
||||||
config.active_job.queue_adapter = :inline
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,7 @@ Rails.application.config.content_security_policy do |policy|
|
||||||
end
|
end
|
||||||
|
|
||||||
# If you are using UJS then enable automatic nonce generation
|
# 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
|
# Report CSP violations to a specified URI
|
||||||
# For further information see the following documentation:
|
# For further information see the following documentation:
|
||||||
|
|
|
@ -17,8 +17,25 @@ en:
|
||||||
new:
|
new:
|
||||||
tos_acceptance_html: "You must accept our Terms of service"
|
tos_acceptance_html: "You must accept our Terms of service"
|
||||||
|
|
||||||
|
simple_form:
|
||||||
|
placeholders:
|
||||||
|
notifications:
|
||||||
|
recipient: john@example.org
|
||||||
|
|
||||||
flashes:
|
flashes:
|
||||||
user_not_authorized: "You are not authorized to access to this resource."
|
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:
|
shared:
|
||||||
navbar:
|
navbar:
|
||||||
sign_up: "Sign up"
|
sign_up: "Sign up"
|
||||||
|
@ -32,3 +49,7 @@ en:
|
||||||
You have not set up a check yet.
|
You have not set up a check yet.
|
||||||
Please add a <a href="%{new_domain_path}">domain</a>
|
Please add a <a href="%{new_domain_path}">domain</a>
|
||||||
or a <a href="%{new_ssl_path}">ssl</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
|
# == Route Map
|
||||||
#
|
#
|
||||||
# Prefix Verb URI Pattern Controller#Action
|
# Prefix Verb URI Pattern Controller#Action
|
||||||
|
# check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy
|
||||||
# checks GET /checks(.:format) checks#index
|
# checks GET /checks(.:format) checks#index
|
||||||
# POST /checks(.:format) checks#create
|
# POST /checks(.:format) checks#create
|
||||||
# new_check GET /checks/new(.:format) checks#new
|
# new_check GET /checks/new(.:format) checks#new
|
||||||
|
@ -46,7 +47,9 @@
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
|
# 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
|
devise_for :users
|
||||||
root to: "pages#home"
|
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.
|
# 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|
|
create_table "check_logs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
|
||||||
t.bigint "check_id"
|
t.bigint "check_id"
|
||||||
|
@ -30,7 +30,7 @@ ActiveRecord::Schema.define(version: 2018_05_30_123611) do
|
||||||
t.string "domain", null: false
|
t.string "domain", null: false
|
||||||
t.datetime "domain_created_at"
|
t.datetime "domain_created_at"
|
||||||
t.datetime "domain_updated_at"
|
t.datetime "domain_updated_at"
|
||||||
t.datetime "domain_expire_at"
|
t.datetime "domain_expires_at"
|
||||||
t.datetime "last_run_at"
|
t.datetime "last_run_at"
|
||||||
t.datetime "last_success_at"
|
t.datetime "last_success_at"
|
||||||
t.string "vendor"
|
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"
|
t.index ["user_id"], name: "index_checks_on_user_id"
|
||||||
end
|
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|
|
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
|
||||||
t.string "email", default: "", null: false
|
t.string "email", default: "", null: false
|
||||||
t.string "encrypted_password", 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 "check_logs", "checks"
|
||||||
add_foreign_key "checks", "users"
|
add_foreign_key "checks", "users"
|
||||||
|
add_foreign_key "notifications", "checks"
|
||||||
end
|
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.
|
Notification.destroy_all
|
||||||
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
|
Check.destroy_all
|
||||||
#
|
User.destroy_all
|
||||||
# Examples:
|
|
||||||
#
|
user1 = User.create!(
|
||||||
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
|
email: "colin@example.org",
|
||||||
# Character.create(name: 'Luke', movie: movies.first)
|
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
|
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||||
driven_by :headless_chrome
|
driven_by :headless_chrome
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
Capybara.reset_sessions!
|
||||||
|
Warden.test_reset!
|
||||||
|
end
|
||||||
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)
|
# 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_email (email) UNIQUE
|
||||||
# index_users_on_reset_password_token (reset_password_token) 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
|
FactoryBot.define do
|
||||||
|
factory :user do
|
||||||
# This model initially had no columns defined. If you add columns to the
|
sequence(:email) { |n| "user-#{n}@chexpire.org" }
|
||||||
# model remove the '{}' from the fixture names and add the columns immediately
|
password "password"
|
||||||
# below each fixture, per the syntax in the comments below
|
confirmed_at Time.new(2018, 4, 1, 12, 0, 0, "+02:00")
|
||||||
#
|
notifications_enabled true
|
||||||
user1:
|
tos_accepted true
|
||||||
email: user@chexpire.org
|
end
|
||||||
encrypted_password: <%= User.new.send(:password_digest, 'password') %>
|
end
|
||||||
confirmed_at: <%= 1.minute.ago %>
|
|
||||||
tos_accepted: true
|
|
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"
|
require "test_helper"
|
||||||
|
|
||||||
class WhoisSyncJobTest < ActiveJob::TestCase
|
class WhoisSyncJobTest < ActiveJob::TestCase
|
||||||
# test "the truth" do
|
test "calls whois database and update check with the response (domain.fr)" do
|
||||||
# assert true
|
domain = "domain.fr"
|
||||||
# end
|
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
|
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)
|
# comment :string(255)
|
||||||
# domain :string(255) not null
|
# domain :string(255) not null
|
||||||
# domain_created_at :datetime
|
# domain_created_at :datetime
|
||||||
# domain_expire_at :datetime
|
# domain_expires_at :datetime
|
||||||
# domain_updated_at :datetime
|
# domain_updated_at :datetime
|
||||||
# kind :integer not null
|
# kind :integer not null
|
||||||
# last_run_at :datetime
|
# last_run_at :datetime
|
||||||
|
@ -29,7 +29,24 @@
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class CheckTest < ActiveSupport::TestCase
|
class CheckTest < ActiveSupport::TestCase
|
||||||
# test "the truth" do
|
test "notifications are resetted when domain expiration date has changed" do
|
||||||
# assert true
|
check = create(:check)
|
||||||
# end
|
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
|
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"
|
require "test_helper"
|
||||||
|
|
||||||
class CheckPolicyTest < ActiveSupport::TestCase
|
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
|
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
|
class CheckLoggerTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@check = checks(:domain_example_org)
|
@check = create(:check)
|
||||||
@logger = CheckLogger.new(@check)
|
@logger = CheckLogger.new(@check)
|
||||||
end
|
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"
|
require "application_system_test_case"
|
||||||
|
|
||||||
class UsersTest < ApplicationSystemTestCase
|
class UsersTest < ApplicationSystemTestCase
|
||||||
|
setup do
|
||||||
|
@user = create(:user)
|
||||||
|
end
|
||||||
|
|
||||||
test "an user can signup from the homepage and confirm its account" do
|
test "an user can signup from the homepage and confirm its account" do
|
||||||
visit root_path
|
visit root_path
|
||||||
|
|
||||||
|
@ -31,28 +35,25 @@ class UsersTest < ApplicationSystemTestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "an user can signin from the homepage" do
|
test "an user can signin from the homepage" do
|
||||||
user = users(:user1)
|
|
||||||
visit root_path
|
visit root_path
|
||||||
|
|
||||||
click_on I18n.t("shared.navbar.sign_in")
|
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"
|
fill_in "user[password]", with: "password"
|
||||||
|
|
||||||
click_button I18n.t("devise.sessions.new.sign_in")
|
click_button I18n.t("devise.sessions.new.sign_in")
|
||||||
|
|
||||||
assert_equal root_path, page.current_path
|
assert_equal root_path, page.current_path
|
||||||
assert page.has_content?(user.email)
|
assert page.has_content?(@user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "an user can signout from the homepage" do
|
test "an user can signout from the homepage" do
|
||||||
user = users(:user1)
|
login_as @user
|
||||||
|
|
||||||
login_as user
|
|
||||||
visit root_path
|
visit root_path
|
||||||
|
|
||||||
find ".navbar" do
|
find ".navbar" do
|
||||||
click_on user.email
|
click_on @user.email
|
||||||
click_on I18n.t("shared.navbar.sign_out")
|
click_on I18n.t("shared.navbar.sign_out")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,12 +91,11 @@ class UsersTest < ApplicationSystemTestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "an user can globally disable its notifications" do
|
test "an user can globally disable its notifications" do
|
||||||
user = users(:user1)
|
login_as @user
|
||||||
login_as user
|
|
||||||
|
|
||||||
visit edit_user_registration_path
|
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
|
assert find_field("user[notifications_enabled]").value
|
||||||
uncheck "user[notifications_enabled]"
|
uncheck "user[notifications_enabled]"
|
||||||
|
@ -104,7 +104,7 @@ class UsersTest < ApplicationSystemTestCase
|
||||||
|
|
||||||
click_button I18n.t("devise.registrations.edit.update")
|
click_button I18n.t("devise.registrations.edit.update")
|
||||||
|
|
||||||
user.reload
|
@user.reload
|
||||||
refute user.notifications_enabled
|
refute @user.notifications_enabled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,22 @@
|
||||||
ENV["RAILS_ENV"] ||= "test"
|
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_relative "../config/environment"
|
||||||
require "rails/test_help"
|
require "rails/test_help"
|
||||||
|
|
||||||
require "minitest/mock"
|
require "minitest/mock"
|
||||||
|
require_relative "test_mocks_helper"
|
||||||
|
require_relative "chexpire_assertions"
|
||||||
|
|
||||||
class ActiveSupport::TestCase
|
class ActiveSupport::TestCase
|
||||||
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
||||||
|
@ -12,8 +26,13 @@ class ActiveSupport::TestCase
|
||||||
Warden.test_mode!
|
Warden.test_mode!
|
||||||
|
|
||||||
# Add more helper methods to be used by all tests here...
|
# Add more helper methods to be used by all tests here...
|
||||||
|
include ActiveJob::TestHelper
|
||||||
|
include FactoryBot::Syntax::Methods
|
||||||
|
include TestMocksHelper
|
||||||
|
include ChexpireAssertions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Capybara configuration
|
||||||
Capybara.register_driver :headless_chrome do |app|
|
Capybara.register_driver :headless_chrome do |app|
|
||||||
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
||||||
"chromeOptions" => { args: %w[headless disable-gpu] + ["window-size=1280,800"] },
|
"chromeOptions" => { args: %w[headless disable-gpu] + ["window-size=1280,800"] },
|
||||||
|
@ -22,3 +41,21 @@ Capybara.register_driver :headless_chrome do |app|
|
||||||
end
|
end
|
||||||
Capybara.save_path = Rails.root.join("tmp/capybara")
|
Capybara.save_path = Rails.root.join("tmp/capybara")
|
||||||
Capybara.javascript_driver = :headless_chrome
|
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