Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/server/go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
50 changes: 50 additions & 0 deletions apps/server/internal/modules/auth/preservation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
114 changes: 114 additions & 0 deletions apps/server/internal/modules/auth/reset_password_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions apps/server/internal/modules/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down
50 changes: 50 additions & 0 deletions apps/server/internal/modules/auth/service_mock_for_test.go
Original file line number Diff line number Diff line change
@@ -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{})
}
Loading