diff --git a/apps/server/go.mod b/apps/server/go.mod index 3a7c049..239eb16 100644 --- a/apps/server/go.mod +++ b/apps/server/go.mod @@ -7,10 +7,10 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.8.0 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo-jwt/v5 v5.0.0 - github.com/labstack/echo/v5 v5.0.3 + github.com/labstack/echo-jwt/v5 v5.0.1 + github.com/labstack/echo/v5 v5.1.0 github.com/stretchr/testify v1.11.1 - github.com/swaggo/echo-swagger v1.5.0 + github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.6 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.47.0 @@ -21,6 +21,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect @@ -31,17 +32,14 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/labstack/echo-jwt/v4 v4.3.0 // indirect - github.com/labstack/echo/v4 v4.13.3 // indirect - github.com/labstack/gommon v0.4.2 // indirect + github.com/labstack/echo/v4 v4.9.0 // indirect + github.com/labstack/gommon v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sv-tools/openapi v0.2.1 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/swag/v2 v2.0.0-rc4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -56,5 +54,4 @@ require ( golang.org/x/tools v0.40.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/apps/server/go.sum b/apps/server/go.sum index 1600153..1fbdf11 100644 --- a/apps/server/go.sum +++ b/apps/server/go.sum @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -74,8 +76,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -91,10 +91,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= -github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= -github.com/swaggo/echo-swagger v1.5.0 h1:nkHxOaBy0SkbJMtMeXZC64KHSa0mJdZFQhVqwEcMres= -github.com/swaggo/echo-swagger v1.5.0/go.mod h1:TzO363X1ZG/MSbjrG2IX6m65Yd3/zpqh5KM6lPctAhk= +github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= +github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= @@ -145,7 +143,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/apps/server/internal/modules/auth/handler_test.go b/apps/server/internal/modules/auth/handler_test.go index 761c36b..c4c38c2 100644 --- a/apps/server/internal/modules/auth/handler_test.go +++ b/apps/server/internal/modules/auth/handler_test.go @@ -9,8 +9,30 @@ import ( "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "testing" + + "github.com/coderz-space/coderz.space/internal/common/middleware/auth" + "github.com/coderz-space/coderz.space/internal/common/utils" + "github.com/coderz-space/coderz.space/internal/config" + db "github.com/coderz-space/coderz.space/internal/db/sqlc" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" ) +// MockQuerier implements db.Querier for testing +type MockQuerier struct { + db.Querier + GetUserByIdFunc func(ctx context.Context, id pgtype.UUID) (db.User, error) +} + +func (m *MockQuerier) GetUserById(ctx context.Context, id pgtype.UUID) (db.User, error) { + if m.GetUserByIdFunc != nil { + return m.GetUserByIdFunc(ctx, id) + } + return db.User{}, nil +} + + // TestSignupPasswordComplexity verifies password validation requirements // // Requirements: 0.5 @@ -397,45 +419,89 @@ func TestLogoutResponseStructure(t *testing.T) { // // Requirements: 0.7, 18.1-18.5 func TestMeAuthentication(t *testing.T) { + e := echo.New() + + validUUIDStr := "550e8400-e29b-41d4-a716-446655440000" + tests := []struct { name string scenario string + setupContext func(c echo.Context) expectedError string expectedStatus int }{ { - name: "authenticated user can get profile", - scenario: "valid JWT token with claims", - expectedStatus: 200, - expectedError: "", + name: "missing claims fails", + scenario: "no claims in context", + setupContext: func(c echo.Context) {}, + expectedStatus: http.StatusUnauthorized, + expectedError: "INVALID_TOKEN_CLAIMS", }, { - name: "missing token fails", - scenario: "no Authorization header or cookie", - expectedStatus: 401, - expectedError: "UNAUTHORIZED", + name: "invalid type for claims fails", + scenario: "claims is not *utils.TokenPayload", + setupContext: func(c echo.Context) { + c.Set(auth.ClaimsKey, "invalid claims") + }, + expectedStatus: http.StatusUnauthorized, + expectedError: "INVALID_TOKEN_CLAIMS", }, { - name: "invalid token fails", - scenario: "malformed or expired JWT token", - expectedStatus: 401, - expectedError: "UNAUTHORIZED", + name: "invalid user ID in claims fails", + scenario: "claims has invalid UUID format", + setupContext: func(c echo.Context) { + payload := &utils.TokenPayload{UserID: "invalid-uuid"} + c.Set(auth.ClaimsKey, payload) + }, + expectedStatus: http.StatusUnauthorized, + expectedError: "INVALID_USER_ID", }, { - name: "invalid claims fails", - scenario: "token valid but claims missing", - expectedStatus: 401, - expectedError: "INVALID_TOKEN_CLAIMS", + name: "authenticated user can get profile", + scenario: "valid JWT token with claims", + setupContext: func(c echo.Context) { + payload := &utils.TokenPayload{UserID: validUUIDStr} + c.Set(auth.ClaimsKey, payload) + }, + expectedStatus: http.StatusOK, + expectedError: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // This test documents that Me: - // - Requires valid JWT authentication - // - Extracts user claims from auth context - // - Returns 401 UNAUTHORIZED for missing/invalid auth - t.Logf("Scenario: %s expects status %d", tt.scenario, tt.expectedStatus) + req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + tt.setupContext(*c) + + mockQuerier := &MockQuerier{ + GetUserByIdFunc: func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{ + ID: id, + Name: "Test User", + Email: pgtype.Text{String: "test@example.com", Valid: true}, + EmailVerified: true, + }, nil + }, + } + service := NewService(mockQuerier, &config.Config{}) + handler := NewHandler(service) + + err := handler.Me(c) + if err != nil { + // if Echo error handling returns an error + t.Fatalf("Unexpected error: %v", err) + } + + assert.Equal(t, tt.expectedStatus, rec.Code) + + if tt.expectedError != "" { + var resp map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &resp) + assert.Equal(t, tt.expectedError, resp["message"]) + } }) } } @@ -444,32 +510,68 @@ func TestMeAuthentication(t *testing.T) { // // Requirements: 0.7 func TestMeUserNotFound(t *testing.T) { + e := echo.New() + validUUIDStr := "550e8400-e29b-41d4-a716-446655440000" + tests := []struct { name string scenario string + setupQuerier func(m *MockQuerier) expectedError string expectedStatus int }{ { name: "existing user returns profile", scenario: "user_id from token exists in database", - expectedStatus: 200, + setupQuerier: func(m *MockQuerier) { + m.GetUserByIdFunc = func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{ + ID: id, + Name: "Test User", + Email: pgtype.Text{String: "test@example.com", Valid: true}, + }, nil + } + }, + expectedStatus: http.StatusOK, expectedError: "", }, { name: "deleted user returns 404", scenario: "user_id from token does not exist", - expectedStatus: 404, + setupQuerier: func(m *MockQuerier) { + m.GetUserByIdFunc = func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{}, errors.New("not found") + } + }, + expectedStatus: http.StatusNotFound, expectedError: "USER_NOT_FOUND", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // This test documents that Me: - // - Looks up user by ID from token claims - // - Returns 404 USER_NOT_FOUND if user deleted - t.Logf("Scenario: %s expects status %d", tt.scenario, tt.expectedStatus) + req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + payload := &utils.TokenPayload{UserID: validUUIDStr} + c.Set(auth.ClaimsKey, payload) + + mockQuerier := &MockQuerier{} + tt.setupQuerier(mockQuerier) + + service := NewService(mockQuerier, &config.Config{}) + handler := NewHandler(service) + + err := handler.Me(c) + assert.NoError(t, err) + assert.Equal(t, tt.expectedStatus, rec.Code) + + if tt.expectedError != "" { + var resp map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &resp) + assert.Equal(t, tt.expectedError, resp["message"]) + } }) } } @@ -479,44 +581,48 @@ func TestMeUserNotFound(t *testing.T) { // Requirements: 0.7 func TestMeResponseStructure(t *testing.T) { t.Run("response includes user profile", func(t *testing.T) { - // This test documents that Me returns: - // - success: true - // - data: user object with id, name, email, emailVerified - // - HTTP 200 status - t.Log("Response follows UserProfileResponse structure") + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + validUUIDStr := "550e8400-e29b-41d4-a716-446655440000" + validUUID, _ := utils.StringToUUID(validUUIDStr) + payload := &utils.TokenPayload{UserID: validUUIDStr} + c.Set(auth.ClaimsKey, payload) + + mockQuerier := &MockQuerier{ + GetUserByIdFunc: func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{ + ID: id, + Name: "Test User", + Email: pgtype.Text{String: "test@example.com", Valid: true}, + EmailVerified: true, + }, nil + }, + } + + service := NewService(mockQuerier, &config.Config{}) + handler := NewHandler(service) + + err := handler.Me(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var resp UserProfileResponse + err = json.Unmarshal(rec.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.True(t, resp.Success) + assert.Equal(t, "Test User", resp.Data.Name) + assert.Equal(t, "test@example.com", resp.Data.Email) + assert.True(t, resp.Data.EmailVerified) + + // Assert ID matches + assert.Equal(t, validUUID.Bytes, resp.Data.ID.Bytes) }) } -// TestForgotPasswordEmailEnumeration verifies security behavior -// -// Requirements: 0.2 -func TestForgotPasswordEmailEnumeration(t *testing.T) { - tests := []struct { - name string - scenario string - }{ - { - name: "existing email returns success", - scenario: "email exists in database", - }, - { - name: "non-existent email returns success", - scenario: "email does not exist in database", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // This test documents that ForgotPassword: - // - Always returns success (HTTP 200) - // - Does not reveal whether email exists - // - Prevents email enumeration attacks - // - Only sends reset token if email exists - t.Logf("Scenario: %s always returns success", tt.scenario) - }) - } -} - // TestForgotPasswordTokenGeneration verifies token creation // // Requirements: 0.1 diff --git a/apps/server/internal/modules/auth/service.go b/apps/server/internal/modules/auth/service.go index ee61263..fd03a89 100644 --- a/apps/server/internal/modules/auth/service.go +++ b/apps/server/internal/modules/auth/service.go @@ -18,11 +18,11 @@ import ( ) type Service struct { - queries *db.Queries + queries db.Querier config *config.Config } -func NewService(queries *db.Queries, config *config.Config) *Service { +func NewService(queries db.Querier, config *config.Config) *Service { return &Service{queries: queries, config: config} } diff --git a/patch_tests.go b/patch_tests.go new file mode 100644 index 0000000..3bcc6d4 --- /dev/null +++ b/patch_tests.go @@ -0,0 +1,283 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + path := "apps/server/internal/modules/auth/handler_test.go" + content, err := os.ReadFile(path) + if err != nil { + fmt.Println("Error reading:", err) + return + } + + newImports := `package auth + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/coderz-space/coderz.space/internal/common/middleware/auth" + "github.com/coderz-space/coderz.space/internal/common/utils" + "github.com/coderz-space/coderz.space/internal/config" + db "github.com/coderz-space/coderz.space/internal/db/sqlc" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" +) + +// MockQuerier implements db.Querier for testing +type MockQuerier struct { + db.Querier + GetUserByIdFunc func(ctx context.Context, id pgtype.UUID) (db.User, error) +} + +func (m *MockQuerier) GetUserById(ctx context.Context, id pgtype.UUID) (db.User, error) { + if m.GetUserByIdFunc != nil { + return m.GetUserByIdFunc(ctx, id) + } + return db.User{}, nil +} +` + + strContent := string(content) + strContent = strings.Replace(strContent, "package auth\n\nimport (\n\t\"testing\"\n)", newImports, 1) + + // Replace TestMeAuthentication + testAuth := `func TestMeAuthentication(t *testing.T) { + e := echo.New() + + validUUIDStr := "550e8400-e29b-41d4-a716-446655440000" + + tests := []struct { + name string + scenario string + setupContext func(c echo.Context) + expectedError string + expectedStatus int + }{ + { + name: "missing claims fails", + scenario: "no claims in context", + setupContext: func(c echo.Context) {}, + expectedStatus: http.StatusUnauthorized, + expectedError: "INVALID_TOKEN_CLAIMS", + }, + { + name: "invalid type for claims fails", + scenario: "claims is not *utils.TokenPayload", + setupContext: func(c echo.Context) { + c.Set(auth.ClaimsKey, "invalid claims") + }, + expectedStatus: http.StatusUnauthorized, + expectedError: "INVALID_TOKEN_CLAIMS", + }, + { + name: "invalid user ID in claims fails", + scenario: "claims has invalid UUID format", + setupContext: func(c echo.Context) { + payload := &utils.TokenPayload{UserID: "invalid-uuid"} + c.Set(auth.ClaimsKey, payload) + }, + expectedStatus: http.StatusUnauthorized, + expectedError: "INVALID_USER_ID", + }, + { + name: "authenticated user can get profile", + scenario: "valid JWT token with claims", + setupContext: func(c echo.Context) { + payload := &utils.TokenPayload{UserID: validUUIDStr} + c.Set(auth.ClaimsKey, payload) + }, + expectedStatus: http.StatusOK, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + tt.setupContext(c) + + mockQuerier := &MockQuerier{ + GetUserByIdFunc: func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{ + ID: id, + Name: "Test User", + Email: pgtype.Text{String: "test@example.com", Valid: true}, + EmailVerified: true, + }, nil + }, + } + service := NewService(mockQuerier, &config.Config{}) + handler := NewHandler(service) + + err := handler.Me(&c) + if err != nil { + // if Echo error handling returns an error + t.Fatalf("Unexpected error: %v", err) + } + + assert.Equal(t, tt.expectedStatus, rec.Code) + + if tt.expectedError != "" { + var resp map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &resp) + assert.Equal(t, tt.expectedError, resp["message"]) + } + }) + } +}` + + // Remove old TestMeAuthentication and replace + startIndex := strings.Index(strContent, "func TestMeAuthentication(t *testing.T) {") + if startIndex != -1 { + endIndex := strings.Index(strContent[startIndex:], "}\n}\n") + if endIndex != -1 { + strContent = strContent[:startIndex] + testAuth + "\n" + strContent[startIndex+endIndex+4:] + } + } + + + // Replace TestMeUserNotFound + testNotFound := `func TestMeUserNotFound(t *testing.T) { + e := echo.New() + validUUIDStr := "550e8400-e29b-41d4-a716-446655440000" + + tests := []struct { + name string + scenario string + setupQuerier func(m *MockQuerier) + expectedError string + expectedStatus int + }{ + { + name: "existing user returns profile", + scenario: "user_id from token exists in database", + setupQuerier: func(m *MockQuerier) { + m.GetUserByIdFunc = func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{ + ID: id, + Name: "Test User", + Email: pgtype.Text{String: "test@example.com", Valid: true}, + }, nil + } + }, + expectedStatus: http.StatusOK, + expectedError: "", + }, + { + name: "deleted user returns 404", + scenario: "user_id from token does not exist", + setupQuerier: func(m *MockQuerier) { + m.GetUserByIdFunc = func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{}, errors.New("not found") + } + }, + expectedStatus: http.StatusNotFound, + expectedError: "USER_NOT_FOUND", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + payload := &utils.TokenPayload{UserID: validUUIDStr} + c.Set(auth.ClaimsKey, payload) + + mockQuerier := &MockQuerier{} + tt.setupQuerier(mockQuerier) + + service := NewService(mockQuerier, &config.Config{}) + handler := NewHandler(service) + + err := handler.Me(&c) + assert.NoError(t, err) + assert.Equal(t, tt.expectedStatus, rec.Code) + + if tt.expectedError != "" { + var resp map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &resp) + assert.Equal(t, tt.expectedError, resp["message"]) + } + }) + } +}` + + startIndex = strings.Index(strContent, "func TestMeUserNotFound(t *testing.T) {") + if startIndex != -1 { + endIndex := strings.Index(strContent[startIndex:], "}\n}\n") + if endIndex != -1 { + strContent = strContent[:startIndex] + testNotFound + "\n" + strContent[startIndex+endIndex+4:] + } + } + + + // Replace TestMeResponseStructure + testRespStr := `func TestMeResponseStructure(t *testing.T) { + t.Run("response includes user profile", func(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + validUUIDStr := "550e8400-e29b-41d4-a716-446655440000" + validUUID, _ := utils.StringToUUID(validUUIDStr) + payload := &utils.TokenPayload{UserID: validUUIDStr} + c.Set(auth.ClaimsKey, payload) + + mockQuerier := &MockQuerier{ + GetUserByIdFunc: func(ctx context.Context, id pgtype.UUID) (db.User, error) { + return db.User{ + ID: id, + Name: "Test User", + Email: pgtype.Text{String: "test@example.com", Valid: true}, + EmailVerified: true, + }, nil + }, + } + + service := NewService(mockQuerier, &config.Config{}) + handler := NewHandler(service) + + err := handler.Me(&c) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var resp UserProfileResponse + err = json.Unmarshal(rec.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.True(t, resp.Success) + assert.Equal(t, "Test User", resp.Data.Name) + assert.Equal(t, "test@example.com", resp.Data.Email) + assert.True(t, resp.Data.EmailVerified) + + // Assert ID matches + assert.Equal(t, validUUID.Bytes, resp.Data.ID.Bytes) + }) +}` + + startIndex = strings.Index(strContent, "func TestMeResponseStructure(t *testing.T) {") + if startIndex != -1 { + endIndex := strings.Index(strContent[startIndex:], "}\n}\n") + if endIndex != -1 { + strContent = strContent[:startIndex] + testRespStr + "\n" + strContent[startIndex+endIndex+4:] + } + } + + os.WriteFile(path, []byte(strContent), 0644) +}