refac: reamaining auth surface rewrite, T0
This commit is contained in:
1 parent
6d510d2805
commit
aa865af2e9
50 files changed
+3723
-149
No files matched your search
@@ -0,0 +1,243 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/handlers/auth"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newRequestPasswordResetRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, publicURL string) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
authHandler := auth.NewHandler(
|
||||
token,
|
||||
useCases.NewGetUserUseCase(userRepo),
|
||||
useCases.NewListUsersUseCase(userRepo),
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, publicURL),
|
||||
useCases.NewAuthenticateUserUseCase(userRepo, hasher),
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
useCases.NewRequestPasswordResetUseCase(userRepo, hasher, mailRepo, publicURL),
|
||||
useCases.NewResetPasswordUseCase(userRepo, hasher),
|
||||
true,
|
||||
)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/password-reset", authHandler.RequestPasswordReset)
|
||||
})
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func sendPasswordResetRequest(router *chi.Mux, body string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/auth/password-reset", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
return recorder
|
||||
}
|
||||
|
||||
const requestPasswordResetGenericBody = `{"message":"If an account exists for that email, a password reset link has been sent."}`
|
||||
|
||||
func TestRequestPasswordResetHTTP(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
|
||||
t.Run("Requesting a reset for an existing email sends the reset email and returns the generic message", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
recorder := sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", recorder.Code)
|
||||
}
|
||||
if strings.TrimSpace(recorder.Body.String()) != requestPasswordResetGenericBody {
|
||||
t.Errorf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
if len(mailRepo.SentEmails) != 1 {
|
||||
t.Fatalf("expected exactly one email, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
if mailRepo.SentEmails[0].Address != "alice@example.com" {
|
||||
t.Errorf("expected email to alice@example.com, got %s", mailRepo.SentEmails[0].Address)
|
||||
}
|
||||
if mailRepo.SentEmails[0].Template != "passwordReset" {
|
||||
t.Errorf("expected template passwordReset, got %s", mailRepo.SentEmails[0].Template)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Requesting a reset for an email with no account returns the identical response and sends no email", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
recorder := sendPasswordResetRequest(router, `{"email": "nobody@example.com"}`)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", recorder.Code)
|
||||
}
|
||||
if strings.TrimSpace(recorder.Body.String()) != requestPasswordResetGenericBody {
|
||||
t.Errorf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
if len(mailRepo.SentEmails) != 0 {
|
||||
t.Errorf("expected no email to be sent, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The reset email's Host reflects the injected public URL", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com"}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
|
||||
if len(mailRepo.SentEmails) != 1 {
|
||||
t.Fatalf("expected exactly one email, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
data, ok := mailRepo.SentEmails[0].Data.(useCases.MailPasswordResetData)
|
||||
if !ok {
|
||||
t.Fatalf("expected data of type MailPasswordResetData, got %T", mailRepo.SentEmails[0].Data)
|
||||
}
|
||||
if data.Host != "https://cms.example.com" {
|
||||
t.Errorf("expected Host https://cms.example.com, got %s", data.Host)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The stored reset code expires one hour after creation", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com"}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
|
||||
stored := userRepo.users[1]
|
||||
want := time.Now().Add(time.Hour)
|
||||
if stored.PasswordResetExpiration.Sub(want).Abs() > 5*time.Second {
|
||||
t.Errorf("expected expiration near %v, got %v", want, stored.PasswordResetExpiration)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A second request overwrites the first code, invalidating it", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com"}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
|
||||
if len(mailRepo.SentEmails) != 2 {
|
||||
t.Fatalf("expected exactly two emails, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
firstData := mailRepo.SentEmails[0].Data.(useCases.MailPasswordResetData)
|
||||
secondData := mailRepo.SentEmails[1].Data.(useCases.MailPasswordResetData)
|
||||
if firstData.VerificationCode == secondData.VerificationCode {
|
||||
t.Errorf("expected the second code to differ from the first")
|
||||
}
|
||||
|
||||
hasher := &stubHasher{}
|
||||
stored := userRepo.users[1]
|
||||
if hasher.Compare(stored.PasswordResetCode, firstData.VerificationCode) == nil {
|
||||
t.Errorf("expected the stored code to no longer match the first raw code")
|
||||
}
|
||||
if hasher.Compare(stored.PasswordResetCode, secondData.VerificationCode) != nil {
|
||||
t.Errorf("expected the stored code to match the second raw code")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A mail-send failure for an existing user still returns the same generic response", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com"}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
mailRepo.fail.failNext("Send")
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
recorder := sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", recorder.Code)
|
||||
}
|
||||
if strings.TrimSpace(recorder.Body.String()) != requestPasswordResetGenericBody {
|
||||
t.Errorf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
if recorder.Header().Get("Set-Cookie") != "" {
|
||||
t.Errorf("expected no jwt cookie to be set, got %s", recorder.Header().Get("Set-Cookie"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unverified user can request a password reset exactly like a verified one", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "bob", Email: "bob@example.com", IsVerified: false}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
recorder := sendPasswordResetRequest(router, `{"email": "bob@example.com"}`)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", recorder.Code)
|
||||
}
|
||||
if strings.TrimSpace(recorder.Body.String()) != requestPasswordResetGenericBody {
|
||||
t.Errorf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
if len(mailRepo.SentEmails) != 1 {
|
||||
t.Fatalf("expected exactly one email, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
if mailRepo.SentEmails[0].Address != "bob@example.com" {
|
||||
t.Errorf("expected email to bob@example.com, got %s", mailRepo.SentEmails[0].Address)
|
||||
}
|
||||
if mailRepo.SentEmails[0].Template != "passwordReset" {
|
||||
t.Errorf("expected template passwordReset, got %s", mailRepo.SentEmails[0].Template)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A malformed request is still rejected, since this is not an enumeration concern", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{"missing email", `{}`},
|
||||
{"malformed email", `{"email": "not-an-email"}`},
|
||||
{"non-json body", `not-json`},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com"}
|
||||
userRepo.nextID = 1
|
||||
mailRepo := newMockMailRepo()
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
recorder := sendPasswordResetRequest(router, tc.body)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", recorder.Code)
|
||||
}
|
||||
if len(mailRepo.SentEmails) != 0 {
|
||||
t.Errorf("expected no email to be sent, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user