Aggiunge il rilevamento di invio e apertura delle email di campagna.
Ogni destinatario mostra se la mail è partita e se è stata aperta, con conteggi in dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
require "base64"
|
||||
|
||||
class MailOpensController < ActionController::Base
|
||||
PIXEL = Base64.decode64("R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==").freeze
|
||||
|
||||
def show
|
||||
record_open
|
||||
expires_now
|
||||
response.set_header("Cache-Control", "no-store, no-cache, must-revalidate, private, max-age=0")
|
||||
response.set_header("Pragma", "no-cache")
|
||||
send_data PIXEL, type: "image/gif", disposition: "inline", filename: "o.gif"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def record_open
|
||||
token = params[:token].to_s
|
||||
recipient = MailingRecipient.find_by(tracking_token: token)
|
||||
recipient&.record_open!
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("[mail_open] #{e.class}: #{e.message}")
|
||||
raise if Rails.env.test?
|
||||
end
|
||||
end
|
||||
@@ -176,6 +176,19 @@ module ApplicationHelper
|
||||
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[status] || 'bg-zinc-100 text-zinc-700'}"
|
||||
end
|
||||
|
||||
def mailing_open_badge(recipient)
|
||||
return unless recipient.status == "sent"
|
||||
|
||||
if recipient.opened?
|
||||
content_tag :span, "Aperta",
|
||||
class: "inline-flex items-center rounded-md bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-800 dark:bg-violet-950 dark:text-violet-200",
|
||||
title: "Prima apertura #{format_dt(recipient.opened_at)}"
|
||||
else
|
||||
content_tag :span, "Non aperta",
|
||||
class: "inline-flex items-center rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300"
|
||||
end
|
||||
end
|
||||
|
||||
def ab_variant_badge(variant)
|
||||
return if variant.blank?
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ class CampaignMailer < ApplicationMailer
|
||||
mailing = recipient.mailing
|
||||
identity = mailing.mail_identity
|
||||
html = Mailings::InlineImages.call(self, html)
|
||||
html = wrap_html(html)
|
||||
html = Mailings::OpenPixel.call(html, recipient)
|
||||
|
||||
mailing.files.each do |file|
|
||||
attachments[file.filename.to_s] = {
|
||||
@@ -20,7 +22,7 @@ class CampaignMailer < ApplicationMailer
|
||||
subject: subject,
|
||||
delivery_method_options: identity.smtp_settings
|
||||
) do |format|
|
||||
format.html { render html: wrap_html(html).html_safe }
|
||||
format.html { render html: html.html_safe }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ module Catalog
|
||||
ACTIVITY_TYPES = {
|
||||
"note" => "Nota",
|
||||
"email_sent" => "Email inviata",
|
||||
"email_opened" => "Email aperta",
|
||||
"email_received" => "Email ricevuta",
|
||||
"call" => "Telefonata",
|
||||
"meeting" => "Meeting",
|
||||
|
||||
@@ -96,6 +96,21 @@ class Mailing < ApplicationRecord
|
||||
mailing_recipients.failed.count
|
||||
end
|
||||
|
||||
def opened_count
|
||||
mailing_recipients.opened.count
|
||||
end
|
||||
|
||||
def variant_opened_count(variant)
|
||||
mailing_recipients.opened.where(ab_variant: variant).count
|
||||
end
|
||||
|
||||
def open_rate
|
||||
total = sent_count
|
||||
return 0 if total.zero?
|
||||
|
||||
((opened_count * 100.0) / total).round
|
||||
end
|
||||
|
||||
def queued_count
|
||||
mailing_recipients.queued.count
|
||||
end
|
||||
|
||||
@@ -5,12 +5,16 @@ class MailingRecipient < ApplicationRecord
|
||||
|
||||
validates :status, inclusion: { in: Catalog::MAILING_RECIPIENT_STATUSES.keys }
|
||||
validates :ab_variant, inclusion: { in: Catalog::AB_VARIANTS.keys }, allow_blank: true
|
||||
validates :tracking_token, presence: true, uniqueness: true
|
||||
|
||||
before_validation :assign_tracking_token, on: :create
|
||||
|
||||
scope :pending, -> { where(status: "pending") }
|
||||
scope :skipped, -> { where(status: "skipped") }
|
||||
scope :sent, -> { where(status: "sent") }
|
||||
scope :failed, -> { where(status: "failed") }
|
||||
scope :queued, -> { where(status: "queued") }
|
||||
scope :opened, -> { where.not(opened_at: nil) }
|
||||
scope :ordered, -> { joins(:organization).order("organizations.list_position ASC NULLS LAST", "organizations.name ASC") }
|
||||
|
||||
def status_label
|
||||
@@ -21,10 +25,33 @@ class MailingRecipient < ApplicationRecord
|
||||
status == "pending"
|
||||
end
|
||||
|
||||
def sent?
|
||||
status == "sent"
|
||||
end
|
||||
|
||||
def email_ok?
|
||||
email.present? && email.match?(URI::MailTo::EMAIL_REGEXP)
|
||||
end
|
||||
|
||||
def opened?
|
||||
opened_at.present?
|
||||
end
|
||||
|
||||
def record_open!(at: Time.current)
|
||||
return unless sent?
|
||||
|
||||
first_open = false
|
||||
with_lock do
|
||||
return unless sent?
|
||||
|
||||
first_open = opened_at.blank?
|
||||
attrs = { last_opened_at: at, open_count: open_count + 1 }
|
||||
attrs[:opened_at] = at if first_open
|
||||
update!(attrs)
|
||||
end
|
||||
log_open_activity! if first_open
|
||||
end
|
||||
|
||||
def merge_context
|
||||
opportunity = organization.campaign_opportunity(mailing.project)
|
||||
{
|
||||
@@ -90,6 +117,29 @@ class MailingRecipient < ApplicationRecord
|
||||
opportunity.update!(attrs) if attrs.any?
|
||||
end
|
||||
|
||||
def assign_tracking_token
|
||||
self.tracking_token ||= self.class.generate_tracking_token
|
||||
end
|
||||
|
||||
def self.generate_tracking_token
|
||||
loop do
|
||||
token = SecureRandom.urlsafe_base64(24)
|
||||
break token unless exists?(tracking_token: token)
|
||||
end
|
||||
end
|
||||
|
||||
def log_open_activity!
|
||||
organization.activities.create!(
|
||||
activity_type: "email_opened",
|
||||
subject: rendered_subject.presence || mailing.subject_for(ab_variant),
|
||||
description: "Apertura rilevata · campagna #{mailing.name}",
|
||||
happened_at: opened_at || Time.current,
|
||||
user: mailing.created_by,
|
||||
contact: contact,
|
||||
opportunity: organization.campaign_opportunity(mailing.project)
|
||||
)
|
||||
end
|
||||
|
||||
def log_activity!(subject_line)
|
||||
variant_note = "variante #{ab_variant}" if mailing.ab_test? && ab_variant.present?
|
||||
organization.activities.create!(
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
module Mailings
|
||||
class OpenPixel
|
||||
def self.call(html, recipient)
|
||||
new(html, recipient).call
|
||||
end
|
||||
|
||||
def initialize(html, recipient)
|
||||
@html = html.to_s
|
||||
@recipient = recipient
|
||||
end
|
||||
|
||||
def call
|
||||
return @html unless trackable?
|
||||
|
||||
url = tracking_url
|
||||
return @html if url.blank?
|
||||
|
||||
tag = %(<img src="#{ERB::Util.html_escape(url)}" width="1" height="1" alt="" style="display:block;border:0;width:1px;height:1px;opacity:0;" />)
|
||||
if @html.match?(/<\/body>/i)
|
||||
@html.sub(/<\/body>/i, "#{tag}</body>")
|
||||
else
|
||||
"#{@html}#{tag}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trackable?
|
||||
@recipient.respond_to?(:tracking_token) && @recipient.tracking_token.present?
|
||||
end
|
||||
|
||||
def tracking_url
|
||||
Rails.application.routes.url_helpers.mail_open_url(@recipient.tracking_token, **PublicUrl.options)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
module Mailings
|
||||
module PublicUrl
|
||||
module_function
|
||||
|
||||
def options
|
||||
opts = Rails.application.config.action_mailer.default_url_options.to_h.symbolize_keys
|
||||
opts[:protocol] ||= protocol
|
||||
port = opts[:port].to_i
|
||||
opts.delete(:port) if port.zero? || default_port?(port)
|
||||
opts
|
||||
end
|
||||
|
||||
def protocol
|
||||
return "https" if Rails.application.config.force_ssl
|
||||
return "https" if ENV["ASSUME_SSL"] == "true"
|
||||
|
||||
"http"
|
||||
end
|
||||
|
||||
def default_port?(port)
|
||||
(protocol == "https" && port == 443) || (protocol == "http" && port == 80)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,6 +17,7 @@
|
||||
<div class="mt-4"><%= progress_bar(mailing.progress_percentage, color: mailing.failed_count.positive? ? "bg-amber-500" : "bg-emerald-500") %></div>
|
||||
<div class="mt-2 flex flex-wrap gap-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<span><strong class="tabular-nums"><%= mailing.sent_count %></strong> inviate</span>
|
||||
<span><strong class="tabular-nums"><%= mailing.opened_count %></strong> aperte</span>
|
||||
<span><strong class="tabular-nums"><%= mailing.pending_count %></strong> in coda</span>
|
||||
<span><strong class="tabular-nums"><%= mailing.skipped_count %></strong> escluse</span>
|
||||
<% if mailing.failed_count.positive? %>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
<div class="mt-3"><%= progress_bar(mailing.progress_percentage) %></div>
|
||||
<div class="mt-2 text-xs text-zinc-500">
|
||||
<%= mailing.sent_count %> inviate · <%= mailing.pending_count %> in coda
|
||||
<%= mailing.sent_count %> inviate · <%= mailing.opened_count %> aperte · <%= mailing.pending_count %> in coda
|
||||
<% if mailing.sending? && mailing.next_send_at.present? && mailing.next_send_at > Time.current %>
|
||||
· Riparte <%= l(mailing.next_send_at, format: :short) %>
|
||||
<% end %>
|
||||
@@ -106,6 +106,7 @@
|
||||
<%= progress_bar(mailing.progress_percentage) %>
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
<%= mailing.sent_count %> inviate
|
||||
· <%= mailing.opened_count %> aperte
|
||||
· <%= mailing.pending_count %> in coda
|
||||
<% if mailing.failed_count.positive? %>
|
||||
· <span class="text-rose-700"><%= mailing.failed_count %> errori</span>
|
||||
|
||||
@@ -62,6 +62,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @mailing.sending? || @mailing.sent? || @mailing.cancelled? %>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Aperte</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.opened_count %></div>
|
||||
<p class="mt-1 text-xs text-zinc-500">su <%= @mailing.sent_count %> inviate</p>
|
||||
</div>
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Tasso apertura</div>
|
||||
<div class="mt-1 text-2xl font-semibold tabular-nums"><%= @mailing.open_rate %>%</div>
|
||||
<p class="mt-1 text-xs text-zinc-500">Si rileva quando il client carica le immagini.</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @mailing.ab_test? %>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="<%= card_class %> p-4">
|
||||
@@ -70,7 +85,11 @@
|
||||
<%= ab_variant_badge("A") %>
|
||||
</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.variant_sent_count("A") %> inviate</div>
|
||||
<p class="mt-1 text-xs text-zinc-500"><%= @mailing.variant_pending_count("A") %> in coda · <%= @mailing.subject %></p>
|
||||
<p class="mt-1 text-xs text-zinc-500">
|
||||
<%= @mailing.variant_opened_count("A") %> aperte
|
||||
· <%= @mailing.variant_pending_count("A") %> in coda
|
||||
· <%= @mailing.subject %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -78,7 +97,11 @@
|
||||
<%= ab_variant_badge("B") %>
|
||||
</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.variant_sent_count("B") %> inviate</div>
|
||||
<p class="mt-1 text-xs text-zinc-500"><%= @mailing.variant_pending_count("B") %> in coda · <%= @mailing.subject_b %></p>
|
||||
<p class="mt-1 text-xs text-zinc-500">
|
||||
<%= @mailing.variant_opened_count("B") %> aperte
|
||||
· <%= @mailing.variant_pending_count("B") %> in coda
|
||||
· <%= @mailing.subject_b %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -131,6 +154,7 @@
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<%= link_to recipient.organization.name, recipient.organization, class: "font-medium hover:underline" %>
|
||||
<%= mailing_recipient_status_badge(recipient.status) %>
|
||||
<%= mailing_open_badge(recipient) %>
|
||||
<% if @mailing.ab_test? %><%= ab_variant_badge(recipient.ab_variant) %><% end %>
|
||||
</div>
|
||||
<div class="mt-1 text-sm break-anywhere text-zinc-600 dark:text-zinc-300">
|
||||
@@ -148,6 +172,17 @@
|
||||
<% if recipient.error_message.present? %>
|
||||
<div class="mt-1 text-xs text-rose-700"><%= recipient.error_message %></div>
|
||||
<% end %>
|
||||
<% if recipient.sent_at.present? %>
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
Inviata <%= format_dt(recipient.sent_at) %>
|
||||
<% if recipient.opened? %>
|
||||
· aperta <%= format_dt(recipient.opened_at) %>
|
||||
<% if recipient.open_count > 1 %>
|
||||
· <%= recipient.open_count %> rilevamenti
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if recipient.email_ok? %>
|
||||
<%= link_to "Anteprima", mailing_path(@mailing, recipient_id: recipient.id, variant: recipient.ab_variant), class: "mt-2 inline-block text-sm text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
<% end %>
|
||||
@@ -167,6 +202,7 @@
|
||||
<th class="px-4 py-3">Test</th>
|
||||
<% end %>
|
||||
<th class="px-4 py-3">Stato</th>
|
||||
<th class="px-4 py-3">Apertura</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -195,11 +231,25 @@
|
||||
<% if recipient.error_message.present? %>
|
||||
<div class="text-xs text-rose-700"><%= recipient.error_message %></div>
|
||||
<% end %>
|
||||
<% if recipient.sent_at.present? %>
|
||||
<div class="text-xs text-zinc-500">Inviata <%= format_dt(recipient.sent_at) %></div>
|
||||
<% end %>
|
||||
</td>
|
||||
<% if @mailing.ab_test? %>
|
||||
<td class="px-4 py-3"><%= ab_variant_badge(recipient.ab_variant) || "—" %></td>
|
||||
<% end %>
|
||||
<td class="px-4 py-3"><%= mailing_recipient_status_badge(recipient.status) %></td>
|
||||
<td class="px-4 py-3">
|
||||
<%= mailing_open_badge(recipient) || "—" %>
|
||||
<% if recipient.opened? %>
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
<%= format_dt(recipient.opened_at) %>
|
||||
<% if recipient.open_count > 1 %>
|
||||
· <%= recipient.open_count %>×
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-sm">
|
||||
<% if recipient.email_ok? %>
|
||||
<%= link_to "Anteprima", mailing_path(@mailing, recipient_id: recipient.id, variant: recipient.ab_variant), class: "text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Rails.application.routes.draw do
|
||||
get "up" => "rails/health#show", as: :rails_health_check
|
||||
get "t/o/:token.gif", to: "mail_opens#show", as: :mail_open, constraints: { token: /[A-Za-z0-9_-]+/ }
|
||||
|
||||
get "login", to: "sessions#new"
|
||||
post "login", to: "sessions#create"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
class AddOpenTrackingToMailingRecipients < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
change_table :mailing_recipients do |t|
|
||||
t.string :tracking_token
|
||||
t.datetime :opened_at
|
||||
t.datetime :last_opened_at
|
||||
t.integer :open_count, null: false, default: 0
|
||||
end
|
||||
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
execute <<~SQL.squish
|
||||
UPDATE mailing_recipients
|
||||
SET tracking_token = replace(gen_random_uuid()::text, '-', '')
|
||||
WHERE tracking_token IS NULL
|
||||
SQL
|
||||
change_column_null :mailing_recipients, :tracking_token, false
|
||||
end
|
||||
end
|
||||
|
||||
add_index :mailing_recipients, :tracking_token, unique: true
|
||||
add_index :mailing_recipients, :opened_at
|
||||
end
|
||||
end
|
||||
Generated
+7
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_08_17_234500) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_08_20_110000) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
@@ -126,17 +126,23 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_17_234500) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "email"
|
||||
t.text "error_message"
|
||||
t.datetime "last_opened_at"
|
||||
t.bigint "mailing_id", null: false
|
||||
t.integer "open_count", default: 0, null: false
|
||||
t.datetime "opened_at"
|
||||
t.bigint "organization_id", null: false
|
||||
t.string "rendered_subject"
|
||||
t.datetime "sent_at"
|
||||
t.string "skip_reason"
|
||||
t.string "status", default: "pending", null: false
|
||||
t.string "tracking_token", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["ab_variant"], name: "index_mailing_recipients_on_ab_variant"
|
||||
t.index ["email"], name: "index_mailing_recipients_on_email"
|
||||
t.index ["mailing_id", "organization_id"], name: "idx_mailing_recipients_unique", unique: true
|
||||
t.index ["opened_at"], name: "index_mailing_recipients_on_opened_at"
|
||||
t.index ["status"], name: "index_mailing_recipients_on_status"
|
||||
t.index ["tracking_token"], name: "index_mailing_recipients_on_tracking_token", unique: true
|
||||
end
|
||||
|
||||
create_table "mailings", force: :cascade do |t|
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
require "test_helper"
|
||||
|
||||
class MailOpensControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
opportunities(:deal).update!(send_status: "to_send")
|
||||
@mailing = create_mailing(audience: "to_send")
|
||||
@mailing.rebuild_recipients!
|
||||
@recipient = @mailing.mailing_recipients.first
|
||||
@recipient.update!(status: "sent", sent_at: Time.current, rendered_subject: "Ciao ASD Test Calcio")
|
||||
end
|
||||
|
||||
test "records the first open without login and returns a gif" do
|
||||
assert_difference -> { Activity.where(activity_type: "email_opened").count }, 1 do
|
||||
get mail_open_path(@recipient.tracking_token)
|
||||
end
|
||||
|
||||
assert_response :success
|
||||
assert_equal "image/gif", response.media_type
|
||||
@recipient.reload
|
||||
assert @recipient.opened?
|
||||
assert_equal 1, @recipient.open_count
|
||||
assert_equal @recipient.opened_at, @recipient.last_opened_at
|
||||
end
|
||||
|
||||
test "counts later loads without duplicating the timeline activity" do
|
||||
get mail_open_path(@recipient.tracking_token)
|
||||
assert_no_difference -> { Activity.where(activity_type: "email_opened").count } do
|
||||
get mail_open_path(@recipient.tracking_token)
|
||||
end
|
||||
|
||||
@recipient.reload
|
||||
assert_equal 2, @recipient.open_count
|
||||
assert @recipient.last_opened_at >= @recipient.opened_at
|
||||
end
|
||||
|
||||
test "unknown token still returns a gif" do
|
||||
get mail_open_path("missing-token-value-here")
|
||||
assert_response :success
|
||||
assert_equal "image/gif", response.media_type
|
||||
end
|
||||
|
||||
test "does not mark pending emails as opened" do
|
||||
@recipient.update!(status: "pending", sent_at: nil)
|
||||
get mail_open_path(@recipient.tracking_token)
|
||||
|
||||
assert_response :success
|
||||
@recipient.reload
|
||||
assert_not @recipient.opened?
|
||||
assert_equal 0, @recipient.open_count
|
||||
end
|
||||
end
|
||||
@@ -65,6 +65,8 @@ class MailingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "contacted", opportunities(:deal).pipeline_stage
|
||||
assert_equal 2, ActionMailer::Base.deliveries.size
|
||||
assert_equal "Ciao ASD Test Calcio", ActionMailer::Base.deliveries.last.subject
|
||||
body = (ActionMailer::Base.deliveries.last.html_part || ActionMailer::Base.deliveries.last).body.to_s
|
||||
assert_match(%r{/t/o/#{Regexp.escape(recipient.reload.tracking_token)}\.gif}, body)
|
||||
end
|
||||
|
||||
test "queue send requires test email first" do
|
||||
@@ -96,7 +98,9 @@ class MailingsControllerTest < ActionDispatch::IntegrationTest
|
||||
mail = ActionMailer::Base.deliveries.last
|
||||
assert_equal [users(:admin).email], mail.to
|
||||
assert_match(/\[TEST\]/, mail.subject)
|
||||
body = (mail.html_part || mail).body.to_s
|
||||
assert_match(/Società Prova/, mail.subject)
|
||||
assert_no_match(%r{/t/o/}, body)
|
||||
assert mailing.reload.tested?
|
||||
assert_equal users(:admin).email, mailing.test_sent_to
|
||||
end
|
||||
|
||||
@@ -23,6 +23,8 @@ class CampaignMailerTest < ActionMailer::TestCase
|
||||
assert_equal "Ciao ASD Test Calcio", email.subject
|
||||
body = (email.html_part || email).body.to_s
|
||||
assert_match(/Ciao Mario Rossi di ASD Test Calcio/, body)
|
||||
recipient.reload
|
||||
assert_match(%r{/t/o/#{Regexp.escape(recipient.tracking_token)}\.gif}, body)
|
||||
assert_equal 1, email.attachments.size
|
||||
assert_equal "brochure.pdf", email.attachments.first.filename
|
||||
end
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
require "test_helper"
|
||||
|
||||
class Mailings::OpenPixelTest < ActiveSupport::TestCase
|
||||
test "injects a tracking pixel before body close" do
|
||||
mailing = create_mailing
|
||||
mailing.rebuild_recipients!
|
||||
recipient = mailing.mailing_recipients.first
|
||||
html = Mailings::OpenPixel.call("<html><body><p>Ciao</p></body></html>", recipient)
|
||||
|
||||
assert_includes html, "/t/o/#{recipient.tracking_token}.gif"
|
||||
assert_match %r{<img src="http://example.com/t/o/#{Regexp.escape(recipient.tracking_token)}\.gif"[^>]*width="1"}, html
|
||||
assert_includes html, "</body>"
|
||||
assert html.index("t/o/") < html.index("</body>")
|
||||
end
|
||||
|
||||
test "skips test recipients without a token" do
|
||||
html = "<p>Ciao</p>"
|
||||
recipient = Mailings::TestRecipient.new(mailing: create_mailing, email: "test@example.com")
|
||||
|
||||
assert_equal html, Mailings::OpenPixel.call(html, recipient)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user