add newsletter and email campaigns
Subscribers with double opt-in confirmation, campaign composer with draft/scheduled/sent lifecycle, admin dashboard with overview stats, CSV export, and shop signup form wired into page builder blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
96
test/berrypod_web/controllers/newsletter_controller_test.exs
Normal file
96
test/berrypod_web/controllers/newsletter_controller_test.exs
Normal file
@@ -0,0 +1,96 @@
|
||||
defmodule BerrypodWeb.NewsletterControllerTest do
|
||||
use BerrypodWeb.ConnCase, async: true
|
||||
|
||||
alias Berrypod.Newsletter
|
||||
alias Berrypod.Newsletter.Subscriber
|
||||
alias Berrypod.Repo
|
||||
|
||||
describe "POST /newsletter/subscribe" do
|
||||
test "subscribes and redirects back", %{conn: conn} do
|
||||
Newsletter.set_newsletter_enabled(true)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("referer", "http://localhost:4002/")
|
||||
|> post(~p"/newsletter/subscribe", %{email: "new@example.com"})
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Check your inbox"
|
||||
end
|
||||
|
||||
test "handles already confirmed subscriber", %{conn: conn} do
|
||||
Newsletter.set_newsletter_enabled(true)
|
||||
|
||||
# Create a confirmed subscriber directly
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
|
||||
%Subscriber{}
|
||||
|> Subscriber.changeset(%{
|
||||
email: "confirmed@example.com",
|
||||
status: "confirmed",
|
||||
confirmed_at: now
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("referer", "http://localhost:4002/about")
|
||||
|> post(~p"/newsletter/subscribe", %{email: "confirmed@example.com"})
|
||||
|
||||
assert redirected_to(conn) == "/about"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "already subscribed"
|
||||
end
|
||||
|
||||
test "shows error for invalid email", %{conn: conn} do
|
||||
Newsletter.set_newsletter_enabled(true)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("referer", "http://localhost:4002/")
|
||||
|> post(~p"/newsletter/subscribe", %{email: "not-an-email"})
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "valid email"
|
||||
end
|
||||
|
||||
test "redirects to / when no referer", %{conn: conn} do
|
||||
Newsletter.set_newsletter_enabled(true)
|
||||
|
||||
conn = post(conn, ~p"/newsletter/subscribe", %{email: "test@example.com"})
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /newsletter/confirm/:token" do
|
||||
test "confirms pending subscriber", %{conn: conn} do
|
||||
# Create a pending subscriber with a known raw token
|
||||
raw_token = "test-confirmation-token-abc123"
|
||||
hashed_token = :crypto.hash(:sha256, raw_token) |> Base.encode16(case: :lower)
|
||||
|
||||
%Subscriber{}
|
||||
|> Subscriber.changeset(%{
|
||||
email: "pending@example.com",
|
||||
status: "pending",
|
||||
confirmation_token: hashed_token
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
conn = get(conn, "/newsletter/confirm/#{raw_token}")
|
||||
|
||||
assert conn.status == 200
|
||||
assert conn.resp_body =~ "subscribed"
|
||||
|
||||
# Verify the subscriber is now confirmed
|
||||
sub = Repo.get_by!(Subscriber, email: "pending@example.com")
|
||||
assert sub.status == "confirmed"
|
||||
end
|
||||
|
||||
test "returns error for invalid token", %{conn: conn} do
|
||||
conn = get(conn, "/newsletter/confirm/invalid-token-here")
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.resp_body =~ "invalid"
|
||||
end
|
||||
end
|
||||
end
|
||||
131
test/berrypod_web/live/admin/newsletter_test.exs
Normal file
131
test/berrypod_web/live/admin/newsletter_test.exs
Normal file
@@ -0,0 +1,131 @@
|
||||
defmodule BerrypodWeb.Admin.NewsletterTest do
|
||||
use BerrypodWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
import Berrypod.NewsletterFixtures
|
||||
|
||||
alias Berrypod.Newsletter
|
||||
|
||||
setup %{conn: conn} do
|
||||
user = user_fixture()
|
||||
conn = log_in_user(conn, user)
|
||||
%{conn: conn, user: user}
|
||||
end
|
||||
|
||||
describe "overview tab" do
|
||||
test "shows newsletter toggle", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter")
|
||||
|
||||
assert has_element?(view, "[role=switch]")
|
||||
end
|
||||
|
||||
test "toggles newsletter enabled", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter")
|
||||
|
||||
refute Newsletter.newsletter_enabled?()
|
||||
|
||||
view |> element("[role=switch]") |> render_click()
|
||||
|
||||
assert Newsletter.newsletter_enabled?()
|
||||
end
|
||||
|
||||
test "shows subscriber counts", %{conn: conn} do
|
||||
confirmed_subscriber_fixture(email: "a@example.com")
|
||||
confirmed_subscriber_fixture(email: "b@example.com")
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/newsletter")
|
||||
|
||||
assert html =~ "2"
|
||||
assert html =~ "Confirmed"
|
||||
end
|
||||
end
|
||||
|
||||
describe "subscribers tab" do
|
||||
test "lists subscribers", %{conn: conn} do
|
||||
confirmed_subscriber_fixture(email: "listed@example.com")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=subscribers")
|
||||
|
||||
assert has_element?(view, "#subscribers")
|
||||
assert render(view) =~ "listed@example.com"
|
||||
end
|
||||
|
||||
test "filters by status", %{conn: conn} do
|
||||
confirmed_subscriber_fixture(email: "confirmed@example.com")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=subscribers")
|
||||
|
||||
# Filter to confirmed only
|
||||
view |> element("button", "Confirmed") |> render_click()
|
||||
|
||||
assert render(view) =~ "confirmed@example.com"
|
||||
end
|
||||
|
||||
test "searches subscribers", %{conn: conn} do
|
||||
confirmed_subscriber_fixture(email: "findme@example.com")
|
||||
confirmed_subscriber_fixture(email: "other@example.com")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=subscribers")
|
||||
|
||||
view |> element("form") |> render_change(%{search: "findme"})
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "findme@example.com"
|
||||
refute html =~ "other@example.com"
|
||||
end
|
||||
|
||||
test "deletes subscriber", %{conn: conn} do
|
||||
confirmed_subscriber_fixture(email: "deleteme@example.com")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=subscribers")
|
||||
|
||||
assert render(view) =~ "deleteme@example.com"
|
||||
|
||||
view |> element("button", "Delete") |> render_click()
|
||||
|
||||
refute render(view) =~ "deleteme@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "campaigns tab" do
|
||||
test "lists campaigns", %{conn: conn} do
|
||||
campaign_fixture(subject: "Test campaign")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=campaigns")
|
||||
|
||||
assert render(view) =~ "Test campaign"
|
||||
end
|
||||
|
||||
test "deletes draft campaign", %{conn: conn} do
|
||||
campaign_fixture(subject: "Delete me")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=campaigns")
|
||||
|
||||
assert render(view) =~ "Delete me"
|
||||
|
||||
view |> element("button", "Delete") |> render_click()
|
||||
|
||||
refute render(view) =~ "Delete me"
|
||||
end
|
||||
|
||||
test "links to new campaign form", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/newsletter?tab=campaigns")
|
||||
|
||||
assert has_element?(view, "a[href='/admin/newsletter/campaigns/new']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /admin/newsletter/export" do
|
||||
test "downloads CSV of subscribers", %{conn: conn} do
|
||||
confirmed_subscriber_fixture(email: "export@example.com")
|
||||
|
||||
conn = get(conn, ~p"/admin/newsletter/export")
|
||||
|
||||
assert response_content_type(conn, :csv) =~ "text/csv"
|
||||
assert get_resp_header(conn, "content-disposition") |> hd() =~ "attachment"
|
||||
assert conn.resp_body =~ "export@example.com"
|
||||
assert conn.resp_body =~ "email,status"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user