From 54e38c9c3df5e550acd1cddd79176373570655e9 Mon Sep 17 00:00:00 2001 From: Letiste Date: Mon, 10 Apr 2023 16:20:10 +0200 Subject: [PATCH 1/2] add task to send periodic email for subscription expiration --- Guardfile | 22 +++++++--- app/mailers/application_mailer.rb | 2 +- app/mailers/user_mailer.rb | 17 +++++++ app/views/layouts/mailer.html.erb | 16 +++---- .../internet_expiration_1_day.html.erb | 1 + .../internet_expiration_1_day.text.erb | 1 + .../internet_expiration_7_days.html.erb | 1 + .../internet_expiration_7_days.text.erb | 1 + lib/tasks/internet_expiration_mail.rake | 20 +++++++++ .../internet_expiration_1_day.html | 14 ++++++ .../internet_expiration_1_day.text | 1 + .../internet_expiration_7_days.html | 14 ++++++ .../internet_expiration_7_days.text | 1 + test/mailers/previews/user_mailer_preview.rb | 8 ++++ test/mailers/user_mailer_test.rb | 41 +++++++++++++++++ test/tasks/internet_expiration_mail_test.rb | 44 +++++++++++++++++++ test/tasks/sync_accounts_test.rb | 1 + 17 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 app/mailers/user_mailer.rb create mode 100644 app/views/user_mailer/internet_expiration_1_day.html.erb create mode 100644 app/views/user_mailer/internet_expiration_1_day.text.erb create mode 100644 app/views/user_mailer/internet_expiration_7_days.html.erb create mode 100644 app/views/user_mailer/internet_expiration_7_days.text.erb create mode 100644 lib/tasks/internet_expiration_mail.rake create mode 100644 test/fixtures/user_mailer/internet_expiration_1_day.html create mode 100644 test/fixtures/user_mailer/internet_expiration_1_day.text create mode 100644 test/fixtures/user_mailer/internet_expiration_7_days.html create mode 100644 test/fixtures/user_mailer/internet_expiration_7_days.text create mode 100644 test/mailers/previews/user_mailer_preview.rb create mode 100644 test/mailers/user_mailer_test.rb create mode 100644 test/tasks/internet_expiration_mail_test.rb diff --git a/Guardfile b/Guardfile index 94504338..78613f05 100644 --- a/Guardfile +++ b/Guardfile @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'active_support/inflector' + # Defines the matching rules for Guard. guard :minitest, spring: 'bin/rails test', all_on_start: false do # rubocop:disable Metrics/BlockLength watch(%r{^test/(.*)/?(.*)_test\.rb$}) @@ -9,9 +11,21 @@ guard :minitest, spring: 'bin/rails test', all_on_start: false do # rubocop:disa watch(%r{^app/models/(.*?)\.rb$}) do |matches| "test/models/#{matches[1]}_test.rb" end + watch(%r{^test/fixtures/(.*?)\.yml$}) do |matches| + "test/models/#{matches[1].singularize}_test.rb" + end watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches| resource_tests(matches[1]) end + watch(%r{^app/mailers/(.*?)\.rb$}) do |matches| + "test/mailers/#{matches[1]}_test.rb" + end + watch(%r{^test/fixtures/(.*)/(.*?)\.(html|text)$}) do |matches| + "test/mailers/#{matches[1]}_test.rb" + end + watch(%r{^lib/tasks/(.*?)\.rake$}) do |matches| + "test/tasks/#{matches[1]}_test.rb" + end watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches| ["test/controllers/#{matches[1]}_controller_test.rb"] + integration_tests(matches[1]) @@ -29,14 +43,8 @@ guard :minitest, spring: 'bin/rails test', all_on_start: false do # rubocop:disa ['test/controllers/sessions_controller_test.rb', 'test/integration/users_login_test.rb'] end - watch('app/controllers/account_activations_controller.rb') do - 'test/integration/users_signup_test.rb' - end - watch(%r{app/views/users/*}) do - resource_tests('users') + - ['test/integration/microposts_interface_test.rb'] - end end + # Returns the integration tests corresponding to the given resource. def integration_tests(resource = :all) if resource == :all diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d84cb6e7..d9caa60e 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' + default from: 'no-reply@rezoleo.fr' layout 'mailer' end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 00000000..e4971b42 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class UserMailer < ApplicationMailer + default from: email_address_with_name('no-reply@rezoleo.fr', 'Lea5') + + def internet_expiration_7_days + @user = params[:user] + mail(to: email_address_with_name(@user.email, "#{@user.firstname} #{@user.lastname}"), + subject: 'Your internet will expire in 7 days') + end + + def internet_expiration_1_day + @user = params[:user] + mail(to: email_address_with_name(@user.email, "#{@user.firstname} #{@user.lastname}"), + subject: 'Your internet will expire tomorrow') + end +end diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index cbd34d2e..dfa86453 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -1,13 +1,13 @@ - - - - + + - - <%= yield %> - + +<%= yield %> + diff --git a/app/views/user_mailer/internet_expiration_1_day.html.erb b/app/views/user_mailer/internet_expiration_1_day.html.erb new file mode 100644 index 00000000..70ebbd1d --- /dev/null +++ b/app/views/user_mailer/internet_expiration_1_day.html.erb @@ -0,0 +1 @@ +Hello From HTML diff --git a/app/views/user_mailer/internet_expiration_1_day.text.erb b/app/views/user_mailer/internet_expiration_1_day.text.erb new file mode 100644 index 00000000..c02e88e4 --- /dev/null +++ b/app/views/user_mailer/internet_expiration_1_day.text.erb @@ -0,0 +1 @@ +Hello from TEXT diff --git a/app/views/user_mailer/internet_expiration_7_days.html.erb b/app/views/user_mailer/internet_expiration_7_days.html.erb new file mode 100644 index 00000000..70ebbd1d --- /dev/null +++ b/app/views/user_mailer/internet_expiration_7_days.html.erb @@ -0,0 +1 @@ +Hello From HTML diff --git a/app/views/user_mailer/internet_expiration_7_days.text.erb b/app/views/user_mailer/internet_expiration_7_days.text.erb new file mode 100644 index 00000000..c02e88e4 --- /dev/null +++ b/app/views/user_mailer/internet_expiration_7_days.text.erb @@ -0,0 +1 @@ +Hello from TEXT diff --git a/lib/tasks/internet_expiration_mail.rake b/lib/tasks/internet_expiration_mail.rake new file mode 100644 index 00000000..2d8cd804 --- /dev/null +++ b/lib/tasks/internet_expiration_mail.rake @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +namespace :lea5 do + desc 'send mail to users whose internet will expire soon' + task internet_expiration_mail: [:environment] do + User.all.each do |user| + break if user.subscription_expiration.nil? + + if 7.days.from_now.to_date == user.subscription_expiration.to_date + puts 'SEDING 7 DAYS' + UserMailer.with(user:).internet_expiration_7_days.deliver_now + end + + if 1.day.from_now.to_date == user.subscription_expiration.to_date + puts 'SEDING 1 DAY' + UserMailer.with(user:).internet_expiration_1_day.deliver_now + end + end + end +end diff --git a/test/fixtures/user_mailer/internet_expiration_1_day.html b/test/fixtures/user_mailer/internet_expiration_1_day.html new file mode 100644 index 00000000..f0f448d0 --- /dev/null +++ b/test/fixtures/user_mailer/internet_expiration_1_day.html @@ -0,0 +1,14 @@ + + + + + + + + +Hello From HTML + + + diff --git a/test/fixtures/user_mailer/internet_expiration_1_day.text b/test/fixtures/user_mailer/internet_expiration_1_day.text new file mode 100644 index 00000000..c02e88e4 --- /dev/null +++ b/test/fixtures/user_mailer/internet_expiration_1_day.text @@ -0,0 +1 @@ +Hello from TEXT diff --git a/test/fixtures/user_mailer/internet_expiration_7_days.html b/test/fixtures/user_mailer/internet_expiration_7_days.html new file mode 100644 index 00000000..f0f448d0 --- /dev/null +++ b/test/fixtures/user_mailer/internet_expiration_7_days.html @@ -0,0 +1,14 @@ + + + + + + + + +Hello From HTML + + + diff --git a/test/fixtures/user_mailer/internet_expiration_7_days.text b/test/fixtures/user_mailer/internet_expiration_7_days.text new file mode 100644 index 00000000..c02e88e4 --- /dev/null +++ b/test/fixtures/user_mailer/internet_expiration_7_days.text @@ -0,0 +1 @@ +Hello from TEXT diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb new file mode 100644 index 00000000..6643089c --- /dev/null +++ b/test/mailers/previews/user_mailer_preview.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Preview all emails at http://localhost:3000/rails/mailers/user_mailer +class UserMailerPreview < ActionMailer::Preview + def hello + UserMailer.hello + end +end diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb new file mode 100644 index 00000000..a446bf51 --- /dev/null +++ b/test/mailers/user_mailer_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'test_helper' + +class UserMailerTest < ActionMailer::TestCase + test 'internet expiration 7 days' do + # Create the email and store it for further assertions + user = users(:ironman) + email = UserMailer.with(user:).internet_expiration_7_days + + # Send the email, then test that it got queued + assert_emails 1 do + email.deliver_now + end + + # Test the body of the sent email contains what we expect it to + assert_equal ['no-reply@rezoleo.fr'], email.from + assert_equal [user.email], email.to + assert_equal 'Your internet will expire in 7 days', email.subject + assert_equal read_fixture('internet_expiration_7_days.text').join.strip, email.text_part.body.to_s.strip + assert_equal read_fixture('internet_expiration_7_days.html').join.strip, email.html_part.body.to_s.strip + end + + test 'internet expiration 1 day' do + # Create the email and store it for further assertions + user = users(:ironman) + email = UserMailer.with(user:).internet_expiration_1_day + + # Send the email, then test that it got queued + assert_emails 1 do + email.deliver_now + end + + # Test the body of the sent email contains what we expect it to + assert_equal ['no-reply@rezoleo.fr'], email.from + assert_equal [user.email], email.to + assert_equal 'Your internet will expire tomorrow', email.subject + assert_equal read_fixture('internet_expiration_1_day.text').join.strip, email.text_part.body.to_s.strip + assert_equal read_fixture('internet_expiration_1_day.html').join.strip, email.html_part.body.to_s.strip + end +end diff --git a/test/tasks/internet_expiration_mail_test.rb b/test/tasks/internet_expiration_mail_test.rb new file mode 100644 index 00000000..7d87b067 --- /dev/null +++ b/test/tasks/internet_expiration_mail_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rake' +require 'test_helper' + +class InternetExpirationMailTest < ActionDispatch::IntegrationTest + # https://blog.10pines.com/2019/01/14/testing-rake-tasks/ + # https://thoughtbot.com/blog/test-rake-tasks-like-a-boss + def setup + Rake.application.rake_require 'tasks/internet_expiration_mail' + Rake::Task.define_task(:environment) + @user = users(:ironman) + end + + test 'should send an email when subscription expires in 7 days' do + @user.subscriptions.destroy_all + @user.subscriptions.new(start_at: Time.current, end_at: 7.days.from_now) + @user.save + assert_emails 1 do + Rake::Task['lea5:internet_expiration_mail'].invoke + end + assert_equal 'Your internet will expire in 7 days', UserMailer.deliveries.first.subject + end + + test 'should send an email when subscription expires tomorrow' do + @user.subscriptions.destroy_all + @user.subscriptions.new(start_at: Time.current, end_at: 1.day.from_now) + @user.save + assert_emails 1 do + Rake::Task['lea5:internet_expiration_mail'].invoke + end + + assert_equal 'Your internet will expire tomorrow', UserMailer.deliveries.first.subject + end + + test 'should not send an email when subscription expires between 7 days and 1 day' do + @user.subscriptions.destroy_all + @user.subscriptions.new(start_at: Time.current, end_at: 6.days.from_now) + @user.save + assert_emails 0 do + Rake::Task['lea5:internet_expiration_mail'].invoke + end + end +end diff --git a/test/tasks/sync_accounts_test.rb b/test/tasks/sync_accounts_test.rb index 016abdcd..9246207e 100644 --- a/test/tasks/sync_accounts_test.rb +++ b/test/tasks/sync_accounts_test.rb @@ -3,6 +3,7 @@ require 'json' require 'rake' require 'webmock' +require 'test_helper' class SyncAccountsTest < ActiveSupport::TestCase # https://blog.10pines.com/2019/01/14/testing-rake-tasks/ From cddf18ea07925b94e17b60391537aa04d3442304 Mon Sep 17 00:00:00 2001 From: Thomas Gaudin Date: Mon, 10 Apr 2023 17:43:29 +0200 Subject: [PATCH 2/2] Fix Rake task tests & add systemd timer for emails --- docs/features/internet_expiration_mail.md | 10 +++++ docs/features/subscription.md | 8 ++-- .../lea5-internet-expiration-mail.service | 38 +++++++++++++++++++ .../lea5-internet-expiration-mail.timer | 9 +++++ lib/tasks/internet_expiration_mail.rake | 3 +- test/tasks/internet_expiration_mail_test.rb | 6 +++ 6 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 docs/features/internet_expiration_mail.md create mode 100644 lib/support/systemd/lea5-internet-expiration-mail.service create mode 100644 lib/support/systemd/lea5-internet-expiration-mail.timer diff --git a/docs/features/internet_expiration_mail.md b/docs/features/internet_expiration_mail.md new file mode 100644 index 00000000..fe41a83a --- /dev/null +++ b/docs/features/internet_expiration_mail.md @@ -0,0 +1,10 @@ +# Internet expiration mail + +To avoid last minute renewal of subscriptions, we send an email to users 7 days and 1 day before their Internet access expires. +The task is defined in [`internet_expiration_mail.rake`](../../lib/tasks/internet_expiration_mail.rake), and runs every day. + +> **Warning** +> The task does not currently handle running more than once a day, to prevent sending multiple emails. +> This means it also cannot "catch up" if an email was not sent. + +The timer is done with service/timer of systemd, the configuration can be found in [systemd folder](../../lib/support/systemd). diff --git a/docs/features/subscription.md b/docs/features/subscription.md index ff8126d4..422e8ddf 100644 --- a/docs/features/subscription.md +++ b/docs/features/subscription.md @@ -9,7 +9,7 @@ graph TD B --> |No| D[User's subscription ends now + subscription duration] ``` -**Warning** -If a user has a free access when a subscription is added, the starting date -of the subscription is based on the subscription expiration status, *not* the internet -expiration status. +> **Warning** +> If a user has a free access when a subscription is added, the starting date +> of the subscription is based on the subscription expiration status, *not* the internet +> expiration status. diff --git a/lib/support/systemd/lea5-internet-expiration-mail.service b/lib/support/systemd/lea5-internet-expiration-mail.service new file mode 100644 index 00000000..4ea94627 --- /dev/null +++ b/lib/support/systemd/lea5-internet-expiration-mail.service @@ -0,0 +1,38 @@ +[Unit] +Description=Lea5 - Send email warning users about their soon-to-expire subscription +Documentation=https://github.com/rezoleo/lea5 + +[Service] +# Command runs once then exits, it is not a background service +Type=oneshot +User=lea5 +Group=lea5 +WorkingDirectory=/opt/lea5 +# Change start command for a new service +ExecStart=/opt/lea5/bin/rails lea5:internet_expiration_mail + +Environment=PATH=/home/lea5/.rbenv/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin +Environment=RAILS_ENV=production +Environment=RAILS_LOG_TO_STDOUT=1 +Environment=RAILS_MASTER_KEY= + +NoNewPrivileges=true +#PrivateNetwork=true +PrivateDevices=true +PrivateMounts=true +PrivateTmp=true +PrivateUsers=true +ProtectHome=tmpfs +BindReadOnlyPaths=/home/lea5/.rbenv +ProtectSystem=strict +ReadWritePaths=/opt/lea5/log +ReadWritePaths=/opt/lea5/tmp +ReadWritePaths=/opt/lea5/storage +ProtectControlGroups=true +ProtectClock=true +ProtectHostname=true +CapabilityBoundingSet= +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectProc=invisible diff --git a/lib/support/systemd/lea5-internet-expiration-mail.timer b/lib/support/systemd/lea5-internet-expiration-mail.timer new file mode 100644 index 00000000..95e6a4da --- /dev/null +++ b/lib/support/systemd/lea5-internet-expiration-mail.timer @@ -0,0 +1,9 @@ +# See https://leethax.org/2017/11/17/systemd-timers.html +# and https://wiki.archlinux.org/title/Systemd/Timers +[Timer] +# Run the service every 24 hours +OnActiveSec=24h +OnUnitActiveSec=24h + +[Install] +WantedBy=timer.target diff --git a/lib/tasks/internet_expiration_mail.rake b/lib/tasks/internet_expiration_mail.rake index 2d8cd804..4eef109b 100644 --- a/lib/tasks/internet_expiration_mail.rake +++ b/lib/tasks/internet_expiration_mail.rake @@ -3,16 +3,15 @@ namespace :lea5 do desc 'send mail to users whose internet will expire soon' task internet_expiration_mail: [:environment] do + # TODO: Handle multiple execution the same day (prevent resending email) User.all.each do |user| break if user.subscription_expiration.nil? if 7.days.from_now.to_date == user.subscription_expiration.to_date - puts 'SEDING 7 DAYS' UserMailer.with(user:).internet_expiration_7_days.deliver_now end if 1.day.from_now.to_date == user.subscription_expiration.to_date - puts 'SEDING 1 DAY' UserMailer.with(user:).internet_expiration_1_day.deliver_now end end diff --git a/test/tasks/internet_expiration_mail_test.rb b/test/tasks/internet_expiration_mail_test.rb index 7d87b067..0c134619 100644 --- a/test/tasks/internet_expiration_mail_test.rb +++ b/test/tasks/internet_expiration_mail_test.rb @@ -12,6 +12,12 @@ def setup @user = users(:ironman) end + def teardown + # Once invoked, a rake task must be re-enabled to be executed a second time + # https://medium.com/@shaneilske/invoke-a-rake-task-multiple-times-1bcb01dee9d9 + Rake::Task['lea5:internet_expiration_mail'].reenable + end + test 'should send an email when subscription expires in 7 days' do @user.subscriptions.destroy_all @user.subscriptions.new(start_at: Time.current, end_at: 7.days.from_now)