Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/mail"
|
||||
)
|
||||
|
||||
func TestHandleForgotPasswordMailerRequired(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{Auth: &auth.Service{}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/forgot-password", bytes.NewBufferString(`{"email":"a@example.com"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleForgotPassword(rec, req)
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%d want 503", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleForgotPasswordRequiresEmail(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{
|
||||
Mail: &recordingMailer{enabled: true},
|
||||
Auth: &auth.Service{},
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/forgot-password", bytes.NewBufferString(`{"email":" "}`))
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleForgotPassword(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d want 400 body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleForgotPasswordIPRateLimited(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{
|
||||
Mail: &recordingMailer{enabled: true},
|
||||
Auth: &auth.Service{},
|
||||
}
|
||||
s.ensureForgotPasswordLimiters()
|
||||
s.forgotPasswordIPRL = newSlidingWindowLimiter(1, time.Minute)
|
||||
s.forgotPasswordEmailRL = newSlidingWindowLimiter(10, time.Hour)
|
||||
key := "forgot-password-ip:203.0.113.50:1"
|
||||
if !s.forgotPasswordIPRL.allow(key) {
|
||||
t.Fatal("setup: expected first allow")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/forgot-password", bytes.NewBufferString(`{"email":"user@example.com"}`))
|
||||
req.RemoteAddr = "203.0.113.50:1"
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleForgotPassword(rec, req)
|
||||
if rec.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("status=%d want 429 body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec.Header().Get("Retry-After") == "" {
|
||||
t.Fatal("expected Retry-After")
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "@") {
|
||||
t.Fatalf("rate-limit body must not include email: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleForgotPasswordEmailRateLimited(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{
|
||||
Mail: &recordingMailer{enabled: true},
|
||||
Auth: &auth.Service{},
|
||||
}
|
||||
s.ensureForgotPasswordLimiters()
|
||||
s.forgotPasswordIPRL = newSlidingWindowLimiter(10, time.Minute)
|
||||
s.forgotPasswordEmailRL = newSlidingWindowLimiter(1, time.Hour)
|
||||
emailKey := "forgot-password-email:user@example.com"
|
||||
if !s.forgotPasswordEmailRL.allow(emailKey) {
|
||||
t.Fatal("setup: expected first allow")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/forgot-password", bytes.NewBufferString(`{"email":"User@Example.com"}`))
|
||||
req.RemoteAddr = "198.51.100.10:9"
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleForgotPassword(rec, req)
|
||||
if rec.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("status=%d want 429 body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec.Header().Get("Retry-After") == "" {
|
||||
t.Fatal("expected Retry-After")
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "@") {
|
||||
t.Fatalf("rate-limit body must not include email: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleResetPasswordAuthRequired(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/reset-password", bytes.NewBufferString(`{"token":"x","password":"password12"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleResetPassword(rec, req)
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%d want 503", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleForgotPasswordSkipsSyntheticEmail(t *testing.T) {
|
||||
t.Parallel()
|
||||
mailer := &recordingMailer{enabled: true}
|
||||
// No Pool: IssuePasswordReset must refuse @legacy.local before any DB access.
|
||||
s := &Server{
|
||||
Mail: mailer,
|
||||
Auth: &auth.Service{},
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/forgot-password",
|
||||
bytes.NewBufferString(`{"email":" Synth_User@Legacy.Local "}`))
|
||||
req.RemoteAddr = "203.0.113.83:1"
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleForgotPassword(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d want 200 body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var opaque map[string]string
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &opaque); err != nil {
|
||||
t.Fatalf("json: %v", err)
|
||||
}
|
||||
if opaque["status"] != "ok" {
|
||||
t.Fatalf("opaque=%v", opaque)
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "legacy.local") || strings.Contains(rec.Body.String(), "synth") {
|
||||
t.Fatalf("response must not leak synthetic email: %s", rec.Body.String())
|
||||
}
|
||||
if len(mailer.sent) != 0 {
|
||||
t.Fatalf("synthetic emails must not receive mail, got %d", len(mailer.sent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestForgotPasswordMessageLink(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := mail.ForgotPasswordMessage("http://localhost:5174/", "a@example.com", "tok123")
|
||||
if msg.To != "a@example.com" {
|
||||
t.Fatalf("to=%q", msg.To)
|
||||
}
|
||||
if !strings.Contains(msg.Text, "/reset-password#token=tok123") {
|
||||
t.Fatalf("text missing reset link: %s", msg.Text)
|
||||
}
|
||||
if strings.Contains(msg.Text, "/accept-invite") {
|
||||
t.Fatal("forgot-password mail must not use accept-invite")
|
||||
}
|
||||
if msg.Subject != "Reset your Descrybe password" {
|
||||
t.Fatalf("subject=%q", msg.Subject)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user