(Feat): Initial Commit
This commit is contained in:
334
internal/api/handlers/auth/post_register_test.go
Normal file
334
internal/api/handlers/auth/post_register_test.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/db"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/url"
|
||||
"github.com/aarondl/null/v8"
|
||||
"github.com/aarondl/sqlboiler/v4/queries/qm"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostRegisterSuccess(t *testing.T) {
|
||||
test.WithTestServer(t, func(s *api.Server) {
|
||||
ctx := t.Context()
|
||||
|
||||
now := time.Date(2025, 2, 5, 11, 42, 30, 0, time.UTC)
|
||||
test.SetMockClock(t, s, now)
|
||||
|
||||
username := "usernew@example.com"
|
||||
payload := test.GenericPayload{
|
||||
"username": username,
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
}
|
||||
|
||||
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||
|
||||
var response types.PostLoginResponse
|
||||
test.ParseResponseAndValidate(t, res, &response)
|
||||
|
||||
assert.NotEmpty(t, response.AccessToken)
|
||||
assert.NotEmpty(t, response.RefreshToken)
|
||||
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||
|
||||
user, err := models.Users(
|
||||
models.UserWhere.Username.EQ(null.StringFrom(username)),
|
||||
qm.Load(models.UserRels.AppUserProfile),
|
||||
qm.Load(models.UserRels.AccessTokens),
|
||||
qm.Load(models.UserRels.RefreshTokens),
|
||||
).One(ctx, s.DB)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(username), user.Username)
|
||||
assert.True(t, user.LastAuthenticatedAt.Valid)
|
||||
assert.Equal(t, now, user.LastAuthenticatedAt.Time)
|
||||
assert.EqualValues(t, s.Config.Auth.DefaultUserScopes, user.Scopes)
|
||||
|
||||
assert.NotNil(t, user.R.AppUserProfile)
|
||||
assert.False(t, user.R.AppUserProfile.LegalAcceptedAt.Valid)
|
||||
|
||||
assert.Len(t, user.R.AccessTokens, 1)
|
||||
assert.Equal(t, strfmt.UUID4(user.R.AccessTokens[0].Token), *response.AccessToken)
|
||||
assert.Len(t, user.R.RefreshTokens, 1)
|
||||
assert.Equal(t, strfmt.UUID4(user.R.RefreshTokens[0].Token), *response.RefreshToken)
|
||||
|
||||
res2 := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||
assert.Equal(t, http.StatusOK, res2.Result().StatusCode)
|
||||
|
||||
var response2 types.PostLoginResponse
|
||||
test.ParseResponseAndValidate(t, res2, &response2)
|
||||
|
||||
assert.NotEmpty(t, response2.AccessToken)
|
||||
assert.NotEqual(t, response.AccessToken, *response2.AccessToken)
|
||||
assert.NotEmpty(t, response2.RefreshToken)
|
||||
assert.NotEqual(t, response.RefreshToken, *response2.RefreshToken)
|
||||
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response2.ExpiresIn)
|
||||
assert.Equal(t, auth.TokenTypeBearer, *response2.TokenType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRegisterWithConfirmationSuccess(t *testing.T) {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
config.Auth.RegistrationRequiresConfirmation = true
|
||||
|
||||
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||
ctx := t.Context()
|
||||
|
||||
username := "usernew-with-confirmation@example.com"
|
||||
payload := test.GenericPayload{
|
||||
"username": username,
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
}
|
||||
|
||||
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||
|
||||
require.Equal(t, http.StatusAccepted, res.Result().StatusCode)
|
||||
|
||||
var response types.RegisterResponse
|
||||
test.ParseResponseAndValidate(t, res, &response)
|
||||
|
||||
assert.True(t, swag.BoolValue(response.RequiresConfirmation))
|
||||
|
||||
// expect the user to be normally created
|
||||
user, err := models.Users(
|
||||
models.UserWhere.Username.EQ(null.StringFrom(username)),
|
||||
qm.Load(models.UserRels.AppUserProfile),
|
||||
qm.Load(models.UserRels.AccessTokens),
|
||||
qm.Load(models.UserRels.RefreshTokens),
|
||||
qm.Load(models.UserRels.ConfirmationTokens),
|
||||
).One(ctx, s.DB)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(username), user.Username)
|
||||
assert.True(t, user.LastAuthenticatedAt.Valid)
|
||||
assert.EqualValues(t, s.Config.Auth.DefaultUserScopes, user.Scopes)
|
||||
assert.False(t, user.IsActive)
|
||||
assert.True(t, user.RequiresConfirmation)
|
||||
|
||||
assert.NotNil(t, user.R.AppUserProfile)
|
||||
assert.False(t, user.R.AppUserProfile.LegalAcceptedAt.Valid)
|
||||
|
||||
// expect the user to have no access or refresh tokens
|
||||
assert.Empty(t, user.R.AccessTokens)
|
||||
assert.Empty(t, user.R.RefreshTokens)
|
||||
require.Len(t, user.R.ConfirmationTokens, 1)
|
||||
confirmationToken := user.R.ConfirmationTokens[0]
|
||||
|
||||
// expect the login to fail
|
||||
res2 := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||
test.RequireHTTPError(t, res2, httperrors.ErrForbiddenUserDeactivated)
|
||||
|
||||
// expect the confirmation email to be sent
|
||||
mails := test.GetSentMails(t, s.Mailer)
|
||||
require.Len(t, mails, 1)
|
||||
|
||||
mail := mails[0]
|
||||
expectedConfirmationLink, err := url.ConfirmationDeeplinkURL(s.Config, confirmationToken.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, username, mail.To[0])
|
||||
assert.Contains(t, string(mail.HTML), expectedConfirmationLink.String())
|
||||
|
||||
// directly register again should trigger the debounce
|
||||
// and not create a new confirmation token
|
||||
registerAgainRes := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||
require.Equal(t, http.StatusAccepted, registerAgainRes.Result().StatusCode)
|
||||
|
||||
var registerAgainResponse types.RegisterResponse
|
||||
test.ParseResponseAndValidate(t, registerAgainRes, ®isterAgainResponse)
|
||||
|
||||
// expect the confirmation to be required
|
||||
assert.True(t, swag.BoolValue(registerAgainResponse.RequiresConfirmation))
|
||||
|
||||
confirmationTokenCount, err := user.ConfirmationTokens().Count(ctx, s.DB)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 1, confirmationTokenCount)
|
||||
|
||||
// register later again
|
||||
test.SetMockClock(t, s, s.Clock.Now().Add(config.Auth.ConfirmationTokenDebounceDuration+time.Second))
|
||||
|
||||
registerLaterAgainRes := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||
require.Equal(t, http.StatusAccepted, registerLaterAgainRes.Result().StatusCode)
|
||||
|
||||
var registerLaterAgainResponse types.RegisterResponse
|
||||
test.ParseResponseAndValidate(t, registerLaterAgainRes, ®isterLaterAgainResponse)
|
||||
assert.True(t, swag.BoolValue(registerLaterAgainResponse.RequiresConfirmation))
|
||||
|
||||
confirmationTokens, err := models.ConfirmationTokens(
|
||||
models.ConfirmationTokenWhere.UserID.EQ(user.ID),
|
||||
db.OrderBy(types.OrderDirDesc, models.ConfirmationTokenColumns.CreatedAt),
|
||||
).All(ctx, s.DB)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, confirmationTokens, 2)
|
||||
|
||||
lastSentMail := test.GetLastSentMail(t, s.Mailer)
|
||||
require.NotNil(t, lastSentMail)
|
||||
|
||||
expectedConfirmationLink, err = url.ConfirmationDeeplinkURL(s.Config, confirmationTokens[0].Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, username, lastSentMail.To[0])
|
||||
assert.Contains(t, string(lastSentMail.HTML), expectedConfirmationLink.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRegisterSuccessLowercaseTrimWhitespaces(t *testing.T) {
|
||||
test.WithTestServer(t, func(s *api.Server) {
|
||||
ctx := t.Context()
|
||||
|
||||
username := " USERNEW@example.com "
|
||||
usernameLowerTrimmed := "usernew@example.com"
|
||||
payload := test.GenericPayload{
|
||||
"username": username,
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
"name": "Trim Whitespaces",
|
||||
}
|
||||
|
||||
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||
|
||||
var response types.PostLoginResponse
|
||||
test.ParseResponseAndValidate(t, res, &response)
|
||||
|
||||
assert.NotEmpty(t, response.AccessToken)
|
||||
assert.NotEmpty(t, response.RefreshToken)
|
||||
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||
|
||||
user, err := models.Users(
|
||||
models.UserWhere.Username.EQ(null.StringFrom(usernameLowerTrimmed)),
|
||||
qm.Load(models.UserRels.AppUserProfile),
|
||||
qm.Load(models.UserRels.AccessTokens),
|
||||
qm.Load(models.UserRels.RefreshTokens),
|
||||
).One(ctx, s.DB)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(usernameLowerTrimmed), user.Username)
|
||||
assert.True(t, user.LastAuthenticatedAt.Valid)
|
||||
assert.WithinDuration(t, s.Clock.Now(), user.LastAuthenticatedAt.Time, time.Second*10)
|
||||
assert.EqualValues(t, s.Config.Auth.DefaultUserScopes, user.Scopes)
|
||||
|
||||
assert.NotNil(t, user.R.AppUserProfile)
|
||||
assert.False(t, user.R.AppUserProfile.LegalAcceptedAt.Valid)
|
||||
|
||||
assert.Len(t, user.R.AccessTokens, 1)
|
||||
assert.Equal(t, strfmt.UUID4(user.R.AccessTokens[0].Token), *response.AccessToken)
|
||||
assert.Len(t, user.R.RefreshTokens, 1)
|
||||
assert.Equal(t, strfmt.UUID4(user.R.RefreshTokens[0].Token), *response.RefreshToken)
|
||||
|
||||
res2 := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||
assert.Equal(t, http.StatusOK, res2.Result().StatusCode)
|
||||
|
||||
var response2 types.PostLoginResponse
|
||||
test.ParseResponseAndValidate(t, res2, &response2)
|
||||
|
||||
assert.NotEmpty(t, response2.AccessToken)
|
||||
assert.NotEqual(t, response.AccessToken, *response2.AccessToken)
|
||||
assert.NotEmpty(t, response2.RefreshToken)
|
||||
assert.NotEqual(t, response.RefreshToken, *response2.RefreshToken)
|
||||
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response2.ExpiresIn)
|
||||
assert.Equal(t, auth.TokenTypeBearer, *response2.TokenType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRegisterAlreadyExists(t *testing.T) {
|
||||
test.WithTestServer(t, func(s *api.Server) {
|
||||
ctx := t.Context()
|
||||
|
||||
fix := fixtures.Fixtures()
|
||||
payload := test.GenericPayload{
|
||||
"username": fix.User1.Username,
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
}
|
||||
|
||||
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||
test.RequireHTTPError(t, res, httperrors.ErrConflictUserAlreadyExists)
|
||||
|
||||
user, err := models.Users(
|
||||
models.UserWhere.Username.EQ(fix.User1.Username),
|
||||
qm.Load(models.UserRels.AppUserProfile),
|
||||
qm.Load(models.UserRels.AccessTokens),
|
||||
qm.Load(models.UserRels.RefreshTokens),
|
||||
).One(ctx, s.DB)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, fix.User1.ID)
|
||||
|
||||
assert.NotNil(t, user.R.AppUserProfile)
|
||||
assert.Len(t, user.R.AccessTokens, 1)
|
||||
assert.Len(t, user.R.RefreshTokens, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostRegisterBadRequest(t *testing.T) {
|
||||
test.WithTestServer(t, func(s *api.Server) {
|
||||
fix := fixtures.Fixtures()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload test.GenericPayload
|
||||
}{
|
||||
{
|
||||
name: "MissingUsername",
|
||||
payload: test.GenericPayload{
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MissingPassword",
|
||||
payload: test.GenericPayload{
|
||||
"username": fix.User1.Username,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidUsername",
|
||||
payload: test.GenericPayload{
|
||||
"username": "definitely not an email",
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyUsername",
|
||||
payload: test.GenericPayload{
|
||||
"username": "",
|
||||
"password": fixtures.PlainTestUserPassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyPassword",
|
||||
payload: test.GenericPayload{
|
||||
"username": fix.User1.Username,
|
||||
"password": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", tt.payload, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||
|
||||
var response httperrors.HTTPValidationError
|
||||
test.ParseResponseAndValidate(t, res, &response)
|
||||
|
||||
test.Snapshoter.Save(t, response)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user