Files
OpenFlare/internal/apps/user/routers_test.go
T
2026-06-10 11:33:33 +08:00

243 lines
7.7 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package user
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func setupUserTestRouter(t *testing.T) *gin.Engine {
t.Helper()
oldCookieName := config.Config.App.SessionCookieName
oldSecret := config.Config.App.SessionSecret
oldDomain := config.Config.App.SessionDomain
oldSecure := config.Config.App.SessionSecure
oldHTTPOnly := config.Config.App.SessionHTTPOnly
t.Cleanup(func() {
config.Config.App.SessionCookieName = oldCookieName
config.Config.App.SessionSecret = oldSecret
config.Config.App.SessionDomain = oldDomain
config.Config.App.SessionSecure = oldSecure
config.Config.App.SessionHTTPOnly = oldHTTPOnly
})
config.Config.App.SessionCookieName = "test_session_id"
config.Config.App.SessionSecret = "test_session_secret"
config.Config.App.SessionDomain = ""
config.Config.App.SessionSecure = false
config.Config.App.SessionHTTPOnly = true
gin.SetMode(gin.TestMode)
r := gin.New()
store := cookie.NewStore([]byte(config.Config.App.SessionSecret))
store.Options(util.GetSessionOptions(3600))
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, store))
api := r.Group("/api/v1")
api.POST("/user/register", Register)
api.POST("/user/login", Login)
api.GET("/user-info", oauth.LoginRequired(), oauth.UserInfo)
return r
}
func performUserRequest(r http.Handler, method, path string, body []byte, cookies []*http.Cookie) *httptest.ResponseRecorder {
var reader *bytes.Reader
if body != nil {
reader = bytes.NewReader(body)
} else {
reader = bytes.NewReader(nil)
}
req, _ := http.NewRequest(method, path, reader)
req.Header.Set("Content-Type", "application/json")
for _, c := range cookies {
req.AddCookie(c)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func sessionCookieFromResponse(t *testing.T, w *httptest.ResponseRecorder) *http.Cookie {
t.Helper()
for _, c := range w.Result().Cookies() {
if c.Name == config.Config.App.SessionCookieName {
return c
}
}
t.Fatalf("sessionCookieFromResponse() did not find %q cookie", config.Config.App.SessionCookieName)
return nil
}
func basicUserInfoFromResponse(t *testing.T, w *httptest.ResponseRecorder) oauth.BasicUserInfo {
t.Helper()
var resp util.ResponseAny
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("basicUserInfoFromResponse() decode response failed: %v", err)
}
if resp.ErrorMsg != "" {
t.Fatalf("basicUserInfoFromResponse() error_msg = %q, want empty", resp.ErrorMsg)
}
data, _ := json.Marshal(resp.Data)
var info oauth.BasicUserInfo
if err := json.Unmarshal(data, &info); err != nil {
t.Fatalf("basicUserInfoFromResponse() decode data failed: %v", err)
}
return info
}
func TestEmailCooldownKeyIncludesScene(t *testing.T) {
email := "user@example.com"
loginKey := getEmailCooldownKey("login", email)
registerKey := getEmailCooldownKey("register", email)
if loginKey == registerKey {
t.Errorf("getEmailCooldownKey(%q, %q) = %q, want different key from register scene", "login", email, loginKey)
}
if want := "email_code:cooldown:login:user@example.com"; loginKey != want {
t.Errorf("getEmailCooldownKey(%q, %q) = %q, want %q", "login", email, loginKey, want)
}
}
func TestGenerateVerificationCode(t *testing.T) {
code, err := generateVerificationCode()
if err != nil {
t.Fatalf("generateVerificationCode() error = %v, want nil", err)
}
if len(code) != 6 {
t.Fatalf("generateVerificationCode() length = %d, want 6. Code: %q", len(code), code)
}
for _, r := range code {
if r < '0' || r > '9' {
t.Fatalf("generateVerificationCode() = %q, want only digits", code)
}
}
}
func TestRegisterCreatesAuthenticatedEncryptedUser(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
router := setupUserTestRouter(t)
payload := registerRequest{
Username: "newuser",
Password: "newpassword123",
Nickname: "New User",
}
body, _ := json.Marshal(payload)
w := performUserRequest(router, http.MethodPost, "/api/v1/user/register", body, nil)
if w.Code != http.StatusOK {
t.Fatalf("Register(%q) status = %d, want %d. Body: %s", payload.Username, w.Code, http.StatusOK, w.Body.String())
}
info := basicUserInfoFromResponse(t, w)
if info.NeedChangePassword {
t.Errorf("Register(%q) need_change_password = true, want false", payload.Username)
}
var dbUser model.User
if err := dbConn.Where("username = ?", payload.Username).First(&dbUser).Error; err != nil {
t.Fatalf("Register(%q) db lookup failed: %v", payload.Username, err)
}
if dbUser.ID < 1000 {
t.Errorf("Register(%q) user ID = %d, want generated snowflake ID", payload.Username, dbUser.ID)
}
if !dbUser.IsPasswordEncrypted() {
t.Errorf("Register(%q) stored plaintext password, want encrypted password", payload.Username)
}
if !dbUser.CheckPassword(payload.Password) {
t.Errorf("Register(%q) stored password does not match original password", payload.Username)
}
sessionCookie := sessionCookieFromResponse(t, w)
w = performUserRequest(router, http.MethodGet, "/api/v1/user-info", nil, []*http.Cookie{sessionCookie})
if w.Code != http.StatusOK {
t.Fatalf("UserInfo() after Register(%q) status = %d, want %d. Body: %s", payload.Username, w.Code, http.StatusOK, w.Body.String())
}
}
func TestLoginRequiresPasswordChangeForInitialPlaintextAdmin(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
const (
adminID = uint64(1)
adminUsername = "admin"
adminPassword = "12345678"
)
now := time.Now()
if err := dbConn.Exec(
`INSERT INTO users (id, username, password, nickname, is_active, is_admin, last_login_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
adminID,
adminUsername,
adminPassword,
"Administrator",
true,
true,
now,
now,
now,
).Error; err != nil {
t.Fatalf("seed initial admin failed: %v", err)
}
router := setupUserTestRouter(t)
payload := loginRequest{
Username: adminUsername,
Password: adminPassword,
}
body, _ := json.Marshal(payload)
w := performUserRequest(router, http.MethodPost, "/api/v1/user/login", body, nil)
if w.Code != http.StatusOK {
t.Fatalf("Login(%q) status = %d, want %d. Body: %s", adminUsername, w.Code, http.StatusOK, w.Body.String())
}
info := basicUserInfoFromResponse(t, w)
if !info.NeedChangePassword {
t.Errorf("Login(%q) need_change_password = false, want true", adminUsername)
}
var dbUser model.User
if err := dbConn.Where("username = ?", adminUsername).First(&dbUser).Error; err != nil {
t.Fatalf("Login(%q) db lookup failed: %v", adminUsername, err)
}
if dbUser.ID != adminID {
t.Errorf("Login(%q) user ID = %d, want %d", adminUsername, dbUser.ID, adminID)
}
if dbUser.IsPasswordEncrypted() {
t.Errorf("Login(%q) encrypted password during login, want plaintext until password change", adminUsername)
}
if !dbUser.CheckPassword(adminPassword) {
t.Errorf("Login(%q) stored password does not match original password", adminUsername)
}
sessionCookie := sessionCookieFromResponse(t, w)
w = performUserRequest(router, http.MethodGet, "/api/v1/user-info", nil, []*http.Cookie{sessionCookie})
if w.Code != http.StatusOK {
t.Fatalf("UserInfo() after Login(%q) status = %d, want %d. Body: %s", adminUsername, w.Code, http.StatusOK, w.Body.String())
}
info = basicUserInfoFromResponse(t, w)
if !info.NeedChangePassword {
t.Errorf("UserInfo() after Login(%q) need_change_password = false, want true", adminUsername)
}
}