feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
+129
-11
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/QuantumNous/new-api/i18n"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/service/authz"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -31,7 +33,7 @@ func setupManageUserTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
require.NoError(t, i18n.Init())
|
||||
previousDB, previousLogDB := model.DB, model.LOG_DB
|
||||
previousRedisEnabled := common.RedisEnabled
|
||||
previousRedisEnabled, previousSecret := common.RedisEnabled, common.SessionSecret
|
||||
previousMainDatabaseType, previousLogDatabaseType := common.MainDatabaseType(), common.LogDatabaseType()
|
||||
dialect := os.Getenv("TEST_MANAGE_USER_DIALECT")
|
||||
if dialect == "" {
|
||||
@@ -49,11 +51,12 @@ func setupManageUserTestDB(t *testing.T) *gorm.DB {
|
||||
}
|
||||
model.DB, model.LOG_DB = db, logDB
|
||||
common.RedisEnabled = false
|
||||
common.SessionSecret = "manage-user-test-secret"
|
||||
common.SetDatabaseTypes(databaseTypes[dialect], databaseTypes[dialect])
|
||||
|
||||
t.Cleanup(func() {
|
||||
model.DB, model.LOG_DB = previousDB, previousLogDB
|
||||
common.RedisEnabled = previousRedisEnabled
|
||||
common.RedisEnabled, common.SessionSecret = previousRedisEnabled, previousSecret
|
||||
common.SetDatabaseTypes(previousMainDatabaseType, previousLogDatabaseType)
|
||||
sqlDB, err := db.DB()
|
||||
if err == nil {
|
||||
@@ -66,7 +69,7 @@ func setupManageUserTestDB(t *testing.T) *gorm.DB {
|
||||
}
|
||||
}
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.User{}, &model.UserSession{}, &model.CasbinRule{}, &model.AuthzRole{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.User{}, &model.UserSession{}, &model.CasbinRule{}, &model.AuthzRole{}, &model.AuthFlow{}, &model.TwoFA{}, &model.PasskeyCredential{}, &model.UserAccessToken{}))
|
||||
require.NoError(t, logDB.AutoMigrate(&model.Log{}, &model.AuditLog{}))
|
||||
versionQuery := "SELECT version()"
|
||||
if dialect == "sqlite" {
|
||||
@@ -79,20 +82,50 @@ func setupManageUserTestDB(t *testing.T) *gorm.DB {
|
||||
}
|
||||
|
||||
func performManageUserRequest(t *testing.T, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
return performVerifiedManageUserRequest(t, body, service.AuthIdentity{UserID: 9999}, "")
|
||||
}
|
||||
|
||||
// performVerifiedManageUserRequest drives ManageUser as the root operator from
|
||||
// a dashboard login session, carrying the step-up proof status or role changes need.
|
||||
func performVerifiedManageUserRequest(t *testing.T, body string, identity service.AuthIdentity, proof string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/user/manage", strings.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Set("id", 9999)
|
||||
if proof != "" {
|
||||
c.Request.Header.Set("X-Security-Proof", proof)
|
||||
}
|
||||
c.Set("id", identity.UserID)
|
||||
c.Set("role", common.RoleRootUser)
|
||||
c.Set("username", "root-operator")
|
||||
c.Set("session_id", identity.SessionID)
|
||||
c.Set("auth_version", identity.UserAuthVersion)
|
||||
c.Set("session_version", identity.SessionVersion)
|
||||
c.Set(common.RequestIdKey, "quota-test-request")
|
||||
ManageUser(c)
|
||||
return recorder
|
||||
}
|
||||
|
||||
// manageUserProof signs the root operator in and issues a password-method proof
|
||||
// bound to one ManageUser operation.
|
||||
func manageUserProof(t *testing.T, db *gorm.DB, operation service.VerificationOperation) (service.AuthIdentity, string) {
|
||||
t.Helper()
|
||||
operator := createQuotaTestOperator(t, db, common.RoleRootUser)
|
||||
require.NoError(t, model.PublishUserAuthCache(operator.Id))
|
||||
bundle, err := service.CreateLoginSession(operator.Id, "password", "127.0.0.1", "manage-user-test")
|
||||
require.NoError(t, err)
|
||||
identity, err := service.ParseAccessToken(bundle.AccessToken)
|
||||
require.NoError(t, err)
|
||||
binding, err := service.BindVerificationOperation(operation)
|
||||
require.NoError(t, err)
|
||||
proof, _, err := service.IssueSecurityProof(identity, service.VerificationMethodPassword, binding)
|
||||
require.NoError(t, err)
|
||||
return identity, proof
|
||||
}
|
||||
|
||||
func TestManageUserDisableAdvancesAuthVersionOnceAndRevokesSession(t *testing.T) {
|
||||
db := setupManageUserTestDB(t)
|
||||
now := time.Now().Unix()
|
||||
@@ -107,7 +140,8 @@ func TestManageUserDisableAdvancesAuthVersionOnceAndRevokesSession(t *testing.T)
|
||||
LastActiveAt: now, ExpiresAt: now + 3600,
|
||||
}).Error)
|
||||
|
||||
recorder := performManageUserRequest(t, fmt.Sprintf(`{"id":%d,"action":"disable"}`, user.Id))
|
||||
identity, proof := manageUserProof(t, db, service.VerificationOperation{Scope: service.VerificationScopeAdminUserManage, Context: []byte(fmt.Sprintf(`{"user_id":%d,"action":"disable"}`, user.Id))})
|
||||
recorder := performVerifiedManageUserRequest(t, fmt.Sprintf(`{"id":%d,"action":"disable"}`, user.Id), identity, proof)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), `"success":true`)
|
||||
|
||||
@@ -148,7 +182,8 @@ func TestManageUserDemoteAdvancesAuthVersionAndRevokesSessionsOnce(t *testing.T)
|
||||
}
|
||||
}))
|
||||
|
||||
recorder := performManageUserRequest(t, fmt.Sprintf(`{"id":%d,"action":"demote"}`, user.Id))
|
||||
identity, proof := manageUserProof(t, db, service.VerificationOperation{Scope: service.VerificationScopeAdminUserManage, Context: []byte(fmt.Sprintf(`{"user_id":%d,"action":"demote"}`, user.Id))})
|
||||
recorder := performVerifiedManageUserRequest(t, fmt.Sprintf(`{"id":%d,"action":"demote"}`, user.Id), identity, proof)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), `"success":true`)
|
||||
|
||||
@@ -174,7 +209,8 @@ func TestManageUserDeleteReturnsImmediatelyAndUnknownActionFails(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, db.Create(&deleted).Error)
|
||||
|
||||
recorder := performManageUserRequest(t, fmt.Sprintf(`{"id":%d,"action":"delete"}`, deleted.Id))
|
||||
identity, proof := manageUserProof(t, db, service.VerificationOperation{Scope: service.VerificationScopeAdminUserDelete, Context: []byte(fmt.Sprintf(`{"user_id":%d}`, deleted.Id))})
|
||||
recorder := performVerifiedManageUserRequest(t, fmt.Sprintf(`{"id":%d,"action":"delete"}`, deleted.Id), identity, proof)
|
||||
assert.Contains(t, recorder.Body.String(), `"success":true`)
|
||||
var deletedCount int64
|
||||
require.NoError(t, db.Unscoped().Model(&model.User{}).Where("id = ? AND deleted_at IS NOT NULL", deleted.Id).Count(&deletedCount).Error)
|
||||
@@ -197,7 +233,7 @@ func createQuotaTestOperator(t *testing.T, db *gorm.DB, role int) model.User {
|
||||
if role == 0 {
|
||||
role = common.RoleRootUser
|
||||
}
|
||||
operator := model.User{Id: 9999, Username: "root-operator", Role: role, Status: common.UserStatusEnabled, AuthVersion: 1, AffCode: "root-operator-aff"}
|
||||
operator := model.User{Id: 9999, Username: "root-operator", Password: "root-operator-hash", Role: role, Status: common.UserStatusEnabled, AuthVersion: 1, AffCode: "root-operator-aff"}
|
||||
require.NoError(t, db.Create(&operator).Error)
|
||||
return operator
|
||||
}
|
||||
@@ -227,7 +263,7 @@ func TestManageUserQuotaRecordsTopupAndAudit(t *testing.T) {
|
||||
require.NoError(t, db.First(&user, user.Id).Error)
|
||||
assert.Equal(t, tc.wantQuota, user.Quota)
|
||||
|
||||
logs, total, err := model.GetAllLogs(model.LogTypeTopup, 0, 0, "", "", "", 0, 20, 0, "", "", "")
|
||||
logs, total, err := model.GetAllLogs(model.LogFilter{Type: model.LogTypeTopup}, 0, 20)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, total)
|
||||
require.Len(t, logs, 1)
|
||||
@@ -239,7 +275,7 @@ func TestManageUserQuotaRecordsTopupAndAudit(t *testing.T) {
|
||||
require.NoError(t, common.UnmarshalJsonStr(logs[0].Other, &other))
|
||||
assert.Equal(t, &model.AuditAdminInfo{AdminID: 9999, AdminUsername: "root-operator", AdminRole: common.RoleRootUser, AuthMethod: "session"}, other.AdminInfo)
|
||||
|
||||
logs, total, err = model.GetUserLogs(user.Id, model.LogTypeTopup, 0, 0, "", "", 0, 20, "", "", "")
|
||||
logs, total, err = model.GetUserLogs(user.Id, model.LogFilter{Type: model.LogTypeTopup}, 0, 20)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, total)
|
||||
require.Len(t, logs, 1)
|
||||
@@ -260,7 +296,7 @@ func TestManageUserQuotaRecordsTopupAndAudit(t *testing.T) {
|
||||
assert.Equal(t, "quota-test-request", logs[0].RequestId)
|
||||
assert.Empty(t, logs[0].Ip, "recipient logs must not disclose the administrator IP")
|
||||
|
||||
logs, total, err = model.GetUserLogs(9999, model.LogTypeTopup, 0, 0, "", "", 0, 20, "", "", "")
|
||||
logs, total, err = model.GetUserLogs(9999, model.LogFilter{Type: model.LogTypeTopup}, 0, 20)
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, total)
|
||||
assert.Empty(t, logs)
|
||||
@@ -606,3 +642,85 @@ func TestManageUserQuotaCacheUsesCommittedIntegerDifference(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Saving notification settings starts from what the user stored: fields the
|
||||
// form does not manage stay, and switches an older client leaves out keep
|
||||
// their stored values. Only administrators change the channel and upstream
|
||||
// switches, and a threshold of 0 means the site default.
|
||||
func TestUpdateUserSettingKeepsWhatTheRequestLeavesOut(t *testing.T) {
|
||||
on, off := true, false
|
||||
stored := dto.UserSetting{
|
||||
NotifyType: dto.NotifyTypeBark,
|
||||
BarkUrl: "https://api.day.app/key/{{title}}/{{content}}",
|
||||
QuotaWarningThreshold: 500000,
|
||||
QuotaWarningEnabled: &off,
|
||||
ChannelNotifyEnabled: &on,
|
||||
UpstreamModelUpdateNotifyEnabled: true,
|
||||
Language: "zhCN",
|
||||
SidebarModules: `{"chat":{"enabled":true}}`,
|
||||
BillingPreference: "wallet_only",
|
||||
}
|
||||
kept := func(setting dto.UserSetting) dto.UserSetting {
|
||||
setting.Language, setting.SidebarModules, setting.BillingPreference = stored.Language, stored.SidebarModules, stored.BillingPreference
|
||||
return setting
|
||||
}
|
||||
switchesChanged := `{"notify_type":"email","quota_warning_threshold":0,"quota_warning_enabled":true,"channel_notify_enabled":false,"upstream_model_update_notify_enabled":false}`
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
role int
|
||||
body string
|
||||
wantSuccess bool
|
||||
want dto.UserSetting
|
||||
}{
|
||||
{
|
||||
name: "older client without the new switches",
|
||||
role: common.RoleAdminUser,
|
||||
body: `{"notify_type":"webhook","quota_warning_threshold":2000,"webhook_url":"https://example.com/hook","webhook_secret":"hook-secret","accept_unset_model_ratio_model":true,"record_ip_log":true}`,
|
||||
wantSuccess: true,
|
||||
want: kept(dto.UserSetting{
|
||||
NotifyType: dto.NotifyTypeWebhook, WebhookUrl: "https://example.com/hook", WebhookSecret: "hook-secret",
|
||||
QuotaWarningThreshold: 2000, QuotaWarningEnabled: &off, ChannelNotifyEnabled: &on, UpstreamModelUpdateNotifyEnabled: true,
|
||||
AcceptUnsetRatioModel: true, RecordIpLog: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "administrator changes the switches and uses the site default threshold",
|
||||
role: common.RoleAdminUser,
|
||||
body: switchesChanged,
|
||||
wantSuccess: true,
|
||||
want: kept(dto.UserSetting{NotifyType: dto.NotifyTypeEmail, QuotaWarningEnabled: &on, ChannelNotifyEnabled: &off}),
|
||||
},
|
||||
{
|
||||
name: "user cannot change the administrator switches",
|
||||
role: common.RoleCommonUser,
|
||||
body: switchesChanged,
|
||||
wantSuccess: true,
|
||||
want: kept(dto.UserSetting{NotifyType: dto.NotifyTypeEmail, QuotaWarningEnabled: &on, ChannelNotifyEnabled: &on, UpstreamModelUpdateNotifyEnabled: true}),
|
||||
},
|
||||
{
|
||||
name: "negative threshold",
|
||||
role: common.RoleCommonUser,
|
||||
body: `{"notify_type":"email","quota_warning_threshold":-1}`,
|
||||
want: stored,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
db := setupManageUserTestDB(t)
|
||||
setting, err := common.Marshal(stored)
|
||||
require.NoError(t, err)
|
||||
user := model.User{Username: "setting-owner", Role: tc.role, Status: common.UserStatusEnabled, AffCode: "setting-owner-aff", Setting: string(setting)}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
c.Request = httptest.NewRequest(http.MethodPut, "/api/user/setting", strings.NewReader(tc.body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Set("id", user.Id)
|
||||
UpdateUserSetting(c)
|
||||
|
||||
assert.Contains(t, recorder.Body.String(), fmt.Sprintf(`"success":%t`, tc.wantSuccess))
|
||||
require.NoError(t, db.First(&user, user.Id).Error)
|
||||
assert.Equal(t, tc.want, user.GetSetting())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user