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:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
@@ -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)
}
}