Skip to content

Commit

Permalink
[pocketbase#75] added option to test s3 connection and send test emails
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Aug 21, 2022
1 parent 3f4f4cf commit 587cfc3
Show file tree
Hide file tree
Showing 49 changed files with 1,539 additions and 838 deletions.
52 changes: 51 additions & 1 deletion apis/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package apis
import (
"net/http"

validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tools/rest"
"github.com/pocketbase/pocketbase/tools/security"
)

// BindSettingsApi registers the settings api endpoints.
Expand All @@ -16,6 +18,8 @@ func BindSettingsApi(app core.App, rg *echo.Group) {
subGroup := rg.Group("/settings", ActivityLogger(app), RequireAdminAuth())
subGroup.GET("", api.list)
subGroup.PATCH("", api.set)
subGroup.POST("/test/s3", api.testS3)
subGroup.POST("/test/email", api.testEmail)
}

type settingsApi struct {
Expand Down Expand Up @@ -43,7 +47,7 @@ func (api *settingsApi) set(c echo.Context) error {

// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
}

event := &core.SettingsUpdateEvent{
Expand Down Expand Up @@ -76,3 +80,49 @@ func (api *settingsApi) set(c echo.Context) error {

return submitErr
}

func (api *settingsApi) testS3(c echo.Context) error {
if !api.app.Settings().S3.Enabled {
return rest.NewBadRequestError("S3 storage is not enabled.", nil)
}

fs, err := api.app.NewFilesystem()
if err != nil {
return rest.NewBadRequestError("Failed to initialize the S3 storage. Raw error: \n"+err.Error(), nil)
}
defer fs.Close()

testFileKey := "pb_test_" + security.RandomString(5) + "/test.txt"

if err := fs.Upload([]byte("test"), testFileKey); err != nil {
return rest.NewBadRequestError("Failed to upload a test file. Raw error: \n"+err.Error(), nil)
}

if err := fs.Delete(testFileKey); err != nil {
return rest.NewBadRequestError("Failed to delete a test file. Raw error: \n"+err.Error(), nil)
}

return c.NoContent(http.StatusNoContent)
}

func (api *settingsApi) testEmail(c echo.Context) error {
form := forms.NewTestEmailSend(api.app)

// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occurred while loading the submitted data.", err)
}

// send
if err := form.Submit(); err != nil {
if fErr, ok := err.(validation.Errors); ok {
// form error
return rest.NewBadRequestError("Failed to send the test email.", fErr)
}

// mailer error
return rest.NewBadRequestError("Failed to send the test email. Raw error: \n"+err.Error(), nil)
}

return c.NoContent(http.StatusNoContent)
}
190 changes: 190 additions & 0 deletions apis/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"
"testing"

"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/tests"
)

Expand Down Expand Up @@ -183,3 +184,192 @@ func TestSettingsSet(t *testing.T) {
scenario.Test(t)
}
}

func TestSettingsTestS3(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodPost,
Url: "/api/settings/test/s3",
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user",
Method: http.MethodPost,
Url: "/api/settings/test/s3",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin (no s3)",
Method: http.MethodPost,
Url: "/api/settings/test/s3",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
// @todo consider creating a test S3 filesystem
}

for _, scenario := range scenarios {
scenario.Test(t)
}
}

func TestSettingsTestEmail(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "verification",
"email": "[email protected]"
}`),
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "verification",
"email": "[email protected]"
}`),
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin (invalid body)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin (empty json)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"email":{"code":"validation_required"`,
`"template":{"code":"validation_required"`,
},
},
{
Name: "authorized as admin (verifiation template)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "verification",
"email": "[email protected]"
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if app.TestMailer.TotalSend != 1 {
t.Fatalf("[verification] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}

if app.TestMailer.LastToAddress.Address != "[email protected]" {
t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "[email protected]", app.TestMailer.LastToAddress.Address)
}

if !strings.Contains(app.TestMailer.LastHtmlBody, "Verify") {
t.Fatalf("[verification] Expected to sent a verification email, got \n%v\n%v", app.TestMailer.LastHtmlSubject, app.TestMailer.LastHtmlBody)
}
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnMailerBeforeUserVerificationSend": 1,
"OnMailerAfterUserVerificationSend": 1,
},
},
{
Name: "authorized as admin (password reset template)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "password-reset",
"email": "[email protected]"
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if app.TestMailer.TotalSend != 1 {
t.Fatalf("[password-reset] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}

if app.TestMailer.LastToAddress.Address != "[email protected]" {
t.Fatalf("[password-reset] Expected the email to be sent to %s, got %s", "[email protected]", app.TestMailer.LastToAddress.Address)
}

if !strings.Contains(app.TestMailer.LastHtmlBody, "Reset password") {
t.Fatalf("[password-reset] Expected to sent a password-reset email, got \n%v\n%v", app.TestMailer.LastHtmlSubject, app.TestMailer.LastHtmlBody)
}
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnMailerBeforeUserResetPasswordSend": 1,
"OnMailerAfterUserResetPasswordSend": 1,
},
},
{
Name: "authorized as admin (email change)",
Method: http.MethodPost,
Url: "/api/settings/test/email",
Body: strings.NewReader(`{
"template": "email-change",
"email": "[email protected]"
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
if app.TestMailer.TotalSend != 1 {
t.Fatalf("[email-change] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}

if app.TestMailer.LastToAddress.Address != "[email protected]" {
t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "[email protected]", app.TestMailer.LastToAddress.Address)
}

if !strings.Contains(app.TestMailer.LastHtmlBody, "Confirm new email") {
t.Fatalf("[email-change] Expected to sent a confirm new email email, got \n%v\n%v", app.TestMailer.LastHtmlSubject, app.TestMailer.LastHtmlBody)
}
},
ExpectedStatus: 204,
ExpectedContent: []string{},
ExpectedEvents: map[string]int{
"OnMailerBeforeUserChangeEmailSend": 1,
"OnMailerAfterUserChangeEmailSend": 1,
},
},
}

for _, scenario := range scenarios {
scenario.Test(t)
}
}
2 changes: 1 addition & 1 deletion forms/realtime_subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4"
)

// RealtimeSubscribe specifies a RealtimeSubscribe request form.
// RealtimeSubscribe is a realtime subscriptions request form.
type RealtimeSubscribe struct {
ClientId string `form:"clientId" json:"clientId"`
Subscriptions []string `form:"subscriptions" json:"subscriptions"`
Expand Down
69 changes: 69 additions & 0 deletions forms/test_email_send.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package forms

import (
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails"
"github.com/pocketbase/pocketbase/models"
)

const (
templateVerification = "verification"
templatePasswordReset = "password-reset"
templateEmailChange = "email-change"
)

// TestEmailSend is a email template test request form.
type TestEmailSend struct {
app core.App

Template string `form:"template" json:"template"`
Email string `form:"email" json:"email"`
}

// NewTestEmailSend creates and initializes new TestEmailSend form.
func NewTestEmailSend(app core.App) *TestEmailSend {
return &TestEmailSend{app: app}
}

// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *TestEmailSend) Validate() error {
return validation.ValidateStruct(form,
validation.Field(
&form.Email,
validation.Required,
validation.Length(1, 255),
is.Email,
),
validation.Field(
&form.Template,
validation.Required,
validation.In(templateVerification, templateEmailChange, templatePasswordReset),
),
)
}

// Submit validates and sends a test email to the form.Email address.
func (form *TestEmailSend) Submit() error {
if err := form.Validate(); err != nil {
return err
}

// create a test user
user := &models.User{}
user.Id = "__pb_test_id__"
user.Email = form.Email
user.RefreshTokenKey()

switch form.Template {
case templateVerification:
return mails.SendUserVerification(form.app, user)
case templatePasswordReset:
return mails.SendUserPasswordReset(form.app, user)
case templateEmailChange:
return mails.SendUserChangeEmail(form.app, user, form.Email)
}

return nil
}
Loading

0 comments on commit 587cfc3

Please sign in to comment.