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/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 new file mode 100644 index 00000000..4eef109b --- /dev/null +++ b/lib/tasks/internet_expiration_mail.rake @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +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 + UserMailer.with(user:).internet_expiration_7_days.deliver_now + end + + if 1.day.from_now.to_date == user.subscription_expiration.to_date + 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..0c134619 --- /dev/null +++ b/test/tasks/internet_expiration_mail_test.rb @@ -0,0 +1,50 @@ +# 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 + + 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) + @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/