Files
descrybe/apps/api/internal/httpapi/admin_set_password_test.go
T
greeneclipse 8580c996c3 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.
2026-08-09 22:47:43 +02:00

118 lines
3.6 KiB
Go

package httpapi
import (
"bytes"
"context"
"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"
"github.com/google/uuid"
)
type recordingMailer struct {
enabled bool
sent []mail.Message
err error
}
func (m *recordingMailer) Enabled() bool { return m.enabled }
func (m *recordingMailer) Send(msg mail.Message) error {
if m.err != nil {
return m.err
}
m.sent = append(m.sent, msg)
return nil
}
func TestHandleAdminSendSetPasswordEmailsUnauthorized(t *testing.T) {
t.Parallel()
s := &Server{Mail: &recordingMailer{enabled: true}, Auth: &auth.Service{}}
req := httptest.NewRequest(http.MethodPost, "/api/admin/emails/set-password", bytes.NewBufferString("{}"))
rec := httptest.NewRecorder()
s.handleAdminSendSetPasswordEmails(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status=%d want 401", rec.Code)
}
}
func TestHandleAdminSendSetPasswordEmailsMailerRequired(t *testing.T) {
t.Parallel()
adminID := uuid.New()
s := &Server{Auth: &auth.Service{}}
ctx := context.WithValue(context.Background(), ctxUserID, adminID)
req := httptest.NewRequest(http.MethodPost, "/api/admin/emails/set-password", bytes.NewBufferString("{}"))
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
s.handleAdminSendSetPasswordEmails(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d want 503", rec.Code)
}
}
func TestHandleAdminSendSetPasswordEmailsRateLimited(t *testing.T) {
t.Parallel()
adminID := uuid.New()
s := &Server{
Mail: &recordingMailer{enabled: true},
Auth: &auth.Service{},
}
s.ensureAdminSetPasswordLimiters()
s.adminSetPasswordReqRL = newSlidingWindowLimiter(1, time.Minute)
reqKey := "admin-set-password:" + adminID.String()
if !s.adminSetPasswordReqRL.allow(reqKey) {
t.Fatal("setup: expected first allow")
}
ctx := context.WithValue(context.Background(), ctxUserID, adminID)
req := httptest.NewRequest(http.MethodPost, "/api/admin/emails/set-password", bytes.NewBufferString("{}"))
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
s.handleAdminSendSetPasswordEmails(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 header")
}
raw := rec.Body.String()
if strings.Contains(raw, "@") {
t.Fatalf("rate-limit response must not include email addresses: %s", raw)
}
}
func TestHandleAdminSendSetPasswordEmailsSendRateLimitedSingleUser(t *testing.T) {
t.Parallel()
adminID := uuid.New()
targetID := uuid.New()
s := &Server{
Mail: &recordingMailer{enabled: true},
Auth: &auth.Service{},
}
s.ensureAdminSetPasswordLimiters()
s.adminSetPasswordReqRL = newSlidingWindowLimiter(10, time.Minute)
s.adminSetPasswordSendRL = newSlidingWindowLimiter(1, time.Minute)
sendKey := "admin-set-password-send:" + adminID.String()
if !s.adminSetPasswordSendRL.allow(sendKey) {
t.Fatal("setup: expected first send allow")
}
body := `{"user_id":"` + targetID.String() + `"}`
ctx := context.WithValue(context.Background(), ctxUserID, adminID)
req := httptest.NewRequest(http.MethodPost, "/api/admin/emails/set-password", bytes.NewBufferString(body))
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
s.handleAdminSendSetPasswordEmails(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 header")
}
}