Files
wehub-resource-sync 75f3dd141c
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI and Release / lint-frontend (push) Has been cancelled
CI and Release / dockerfile-scan (push) Has been cancelled
CI and Release / test-frontend (push) Has been cancelled
CI and Release / lint-verification-agent (push) Has been cancelled
CI and Release / test-verification-agent (push) Has been cancelled
CI and Release / e2e-verification-agent (push) Has been cancelled
CI and Release / test-backend (push) Has been cancelled
CI and Release / build-dev-image (push) Has been cancelled
CI and Release / push-dev-image (push) Has been cancelled
CI and Release / build-image (push) Has been cancelled
CI and Release / build-verification-image (push) Has been cancelled
CI and Release / determine-version (push) Has been cancelled
CI and Release / push-image (push) Has been cancelled
CI and Release / push-verification-image (12) (push) Has been cancelled
CI and Release / lint-backend (push) Has been cancelled
CI and Release / push-verification-image (17) (push) Has been cancelled
CI and Release / push-verification-image (18) (push) Has been cancelled
CI and Release / release (push) Has been cancelled
CI and Release / publish-helm-chart (push) Has been cancelled
CI and Release / push-verification-image (13) (push) Has been cancelled
CI and Release / push-verification-image (14) (push) Has been cancelled
CI and Release / push-verification-image (15) (push) Has been cancelled
CI and Release / push-verification-image (16) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:11:07 +08:00

594 lines
14 KiB
Go

package users_controllers
import (
"net/http"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/bcrypt"
users_dto "databasus-backend/internal/features/users/dto"
users_enums "databasus-backend/internal/features/users/enums"
users_models "databasus-backend/internal/features/users/models"
users_services "databasus-backend/internal/features/users/services"
users_testing "databasus-backend/internal/features/users/testing"
"databasus-backend/internal/storage"
test_utils "databasus-backend/internal/util/testing"
)
func Test_SendResetPasswordCode_WithValidEmail_CodeSent(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
user := users_testing.CreateTestUser(users_enums.UserRoleMember)
request := users_dto.SendResetPasswordCodeRequestDTO{
Email: user.Email,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
request,
http.StatusOK,
)
assert.Equal(t, 1, len(mockEmailSender.SentEmails))
assert.Equal(t, user.Email, mockEmailSender.SentEmails[0].To)
assert.Contains(t, mockEmailSender.SentEmails[0].Subject, "Password Reset")
}
func Test_SendResetPasswordCode_WithNonExistentUser_ReturnsSuccess(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
request := users_dto.SendResetPasswordCodeRequestDTO{
Email: "nonexistent" + uuid.New().String() + "@example.com",
}
// Should return success to prevent enumeration attacks
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
request,
http.StatusOK,
)
// But no email should be sent
assert.Equal(t, 0, len(mockEmailSender.SentEmails))
}
func Test_SendResetPasswordCode_WithInvitedUser_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
adminUser := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
email := "invited" + uuid.New().String() + "@example.com"
inviteRequest := users_dto.InviteUserRequestDTO{
Email: email,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/invite",
"Bearer "+adminUser.Token,
inviteRequest,
http.StatusOK,
)
request := users_dto.SendResetPasswordCodeRequestDTO{
Email: email,
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
request,
http.StatusBadRequest,
)
assert.Contains(t, string(resp.Body), "only active users")
}
func Test_SendResetPasswordCode_WithRateLimitExceeded_ReturnsTooManyRequests(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
user := users_testing.CreateTestUser(users_enums.UserRoleMember)
request := users_dto.SendResetPasswordCodeRequestDTO{
Email: user.Email,
}
// Make 3 requests (should succeed)
for range 3 {
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
request,
http.StatusOK,
)
}
// 4th request should be rate limited
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
request,
http.StatusTooManyRequests,
)
assert.Contains(t, string(resp.Body), "Rate limit exceeded")
}
func Test_SendResetPasswordCode_WithInvalidJSON_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
resp := test_utils.MakeRequest(t, router, test_utils.RequestOptions{
Method: "POST",
URL: "/api/v1/users/send-reset-password-code",
Body: "invalid json",
ExpectedStatus: http.StatusBadRequest,
})
assert.Contains(t, string(resp.Body), "Invalid request format")
}
func Test_ResetPassword_WithValidCode_PasswordReset(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
email := "resettest" + uuid.New().String() + "@example.com"
oldPassword := "oldpassword123"
newPassword := "newpassword456"
// Create user
signupRequest := users_dto.SignUpRequestDTO{
Email: email,
Password: oldPassword,
Name: "Test User",
}
test_utils.MakePostRequest(t, router, "/api/v1/users/signup", "", signupRequest, http.StatusOK)
// Request reset code
sendCodeRequest := users_dto.SendResetPasswordCodeRequestDTO{
Email: email,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
sendCodeRequest,
http.StatusOK,
)
// Extract code from email
assert.Equal(t, 1, len(mockEmailSender.SentEmails))
emailBody := mockEmailSender.SentEmails[0].Body
code := extractCodeFromEmail(emailBody)
t.Logf("Extracted code: %s from email body (length: %d)", code, len(code))
assert.NotEmpty(t, code, "Code should be extracted from email")
assert.Len(t, code, 6, "Code should be 6 digits")
// Reset password
resetRequest := users_dto.ResetPasswordRequestDTO{
Email: email,
Code: code,
NewPassword: newPassword,
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest,
http.StatusOK,
)
if resp.StatusCode != http.StatusOK {
t.Logf("Reset password failed with body: %s", string(resp.Body))
}
// Verify old password doesn't work
oldSigninRequest := users_dto.SignInRequestDTO{
Email: email,
Password: oldPassword,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/signin",
"",
oldSigninRequest,
http.StatusBadRequest,
)
// Verify new password works
newSigninRequest := users_dto.SignInRequestDTO{
Email: email,
Password: newPassword,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/signin",
"",
newSigninRequest,
http.StatusOK,
)
}
func Test_ResetPassword_WithExpiredCode_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
user := users_testing.CreateTestUser(users_enums.UserRoleMember)
// Create expired reset code directly in database
code := "123456"
hashedCode, _ := bcrypt.GenerateFromPassword([]byte(code), bcrypt.DefaultCost)
expiredCode := &users_models.PasswordResetCode{
ID: uuid.New(),
UserID: user.UserID,
HashedCode: string(hashedCode),
ExpiresAt: time.Now().UTC().Add(-1 * time.Hour), // Expired 1 hour ago
IsUsed: false,
CreatedAt: time.Now().UTC().Add(-2 * time.Hour),
}
storage.GetDb().Create(expiredCode)
resetRequest := users_dto.ResetPasswordRequestDTO{
Email: user.Email,
Code: code,
NewPassword: "newpassword123",
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest,
http.StatusBadRequest,
)
assert.Contains(t, string(resp.Body), "invalid or expired")
}
func Test_ResetPassword_WithUsedCode_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
email := "usedcode" + uuid.New().String() + "@example.com"
// Create user
signupRequest := users_dto.SignUpRequestDTO{
Email: email,
Password: "password123",
Name: "Test User",
}
test_utils.MakePostRequest(t, router, "/api/v1/users/signup", "", signupRequest, http.StatusOK)
// Request reset code
sendCodeRequest := users_dto.SendResetPasswordCodeRequestDTO{
Email: email,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
sendCodeRequest,
http.StatusOK,
)
code := extractCodeFromEmail(mockEmailSender.SentEmails[0].Body)
// Use code first time
resetRequest := users_dto.ResetPasswordRequestDTO{
Email: email,
Code: code,
NewPassword: "newpassword123",
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest,
http.StatusOK,
)
// Try to use same code again
resetRequest2 := users_dto.ResetPasswordRequestDTO{
Email: email,
Code: code,
NewPassword: "anotherpassword456",
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest2,
http.StatusBadRequest,
)
assert.Contains(t, string(resp.Body), "invalid or expired")
}
func Test_ResetPassword_WithWrongCode_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
user := users_testing.CreateTestUser(users_enums.UserRoleMember)
// Request reset code
sendCodeRequest := users_dto.SendResetPasswordCodeRequestDTO{
Email: user.Email,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
sendCodeRequest,
http.StatusOK,
)
// Try to reset with wrong code
resetRequest := users_dto.ResetPasswordRequestDTO{
Email: user.Email,
Code: "999999", // Wrong code
NewPassword: "newpassword123",
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest,
http.StatusBadRequest,
)
assert.Contains(t, string(resp.Body), "invalid")
}
func Test_ResetPassword_WithInvalidNewPassword_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
user := users_testing.CreateTestUser(users_enums.UserRoleMember)
resetRequest := users_dto.ResetPasswordRequestDTO{
Email: user.Email,
Code: "123456",
NewPassword: "short", // Too short
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest,
http.StatusBadRequest,
)
}
func Test_ResetPassword_EmailSendFailure_ReturnsError(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
mockEmailSender.ShouldFail = true
users_services.GetUserService().SetEmailSender(mockEmailSender)
user := users_testing.CreateTestUser(users_enums.UserRoleMember)
request := users_dto.SendResetPasswordCodeRequestDTO{
Email: user.Email,
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
request,
http.StatusBadRequest,
)
assert.Contains(t, string(resp.Body), "failed to send email")
}
func Test_ResetPasswordFlow_E2E_CompletesSuccessfully(t *testing.T) {
router := createUserTestRouter()
mockEmailSender := users_testing.NewMockEmailSender()
users_services.GetUserService().SetEmailSender(mockEmailSender)
email := "e2e" + uuid.New().String() + "@example.com"
initialPassword := "initialpass123"
newPassword := "brandnewpass456"
// 1. Create user via signup
signupRequest := users_dto.SignUpRequestDTO{
Email: email,
Password: initialPassword,
Name: "E2E Test User",
}
test_utils.MakePostRequest(t, router, "/api/v1/users/signup", "", signupRequest, http.StatusOK)
// 2. Verify can sign in with initial password
signinRequest := users_dto.SignInRequestDTO{
Email: email,
Password: initialPassword,
}
var signinResponse users_dto.SignInResponseDTO
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/users/signin",
"",
signinRequest,
http.StatusOK,
&signinResponse,
)
assert.NotEmpty(t, signinResponse.Token)
// 3. Request password reset code
sendCodeRequest := users_dto.SendResetPasswordCodeRequestDTO{
Email: email,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/send-reset-password-code",
"",
sendCodeRequest,
http.StatusOK,
)
// 4. Verify email was sent
assert.Equal(t, 1, len(mockEmailSender.SentEmails))
code := extractCodeFromEmail(mockEmailSender.SentEmails[0].Body)
assert.NotEmpty(t, code)
// 5. Reset password using code
resetRequest := users_dto.ResetPasswordRequestDTO{
Email: email,
Code: code,
NewPassword: newPassword,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/reset-password",
"",
resetRequest,
http.StatusOK,
)
// 6. Verify old password no longer works
oldSignin := users_dto.SignInRequestDTO{
Email: email,
Password: initialPassword,
}
test_utils.MakePostRequest(
t,
router,
"/api/v1/users/signin",
"",
oldSignin,
http.StatusBadRequest,
)
// 7. Verify new password works
newSignin := users_dto.SignInRequestDTO{
Email: email,
Password: newPassword,
}
var finalResponse users_dto.SignInResponseDTO
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/users/signin",
"",
newSignin,
http.StatusOK,
&finalResponse,
)
assert.NotEmpty(t, finalResponse.Token)
}
func Test_ResetPassword_WithInvalidJSON_ReturnsBadRequest(t *testing.T) {
router := createUserTestRouter()
resp := test_utils.MakeRequest(t, router, test_utils.RequestOptions{
Method: "POST",
URL: "/api/v1/users/reset-password",
Body: "invalid json",
ExpectedStatus: http.StatusBadRequest,
})
assert.Contains(t, string(resp.Body), "Invalid request format")
}
// Helper function to extract 6-digit code from email HTML body
func extractCodeFromEmail(emailBody string) string {
// Look for pattern: <h1 ... >CODE</h1>
// First find <h1
h1Start := 0
for i := range len(emailBody) - 3 {
if emailBody[i:i+3] == "<h1" {
h1Start = i
break
}
}
if h1Start == 0 {
return ""
}
// Find the > after <h1
contentStart := h1Start
for i := h1Start; i < len(emailBody); i++ {
if emailBody[i] == '>' {
contentStart = i + 1
break
}
}
// Find </h1>
contentEnd := contentStart
for i := contentStart; i < len(emailBody)-5; i++ {
if emailBody[i:i+5] == "</h1>" {
contentEnd = i
break
}
}
if contentEnd <= contentStart {
return ""
}
// Extract content and remove whitespace
content := emailBody[contentStart:contentEnd]
code := ""
for i := range len(content) {
if isDigit(content[i]) {
code += string(content[i])
}
}
if len(code) == 6 {
return code
}
return ""
}
func isDigit(b byte) bool {
return b >= '0' && b <= '9'
}