diff --git a/.rubocop.yml b/.rubocop.yml index f9d86d4..dc47f07 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,10 @@ # Omakase Ruby styling for Rails inherit_gem: { rubocop-rails-omakase: rubocop.yml } +AllCops: + Exclude: + - 'app/assets/stylesheets/**/*' + # Overwrite or add rules to create your own house style # # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` diff --git a/Gemfile b/Gemfile index 3df01a8..f0cd11f 100644 --- a/Gemfile +++ b/Gemfile @@ -152,6 +152,8 @@ group :production do gem "cloudflare-rails" end +gem "premailer-rails" + gem "htmlcompressor", "~> 0.4.0" gem "doorkeeper", "~> 5.8" diff --git a/Gemfile.lock b/Gemfile.lock index f23918a..f64ae2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,6 +144,8 @@ GEM bigdecimal rexml crass (1.0.6) + css_parser (2.0.0) + addressable csv (3.3.5) date (3.5.1) debug (1.11.1) @@ -231,6 +233,7 @@ GEM hashie (5.1.0) logger htmlcompressor (0.4.0) + htmlentities (4.4.2) http (5.3.1) addressable (~> 2.8) http-cookie (~> 1.0) @@ -373,6 +376,14 @@ GEM concurrent-ruby (~> 1) pp (0.6.3) prettyprint + premailer (1.28.0) + addressable + css_parser (>= 1.19.0) + htmlentities (>= 4.0.0) + premailer-rails (1.12.0) + actionmailer (>= 3) + net-smtp + premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) prism (1.9.0) propshaft (1.3.1) @@ -675,6 +686,7 @@ DEPENDENCIES paper_trail pg posthog-ruby + premailer-rails propshaft puma (>= 5.0) query_count diff --git a/app/assets/stylesheets/mailer.css b/app/assets/stylesheets/mailer.css new file mode 100644 index 0000000..e7589d6 --- /dev/null +++ b/app/assets/stylesheets/mailer.css @@ -0,0 +1,266 @@ +/* + * Tailwind-named utility classes with static values for email client compatibility. + * + * "Wait, this is stupid. Can I not just use Tailwind directly?" AHAHAHAHAHA no. Believe me I tried. + * (Well, technically you can, you just get super patchy support in email clients). + * + * The two big ones are: + * - Tailwind 4 requires `oklch`, not supported in most email clients: https://www.caniemail.com/features/css-modern-color/ + * - Tailwind 4 requires CSS variables, which has less thatn 50% coverage: https://www.caniemail.com/features/css-variables/ + */ + +.wrapper { + padding: 1.25rem; + margin: 0 auto; + max-width: 600px; +} +.container { + padding: 0; + margin: 0; + width: 100%; + max-width: 100%; +} +.section { + padding: 0.5rem 1rem; +} + +.max-w-\[600px\] { + max-width: 600px; +} + +.block { + display: block; +} +.inline-block { + display: inline-block; +} +.hidden { + display: none; +} + +.w-full { + width: 100%; +} +.max-w-full { + max-width: 100%; +} +.h-auto { + height: auto; +} + +.m-0 { + margin: 0; +} +.mx-auto { + margin-left: auto; + margin-right: auto; +} +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} +.mt-0 { + margin-top: 0; +} +.mt-1 { + margin-top: 0.25rem; +} +.mt-2 { + margin-top: 0.5rem; +} +.mt-4 { + margin-top: 1rem; +} +.mt-6 { + margin-top: 1.5rem; +} +.mt-8 { + margin-top: 2rem; +} +.mb-0 { + margin-bottom: 0; +} +.mb-2 { + margin-bottom: 0.5rem; +} +.mb-4 { + margin-bottom: 1rem; +} + +/* ── Padding ── */ +.p-0 { padding: 0; } +.p-4 { padding: 1rem; } +.p-5 { padding: 1.25rem; } +.p-6 { padding: 1.5rem; } +.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; } +.px-4 { padding-left: 1rem; padding-right: 1rem; } +.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; } +.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } +.pt-2 { padding-top: 0.5rem; } +.pt-6 { padding-top: 1.5rem; } +.pb-6 { padding-bottom: 1.5rem; } +.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; } +.py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; } +.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } +.py-4 { padding-top: 1rem; padding-bottom: 1rem; } +.py-5 { padding-top: 1.25rem; padding-bottom: 1.25rem; } +.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; } + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.font-normal { + font-weight: 400; +} +.font-medium { + font-weight: 500; +} +.font-semibold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} + +.leading-5 { + line-height: 1.25rem; +} +.leading-6 { + line-height: 1.5rem; +} +.leading-relaxed { + line-height: 1.625; +} +.leading-tight { + line-height: 1.25; +} + +.tracking-tight { + letter-spacing: -0.025em; +} + +.text-left { + text-align: left; +} +.text-center { + text-align: center; +} +.text-right { + text-align: right; +} + +.no-underline { + text-decoration: none; +} +.underline { + text-decoration: underline; +} + +.align-top { + vertical-align: top; +} + +.text-white { + color: #ffffff; +} +.text-black { + color: #000000; +} +.text-gray-400 { + color: #9ca3af; +} +.text-gray-500 { + color: #6b7280; +} +.text-gray-600 { + color: #4b5563; +} +.text-gray-700 { + color: #374151; +} +.text-gray-900 { + color: #111827; +} +.text-indigo-600 { + color: #4f46e5; +} + +.bg-white { + background-color: #ffffff; +} +.bg-gray-50 { + background-color: #f9fafb; +} +.bg-gray-100 { + background-color: #f3f4f6; +} +.bg-indigo-600 { + background-color: #4f46e5; +} + +.border { + border-width: 1px; + border-style: solid; + border-color: #e5e7eb; +} +.border-t { + border-top-width: 1px; + border-top-style: solid; + border-top-color: #e5e7eb; +} +.border-b { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: #e5e7eb; +} +.border-gray-200 { + border-color: #e5e7eb; +} + +/* ── Border Radius ── */ +.rounded { + border-radius: 0.25rem; +} +.rounded-md { + border-radius: 0.375rem; +} +.rounded-lg { + border-radius: 0.5rem; +} +.rounded-xl { + border-radius: 0.75rem; +} + +.overflow-hidden { + overflow: hidden; +} diff --git a/app/jobs/weekly_summary_email_job.rb b/app/jobs/weekly_summary_email_job.rb index 6625c22..52737ff 100644 --- a/app/jobs/weekly_summary_email_job.rb +++ b/app/jobs/weekly_summary_email_job.rb @@ -2,7 +2,10 @@ class WeeklySummaryEmailJob < ApplicationJob queue_as :literally_whenever def perform(reference_time = Time.current) - # See https://hackclub.slack.com/archives/D083UR1DR7V/p1772321709715969 + # Weekly summary delivery is intentionally disabled for now. + # Context: https://hackclub.slack.com/archives/D083UR1DR7V/p1772321709715969 + # Keep this no-op until we explicitly decide to turn the campaign back on. + reference_time # now_utc = reference_time.utc # return unless send_window?(now_utc) diff --git a/app/views/email_verification_mailer/verify_email.html.erb b/app/views/email_verification_mailer/verify_email.html.erb index 61e02d8..9701576 100644 --- a/app/views/email_verification_mailer/verify_email.html.erb +++ b/app/views/email_verification_mailer/verify_email.html.erb @@ -1,13 +1,13 @@ -

+

Verify your email address

-

+

Hi <%= h(@user.display_name) %>, please confirm that you want to add <%= @verification_request.email %> to your Hackatime account.

<%= render "shared/mailer/button", url: @verification_url, label: "Verify email address" %> -

+

If you didn't request this change, you can ignore this email.

diff --git a/app/views/email_verification_mailer/verify_email.text.erb b/app/views/email_verification_mailer/verify_email.text.erb index cea1c0e..0c7ee01 100644 --- a/app/views/email_verification_mailer/verify_email.text.erb +++ b/app/views/email_verification_mailer/verify_email.text.erb @@ -1,12 +1,12 @@ -Verify your email address for Hackatime +Hackatime — Verify Your Email +============================== Hi <%= h(@user.display_name) %>, -You've requested to add <%= @verification_request.email %> to your Hackatime account. -Click the link below to verify this email address: +Please confirm that you want to add <%= @verification_request.email %> to your Hackatime account: <%= @verification_url %> -This link will expire in 30 minutes and can only be used once. +This link expires in 30 minutes and can only be used once. -If you didn't request this email, you can safely ignore it. +If you didn't request this, you can safely ignore this email. diff --git a/app/views/heartbeat_export_mailer/export_ready.html.erb b/app/views/heartbeat_export_mailer/export_ready.html.erb index c762ea4..4ae4a68 100644 --- a/app/views/heartbeat_export_mailer/export_ready.html.erb +++ b/app/views/heartbeat_export_mailer/export_ready.html.erb @@ -1,11 +1,11 @@ -

+

Your heartbeat export is ready

-

+

Hi <%= h(@user.display_name) %>, your requested Hackatime heartbeat export is ready to download.

<%= render "shared/mailer/button", url: @download_url, label: "Download #{@filename}" %> -

+

This link will stop working after 7 days.

diff --git a/app/views/heartbeat_export_mailer/export_ready.text.erb b/app/views/heartbeat_export_mailer/export_ready.text.erb index b611ef9..bc026f0 100644 --- a/app/views/heartbeat_export_mailer/export_ready.text.erb +++ b/app/views/heartbeat_export_mailer/export_ready.text.erb @@ -1,10 +1,10 @@ -Your heartbeat export is ready +Hackatime — Your Export is Ready +================================== Hi <%= h(@user.display_name) %>, -Your Hackatime heartbeat export has been generated. +Your heartbeat export (<%= @filename %>) is ready to download: -Download file: <%= @download_url %> -Filename: <%= @filename %> +<%= @download_url %> -This link will stop working after 7 days. +This link will expire in 7 days. diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index 7f6bbc5..ef70c3d 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -1,23 +1,38 @@ - - - - - - - -
-
-
- <%= yield %> -
- -
- <% footer_icon_url = "#{root_url.chomp('/')}#{image_path('favicon.png')}" %> - <%= image_tag(footer_icon_url, alt: "Hackatime", style: "width: 36px; height: 36px; opacity: 0.6; margin-bottom: 12px;") %> -

15 Falls Road, Shelburne, VT 05482, United States

-
-
-
- + + + + + + <%= stylesheet_link_tag "mailer" %> + + +
+ + + + + + + + + + + + + + +
+ <%= render "layouts/mailer/header" %> +
+
+ <%= yield %> +
+
+
+

15 Falls Road, Shelburne, VT 05482, United States

+
+
+
+ diff --git a/app/views/layouts/mailer/_header.html.erb b/app/views/layouts/mailer/_header.html.erb new file mode 100644 index 0000000..835538d --- /dev/null +++ b/app/views/layouts/mailer/_header.html.erb @@ -0,0 +1,6 @@ +
+ + <% logo_url = "#{root_url.chomp('/')}#{image_path('favicon.png')}" %> + <%= image_tag(logo_url, alt: "Hackatime", style: "height: 2rem", height: "32") %> + +
diff --git a/app/views/loops_mailer/sign_in_email.html.erb b/app/views/loops_mailer/sign_in_email.html.erb index 6fa33a6..b0c8db7 100644 --- a/app/views/loops_mailer/sign_in_email.html.erb +++ b/app/views/loops_mailer/sign_in_email.html.erb @@ -1,12 +1,12 @@ -

+

Your Hackatime sign-in link

-

+

Use the secure link below to sign in to your account.

<%= render "shared/mailer/button", url: @sign_in_url, label: "Sign in to Hackatime" %> -

+

If you did not request this email, you can safely ignore it.

diff --git a/app/views/loops_mailer/sign_in_email.text.erb b/app/views/loops_mailer/sign_in_email.text.erb index e70090d..5ac11de 100644 --- a/app/views/loops_mailer/sign_in_email.text.erb +++ b/app/views/loops_mailer/sign_in_email.text.erb @@ -1,8 +1,10 @@ -Your Hackatime sign-in link +Hackatime — Sign In +==================== + +Use the link below to sign in to your account: -Use this secure sign-in link to access your account: <%= @sign_in_url %> -For security, this link expires in 30 minutes and can only be used once. +This link expires in 30 minutes and can only be used once. -If you didn't request this email, you can ignore it. +If you didn't request this, you can safely ignore this email. diff --git a/app/views/shared/mailer/_button.html.erb b/app/views/shared/mailer/_button.html.erb index fb973b1..d48d490 100644 --- a/app/views/shared/mailer/_button.html.erb +++ b/app/views/shared/mailer/_button.html.erb @@ -1,6 +1,6 @@ <%# locals: (url:, label:) %> -
- +
+ <%= label %>
diff --git a/app/views/weekly_summary_mailer/weekly_summary.html.erb b/app/views/weekly_summary_mailer/weekly_summary.html.erb index 74fc174..2e8ec5a 100644 --- a/app/views/weekly_summary_mailer/weekly_summary.html.erb +++ b/app/views/weekly_summary_mailer/weekly_summary.html.erb @@ -1,55 +1,55 @@ -

+

Your coding recap

-

+

<%= @period_label %>

-

+

Hi <%= h(@user.display_name) %>, here is your weekly coding snapshot from Hackatime.

-
-
-

Total coding time

-

+

+
+

Total coding time

+

<%= short_time_simple(@total_seconds) %>

- - -
-

Daily average

-

<%= short_time_simple(@daily_average_seconds) %>

+
+

Daily average

+

<%= short_time_simple(@daily_average_seconds) %>

-

Active days

-

<%= @active_days %>/7

+
+

Active days

+

<%= @active_days %>/7

-

Heartbeats

-

<%= @total_heartbeats %>

+
+

Heartbeats

+

<%= @total_heartbeats %>

<% if @total_seconds.zero? %> -
-

+

+

No coding activity was recorded this week. Start a new session and your next summary will include your project and language breakdown.

<% else %> -
-

Top projects

+
+

Top projects

<% @top_projects.each_with_index do |project, index| %> - - @@ -57,15 +57,15 @@
+ <%= project[:name] %> + <%= project[:duration_label] %>
-
-

Top languages

+
+

Top languages

<% @top_languages.each_with_index do |language, index| %> - - diff --git a/app/views/weekly_summary_mailer/weekly_summary.text.erb b/app/views/weekly_summary_mailer/weekly_summary.text.erb index ef375ff..00aaaa1 100644 --- a/app/views/weekly_summary_mailer/weekly_summary.text.erb +++ b/app/views/weekly_summary_mailer/weekly_summary.text.erb @@ -1,22 +1,31 @@ -Your Hackatime weekly summary: <%= @period_label %> +Hackatime — Your Coding Recap +============================== +<%= @period_label %> Hi <%= h(@user.display_name) %>, -Total coding time: <%= short_time_simple(@total_seconds) %> -Daily average: <%= short_time_simple(@daily_average_seconds) %> -Active days: <%= @active_days %>/7 -Heartbeats: <%= @total_heartbeats %> +Here's your weekly coding snapshot from Hackatime. +OVERVIEW +-------- +Total coding time: <%= short_time_simple(@total_seconds) %> +Daily average: <%= short_time_simple(@daily_average_seconds) %> +Active days: <%= @active_days %>/7 +Heartbeats: <%= @total_heartbeats %> <% if @total_seconds.zero? %> -No coding activity was recorded this week. + +No coding activity was recorded this week. Start a new session and your next summary will include your project and language breakdown. <% else %> -Top projects: -<% @top_projects.each do |project| %> -- <%= project[:name] %>: <%= project[:duration_label] %> + +TOP PROJECTS +------------ +<% @top_projects.each do |project| -%> + • <%= project[:name] %> - <%= project[:duration_label] %> <% end %> -Top languages: -<% @top_languages.each do |language| %> -- <%= language[:name] %>: <%= language[:duration_label] %> -<% end %> +TOP LANGUAGES +------------- +<% @top_languages.each do |language| -%> + • <%= language[:name] %> - <%= language[:duration_label] %> +<% end -%> <% end %> diff --git a/config/application.rb b/config/application.rb index 36c0ed3..a5a9930 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,12 @@ module Harbor # Initialize configuration defaults for originally generated Rails version. config.load_defaults 8.0 + if ENV["RAILS_ENV"] == "test" + ENV["ENCRYPTION_PRIMARY_KEY"] = "test_primary_key_for_active_record_encryption_123" if ENV["ENCRYPTION_PRIMARY_KEY"].to_s.empty? + ENV["ENCRYPTION_DETERMINISTIC_KEY"] = "test_deterministic_key_for_active_record_encrypt_456" if ENV["ENCRYPTION_DETERMINISTIC_KEY"].to_s.empty? + ENV["ENCRYPTION_KEY_DERIVATION_SALT"] = "test_key_derivation_salt_789" if ENV["ENCRYPTION_KEY_DERIVATION_SALT"].to_s.empty? + end + config.active_record.encryption.primary_key = ENV["ENCRYPTION_PRIMARY_KEY"] config.active_record.encryption.deterministic_key = ENV["ENCRYPTION_DETERMINISTIC_KEY"] config.active_record.encryption.key_derivation_salt = ENV["ENCRYPTION_KEY_DERIVATION_SALT"] diff --git a/config/initializers/active_record_encryption.rb b/config/initializers/active_record_encryption.rb index dfb357b..9bd6ae1 100644 --- a/config/initializers/active_record_encryption.rb +++ b/config/initializers/active_record_encryption.rb @@ -1,7 +1,7 @@ if Rails.env.test? - ENV["ENCRYPTION_PRIMARY_KEY"] ||= "test_primary_key_for_active_record_encryption_123" - ENV["ENCRYPTION_DETERMINISTIC_KEY"] ||= "test_deterministic_key_for_active_record_encrypt_456" - ENV["ENCRYPTION_KEY_DERIVATION_SALT"] ||= "test_key_derivation_salt_789" + ENV["ENCRYPTION_PRIMARY_KEY"] = "test_primary_key_for_active_record_encryption_123" if ENV["ENCRYPTION_PRIMARY_KEY"].to_s.empty? + ENV["ENCRYPTION_DETERMINISTIC_KEY"] = "test_deterministic_key_for_active_record_encrypt_456" if ENV["ENCRYPTION_DETERMINISTIC_KEY"].to_s.empty? + ENV["ENCRYPTION_KEY_DERIVATION_SALT"] = "test_key_derivation_salt_789" if ENV["ENCRYPTION_KEY_DERIVATION_SALT"].to_s.empty? end Rails.application.config.active_record.encryption.primary_key = ENV["ENCRYPTION_PRIMARY_KEY"] diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb new file mode 100644 index 0000000..6619335 --- /dev/null +++ b/config/initializers/premailer.rb @@ -0,0 +1,5 @@ +Premailer::Rails.config.merge!( + preserve_styles: false, + remove_ids: false, + remove_comments: true +) diff --git a/test/jobs/weekly_summary_email_job_test.rb b/test/jobs/weekly_summary_email_job_test.rb index f63344f..0e39b65 100644 --- a/test/jobs/weekly_summary_email_job_test.rb +++ b/test/jobs/weekly_summary_email_job_test.rb @@ -1,66 +1,74 @@ require "test_helper" class WeeklySummaryEmailJobTest < ActiveJob::TestCase + DISABLED_REASON = "Weekly summary delivery is intentionally disabled. See WeeklySummaryEmailJob for context.".freeze + setup do ActionMailer::Base.deliveries.clear end test "sends weekly summaries only for opted-in users with email at friday 17:30 utc" do - enabled_user = User.create!(timezone: "UTC", weekly_summary_email_enabled: true) - enabled_user.email_addresses.create!(email: "enabled-#{SecureRandom.hex(4)}@example.com", source: :signing_in) - create_coding_heartbeat(enabled_user, Time.utc(2026, 2, 21, 12, 0, 0), "project-enabled", "Ruby") - create_coding_heartbeat(enabled_user, Time.utc(2026, 2, 21, 12, 20, 0), "project-enabled", "Ruby") + skip DISABLED_REASON - disabled_user = User.create!(timezone: "UTC", weekly_summary_email_enabled: false) - disabled_user.email_addresses.create!(email: "disabled-#{SecureRandom.hex(4)}@example.com", source: :signing_in) - create_coding_heartbeat(disabled_user, Time.utc(2026, 2, 21, 13, 0, 0), "project-disabled", "Ruby") - create_coding_heartbeat(disabled_user, Time.utc(2026, 2, 21, 13, 20, 0), "project-disabled", "Ruby") + # enabled_user = User.create!(timezone: "UTC", weekly_summary_email_enabled: true) + # enabled_user.email_addresses.create!(email: "enabled-#{SecureRandom.hex(4)}@example.com", source: :signing_in) + # create_coding_heartbeat(enabled_user, Time.utc(2026, 2, 21, 12, 0, 0), "project-enabled", "Ruby") + # create_coding_heartbeat(enabled_user, Time.utc(2026, 2, 21, 12, 20, 0), "project-enabled", "Ruby") - user_without_email = User.create!(timezone: "UTC", weekly_summary_email_enabled: true) - create_coding_heartbeat(user_without_email, Time.utc(2026, 2, 20, 14, 0, 0), "project-no-email", "Ruby") + # disabled_user = User.create!(timezone: "UTC", weekly_summary_email_enabled: false) + # disabled_user.email_addresses.create!(email: "disabled-#{SecureRandom.hex(4)}@example.com", source: :signing_in) + # create_coding_heartbeat(disabled_user, Time.utc(2026, 2, 21, 13, 0, 0), "project-disabled", "Ruby") + # create_coding_heartbeat(disabled_user, Time.utc(2026, 2, 21, 13, 20, 0), "project-disabled", "Ruby") - reference_time = Time.utc(2026, 2, 27, 17, 30, 0) # Friday, 5:30 PM GMT + # user_without_email = User.create!(timezone: "UTC", weekly_summary_email_enabled: true) + # create_coding_heartbeat(user_without_email, Time.utc(2026, 2, 20, 14, 0, 0), "project-no-email", "Ruby") - assert_difference -> { ActionMailer::Base.deliveries.count }, 1 do - WeeklySummaryEmailJob.perform_now(reference_time) - end + # reference_time = Time.utc(2026, 2, 27, 17, 30, 0) # Friday, 5:30 PM GMT - mail = ActionMailer::Base.deliveries.last - assert_equal [ enabled_user.email_addresses.first.email ], mail.to - assert_equal "Your Hackatime weekly summary (Feb 16 - Feb 23, 2026)", mail.subject - assert_includes mail.text_part.body.decoded, "Top projects:" + # assert_difference -> { ActionMailer::Base.deliveries.count }, 1 do + # WeeklySummaryEmailJob.perform_now(reference_time) + # end + + # mail = ActionMailer::Base.deliveries.last + # assert_equal [ enabled_user.email_addresses.first.email ], mail.to + # assert_equal "Your Hackatime weekly summary (Feb 16 - Feb 23, 2026)", mail.subject + # assert_includes mail.text_part.body.decoded, "Top projects:" end test "uses previous local calendar week for each user's timezone" do - user = User.create!(timezone: "America/Los_Angeles", weekly_summary_email_enabled: true) - user.email_addresses.create!(email: "la-#{SecureRandom.hex(4)}@example.com", source: :signing_in) - create_coding_heartbeat(user, Time.utc(2026, 2, 16, 8, 30, 0), "local-week-in-range", "Ruby") - create_coding_heartbeat(user, Time.utc(2026, 2, 16, 8, 50, 0), "local-week-in-range", "Ruby") - create_coding_heartbeat(user, Time.utc(2026, 2, 16, 7, 30, 0), "local-week-out-of-range", "Ruby") + skip DISABLED_REASON - reference_time = Time.utc(2026, 2, 27, 17, 30, 0) + # user = User.create!(timezone: "America/Los_Angeles", weekly_summary_email_enabled: true) + # user.email_addresses.create!(email: "la-#{SecureRandom.hex(4)}@example.com", source: :signing_in) + # create_coding_heartbeat(user, Time.utc(2026, 2, 16, 8, 30, 0), "local-week-in-range", "Ruby") + # create_coding_heartbeat(user, Time.utc(2026, 2, 16, 8, 50, 0), "local-week-in-range", "Ruby") + # create_coding_heartbeat(user, Time.utc(2026, 2, 16, 7, 30, 0), "local-week-out-of-range", "Ruby") - assert_difference -> { ActionMailer::Base.deliveries.count }, 1 do - WeeklySummaryEmailJob.perform_now(reference_time) - end + # reference_time = Time.utc(2026, 2, 27, 17, 30, 0) - mail = ActionMailer::Base.deliveries.last - assert_equal "Your Hackatime weekly summary (Feb 16 - Feb 23, 2026)", mail.subject - assert_includes mail.text_part.body.decoded, "Feb 16 - Feb 23, 2026" - assert_includes mail.text_part.body.decoded, "local-week-in-range" - assert_not_includes mail.text_part.body.decoded, "local-week-out-of-range" + # assert_difference -> { ActionMailer::Base.deliveries.count }, 1 do + # WeeklySummaryEmailJob.perform_now(reference_time) + # end + + # mail = ActionMailer::Base.deliveries.last + # assert_equal "Your Hackatime weekly summary (Feb 16 - Feb 23, 2026)", mail.subject + # assert_includes mail.text_part.body.decoded, "Feb 16 - Feb 23, 2026" + # assert_includes mail.text_part.body.decoded, "local-week-in-range" + # assert_not_includes mail.text_part.body.decoded, "local-week-out-of-range" end test "does not send weekly summaries outside friday 17:30 utc" do - user = User.create!(timezone: "UTC", weekly_summary_email_enabled: true) - user.email_addresses.create!(email: "outside-window-#{SecureRandom.hex(4)}@example.com", source: :signing_in) - create_coding_heartbeat(user, Time.utc(2026, 2, 20, 12, 0, 0), "project-outside", "Ruby") + skip DISABLED_REASON - reference_time = Time.utc(2026, 2, 27, 17, 29, 0) + # user = User.create!(timezone: "UTC", weekly_summary_email_enabled: true) + # user.email_addresses.create!(email: "outside-window-#{SecureRandom.hex(4)}@example.com", source: :signing_in) + # create_coding_heartbeat(user, Time.utc(2026, 2, 20, 12, 0, 0), "project-outside", "Ruby") - assert_no_difference -> { ActionMailer::Base.deliveries.count } do - WeeklySummaryEmailJob.perform_now(reference_time) - end + # reference_time = Time.utc(2026, 2, 27, 17, 29, 0) + + # assert_no_difference -> { ActionMailer::Base.deliveries.count } do + # WeeklySummaryEmailJob.perform_now(reference_time) + # end end private diff --git a/test/mailers/heartbeat_export_mailer_test.rb b/test/mailers/heartbeat_export_mailer_test.rb index f421f25..f781286 100644 --- a/test/mailers/heartbeat_export_mailer_test.rb +++ b/test/mailers/heartbeat_export_mailer_test.rb @@ -30,7 +30,7 @@ class HeartbeatExportMailerTest < ActionMailer::TestCase assert_equal 0, mail.attachments.size assert_includes mail.html_part.body.decoded, "Your heartbeat export is ready" - assert_includes mail.text_part.body.decoded, "Your Hackatime heartbeat export has been generated" + assert_includes mail.text_part.body.decoded, "Your heartbeat export (heartbeats_test.zip) is ready to download" assert_includes mail.text_part.body.decoded, @user.display_name assert_includes mail.text_part.body.decoded, "/rails/active_storage/" assert_includes mail.text_part.body.decoded, "heartbeats_test.zip" diff --git a/test/mailers/weekly_summary_mailer_test.rb b/test/mailers/weekly_summary_mailer_test.rb index c6993a9..8f6f1a9 100644 --- a/test/mailers/weekly_summary_mailer_test.rb +++ b/test/mailers/weekly_summary_mailer_test.rb @@ -30,7 +30,7 @@ class WeeklySummaryMailerTest < ActionMailer::TestCase assert_includes mail.html_part.body.decoded, "Your coding recap" assert_includes mail.html_part.body.decoded, "Top projects" assert_includes mail.text_part.body.decoded, "Feb 20 - Feb 27, 2026" - assert_includes mail.text_part.body.decoded, "Top languages:" + assert_includes mail.text_part.body.decoded, "TOP LANGUAGES" assert_includes mail.text_part.body.decoded, "hackatime-web" assert_not_includes mail.html_part.body.decoded.downcase, "gradient" end
+ <%= language[:name] %> + <%= language[:duration_label] %>