fix(review): fix review findings

* admin api: make email primary when user has no emails
* utils: move get updated user and webhook trigger to utils to reduce duplicated code
* events: remove unused user and email event - Check is replaced with string variant
* remove unused dtos
* fix tests after changes
* webhook tests: switch to test.Suite instead of TestPersister -> added deprecation annotation to test.NewPersister
* Email Verification: Fix trigger of webhook when email verification is enabled and a email is created but not validated

Closes: #692, #1051
This commit is contained in:
Stefan Jacobi
2024-01-25 12:01:00 +01:00
parent 8139cc22b7
commit c9994bdc3a
20 changed files with 281 additions and 617 deletions

View File

@ -1,6 +0,0 @@
package dto
type UpdateCredentialDto struct {
Id string `json:"id"`
Name *string `json:"name,omitempty"`
}

View File

@ -1,5 +0,0 @@
package dto
type TokenDto struct {
Token string `json:"token"`
}

View File

@ -10,7 +10,6 @@ import (
auditlog "github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/dto/admin"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/session"
@ -20,10 +19,6 @@ import (
"strings"
)
const (
UpdatedUserErrorMessage = "failed to fetch updated user: %w"
)
type EmailHandler struct {
persister persistence.Persister
cfg *config.Config
@ -31,13 +26,13 @@ type EmailHandler struct {
auditLogger auditlog.Logger
}
func NewEmailHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, auditLogger auditlog.Logger) (*EmailHandler, error) {
func NewEmailHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, auditLogger auditlog.Logger) *EmailHandler {
return &EmailHandler{
persister: persister,
cfg: cfg,
sessionManager: sessionManager,
auditLogger: auditLogger,
}, nil
}
}
func (h *EmailHandler) List(c echo.Context) error {
@ -145,15 +140,8 @@ func (h *EmailHandler) Create(c echo.Context) error {
return fmt.Errorf("failed to create audit log: %w", err)
}
updatedUser, err := h.persister.GetUserPersisterWithConnection(tx).Get(user.ID)
if err != nil {
if err != nil {
return fmt.Errorf(UpdatedUserErrorMessage, err)
}
}
err = utils.TriggerWebhooks(c, events.EmailCreate, admin.FromUserModel(*updatedUser))
if err != nil {
c.Logger().Warn(err)
if !h.cfg.Emails.RequireVerification {
utils.NotifyUserChange(c, tx, h.persister, events.EmailCreate, userId)
}
return c.JSON(http.StatusOK, email)
@ -215,16 +203,7 @@ func (h *EmailHandler) SetPrimaryEmail(c echo.Context) error {
return fmt.Errorf("failed to create audit log: %w", err)
}
updatedUser, err := h.persister.GetUserPersisterWithConnection(tx).Get(user.ID)
if err != nil {
if err != nil {
return fmt.Errorf(UpdatedUserErrorMessage, err)
}
}
err = utils.TriggerWebhooks(c, events.EmailPrimary, admin.FromUserModel(*updatedUser))
if err != nil {
c.Logger().Warn(err)
}
utils.NotifyUserChange(c, tx, h.persister, events.EmailPrimary, userId)
return c.NoContent(http.StatusNoContent)
})
@ -268,16 +247,7 @@ func (h *EmailHandler) Delete(c echo.Context) error {
return fmt.Errorf("failed to create audit log: %w", err)
}
updatedUser, err := h.persister.GetUserPersisterWithConnection(tx).Get(user.ID)
if err != nil {
if err != nil {
return fmt.Errorf(UpdatedUserErrorMessage, err)
}
}
err = utils.TriggerWebhooks(c, events.EmailDelete, admin.FromUserModel(*updatedUser))
if err != nil {
c.Logger().Warn(err)
}
utils.NotifyUserChange(c, tx, h.persister, events.EmailDelete, userId)
return c.NoContent(http.StatusNoContent)
})

View File

@ -144,17 +144,14 @@ func (h *emailAdminHandler) Create(ctx echo.Context) error {
}
}
updatedUser, err := h.persister.GetUserPersisterWithConnection(tx).Get(user.ID)
if err != nil {
if err != nil {
return fmt.Errorf(UpdatedUserErrorMessage, err)
}
}
err = utils.TriggerWebhooks(ctx, events.EmailCreate, admin.FromUserModel(*updatedUser))
if err != nil {
ctx.Logger().Warn(err)
// make email primary if user had no emails prior to email creation
if len(user.Emails) == 0 {
primaryEmail := models.NewPrimaryEmail(email.ID, user.ID)
err = h.persister.GetPrimaryEmailPersisterWithConnection(tx).Create(*primaryEmail)
}
utils.NotifyUserChange(ctx, tx, h.persister, events.EmailCreate, userId)
return ctx.JSON(http.StatusCreated, admin.FromEmailModel(email))
})
}
@ -232,16 +229,7 @@ func (h *emailAdminHandler) Delete(ctx echo.Context) error {
return fmt.Errorf("failed to delete email from db: %w", err)
}
updatedUser, err := h.persister.GetUserPersisterWithConnection(tx).Get(user.ID)
if err != nil {
if err != nil {
return fmt.Errorf(UpdatedUserErrorMessage, err)
}
}
err = utils.TriggerWebhooks(ctx, events.EmailDelete, admin.FromUserModel(*updatedUser))
if err != nil {
ctx.Logger().Warn(err)
}
utils.NotifyUserChange(ctx, tx, h.persister, events.EmailDelete, userId)
return ctx.NoContent(http.StatusNoContent)
})
@ -287,16 +275,7 @@ func (h *emailAdminHandler) SetPrimaryEmail(ctx echo.Context) error {
return err
}
updatedUser, err := h.persister.GetUserPersisterWithConnection(tx).Get(user.ID)
if err != nil {
if err != nil {
return fmt.Errorf(UpdatedUserErrorMessage, err)
}
}
err = utils.TriggerWebhooks(ctx, events.EmailPrimary, admin.FromUserModel(*updatedUser))
if err != nil {
ctx.Logger().Warn(err)
}
utils.NotifyUserChange(ctx, tx, h.persister, events.EmailPrimary, userId)
return ctx.NoContent(http.StatusNoContent)
})

View File

@ -26,8 +26,7 @@ type emailSuite struct {
}
func (s *emailSuite) TestEmailHandler_New() {
emailHandler, err := NewEmailHandler(&config.Config{}, s.Storage, sessionManager{}, test.NewAuditLogger())
s.NoError(err)
emailHandler := NewEmailHandler(&config.Config{}, s.Storage, sessionManager{}, test.NewAuditLogger())
s.NotEmpty(emailHandler)
}

View File

@ -17,6 +17,8 @@ import (
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/rate_limiter"
"github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/webhooks/events"
"github.com/teamhanko/hanko/backend/webhooks/utils"
"golang.org/x/crypto/bcrypt"
"gopkg.in/gomail.v2"
"net/http"
@ -323,7 +325,10 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
return echo.NewHTTPError(http.StatusForbidden).SetInternal(errors.New("passcode finalization not allowed"))
}
wasUnverified := false
if !passcode.Email.Verified {
wasUnverified = true
// Update email verified status and assign the email address to the user.
passcode.Email.Verified = true
passcode.Email.UserID = &user.ID
@ -377,6 +382,11 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
return fmt.Errorf("failed to create audit log: %w", err)
}
// notify about email verification result. Last step to prevent a trigger and rollback scenario
if h.cfg.Emails.RequireVerification && wasUnverified {
utils.NotifyUserChange(c, tx, h.persister, events.EmailCreate, user.ID)
}
return c.JSON(http.StatusOK, dto.PasscodeReturn{
Id: passcode.ID.String(),
TTL: passcode.Ttl,

View File

@ -125,10 +125,7 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
wellKnown.GET("/jwks.json", wellKnownHandler.GetPublicKeys)
wellKnown.GET("/config", wellKnownHandler.GetConfig)
emailHandler, err := NewEmailHandler(cfg, persister, sessionManager, auditLogger)
if err != nil {
panic(fmt.Errorf("failed to create public email handler: %w", err))
}
emailHandler := NewEmailHandler(cfg, persister, sessionManager, auditLogger)
webauthn := g.Group("/webauthn")
webauthnRegistration := webauthn.Group("/registration", sessionMiddleware)
@ -147,7 +144,7 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
passcode := g.Group("/passcode")
passcodeLogin := passcode.Group("/login")
passcodeLogin.POST("/initialize", passcodeHandler.Init)
passcodeLogin.POST("/finalize", passcodeHandler.Finish)
passcodeLogin.POST("/finalize", passcodeHandler.Finish, webhookMiddlware)
email := g.Group("/emails", sessionMiddleware, webhookMiddlware)
email.GET("", emailHandler.List)

View File

@ -145,10 +145,12 @@ func (h *UserHandler) Create(c echo.Context) error {
EmailID: email.ID,
}
if !h.cfg.Emails.RequireVerification {
err = utils.TriggerWebhooks(c, events.UserCreate, admin.FromUserModel(newUser))
if err != nil {
c.Logger().Warn(err)
}
}
return c.JSON(http.StatusOK, newUserDto)
})

View File

@ -40,7 +40,7 @@ func (s *webhookSuite) TestWebhookHandler_List() {
Hooks: config.Webhooks{
config.Webhook{
Callback: "http://lorem",
Events: events.Events{events.User},
Events: events.Events{events.UserDelete},
},
config.Webhook{
Callback: "http://ipsum",
@ -62,7 +62,7 @@ func (s *webhookSuite) TestWebhookHandler_List() {
err = json.Unmarshal(rec.Body.Bytes(), &dto)
s.Require().NoError(err)
s.Equal(3, len(dto.Database))
s.Equal(5, len(dto.Database))
s.Equal(2, len(dto.Config))
}
@ -78,7 +78,7 @@ func (s *webhookSuite) TestWebhookHandler_Create() {
testBody := admin.CreateWebhookRequestDto{
Callback: "http://lorem",
Events: events.Events{
events.User,
events.UserDelete,
},
}
testBodyJson, err := json.Marshal(testBody)
@ -121,24 +121,24 @@ func (s *webhookSuite) TestWebhookHandler_CreateWithParams() {
{
name: "success",
callback: "http://lorem.ipsum",
events: events.Events{events.User},
events: events.Events{events.UserDelete},
expectedStatus: http.StatusCreated,
},
{
name: "empty callback",
callback: "",
events: events.Events{events.User},
events: events.Events{events.UserDelete},
expectedStatus: http.StatusBadRequest,
},
{
name: "missing callback",
events: events.Events{events.User},
events: events.Events{events.UserDelete},
expectedStatus: http.StatusBadRequest,
},
{
name: "wrong callback",
callback: "lorem",
events: events.Events{events.User},
events: events.Events{events.UserDelete},
expectedStatus: http.StatusBadRequest,
},
{
@ -223,7 +223,7 @@ func (s *webhookSuite) TestWebhookHandler_Delete() {
s.Require().NoError(err)
s.Require().Nil(entry)
s.Equal(2, len(list))
s.Equal(4, len(list))
err = e.Close()
s.Require().NoError(err)
@ -409,7 +409,7 @@ func (s *webhookSuite) TestWebhookHandler_Update() {
CreateWebhookRequestDto: admin.CreateWebhookRequestDto{
Callback: "https://ipsum.magna/lorem",
Events: events.Events{
events.User,
events.UserDelete,
},
},
Enabled: true,
@ -470,7 +470,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "8b00da9a-cacf-45ea-b25d-c1ce0f0d7da4",
callback: "https://lorem.ipsum.et",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusOK,
@ -480,7 +480,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "8b00da9a-cacf-45ea-b25d-c1ce0f0d7da7",
callback: "https://lorem.ipsum.et",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusNotFound,
@ -490,7 +490,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "",
callback: "https://lorem.ipsum.et",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusNotFound,
@ -500,7 +500,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "lorem",
callback: "https://lorem.ipsum.et",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusBadRequest,
@ -509,7 +509,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
name: "missing ID",
callback: "https://lorem.ipsum.et",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusNotFound,
@ -519,7 +519,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "8b00da9a-cacf-45ea-b25d-c1ce0f0d7da4",
callback: "",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusBadRequest,
@ -529,7 +529,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "8b00da9a-cacf-45ea-b25d-c1ce0f0d7da4",
callback: "lorem",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusBadRequest,
@ -538,7 +538,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
name: "missing Callback",
testId: "8b00da9a-cacf-45ea-b25d-c1ce0f0d7da4",
events: events.Events{
events.User,
events.UserDelete,
},
enabled: true,
expectedStatus: http.StatusBadRequest,
@ -563,7 +563,7 @@ func (s *webhookSuite) TestWebhookHandler_UpdateWithParams() {
testId: "8b00da9a-cacf-45ea-b25d-c1ce0f0d7da4",
callback: "https://lorem.ipsum.et",
events: events.Events{
events.User,
events.UserDelete,
},
expectedStatus: http.StatusBadRequest,
},

View File

@ -31,13 +31,6 @@ func (w *Webhook) Validate(tx *pop.Connection) (*validate.Errors, error) {
&validators.UUIDIsPresent{Name: "ID", Field: w.ID},
&validators.StringIsPresent{Name: "Callback", Field: w.Callback},
&validators.TimeIsPresent{Name: "ExpiresAt", Field: w.ExpiresAt},
&validators.TimeAfterTime{
FirstName: "Expires At",
FirstTime: w.ExpiresAt,
SecondName: "Now",
SecondTime: time.Now(),
},
&validators.TimeIsPresent{Name: "UpdatedAt", Field: w.UpdatedAt},
&validators.TimeIsPresent{Name: "CreatedAt", Field: w.CreatedAt},
), nil

View File

@ -19,3 +19,17 @@
expires_at: 2220-12-31 23:59:59
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 8b00da9a-cacf-45ea-b25d-c1ce0f0d7da3
callback: http://localhost
enabled: 1
failures: 0
expires_at: 2020-12-31 23:59:59
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 8b00da9a-cacf-45ea-b25d-c1ce0f0d7da2
callback: http://localhost
enabled: 1
failures: 5
expires_at: 2020-12-31 23:59:59
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59

View File

@ -6,6 +6,7 @@ import (
"github.com/teamhanko/hanko/backend/persistence/models"
)
// Deprecated: NewPersister is deprecated. User test.Suite instead
func NewPersister(
user []models.User,
passcodes []models.Passcode,

View File

@ -12,7 +12,7 @@ import (
func TestNewConfigHook(t *testing.T) {
hook := config.Webhook{
Callback: "http://lorem.ipsum",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
cfgHook := NewConfigHook(hook, nil)
@ -23,7 +23,7 @@ func TestConfigHook_DisableOnExpiryDate(t *testing.T) {
now := time.Now()
hook := config.Webhook{
Callback: "http://lorem.ipsum",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
dbHook := NewConfigHook(hook, nil)
@ -34,7 +34,7 @@ func TestConfigHook_DisableOnExpiryDate(t *testing.T) {
func TestConfigHook_DisableOnFailure(t *testing.T) {
hook := config.Webhook{
Callback: "http://lorem.ipsum",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
dbHook := NewConfigHook(hook, nil)
@ -45,7 +45,7 @@ func TestConfigHook_DisableOnFailure(t *testing.T) {
func TestConfigHook_Reset(t *testing.T) {
hook := config.Webhook{
Callback: "http://lorem.ipsum",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
dbHook := NewConfigHook(hook, nil)
@ -56,7 +56,7 @@ func TestConfigHook_Reset(t *testing.T) {
func TestConfigHook_IsEnabled(t *testing.T) {
hook := config.Webhook{
Callback: "http://lorem.ipsum",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
dbHook := NewConfigHook(hook, nil)

View File

@ -2,322 +2,134 @@ package webhooks
import (
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/test"
"testing"
"time"
)
func TestNewDatabaseHook(t *testing.T) {
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
func TestDatabaseHookSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(databaseHookSuite))
}
type databaseHookSuite struct {
test.Suite
}
func (s *databaseHookSuite) TestNewDatabaseHook() {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
s.Require().NoError(err)
hook := models.Webhook{
ID: hookId,
Enabled: false,
Failures: 0,
ExpiresAt: time.Now().Add(24 * -1 * time.Hour),
ExpiresAt: time.Now().Add(WebhookExpireDuration),
}
dbHook := NewDatabaseHook(hook, persister.GetWebhookPersister(nil), nil)
require.NotEmpty(t, dbHook)
dbHook := NewDatabaseHook(hook, s.Storage.GetWebhookPersister(nil), nil)
s.NotEmpty(dbHook)
}
func TestDatabaseHook_DisableOnExpiryDate(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
func (s *databaseHookSuite) TestDatabaseHook_DisableOnExpiryDate() {
hook, whPersister := s.loadWebhook("8b00da9a-cacf-45ea-b25d-c1ce0f0d7da3")
dbHook := NewDatabaseHook(hook, whPersister, nil)
now := time.Now()
hook := models.Webhook{
ID: hookId,
Enabled: true,
Failures: 0,
ExpiresAt: now.Add(24 * -1 * time.Hour),
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
dbHook := NewDatabaseHook(hook, whPersister, nil)
err = dbHook.DisableOnExpiryDate(now)
assert.NoError(t, err)
err := dbHook.DisableOnExpiryDate(now)
s.NoError(err)
updatedHook, err := whPersister.Get(hook.ID)
assert.NoError(t, err)
s.Require().NoError(err)
require.False(t, updatedHook.Enabled)
s.False(updatedHook.Enabled)
}
func TestDatabaseHook_DoNotDisableOnExpiryDate(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
func (s *databaseHookSuite) TestDatabaseHook_DoNotDisableOnExpiryDate() {
hook, whPersister := s.loadWebhook("a47fe92a-1e4b-4119-8653-55ad82737c88")
dbHook := NewDatabaseHook(hook, whPersister, nil)
now := time.Now()
hook := models.Webhook{
ID: hookId,
Enabled: true,
Failures: 0,
ExpiresAt: now.Add(24 * 1 * time.Hour),
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
dbHook := NewDatabaseHook(hook, whPersister, nil)
err = dbHook.DisableOnExpiryDate(now)
assert.NoError(t, err)
err := dbHook.DisableOnExpiryDate(now)
s.NoError(err)
updatedHook, err := whPersister.Get(hook.ID)
assert.NoError(t, err)
s.Require().NoError(err)
require.True(t, updatedHook.Enabled)
s.True(updatedHook.Enabled)
}
func TestDatabaseHook_DisableOnFailure(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
hook := models.Webhook{
ID: hookId,
Enabled: true,
Failures: FailureExpireRate,
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
func (s *databaseHookSuite) TestDatabaseHook_DisableOnFailure() {
hook, whPersister := s.loadWebhook("8b00da9a-cacf-45ea-b25d-c1ce0f0d7da2")
dbHook := NewDatabaseHook(hook, whPersister, nil)
err = dbHook.DisableOnFailure()
assert.NoError(t, err)
err := dbHook.DisableOnFailure()
s.Require().NoError(err)
updatedHook, err := whPersister.Get(hook.ID)
assert.NoError(t, err)
s.NoError(err)
require.False(t, updatedHook.Enabled)
s.False(updatedHook.Enabled)
}
func TestDatabaseHook_DoNotDisableOnFailure(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
hook := models.Webhook{
ID: hookId,
Enabled: true,
Failures: 0,
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
func (s *databaseHookSuite) TestDatabaseHook_DoNotDisableOnFailure() {
hook, whPersister := s.loadWebhook("8b00da9a-cacf-45ea-b25d-c1ce0f0d7da3")
dbHook := NewDatabaseHook(hook, whPersister, nil)
err = dbHook.DisableOnFailure()
assert.NoError(t, err)
err := dbHook.DisableOnFailure()
s.NoError(err)
updatedHook, err := whPersister.Get(hook.ID)
assert.NoError(t, err)
s.Require().NoError(err)
require.True(t, updatedHook.Enabled)
s.True(updatedHook.Enabled)
}
func TestDatabaseHook_Reset(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
func (s *databaseHookSuite) TestDatabaseHook_Reset() {
hook, whPersister := s.loadWebhook("8b00da9a-cacf-45ea-b25d-c1ce0f0d7da2")
dbHook := NewDatabaseHook(hook, whPersister, nil)
err := dbHook.Reset()
s.NoError(err)
updatedHook, err := whPersister.Get(hook.ID)
s.Require().NoError(err)
s.Less(updatedHook.Failures, hook.Failures, "Failures should be reset to 0")
s.Equal(0, updatedHook.Failures)
now := time.Now()
hook := models.Webhook{
ID: hookId,
Enabled: true,
Failures: 3,
s.True(updatedHook.ExpiresAt.After(now))
s.True(updatedHook.UpdatedAt.After(now))
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
dbHook := NewDatabaseHook(hook, whPersister, nil)
err = dbHook.Reset()
assert.NoError(t, err)
updatedHook, err := whPersister.Get(hook.ID)
assert.NoError(t, err)
require.Less(t, updatedHook.Failures, hook.Failures, "Failures should be reset to 0")
require.Equal(t, 0, updatedHook.Failures)
require.True(t, updatedHook.ExpiresAt.After(now))
require.True(t, updatedHook.UpdatedAt.After(now))
}
func TestDatabaseHook_IsEnabled(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
hook := models.Webhook{
ID: hookId,
Enabled: true,
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
func (s *databaseHookSuite) TestDatabaseHook_IsEnabled() {
hook, whPersister := s.loadWebhook("a47fe92a-1e4b-4119-8653-55ad82737c88")
dbHook := NewDatabaseHook(hook, whPersister, nil)
require.True(t, dbHook.IsEnabled())
s.True(dbHook.IsEnabled())
}
func TestDatabaseHook_IsDisabled(t *testing.T) {
hookId, err := uuid.NewV4()
assert.NoError(t, err)
hook := models.Webhook{
ID: hookId,
Enabled: false,
}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{hook},
nil,
)
whPersister := persister.GetWebhookPersister(nil)
func (s *databaseHookSuite) TestDatabaseHook_IsDisabled() {
hook, whPersister := s.loadWebhook("279beae1-8a6d-4eaf-a791-1fa79d21d37a")
dbHook := NewDatabaseHook(hook, whPersister, nil)
require.False(t, dbHook.IsEnabled())
s.False(dbHook.IsEnabled())
}
func (s *databaseHookSuite) loadWebhook(hookId string) (models.Webhook, persistence.WebhookPersister) {
err := s.LoadFixtures("../test/fixtures/webhooks")
s.Require().NoError(err)
whPersister := s.Storage.GetWebhookPersister(nil)
hook, err := whPersister.Get(uuid.FromStringOrNil(hookId))
s.Require().NoError(err)
s.Require().NotEmpty(hook)
return *hook, whPersister
}

View File

@ -5,11 +5,9 @@ import "github.com/teamhanko/hanko/backend/persistence/models"
type Event string
const (
User Event = "user"
UserCreate Event = "user.create"
UserUpdate Event = "user.update"
UserDelete Event = "user.delete"
Email Event = "user.update.email"
EmailCreate Event = "user.update.email.create"
EmailPrimary Event = "user.update.email.primary"
EmailDelete Event = "user.update.email.delete"
@ -23,7 +21,7 @@ func StringIsValidEvent(value string) bool {
func IsValidEvent(evt Event) bool {
var isValid bool
switch evt {
case User, UserCreate, UserUpdate, UserDelete, Email, EmailCreate, EmailPrimary, EmailDelete:
case "user", "user.update.email", UserCreate, UserUpdate, UserDelete, EmailCreate, EmailPrimary, EmailDelete:
isValid = true
default:
isValid = false

View File

@ -2,9 +2,9 @@ package webhooks
import (
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/test"
"github.com/teamhanko/hanko/backend/webhooks/events"
@ -14,106 +14,68 @@ import (
"time"
)
func TestNewManager(t *testing.T) {
cfg := config.Config{}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
assert.NoError(t, err)
require.NotEmpty(t, manager)
func TestManagerSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(managerSuite))
}
func TestManager_GenerateJWT(t *testing.T) {
type managerSuite struct {
test.Suite
}
func (s *managerSuite) TestNewManager() {
cfg := config.Config{}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
manager, err := NewManager(&cfg, s.Storage.GetWebhookPersister(nil), jwkManager, nil)
s.NoError(err)
s.NotEmpty(manager)
}
func (s *managerSuite) TestManager_GenerateJWT() {
cfg := config.Config{}
jwkManager := test.JwkManager{}
manager, err := NewManager(&cfg, s.Storage.GetWebhookPersister(nil), jwkManager, nil)
testData := "lorem-ipsum"
dataToken, err := manager.GenerateJWT(testData, events.User)
assert.NoError(t, err)
assert.NotEmpty(t, dataToken)
dataToken, err := manager.GenerateJWT(testData, events.UserCreate)
s.NoError(err)
s.NotEmpty(dataToken)
}
func TestManager_TriggerWithoutHook(t *testing.T) {
func (s *managerSuite) TestManager_TriggerWithoutHook() {
triggered := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Fail(t, "no hook should not trigger a http request")
triggered = true
}))
defer server.Close()
cfg := config.Config{}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
assert.NoError(t, err)
manager, err := NewManager(&cfg, s.Storage.GetWebhookPersister(nil), jwkManager, nil)
s.Require().NoError(err)
manager.Trigger(events.User, "lorem-ipsum")
manager.Trigger(events.UserCreate, "lorem-ipsum")
// give it 1 sec to trigger
time.Sleep(1 * time.Second)
s.False(triggered)
}
func TestManager_TriggerWithConfigHook(t *testing.T) {
func (s *managerSuite) TestManager_TriggerWithConfigHook() {
triggered := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.True(t, true)
triggered = true
}))
defer server.Close()
hooks := config.Webhooks{config.Webhook{
Callback: server.URL,
Events: events.Events{
events.User,
events.UserCreate,
},
}}
@ -125,43 +87,28 @@ func TestManager_TriggerWithConfigHook(t *testing.T) {
}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
manager, err := NewManager(&cfg, s.Storage.GetWebhookPersister(nil), jwkManager, nil)
s.Require().NoError(err)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
assert.NoError(t, err)
manager.Trigger(events.User, "lorem-ipsum")
manager.Trigger(events.UserCreate, "lorem-ipsum")
// give it 1 sec to trigger
time.Sleep(1 * time.Second)
s.True(triggered)
}
func TestManager_TriggerWithDisabledConfigHook(t *testing.T) {
func (s *managerSuite) TestManager_TriggerWithDisabledConfigHook() {
triggered := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Fail(t, "no hook should not trigger a http request")
triggered = true
}))
defer server.Close()
hooks := config.Webhooks{config.Webhook{
Callback: server.URL,
Events: events.Events{
events.User,
events.UserCreate,
},
}}
@ -173,151 +120,87 @@ func TestManager_TriggerWithDisabledConfigHook(t *testing.T) {
}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
manager, err := NewManager(&cfg, s.Storage.GetWebhookPersister(nil), jwkManager, nil)
s.Require().NoError(err)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
assert.NoError(t, err)
manager.Trigger(events.User, "lorem-ipsum")
manager.Trigger(events.UserCreate, "lorem-ipsum")
// give it 1 sec to trigger
time.Sleep(1 * time.Second)
s.False(triggered)
}
func TestManager_TriggerWithDbHook(t *testing.T) {
func (s *managerSuite) TestManager_TriggerWithDbHook() {
triggered := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.True(t, true)
triggered = true
}))
defer server.Close()
hookUuid, err := uuid.NewV4()
assert.NoError(t, err)
eventUuid, err := uuid.NewV4()
assert.NoError(t, err)
cfg := config.Config{}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{
models.Webhook{
ID: hookUuid,
Callback: server.URL,
Enabled: true,
Failures: 0,
ExpiresAt: time.Now(),
WebhookEvents: models.WebhookEvents{
models.WebhookEvent{
ID: eventUuid,
Webhook: nil,
WebhookID: hookUuid,
Event: string(events.User),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
nil,
)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
assert.NoError(t, err)
persister := s.Storage.GetWebhookPersister(nil)
manager.Trigger(events.User, "lorem-ipsum")
s.createTestDatabaseWebhook(persister, true, server.URL)
manager, err := NewManager(&cfg, persister, jwkManager, nil)
s.Require().NoError(err)
manager.Trigger(events.UserCreate, "lorem-ipsum")
// give it 1 sec to trigger
time.Sleep(1 * time.Second)
s.True(triggered)
}
func TestManager_TriggerWithDisabledDbHook(t *testing.T) {
func (s *managerSuite) TestManager_TriggerWithDisabledDbHook() {
triggered := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Fail(t, "no hook should not trigger a http request")
triggered = true
}))
defer server.Close()
hookUuid, err := uuid.NewV4()
assert.NoError(t, err)
eventUuid, err := uuid.NewV4()
assert.NoError(t, err)
cfg := config.Config{}
jwkManager := test.JwkManager{}
persister := test.NewPersister(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
models.Webhooks{
models.Webhook{
ID: hookUuid,
Callback: server.URL,
Enabled: false,
Failures: 0,
ExpiresAt: time.Now(),
WebhookEvents: models.WebhookEvents{
models.WebhookEvent{
ID: eventUuid,
Webhook: nil,
WebhookID: hookUuid,
Event: string(events.User),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
nil,
)
persister := s.Storage.GetWebhookPersister(nil)
manager, err := NewManager(&cfg, persister.GetWebhookPersister(nil), jwkManager, nil)
assert.NoError(t, err)
s.createTestDatabaseWebhook(persister, false, server.URL)
manager.Trigger(events.User, "lorem-ipsum")
manager, err := NewManager(&cfg, persister, jwkManager, nil)
s.Require().NoError(err)
manager.Trigger(events.UserCreate, "lorem-ipsum")
// give it 1 sec to trigger
time.Sleep(1 * time.Second)
s.False(triggered)
}
func (s *managerSuite) createTestDatabaseWebhook(persister persistence.WebhookPersister, isEnabled bool, callback string) {
now := time.Now()
hookId := uuid.FromStringOrNil("8b00da9a-cacf-45ea-b25d-c1ce0f0d7da1")
err := persister.Create(
models.Webhook{
ID: hookId,
Callback: callback,
Enabled: isEnabled,
Failures: 0,
ExpiresAt: now,
CreatedAt: now,
UpdatedAt: now,
},
models.WebhookEvents{
models.WebhookEvent{
ID: uuid.FromStringOrNil("8b00da9a-cacf-45ea-b25d-c1ce0f0d7da0"),
WebhookID: hookId,
Event: string(events.UserCreate),
CreatedAt: now,
UpdatedAt: now,
},
})
s.Require().NoError(err)
}

View File

@ -2,7 +2,11 @@ package utils
import (
"fmt"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/dto/admin"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/webhooks"
"github.com/teamhanko/hanko/backend/webhooks/events"
)
@ -19,3 +23,16 @@ func TriggerWebhooks(ctx echo.Context, evt events.Event, data interface{}) error
return nil
}
func NotifyUserChange(ctx echo.Context, tx *pop.Connection, persister persistence.Persister, event events.Event, userId uuid.UUID) {
updatedUser, err := persister.GetUserPersisterWithConnection(tx).Get(userId)
if err != nil {
ctx.Logger().Warn(fmt.Errorf("failed to fetch updated user: %w", err))
return
}
err = TriggerWebhooks(ctx, event, admin.FromUserModel(*updatedUser))
if err != nil {
ctx.Logger().Warn(err)
}
}

View File

@ -28,7 +28,7 @@ func TestWebhook_TriggerWithoutManager(t *testing.T) {
ctx := e.NewContext(req, rec)
err := TriggerWebhooks(ctx, events.User, "lorem")
err := TriggerWebhooks(ctx, "user", "lorem")
require.Error(t, err)
err = e.Close()
@ -47,7 +47,7 @@ func TestWebhook_Trigger(t *testing.T) {
ctx := e.NewContext(req, rec)
ctx.Set("webhook_manager", tm)
err := TriggerWebhooks(ctx, events.User, "lorem")
err := TriggerWebhooks(ctx, "user", "lorem")
require.NoError(t, err)
err = e.Close()

View File

@ -14,17 +14,17 @@ func TestBaseWebhook_HasEvent(t *testing.T) {
baseHook := BaseWebhook{
Logger: nil,
Callback: "http://ipsum.lorem",
Events: events.Events{events.User},
Events: events.Events{events.UserUpdate},
}
require.True(t, baseHook.HasEvent(events.User))
require.True(t, baseHook.HasEvent(events.EmailCreate))
}
func TestBaseWebhook_HasSubEvent(t *testing.T) {
baseHook := BaseWebhook{
Logger: nil,
Callback: "http://ipsum.lorem",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
require.True(t, baseHook.HasEvent(events.UserCreate))
@ -37,7 +37,7 @@ func TestBaseWebhook_DoesNotHaveEvent(t *testing.T) {
Events: events.Events{events.UserCreate},
}
require.False(t, baseHook.HasEvent(events.User))
require.False(t, baseHook.HasEvent("user"))
}
func TestBaseWebhook_Trigger(t *testing.T) {
@ -49,12 +49,12 @@ func TestBaseWebhook_Trigger(t *testing.T) {
baseHook := BaseWebhook{
Logger: nil,
Callback: server.URL,
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
data := JobData{
Token: "test-token",
Event: events.User,
Event: "user",
}
err := baseHook.Trigger(data)
@ -65,12 +65,12 @@ func TestBaseWebhook_TriggerWithWrongUrl(t *testing.T) {
baseHook := BaseWebhook{
Logger: log.New("test"),
Callback: "http://broken!",
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
data := JobData{
Token: "test-token",
Event: events.User,
Event: "user",
}
err := baseHook.Trigger(data)
@ -87,12 +87,12 @@ func TestBaseWebhook_TriggerWithBadStatusCode(t *testing.T) {
baseHook := BaseWebhook{
Logger: log.New("test"),
Callback: server.URL,
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
data := JobData{
Token: "test-token",
Event: events.User,
Event: "user",
}
err := baseHook.Trigger(data)
@ -112,12 +112,12 @@ func TestBaseWebhook_TriggerWithBadServer(t *testing.T) {
baseHook := BaseWebhook{
Logger: log.New("test"),
Callback: server.URL,
Events: events.Events{events.User},
Events: events.Events{events.UserCreate},
}
data := JobData{
Token: "test-token",
Event: events.User,
Event: "user",
}
err := baseHook.Trigger(data)

View File

@ -66,7 +66,7 @@ func TestWorker_RunJob(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -101,7 +101,7 @@ func TestWorker_RunJobWithError(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -126,7 +126,7 @@ func TestWorker_TriggerWebhook(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -162,7 +162,7 @@ func TestWorker_TriggerWebhookWithExpireError(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -182,7 +182,7 @@ func TestWorker_TriggerWebhookIgnoreDisabledJob(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -206,7 +206,7 @@ func TestWorker_TriggerWebhookTriggerWithError(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -238,7 +238,7 @@ func TestWorker_TriggerWebhookDisableOnFailure(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{
@ -269,7 +269,7 @@ func TestWorker_TriggerWebhookResetError(t *testing.T) {
job := Job{
Data: JobData{
Token: "test-token",
Event: events.User,
Event: events.UserCreate,
},
Hook: &TestHook{