Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
CI / scan_ruby (push) Failing after 11m20s
CI / scan_js (push) Successful in 10m35s
CI / lint (push) Has been cancelled

Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 23:01:48 +02:00
co-authored by Cursor
commit c4e5f289cf
258 changed files with 11293 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
require "test_helper"
class AuthenticationTest < ActionDispatch::IntegrationTest
test "requires login" do
get root_path
assert_redirected_to login_path
end
test "login and logout" do
login_as users(:admin)
assert_response :redirect
follow_redirect!
assert_response :success
delete logout_path
assert_redirected_to login_path
end
test "non admin cannot create users" do
login_as users(:marco)
follow_redirect! if response.redirect?
post users_path, params: {
user: { email: "x@example.com", first_name: "X", last_name: "Y", role: "user", password: "password123", password_confirmation: "password123" }
}
assert_response :redirect
end
test "project space is isolated by url" do
login_as users(:admin)
follow_redirect! if response.redirect?
get project_root_path(project_code: "matchlivetv")
assert_response :success
assert_match(/MatchLiveTV/, response.body)
get project_root_path(project_code: "riskmeter")
assert_response :success
assert_match(/RiskMeter/, response.body)
end
test "user cannot open disabled project" do
login_as users(:marco)
follow_redirect! if response.redirect?
get project_root_path(project_code: "riskmeter")
assert_redirected_to root_path
end
test "disabled user cannot login" do
admin = users(:admin)
User.create!(
email: "keeper@example.com",
first_name: "Keep",
last_name: "Admin",
role: "admin",
password: "password123",
password_confirmation: "password123"
)
assert admin.update(active: false)
post login_path, params: { email: admin.email, password: "password123" }
assert_response :unprocessable_entity
assert_match(/Email o password non validi/i, response.body)
end
test "admin can open platform settings" do
login_as users(:admin)
follow_redirect! if response.redirect?
get admin_path
assert_response :success
assert_match(/Impostazioni piattaforma/, response.body)
assert_match(/Utenti/, response.body)
end
test "non admin cannot open platform settings" do
login_as users(:marco)
follow_redirect! if response.redirect?
get admin_path
assert_redirected_to root_path
end
test "admin home has new project and settings actions" do
login_as users(:admin)
follow_redirect! if response.redirect?
get root_path
assert_response :success
assert_match(/Nuovo progetto/, response.body)
assert_match(/Impostazioni/, response.body)
assert_no_match(/Amministrazione progetti/, response.body)
end
end
@@ -0,0 +1,37 @@
require "test_helper"
class MailIdentitiesControllerTest < ActionDispatch::IntegrationTest
test "non admin cannot manage smtp accounts" do
login_as users(:marco)
follow_redirect! if response.redirect?
get mail_identities_path
assert_redirected_to root_path
end
test "admin can create an smtp account" do
login_as users(:admin)
follow_redirect! if response.redirect?
assert_difference "MailIdentity.count", 1 do
post mail_identities_path, params: {
mail_identity: {
name: "MatchLiveTV",
from_name: "MatchLiveTV",
from_email: "hello@matchlivetv.test",
smtp_host: "smtp.test",
smtp_port: 587,
smtp_username: "hello@matchlivetv.test",
smtp_password: "super-secret",
smtp_authentication: "plain",
encryption: "starttls",
verify_ssl: "1",
active: "1"
}
}
end
assert_redirected_to mail_identities_path
identity = MailIdentity.find_by!(from_email: "hello@matchlivetv.test")
assert_equal "super-secret", identity.smtp_password
end
end
@@ -0,0 +1,44 @@
require "test_helper"
require "base64"
class MailImagesControllerTest < ActionDispatch::IntegrationTest
PNG = Base64.decode64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")
test "admin can upload an image for the editor" do
login_as users(:admin)
follow_redirect! if response.redirect?
upload = png_upload
assert_difference -> { ActiveStorage::Blob.count }, 1 do
post mail_images_path(project_code: "matchlivetv"), params: { file: upload }
end
assert_response :success
json = JSON.parse(response.body)
assert json["url"].present?
assert_match(/active_storage/, json["url"])
end
test "rejects non image files" do
login_as users(:admin)
follow_redirect! if response.redirect?
file = Tempfile.new(["note", ".txt"])
file.write("not-an-image")
file.rewind
post mail_images_path(project_code: "matchlivetv"), params: {
file: Rack::Test::UploadedFile.new(file.path, "text/plain")
}
assert_response :unprocessable_entity
end
private
def png_upload
file = Tempfile.new(["pixel", ".png"])
file.binmode
file.write(PNG)
file.rewind
Rack::Test::UploadedFile.new(file.path, "image/png")
end
end
@@ -0,0 +1,107 @@
require "test_helper"
class MailingsControllerTest < ActionDispatch::IntegrationTest
setup do
@project = projects(:matchlivetv)
@identity = create_mail_identity
opportunities(:deal).update!(send_status: "to_send", pipeline_stage: "to_contact")
end
test "lists mailings in project space" do
login_as users(:admin)
follow_redirect! if response.redirect?
get mailings_path(project_code: @project.code)
assert_response :success
assert_match(/Email/, response.body)
end
test "creates a draft and builds recipients" do
login_as users(:admin)
follow_redirect! if response.redirect?
assert_difference -> { Mailing.count } => 1, -> { MailingRecipient.count } => 1 do
post mailings_path(project_code: @project.code), params: {
mailing: {
name: "Lancio",
mail_identity_id: @identity.id,
audience: "to_send",
subject: "Ciao {{societa}}",
body_html: "<p>Ciao {{contatto_nome}}</p>",
interval_seconds: 0
}
}
end
mailing = Mailing.last
assert_redirected_to mailing_path(mailing, project_code: @project.code)
assert_equal "pending", mailing.mailing_recipients.first.status
end
test "check recipients then queue send" do
login_as users(:admin)
follow_redirect! if response.redirect?
mailing = create_mailing(identity: @identity, audience: "to_send")
mailing.rebuild_recipients!
recipient = mailing.mailing_recipients.first
patch update_recipients_mailing_path(mailing, project_code: @project.code), params: { pending_ids: [recipient.id] }
assert_redirected_to mailing_path(mailing, project_code: @project.code)
assert_equal "pending", recipient.reload.status
perform_enqueued_jobs do
post queue_mailing_path(mailing, project_code: @project.code)
end
assert_equal "sent", mailing.reload.status
assert_equal "sent", recipient.reload.status
assert_equal "sent", opportunities(:deal).reload.send_status
assert_equal "contacted", opportunities(:deal).pipeline_stage
assert_equal 1, ActionMailer::Base.deliveries.size
assert_equal "Ciao ASD Test Calcio", ActionMailer::Base.deliveries.last.subject
end
test "test send goes to current user" do
login_as users(:admin)
follow_redirect! if response.redirect?
mailing = create_mailing(identity: @identity, audience: "all")
mailing.rebuild_recipients!
assert_emails 1 do
post test_send_mailing_path(mailing, project_code: @project.code)
end
mail = ActionMailer::Base.deliveries.last
assert_equal [users(:admin).email], mail.to
assert_match(/\[TEST\]/, mail.subject)
end
test "creates an A/B mailing and sends variant B as test" do
login_as users(:admin)
follow_redirect! if response.redirect?
post mailings_path(project_code: @project.code), params: {
mailing: {
name: "AB lancio",
mail_identity_id: @identity.id,
audience: "to_send",
ab_test: "1",
ab_assignment: "from_record",
subject: "Oggetto A {{societa}}",
body_html: "<p>Versione A</p>",
subject_b: "Oggetto B {{societa}}",
body_html_b: "<p>Versione B</p>",
interval_seconds: 0
}
}
mailing = Mailing.last
assert mailing.ab_test?
assert_equal "A", mailing.mailing_recipients.first.ab_variant
assert_emails 1 do
post test_send_mailing_path(mailing, project_code: @project.code, variant: "B")
end
mail = ActionMailer::Base.deliveries.last
assert_match(/\[TEST B\] Oggetto B ASD Test Calcio/, mail.subject)
body = (mail.html_part || mail).body.to_s
assert_match(/Versione B/, body)
end
end
+117
View File
@@ -0,0 +1,117 @@
require "test_helper"
class UsersControllerTest < ActionDispatch::IntegrationTest
test "non admin cannot list or create users" do
login_as users(:marco)
follow_redirect! if response.redirect?
get users_path
assert_redirected_to root_path
post users_path, params: {
user: { email: "x@example.com", first_name: "X", last_name: "Y", role: "user", password: "password123", password_confirmation: "password123" }
}
assert_redirected_to root_path
end
test "admin can create a user" do
login_as users(:admin)
follow_redirect! if response.redirect?
assert_difference "User.count", 1 do
post users_path, params: {
user: {
email: "nuovo@example.com",
first_name: "Nuovo",
last_name: "Utente",
role: "user",
password: "password123",
password_confirmation: "password123"
},
project_ids: [ projects(:matchlivetv).id ]
}
end
assert_redirected_to users_path
user = User.find_by!(email: "nuovo@example.com")
assert user.project_enabled?(projects(:matchlivetv))
assert_not user.project_enabled?(projects(:riskmeter))
end
test "admin can disable another admin when a second admin exists" do
login_as users(:admin)
follow_redirect! if response.redirect?
other = User.create!(
email: "emiliano@example.com",
first_name: "Emiliano",
last_name: "Admin",
role: "admin",
password: "password123",
password_confirmation: "password123"
)
patch user_path(other), params: {
user: { first_name: other.first_name, last_name: other.last_name, email: other.email, role: "admin", active: "0" }
}
assert_redirected_to users_path
assert_not other.reload.active?
end
test "admin cannot disable last active admin" do
login_as users(:admin)
follow_redirect! if response.redirect?
admin = users(:admin)
patch user_path(admin), params: {
user: { first_name: admin.first_name, last_name: admin.last_name, email: admin.email, role: "admin", active: "0" }
}
assert_response :unprocessable_entity
assert admin.reload.active?
assert_match(/almeno un amministratore attivo/i, response.body)
end
test "admin cannot delete self" do
login_as users(:admin)
follow_redirect! if response.redirect?
assert_no_difference "User.count" do
delete user_path(users(:admin))
end
assert_redirected_to users_path
follow_redirect!
assert_match(/Non puoi eliminare il tuo account/i, response.body)
end
test "admin can delete another admin when a second remains" do
login_as users(:admin)
follow_redirect! if response.redirect?
other = User.create!(
email: "other-admin@example.com",
first_name: "Other",
last_name: "Admin",
role: "admin",
password: "password123",
password_confirmation: "password123"
)
assert_difference "User.count", -1 do
delete user_path(other)
end
assert_redirected_to users_path
end
test "admin can delete another user and nullify activities" do
login_as users(:admin)
follow_redirect! if response.redirect?
marco = users(:marco)
activity = activities(:note)
assert_equal marco.id, activity.user_id
assert_difference "User.count", -1 do
delete user_path(marco)
end
assert_redirected_to users_path
assert_nil activity.reload.user_id
end
end
+9
View File
@@ -0,0 +1,9 @@
note:
activity_type: note
subject: Prima nota
description: Test
happened_at: <%= 2.days.ago %>
user: marco
organization: acme
contact: primary
opportunity: deal
+9
View File
@@ -0,0 +1,9 @@
primary:
organization: acme
first_name: Mario
last_name: Rossi
role: Presidente
email: mario@asdtest.example.it
phone: "+39 333 0001111"
primary_contact: true
preferred_contact_method: email
+11
View File
@@ -0,0 +1,11 @@
deal:
organization: acme
project: matchlivetv
name: Licenza Full
pipeline_stage: interested
estimated_value: 1990
probability: 40
product: Full
assigned_user: marco
stage_changed_at: <%= 3.days.ago %>
first_contacted_at: <%= 10.days.ago %>
+3
View File
@@ -0,0 +1,3 @@
acme_matchlivetv:
organization: acme
project: matchlivetv
+12
View File
@@ -0,0 +1,12 @@
acme:
name: ASD Test Calcio
organization_type: societa_sportiva
sport: Calcio
country: Italia
region: Lombardia
city: Milano
status: prospect
lead_source: outbound
assigned_user: marco
email: info@asdtest.example.it
website: asdtest.example.it
+5
View File
@@ -0,0 +1,5 @@
light:
name: Light
code: light
active: true
position: 0
+13
View File
@@ -0,0 +1,13 @@
matchlivetv:
name: MatchLiveTV
code: matchlivetv
description: Streaming
active: true
position: 0
riskmeter:
name: RiskMeter
code: riskmeter
description: Risk
active: true
position: 1
+8
View File
@@ -0,0 +1,8 @@
september:
name: Prime 10 società paganti
metric: customers_acquired
target_value: 10
start_date: 2026-08-16
end_date: 2026-09-30
active: true
project: matchlivetv
+10
View File
@@ -0,0 +1,10 @@
follow_up:
title: Richiama presidente
organization: acme
contact: primary
opportunity: deal
assigned_user: marco
due_at: <%= Time.zone.now.change(hour: 15) %>
priority: high
task_type: follow_up
status: pending
+9
View File
@@ -0,0 +1,9 @@
marco_matchlivetv:
user: marco
project: matchlivetv
enabled: true
marco_riskmeter:
user: marco
project: riskmeter
enabled: false
+17
View File
@@ -0,0 +1,17 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
admin:
email: admin@example.com
password_digest: <%= BCrypt::Password.create("password123") %>
first_name: Admin
last_name: User
role: admin
active: true
marco:
email: marco@example.com
password_digest: <%= BCrypt::Password.create("password123") %>
first_name: Marco
last_name: Test
role: user
active: true
@@ -0,0 +1,17 @@
require "test_helper"
class SendMailingRecipientJobTest < ActiveJob::TestCase
test "sends pending recipients in sequence" do
opportunities(:deal).update!(send_status: "to_send")
mailing = create_mailing(audience: "to_send")
mailing.rebuild_recipients!
mailing.update!(status: "sending", queued_at: Time.current)
perform_enqueued_jobs do
SendMailingRecipientJob.perform_later(mailing.mailing_recipients.first.id)
end
assert_equal "sent", mailing.reload.status
assert_equal "sent", mailing.mailing_recipients.first.status
end
end
+47
View File
@@ -0,0 +1,47 @@
require "test_helper"
require "base64"
class CampaignMailerTest < ActionMailer::TestCase
test "sends html with merge and attachments from the identity" do
mailing = create_mailing
mailing.rebuild_recipients!
mailing.files.attach(io: StringIO.new("brochure"), filename: "brochure.pdf", content_type: "application/pdf")
recipient = mailing.mailing_recipients.first
email = CampaignMailer.outreach(
recipient,
html: recipient.rendered_html,
subject: recipient.rendered_subject_line
)
assert_emails 1 do
email.deliver_now
end
assert_equal ["hello@example.com"], email.from
assert_equal ["mario@asdtest.example.it"], email.to
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)
assert_equal 1, email.attachments.size
assert_equal "brochure.pdf", email.attachments.first.filename
end
test "inlines active storage images as cid attachments" do
png = Base64.decode64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")
blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(png), filename: "pixel.png", content_type: "image/png")
src = Rails.application.routes.url_helpers.rails_blob_path(blob, only_path: true)
mailing = create_mailing(body_html: %(<p>Foto</p><figure><img src="#{src}" width="240" height="120"></figure>))
mailing.rebuild_recipients!
recipient = mailing.mailing_recipients.first
email = CampaignMailer.outreach(recipient, html: recipient.rendered_html, subject: "Foto")
email.deliver_now
assert email.attachments.any? { |att| att.filename.to_s.include?("pixel.png") }
body = (email.html_part || email).body.to_s
assert_match(/cid:/, body)
assert_match(/width:\s*240px/, body)
assert_no_match(/<figure/, body)
end
end
+16
View File
@@ -0,0 +1,16 @@
require "test_helper"
class DashboardMetricsTest < ActiveSupport::TestCase
test "computes stage counts and pipeline value" do
metrics = Dashboard::Metrics.new
assert metrics.stage_counts.key?("interested")
assert_kind_of Numeric, metrics.open_pipeline_value
assert_kind_of Array, metrics.funnel_steps
end
test "goal progress" do
goal = sales_goals(:september)
assert_equal 0, goal.current_value
assert_equal 0, goal.progress_percentage
end
end
+35
View File
@@ -0,0 +1,35 @@
require "test_helper"
class OpportunityTest < ActiveSupport::TestCase
setup do
@opp = opportunities(:deal)
@user = users(:marco)
Current.user = @user
Current.project = projects(:matchlivetv)
end
teardown do
Current.user = nil
Current.project = nil
end
test "move to won sets won_at and activates customer" do
@opp.move_to_stage!("won", user: @user)
assert @opp.won?
assert_not_nil @opp.won_at
assert_equal "active_customer", @opp.organization.reload.status
end
test "move to lost requires reason" do
assert_raises(ActiveRecord::RecordInvalid) do
@opp.move_to_stage!("lost", user: @user)
end
end
test "move to lost with reason" do
@opp.move_to_stage!("lost", lost_reason: "price", user: @user)
assert @opp.lost?
assert_equal "price", @opp.lost_reason
assert_not_nil @opp.lost_at
end
end
+16
View File
@@ -0,0 +1,16 @@
require "test_helper"
class OrganizationTest < ActiveSupport::TestCase
test "requires name" do
org = Organization.new(status: "prospect", organization_type: "azienda")
assert_not org.valid?
assert_includes org.errors[:name], "non può essere vuoto"
end
test "has associations" do
org = organizations(:acme)
assert_includes org.contacts, contacts(:primary)
assert_includes org.opportunities, opportunities(:deal)
assert_includes org.tasks, tasks(:follow_up)
end
end
+21
View File
@@ -0,0 +1,21 @@
require "test_helper"
class ProjectAccessTest < ActiveSupport::TestCase
test "admin can access all projects" do
admin = users(:admin)
assert admin.can_access_project?(projects(:matchlivetv))
assert admin.can_access_project?(projects(:riskmeter))
assert_equal Project.active.count, admin.accessible_projects.count
end
test "user only accesses enabled projects" do
marco = users(:marco)
assert marco.can_access_project?(projects(:matchlivetv))
assert_not marco.can_access_project?(projects(:riskmeter))
end
test "organization scoped to project" do
assert_includes Organization.for_project(projects(:matchlivetv)), organizations(:acme)
assert_not_includes Organization.for_project(projects(:riskmeter)), organizations(:acme)
end
end
+19
View File
@@ -0,0 +1,19 @@
require "test_helper"
class TaskTest < ActiveSupport::TestCase
setup do
@task = tasks(:follow_up)
@user = users(:marco)
Current.user = @user
end
teardown { Current.user = nil }
test "complete creates activity" do
assert_difference -> { Activity.count }, 1 do
assert @task.complete!(user: @user)
end
assert @task.completed?
assert_not_nil @task.completed_at
end
end
+51
View File
@@ -0,0 +1,51 @@
require "test_helper"
class UserManagementTest < ActiveSupport::TestCase
test "cannot deactivate the last active admin" do
admin = users(:admin)
assert admin.last_active_admin?
assert_not admin.can_be_deactivated?
admin.active = false
assert_not admin.valid?
assert_includes admin.errors[:base].join, "almeno un amministratore attivo"
end
test "cannot demote the last active admin" do
admin = users(:admin)
admin.role = "user"
assert_not admin.valid?
assert_includes admin.errors[:base].join, "almeno un amministratore attivo"
end
test "can deactivate seed admin when another admin exists" do
other = User.create!(
email: "second-admin@example.com",
first_name: "Second",
last_name: "Admin",
role: "admin",
password: "password123",
password_confirmation: "password123",
active: true
)
admin = users(:admin)
assert_not admin.last_active_admin?
assert admin.can_be_deactivated?
assert admin.update(active: false)
assert_not admin.reload.active?
assert other.reload.active?
end
test "cannot destroy the last active admin" do
admin = users(:admin)
assert_not admin.destroy
assert admin.errors[:base].join.include?("unico amministratore attivo") || User.exists?(admin.id)
end
test "can destroy a regular user" do
marco = users(:marco)
assert marco.destroy
assert_not User.exists?(marco.id)
end
end
@@ -0,0 +1,51 @@
require "test_helper"
require "tempfile"
class CampaignImportMatchlivetvLaunchTest < ActiveSupport::TestCase
test "imports campaign rows onto MatchLiveTV" do
csv = Tempfile.new(["campagna", ".csv"])
csv.write(<<~CSV)
N.,Regione,Prov.,Società,M/F,Email verificata,Sito / profilo,Streaming rilevato,Evidenza / fit commerciale,Test A/B,Stato invio,Data invio,Esito,Demo / Trial,Conversione,Piano,Fonte contatto / ricerca,Data verifica,Note follow-up
1,Piemonte,TO,Vol-Ley Academy Volpiano,F,volley.academy.to@gmail.com,https://www.facebook.com/p/Vol-Ley-Academy-100063526780569/,NON RILEVATO,Academy giovanile,A,DA INVIARE,,,NO,NO,,https://example.com/fonte,2026-08-17,
7,Lombardia,MB,Volley Brianza Est,F,ds@volleybrianzaest.it,https://www.volleybrianzaest.it/, / PARZIALE,U16/U18 nazionale,A,DA INVIARE,,,NO,NO,,https://www.volleybrianzaest.it/,46251,
100,Valle d'Aosta,AO,Cogne Aosta Volley,F,cogneaostavolley@gmail.com,https://www.youtube.com/@CogneAostaVolley, SPORTCAM,Vivaio prolifico,B,DA INVIARE,,,NO,NO,,https://www.fipav-vda.com/societa,2026-08-17,
CSV
csv.flush
result = CampaignImport::MatchlivetvLaunch.new(
path: csv.path,
user: users(:admin),
project: projects(:matchlivetv),
wipe: false
).call
assert_empty result.errors, result.errors.inspect
assert_equal 3, result.imported
org = Organization.find_by!(name: "Vol-Ley Academy Volpiano")
assert_equal "female", org.team_gender
assert_equal "not_detected", org.streaming_status
assert_equal 1, org.list_position
assert_equal Date.new(2026, 8, 17), org.verified_at
assert_equal "campaign", org.lead_source
assert org.projects.exists?(code: "matchlivetv")
assert_equal "volley.academy.to@gmail.com", org.contacts.primary_first.first.email
opp = org.campaign_opportunity(projects(:matchlivetv))
assert_equal "A", opp.ab_variant
assert_equal "to_send", opp.send_status
assert_equal "to_contact", opp.pipeline_stage
assert_not opp.demo_trial
assert_not opp.converted
brianza = Organization.find_by!(name: "Volley Brianza Est")
assert_equal "yes_partial", brianza.streaming_status
assert_equal Date.new(2026, 8, 17), brianza.verified_at
cogne = Organization.find_by!(name: "Cogne Aosta Volley")
assert_equal "yes_sportcam", cogne.streaming_status
assert_equal "B", cogne.campaign_opportunity(projects(:matchlivetv)).ab_variant
ensure
csv.close!
end
end
+36
View File
@@ -0,0 +1,36 @@
require "test_helper"
class MailMergeTest < ActiveSupport::TestCase
test "replaces tokens from organization and contact" do
html = MailMerge.render(
"Ciao {{contatto_nome}} di {{societa}} in {{regione}}",
organization: organizations(:acme),
contact: contacts(:primary),
project: projects(:matchlivetv),
opportunity: opportunities(:deal)
)
assert_equal "Ciao Mario Rossi di ASD Test Calcio in Lombardia", html
end
test "falls back to primary contact and org email" do
vars = MailMerge.variables_for(organization: organizations(:acme), project: projects(:matchlivetv))
assert_equal "Mario Rossi", vars["contatto_nome"]
assert_equal "mario@asdtest.example.it", vars["email"]
assert_equal "MatchLiveTV", vars["progetto"]
assert_equal "Full", vars["piano"]
end
test "test_ab prefers explicit variant" do
vars = MailMerge.variables_for(
organization: organizations(:acme),
project: projects(:matchlivetv),
ab_variant: "B"
)
assert_equal "B", vars["test_ab"]
end
test "unknown tokens become empty string" do
assert_equal "X Y", MailMerge.render("X {{sconosciuto}} Y")
end
end
@@ -0,0 +1,75 @@
require "test_helper"
class MailingsRecipientBuilderTest < ActiveSupport::TestCase
setup do
opportunities(:deal).update!(send_status: "to_send", ab_variant: "A")
end
test "audience to_send includes only pending campaign orgs with valid email" do
mailing = create_mailing(audience: "to_send")
mailing.rebuild_recipients!
recipient = mailing.mailing_recipients.find_by!(organization: organizations(:acme))
assert_equal "pending", recipient.status
assert_equal "mario@asdtest.example.it", recipient.email
end
test "skips organizations without email" do
org = organizations(:acme)
org.update!(email: nil)
org.contacts.update_all(email: nil)
mailing = create_mailing(audience: "all")
mailing.rebuild_recipients!
recipient = mailing.mailing_recipients.find_by!(organization: org)
assert_equal "skipped", recipient.status
assert_equal "manca email", recipient.skip_reason
end
test "audience test_a filters by variant" do
mailing = create_mailing(audience: "test_a")
mailing.rebuild_recipients!
assert_equal 1, mailing.mailing_recipients.count
mailing.update!(audience: "test_b")
mailing.rebuild_recipients!
assert_equal 0, mailing.mailing_recipients.count
end
test "ab test from_record uses opportunity variant" do
create_campaign_org(name: "Beta Volley", email: "beta@example.com", ab_variant: "B", list_position: 2)
mailing = create_mailing(
audience: "all",
ab_test: true,
ab_assignment: "from_record",
subject_b: "Offerta B {{societa}}",
body_html_b: "<p>Versione B</p>"
)
mailing.rebuild_recipients!
acme = mailing.mailing_recipients.find_by!(organization: organizations(:acme))
beta = mailing.mailing_recipients.joins(:organization).find_by!(organizations: { name: "Beta Volley" })
assert_equal "A", acme.ab_variant
assert_equal "B", beta.ab_variant
assert_match(/Ciao Mario Rossi/, acme.rendered_html)
assert_equal "Offerta B Beta Volley", beta.rendered_subject_line
assert_match(/Versione B/, beta.rendered_html)
end
test "ab test split alternates A and B" do
create_campaign_org(name: "Beta Volley", email: "beta@example.com", ab_variant: "A", list_position: 2)
organizations(:acme).update!(list_position: 1)
mailing = create_mailing(
audience: "all",
ab_test: true,
ab_assignment: "split",
subject_b: "B {{societa}}",
body_html_b: "<p>B</p>"
)
mailing.rebuild_recipients!
variants = mailing.mailing_recipients.pending.ordered.map(&:ab_variant)
assert_equal %w[A B], variants
end
end
+85
View File
@@ -0,0 +1,85 @@
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
module ActiveSupport
class TestCase
parallelize(workers: :number_of_processors)
fixtures :all
end
end
class ActionDispatch::IntegrationTest
def login_as(user, password: "password123")
post login_path, params: { email: user.email, password: password }
end
end
module MailMarketingTestHelper
def create_mail_identity(**attrs)
MailIdentity.create!(
{
name: "SMTP test",
from_name: "eminuxCRM",
from_email: "hello@example.com",
smtp_host: "smtp.example.com",
smtp_port: 587,
smtp_username: "user",
smtp_password: "secret",
smtp_authentication: "plain",
encryption: "starttls",
verify_ssl: true,
active: true
}.merge(attrs)
)
end
def create_mailing(project: projects(:matchlivetv), identity: nil, **attrs)
Mailing.create!(
{
project: project,
mail_identity: identity || create_mail_identity,
name: "Campagna test",
subject: "Ciao {{societa}}",
body_html: "<p>Ciao {{contatto_nome}} di {{societa}}</p>",
audience: "all",
interval_seconds: 0,
ab_test: false,
ab_assignment: "from_record"
}.merge(attrs)
)
end
def create_campaign_org(name:, email:, ab_variant: nil, send_status: "to_send", list_position: nil)
project = projects(:matchlivetv)
org = Organization.new(
name: name,
status: "prospect",
organization_type: "societa_sportiva",
email: email,
list_position: list_position
)
org.organization_projects.build(project: project)
org.save!
Contact.create!(
organization: org,
first_name: "Contatto",
last_name: name,
email: email,
primary_contact: true
)
Opportunity.create!(
organization: org,
project: project,
name: "Campagna",
pipeline_stage: "to_contact",
send_status: send_status,
ab_variant: ab_variant
)
org
end
end
ActiveSupport::TestCase.include MailMarketingTestHelper
ActionDispatch::IntegrationTest.include MailMarketingTestHelper