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

Merge pull request #90 from Evolix/unsupported-whois

Manual mode for unsupported TLDs
This commit is contained in:
Colin Darie 2018-08-30 19:04:27 +02:00 committed by GitHub
commit 0f3571b3bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 248 additions and 38 deletions

View file

@ -3,7 +3,7 @@
class ChecksController < ApplicationController
before_action :authenticate_user!
before_action :set_check, except: [:index, :new, :create]
before_action :set_check, except: [:index, :new, :create, :supports]
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
@ -65,6 +65,11 @@ class ChecksController < ApplicationController
redirect_to checks_path
end
def supports
@check = Check.new(new_check_params)
authorize @check
end
private
def set_check
@ -82,7 +87,7 @@ class ChecksController < ApplicationController
def check_params(*others)
params.require(:check)
.permit(:domain, :domain_created_at, :comment, :vendor, :round_robin, *others,
.permit(:domain, :domain_expires_at, :comment, :vendor, :round_robin, *others,
notifications_attributes: [:id, :channel, :recipient, :interval])
end

View file

@ -0,0 +1,57 @@
// Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
// License: GNU AGPL-3+ (see full text in LICENSE file)
function checkValidationInitialize() {
const element = document.getElementById("check_domain");
if (element && element.dataset.kind == "domain") {
addEventSupportListener(element);
}
}
function addEventSupportListener(element) {
element.addEventListener("blur", event => {
const request = $.ajax("/checks/supports.json", {
method: "post",
dataType: "json",
data: {
check: {
domain: event.target.value,
kind: element.dataset.kind,
}
}
})
request.done(response => {
const { supported } = response.check;
toggleUnsupportedContainers(supported);
setFocus(supported);
// set normalized domain
element.value = response.check.domain;
});
});
}
function toggleUnsupportedContainers(supported) {
const containerClass = supported ? "d-none" : "d-block";
document.getElementById("check_domain_expires_at_container").className = containerClass;
const domainHint = document.getElementById("check_domain_unsupported_container");
domainHint.classList.remove("d-none");
domainHint.classList.remove("d-block");
domainHint.classList.add(containerClass);
}
function setFocus(supported) {
if (supported) {
return;
}
document.getElementById("check_domain_expires_at").focus();
}
export default checkValidationInitialize;

View file

@ -20,9 +20,13 @@ import 'bootstrap/js/dist/tooltip';
import '../scss';
import checkValidationInitialize from '../components/check_validation';
Rails.start()
Turbolinks.start()
document.addEventListener("turbolinks:load", () => {
$('[data-toggle="tooltip"]').tooltip();
checkValidationInitialize();
});

View file

@ -1,7 +1,7 @@
// Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
// License: GNU AGPL-3+ (see full text in LICENSE file)
$input-placeholder-color: #013d3a;
$input-placeholder-color: #b9bbbb;
$enable-rounded: false;
$theme-colors: (
"primary": #118b83, //light-green

View file

@ -20,9 +20,6 @@ class WhoisSyncJob < ApplicationJob
return unless response.valid?
update_from_response(response)
rescue Whois::DomainNotFoundError
check.active = false
check.save!
end
def update_from_response(response)

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
@ -39,6 +40,7 @@ class Check < ApplicationRecord
reject_if: lambda { |at| at["recipient"].blank? && at["interval"].blank? }
enum kind: [:domain, :ssl]
enum mode: [:auto, :manual]
self.skip_time_zone_conversion_for_attributes = [
:domain_created_at,
@ -50,10 +52,12 @@ class Check < ApplicationRecord
validates :domain, presence: true
validate :domain_created_at_past
validate :domain_updated_at_past
validates :domain_expires_at, presence: true, unless: :supported?
validates :comment, length: { maximum: 255 }
validates :vendor, length: { maximum: 255 }
before_save :reset_consecutive_failures
before_save :set_mode
after_update :reset_notifications
after_save :enqueue_sync
@ -84,6 +88,20 @@ class Check < ApplicationRecord
save!
end
def supported?
return true unless domain?
return true if domain.blank?
begin
Whois::Parser.for(domain)
true
rescue Whois::UnsupportedDomainError
false
rescue StandardError
false
end
end
private
def domain_created_at_past
@ -113,4 +131,9 @@ class Check < ApplicationRecord
self.consecutive_failures = 0
end
def set_mode
return unless domain_changed?
self.mode = supported? ? :auto : :manual
end
end

View file

@ -20,6 +20,10 @@ class CheckPolicy < ApplicationPolicy
owner?
end
def supports?
new?
end
private
def owner?

View file

@ -54,6 +54,7 @@ module CheckProcessor
def base_scope
Check
.active
.auto
.where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)")
end

View file

@ -3,13 +3,33 @@
<%= simple_form_for(check) do |f| %>
<%= f.input :domain,
autofocus: true,
input_html: { autocapitalize: :none, autocorrect: :off },
label: t(".#{check.kind || "generic" }.domain") %>
input_html: { autocapitalize: :none, autocorrect: :off, data: { kind: check.kind } },
label: t(".#{check.kind || "generic" }.domain"),
hint: t(".#{check.kind || "generic" }.unsupported"),
hint_html: {
id: "check_domain_unsupported_container",
class: "#{check.supported? && 'd-none'}",
}
%>
<% if check.new_record? %>
<%= f.input :kind, as: check.kind.present? ? :hidden : :radio_buttons, collection: Check.kinds.keys %>
<% end %>
<div id="check_domain_expires_at_container" class="<%= check.supported? ? "d-none" : "d-block" %>">
<%= f.input :domain_expires_at,
required: true,
input_html: {
type: :date,
value: check.domain_expires_at&.to_date,
min: Date.yesterday,
max: 10.years.from_now.end_of_year.to_date
},
as: :string,
placeholder: t(".domain_expires_at_placeholder")
%>
</div>
<%= f.input :comment %>
<%= f.input :vendor %>

View file

@ -0,0 +1,7 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
json.check do
json.supported @check.supported?
json.domain normalize_domain(@check.domain)
end

View file

@ -119,6 +119,10 @@ en:
domain: Domain
domain:
domain: Domain name
unsupported: |
This top-level domain isn't currently automatically supported.
You'll have to fill and maintain yourself the expiry date.
domain_expires_at_placeholder: YYYY-MM-DD.
ssl:
domain: Hostname
notifications_hint: |

View file

@ -11,6 +11,7 @@ fr:
kind: Type
domain_created_at: "Date de création"
domain_updated_at: "Date de modification"
domain_expires_at: "Date d'expiration"
notification:
interval: Délai
recipient: Destinataire
@ -151,6 +152,10 @@ fr:
domain: Domaine
domain:
domain: Nom de domaine
unsupported: |
Cette extension n'est pas supportée automatiquement actuellement.
Vous devrez saisir et maintenir vous-même sa date d'expiration.
domain_expires_at_placeholder: AAAA-MM-JJ
ssl:
domain: Nom d'hôte
notifications_hint: |

View file

@ -1,10 +1,29 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
# In order to update the route map below,
# run `bundle exec annotate -r` after modifying this file
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
devise_for :users
root to: "pages#home"
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
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
# new_check GET /checks/new(.:format) checks#new
@ -44,18 +63,3 @@
# letters GET / letter_opener_web/letters#index
# letter GET /:id(/:style)(.:format) letter_opener_web/letters#show
# GET /:id/attachments/:file(.:format) letter_opener_web/letters#attachment
# In order to update the route map above,
# run `bundle exec annotate -r` after modifying this file
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]
end
devise_for :users
root to: "pages#home"
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
end

View file

@ -0,0 +1,5 @@
class AddModeToChecks < ActiveRecord::Migration[5.2]
def change
add_column :checks, :mode, :integer, default: 0, null: false
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_01_072038) do
ActiveRecord::Schema.define(version: 2018_08_29_134404) do
create_table "check_logs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "check_id"
@ -40,6 +40,7 @@ ActiveRecord::Schema.define(version: 2018_08_01_072038) do
t.datetime "updated_at", null: false
t.boolean "round_robin", default: true
t.integer "consecutive_failures", default: 0, null: false
t.integer "mode", default: 0, null: false
t.index ["user_id"], name: "index_checks_on_user_id"
end

View file

@ -8,7 +8,7 @@ if Rails.env.development?
# same name.
Annotate.set_defaults(
'routes' => 'before',
'position_in_routes' => 'before',
'position_in_routes' => 'after',
'position_in_class' => 'before',
'position_in_test' => 'before',
'position_in_fixture' => 'before',

14
lib/tasks/one_shot.rake Normal file
View file

@ -0,0 +1,14 @@
namespace :one_shot do
desc "Set manual mode for unsupported checks"
task reset_checks_modes: :environment do
Check.domain.find_each do |check|
check.mode = if check.supported?
:auto
else
:manual
end
check.save(validate: false)
end
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
@ -44,6 +45,7 @@ FactoryBot.define do
last_run_at nil
last_success_at nil
consecutive_failures 0
mode :auto
trait :domain do
kind :domain

View file

@ -24,7 +24,6 @@ class WhoisSyncJobTest < ActiveJob::TestCase
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.perform_now(check.id)
@ -34,7 +33,6 @@ class WhoisSyncJobTest < ActiveJob::TestCase
assert_just_now check.last_run_at
assert_nil check.last_success_at
assert_equal original_updated_at, check.updated_at
assert check.active?
assert_equal 1, check.consecutive_failures
end
@ -62,7 +60,7 @@ class WhoisSyncJobTest < ActiveJob::TestCase
assert_equal 1, check.consecutive_failures
end
test "disable check when whois responds domain not found" do
test "increment consecutive failures when whois responds domain not found" do
domain = "willneverexist.fr"
check = create(:check, :nil_dates, domain: domain)
@ -72,7 +70,6 @@ class WhoisSyncJobTest < ActiveJob::TestCase
check.reload
refute check.active?
assert_just_now check.last_run_at
assert_nil check.last_success_at
assert_equal 1, check.consecutive_failures

View file

@ -71,4 +71,34 @@ class CheckTest < ActiveSupport::TestCase
check = build(:check, last_success_at: (10.1 * 24).hours.ago)
assert_equal 10, check.days_from_last_success
end
test "supported? for domain" do
check = build(:check, :domain, domain: "domain.fr")
assert check.supported?
check = build(:check, :domain, domain: "domain.cn")
refute check.supported?
# an empty domain name is still considered as supported
check = build(:check, :domain, domain: "")
assert check.supported?
end
test "supported? for SSL" do
check = build(:check, :ssl)
assert check.supported?
check = build(:check, :ssl, domain: "domain.cn")
assert check.supported?
end
test "set mode before saving" do
check = build(:check, domain: "domain.fr")
check.save!
assert check.auto?
check.domain = "domain.xyz"
check.save!
assert check.mode?
end
end

View file

@ -108,6 +108,16 @@ class CheckProcessorTest < ActiveSupport::TestCase
assert_not_includes checks, c2
end
test "resolvers does not include manual checks" do
c1 = create(:check, :expires_next_week)
c2 = create(:check, :expires_next_week, domain: "fff.wxyz")
checks = @processor.resolve_expire_short_term
assert_includes checks, c1
assert_not_includes checks, c2
end
test "#sync_dates respects the interval configuration between sends" do
create_list(:check, 3, :expires_next_week)

View file

@ -25,6 +25,26 @@ class ChecksTest < ApplicationSystemTestCase
fill_and_valid_new_check
end
test "create a manual domain check" do
visit new_check_path(kind: :domain)
domain = "unsupported.wxyz"
fill_in("check[domain]", with: domain)
page.find("body").click # simulate blur
fill_in("check[domain_expires_at]", with: "2022-04-05")
click_button
assert_equal checks_path, page.current_path
assert page.has_css?(".alert-success")
assert page.has_content?(domain)
check = Check.last
assert_equal Date.new(2022, 4, 5), check.domain_expires_at
end
test "create a predefined ssl check" do
visit new_check_path(kind: :ssl)