Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
189 lines
6.2 KiB
Go
189 lines
6.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNotificationEmailPreviewEscapesHTMLAndSanitizesSubject(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := NewNotificationEmailService(newNotificationEmailMemorySettingRepo(), nil)
|
|
|
|
preview, err := svc.PreviewTemplate(ctx, NotificationEmailPreviewInput{
|
|
Event: NotificationEmailEventBalanceLow,
|
|
Locale: "en-US,en;q=0.9",
|
|
Subject: "Low balance for {{recipient_name}}\r\nInjected",
|
|
HTML: `<p>{{recipient_name}}</p><a href="{{recharge_url}}">Recharge</a>`,
|
|
Variables: map[string]string{
|
|
"recipient_name": `<script>alert("x")</script>`,
|
|
"recharge_url": `javascript:alert(1)`,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotContains(t, preview.Subject, "\r")
|
|
require.NotContains(t, preview.Subject, "\n")
|
|
require.Contains(t, preview.Subject, `Low balance for <script>alert("x")</script>Injected`)
|
|
require.Contains(t, preview.HTML, `<script>alert("x")</script>`)
|
|
require.NotContains(t, preview.HTML, `javascript:alert(1)`)
|
|
require.Contains(t, preview.HTML, `href=""`)
|
|
}
|
|
|
|
func TestNotificationEmailTemplateOverrideAndRestore(t *testing.T) {
|
|
ctx := context.Background()
|
|
repo := newNotificationEmailMemorySettingRepo()
|
|
svc := NewNotificationEmailService(repo, nil)
|
|
|
|
official, err := svc.GetTemplate(ctx, NotificationEmailEventBalanceRechargeSuccess, "en")
|
|
require.NoError(t, err)
|
|
require.False(t, official.IsCustom)
|
|
|
|
updated, err := svc.UpdateTemplate(
|
|
ctx,
|
|
NotificationEmailEventBalanceRechargeSuccess,
|
|
"zh-Hans",
|
|
"充值完成:{{recharge_amount}}",
|
|
"<p>{{recipient_name}} 已充值 {{recharge_amount}}</p>",
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, updated.IsCustom)
|
|
require.Equal(t, "zh", updated.Locale)
|
|
require.Equal(t, "充值完成:{{recharge_amount}}", updated.Subject)
|
|
require.NotNil(t, updated.UpdatedAt)
|
|
|
|
restored, err := svc.RestoreOfficialTemplate(ctx, NotificationEmailEventBalanceRechargeSuccess, "zh")
|
|
require.NoError(t, err)
|
|
require.False(t, restored.IsCustom)
|
|
require.NotEqual(t, updated.Subject, restored.Subject)
|
|
_, err = repo.GetValue(ctx, notificationEmailTemplateKey(NotificationEmailEventBalanceRechargeSuccess, "zh"))
|
|
require.ErrorIs(t, err, ErrSettingNotFound)
|
|
}
|
|
|
|
func TestNotificationEmailTemplateRejectsUnsupportedPlaceholder(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := NewNotificationEmailService(newNotificationEmailMemorySettingRepo(), nil)
|
|
|
|
_, err := svc.UpdateTemplate(
|
|
ctx,
|
|
NotificationEmailEventSubscriptionPurchaseSuccess,
|
|
"en",
|
|
"Purchased {{not_allowed}}",
|
|
"<p>{{subscription_group}}</p>",
|
|
)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "unsupported placeholder")
|
|
}
|
|
|
|
func TestNotificationEmailUnsubscribeOnlyAllowsOptionalEvents(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := NewNotificationEmailService(newNotificationEmailMemorySettingRepo(), nil)
|
|
|
|
token, err := svc.createUnsubscribeToken(ctx, "User@Example.com", NotificationEmailEventBalanceLow)
|
|
require.NoError(t, err)
|
|
result, err := svc.Unsubscribe(ctx, token)
|
|
require.NoError(t, err)
|
|
require.True(t, result.Done)
|
|
require.Equal(t, NotificationEmailEventBalanceLow, result.Event)
|
|
unsubscribed, err := svc.IsUnsubscribed(ctx, "user@example.com", NotificationEmailEventBalanceLow)
|
|
require.NoError(t, err)
|
|
require.True(t, unsubscribed)
|
|
|
|
transactionalToken, err := svc.createUnsubscribeToken(ctx, "user@example.com", NotificationEmailEventBalanceRechargeSuccess)
|
|
require.NoError(t, err)
|
|
_, err = svc.Unsubscribe(ctx, transactionalToken)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "transactional")
|
|
}
|
|
|
|
func TestNotificationEmailLocaleMemoryNormalizesAcceptLanguage(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := NewNotificationEmailService(newNotificationEmailMemorySettingRepo(), nil)
|
|
|
|
svc.RememberRecipientLocale(ctx, 42, "User@Example.com", "zh-CN,zh;q=0.9,en;q=0.8")
|
|
require.Equal(t, "zh", svc.ResolveRecipientLocale(ctx, 42, "user@example.com"))
|
|
require.Equal(t, "zh", svc.ResolveRecipientLocale(ctx, 0, "user@example.com"))
|
|
}
|
|
|
|
type notificationEmailMemorySettingRepo struct {
|
|
mu sync.RWMutex
|
|
values map[string]string
|
|
}
|
|
|
|
func newNotificationEmailMemorySettingRepo() *notificationEmailMemorySettingRepo {
|
|
return ¬ificationEmailMemorySettingRepo{values: make(map[string]string)}
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) Get(_ context.Context, key string) (*Setting, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
value, ok := r.values[key]
|
|
if !ok {
|
|
return nil, ErrSettingNotFound
|
|
}
|
|
return &Setting{Key: key, Value: value}, nil
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) GetValue(ctx context.Context, key string) (string, error) {
|
|
setting, err := r.Get(ctx, key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return setting.Value, nil
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) Set(_ context.Context, key, value string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.values[key] = value
|
|
return nil
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make(map[string]string, len(keys))
|
|
for _, key := range keys {
|
|
if value, ok := r.values[key]; ok {
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) SetMultiple(_ context.Context, settings map[string]string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for key, value := range settings {
|
|
r.values[key] = value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) GetAll(_ context.Context) (map[string]string, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make(map[string]string, len(r.values))
|
|
for key, value := range r.values {
|
|
out[key] = value
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *notificationEmailMemorySettingRepo) Delete(_ context.Context, key string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, ok := r.values[key]; !ok {
|
|
return ErrSettingNotFound
|
|
}
|
|
delete(r.values, key)
|
|
return nil
|
|
}
|
|
|
|
func TestNotificationEmailMemorySettingRepoSatisfiesInterface(t *testing.T) {
|
|
var _ SettingRepository = (*notificationEmailMemorySettingRepo)(nil)
|
|
require.False(t, strings.Contains(notificationEmailPreferenceKey(NotificationEmailEventBalanceLow, "User@Example.com"), "User@Example.com"))
|
|
}
|