21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-04-26 05:50:50 +02:00

Merge pull request #91 from Evolix/notifications-templates

Notifications templates
This commit is contained in:
Colin Darie 2018-08-31 10:17:10 +02:00 committed by GitHub
commit 3aa1cc376e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 950 additions and 316 deletions

View file

@ -15,6 +15,18 @@ class ApplicationController < ActionController::Base
devise_parameter_sanitizer.permit(:account_update, keys: [:notifications_enabled, :locale])
end
def after_sign_in_path_for(_resource)
checks_path
end
def after_sign_up_path_for(_resource)
checks_path
end
def after_sign_out_path_for(_resource)
root_path
end
def user_not_authorized
flash[:alert] = I18n.t("user_not_authorized", scope: :flashes)
redirect_to(request.referrer || root_path)

View file

@ -1,7 +1,7 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Jeremy Lecour <jlecour@evolix.fr>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class ChecksController < ApplicationController
class ChecksController < ApplicationController # rubocop:disable Metrics/ClassLength
before_action :authenticate_user!
before_action :set_check, except: [:index, :new, :create, :supports]
after_action :verify_authorized, except: :index
@ -29,16 +29,17 @@ class ChecksController < ApplicationController
build_empty_notification
end
def create
@check = Check.new(new_check_params)
@check.user = current_user
def create # rubocop:disable Metrics/AbcSize
@check = Check.new(new_check_params.merge(user: current_user))
authorize @check
if @check.save
flash[:notice] = t(".saved")
flash[:notice] = t("checks.created", scope: :flashes)
redirect_to checks_path
else
flash.now[:alert] = t(".invalid")
flash.now[:alert] = t("checks.invalid", scope: :flashes)
fill_or_build_new_notification
render :new
end
end
@ -49,11 +50,12 @@ class ChecksController < ApplicationController
def update
if @check.update(update_check_params)
flash[:notice] = "Your check has been updated."
flash[:notice] = t("checks.updated", scope: :flashes)
redirect_to checks_path
else
flash.now[:alert] = "An error occured."
build_empty_notification
flash.now[:alert] = t("checks.invalid", scope: :flashes)
fill_or_build_new_notification
render :edit
end
end
@ -61,7 +63,7 @@ class ChecksController < ApplicationController
def destroy
@check.destroy!
flash[:notice] = "Your check has been destroyed."
flash[:notice] = t("checks.destroyed", scope: :flashes)
redirect_to checks_path
end
@ -86,13 +88,39 @@ class ChecksController < ApplicationController
end
def check_params(*others)
params.require(:check)
.permit(:domain, :domain_expires_at, :comment, :vendor, :round_robin, *others,
notifications_attributes: [:id, :channel, :recipient, :interval])
permitted = params.require(:check)
.permit(:domain, :domain_expires_at, :comment, :vendor,
:round_robin, *others,
notification_ids: [],
notifications_attributes: [:channel, :label, :recipient, :interval])
merge_current_user!(permitted)
permitted
end
def merge_current_user!(permitted)
return unless permitted[:notifications_attributes].present?
permitted[:notifications_attributes].each_pair do |_key, attributes|
attributes.merge!(user: current_user)
end
end
def build_empty_notification
@check.notifications.build
@new_notification = @check.notifications.build
@new_notification.recipient = current_user.email
end
def fill_or_build_new_notification
last_notification = @check.notifications.last
# user has filled a new notification: we use it for the form
if last_notification.new_record?
@new_notification = last_notification
else # otherwise, set a new empty notification
build_empty_notification
end
end
def current_sort

View file

@ -3,45 +3,61 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!
before_action :set_notification, except: [:create]
before_action :set_notification, except: [:index, :new, :create]
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@notifications = policy_scope(Notification).order(checks_count: :desc)
end
def new
@notification = Notification.new
authorize @notification
@notification.recipient = current_user.email
end
def create
check = Check.find(params[:check_id])
@notification = check.notifications.build(notification_params)
@notification = Notification.new(notification_params)
@notification.user = current_user
authorize @notification
if @notification.save
flash[:notice] = "Your notification has been saved."
redirect_to check_path
flash[:notice] = t("notifications.created", scope: :flashes)
redirect_to notifications_path
else
flash.now[:alert] = "An error occured."
render "checks/edit"
flash.now[:alert] = t("notifications.invalid", scope: :flashes)
render :new
end
end
def edit; end
def update
if @notification.update(notification_params)
flash[:notice] = t("notifications.updated", scope: :flashes)
redirect_to notifications_path
else
flash.now[:alert] = t("notifications.error", scope: :flashes)
render :edit
end
end
def destroy
@notification.destroy!
respond_to do |format|
format.js
end
flash[:notice] = t("notifications.destroyed", scope: :flashes)
redirect_to notifications_path
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])
@notification = Notification.find(params[:id])
authorize @notification
end
def notification_params
params.require(:notification).permit(:channel, :recipient, :interval)
end
def check_path
edit_check_path(check_id: params[:check_id])
params.require(:notification).permit(:label, :recipient, :interval)
end
end

View file

@ -2,5 +2,7 @@
# License: GNU AGPL-3+ (see full text in LICENSE file)
class PagesController < ApplicationController
def home; end
def home
redirect_to checks_path if user_signed_in?
end
end

View file

@ -0,0 +1,51 @@
// Taken from Bootstrap 4 documentation
.bd-callout {
padding: 1.25rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #eee;
border-left-width: .25rem;
border-radius: .25rem
}
.bd-callout h4 {
margin-top: 0;
margin-bottom: .25rem
}
.bd-callout p:last-child {
margin-bottom: 0
}
.bd-callout code {
border-radius: .25rem
}
.bd-callout+.bd-callout {
margin-top: -.25rem
}
.bd-callout-info {
border-left-color: #5bc0de
}
.bd-callout-info h4 {
color: #5bc0de
}
.bd-callout-warning {
border-left-color: #f0ad4e
}
.bd-callout-warning h4 {
color: #f0ad4e
}
.bd-callout-danger {
border-left-color: #d9534f
}
.bd-callout-danger h4 {
color: #d9534f
}

View file

@ -0,0 +1,8 @@
// Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
// License: GNU AGPL-3+ (see full text in LICENSE file)
.notifications-table {
.action a {
color: black;
}
}

View file

@ -5,5 +5,7 @@
@import '~bootstrap/scss/bootstrap';
@import 'layout';
@import 'icons';
@import 'components/callouts';
@import 'components/users';
@import 'components/checks';
@import 'components/notifications';

View file

@ -0,0 +1,5 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
module CheckNotificationsHelper
end

View file

@ -1,12 +1,9 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
module NotificationsHelper
def many_channels_available?
Notification.channels.many?
end
def recipient_col_class
many_channels_available? ? "col-md-7" : "col-md-9"
def notification_variable_col_class
many_channels_available? ? "col-md-4" : "col-md-5"
end
end

View file

@ -5,8 +5,9 @@ class NotificationsMailer < ApplicationMailer
helper :application
before_action except: :recurrent_failures do
@notification = params.fetch(:notification)
@check = @notification.check
@check_notification = params.fetch(:check_notification)
@check = @check_notification.check
@notification = @check_notification.notification
end
def domain_expires_soon

View file

@ -34,10 +34,12 @@
class Check < ApplicationRecord
belongs_to :user
has_many :logs, class_name: "CheckLog", dependent: :destroy
has_many :notifications, validate: true, dependent: :destroy
has_many :check_notifications, dependent: :destroy
has_many :notifications, -> { order(checks_count: :desc) },
through: :check_notifications, validate: true
accepts_nested_attributes_for :notifications,
allow_destroy: true,
reject_if: lambda { |at| at["recipient"].blank? && at["interval"].blank? }
reject_if: lambda { |att| att["interval"].blank? }
enum kind: [:domain, :ssl]
enum mode: [:auto, :manual]
@ -58,7 +60,7 @@ class Check < ApplicationRecord
before_save :reset_consecutive_failures
before_save :set_mode
after_update :reset_notifications
after_update :reset_check_notifications
after_save :enqueue_sync
scope :active, -> { where(active: true) }
@ -119,10 +121,10 @@ class Check < ApplicationRecord
ResyncJob.perform_later(id)
end
def reset_notifications
def reset_check_notifications
return unless (saved_changes.keys & %w[domain domain_expires_at]).present?
notifications.each(&:reset!)
check_notifications.each(&:reset!)
end
def reset_consecutive_failures

View file

@ -0,0 +1,46 @@
# == Schema Information
#
# Table name: check_notifications
#
# id :bigint(8) not null, primary key
# sent_at :datetime
# status :integer default("pending"), not null
# created_at :datetime not null
# updated_at :datetime not null
# check_id :bigint(8)
# notification_id :bigint(8)
#
# Indexes
#
# index_check_notifications_on_check_id (check_id)
# index_check_notifications_on_notification_id (notification_id)
#
# Foreign Keys
#
# fk_rails_... (check_id => checks.id)
# fk_rails_... (notification_id => notifications.id)
#
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class CheckNotification < ApplicationRecord
belongs_to :check
belongs_to :notification, counter_cache: :checks_count
enum status: [:pending, :ongoing, :succeed, :failed]
scope :active_check, -> { Check.active }
scope :check_last_run_failed, -> { Check.last_run_failed }
def pending!
self.sent_at = nil
super
end
alias reset! pending!
def ongoing!
self.sent_at = Time.now
super
end
end

View file

@ -1,50 +1,45 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Jeremy Lecour <jlecour@evolix.fr>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
# == Schema Information
#
# Table name: notifications
#
# id :bigint(8) not null, primary key
# channel :integer default("email"), not null
# interval :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)
# id :bigint(8) not null, primary key
# channel :integer default("email"), not null
# checks_count :integer default(0), not null
# interval :integer not null
# label :string(255)
# recipient :string(255) not null
# created_at :datetime not null
# updated_at :datetime not null
# check_id :bigint(8)
# user_id :bigint(8)
#
# Indexes
#
# index_notifications_on_check_id (check_id)
# index_notifications_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (check_id => checks.id)
# fk_rails_... (user_id => users.id)
#
class Notification < ApplicationRecord
belongs_to :check
belongs_to :user
has_many :check_notifications, dependent: :destroy
has_many :checks, -> { order(domain_expires_at: :asc) }, through: :check_notifications
enum channel: [:email]
enum status: [:pending, :ongoing, :succeed, :failed]
validates :channel, presence: true
validates :interval, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates :recipient, presence: true
scope :active_check, -> { Check.active }
scope :check_last_run_failed, -> { Check.last_run_failed }
def notifical_label
return label if label.present?
def pending!
self.sent_at = nil
super
end
alias reset! pending!
def ongoing!
self.sent_at = Time.now
super
"#{recipient} (#{interval})"
end
end

View file

@ -39,7 +39,8 @@ class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable
has_many :checks
has_many :checks, dependent: :destroy
has_many :notifications, dependent: :destroy
validates :tos_accepted, acceptance: true
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }

View file

@ -0,0 +1,24 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class CheckNotificationPolicy < 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

View file

@ -4,21 +4,25 @@
class NotificationPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.joins(:check).where(checks: { user: user })
scope.where(user: user)
end
end
def destroy?
check_owner?
def create?
true
end
def show?
false
def update?
owner?
end
def destroy?
owner?
end
private
def check_owner?
record.check.user == user
def owner?
record.user == user
end
end

View file

@ -4,26 +4,26 @@
module Notifier
module Channels
class Base
def notify(notification) # rubocop:disable Metrics/MethodLength
return unless supports?(notification)
def notify(check_notification) # rubocop:disable Metrics/MethodLength
return unless supports?(check_notification)
notification.ongoing!
check_notification.ongoing!
case notification.check.kind.to_sym
case check_notification.check.kind.to_sym
when :domain
domain_notify_expires_soon(notification)
domain_notify_expires_soon(check_notification)
when :ssl
ssl_notify_expires_soon(notification)
ssl_notify_expires_soon(check_notification)
else
fail ArgumentError,
"Invalid notification for check kind `#{notification.check.kind}`."
"Invalid notification for check kind `#{check_notification.check.kind}`."
end
end
private
# :nocov:
def supports?(_notification)
def supports?(_check_notification)
fail NotImplementedError,
"#{self.class.name} channel did not implemented method #{__callee__}"
end

View file

@ -11,17 +11,21 @@ module Notifier
protected
def supports?(_notification)
def supports?(_check_notification)
true
end
# Expiration notifications
def domain_notify_expires_soon(notification)
NotificationsMailer.with(notification: notification).domain_expires_soon.deliver_now
def domain_notify_expires_soon(check_notification)
NotificationsMailer.with(check_notification: check_notification)
.domain_expires_soon
.deliver_now
end
def ssl_notify_expires_soon(notification)
NotificationsMailer.with(notification: notification).ssl_expires_soon.deliver_now
def ssl_notify_expires_soon(_notification)
NotificationsMailer.with(check_notification: check_notification)
.ssl_expires_soon
.deliver_now
end
end
end

View file

@ -19,8 +19,8 @@ module Notifier
end
def process_expires_soon
resolver.notifications_expiring_soon.find_each do |notification|
notifier_channel_for(notification).notify(notification)
resolver.notifications_expiring_soon.find_each do |check_notification|
notifier_channel_for(check_notification.notification).notify(check_notification)
sleep configuration.interval
end

View file

@ -21,8 +21,8 @@ module Notifier
private
def scope
Notification
.includes(:check)
CheckNotification
.includes(:check, :notification)
.where(status: [:pending, :failed])
.merge(Check.active)
.where.not(checks: { user: ignore_users })

View file

@ -41,18 +41,17 @@
<%= f.input :active %>
<% end %>
<h2 class="mt-5"><%= t(".notifications") %></h2>
<p class="alert alert-light"><%= t(".notifications_hint") %></p>
<p class="mt-5 bd-callout bd-callout-info"><%= 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>
<%= f.association :notifications, as: :check_boxes,
collection: policy_scope(Notification),
label_method: :notifical_label,
label_text: false %>
<%= f.fields_for :notifications, new_notification do |nf| %>
<%= render "notifications/nested_form_headers", f: nf %>
<%= render "notifications/nested_form", f: nf, notification: new_notification %>
<% end %>
<%= f.button :submit, class: "btn-primary mt-5" %>
<% end %>

View file

@ -7,16 +7,20 @@
<th scope="col"></th>
<th scope="col">
<%= t(".th.domain") %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain) %>
</span>
<% unless defined?(skip_sort) %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain) %>
</span>
<% end %>
</th>
<th scope="col">
<span class="d-none d-sm-inline"><%= t(".th.expiry_date") %></span>
<span class="d-inline d-sm-none"><%= t(".th.expiry_date_short") %></span>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain_expires_at) %>
</span>
<% unless defined?(skip_sort) %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain_expires_at) %>
</span>
<% end %>
</th>
<th scope="col" class="text-right"><%= t(".th.edit") %></th>
</tr>
@ -48,4 +52,4 @@
</table>
</div>
<%= paginate @checks %>
<%= paginate checks unless defined?(skip_pagination)%>

View file

@ -3,16 +3,16 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1>Edit your check</h1>
<h1><%= t(".title") %></h1>
<%= render "form", check: @check %>
<%= render "form", check: @check, new_notification: @new_notification %>
</div>
</div>
<div class="row mt-5 justify-content-center">
<div class="col-12 col-lg-10">
<%= button_to("Delete", check_path(@check), class: "btn btn-danger", method: :delete,
data: { confirm: "Are you sure ?" }) %>
<%= button_to(t("helpers.submit.check.delete"), check_path(@check), class: "btn btn-danger", method: :delete,
data: { confirm: t(".destroy_confirmation") }) %>
</div>
</div>
</div>

View file

@ -5,7 +5,7 @@
<div class="col-12 col-lg-10">
<h1><%= t(".#{@check.kind}.title") %></h1>
<%= render "form", check: @check %>
<%= render "form", check: @check, new_notification: @new_notification %>
</div>
</div>
</div>

View file

@ -9,6 +9,9 @@
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
</head>
<body>

View file

@ -0,0 +1,13 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<%= simple_form_for(notification) do |f| %>
<%= f.input :recipient, as: :email,
input_html: { autocapitalize: :none, autocorrect: :off }
%>
<%= f.input :interval, as: :integer, required: true %>
<%= f.input :label, hint: t(".label_hint") %>
<%= f.button :submit, class: "btn-primary mt-3" %>
<% end %>

View file

@ -12,20 +12,16 @@
</div>
<% end %>
<div class="form-group <%= recipient_col_class %>">
<div class="form-group <%= notification_variable_col_class %>">
<%= f.input :recipient, as: :email, label: false %>
</div>
<div class="form-group col-md-2">
<%= f.input :interval, as: :integer, label: false %>
<%= f.input :interval, as: :integer, label: false, required: true %>
</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 class="form-group <%= notification_variable_col_class %>">
<%= f.input :label, label: false %>
</div>
</div>
</fieldset>

View file

@ -7,11 +7,15 @@
</div>
<% end %>
<div class="<%= recipient_col_class %>">
<div class="<%= notification_variable_col_class %>">
<%= f.label :recipient %>
</div>
<div class="col-md-2">
<%= f.label :interval %>
<%= f.label :interval, required: true %>
</div>
<div class="<%= notification_variable_col_class %>">
<%= f.label :label %>
</div>
</div>

View file

@ -0,0 +1,46 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="mb-4 table-responsive">
<table class="table notifications-table">
<thead>
<tr>
<th scope="col">
<%= Notification.human_attribute_name("label") %>
</th>
<th scope="col">
<%= Notification.human_attribute_name("recipient") %>
</th>
<th scope="col">
<%= Notification.human_attribute_name("interval") %>
</th>
<th scope="col">
<%= Notification.human_attribute_name("checks_count") %>
</th>
<th scope="col" class="text-right"><%= t(".th.edit") %></th>
</tr>
</thead>
<tbody>
<% notifications.each do |notification| %>
<tr class="notification-row">
<td>
<strong><%= notification.label %></strong>
</td>
<td>
<%= notification.recipient %>
</td>
<td>
<%= t(".interval_in_days", count: notification.interval) %>
</td>
<td>
<%= notification.checks_count %>
</td>
<td class="action text-right">
<%= link_to edit_notification_path(notification), "data-turbolinks" => false do %>
<%== Octicons::Octicon.new("pencil").to_svg %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

View file

@ -1,3 +0,0 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
document.querySelector("[data-notification-id='<%= @notification.id %>']").remove();

View file

@ -0,0 +1,27 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1><%= t(".title") %></h1>
<%= render "form", notification: @notification %>
</div>
</div>
<div class="row mt-5 justify-content-center">
<div class="col-12 col-lg-10">
<h4><%= t(".checks", count: @notification.checks_count) %></h4>
<% if @notification.checks_count.positive? %>
<%= render "checks/table", checks: @notification.checks, skip_sort: true, skip_pagination: true %>
<% end %>
</div>
</div>
<div class="row mt-5 mb-3 justify-content-center">
<div class="col-12 col-lg-10">
<%= button_to(t("helpers.submit.notification.delete"), notification_path(@notification), class: "btn btn-danger", method: :delete,
data: { confirm: t(".destroy_confirmation", count: @notification.checks_count) }) %>
</div>
</div>
</div>

View file

@ -0,0 +1,22 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-9">
<% if @notifications.empty? %>
<div class="alert alert-info">
<%= t(".no_notification_yet_html", new_path: new_notification_path) %>
</div>
<% else %>
<div class="row justify-content-md-end">
<%= link_to("Ajouter une notification", new_notification_path, class: "btn btn-primary") %>
</div>
<h1 class="mb-3 mb-sm-5"><%= t(".title") %></h1>
<%= render "table", notifications: @notifications %>
<% end %>
<div class="bd-callout bd-callout-info">
<p><%= t(".explanation") %></p>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1><%= t(".title") %></h1>
<%= render "form", notification: @notification %>
</div>
</div>
</div>

View file

@ -2,7 +2,7 @@
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<nav class="navbar navbar-expand-lg navbar-dark justify-content-between">
<div class="container-fluid">
<%= link_to root_path , class: "navbar-brand" do %>
<%= link_to (user_signed_in? ? checks_path : root_path), class: "navbar-brand" do %>
<%= image_tag 'chexpire10.png', width: 200, alt: 'chexpire logo' %>
<% end %>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
@ -24,6 +24,9 @@
<li class="nav-item">
<%= link_to(t(".GitHub"), "https://github.com/Evolix/chexpire", class: "nav-link") %>
</li>
<li class="nav-item">
<%= link_to(t(".my_notifications"), notifications_path, class: "nav-link") %>
</li>
<% end %>
</ul>

View file

@ -33,8 +33,29 @@ en:
notifications:
recipient: john@example.com
helpers:
submit:
check:
create: "Create Check"
update: "Update Check"
delete: "Destroy Check"
notification:
create: "Create Notification"
update: "Update Notification"
delete: "Destroy Notification"
flashes:
user_not_authorized: "You are not authorized to access to this resource."
checks:
create: The check has been saved.
updated: The check has been updated.
invalid: Please check the form.
destroyed: The check has been destroyed.
notifications:
created: The notification has been created.
updated: The notification has been updated."
destroyed: The notification has been destroyed.
invalid: Please check the form.
notifications_mailer:
domain_expires_soon:
@ -68,6 +89,7 @@ en:
my_checks: "My checks"
new_domain_check: "New domain check"
new_ssl_check: "New SSL check"
my_notifications: "My notifications"
sign_up: "Sign up"
sign_in: "Log in"
sign_out: "Log out"
@ -104,7 +126,9 @@ en:
title: New domain check
ssl:
title: New SSL check
edit:
title: Check edition
destroy_confirmation: Are you sure to destroy this check ?
create:
saved: "Your check has been saved."
invalid: "Please check the form."
@ -127,7 +151,7 @@ en:
domain: Hostname
notifications_hint: |
Receive notifications to warn you when our system detects that the
expiration date is coming. The time is set in number of days.
expiration date is coming.
table:
th:
@ -143,3 +167,36 @@ en:
zero: "Last check successful: today"
one: "Last check successful: yesterday"
other: "Last check successful %{count} days ago"
notifications:
index:
title: "List of your notifications"
no_notification_yet_html: |
You have not set up a notification yet.
<a href="%{new_path}">Create your first notification</a>.
explanation: |
For each of your checks,
you can associate one or more notifications that you will receive
by email at the interval of your choice before the expiration date.
table:
th:
edit: Edit
interval_in_days:
one: 1 day
other: "%{count} days"
edit:
title: "Edit the notification"
checks:
zero: No check associated check.
one: Check associated
other: Checks associated
destroy_confirmation:
zero: "Are you sure ?"
one: |
Are you sure ?
You won't receive this notification for the associated check.
other: |
Are you sure ?
You won't receive this notification for the associated checks.
form:
label_hint: This label allows you to identify this notification and associate it with a check.

View file

@ -13,8 +13,10 @@ fr:
domain_updated_at: "Date de modification"
domain_expires_at: "Date d'expiration"
notification:
label: Étiquette
interval: Délai
recipient: Destinataire
checks_count: Vérifications
user:
tos_accepted: "Conditions d'utilisation"
notifications_enabled: "Notifications activées"
@ -25,8 +27,13 @@ fr:
helpers:
submit:
check:
create: "Créer"
update: "Valider"
create: "Créer la vérification"
update: "Modifier la vérification"
delete: "Supprimer la vérification"
notification:
create: "Créer la notification"
update: "Modifier la notification"
delete: "Supprimer la notification"
page_entries_info:
one_page:
display_entries:
@ -71,6 +78,16 @@ fr:
flashes:
user_not_authorized: "Vous n'êtes pas autorisé•e à accéder à cette ressouce."
checks:
create: La vérification est enregistrée.
updated: La vérification est mise à jour.
invalid: Veuillez vérifier le formulaire.
destroyed: La vérification est supprimée.
notifications:
created: La notification est créée.
updated: "La notification est mise à jour."
destroyed: La notification est supprimée.
invalid: Veuillez vérifier le formulaire.
notifications_mailer:
domain_expires_soon:
@ -101,6 +118,7 @@ fr:
my_checks: "Mes vérifications"
new_domain_check: "Nouveau nom de domaine"
new_ssl_check: "Nouveau certificat SSL"
my_notifications: "Mes notifications"
sign_up: "Enregistrement"
sign_in: "Connexion"
sign_out: "Déconnexion"
@ -137,10 +155,9 @@ fr:
title: Nouvelle vérification d'un nom de domaine
ssl:
title: Nouvelle vérification d'un certificat SSL
create:
saved: La vérification est enregistrée.
invalid: Veuillez vérifier le formulaire.
edit:
title: Modification de la vérification
destroy_confirmation: Confirmez-vous la suppression de cette vérification ?
filters:
kind_domain: Domaine
@ -160,7 +177,7 @@ fr:
domain: Nom d'hôte
notifications_hint: |
Recevez des notifications pour vous avertir lorsque notre système détecte
que la date d'expiration approche. Le délai est indiqué ennombre de jours.
que la date d'expiration approche.
table:
th:
@ -176,3 +193,38 @@ fr:
zero: "Dernière vérification réussie : aujourd'hui"
one: "Dernière vérification réussie : hier"
other: "Dernière vérification réussie il y a %{count} jours"
notifications:
index:
title: "Liste de vos notifications"
no_notification_yet_html: |
Vous n'avez pas encore créé de notification.
<a href="%{new_domain_path}">Créez votre première notification</a>.
explanation: |
Pour chacune de vos vérifications,
vous pouvez associer une ou plusieurs notifications que vous recevrez
par email un certain nombre de jours avant la date d'expiration.
table:
th:
edit: Modifier
interval_in_days:
one: 1 jour
other: "%{count} jours"
new:
title: Nouvelle notification
edit:
title: "Modification d'une notification"
checks:
zero: Aucune vérification associée.
one: Vérification associée
other: Vérifications associées
destroy_confirmation:
zero: "Confirmez-vous la suppression de cette notification ?"
one: |
Confirmez-vous la suppression ?
Vous ne recevrez plus la notification associée à cette vérification.
other: |
Confirmez-vous la suppression ?
Vous ne recevrez plus les notifications associées à ces vérifications.
form:
label_hint: Ce nom vous permet d'identifier cette notification pour l'associer à une vérification.

View file

@ -7,12 +7,13 @@ 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] do
resources :notifications, only: [:destroy]
collection do
post :supports, format: :json
end
end
resources :notifications, except: [:show]
devise_for :users
root to: "pages#home"
@ -22,7 +23,6 @@ end
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy
# supports_checks POST /checks/supports(.:format) checks#supports
# checks GET /checks(.:format) checks#index
# POST /checks(.:format) checks#create
@ -31,6 +31,13 @@ end
# check PATCH /checks/:id(.:format) checks#update
# PUT /checks/:id(.:format) checks#update
# DELETE /checks/:id(.:format) checks#destroy
# notifications GET /notifications(.:format) notifications#index
# POST /notifications(.:format) notifications#create
# new_notification GET /notifications/new(.:format) notifications#new
# edit_notification GET /notifications/:id/edit(.:format) notifications#edit
# notification PATCH /notifications/:id(.:format) notifications#update
# PUT /notifications/:id(.:format) notifications#update
# DELETE /notifications/:id(.:format) notifications#destroy
# new_user_session GET /users/sign_in(.:format) devise/sessions#new
# user_session POST /users/sign_in(.:format) devise/sessions#create
# destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
@ -56,7 +63,7 @@ end
# rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
# update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
# rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
#
#
# Routes for LetterOpenerWeb::Engine:
# clear_letters DELETE /clear(.:format) letter_opener_web/letters#clear
# delete_letter DELETE /:id(.:format) letter_opener_web/letters#destroy

View file

@ -1,3 +1,6 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class AddConsecutiveFailuresToChecks < ActiveRecord::Migration[5.2]
def change
add_column :checks, :consecutive_failures, :integer, default: 0, null: false

View file

@ -0,0 +1,15 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class CreateCheckNotifications < ActiveRecord::Migration[5.2]
def change
create_table :check_notifications do |t|
t.references :check, foreign_key: true
t.references :notification, foreign_key: true
t.integer :status, null: false, default: 0, limit: 1
t.datetime :sent_at
t.timestamps
end
end
end

View file

@ -0,0 +1,41 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class AddFieldsToNotifications < ActiveRecord::Migration[5.2]
def change
add_reference :notifications, :user, foreign_key: true
add_column :notifications, :label, :string
add_column :notifications, :checks_count, :integer, default: 0, null: false
reversible do |dir|
dir.up do
# first set user & label for *all* notifications
Notification.find_each do |notification|
check = Check.find(notification.check_id) # check relation does not exist anymore
notification.user_id = check.user_id
notification.save!
end
# then build the equivalent check notification
Notification.find_each do |notification|
assoc_notification = Notification.where(
user_id: notification.user_id,
recipient: notification.recipient,
interval: notification.interval,
).order(checks_count: :desc).limit(1).first
CheckNotification.create!(
check_id: notification.check_id,
notification: assoc_notification,
status: notification.status,
sent_at: notification.sent_at
)
end
# last delete duplicate notification templates not used
Notification.where(checks_count: 0).destroy_all
end
end
end
end

View file

@ -0,0 +1,9 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class RemoveObsoleteFieldsToNotifications < ActiveRecord::Migration[5.2]
def change
remove_column :notifications, :status, :integer, null: false, limit: 1, default: 0
remove_column :notifications, :sent_at, :datetime
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_08_29_134404) do
ActiveRecord::Schema.define(version: 2018_08_30_083927) do
create_table "check_logs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "check_id"
@ -24,6 +24,17 @@ ActiveRecord::Schema.define(version: 2018_08_29_134404) do
t.index ["check_id"], name: "index_check_logs_on_check_id"
end
create_table "check_notifications", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "check_id"
t.bigint "notification_id"
t.integer "status", limit: 1, 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_check_notifications_on_check_id"
t.index ["notification_id"], name: "index_check_notifications_on_notification_id"
end
create_table "checks", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "user_id"
t.integer "kind", null: false
@ -38,8 +49,8 @@ ActiveRecord::Schema.define(version: 2018_08_29_134404) do
t.boolean "active", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "round_robin", default: true
t.integer "consecutive_failures", default: 0, null: false
t.boolean "round_robin", default: true
t.integer "mode", default: 0, null: false
t.index ["user_id"], name: "index_checks_on_user_id"
end
@ -49,11 +60,13 @@ ActiveRecord::Schema.define(version: 2018_08_29_134404) do
t.integer "channel", default: 0, null: false
t.string "recipient", null: false
t.integer "interval", 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.bigint "user_id"
t.string "label"
t.integer "checks_count", default: 0, null: false
t.index ["check_id"], name: "index_notifications_on_check_id"
t.index ["user_id"], name: "index_notifications_on_user_id"
end
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
@ -82,6 +95,9 @@ ActiveRecord::Schema.define(version: 2018_08_29_134404) do
end
add_foreign_key "check_logs", "checks"
add_foreign_key "check_notifications", "checks"
add_foreign_key "check_notifications", "notifications"
add_foreign_key "checks", "users"
add_foreign_key "notifications", "checks"
add_foreign_key "notifications", "users"
end

View file

@ -1,6 +1,3 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
require "test_helper"
class NotificationsControllerTest < ActionDispatch::IntegrationTest

View file

@ -0,0 +1,44 @@
# == Schema Information
#
# Table name: check_notifications
#
# id :bigint(8) not null, primary key
# sent_at :datetime
# status :integer default("pending"), not null
# created_at :datetime not null
# updated_at :datetime not null
# check_id :bigint(8)
# notification_id :bigint(8)
#
# Indexes
#
# index_check_notifications_on_check_id (check_id)
# index_check_notifications_on_notification_id (notification_id)
#
# Foreign Keys
#
# fk_rails_... (check_id => checks.id)
# fk_rails_... (notification_id => notifications.id)
#
FactoryBot.define do
factory :check_notification do
check
notification
status :pending
sent_at nil
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

View file

@ -86,7 +86,9 @@ FactoryBot.define do
trait :with_notifications do
after :create do |check|
create_list :notification, 2, check: check
create_list :check_notification, 2,
check: check,
notification: build(:notification, user: check.user)
end
end
end

View file

@ -1,53 +1,41 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Jeremy Lecour <jlecour@evolix.fr>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
# == Schema Information
#
# Table name: notifications
#
# id :bigint(8) not null, primary key
# channel :integer default("email"), not null
# interval :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)
# id :bigint(8) not null, primary key
# channel :integer default("email"), not null
# checks_count :integer default(0), not null
# interval :integer not null
# label :string(255)
# recipient :string(255) not null
# created_at :datetime not null
# updated_at :datetime not null
# check_id :bigint(8)
# user_id :bigint(8)
#
# Indexes
#
# index_notifications_on_check_id (check_id)
# index_notifications_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (check_id => checks.id)
# fk_rails_... (user_id => users.id)
#
FactoryBot.define do
factory :notification do
check
user
interval 30
channel :email
label { "#{recipient} (#{interval})" }
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

View file

@ -6,10 +6,11 @@ require "test_helper"
class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics/ClassLength
test "domain_expires_soon" do
check = create(:check, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
notification = build(:notification, interval: 10, recipient: "colin@example.org")
check_notification = build(:check_notification, check: check, notification: notification)
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
mail = NotificationsMailer.with(check_notification: check_notification).domain_expires_soon
assert_emails 1 do
mail.deliver_now
@ -37,10 +38,11 @@ class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics
check = create(:check,
domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"),
user: build(:user, :fr))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
notification = build(:notification, interval: 10, recipient: "colin@example.org")
check_notification = build(:check_notification, check: check, notification: notification)
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
mail = NotificationsMailer.with(check_notification: check_notification).domain_expires_soon
assert_emails 1 do
mail.deliver_now
@ -69,9 +71,9 @@ class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics
domain_expires_at: 1.week.from_now,
comment: "My comment",
vendor: "The vendor")
notification = build(:notification, check: check)
check_notification = build(:check_notification, check: check)
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
mail = NotificationsMailer.with(check_notification: check_notification).domain_expires_soon
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
@ -87,9 +89,9 @@ class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics
comment: "My comment",
vendor: "The vendor",
user: build(:user, :fr))
notification = build(:notification, check: check)
check_notification = build(:check_notification, check: check)
mail = NotificationsMailer.with(notification: notification).domain_expires_soon
mail = NotificationsMailer.with(check_notification: check_notification).domain_expires_soon
parts = [mail.text_part.decode_body, mail.html_part.decode_body]
@ -158,10 +160,11 @@ class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics
test "ssl_expires_soon" do
check = create(:check, :ssl, domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
notification = build(:notification, interval: 10, recipient: "colin@example.org")
check_notification = build(:check_notification, check: check, notification: notification)
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).ssl_expires_soon
mail = NotificationsMailer.with(check_notification: check_notification).ssl_expires_soon
assert_emails 1 do
mail.deliver_now
@ -190,10 +193,11 @@ class NotificationsMailerTest < ActionMailer::TestCase # rubocop:disable Metrics
check = create(:check, :ssl,
domain_expires_at: Time.new(2018, 6, 10, 12, 0, 5, "+02:00"),
user: build(:user, :fr))
notification = build(:notification, interval: 10, check: check, recipient: "colin@example.org")
notification = build(:notification, interval: 10, recipient: "colin@example.org")
check_notification = build(:check_notification, check: check, notification: notification)
Date.stub :today, Date.new(2018, 6, 2) do
mail = NotificationsMailer.with(notification: notification).ssl_expires_soon
mail = NotificationsMailer.with(check_notification: check_notification).ssl_expires_soon
assert_emails 1 do
mail.deliver_now

View file

@ -0,0 +1,30 @@
# == Schema Information
#
# Table name: check_notifications
#
# id :bigint(8) not null, primary key
# sent_at :datetime
# status :integer default("pending"), not null
# created_at :datetime not null
# updated_at :datetime not null
# check_id :bigint(8)
# notification_id :bigint(8)
#
# Indexes
#
# index_check_notifications_on_check_id (check_id)
# index_check_notifications_on_notification_id (notification_id)
#
# Foreign Keys
#
# fk_rails_... (check_id => checks.id)
# fk_rails_... (notification_id => notifications.id)
#
require "test_helper"
class CheckNotificationTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -15,6 +15,7 @@
# kind :integer not null
# last_run_at :datetime
# last_success_at :datetime
# mode :integer default("auto"), not null
# round_robin :boolean default(TRUE)
# vendor :string(255)
# created_at :datetime not null
@ -35,23 +36,23 @@ require "test_helper"
class CheckTest < ActiveSupport::TestCase
test "notifications are resetted when domain expiration date has changed" do
check = create(:check)
notification = create(:notification, :succeed, check: check)
check_notification = create(:check_notification, :succeed, check: check)
check.comment = "Will not reset because of this attribute"
check.save!
notification.reload
check_notification.reload
assert notification.succeed?
assert_not_nil notification.sent_at
assert check_notification.succeed?
assert_not_nil check_notification.sent_at
check.domain_expires_at = 1.year.from_now
check.save!
notification.reload
check_notification.reload
assert notification.pending?
assert_nil notification.sent_at
assert check_notification.pending?
assert_nil check_notification.sent_at
end
test "days_from_last_success without any success" do

View file

@ -1,27 +1,29 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Jeremy Lecour <jlecour@evolix.fr>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
# == Schema Information
#
# Table name: notifications
#
# id :bigint(8) not null, primary key
# channel :integer default("email"), not null
# interval :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)
# id :bigint(8) not null, primary key
# channel :integer default("email"), not null
# checks_count :integer default(0), not null
# interval :integer not null
# label :string(255)
# recipient :string(255) not null
# created_at :datetime not null
# updated_at :datetime not null
# check_id :bigint(8)
# user_id :bigint(8)
#
# Indexes
#
# index_notifications_on_check_id (check_id)
# index_notifications_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (check_id => checks.id)
# fk_rails_... (user_id => users.id)
#
require "test_helper"

View file

@ -0,0 +1,35 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
require "test_helper"
class CheckNotificationPolicyTest < ActiveSupport::TestCase
setup do
@owner, @other = create_list(:user, 2)
@check_notification = create(:check_notification, check: build(:check, user: @owner))
end
test "permit to check user" do
assert_permit @owner, @check_notification, :destroy
end
test "disallow to anonymous and other user" do
refute_permit @other, @check_notification, :destroy
refute_permit nil, @check_notification, :destroy
end
test "scope only to user checks" do
other_notifications = create_list(:check_notification, 2, check: build(:check, user: @other))
assert_empty Pundit.policy_scope!(nil, CheckNotification)
assert_equal [@check_notification], Pundit.policy_scope!(@owner, CheckNotification)
assert_equal other_notifications, Pundit.policy_scope!(@other, CheckNotification)
end
test "disabled actions" do
refute_permit @owner, @check_notification, :update
refute_permit @owner, @check_notification, :edit
refute_permit @owner, @check_notification, :create
refute_permit @owner, @check_notification, :index
end
end

View file

@ -6,30 +6,32 @@ require "test_helper"
class NotificationPolicyTest < ActiveSupport::TestCase
setup do
@owner, @other = create_list(:user, 2)
@notification = create(:notification, check: build(:check, user: @owner))
@notification = create(:notification, user: @owner)
end
test "permit to check user" do
test "create" do
assert_permit @other, Notification, :create
assert_permit @other, Notification, :new
end
test "permit to owner" do
assert_permit @owner, @notification, :edit
assert_permit @owner, @notification, :update
assert_permit @owner, @notification, :destroy
end
test "disallow to anonymous and other user" do
refute_permit @other, @notification, :destroy
refute_permit nil, @notification, :destroy
%i[update edit destroy].each do |action|
refute_permit @other, @notification, action
refute_permit nil, @notification, action
end
end
test "scope only to user checks" do
other_notifications = create_list(:notification, 2, check: build(:check, user: @other))
test "scope only to owners" do
other_notifications = create_list(:notification, 2, 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

View file

@ -8,8 +8,8 @@ module Notifier
class BaseTest < ActiveSupport::TestCase
setup do
class FakeChannel < Base
def supports?(notification)
notification.interval < 1_000
def supports?(check_notification)
check_notification.notification.interval < 1_000
end
def domain_notify_expires_soon(*); end
@ -19,45 +19,46 @@ module Notifier
end
test "#notify change the status of the notification" do
notification = create(:notification)
check_notification = create(:check_notification)
@channel.notify(notification)
@channel.notify(check_notification)
notification.reload
check_notification.reload
assert notification.ongoing?
assert_just_now notification.sent_at
assert check_notification.ongoing?
assert_just_now check_notification.sent_at
end
test "#notify raises an exception for a non supported check kind" do
notification = Minitest::Mock.new
notification.expect :ongoing!, true
notification.expect :interval, 10
check_notification = Minitest::Mock.new
check_notification.expect :ongoing!, true
check_notification.expect :notification, OpenStruct.new(interval: 10)
check = Minitest::Mock.new
check.expect(:kind, :invalid_kind)
check.expect(:kind, :invalid_kind) # twice (second call for exception message)
notification.expect :check, check
notification.expect :check, check
check_notification.expect :check, check
check_notification.expect :check, check
assert_raises ArgumentError do
@channel.notify(notification)
@channel.notify(check_notification)
end
check.verify
notification.verify
check_notification.verify
end
test "#notify does nothing when channel doesn't support a notification whatever the reason" do
notification = create(:notification, interval: 10_000)
check_notification = create(:check_notification,
notification: build(:notification, interval: 10_000))
@channel.notify(notification)
@channel.notify(check_notification)
notification.reload
check_notification.reload
assert notification.pending?
assert_nil notification.sent_at
assert check_notification.pending?
assert_nil check_notification.sent_at
end
end
end

View file

@ -5,10 +5,11 @@ require "test_helper"
module Notifier
class ProcessorTest < ActiveSupport::TestCase
# rubocop:disable Metrics/LineLength
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))
create_list(:check_notification, 3, notification: email_notification, check: build(:check, :expires_next_week))
create(:check_notification, notification: email_notification, check: build(:check, :nil_dates))
create(:check_notification, notification: email_notification, check: build(:check, :inactive))
processor = Processor.new
assert_difference "ActionMailer::Base.deliveries.size", +3 do
@ -20,7 +21,7 @@ module Notifier
end
test "#process_expires_soon respects the interval configuration between sends" do
create_list(:notification, 3, :email, check: build(:check, :expires_next_week))
create_list(:check_notification, 3, notification: email_notification, check: build(:check, :expires_next_week))
test_interval_respected(:process_expires_soon, 3)
end
@ -30,9 +31,14 @@ module Notifier
configuration.expect(:consecutive_failures, 4.2)
end
end
# rubocop:enable Metrics/LineLength
private
def email_notification
build(:notification, :email)
end
# rubocop:disable Metrics/MethodLength
def test_interval_respected(process_method, count_expected)
configuration = Minitest::Mock.new

View file

@ -3,6 +3,7 @@
require "test_helper"
# rubocop:disable Metrics/LineLength
module Notifier
class ResolverTest < ActiveSupport::TestCase
setup do
@ -10,74 +11,74 @@ module Notifier
end
test "#notifications_expiring_soon ignores user having notification disabled" do
n1 = create(:notification, check: build(:check, :expires_next_week))
n1 = create(:check_notification, check: build(:check, :expires_next_week))
n1.check.user.update_attribute(:notifications_enabled, false)
n2 = create(:notification, check: build(:check, :expires_next_week))
n2 = create(:check_notification, check: build(:check, :expires_next_week))
notifications = @resolver.notifications_expiring_soon
check_notifications = @resolver.notifications_expiring_soon
assert_not_includes notifications, n1
assert_includes notifications, n2
assert_not_includes check_notifications, n1
assert_includes check_notifications, n2
end
test "#notifications_expiring_soon ignores inactive checks" do
n1 = create(:notification, check: build(:check, :expires_next_week, :inactive))
n2 = create(:notification, check: build(:check, :expires_next_week))
n1 = create(:check_notification, check: build(:check, :expires_next_week, :inactive))
n2 = create(:check_notification, check: build(:check, :expires_next_week))
notifications = @resolver.notifications_expiring_soon
check_notifications = @resolver.notifications_expiring_soon
assert_not_includes notifications, n1
assert_includes notifications, n2
assert_not_includes check_notifications, n1
assert_includes check_notifications, n2
end
test "#notifications_expiring_soon gets only checks inside interval" do
n1 = create(:notification, check: build(:check, :expires_next_week), interval: 6)
n2 = create(:notification, check: build(:check, :expires_next_week), interval: 7)
n1 = create(:check_notification, check: build(:check, :expires_next_week), notification: build(:notification, interval: 6))
n2 = create(:check_notification, check: build(:check, :expires_next_week), notification: build(:notification, interval: 7))
notifications = @resolver.notifications_expiring_soon
check_notifications = @resolver.notifications_expiring_soon
assert_not_includes notifications, n1
assert_includes notifications, n2
assert_not_includes check_notifications, n1
assert_includes check_notifications, n2
end
test "#notifications_expiring_soon can gets several notifications for a same check" do
check = create(:check, :expires_next_week)
n1 = create(:notification, check: check, interval: 3)
n2 = create(:notification, check: check, interval: 10)
n3 = create(:notification, check: check, interval: 30)
n1 = create(:check_notification, check: check, notification: build(:notification, interval: 3))
n2 = create(:check_notification, check: check, notification: build(:notification, interval: 10))
n3 = create(:check_notification, check: check, notification: build(:notification, interval: 30))
notifications = @resolver.notifications_expiring_soon
check_notifications = @resolver.notifications_expiring_soon
assert_not_includes notifications, n1
assert_includes notifications, n2
assert_includes notifications, n3
assert_not_includes check_notifications, n1
assert_includes check_notifications, n2
assert_includes check_notifications, n3
end
test "#notifications_expiring_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)
n1 = create(:check_notification, check: check)
n2 = create(:check_notification, :failed, check: check)
n3 = create(:check_notification, :ongoing, check: check)
n4 = create(:check_notification, :succeed, check: check)
notifications = @resolver.notifications_expiring_soon
check_notifications = @resolver.notifications_expiring_soon
assert_includes notifications, n1
assert_includes notifications, n2
assert_not_includes notifications, n3
assert_not_includes notifications, n4
assert_includes check_notifications, n1
assert_includes check_notifications, n2
assert_not_includes check_notifications, n3
assert_not_includes check_notifications, n4
end
test "#notifications_expiring_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))
n1 = create(:check_notification, check: build(:check, :expires_next_week))
n2 = create(:check_notification, check: build(:check, domain_expires_at: 1.week.ago))
n3 = create(:check_notification, check: build(:check, :nil_dates))
notifications = @resolver.notifications_expiring_soon
check_notifications = @resolver.notifications_expiring_soon
assert_includes notifications, n1
assert_not_includes notifications, n2
assert_not_includes notifications, n3
assert_includes check_notifications, n1
assert_not_includes check_notifications, n2
assert_not_includes check_notifications, n3
end
test "#checks_recurrent_failures ignores inactive checks" do
@ -116,3 +117,4 @@ module Notifier
end
end
end
# rubocop:enable Metrics/LineLength

View file

@ -9,7 +9,7 @@ class ChecksTest < ApplicationSystemTestCase
login_as(@user)
end
test "create a check and a notification without kind" do
test "create a check and a new notification without kind" do
visit new_check_path
choose "domain"
@ -53,20 +53,34 @@ class ChecksTest < ApplicationSystemTestCase
fill_and_valid_new_check
end
test "remove a notification" do
check = create(:check, :with_notifications, domain: "dom-with-notif.net", user: @user)
test "dissociate a notification" do
check = create(:check, :with_notifications, user: @user)
notification = create(:notification, label: "label-notification", user: @user)
check.notifications << notification
visit edit_check_path(check)
notification = check.notifications.first
selector = "[data-notification-id=\"#{notification.id}\"]"
uncheck notification.label
assert_difference "Notification.where(check_id: #{check.id}).count", -1 do
within selector do
find(".btn-danger").click
end
click_button "Update Check"
page.has_no_content?(selector)
end
notification.reload
assert_equal 0, notification.checks_count
assert_equal 2, check.check_notifications.count
end
test "associate a notification" do
check = create(:check, user: @user)
notification = create(:notification, label: "label-notification", user: @user)
visit edit_check_path(check)
check notification.label
click_button "Update Check"
notification.reload
assert_equal 1, notification.checks_count
assert_equal 1, check.check_notifications.count
end
test "update a check" do
@ -84,29 +98,6 @@ class ChecksTest < ApplicationSystemTestCase
assert_equal "My comment", check.comment
end
test "add a notification" do
check = create(:check, :with_notifications, domain: "dom-with-notif.net", user: @user)
visit edit_check_path(check)
recipient = "recipient2@example.org"
fill_in("check[notifications_attributes][2][recipient]", with: recipient)
fill_in("check[notifications_attributes][2][interval]", 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.interval
assert notification.email?
assert notification.pending?
end
test "list my checks" do
create(:check, :domain, domain: "dom.com", domain_expires_at: Time.new(2018, 7, 5, 12), user: @user) # rubocop:disable Metrics/LineLength
create(:check, :ssl, domain: "ssldom.com", user: @user)
@ -286,6 +277,8 @@ class ChecksTest < ApplicationSystemTestCase
fill_in("check[domain]", with: domain)
recipient = "recipient@example.org"
label = "my new notificatiion"
fill_in("check[notifications_attributes][0][label]", with: label)
fill_in("check[notifications_attributes][0][recipient]", with: recipient)
fill_in("check[notifications_attributes][0][interval]", with: 30)
@ -297,10 +290,14 @@ class ChecksTest < ApplicationSystemTestCase
assert page.has_content?(domain)
notification = Notification.last
assert_equal label, notification.label
assert_equal recipient, notification.recipient
assert_equal 30, notification.interval
assert notification.email?
assert notification.pending?
check_notification = CheckNotification.last
assert check_notification.pending?
assert_nil check_notification.sent_at
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength

View file

@ -52,7 +52,7 @@ class UsersTest < ApplicationSystemTestCase
click_button I18n.t("devise.sessions.new.sign_in")
assert_equal root_path, page.current_path
assert_equal checks_path, page.current_path
assert page.has_content?(@user.email)
end