diff --git a/apps/server/go.mod b/apps/server/go.mod index f8d76d9..0c09ae6 100644 --- a/apps/server/go.mod +++ b/apps/server/go.mod @@ -1,6 +1,6 @@ module github.com/coderz-space/coderz.space -go 1.24.3 +go 1.25.0 require ( github.com/go-playground/validator/v10 v10.30.1 diff --git a/apps/server/internal/modules/auth/preservation_test.go b/apps/server/internal/modules/auth/preservation_test.go index ce066a6..133c373 100644 --- a/apps/server/internal/modules/auth/preservation_test.go +++ b/apps/server/internal/modules/auth/preservation_test.go @@ -294,3 +294,53 @@ func isValidEmail(email string) bool { } return true } + +// TestResetPasswordPreservation_InvalidRequests verifies that invalid reset password requests +// produce validation errors with status 400. +// +// **Validates: Requirements 0.4, 0.5** +func TestResetPasswordPreservation_InvalidRequests(t *testing.T) { + testCases := []struct { + name string + requestBody map[string]interface{} + description string + }{ + { + name: "missing_token", + requestBody: map[string]interface{}{"newPassword": "NewPassword123"}, + description: "Missing required token field", + }, + { + name: "missing_password", + requestBody: map[string]interface{}{"token": "valid-token-123"}, + description: "Missing required password field", + }, + { + name: "password_too_short", + requestBody: map[string]interface{}{"token": "valid-token-123", "newPassword": "Pass1"}, + description: "Password is too short", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bodyBytes, _ := json.Marshal(tc.requestBody) + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password", bytes.NewReader(bodyBytes)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + handler := NewHandler(&Service{}) + err := handler.ResetPassword(c) + + // Note: This test verifies that we return a Bad Request early without calling the service + if err != nil { + t.Logf("Expected no generic error (handled by NewResponse), but err=%v", err) + } + if rec.Code != http.StatusBadRequest { + t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, rec.Code) + } + }) + } +} diff --git a/apps/server/internal/modules/auth/reset_password_test.go b/apps/server/internal/modules/auth/reset_password_test.go new file mode 100644 index 0000000..c2bfdf1 --- /dev/null +++ b/apps/server/internal/modules/auth/reset_password_test.go @@ -0,0 +1,114 @@ +package auth + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/coderz-space/coderz.space/internal/common/utils" + db "github.com/coderz-space/coderz.space/internal/db/sqlc" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v5" +) + +func TestResetPassword_BindError(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password", bytes.NewReader([]byte(`invalid json`))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + handler := NewHandler(setupTestServiceWithMock(&MockQuerier{})) + err := handler.ResetPassword(c) + + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + if rec.Code != http.StatusBadRequest { + t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, rec.Code) + } +} + +func TestResetPassword_ServiceSuccess(t *testing.T) { + e := echo.New() + + bodyBytes, _ := json.Marshal(map[string]string{ + "token": "valid-token", + "newPassword": "ValidPassword123", + }) + + req := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password", bytes.NewReader(bodyBytes)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // Mock querier simulating a successful reset password + userID, _ := utils.StringToUUID("11111111-1111-1111-1111-111111111111") + mockQ := &MockQuerier{ + GetPasswordResetTokenFunc: func(ctx context.Context, tokenHash string) (db.PasswordResetToken, error) { + return db.PasswordResetToken{ + UserID: userID, + }, nil + }, + UpdateUserPasswordFunc: func(ctx context.Context, arg db.UpdateUserPasswordParams) error { + return nil + }, + DeletePasswordResetTokenFunc: func(ctx context.Context, tokenHash string) error { + return nil + }, + DeleteUserRefreshTokensFunc: func(ctx context.Context, id pgtype.UUID) error { + return nil + }, + } + + handler := NewHandler(setupTestServiceWithMock(mockQ)) + err := handler.ResetPassword(c) + + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + if rec.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, rec.Code) + } + + var res GenericResponse + _ = json.Unmarshal(rec.Body.Bytes(), &res) + if !res.Success { + t.Errorf("Expected success response, got %v", res.Success) + } +} + +func TestResetPassword_ServiceFailure(t *testing.T) { + e := echo.New() + + bodyBytes, _ := json.Marshal(map[string]string{ + "token": "invalid-token", + "newPassword": "ValidPassword123", + }) + + req := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password", bytes.NewReader(bodyBytes)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // Mock querier simulating an expired/invalid token + mockQ := &MockQuerier{ + GetPasswordResetTokenFunc: func(ctx context.Context, tokenHash string) (db.PasswordResetToken, error) { + return db.PasswordResetToken{}, errors.New("not found") + }, + } + + handler := NewHandler(setupTestServiceWithMock(mockQ)) + err := handler.ResetPassword(c) + + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + if rec.Code != http.StatusBadRequest { + t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, rec.Code) + } +} diff --git a/apps/server/internal/modules/auth/service.go b/apps/server/internal/modules/auth/service.go index 794302f..14ffe0a 100644 --- a/apps/server/internal/modules/auth/service.go +++ b/apps/server/internal/modules/auth/service.go @@ -15,11 +15,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/apps/server/internal/modules/auth/service_mock_for_test.go b/apps/server/internal/modules/auth/service_mock_for_test.go new file mode 100644 index 0000000..3fbe714 --- /dev/null +++ b/apps/server/internal/modules/auth/service_mock_for_test.go @@ -0,0 +1,50 @@ +package auth + +import ( + "context" + + "github.com/coderz-space/coderz.space/internal/config" + db "github.com/coderz-space/coderz.space/internal/db/sqlc" + "github.com/jackc/pgx/v5/pgtype" +) + +// MockQuerier implements db.Querier for testing +type MockQuerier struct { + db.Querier + GetPasswordResetTokenFunc func(ctx context.Context, tokenHash string) (db.PasswordResetToken, error) + UpdateUserPasswordFunc func(ctx context.Context, arg db.UpdateUserPasswordParams) error + DeletePasswordResetTokenFunc func(ctx context.Context, tokenHash string) error + DeleteUserRefreshTokensFunc func(ctx context.Context, userID pgtype.UUID) error +} + +func (m *MockQuerier) GetPasswordResetToken(ctx context.Context, tokenHash string) (db.PasswordResetToken, error) { + if m.GetPasswordResetTokenFunc != nil { + return m.GetPasswordResetTokenFunc(ctx, tokenHash) + } + return db.PasswordResetToken{}, nil +} + +func (m *MockQuerier) UpdateUserPassword(ctx context.Context, arg db.UpdateUserPasswordParams) error { + if m.UpdateUserPasswordFunc != nil { + return m.UpdateUserPasswordFunc(ctx, arg) + } + return nil +} + +func (m *MockQuerier) DeletePasswordResetToken(ctx context.Context, tokenHash string) error { + if m.DeletePasswordResetTokenFunc != nil { + return m.DeletePasswordResetTokenFunc(ctx, tokenHash) + } + return nil +} + +func (m *MockQuerier) DeleteUserRefreshTokens(ctx context.Context, userID pgtype.UUID) error { + if m.DeleteUserRefreshTokensFunc != nil { + return m.DeleteUserRefreshTokensFunc(ctx, userID) + } + return nil +} + +func setupTestServiceWithMock(q db.Querier) *Service { + return NewService(q, &config.Config{}) +}