From 9200fcbbd24347b80e182c5aef3730717936f1e7 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sun, 3 Sep 2023 23:25:32 +0200 Subject: [PATCH] added tests for email templating --- .../cron/emailTemplates/feedbackBody.gohtml | 31 ++-- .../cron/emailTemplates/feedbackBody.txt.tmpl | 22 +-- server/backend/cron/feedbackEmail.go | 114 ++++++++----- server/backend/cron/feedbackEmail_test.go | 155 ++++++++++++++++++ 4 files changed, 256 insertions(+), 66 deletions(-) create mode 100644 server/backend/cron/feedbackEmail_test.go diff --git a/server/backend/cron/emailTemplates/feedbackBody.gohtml b/server/backend/cron/emailTemplates/feedbackBody.gohtml index c4a2fc1d..411eb53e 100644 --- a/server/backend/cron/emailTemplates/feedbackBody.gohtml +++ b/server/backend/cron/emailTemplates/feedbackBody.gohtml @@ -1,17 +1,17 @@

Feedback via TumCampusApp:

- -{{ if .Feedback.Valid }} +{{ if .Feedback.Valid -}}
- {{ .Feedback.String }} + {{- .Feedback.String -}}
-{{ end }} - +{{- else -}} +no feedback provided +{{- end }} - {{ if .Latitude.Valid }} + {{- if .Latitude.Valid }} - {{ end }} + {{- end }} - +
Inforation type Details
Nutzer-Standort @@ -20,24 +20,23 @@
OS-Version {{ if .OsVersion.Valid }}{{.OsVersion.String }}{{else}}unknown{{end}}
App-Version{{ if .AppVersion.Valid }}{{.APPVersion.String }}{{else}}unknown{{end}}{{ if .AppVersion.Valid }}{{.AppVersion.String }}{{else}}unknown{{end}}
- -{{ if .ImageCount }} +{{- if .ImageCount }}

Fotos:


    - {{ range $val := Iterate .ImageCount }} -
  1. - Foto {{ $val }} -
  2. - {{ end }} +{{- range $val := iterate .ImageCount }} +
  3. + Foto {{ $val }} +
  4. +{{- end }}
-{{ end }} +{{- end -}} diff --git a/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl b/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl index cb3f5820..e4c6c7d0 100644 --- a/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl +++ b/server/backend/cron/emailTemplates/feedbackBody.txt.tmpl @@ -1,20 +1,22 @@ Feedback via TumCampusApp: -{{- if .Feedback.Valid }} -{{ .Feedback.String }} +{{ if .Feedback.Valid }} +{{- .Feedback.String -}} +{{ else -}} +no feedback provided {{- end }} +Metadata: {{- if .Latitude.Valid }} -- Nutzer-Standort: - latitude: {{ .Latitude.Float64 }}, longitude: {{ .Longitude.Float64 }} - https://www.google.com/maps/search/?api=1&query={{ .Latitude.Float64 }},{{ .Longitude.Float64 }} +- Nutzer-Standort: {{ .Latitude.Float64 }},{{ .Longitude.Float64 }} (latitude,longitude) + https://www.google.com/maps/search/?api=1&query={{ .Latitude.Float64 }},{{ .Longitude.Float64 }} {{- end }} - OS-Version: {{ if .OsVersion.Valid }}{{.OsVersion.String }}{{else}}unknown{{end}} -- App-Version: {{ if .AppVersion.Valid }}{{.APPVersion.String }}{{else}}unknown{{end}} +- App-Version: {{ if .AppVersion.Valid }}{{.AppVersion.String }}{{else}}unknown{{end}} +{{- if .ImageCount }} -{{ if .ImageCount }} -Fotos: - {{- range $val := Iterate .ImageCount }} -- Foto {{ $val }}: https://app.tum.de/File/feedback/{{ $.Id }}/{{ $val }}.png +Photos: + {{- range $val := iterate .ImageCount }} +- Photo {{ $val }}: https://app.tum.de/File/feedback/{{ $.Id }}/{{ $val }}.png {{- end -}} {{- end -}} diff --git a/server/backend/cron/feedbackEmail.go b/server/backend/cron/feedbackEmail.go index f52f539d..fe74478f 100644 --- a/server/backend/cron/feedbackEmail.go +++ b/server/backend/cron/feedbackEmail.go @@ -14,14 +14,14 @@ import ( ) import _ "embed" -// Iterate is necessary, as go otherwise cannot count up in a for loop inside templates -func Iterate(count int32) []int32 { - var Items []int32 +// iterate is necessary, as go otherwise cannot count up in a for loop inside templates +func iterate(count int32) []int32 { + var items []int32 var i int32 for i = 0; i < count; i++ { - Items = append(Items, i) + items = append(items, i) } - return Items + return items } //go:embed emailTemplates/feedbackBody.gohtml @@ -30,21 +30,75 @@ var htmlFeedbackBody string //go:embed emailTemplates/feedbackBody.txt.tmpl var txtFeedbackBody string +func parseTemplates() (*htmlTemplate.Template, *textTemplate.Template, error) { + funcMap := textTemplate.FuncMap{"iterate": iterate} + parsedHtmlBody, err := htmlTemplate.New("htmlFeedbackBody").Funcs(funcMap).Parse(htmlFeedbackBody) + if err != nil { + return nil, nil, err + } + parsedTxtBody, err := textTemplate.New("txtFeedbackBody").Funcs(funcMap).Parse(txtFeedbackBody) + if err != nil { + return nil, nil, err + } + return parsedHtmlBody, parsedTxtBody, nil + +} + +type MailHeaders struct { + From string + To string + ReplyTo string //optional + Timestamp time.Time + Subject string +} + +func messageWithHeaders(feedback *model.Feedback) *gomail.Message { + m := gomail.NewMessage() + // From + m.SetAddressHeader("From", os.Getenv("SMTP_USERNAME"), "TUM Campus App") + // To + if feedback.Receiver.Valid { + m.SetHeader("To", feedback.Receiver.String) + } else { + m.SetHeader("To", "app@tum.de") + } + // ReplyTo + if feedback.ReplyTo.Valid { + m.SetHeader("Reply-To", feedback.ReplyTo.String) + } + // Timestamp + if feedback.Timestamp.Valid { + m.SetDateHeader("Date", feedback.Timestamp.Time) + } else { + m.SetDateHeader("Date", time.Now()) + } + // Subject + m.SetHeader("Subject", "Feedback via Tum Campus App") + return m +} + +func generateTemplatedMail(parsedHtmlBody *htmlTemplate.Template, parsedTxtBody *textTemplate.Template, feedback *model.Feedback) (string, string, error) { + var htmlBodyBuffer bytes.Buffer + if err := parsedHtmlBody.Execute(&htmlBodyBuffer, feedback); err != nil { + return "", "", err + } + var txtBodyBuffer bytes.Buffer + if err := parsedTxtBody.Execute(&txtBodyBuffer, feedback); err != nil { + return "", "", err + } + return htmlBodyBuffer.String(), txtBodyBuffer.String(), nil +} + func (c *CronService) feedbackEmailCron() error { + var results []model.Feedback if err := c.db.Find(&results, "processed = false").Scan(&results).Error; err != nil { log.WithError(err).Fatal("could not get unprocessed feedback") return err } - funcMap := textTemplate.FuncMap{"Iterate": Iterate} - parsedHtmlBody, err := htmlTemplate.New("htmlFeedbackBody").Funcs(funcMap).Parse(htmlFeedbackBody) - if err != nil { - log.WithError(err).Fatal("htmlFeedbackBody is not a valid template") - return err - } - parsedTxtBody, err := textTemplate.New("txtFeedbackBody").Funcs(funcMap).Parse(txtFeedbackBody) + parsedHtmlBody, parsedTxtBody, err := parseTemplates() if err != nil { - log.WithError(err).Fatal("txtFeedbackBody is not a valid template") + log.WithError(err).Fatal("could not parse email templates") return err } @@ -56,43 +110,23 @@ func (c *CronService) feedbackEmailCron() error { d := gomail.NewDialer(os.Getenv("SMTP_URL"), smtpPort, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD")) d.TLSConfig = &tls.Config{InsecureSkipVerify: true} for i, feedback := range results { - m := gomail.NewMessage() - // set message-headers - m.SetAddressHeader("From", os.Getenv("SMTP_USERNAME"), "TUM Campus App") - if feedback.Receiver.Valid { - m.SetHeader("To", feedback.Receiver.String) - } else { - m.SetHeader("To", "app@tum.de") - } - if feedback.ReplyTo.Valid { - m.SetHeader("Reply-To", feedback.ReplyTo.String) - } - if feedback.Timestamp.Valid { - m.SetDateHeader("Date", feedback.Timestamp.Time) - } else { - m.SetDateHeader("Date", time.Time{}) - } - m.SetHeader("Subject", "Feedback via Tum Campus App") + m := messageWithHeaders(&feedback) // attach a body - var txtBodyBuffer bytes.Buffer - if err := parsedTxtBody.Execute(&txtBodyBuffer, feedback); err != nil { - return err - } - m.SetBody("text/plain", txtBodyBuffer.String()) - - var htmlBodyBuffer bytes.Buffer - if err := parsedHtmlBody.Execute(&htmlBodyBuffer, feedback); err != nil { + htmlBodyBuffer, txtBodyBuffer, err := generateTemplatedMail(parsedHtmlBody, parsedTxtBody, &feedback) + if err != nil { + log.WithError(err).Error("Could not template mail body") return err } - m.AddAlternative("text/html", htmlBodyBuffer.String()) + m.SetBody("text/plain", txtBodyBuffer) + m.AddAlternative("text/html", htmlBodyBuffer) // send mail if err := d.DialAndSend(m); err != nil { log.WithError(err).Error("could not send mail") continue } - log.Trace("sending feedback %d to %s successfull", i, feedback.Receiver) + log.Tracef("sending feedback %d to %v successfull", i, feedback.Receiver) // prevent the message being send the next time around if err := c.db.Find(model.Feedback{}, "id = ?", feedback.Id).Update("processed", "true").Error; err != nil { diff --git a/server/backend/cron/feedbackEmail_test.go b/server/backend/cron/feedbackEmail_test.go new file mode 100644 index 00000000..aa3f62c8 --- /dev/null +++ b/server/backend/cron/feedbackEmail_test.go @@ -0,0 +1,155 @@ +package cron + +import ( + "database/sql" + "github.com/TUM-Dev/Campus-Backend/server/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" + "time" +) + +func TestIterate(t *testing.T) { + assert.Equal(t, []int32(nil), iterate(0)) + assert.Equal(t, []int32{0}, iterate(1)) + assert.Equal(t, []int32{0, 1}, iterate(2)) + assert.Equal(t, []int32{0, 1, 2}, iterate(3)) + assert.Equal(t, []int32{0, 1, 2, 3}, iterate(4)) + assert.Equal(t, 42, len(iterate(42))) +} + +func fullFeedback() *model.Feedback { + return &model.Feedback{ + EmailId: sql.NullString{String: "magic-id", Valid: true}, + Receiver: sql.NullString{String: "tca", Valid: true}, + ReplyTo: sql.NullString{String: "test@example.de", Valid: true}, + Feedback: sql.NullString{String: "This is a Test", Valid: true}, + ImageCount: 1, + Latitude: sql.NullFloat64{Float64: 0, Valid: true}, + Longitude: sql.NullFloat64{Float64: 0, Valid: true}, + AppVersion: sql.NullString{String: "TCA 10.2", Valid: true}, + OsVersion: sql.NullString{String: "Android 10.0", Valid: true}, + Timestamp: sql.NullTime{Time: time.Time{}, Valid: true}, + } +} + +func emptyFeedback() *model.Feedback { + return &model.Feedback{ + EmailId: sql.NullString{Valid: false}, + Receiver: sql.NullString{Valid: false}, + ReplyTo: sql.NullString{Valid: false}, + Feedback: sql.NullString{Valid: false}, + ImageCount: 0, + Latitude: sql.NullFloat64{Valid: false}, + Longitude: sql.NullFloat64{Valid: false}, + AppVersion: sql.NullString{Valid: false}, + OsVersion: sql.NullString{Valid: false}, + Timestamp: sql.NullTime{Valid: false}, + } +} + +func TestHeaderInstantiationWithFullFeedback(t *testing.T) { + require.NoError(t, os.Setenv("SMTP_USERNAME", "outgoing@example.de")) + fb := fullFeedback() + m := messageWithHeaders(fb) + assert.Equal(t, []string{`"TUM Campus App" `}, m.GetHeader("From")) + assert.Equal(t, []string{fb.Receiver.String}, m.GetHeader("To")) + assert.Equal(t, []string{"test@example.de"}, m.GetHeader("Reply-To")) + assert.Equal(t, []string{fb.Timestamp.Time.Format(time.RFC1123Z)}, m.GetHeader("Date")) + assert.Equal(t, []string{"Feedback via Tum Campus App"}, m.GetHeader("Subject")) +} + +func TestHeaderInstantiationWithEmptyFeedback(t *testing.T) { + require.NoError(t, os.Setenv("SMTP_USERNAME", "outgoing@example.de")) + m := messageWithHeaders(emptyFeedback()) + assert.Equal(t, []string{`"TUM Campus App" `}, m.GetHeader("From")) + assert.Equal(t, []string{"app@tum.de"}, m.GetHeader("To")) + assert.Equal(t, []string(nil), m.GetHeader("Reply-To")) + // Date is set to now in messageWithHeaders => checking that this is actually now is a bit tricker + dates := m.GetHeader("Date") + assert.Equal(t, 1, len(dates)) + date, err := time.Parse(time.RFC1123Z, dates[0]) + require.NoError(t, err) + assert.WithinDuration(t, time.Now(), date, time.Minute) + assert.Equal(t, []string{"Feedback via Tum Campus App"}, m.GetHeader("Subject")) +} + +func TestTemplatingResultsWithFullFeedback(t *testing.T) { + html, txt, err := parseTemplates() + require.NoError(t, err) + htmlBody, txtBody, err := generateTemplatedMail(html, txt, fullFeedback()) + require.NoError(t, err) + assert.Equal(t, `

Feedback via TumCampusApp:

+
This is a Test
+ + + + + + + + + + + + + + + + + +
Inforation typeDetails
Nutzer-Standort + + latitude: 0, longitude: 0 + +
OS-VersionAndroid 10.0
App-VersionTCA 10.2
+

Fotos:


+
    +
  1. + Foto 0 +
  2. +
`, htmlBody) + assert.Equal(t, `Feedback via TumCampusApp: + +This is a Test + +Metadata: +- Nutzer-Standort: 0,0 (latitude,longitude) + https://www.google.com/maps/search/?api=1&query=0,0 +- OS-Version: Android 10.0 +- App-Version: TCA 10.2 + +Photos: +- Photo 0: https://app.tum.de/File/feedback/0/0.png`, txtBody) +} + +func TestTemplatingResultsWithEmptyFeedback(t *testing.T) { + html, txt, err := parseTemplates() + require.NoError(t, err) + htmlBody, txtBody, err := generateTemplatedMail(html, txt, emptyFeedback()) + require.NoError(t, err) + assert.Equal(t, `

Feedback via TumCampusApp:

+no feedback provided + + + + + + + + + + + + + +
Inforation typeDetails
OS-Versionunknown
App-Versionunknown
`, htmlBody) + assert.Equal(t, `Feedback via TumCampusApp: + +no feedback provided + +Metadata: +- OS-Version: unknown +- App-Version: unknown`, txtBody) +}