mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
455e2f8be5
Public config now comes from admin as a flat visibility=1 map instead of a cross-plugin query that compared an integer column to "visible". Login and register resolve CaptchaService per request so CAP is not skipped when user applies before cap.
151 lines
4.0 KiB
Go
151 lines
4.0 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package user_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"Wavelet/core"
|
|
"Wavelet/core/contracts"
|
|
"Wavelet/plugins/domain/user"
|
|
)
|
|
|
|
func TestApplyWithoutCaptchaServiceKeepsAuthRoutes(t *testing.T) {
|
|
ctx := core.NewContext(context.Background())
|
|
if err := user.New().Apply(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]bool{
|
|
"POST /api/v1/user/login": false,
|
|
"POST /api/v1/user/register": false,
|
|
"POST /api/v1/user/send-email-code": false,
|
|
}
|
|
for _, rd := range ctx.Router().Routes() {
|
|
key := rd.Method + " " + rd.Path
|
|
if _, ok := want[key]; ok {
|
|
want[key] = true
|
|
}
|
|
}
|
|
for key, ok := range want {
|
|
if !ok {
|
|
t.Errorf("missing route %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInjectDoesNotRequireCaptchaService(t *testing.T) {
|
|
for _, dep := range user.New().Inject() {
|
|
if dep == reflect.TypeFor[contracts.CaptchaService]() {
|
|
t.Fatal("CaptchaService must not be a hard Inject() dependency")
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeCaptchaService struct{}
|
|
|
|
func (fakeCaptchaService) VerifyMiddleware(scope string) any {
|
|
return gin.HandlerFunc(func(c *gin.Context) { c.Next() })
|
|
}
|
|
|
|
func (fakeCaptchaService) ChallengeHandler() any { return gin.HandlerFunc(func(c *gin.Context) {}) }
|
|
|
|
func (fakeCaptchaService) RedeemHandler() any { return gin.HandlerFunc(func(c *gin.Context) {}) }
|
|
|
|
func TestApplyWithCaptchaServiceWrapsLogin(t *testing.T) {
|
|
ctx := core.NewContext(context.Background())
|
|
core.Provide[contracts.CaptchaService](ctx, fakeCaptchaService{})
|
|
if err := user.New().Apply(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, rd := range ctx.Router().Routes() {
|
|
if rd.Method == "POST" && rd.Path == "/api/v1/user/login" {
|
|
if len(rd.Handlers) <= 1 {
|
|
t.Fatalf("login handler chain length = %d, want > 1", len(rd.Handlers))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("missing POST /api/v1/user/login")
|
|
}
|
|
|
|
type denyCaptchaService struct{}
|
|
|
|
func (denyCaptchaService) VerifyMiddleware(string) any {
|
|
return gin.HandlerFunc(func(c *gin.Context) {
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func (denyCaptchaService) ChallengeHandler() any { return gin.HandlerFunc(func(c *gin.Context) {}) }
|
|
|
|
func (denyCaptchaService) RedeemHandler() any { return gin.HandlerFunc(func(c *gin.Context) {}) }
|
|
|
|
func TestLoginCaptchaGuardResolvesServiceAfterApply(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
ctx := core.NewContext(context.Background())
|
|
if err := user.New().Apply(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
core.Provide[contracts.CaptchaService](ctx, denyCaptchaService{})
|
|
|
|
handler := loginCaptchaGuard(t, ctx)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/user/login", nil)
|
|
handler(c)
|
|
|
|
if !c.IsAborted() {
|
|
t.Fatal("login captcha guard did not abort after late CaptchaService provide")
|
|
}
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusUnauthorized)
|
|
}
|
|
}
|
|
|
|
func TestLoginCaptchaGuardPassesWithoutCaptchaService(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
ctx := core.NewContext(context.Background())
|
|
if err := user.New().Apply(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
handler := loginCaptchaGuard(t, ctx)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/user/login", nil)
|
|
handler(c)
|
|
|
|
if c.IsAborted() {
|
|
t.Fatal("login captcha guard aborted without CaptchaService")
|
|
}
|
|
}
|
|
|
|
func loginCaptchaGuard(t *testing.T, ctx *core.Context) gin.HandlerFunc {
|
|
t.Helper()
|
|
for _, rd := range ctx.Router().Routes() {
|
|
if rd.Method != "POST" || rd.Path != "/api/v1/user/login" {
|
|
continue
|
|
}
|
|
if len(rd.Handlers) == 0 {
|
|
t.Fatal("POST /api/v1/user/login has no handlers")
|
|
}
|
|
switch h := rd.Handlers[0].(type) {
|
|
case gin.HandlerFunc:
|
|
return h
|
|
case func(*gin.Context):
|
|
return h
|
|
default:
|
|
t.Fatalf("unexpected handler type %T", rd.Handlers[0])
|
|
}
|
|
}
|
|
t.Fatal("missing POST /api/v1/user/login")
|
|
return nil
|
|
}
|