Simplify email backend and remove --appEnv (#710)

* Improve logging

* Remove AppEnv

* Simplify email backend
This commit is contained in:
Sung 2025-11-01 00:54:27 -07:00 committed by GitHub
commit e72322f847
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 196 additions and 161 deletions

View file

@ -50,8 +50,6 @@ func TestServerStart(t *testing.T) {
cmd := exec.Command(testServerBinary, "start", "--port", port)
cmd.Env = append(os.Environ(),
"DBPath="+tmpDB,
"WebURL=http://localhost:"+port,
"APP_ENV=PRODUCTION",
)
if err := cmd.Start(); err != nil {
@ -140,7 +138,6 @@ func TestServerStartHelp(t *testing.T) {
outputStr := string(output)
assert.Equal(t, strings.Contains(outputStr, "dnote-server start [flags]"), true, "output should contain usage")
assert.Equal(t, strings.Contains(outputStr, "--appEnv"), true, "output should contain appEnv flag")
assert.Equal(t, strings.Contains(outputStr, "--port"), true, "output should contain port flag")
assert.Equal(t, strings.Contains(outputStr, "--webUrl"), true, "output should contain webUrl flag")
assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "output should contain dbPath flag")

View file

@ -35,7 +35,6 @@ import (
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/controllers"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/mailer"
apitest "github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
"gorm.io/gorm"
@ -98,7 +97,6 @@ func setupTestServer(t *testing.T, serverTime time.Time) (*httptest.Server, *gor
a := app.NewTest()
a.Clock = mockClock
a.EmailTemplates = mailer.Templates{}
a.EmailBackend = &apitest.MockEmailbackendImplementation{}
a.DB = db

View file

@ -1,2 +0,0 @@
APP_ENV=DEVELOPMENT
DBPath=../../dev-server.db

View file

@ -29,8 +29,6 @@ var (
ErrEmptyClock = errors.New("No clock was provided")
// ErrEmptyWebURL is an error for missing WebURL content in the app configuration
ErrEmptyWebURL = errors.New("No WebURL was provided")
// ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration
ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided")
// ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration
ErrEmptyEmailBackend = errors.New("No EmailBackend was provided")
// ErrEmptyHTTP500Page is an error for missing HTTP 500 page content
@ -41,11 +39,9 @@ var (
type App struct {
DB *gorm.DB
Clock clock.Clock
EmailTemplates mailer.Templates
EmailBackend mailer.Backend
Files map[string][]byte
HTTP500Page []byte
AppEnv string
WebURL string
DisableRegistration bool
Port string
@ -61,9 +57,6 @@ func (a *App) Validate() error {
if a.Clock == nil {
return ErrEmptyClock
}
if a.EmailTemplates == nil {
return ErrEmptyEmailTemplates
}
if a.EmailBackend == nil {
return ErrEmptyEmailBackend
}

View file

@ -64,21 +64,18 @@ func getNoreplySender(webURL string) (string, error) {
// SendWelcomeEmail sends welcome email
func (a *App) SendWelcomeEmail(email string) error {
body, err := a.EmailTemplates.Execute(mailer.EmailTypeWelcome, mailer.EmailKindText, mailer.WelcomeTmplData{
AccountEmail: email,
WebURL: a.WebURL,
})
if err != nil {
return errors.Wrapf(err, "executing reset verification template for %s", email)
}
from, err := GetSenderEmail(a.WebURL, defaultSender)
if err != nil {
return errors.Wrap(err, "getting the sender email")
}
if err := a.EmailBackend.Queue("Welcome to Dnote!", from, []string{email}, mailer.EmailKindText, body); err != nil {
return errors.Wrapf(err, "queueing email for %s", email)
data := mailer.WelcomeTmplData{
AccountEmail: email,
WebURL: a.WebURL,
}
if err := a.EmailBackend.SendEmail(mailer.EmailTypeWelcome, from, []string{email}, data); err != nil {
return errors.Wrapf(err, "sending welcome email for %s", email)
}
return nil
@ -90,26 +87,23 @@ func (a *App) SendPasswordResetEmail(email, tokenValue string) error {
return ErrEmailRequired
}
body, err := a.EmailTemplates.Execute(mailer.EmailTypeResetPassword, mailer.EmailKindText, mailer.EmailResetPasswordTmplData{
AccountEmail: email,
Token: tokenValue,
WebURL: a.WebURL,
})
if err != nil {
return errors.Wrapf(err, "executing reset password template for %s", email)
}
from, err := GetSenderEmail(a.WebURL, defaultSender)
if err != nil {
return errors.Wrap(err, "getting the sender email")
}
if err := a.EmailBackend.Queue("Reset your password", from, []string{email}, mailer.EmailKindText, body); err != nil {
data := mailer.EmailResetPasswordTmplData{
AccountEmail: email,
Token: tokenValue,
WebURL: a.WebURL,
}
if err := a.EmailBackend.SendEmail(mailer.EmailTypeResetPassword, from, []string{email}, data); err != nil {
if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
return ErrInvalidSMTPConfig
}
return errors.Wrapf(err, "queueing email for %s", email)
return errors.Wrapf(err, "sending password reset email for %s", email)
}
return nil
@ -117,21 +111,18 @@ func (a *App) SendPasswordResetEmail(email, tokenValue string) error {
// SendPasswordResetAlertEmail sends email that notifies users of a password change
func (a *App) SendPasswordResetAlertEmail(email string) error {
body, err := a.EmailTemplates.Execute(mailer.EmailTypeResetPasswordAlert, mailer.EmailKindText, mailer.EmailResetPasswordAlertTmplData{
AccountEmail: email,
WebURL: a.WebURL,
})
if err != nil {
return errors.Wrapf(err, "executing reset password alert template for %s", email)
}
from, err := GetSenderEmail(a.WebURL, defaultSender)
if err != nil {
return errors.Wrap(err, "getting the sender email")
}
if err := a.EmailBackend.Queue("Dnote password changed", from, []string{email}, mailer.EmailKindText, body); err != nil {
return errors.Wrapf(err, "queueing email for %s", email)
data := mailer.EmailResetPasswordAlertTmplData{
AccountEmail: email,
WebURL: a.WebURL,
}
if err := a.EmailBackend.SendEmail(mailer.EmailTypeResetPasswordAlert, from, []string{email}, data); err != nil {
return errors.Wrapf(err, "sending password reset alert email for %s", email)
}
return nil

View file

@ -73,7 +73,7 @@ var (
// ErrInvalidPasswordChangeInput is an error for changing password
ErrInvalidPasswordChangeInput appError = "Both current and new passwords are required to change the password."
ErrInvalidPassword appError = "Invalid currnet password."
ErrInvalidPassword appError = "Invalid current password."
// ErrEmailTooLong is an error for email length exceeding the limit
ErrEmailTooLong appError = "Email is too long."

View file

@ -48,7 +48,7 @@ func (a *App) CreateSession(userID int) (database.Session, error) {
// DeleteUserSessions deletes all existing sessions for the given user. It effectively
// invalidates all existing sessions.
func (a *App) DeleteUserSessions(db *gorm.DB, userID int) error {
if err := db.Debug().Where("user_id = ?", userID).Delete(&database.Session{}).Error; err != nil {
if err := db.Where("user_id = ?", userID).Delete(&database.Session{}).Error; err != nil {
return errors.Wrap(err, "deleting sessions")
}

View file

@ -18,7 +18,6 @@ package app
import (
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/assets"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
)
@ -26,10 +25,8 @@ import (
func NewTest() App {
return App{
Clock: clock.NewMock(),
EmailTemplates: mailer.NewTemplates(),
EmailBackend: &testutils.MockEmailbackendImplementation{},
HTTP500Page: assets.MustGetHTTP500ErrorPage(),
AppEnv: "TEST",
WebURL: "http://127.0.0.0.1",
Port: "3000",
DisableRegistration: false,

View file

@ -37,23 +37,26 @@ func initDB(dbPath string) *gorm.DB {
return db
}
func getEmailBackend() mailer.Backend {
defaultBackend, err := mailer.NewDefaultBackend()
if err != nil {
log.Debug("SMTP not configured, using StdoutBackend for emails")
return mailer.NewStdoutBackend()
}
log.Debug("Email backend configured")
return defaultBackend
}
func initApp(cfg config.Config) app.App {
db := initDB(cfg.DBPath)
emailBackend, err := mailer.NewDefaultBackend(cfg.IsProd())
if err != nil {
emailBackend = &mailer.DefaultBackend{Enabled: false}
} else {
log.Info("Email backend configured")
}
emailBackend := getEmailBackend()
return app.App{
DB: db,
Clock: clock.New(),
EmailTemplates: mailer.NewTemplates(),
EmailBackend: emailBackend,
HTTP500Page: cfg.HTTP500Page,
AppEnv: cfg.AppEnv,
WebURL: cfg.WebURL,
DisableRegistration: cfg.DisableRegistration,
Port: cfg.Port,

View file

@ -32,7 +32,6 @@ import (
func startCmd(args []string) {
fs := setupFlagSet("start", "dnote-server start")
appEnv := fs.String("appEnv", "", "Application environment (env: APP_ENV, default: PRODUCTION)")
port := fs.String("port", "", "Server port (env: PORT, default: 3001)")
webURL := fs.String("webUrl", "", "Full URL to server without trailing slash (env: WebURL, default: http://localhost:3001)")
dbPath := fs.String("dbPath", "", "Path to SQLite database file (env: DBPath, default: $XDG_DATA_HOME/dnote/server.db)")
@ -42,7 +41,6 @@ func startCmd(args []string) {
fs.Parse(args)
cfg, err := config.New(config.Params{
AppEnv: *appEnv,
Port: *port,
WebURL: *webURL,
DBPath: *dbPath,

View file

@ -26,8 +26,6 @@ import (
)
const (
// AppEnvProduction represents an app environment for production.
AppEnvProduction string = "PRODUCTION"
// DefaultDBDir is the default directory name for Dnote data
DefaultDBDir = "dnote"
// DefaultDBFilename is the default database filename
@ -65,7 +63,6 @@ func getOrEnv(value, envKey, defaultVal string) string {
// Config is an application configuration
type Config struct {
AppEnv string
WebURL string
DisableRegistration bool
Port string
@ -77,7 +74,6 @@ type Config struct {
// Params are the configuration parameters for creating a new Config
type Params struct {
AppEnv string
Port string
WebURL string
DBPath string
@ -89,7 +85,6 @@ type Params struct {
// Empty string params will fall back to environment variables and defaults.
func New(p Params) (Config, error) {
c := Config{
AppEnv: getOrEnv(p.AppEnv, "APP_ENV", AppEnvProduction),
Port: getOrEnv(p.Port, "PORT", "3001"),
WebURL: getOrEnv(p.WebURL, "WebURL", "http://localhost:3001"),
DBPath: getOrEnv(p.DBPath, "DBPath", DefaultDBPath),
@ -106,11 +101,6 @@ func New(p Params) (Config, error) {
return c, nil
}
// IsProd checks if the app environment is configured to be production.
func (c Config) IsProd() bool {
return c.AppEnv == AppEnvProduction
}
func validate(c Config) error {
if _, err := url.ParseRequestURI(c.WebURL); err != nil {
return errors.Wrapf(ErrWebURLInvalid, "'%s'", c.WebURL)

View file

@ -20,9 +20,11 @@ import (
"path/filepath"
"time"
"github.com/dnote/dnote/pkg/server/log"
"github.com/pkg/errors"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var (
@ -30,6 +32,22 @@ var (
MigrationTableName = "migrations"
)
// getDBLogLevel converts application log level to GORM log level
func getDBLogLevel(level string) logger.LogLevel {
switch level {
case log.LevelDebug:
return logger.Info
case log.LevelInfo:
return logger.Info
case log.LevelWarn:
return logger.Warn
case log.LevelError:
return logger.Error
default:
return logger.Error
}
}
// InitSchema migrates database schema to reflect the latest model definition
func InitSchema(db *gorm.DB) {
if err := db.AutoMigrate(
@ -51,7 +69,9 @@ func Open(dbPath string) *gorm.DB {
panic(errors.Wrapf(err, "creating database directory at %s", dir))
}
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(getDBLogLevel(log.GetLevel())),
})
if err != nil {
panic(errors.Wrap(err, "opening database conection"))
}
@ -96,16 +116,14 @@ func StartWALCheckpointing(db *gorm.DB, interval time.Duration) {
for range ticker.C {
// TRUNCATE mode removes the WAL file after checkpointing
if err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil {
// Log error but don't panic - this is a background maintenance task
// TODO: Use proper logging once available
_ = err
log.ErrorWrap(err, "WAL checkpoint failed")
}
}
}()
}
// StartPeriodicVacuum runs full VACUUM on a schedule to reclaim space and defragment.
// WARNING: VACUUM acquires an exclusive lock and blocks all database operations briefly.
// VACUUM acquires an exclusive lock and blocks all database operations briefly.
func StartPeriodicVacuum(db *gorm.DB, interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
@ -113,9 +131,7 @@ func StartPeriodicVacuum(db *gorm.DB, interval time.Duration) {
for range ticker.C {
if err := db.Exec("VACUUM").Error; err != nil {
// Log error but don't panic - this is a background maintenance task
// TODO: Use proper logging once available
_ = err
log.ErrorWrap(err, "VACUUM failed")
}
}
}()

View file

@ -70,6 +70,11 @@ func SetLevel(level string) {
currentLevel = level
}
// GetLevel returns the current global log level
func GetLevel() string {
return currentLevel
}
// levelPriority returns a numeric priority for comparison
func levelPriority(level string) int {
switch level {

View file

@ -29,7 +29,7 @@ var ErrSMTPNotConfigured = errors.New("SMTP is not configured")
// Backend is an interface for sending emails.
type Backend interface {
Queue(subject, from string, to []string, contentType, body string) error
SendEmail(templateType, from string, to []string, data interface{}) error
}
// EmailDialer is an interface for sending email messages
@ -44,9 +44,10 @@ type gomailDialer struct {
// DefaultBackend is an implementation of the Backend
// that sends an email without queueing.
// This backend is always enabled and will send emails via SMTP.
type DefaultBackend struct {
Dialer EmailDialer
Enabled bool
Dialer EmailDialer
Templates Templates
}
type dialerParams struct {
@ -82,7 +83,7 @@ func getSMTPParams() (*dialerParams, error) {
}
// NewDefaultBackend creates a default backend
func NewDefaultBackend(enabled bool) (*DefaultBackend, error) {
func NewDefaultBackend() (*DefaultBackend, error) {
p, err := getSMTPParams()
if err != nil {
return nil, err
@ -91,24 +92,24 @@ func NewDefaultBackend(enabled bool) (*DefaultBackend, error) {
d := gomail.NewDialer(p.Host, p.Port, p.Username, p.Password)
return &DefaultBackend{
Dialer: &gomailDialer{Dialer: d},
Enabled: enabled,
Dialer: &gomailDialer{Dialer: d},
Templates: NewTemplates(),
}, nil
}
// Queue is an implementation of Backend.Queue.
func (b *DefaultBackend) Queue(subject, from string, to []string, contentType, body string) error {
// If not enabled, just log the email
if !b.Enabled {
log.WithFields(log.Fields{
"subject": subject,
"to": to,
"from": from,
"body": body,
}).Info("Not sending email because email backend is not configured.")
return nil
// SendEmail is an implementation of Backend.SendEmail.
// It renders the template and sends the email immediately via SMTP.
func (b *DefaultBackend) SendEmail(templateType, from string, to []string, data interface{}) error {
subject, body, err := b.Templates.Execute(templateType, EmailKindText, data)
if err != nil {
return errors.Wrap(err, "executing template")
}
return b.queue(subject, from, to, EmailKindText, body)
}
// queue sends the email immediately via SMTP.
func (b *DefaultBackend) queue(subject, from string, to []string, contentType, body string) error {
m := gomail.NewMessage()
m.SetHeader("From", from)
m.SetHeader("To", to...)
@ -121,3 +122,34 @@ func (b *DefaultBackend) Queue(subject, from string, to []string, contentType, b
return nil
}
// StdoutBackend is an implementation of the Backend
// that prints emails to stdout instead of sending them.
// This is useful for development and testing.
type StdoutBackend struct {
Templates Templates
}
// NewStdoutBackend creates a stdout backend
func NewStdoutBackend() *StdoutBackend {
return &StdoutBackend{
Templates: NewTemplates(),
}
}
// SendEmail is an implementation of Backend.SendEmail.
// It renders the template and logs the email to stdout instead of sending it.
func (b *StdoutBackend) SendEmail(templateType, from string, to []string, data interface{}) error {
subject, body, err := b.Templates.Execute(templateType, EmailKindText, data)
if err != nil {
return errors.Wrap(err, "executing template")
}
log.WithFields(log.Fields{
"subject": subject,
"to": to,
"from": from,
"body": body,
}).Info("Email (not sent, using StdoutBackend)")
return nil
}

View file

@ -31,40 +31,28 @@ func (m *mockDialer) DialAndSend(msgs ...*gomail.Message) error {
return m.err
}
func TestDefaultBackendQueue(t *testing.T) {
t.Run("enabled sends email", func(t *testing.T) {
func TestDefaultBackendSendEmail(t *testing.T) {
t.Run("sends email", func(t *testing.T) {
mock := &mockDialer{}
backend := &DefaultBackend{
Dialer: mock,
Enabled: true,
Dialer: mock,
Templates: NewTemplates(),
}
err := backend.Queue("Test Subject", "alice@example.com", []string{"bob@example.com"}, "text/plain", "Test body")
data := WelcomeTmplData{
AccountEmail: "bob@example.com",
WebURL: "https://example.com",
}
err := backend.SendEmail(EmailTypeWelcome, "alice@example.com", []string{"bob@example.com"}, data)
if err != nil {
t.Fatalf("Queue failed: %v", err)
t.Fatalf("SendEmail failed: %v", err)
}
if len(mock.sentMessages) != 1 {
t.Errorf("expected 1 message sent, got %d", len(mock.sentMessages))
}
})
t.Run("disabled does not send email", func(t *testing.T) {
mock := &mockDialer{}
backend := &DefaultBackend{
Dialer: mock,
Enabled: false,
}
err := backend.Queue("Test Subject", "alice@example.com", []string{"bob@example.com"}, "text/plain", "Test body")
if err != nil {
t.Fatalf("Queue failed: %v", err)
}
if len(mock.sentMessages) != 0 {
t.Errorf("expected 0 messages sent when disabled, got %d", len(mock.sentMessages))
}
})
}
func TestNewDefaultBackend(t *testing.T) {
@ -74,14 +62,11 @@ func TestNewDefaultBackend(t *testing.T) {
t.Setenv("SmtpUsername", "user@example.com")
t.Setenv("SmtpPassword", "secret")
backend, err := NewDefaultBackend(true)
backend, err := NewDefaultBackend()
if err != nil {
t.Fatalf("NewDefaultBackend failed: %v", err)
}
if backend.Enabled != true {
t.Errorf("expected Enabled to be true, got %v", backend.Enabled)
}
if backend.Dialer == nil {
t.Error("expected Dialer to be set")
}
@ -93,7 +78,7 @@ func TestNewDefaultBackend(t *testing.T) {
t.Setenv("SmtpUsername", "")
t.Setenv("SmtpPassword", "")
_, err := NewDefaultBackend(true)
_, err := NewDefaultBackend()
if err == nil {
t.Error("expected error when SMTP not configured")
}
@ -102,3 +87,21 @@ func TestNewDefaultBackend(t *testing.T) {
}
})
}
func TestStdoutBackendSendEmail(t *testing.T) {
t.Run("logs email without sending", func(t *testing.T) {
backend := NewStdoutBackend()
data := WelcomeTmplData{
AccountEmail: "bob@example.com",
WebURL: "https://example.com",
}
err := backend.SendEmail(EmailTypeWelcome, "alice@example.com", []string{"bob@example.com"}, data)
if err != nil {
t.Fatalf("SendEmail failed: %v", err)
}
// StdoutBackend should never return an error, just log
})
}

View file

@ -40,13 +40,19 @@ var (
EmailKindText = "text/plain"
)
// template is the common interface shared between Template from
// tmpl is the common interface shared between Template from
// html/template and text/template
type template interface {
type tmpl interface {
Execute(wr io.Writer, data interface{}) error
}
// Templates holds the parsed email templates
// template wraps a template with its subject line
type template struct {
tmpl tmpl
subject string
}
// Templates holds the parsed email templates with their subjects
type Templates map[string]template
func getTemplateKey(name, kind string) string {
@ -56,16 +62,19 @@ func getTemplateKey(name, kind string) string {
func (tmpl Templates) get(name, kind string) (template, error) {
key := getTemplateKey(name, kind)
t := tmpl[key]
if t == nil {
return nil, errors.Errorf("unsupported template '%s' with type '%s'", name, kind)
if t.tmpl == nil {
return template{}, errors.Errorf("unsupported template '%s' with type '%s'", name, kind)
}
return t, nil
}
func (tmpl Templates) set(name, kind string, t template) {
func (tmpl Templates) set(name, kind string, t tmpl, subject string) {
key := getTemplateKey(name, kind)
tmpl[key] = t
tmpl[key] = template{
tmpl: t,
subject: subject,
}
}
// NewTemplates initializes templates
@ -84,15 +93,15 @@ func NewTemplates() Templates {
}
T := Templates{}
T.set(EmailTypeResetPassword, EmailKindText, passwordResetText)
T.set(EmailTypeResetPasswordAlert, EmailKindText, passwordResetAlertText)
T.set(EmailTypeWelcome, EmailKindText, welcomeText)
T.set(EmailTypeResetPassword, EmailKindText, passwordResetText, "Reset your Dnote password")
T.set(EmailTypeResetPasswordAlert, EmailKindText, passwordResetAlertText, "Your Dnote password was changed")
T.set(EmailTypeWelcome, EmailKindText, welcomeText, "Welcome to Dnote!")
return T
}
// initTextTmpl returns a template instance by parsing the template with the given name
func initTextTmpl(templateName string) (template, error) {
func initTextTmpl(templateName string) (tmpl, error) {
filename := fmt.Sprintf("%s.txt", templateName)
content, err := templates.Files.ReadFile(filename)
@ -108,17 +117,17 @@ func initTextTmpl(templateName string) (template, error) {
return t, nil
}
// Execute executes the template with the given name with the givn data
func (tmpl Templates) Execute(name, kind string, data any) (string, error) {
// Execute executes the template and returns the subject, body, and any error
func (tmpl Templates) Execute(name, kind string, data any) (subject, body string, err error) {
t, err := tmpl.get(name, kind)
if err != nil {
return "", errors.Wrap(err, "getting template")
return "", "", errors.Wrap(err, "getting template")
}
buf := new(bytes.Buffer)
if err := t.Execute(buf, data); err != nil {
return "", errors.Wrap(err, "executing the template")
if err := t.tmpl.Execute(buf, data); err != nil {
return "", "", errors.Wrap(err, "executing the template")
}
return buf.String(), nil
return t.subject, buf.String(), nil
}

View file

@ -65,11 +65,14 @@ func TestResetPasswordEmail(t *testing.T) {
Token: tc.token,
WebURL: tc.webURL,
}
body, err := tmpl.Execute(EmailTypeResetPassword, EmailKindText, dat)
subject, body, err := tmpl.Execute(EmailTypeResetPassword, EmailKindText, dat)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
if subject != "Reset your Dnote password" {
t.Errorf("expected subject 'Reset your Dnote password', got '%s'", subject)
}
if ok := strings.Contains(body, tc.webURL); !ok {
t.Errorf("email body did not contain %s", tc.webURL)
}
@ -103,11 +106,14 @@ func TestWelcomeEmail(t *testing.T) {
AccountEmail: tc.accountEmail,
WebURL: tc.webURL,
}
body, err := tmpl.Execute(EmailTypeWelcome, EmailKindText, dat)
subject, body, err := tmpl.Execute(EmailTypeWelcome, EmailKindText, dat)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
if subject != "Welcome to Dnote!" {
t.Errorf("expected subject 'Welcome to Dnote!', got '%s'", subject)
}
if ok := strings.Contains(body, tc.webURL); !ok {
t.Errorf("email body did not contain %s", tc.webURL)
}
@ -141,11 +147,14 @@ func TestResetPasswordAlertEmail(t *testing.T) {
AccountEmail: tc.accountEmail,
WebURL: tc.webURL,
}
body, err := tmpl.Execute(EmailTypeResetPasswordAlert, EmailKindText, dat)
subject, body, err := tmpl.Execute(EmailTypeResetPasswordAlert, EmailKindText, dat)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
if subject != "Your Dnote password was changed" {
t.Errorf("expected subject 'Your Dnote password was changed', got '%s'", subject)
}
if ok := strings.Contains(body, tc.webURL); !ok {
t.Errorf("email body did not contain %s", tc.webURL)
}

View file

@ -17,7 +17,6 @@ package middleware
import (
"net/http"
"os"
"strings"
"sync"
"time"
@ -143,7 +142,7 @@ func (rl *RateLimiter) Limit(next http.Handler) http.HandlerFunc {
func ApplyLimit(h http.HandlerFunc, rateLimit bool) http.Handler {
ret := h
if rateLimit && os.Getenv("APP_ENV") != "TEST" {
if rateLimit {
ret = defaultLimiter.Limit(ret)
}

View file

@ -210,10 +210,10 @@ func MustRespondJSON(t *testing.T, w http.ResponseWriter, i interface{}, message
// MockEmail is a mock email data
type MockEmail struct {
Subject string
From string
To []string
Body string
TemplateType string
From string
To []string
Data interface{}
}
// MockEmailbackendImplementation is an email backend that simply discards the emails
@ -230,16 +230,16 @@ func (b *MockEmailbackendImplementation) Clear() {
b.Emails = []MockEmail{}
}
// Queue is an implementation of Backend.Queue.
func (b *MockEmailbackendImplementation) Queue(subject, from string, to []string, contentType, body string) error {
// SendEmail is an implementation of Backend.SendEmail.
func (b *MockEmailbackendImplementation) SendEmail(templateType, from string, to []string, data interface{}) error {
b.mu.Lock()
defer b.mu.Unlock()
b.Emails = append(b.Emails, MockEmail{
Subject: subject,
From: from,
To: to,
Body: body,
TemplateType: templateType,
From: from,
To: to,
Data: data,
})
return nil

View file

@ -7,11 +7,8 @@ dir=$(dirname "${BASH_SOURCE[0]}")
basePath="$dir/../.."
serverPath="$basePath/pkg/server"
# load env
set -a
dotenvPath="$serverPath/.env.dev"
source "$dotenvPath"
set +a
# Set env
DBPath=../../dev-server.db
# copy assets
mkdir -p "$basePath/pkg/server/static"