-
|
-
{{.Content}}
", + Plain: "Title: {{.Title}}\nContent: {{.Content}}", +} + +type testData struct { + Title string + Content string +} + +func TestCompose(t *testing.T) { + // valid data + data := testData{ + Title: "Test Title", + Content: "Test Content", + } + email, err := testTemplate.Compose(notification.NotificationParams{ + To: testReceiver, + Subject: testSubject, + }, data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + if email.Params.To != testReceiver { + t.Fatalf("got %v, want %v", email.Params.To, testReceiver) + } + if email.Params.Subject != testSubject { + t.Fatalf("got %v, want %v", email.Params.Subject, testSubject) + } + expectedBody := "Test Content
" + expectedPlain := "Title: Test Title\nContent: Test Content" + if string(email.Body) != expectedBody { + t.Fatalf("got %v, want %v", string(email.Body), expectedBody) + } + if string(email.PlainBody) != expectedPlain { + t.Fatalf("got %v, want %v", string(email.PlainBody), expectedPlain) + } + // no subject + if _, err := testTemplate.Compose(notification.NotificationParams{ + To: testReceiver, + Subject: "", + }, data); err == nil { + t.Fatalf("expected error, got nil") + } + // no to address + if _, err := testTemplate.Compose(notification.NotificationParams{ + To: "", + Subject: testSubject, + }, data); err == nil { + t.Fatalf("expected error, got nil") + } + // bad to address + if _, err := testTemplate.Compose(notification.NotificationParams{ + To: "bad email", + Subject: testSubject, + }, data); err == nil { + t.Fatalf("expected error, got nil") + } + // invalid template + emptyTemplate := &EmailTemplate{} + validParams := notification.NotificationParams{ + To: testReceiver, + Subject: testSubject, + } + if _, err := emptyTemplate.Compose(validParams, data); err == nil { + t.Fatalf("expected error, got nil") + } + // no html template + noHTMLTemplate := &EmailTemplate{Plain: testTemplate.Plain} + onlyPlainEmail, err := noHTMLTemplate.Compose(validParams, data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + if onlyPlainEmail.Body != nil { + t.Fatalf("expected nil, got %v", string(onlyPlainEmail.Body)) + } + if string(onlyPlainEmail.PlainBody) != expectedPlain { + t.Fatalf("got %v, want %v", string(onlyPlainEmail.PlainBody), expectedPlain) + } + // no plain template + noPlainTemplate := &EmailTemplate{HTML: testTemplate.HTML} + onlyHTMLEmail, err := noPlainTemplate.Compose(validParams, data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + if string(onlyHTMLEmail.Body) != expectedBody { + t.Fatalf("got %v, want %v", string(onlyHTMLEmail.Body), expectedBody) + } + if onlyHTMLEmail.PlainBody != nil { + t.Fatalf("expected nil, got %v", string(onlyHTMLEmail.PlainBody)) + } +} + +func Test_composePlain(t *testing.T) { + // valid data and template + data := testData{ + Title: "Test Title", + Content: "Test Content", + } + body, err := testTemplate.composePlain(data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + expected := "Title: Test Title\nContent: Test Content" + if string(body) != expected { + t.Fatalf("got %v, want %v", string(body), expected) + } + // no plain template + wrongPlainTemplate := *testTemplate + wrongPlainTemplate.Plain = "" + body, err = wrongPlainTemplate.composePlain(data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + if body != nil { + t.Fatalf("expected nil, got %v", string(body)) + } +} + +func Test_composeHTML(t *testing.T) { + // valid data and template + data := testData{ + Title: "Test Title", + Content: "Test Content", + } + body, err := testTemplate.composeHTML(data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + expected := "Test Content
" + if string(body) != expected { + t.Fatalf("got %v, want %v", string(body), expected) + } + // no html template + wrongHTMLTemplate := *testTemplate + wrongHTMLTemplate.HTML = "" + body, err = wrongHTMLTemplate.composeHTML(data) + if err != nil { + t.Fatalf("expected nil, got error: %v", err) + } + if body != nil { + t.Fatalf("expected nil, got %v", string(body)) + } +} diff --git a/notification/notification.go b/notification/notification.go new file mode 100644 index 0000000..308c79a --- /dev/null +++ b/notification/notification.go @@ -0,0 +1,33 @@ +package notification + +import "net/mail" + +type NotificationParams struct { + To string + Subject string +} + +func (p NotificationParams) Valid() bool { + _, err := mail.ParseAddress(p.To) + return err == nil && p.Subject != "" +} + +type Notification struct { + Params NotificationParams + Body []byte + PlainBody []byte +} + +// Valid method checks if the email is valid. It returns true if the recipient +// email address, the subject and the body are not empty. +func (n *Notification) Valid() bool { + return n.Params.Valid() && max(len(n.Body), len(n.PlainBody)) > 0 +} + +type Queue interface { + Start() + Stop() + Pop() (Notification, bool) + Push(Notification) error + Send(Notification) error +} diff --git a/notification/notification_test.go b/notification/notification_test.go new file mode 100644 index 0000000..573947c --- /dev/null +++ b/notification/notification_test.go @@ -0,0 +1,90 @@ +package notification + +import ( + "testing" +) + +func TestNotificationParams_Valid(t *testing.T) { + tests := []struct { + name string + params NotificationParams + valid bool + }{ + { + name: "Valid params", + params: NotificationParams{To: "test@example.com", Subject: "Test Subject"}, + valid: true, + }, + { + name: "Invalid email", + params: NotificationParams{To: "invalid-email", Subject: "Test Subject"}, + valid: false, + }, + { + name: "Empty subject", + params: NotificationParams{To: "test@example.com", Subject: ""}, + valid: false, + }, + { + name: "Empty email and subject", + params: NotificationParams{To: "", Subject: ""}, + valid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.params.Valid(); got != tt.valid { + t.Errorf("expected valid: %v, got: %v", tt.valid, got) + } + }) + } +} + +func TestNotification_Valid(t *testing.T) { + tests := []struct { + name string + notification Notification + valid bool + }{ + { + name: "Valid notification with Body", + notification: Notification{ + Params: NotificationParams{To: "test@example.com", Subject: "Test Subject"}, + Body: []byte("Test Body"), + }, + valid: true, + }, + { + name: "Valid notification with PlainBody", + notification: Notification{ + Params: NotificationParams{To: "test@example.com", Subject: "Test Subject"}, + PlainBody: []byte("Test Plain Body"), + }, + valid: true, + }, + { + name: "Invalid notification with empty Body and PlainBody", + notification: Notification{ + Params: NotificationParams{To: "test@example.com", Subject: "Test Subject"}, + }, + valid: false, + }, + { + name: "Invalid notification with invalid Params", + notification: Notification{ + Params: NotificationParams{To: "invalid-email", Subject: "Test Subject"}, + Body: []byte("Test Body"), + }, + valid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.notification.Valid(); got != tt.valid { + t.Errorf("expected valid: %v, got: %v", tt.valid, got) + } + }) + } +} diff --git a/notification/templates/login/definition.go b/notification/templates/login/definition.go new file mode 100644 index 0000000..9a66b67 --- /dev/null +++ b/notification/templates/login/definition.go @@ -0,0 +1,64 @@ +package login + +import ( + _ "embed" + "regexp" + + "github.com/simpleauthlink/authapi/notification" + "github.com/simpleauthlink/authapi/notification/email" + "github.com/simpleauthlink/authapi/token" +) + +//go:embed template.html +var htmlTemplate string + +// Data struct contains the required data to fill the login email template. +type Data struct { + AppName string + Email string + Token string + Link string +} + +// Subject returns the email subject based on the login data. +func (d Data) Subject() string { + return "Your token for '" + d.AppName + "'" +} + +// FindToken function extracts the token from the email content. It uses a +// regular expression to fill the template with regex and find the token in +// the email content. Then it decodes the token and returns it. If the token +// is not found, it returns nil. +func FindToken(email, content string) *token.Token { + loginData := Data{ + AppName: `.+`, + Email: email, + Token: `(.+\..+)`, + Link: `.+`, + } + loginEmail, err := Template.Compose(notification.NotificationParams{ + To: email, + Subject: loginData.Subject(), + }, loginData) + if err != nil { + return nil + } + tokenRgx := regexp.MustCompile(string(loginEmail.PlainBody)) + tokenResult := tokenRgx.FindAllStringSubmatch(content, -1) + if len(tokenResult) < 1 || len(tokenResult[0]) < 2 { + return nil + } + return new(token.Token).SetString(tokenResult[0][1]) +} + +// Template is the login email template definition, which contains the HTML +// and plain text templates. +var Template = email.EmailTemplate{ + HTML: htmlTemplate, + Plain: `Hi, {{.Email}} +You can access to '{{.AppName}}' app using the following link: +{{.Link}} +It contains your login token: '{{.Token}}' +Which is only valid for you and for a short period of time. +If you didn't request this, you can ignore this email.`, +} diff --git a/assets/token_email_template.html b/notification/templates/login/template.html similarity index 60% rename from assets/token_email_template.html rename to notification/templates/login/template.html index 2a51785..ec1476c 100644 --- a/assets/token_email_template.html +++ b/notification/templates/login/template.html @@ -1,5 +1,4 @@ - @@ -7,21 +6,13 @@| @@ -29,35 +20,41 @@ style="border-collapse: collapse; border: 1px solid #cccccc;"> | |||
- SimpleAuth.link |
|||
| - π Hi, {{.EmailHandler}}! + |
+ π Hi, {{.Email}}!- Your magic link to login to '{{.AppName}}' is ready π. + Your magic link is ready! π + You can access to your {{.AppName}} account using it π. + Click the button below to login to your account. π |
||
SimpleAuth
or copy and paste the following link in your browser:
|
|||
| - {{.AppName}} || Made by SimpleAuth.link + | + {{.AppName}} || Made by SimpleAuth.link |