Files
ttstd-infinite-canvas/backend/internal/auth/auth_registration_test.go

187 lines
6.6 KiB
Go

package auth
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"yingce/backend/internal/model"
"gorm.io/gorm"
)
func newRegistrationTestService(t *testing.T) (*Service, *gorm.DB) {
t.Helper()
svc, db := newPasswordResetTestService(t)
if err := db.AutoMigrate(&model.UserIdentity{}, &model.OAuthState{}, &model.CreditAccount{}); err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
return svc, db
}
func TestRegisterRequiresAcceptedTermsBeforeWriting(t *testing.T) {
for _, payload := range []string{
`{"username":"new-user","password":"password"}`,
`{"username":"new-user","password":"password","acceptedTerms":false}`,
} {
svc, db := newRegistrationTestService(t)
var req RegisterRequest
if err := json.Unmarshal([]byte(payload), &req); err != nil {
t.Fatal(err)
}
_, err := svc.Register(req)
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Status != 400 || authErr.Message != "请先同意影策服务协议" {
t.Fatalf("Register() error = %v", err)
}
for _, entity := range []any{&model.User{}, &model.AuthSession{}} {
var count int64
if err := db.Model(entity).Count(&count).Error; err != nil || count != 0 {
t.Fatalf("unaccepted registration wrote %T: count=%d err=%v", entity, count, err)
}
}
}
}
func TestRegisterAcceptedTermsCreatesFirstAdmin(t *testing.T) {
svc, _ := newRegistrationTestService(t)
result, err := svc.Register(RegisterRequest{Username: "new-user", Password: "password", AcceptedTerms: true})
if err != nil {
t.Fatal(err)
}
if result.User.Username != "new-user" || result.User.Role != model.UserRoleAdmin || result.Session == "" {
t.Fatalf("Register() result = %#v", result)
}
}
func TestRegisterAcceptedTermsCreatesEmailUser(t *testing.T) {
svc, db := newRegistrationTestService(t)
if err := db.Create(&model.User{ID: "admin", Username: "admin", Role: model.UserRoleAdmin, Status: model.UserStatusActive}).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&model.SystemSetting{Key: registrationSettingKey, ValueJSON: `{"enabled":true}`}).Error; err != nil {
t.Fatal(err)
}
var code string
svc.SetMailSender(func(_ EmailSettingValue, _, _ string, body string) error {
code = codeFromEmailBody(body)
return nil
})
if err := svc.SendRegistrationEmailCode("member@example.com"); err != nil {
t.Fatal(err)
}
result, err := svc.Register(RegisterRequest{Username: "member", Email: "member@example.com", EmailCode: code, Password: "password", AcceptedTerms: true})
if err != nil {
t.Fatal(err)
}
if result.User.Role != model.UserRoleUser || result.Session == "" {
t.Fatalf("Register() result = %#v", result)
}
}
func TestLinuxDORegistrationAgreement(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var payload any
switch r.URL.Path {
case "/token":
payload = map[string]string{"access_token": "test-access-token"}
case "/profile":
payload = map[string]string{"id": "42", "username": "linux-user", "name": "Linux User"}
default:
http.NotFound(w, r)
return
}
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Error(err)
}
}))
defer server.Close()
for _, tc := range []struct {
name string
accepted bool
existing bool
registration bool
wantError string
}{
{name: "new user without agreement", registration: true, wantError: "请先同意影策服务协议"},
{name: "new user with agreement", accepted: true, registration: true},
{name: "existing user without agreement", existing: true, registration: true},
{name: "existing user with registration closed", existing: true},
{name: "agreement does not bypass registration closure", accepted: true, wantError: "管理员未开放新用户注册"},
} {
t.Run(tc.name, func(t *testing.T) {
svc, db := newRegistrationTestService(t)
if err := db.Create(&model.User{ID: "admin", Username: "admin", Role: model.UserRoleAdmin, Status: model.UserStatusActive}).Error; err != nil {
t.Fatal(err)
}
setting, err := json.Marshal(linuxDOSettingValue{Enabled: true, ClientID: "client", AuthorizationURL: server.URL + "/authorize", TokenURL: server.URL + "/token", UserInfoURL: server.URL + "/profile", RedirectURL: "https://example.com/callback"})
if err != nil {
t.Fatal(err)
}
for _, value := range []model.SystemSetting{
{Key: linuxDOSettingKey, ValueJSON: string(setting)},
{Key: registrationSettingKey, ValueJSON: `{"enabled":` + strconv.FormatBool(tc.registration) + `}`},
} {
if err := db.Create(&value).Error; err != nil {
t.Fatal(err)
}
}
if tc.existing {
if err := svc.repo.CreateOAuthUser(
&model.User{ID: "existing-user", Username: "linux-user", Role: model.UserRoleUser, Status: model.UserStatusActive},
&model.UserIdentity{ID: "identity", UserID: "existing-user", Provider: "linuxdo", Subject: "42"},
); err != nil {
t.Fatal(err)
}
}
target, err := svc.BeginLinuxDOLogin("/create", tc.accepted)
if err != nil {
t.Fatal(err)
}
parsed, err := url.Parse(target)
if err != nil {
t.Fatal(err)
}
stateValue := parsed.Query().Get("state")
var saved model.OAuthState
if err := db.Where("state_hash = ?", HashToken(stateValue)).First(&saved).Error; err != nil || saved.AcceptedTerms != tc.accepted {
t.Fatalf("saved consent = %v, err=%v", saved.AcceptedTerms, err)
}
result, err := svc.CompleteLinuxDOLogin(stateValue, "test-code")
wantUsers, wantRecords := int64(2), int64(1)
if tc.wantError != "" {
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Message != tc.wantError {
t.Fatalf("CompleteLinuxDOLogin() error = %v", err)
}
wantUsers, wantRecords = 1, 0
} else if err != nil || result == nil || result.Session.Session == "" || result.Next != "/create" {
t.Fatalf("CompleteLinuxDOLogin() result=%#v err=%v", result, err)
}
if count, err := svc.repo.UserCount(); err != nil || count != wantUsers {
t.Fatalf("user count=%d want=%d err=%v", count, wantUsers, err)
}
for _, entity := range []any{&model.UserIdentity{}, &model.CreditAccount{}, &model.AuthSession{}} {
var count int64
if err := db.Model(entity).Count(&count).Error; err != nil || count != wantRecords {
t.Fatalf("%T count=%d want=%d err=%v", entity, count, wantRecords, err)
}
}
if _, err := svc.CompleteLinuxDOLogin(stateValue, "test-code"); err == nil {
t.Fatal("consumed OAuth state was accepted twice")
}
})
}
}