Files
RenewCMS/test/request_password_reset_mail_failure_test.go
T

115 lines
3.8 KiB
Go

package test
import (
"RenewCMS/api/handlers/auth"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"bytes"
"errors"
"log"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"github.com/go-chi/chi/v5"
)
type resetMailFailureHasher struct{}
func (resetMailFailureHasher) Hash(value string) (string, error) {
return "hash-of-" + value, nil
}
func (resetMailFailureHasher) Compare(hashed string, value string) error {
return nil
}
func captureStandardLog(t *testing.T) *bytes.Buffer {
t.Helper()
var buffer bytes.Buffer
previousOutput, previousFlags := log.Writer(), log.Flags()
log.SetOutput(&buffer)
t.Cleanup(func() {
log.SetOutput(previousOutput)
log.SetFlags(previousFlags)
})
return &buffer
}
func resetRequestWith(mailRepo *mockMailRepo, email string) (*httptest.ResponseRecorder, string) {
userRepo := newMockUserRepo()
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true}
userRepo.nextID = 1
hasher := resetMailFailureHasher{}
handler := auth.NewPasswordResetHandler(
useCases.NewRequestPasswordResetUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
useCases.NewResetPasswordUseCase(userRepo, hasher),
)
router := chi.NewRouter()
router.Post("/v1/auth/password-reset", handler.RequestPasswordReset)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/password-reset", bytes.NewBufferString(`{"email": "`+email+`"}`))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
return recorder, userRepo.lastUpdatePasswordResetCodeCode
}
func TestRequestPasswordResetMailFailure(t *testing.T) {
validator.InitValidator()
sendFailure := errors.New("smtp: connection refused")
failingMail := func() *mockMailRepo {
mailRepo := newMockMailRepo()
mailRepo.fail.failNextWith("Send", sendFailure)
return mailRepo
}
t.Run("A failed send is logged and the response is unchanged", func(t *testing.T) {
logged := captureStandardLog(t)
recorder, _ := resetRequestWith(failingMail(), "alice@example.com")
if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != requestPasswordResetGenericBody {
t.Errorf("expected the generic 200, got %d %s", recorder.Code, recorder.Body.String())
}
if jwtCookieOf(recorder) != nil {
t.Errorf("no jwt cookie may be set")
}
line := logged.String()
if !strings.Contains(line, "Send") || !strings.Contains(line, "smtp: connection refused") {
t.Errorf("log must name the failed send and its error, got %q", line)
}
})
t.Run("The response is identical for a failed send, a successful send and an unknown email", func(t *testing.T) {
captureStandardLog(t)
failed, _ := resetRequestWith(failingMail(), "alice@example.com")
sent, _ := resetRequestWith(newMockMailRepo(), "alice@example.com")
unknown, _ := resetRequestWith(newMockMailRepo(), "nobody@example.com")
for name, other := range map[string]*httptest.ResponseRecorder{"successful send": sent, "unknown email": unknown} {
if failed.Code != other.Code || failed.Body.String() != other.Body.String() {
t.Errorf("%s differs: %d %q vs %d %q", name, failed.Code, failed.Body.String(), other.Code, other.Body.String())
}
if !reflect.DeepEqual(failed.Header(), other.Header()) {
t.Errorf("%s headers differ: %v vs %v", name, failed.Header(), other.Header())
}
}
})
t.Run("The log does not contain the raw reset code", func(t *testing.T) {
logged := captureStandardLog(t)
_, storedHash := resetRequestWith(failingMail(), "alice@example.com")
rawCode := strings.TrimPrefix(storedHash, "hash-of-")
if rawCode == "" || strings.Contains(logged.String(), rawCode) || strings.Contains(logged.String(), storedHash) {
t.Errorf("log must contain neither the raw code nor its hash, got %q", logged.String())
}
})
}