Kaynağa Gözat

finish create user endpoint

Alexander Belanger 4 yıl önce
ebeveyn
işleme
9c103f8129

+ 95 - 0
api/server/handlers/user/create.go

@@ -0,0 +1,95 @@
+package user
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type UserCreateHandler struct {
+	config           *shared.Config
+	decoderValidator shared.RequestDecoderValidator
+	writer           shared.ResultWriter
+}
+
+func NewUserCreateHandler(
+	config *shared.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UserCreateHandler {
+	return &UserCreateHandler{config, decoderValidator, writer}
+}
+
+func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	request := &types.CreateUserRequest{}
+
+	ok := u.decoderValidator.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	user := &models.User{
+		Email:    request.Email,
+		Password: request.Password,
+	}
+
+	// check if user exists
+	doesExist := doesUserExist(u.config.Repo, user)
+
+	if doesExist {
+		apierrors.HandleAPIError(
+			w,
+			u.config.Logger,
+			apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("email already taken"),
+				http.StatusBadRequest,
+			),
+		)
+		return
+	}
+
+	// write the user to the db
+	user, err := u.config.Repo.User().CreateUser(user)
+
+	if err != nil {
+		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	session, err := u.config.Store.Get(r, u.config.CookieName)
+
+	if err != nil {
+		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var redirect string
+
+	if valR := session.Values["redirect"]; valR != nil {
+		redirect = session.Values["redirect"].(string)
+	}
+
+	session.Values["authenticated"] = true
+	session.Values["user_id"] = user.ID
+	session.Values["email"] = user.Email
+	session.Values["redirect"] = redirect
+
+	if err := session.Save(r, w); err != nil {
+		apierrors.HandleAPIError(w, u.config.Logger, apierrors.NewErrInternal(err))
+		return
+	}
+
+	u.writer.WriteResult(w, user.ToUserType())
+}
+
+func doesUserExist(repo repository.Repository, user *models.User) bool {
+	user, err := repo.User().ReadUserByEmail(user.Email)
+
+	return user != nil && err == nil
+}

+ 173 - 0
api/server/handlers/user/create_test.go

@@ -0,0 +1,173 @@
+package user_test
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/porter-dev/porter/api/server/handlers/user"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/repository/test"
+)
+
+func TestCreateUserSuccessful(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users",
+		&types.CreateUserRequest{
+			Email:    "test@test.it",
+			Password: "somepassword",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+
+	handler := user.NewUserCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	expUser := &types.CreateUserResponse{
+		ID:            1,
+		Email:         "test@test.it",
+		EmailVerified: false,
+	}
+
+	gotUser := &types.CreateUserResponse{}
+
+	apitest.AssertResponseExpected(t, rr, expUser, gotUser)
+}
+
+func TestCreateUserBadEmail(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users",
+		&types.CreateUserRequest{
+			Email:    "notanemail",
+			Password: "somepassword",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+
+	handler := user.NewUserCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseError(t, rr, http.StatusBadRequest, &types.ExternalError{
+		Error: fmt.Sprintf("validation failed on field 'Email' on condition 'email'"),
+	})
+}
+
+func TestCreateUserMissingField(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users",
+		&types.CreateUserRequest{
+			Email: "test@test.it",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+
+	handler := user.NewUserCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseError(t, rr, http.StatusBadRequest, &types.ExternalError{
+		Error: fmt.Sprintf("validation failed on field 'Password' on condition 'required'"),
+	})
+}
+
+func TestCreateUserSameEmail(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users",
+		&types.CreateUserRequest{
+			Email:    "test@test.it",
+			Password: "somepassword",
+		},
+	)
+
+	config := apitest.LoadConfig(t)
+
+	// create the existing user
+	apitest.CreateTestUser(t, config)
+
+	handler := user.NewUserCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseError(t, rr, http.StatusBadRequest, &types.ExternalError{
+		Error: fmt.Sprintf("email already taken"),
+	})
+}
+
+func TestFailingCreateUserMethod(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users",
+		&types.CreateUserRequest{
+			Email:    "test@test.it",
+			Password: "somepassword",
+		},
+	)
+
+	config := apitest.LoadConfig(t, test.CreateUserMethod)
+
+	handler := user.NewUserCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseInternalServerError(t, rr)
+}
+
+func TestFailingCreateSessionMethod(t *testing.T) {
+	req, rr := apitest.GetRequestAndRecorder(
+		t,
+		string(types.HTTPVerbPost),
+		"/api/users",
+		&types.CreateUserRequest{
+			Email:    "test@test.it",
+			Password: "somepassword",
+		},
+	)
+
+	config := apitest.LoadConfig(t, test.CreateSessionMethod)
+
+	handler := user.NewUserCreateHandler(
+		config,
+		shared.NewDefaultRequestDecoderValidator(config),
+		shared.NewDefaultResultWriter(config),
+	)
+
+	handler.ServeHTTP(rr, req)
+
+	apitest.AssertResponseInternalServerError(t, rr)
+}

+ 1 - 1
api/server/shared/apitest/response.go

@@ -66,5 +66,5 @@ func AssertResponseError(t *testing.T, rr *httptest.ResponseRecorder, statusCode
 	}
 
 	assert.Equal(t, statusCode, rr.Result().StatusCode, "status code should match")
-	assert.Equal(t, expReqErr, reqErr, "body should be internal server error")
+	assert.Equal(t, expReqErr, reqErr, "body should be matching error")
 }

+ 10 - 2
api/types/user.go

@@ -1,6 +1,14 @@
 package types
 
 type User struct {
-	ID    uint   `json:"id"`
-	Email string `json:"email"`
+	ID            uint   `json:"id"`
+	Email         string `json:"email"`
+	EmailVerified bool   `json:"email_verified"`
 }
+
+type CreateUserRequest struct {
+	Email    string `json:"email" form:"required,max=255,email"`
+	Password string `json:"password" form:"required,max=255"`
+}
+
+type CreateUserResponse User

+ 3 - 0
internal/auth/sessionstore/sessionstore.go

@@ -172,6 +172,9 @@ func (store *PGStore) New(r *http.Request, name string) (*sessions.Session, erro
 				} else if strings.Contains(err.Error(), "expired timestamp") {
 					err = nil
 					session.IsNew = false
+				} else {
+					// if error is not record not found, db read is failing, so return fatal err
+					return nil, err
 				}
 			} else {
 				session.IsNew = false

+ 10 - 0
internal/models/user.go

@@ -1,6 +1,7 @@
 package models
 
 import (
+	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 )
 
@@ -32,3 +33,12 @@ func (u *User) Externalize() *UserExternal {
 		EmailVerified: u.EmailVerified,
 	}
 }
+
+// ToUserType generates an external types.User to be shared over REST
+func (u *User) ToUserType() *types.User {
+	return &types.User{
+		ID:            u.ID,
+		Email:         u.Email,
+		EmailVerified: u.EmailVerified,
+	}
+}

+ 2 - 2
internal/repository/test/repository.go

@@ -111,8 +111,8 @@ func (t *TestRepository) AWSIntegration() repository.AWSIntegrationRepository {
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
 	return &TestRepository{
-		user:             NewUserRepository(canQuery),
-		session:          NewSessionRepository(canQuery),
+		user:             NewUserRepository(canQuery, failingMethods...),
+		session:          NewSessionRepository(canQuery, failingMethods...),
 		project:          NewProjectRepository(canQuery, failingMethods...),
 		cluster:          NewClusterRepository(canQuery),
 		helmRepo:         NewHelmRepoRepository(canQuery),

+ 13 - 6
internal/repository/test/session.go

@@ -2,26 +2,33 @@ package test
 
 import (
 	"errors"
+	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 )
 
+const (
+	CreateSessionMethod string = "create_session_0"
+	SelectSessionMethod string = "select_session_0"
+)
+
 // SessionRepository uses gorm.DB for querying the database
 type SessionRepository struct {
-	canQuery bool
-	sessions []*models.Session
+	canQuery       bool
+	failingMethods string
+	sessions       []*models.Session
 }
 
 // NewSessionRepository returns pointer to repo along with the db
-func NewSessionRepository(canQuery bool) repository.SessionRepository {
-	return &SessionRepository{canQuery, []*models.Session{}}
+func NewSessionRepository(canQuery bool, failingMethods ...string) repository.SessionRepository {
+	return &SessionRepository{canQuery, strings.Join(failingMethods, ","), []*models.Session{}}
 }
 
 // CreateSession must take in Key, Data, and ExpiresAt as arguments.
 func (repo *SessionRepository) CreateSession(session *models.Session) (*models.Session, error) {
-	if !repo.canQuery {
+	if !repo.canQuery || strings.Contains(repo.failingMethods, CreateSessionMethod) {
 		return nil, errors.New("Cannot write database")
 	}
 
@@ -81,7 +88,7 @@ func (repo *SessionRepository) DeleteSession(session *models.Session) (*models.S
 
 // SelectSession returns a session with matching key
 func (repo *SessionRepository) SelectSession(session *models.Session) (*models.Session, error) {
-	if !repo.canQuery {
+	if !repo.canQuery || strings.Contains(repo.failingMethods, SelectSessionMethod) {
 		return nil, errors.New("Cannot write database")
 	}
 

+ 11 - 5
internal/repository/test/user.go

@@ -2,6 +2,7 @@ package test
 
 import (
 	"errors"
+	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -9,22 +10,27 @@ import (
 	"gorm.io/gorm"
 )
 
+const (
+	CreateUserMethod string = "create_user_0"
+)
+
 // UserRepository will return errors on queries if canQuery is false
 // and only stores a small set of users in-memory that are indexed by their
 // array index + 1
 type UserRepository struct {
-	canQuery bool
-	users    []*models.User
+	canQuery       bool
+	failingMethods string
+	users          []*models.User
 }
 
 // NewUserRepository will return errors if canQuery is false
-func NewUserRepository(canQuery bool) repository.UserRepository {
-	return &UserRepository{canQuery, []*models.User{}}
+func NewUserRepository(canQuery bool, failingMethods ...string) repository.UserRepository {
+	return &UserRepository{canQuery, strings.Join(failingMethods, ","), []*models.User{}}
 }
 
 // CreateUser adds a new User row to the Users table in array memory
 func (repo *UserRepository) CreateUser(user *models.User) (*models.User, error) {
-	if !repo.canQuery {
+	if !repo.canQuery || strings.Contains(repo.failingMethods, CreateUserMethod) {
 		return nil, errors.New("Cannot write database")
 	}