feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Delegates all credential management (login, register, password reset, email verification, social sign-in) to Ory Kratos. The Go API now acts as a resource server: the new KratosAuth middleware validates sessions against the Kratos whoami endpoint, writes the local User mirror into Echo context, and all existing domain handlers continue working unchanged. Hand-rolled token auth, AuthToken model, apple_auth/ google_auth services, and the auth refresh flow are removed. Tests are updated to use the fake-token middleware pattern so existing integration assertions require no rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
// auth_handler_test.go tests the auth handler endpoints that survived the
|
||||
// Ory Kratos migration: GET /me/ and PUT/PATCH /profile/.
|
||||
// Login, register, logout, forgot-password, and social sign-in are now
|
||||
// handled by Kratos.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@@ -34,204 +38,32 @@ func setupAuthHandler(t *testing.T) (*AuthHandler, *echo.Echo, *repositories.Use
|
||||
return handler, e, userRepo
|
||||
}
|
||||
|
||||
func TestAuthHandler_Register(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
|
||||
t.Run("successful registration", func(t *testing.T) {
|
||||
req := requests.RegisterRequest{
|
||||
Username: "newuser",
|
||||
Email: "new@test.com",
|
||||
Password: "Password123",
|
||||
FirstName: "New",
|
||||
LastName: "User",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.AssertJSONFieldExists(t, response, "token")
|
||||
testutil.AssertJSONFieldExists(t, response, "user")
|
||||
testutil.AssertJSONFieldExists(t, response, "message")
|
||||
|
||||
user := response["user"].(map[string]interface{})
|
||||
assert.Equal(t, "newuser", user["username"])
|
||||
assert.Equal(t, "new@test.com", user["email"])
|
||||
assert.Equal(t, "New", user["first_name"])
|
||||
assert.Equal(t, "User", user["last_name"])
|
||||
})
|
||||
|
||||
t.Run("registration with missing fields", func(t *testing.T) {
|
||||
req := map[string]string{
|
||||
"username": "test",
|
||||
// Missing email and password
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
testutil.AssertJSONFieldExists(t, response, "error")
|
||||
})
|
||||
|
||||
t.Run("registration with short password", func(t *testing.T) {
|
||||
req := requests.RegisterRequest{
|
||||
Username: "testuser",
|
||||
Email: "test@test.com",
|
||||
Password: "short", // Less than 8 chars
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("registration with duplicate username", func(t *testing.T) {
|
||||
// First registration
|
||||
req := requests.RegisterRequest{
|
||||
Username: "duplicate",
|
||||
Email: "unique1@test.com",
|
||||
Password: "Password123",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
// Try to register again with same username
|
||||
req.Email = "unique2@test.com"
|
||||
w = testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusConflict) // 409 for duplicate resource
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["error"], "Username already taken")
|
||||
})
|
||||
|
||||
t.Run("registration with duplicate email", func(t *testing.T) {
|
||||
// First registration
|
||||
req := requests.RegisterRequest{
|
||||
Username: "user1",
|
||||
Email: "duplicate@test.com",
|
||||
Password: "Password123",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
// Try to register again with same email
|
||||
req.Username = "user2"
|
||||
w = testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusConflict) // 409 for duplicate resource
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["error"], "Email already registered")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/login/", handler.Login)
|
||||
|
||||
// Create a test user
|
||||
registerReq := requests.RegisterRequest{
|
||||
Username: "logintest",
|
||||
Email: "login@test.com",
|
||||
Password: "Password123",
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", registerReq, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
t.Run("successful login with username", func(t *testing.T) {
|
||||
req := requests.LoginRequest{
|
||||
Username: "logintest",
|
||||
Password: "Password123",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.AssertJSONFieldExists(t, response, "token")
|
||||
testutil.AssertJSONFieldExists(t, response, "user")
|
||||
|
||||
user := response["user"].(map[string]interface{})
|
||||
assert.Equal(t, "logintest", user["username"])
|
||||
assert.Equal(t, "login@test.com", user["email"])
|
||||
})
|
||||
|
||||
t.Run("successful login with email", func(t *testing.T) {
|
||||
req := requests.LoginRequest{
|
||||
Username: "login@test.com", // Using email as username
|
||||
Password: "Password123",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("login with wrong password", func(t *testing.T) {
|
||||
req := requests.LoginRequest{
|
||||
Username: "logintest",
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["error"], "Invalid credentials")
|
||||
})
|
||||
|
||||
t.Run("login with non-existent user", func(t *testing.T) {
|
||||
req := requests.LoginRequest{
|
||||
Username: "nonexistent",
|
||||
Password: "Password123",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("login with missing fields", func(t *testing.T) {
|
||||
req := map[string]string{
|
||||
"username": "logintest",
|
||||
// Missing password
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_CurrentUser(t *testing.T) {
|
||||
handler, e, userRepo := setupAuthHandler(t)
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "metest", "me@test.com", "Password123")
|
||||
user := testutil.CreateTestUser(t, db, "metest", "me@test.com", "")
|
||||
user.FirstName = "Test"
|
||||
user.LastName = "User"
|
||||
userRepo.Update(user)
|
||||
// Use the userRepo from setupAuthHandler's DB, but since we need the user
|
||||
// in the same DB we re-create it there.
|
||||
db2 := testutil.SetupTestDB(t)
|
||||
user2 := testutil.CreateTestUser(t, db2, "metest2", "me2@test.com", "")
|
||||
user2.FirstName = "Test"
|
||||
user2.LastName = "User"
|
||||
userRepo2 := repositories.NewUserRepository(db2)
|
||||
require.NoError(t, userRepo2.Update(user2))
|
||||
|
||||
// Build handler against db2
|
||||
cfg := &config.Config{}
|
||||
authService2 := services.NewAuthService(userRepo2, cfg)
|
||||
handler2 := NewAuthHandler(authService2, nil, nil)
|
||||
|
||||
// Set up route with mock auth middleware
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/me/", handler.CurrentUser)
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user2))
|
||||
authGroup.GET("/me/", handler2.CurrentUser)
|
||||
|
||||
_ = handler // avoid unused
|
||||
|
||||
t.Run("get current user", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(e, "GET", "/api/auth/me/", nil, "test-token")
|
||||
@@ -242,23 +74,26 @@ func TestAuthHandler_CurrentUser(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "metest", response["username"])
|
||||
assert.Equal(t, "me@test.com", response["email"])
|
||||
assert.Equal(t, "metest2", response["username"])
|
||||
assert.Equal(t, "me2@test.com", response["email"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_UpdateProfile(t *testing.T) {
|
||||
handler, e, userRepo := setupAuthHandler(t)
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "updatetest", "update@test.com", "Password123")
|
||||
userRepo.Update(user)
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
cfg := &config.Config{}
|
||||
authService := services.NewAuthService(userRepo, cfg)
|
||||
handler := NewAuthHandler(authService, nil, nil)
|
||||
e := testutil.SetupTestRouter()
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "updatetest", "update@test.com", "")
|
||||
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.PUT("/profile/", handler.UpdateProfile)
|
||||
|
||||
t.Run("update profile", func(t *testing.T) {
|
||||
t.Run("update first and last name", func(t *testing.T) {
|
||||
firstName := "Updated"
|
||||
lastName := "Name"
|
||||
req := requests.UpdateProfileRequest{
|
||||
@@ -278,130 +113,3 @@ func TestAuthHandler_UpdateProfile(t *testing.T) {
|
||||
assert.Equal(t, "Name", response["last_name"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_ForgotPassword(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/forgot-password/", handler.ForgotPassword)
|
||||
|
||||
// Create a test user
|
||||
registerReq := requests.RegisterRequest{
|
||||
Username: "forgottest",
|
||||
Email: "forgot@test.com",
|
||||
Password: "Password123",
|
||||
}
|
||||
testutil.MakeRequest(e, "POST", "/api/auth/register/", registerReq, "")
|
||||
|
||||
t.Run("forgot password with valid email", func(t *testing.T) {
|
||||
req := requests.ForgotPasswordRequest{
|
||||
Email: "forgot@test.com",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/forgot-password/", req, "")
|
||||
|
||||
// Always returns 200 to prevent email enumeration
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
testutil.AssertJSONFieldExists(t, response, "message")
|
||||
})
|
||||
|
||||
t.Run("forgot password with invalid email", func(t *testing.T) {
|
||||
req := requests.ForgotPasswordRequest{
|
||||
Email: "nonexistent@test.com",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/forgot-password/", req, "")
|
||||
|
||||
// Still returns 200 to prevent email enumeration
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_Logout(t *testing.T) {
|
||||
handler, e, userRepo := setupAuthHandler(t)
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "logouttest", "logout@test.com", "Password123")
|
||||
userRepo.Update(user)
|
||||
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/logout/", handler.Logout)
|
||||
|
||||
t.Run("successful logout", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/logout/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["message"], "Logged out successfully")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_JSONResponses(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/login/", handler.Login)
|
||||
|
||||
t.Run("register response has correct JSON structure", func(t *testing.T) {
|
||||
req := requests.RegisterRequest{
|
||||
Username: "jsontest",
|
||||
Email: "json@test.com",
|
||||
Password: "Password123",
|
||||
FirstName: "JSON",
|
||||
LastName: "Test",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify top-level structure
|
||||
assert.Contains(t, response, "token")
|
||||
assert.Contains(t, response, "user")
|
||||
assert.Contains(t, response, "message")
|
||||
|
||||
// Verify token is not empty
|
||||
assert.NotEmpty(t, response["token"])
|
||||
|
||||
// Verify user structure
|
||||
user := response["user"].(map[string]interface{})
|
||||
assert.Contains(t, user, "id")
|
||||
assert.Contains(t, user, "username")
|
||||
assert.Contains(t, user, "email")
|
||||
assert.Contains(t, user, "first_name")
|
||||
assert.Contains(t, user, "last_name")
|
||||
assert.Contains(t, user, "is_active")
|
||||
assert.Contains(t, user, "date_joined")
|
||||
|
||||
// Verify types
|
||||
assert.IsType(t, float64(0), user["id"]) // JSON numbers are float64
|
||||
assert.IsType(t, "", user["username"])
|
||||
assert.IsType(t, "", user["email"])
|
||||
assert.IsType(t, true, user["is_active"])
|
||||
})
|
||||
|
||||
t.Run("error response has correct JSON structure", func(t *testing.T) {
|
||||
req := map[string]string{
|
||||
"username": "test",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response, "error")
|
||||
assert.IsType(t, "", response["error"])
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user