Add email subscriptions (#38163)

This commit is contained in:
Eugen Rochko
2026-03-25 17:25:45 +01:00
committed by GitHub
parent b46d003e20
commit bcf0718a9a
108 changed files with 839 additions and 516 deletions

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
Fabricator(:email_subscription) do
account
email { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } }
locale 'en'
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe EmailSubscriptionMailer do
describe '.confirmation' do
let(:email_subscription) { Fabricate(:email_subscription) }
let(:mail) { described_class.with(subscription: email_subscription).confirmation }
it 'renders the email' do
expect { mail.deliver }
.to send_email(
to: email_subscription.email,
from: 'notifications@localhost',
subject: I18n.t('email_subscription_mailer.confirmation.subject')
)
end
end
describe '.notification' do
let(:email_subscription) { Fabricate(:email_subscription, confirmed_at: Time.now.utc) }
let(:statuses) { Fabricate.times(num_of_statuses, :status) }
let(:mail) { described_class.with(subscription: email_subscription).notification(statuses) }
context 'with a single status' do
let(:num_of_statuses) { 1 }
it 'renders the email' do
expect { mail.deliver }
.to send_email(
to: email_subscription.email,
from: 'notifications@localhost',
subject: I18n.t('email_subscription_mailer.notification.subject', count: statuses.size, name: email_subscription.account.display_name, excerpt: statuses.first.text.truncate(17))
)
end
end
context 'with multiple statuses' do
let(:num_of_statuses) { 2 }
it 'renders the email' do
expect { mail.deliver }
.to send_email(
to: email_subscription.email,
from: 'notifications@localhost',
subject: I18n.t('email_subscription_mailer.notification.subject', count: statuses.size, name: email_subscription.account.display_name, excerpt: ActionController::Base.helpers.truncate(statuses.first.text, length: 17))
)
end
end
end
end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
# Preview all emails at http://localhost:3000/rails/mailers/admin_mailer
class EmailSubscriptionMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/email_subscription_mailer/confirmation
def confirmation
EmailSubscriptionMailer.with(subscription: EmailSubscription.last!).confirmation
end
# Preview this email at http://localhost:3000/rails/mailers/email_subscription_mailer/notification
def notification
EmailSubscriptionMailer.with(subscription: EmailSubscription.last!).notification(Status.where(visibility: :public).without_replies.without_reblogs.limit(5))
end
end

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe EmailSubscription do
describe '#confirmed?' do
it 'returns true when confirmed' do
subject.confirmed_at = Time.now.utc
expect(subject.confirmed?).to be true
end
it 'returns false when not confirmed' do
subject.confirmed_at = nil
expect(subject.confirmed?).to be false
end
end
describe '#confirm!' do
subject { Fabricate(:email_subscription) }
it 'records confirmation time' do
subject.confirm!
expect(subject.confirmed_at).to_not be_nil
end
end
describe 'Callbacks' do
subject { Fabricate(:email_subscription) }
it 'generates token and delivers confirmation email', :inline_jobs do
emails = capture_emails { subject }
expect(subject.confirmed_at).to be_nil
expect(subject.confirmation_token).to_not be_nil
expect(emails.size).to eq(1)
expect(emails.first)
.to have_attributes(
to: contain_exactly(subject.email),
subject: eq(I18n.t('email_subscription_mailer.confirmation.subject', name: subject.account.username, domain: Rails.configuration.x.local_domain))
)
end
end
end

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Accounts Email Subscriptions API', feature: :email_subscriptions do
let(:account) { Fabricate(:user).account }
describe 'POST /api/v1/accounts/:id/email_subscriptions' do
context 'when the account has the permission' do
let(:role) { Fabricate(:user_role, permissions: UserRole::FLAGS[:manage_email_subscriptions]) }
before do
account.user.update!(role: role)
end
context 'when user has enabled the setting' do
before do
account.user.settings['email_subscriptions'] = true
account.user.save!
end
it 'returns http success' do
post "/api/v1/accounts/#{account.id}/email_subscriptions", params: { email: 'test@example.com' }
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when user has not enabled the setting' do
it 'returns http not found' do
post "/api/v1/accounts/#{account.id}/email_subscriptions", params: { email: 'test@example.com' }
expect(response).to have_http_status(404)
end
end
end
context 'when the account does not have the permission' do
it 'returns http not found' do
post "/api/v1/accounts/#{account.id}/email_subscriptions", params: { email: 'test@example.com' }
expect(response).to have_http_status(404)
end
end
end
end

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Email Subscriptions Confirmation' do
describe 'GET /email_subscriptions/confirmation' do
context 'when email subscription is unconfirmed' do
let!(:email_subscription) { Fabricate(:email_subscription, confirmed_at: nil) }
it 'renders success page and updates subscription as confirmed' do
get email_subscriptions_confirmation_path(confirmation_token: email_subscription.confirmation_token)
expect(response)
.to have_http_status(200)
expect(email_subscription.reload.confirmed?)
.to be true
end
end
context 'when email subscription is already confirmed' do
let!(:email_subscription) { Fabricate(:email_subscription, confirmed_at: Time.now.utc) }
it 'renders success page' do
get email_subscriptions_confirmation_path(confirmation_token: email_subscription.confirmation_token)
expect(response)
.to have_http_status(200)
expect(email_subscription.reload.confirmed?)
.to be true
end
end
end
end

View File

@@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe 'MailSubscriptionsController' do
RSpec.describe 'UnsubscriptionsController' do
let(:user) { Fabricate(:user) }
let(:token) { user.to_sgid(for: 'unsubscribe').to_s }
let(:type) { 'follow' }
@@ -39,9 +39,8 @@ RSpec.describe 'MailSubscriptionsController' do
expect(response).to have_http_status(200)
expect(response.body).to include(
I18n.t('mail_subscriptions.unsubscribe.action')
I18n.t('unsubscriptions.show.action')
)
expect(response.body).to include(user.email)
end
end
@@ -60,9 +59,8 @@ RSpec.describe 'MailSubscriptionsController' do
expect(response).to have_http_status(200)
expect(response.body).to include(
I18n.t('mail_subscriptions.unsubscribe.complete')
I18n.t('unsubscriptions.create.title')
)
expect(response.body).to include(user.email)
end
it 'updates notification settings' do

View File

@@ -3,35 +3,31 @@
require 'rails_helper'
RSpec.describe BootstrapTimelineService do
subject { described_class.new }
subject { described_class.new.call(new_user.account) }
let(:invite) { nil }
let(:new_user) { Fabricate(:user, invite_code: invite&.code) }
context 'when the new user has registered from an invite' do
let(:service) { instance_double(FollowService) }
let(:autofollow) { false }
let(:inviter) { Fabricate(:user, confirmed_at: 2.days.ago) }
let(:invite) { Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now, autofollow: autofollow) }
let(:new_user) { Fabricate(:user, invite_code: invite.code) }
before do
allow(FollowService).to receive(:new).and_return(service)
allow(service).to receive(:call)
end
context 'when the invite has auto-follow enabled' do
let(:autofollow) { true }
it 'calls FollowService to follow the inviter' do
subject.call(new_user.account)
expect(service).to have_received(:call).with(new_user.account, inviter.account)
it 'follows the inviter' do
subject
expect(new_user.account.following?(inviter.account)).to be true
end
end
context 'when the invite does not have auto-follow enable' do
let(:autofollow) { false }
it 'calls FollowService to follow the inviter' do
subject.call(new_user.account)
expect(service).to_not have_received(:call)
it 'does not follow the inviter' do
subject
expect(new_user.account.following?(inviter.account)).to be false
end
end
end