WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
apperrors "github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// stubRegisterUserService is a UserService whose ONLY useful method is
|
||||
// Register; every other call panics. Using an interface embedding plus a
|
||||
// targeted override keeps the test focused on the Register handler's
|
||||
// branching logic without dragging in the entire user service surface.
|
||||
type stubRegisterUserService struct {
|
||||
interfaces.UserService
|
||||
register func(ctx context.Context, req *types.RegisterRequest) (*types.User, error)
|
||||
}
|
||||
|
||||
func (s *stubRegisterUserService) Register(ctx context.Context, req *types.RegisterRequest) (*types.User, error) {
|
||||
return s.register(ctx, req)
|
||||
}
|
||||
|
||||
// errorCapture mirrors gin's default ErrorHandler behaviour for tests:
|
||||
// when a handler calls c.Error(), we surface it as an HTTP response so the
|
||||
// recorder reflects the real client-visible status. The production
|
||||
// middleware does the same thing in middleware/error_handler.go.
|
||||
func errorCapture() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
if len(c.Errors) == 0 {
|
||||
return
|
||||
}
|
||||
err := c.Errors.Last().Err
|
||||
if appErr, ok := err.(*apperrors.AppError); ok {
|
||||
c.JSON(appErr.HTTPCode, gin.H{"error": appErr.Message})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
func newRegisterTestRouter(h *AuthHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(errorCapture())
|
||||
r.POST("/auth/register", h.Register)
|
||||
return r
|
||||
}
|
||||
|
||||
func doRegister(t *testing.T, r *gin.Engine, body any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
buf, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewReader(buf))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// validRegisterBody returns a payload that passes parameter validation, so
|
||||
// each test is exercising the gate logic and not the body parser.
|
||||
func validRegisterBody() map[string]string {
|
||||
return map[string]string{
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"password": "supersecret1",
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_InviteOnlyRejects(t *testing.T) {
|
||||
// PR 3 (#1303): when auth.registration_mode=invite_only, Register
|
||||
// must respond 403 BEFORE touching the user service. The frontend
|
||||
// already hides the sign-up link via /auth/config; this is the
|
||||
// server-side enforcement for direct API hits.
|
||||
called := false
|
||||
us := &stubRegisterUserService{
|
||||
register: func(context.Context, *types.RegisterRequest) (*types.User, error) {
|
||||
called = true
|
||||
return &types.User{ID: "u1"}, nil
|
||||
},
|
||||
}
|
||||
h := NewAuthHandler(&config.Config{
|
||||
Auth: &config.AuthConfig{RegistrationMode: config.AuthRegistrationModeInviteOnly},
|
||||
}, us, nil, nil, nil)
|
||||
|
||||
w := doRegister(t, newRegisterTestRouter(h), validRegisterBody())
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("invite_only must return 403, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if called {
|
||||
t.Fatalf("UserService.Register must not be called when invite_only blocks the request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_SelfServeAllowsRegistration(t *testing.T) {
|
||||
// Default registration_mode keeps PR 1 behaviour intact: the gate
|
||||
// is dormant and the request reaches the user service. We don't
|
||||
// exercise the real service here — just confirm the gate let it
|
||||
// through by observing the stub being invoked.
|
||||
called := false
|
||||
us := &stubRegisterUserService{
|
||||
register: func(_ context.Context, req *types.RegisterRequest) (*types.User, error) {
|
||||
called = true
|
||||
if req.TenantProvisioning != types.TenantProvisioningCreatePersonal {
|
||||
t.Fatalf("default provisioning = %q, want create_personal", req.TenantProvisioning)
|
||||
}
|
||||
return &types.User{ID: "u1", Email: "alice@example.com"}, nil
|
||||
},
|
||||
}
|
||||
h := NewAuthHandler(&config.Config{
|
||||
Auth: &config.AuthConfig{RegistrationMode: config.AuthRegistrationModeSelfServe},
|
||||
}, us, nil, nil, nil)
|
||||
|
||||
w := doRegister(t, newRegisterTestRouter(h), validRegisterBody())
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("self_serve must allow registration, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !called {
|
||||
t.Fatalf("UserService.Register should have been invoked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_TenantlessProvisioningFromConfig(t *testing.T) {
|
||||
us := &stubRegisterUserService{
|
||||
register: func(_ context.Context, req *types.RegisterRequest) (*types.User, error) {
|
||||
if req.TenantProvisioning != types.TenantProvisioningTenantless {
|
||||
t.Fatalf("provisioning = %q, want tenantless", req.TenantProvisioning)
|
||||
}
|
||||
return &types.User{ID: "u1", Email: "alice@example.com"}, nil
|
||||
},
|
||||
}
|
||||
h := NewAuthHandler(&config.Config{
|
||||
Auth: &config.AuthConfig{
|
||||
RegistrationMode: config.AuthRegistrationModeSelfServe,
|
||||
DefaultTenantMode: config.AuthDefaultTenantModeTenantless,
|
||||
},
|
||||
}, us, nil, nil, nil)
|
||||
|
||||
w := doRegister(t, newRegisterTestRouter(h), validRegisterBody())
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("tenantless self-serve registration got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthConfigExposesComplexPassword(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
h := &AuthHandler{
|
||||
configInfo: &config.Config{Auth: &config.AuthConfig{
|
||||
RegistrationMode: config.AuthRegistrationModeSelfServe,
|
||||
}},
|
||||
systemSettingSvc: &tenantPolicySettingService{enabled: true},
|
||||
}
|
||||
r := gin.New()
|
||||
r.GET("/auth/config", h.GetAuthConfig)
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"complex_password_enabled":true`) {
|
||||
t.Fatalf("body=%s, want complex_password_enabled true", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_NilAuthConfigDoesNotPanic(t *testing.T) {
|
||||
// Defensive: a nil Auth section means the operator hasn't set the
|
||||
// registration mode at all, which must not crash and must keep the
|
||||
// legacy "registration enabled" behaviour. Mirrors the nil guard in
|
||||
// the handler so a config-loading bug doesn't take the server down.
|
||||
us := &stubRegisterUserService{
|
||||
register: func(_ context.Context, _ *types.RegisterRequest) (*types.User, error) {
|
||||
return &types.User{ID: "u1", Email: "alice@example.com"}, nil
|
||||
},
|
||||
}
|
||||
h := NewAuthHandler(&config.Config{}, us, nil, nil, nil)
|
||||
|
||||
w := doRegister(t, newRegisterTestRouter(h), validRegisterBody())
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("nil Auth config must fall back to allow, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegister_PreservesPasswordBytes is the regression for #2521.
|
||||
// SanitizeForLog is for log output only; applying it to credentials
|
||||
// rewrites tabs/newlines/control chars before hashing, so a password
|
||||
// that registers successfully cannot be used at login.
|
||||
func TestRegister_PreservesPasswordBytes(t *testing.T) {
|
||||
// Password contains a tab and a newline — both are rewritten by
|
||||
// SanitizeForLog (tab → space, newline → space). The handler must
|
||||
// pass the exact original string to UserService.Register.
|
||||
const originalPassword = "abc\t123!\nX"
|
||||
|
||||
var gotPassword string
|
||||
us := &stubRegisterUserService{
|
||||
register: func(_ context.Context, req *types.RegisterRequest) (*types.User, error) {
|
||||
gotPassword = req.Password
|
||||
return &types.User{ID: "u1", Email: "alice@example.com"}, nil
|
||||
},
|
||||
}
|
||||
h := NewAuthHandler(&config.Config{
|
||||
Auth: &config.AuthConfig{RegistrationMode: config.AuthRegistrationModeSelfServe},
|
||||
}, us, nil, nil, nil)
|
||||
|
||||
body := validRegisterBody()
|
||||
body["password"] = originalPassword
|
||||
w := doRegister(t, newRegisterTestRouter(h), body)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("registration with control chars in password must succeed, got %d body=%s",
|
||||
w.Code, w.Body.String())
|
||||
}
|
||||
if gotPassword != originalPassword {
|
||||
t.Fatalf("password mutated before UserService.Register:\n got %q\n want %q",
|
||||
gotPassword, originalPassword)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user