修复角色日志迁移与插件授权测试

This commit is contained in:
smail 2026-09-10 11:49:06 +08:00
parent 46fd3a6bff
commit c6071414df
15 changed files with 153 additions and 76 deletions

View File

@ -19,7 +19,7 @@ const (
) )
func GetPluginList(ctx *context.Context, w http.ResponseWriter, req *http.Request) { func GetPluginList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if err := authorizeCapability(ctx, access.CapabilityPluginRead, "", false); err != nil { if !authenticatedPluginUser(ctx) || authorizeCapability(ctx, access.CapabilityPluginRead, "", false) != nil {
response.NewErrorResponse(response.NoAccessPrivileges, "\u65e0\u6743\u8bbf\u95ee", "").FPrint(w) response.NewErrorResponse(response.NoAccessPrivileges, "\u65e0\u6743\u8bbf\u95ee", "").FPrint(w)
return return
} }
@ -32,7 +32,7 @@ func GetPluginList(ctx *context.Context, w http.ResponseWriter, req *http.Reques
} }
func SettingsHtml(ctx *context.Context, w http.ResponseWriter, req *http.Request) { func SettingsHtml(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if err := authorizeCapability(ctx, access.CapabilityPluginReadGlobal, "", false); err != nil { if !authenticatedPluginUser(ctx) || authorizeCapability(ctx, access.CapabilityPluginReadGlobal, "", false) != nil {
response.NewErrorResponse(response.NoAccessPrivileges, "\u65e0\u6743\u8bbf\u95ee", "").FPrint(w) response.NewErrorResponse(response.NoAccessPrivileges, "\u65e0\u6743\u8bbf\u95ee", "").FPrint(w)
return return
} }

View File

@ -10,6 +10,7 @@ import (
"git.pcux.cn/xuans/Smail/hooks" "git.pcux.cn/xuans/Smail/hooks"
hooksframework "git.pcux.cn/xuans/Smail/hooks/framework" hooksframework "git.pcux.cn/xuans/Smail/hooks/framework"
"git.pcux.cn/xuans/Smail/models" "git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/services/access"
"git.pcux.cn/xuans/Smail/utils/context" "git.pcux.cn/xuans/Smail/utils/context"
) )
@ -43,7 +44,7 @@ func TestSettingsHtmlRejectsPathTraversal(t *testing.T) {
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/plugin/settings/%2e%2e/index.html", nil) request := httptest.NewRequest(http.MethodGet, "/api/plugin/settings/%2e%2e/index.html", nil)
SettingsHtml(&context.Context{IsAdmin: true}, recorder, request) SettingsHtml(&context.Context{UserID: 1, Role: access.RoleSuperAdmin}, recorder, request)
if strings.Contains(recorder.Body.String(), "ok") { if strings.Contains(recorder.Body.String(), "ok") {
t.Fatal("path traversal reached a plugin") t.Fatal("path traversal reached a plugin")
} }
@ -56,13 +57,13 @@ func TestSettingsHtmlSeparatesStaticAndSaveRequests(t *testing.T) {
hooks.HookList = map[string]hooksframework.EmailHook{"PluginTest": hook} hooks.HookList = map[string]hooksframework.EmailHook{"PluginTest": hook}
getRecorder := httptest.NewRecorder() getRecorder := httptest.NewRecorder()
SettingsHtml(&context.Context{IsAdmin: true}, getRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/PluginTest/index.html", nil)) SettingsHtml(&context.Context{UserID: 1, Role: access.RoleSuperAdmin}, getRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/PluginTest/index.html", nil))
if hook.resource != "PluginTest/index.html" || !strings.HasPrefix(getRecorder.Header().Get("Content-Type"), "text/html") { if hook.resource != "PluginTest/index.html" || !strings.HasPrefix(getRecorder.Header().Get("Content-Type"), "text/html") {
t.Fatalf("static plugin page was not served safely: resource=%s type=%s", hook.resource, getRecorder.Header().Get("Content-Type")) t.Fatalf("static plugin page was not served safely: resource=%s type=%s", hook.resource, getRecorder.Header().Get("Content-Type"))
} }
postRecorder := httptest.NewRecorder() postRecorder := httptest.NewRecorder()
SettingsHtml(&context.Context{IsAdmin: true}, postRecorder, httptest.NewRequest(http.MethodPost, "/api/plugin/settings/PluginTest/save", strings.NewReader(`{"url":"https://model.example.com"}`))) SettingsHtml(&context.Context{UserID: 1, Role: access.RoleSuperAdmin}, postRecorder, httptest.NewRequest(http.MethodPost, "/api/plugin/settings/PluginTest/save", strings.NewReader(`{"url":"https://model.example.com"}`)))
if postRecorder.Code != http.StatusGone { if postRecorder.Code != http.StatusGone {
t.Fatalf("legacy plugin save status = %d, want %d", postRecorder.Code, http.StatusGone) t.Fatalf("legacy plugin save status = %d, want %d", postRecorder.Code, http.StatusGone)
} }
@ -78,7 +79,7 @@ func TestSettingsHtmlRejectsOversizedPluginResponse(t *testing.T) {
hooks.HookList = map[string]hooksframework.EmailHook{"PluginTest": hook} hooks.HookList = map[string]hooksframework.EmailHook{"PluginTest": hook}
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
SettingsHtml(&context.Context{IsAdmin: true}, recorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/PluginTest/index.html", nil)) SettingsHtml(&context.Context{UserID: 1, Role: access.RoleSuperAdmin}, recorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/PluginTest/index.html", nil))
if strings.Contains(recorder.Body.String(), hook.response) { if strings.Contains(recorder.Body.String(), hook.response) {
t.Fatal("oversized plugin response was returned") t.Fatal("oversized plugin response was returned")
} }
@ -109,11 +110,17 @@ func TestPluginConfigurationRejectsNonAdminAccess(t *testing.T) {
listRecorder := httptest.NewRecorder() listRecorder := httptest.NewRecorder()
GetPluginList(&context.Context{}, listRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/list", nil)) GetPluginList(&context.Context{}, listRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/list", nil))
if !strings.Contains(listRecorder.Body.String(), "\u65e0\u6743\u8bbf\u95ee") { if !strings.Contains(listRecorder.Body.String(), "\u65e0\u6743\u8bbf\u95ee") {
t.Fatalf("non-admin plugin list was not rejected: body=%s", listRecorder.Body.String()) t.Fatalf("unauthenticated plugin list was not rejected: body=%s", listRecorder.Body.String())
}
userListRecorder := httptest.NewRecorder()
GetPluginList(&context.Context{UserID: 2, Role: access.RoleUser}, userListRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/list", nil))
if !strings.Contains(userListRecorder.Body.String(), "PluginTest") {
t.Fatalf("authenticated user cannot read plugin list: body=%s", userListRecorder.Body.String())
} }
pageRecorder := httptest.NewRecorder() pageRecorder := httptest.NewRecorder()
SettingsHtml(&context.Context{}, pageRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/PluginTest/index.html", nil)) SettingsHtml(&context.Context{UserID: 2, Role: access.RoleUser}, pageRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/PluginTest/index.html", nil))
if !strings.Contains(pageRecorder.Body.String(), "\u65e0\u6743\u8bbf\u95ee") { if !strings.Contains(pageRecorder.Body.String(), "\u65e0\u6743\u8bbf\u95ee") {
t.Fatalf("non-admin plugin page was not rejected: body=%s", pageRecorder.Body.String()) t.Fatalf("non-admin plugin page was not rejected: body=%s", pageRecorder.Body.String())
} }

View File

@ -55,6 +55,13 @@ func TestDefaultMigrationsCreateCompleteSQLiteSchema(t *testing.T) {
t.Fatalf("index %s was not created", index) t.Fatalf("index %s was not created", index)
} }
} }
exists, err = columnExists(context.Background(), db, DialectSQLite, "operation_log", "actor_role")
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatal("column operation_log.actor_role was not created")
}
} }
func sqliteIndexExists(t *testing.T, db *sql.DB, index string) bool { func sqliteIndexExists(t *testing.T, db *sql.DB, index string) bool {
@ -161,6 +168,60 @@ func TestUserEmailLoginAddressMigrationRejectsUnexpectedDialect(t *testing.T) {
} }
} }
func TestOperationLogActorRoleMigrationBackfillsValidatedRoles(t *testing.T) {
db := openSQLite(t)
for _, statement := range logStatements()[DialectSQLite] {
if _, err := db.Exec(statement); err != nil {
t.Fatal(err)
}
}
rows := []struct {
id string
payload string
want any
}{
{id: "user", payload: `{"actorRole":"user"}`, want: "user"},
{id: "admin", payload: `{"actorRole":" admin "}`, want: "admin"},
{id: "super", payload: `{"actorRole":"super_admin"}`, want: "super_admin"},
{id: "missing", payload: `{}`, want: nil},
{id: "unknown", payload: `{"actorRole":"owner"}`, want: nil},
{id: "invalid", payload: `{`, want: nil},
}
for _, row := range rows {
if _, err := db.Exec(`INSERT INTO operation_log (id, created_at, kind, actor_id, action, section, result, payload) VALUES (?, 1, 'operation', '1', 'test', '', 'success', ?)`, row.id, row.payload); err != nil {
t.Fatal(err)
}
}
if err := applyOperationLogActorRoleSchema(context.Background(), db, DialectSQLite); err != nil {
t.Fatal(err)
}
if err := applyOperationLogActorRoleSchema(context.Background(), db, DialectSQLite); err != nil {
t.Fatal(err)
}
for _, row := range rows {
var got sql.NullString
if err := db.QueryRow(`SELECT actor_role FROM operation_log WHERE id = ?`, row.id).Scan(&got); err != nil {
t.Fatal(err)
}
if row.want == nil {
if got.Valid {
t.Fatalf("invalid historical role %s was backfilled as %q", row.id, got.String)
}
continue
}
if !got.Valid || got.String != row.want {
t.Fatalf("historical role %s = %q, want %q", row.id, got.String, row.want)
}
}
}
func TestOperationLogActorRoleMigrationRejectsUnexpectedDialect(t *testing.T) {
err := applyOperationLogActorRoleSchema(context.Background(), nil, Dialect("unsupported"))
if err == nil || !strings.Contains(err.Error(), "unsupported") {
t.Fatalf("unexpected dialect error = %v", err)
}
}
func TestSecurityEmailMySQLSchemaDoesNotCreateDuplicateIndex(t *testing.T) { func TestSecurityEmailMySQLSchemaDoesNotCreateDuplicateIndex(t *testing.T) {
statements := securityEmailStatements()[DialectMySQL] statements := securityEmailStatements()[DialectMySQL]
for _, statement := range statements { for _, statement := range statements {
@ -221,6 +282,7 @@ func TestDefaultMigrationChecksumsAreImmutable(t *testing.T) {
15: "59f0ff9acfed3619c17f078d21a1362ddd5f1feacb9a2d77626f69040d25de65", 15: "59f0ff9acfed3619c17f078d21a1362ddd5f1feacb9a2d77626f69040d25de65",
16: "00cc275529e522d93b41e608692fb6e2271f1aa9300ebe020fcaed4d68d0ff76", 16: "00cc275529e522d93b41e608692fb6e2271f1aa9300ebe020fcaed4d68d0ff76",
17: "1dad66acb5e7fecf6c39f4a78526b994748d90045220f446a2e80c1b5fd881e7", 17: "1dad66acb5e7fecf6c39f4a78526b994748d90045220f446a2e80c1b5fd881e7",
18: "17622df3406657f199d78a3a1aa1f3a8693baf85aa7707910cc6d6cb44c13ee4",
} }
migrations := DefaultMigrations() migrations := DefaultMigrations()
if len(migrations) != len(expected) { if len(migrations) != len(expected) {

View File

@ -123,16 +123,6 @@ func (s *SpamBlock) SettingsHtml(ctx *context.Context, url string, requestData s
return "没有访问权限" return "没有访问权限"
} }
return "没有访问权限" return "没有访问权限"
cfg, err := decodeSpamBlockConfig([]byte(requestData))
if err != nil {
return err.Error()
}
s.applyConfig(cfg)
return "保存成功"
} }
func canManageSpamBlockSettings(ctx *context.Context) bool { func canManageSpamBlockSettings(ctx *context.Context) bool {

View File

@ -179,15 +179,15 @@ func TestDecodeSpamBlockConfigUsesUnifiedValidation(t *testing.T) {
func TestSettingsHtmlAppliesValidatedConfiguration(t *testing.T) { func TestSettingsHtmlAppliesValidatedConfiguration(t *testing.T) {
s := &SpamBlock{} s := &SpamBlock{}
result := s.SettingsHtml(&context.Context{IsAdmin: true}, "SpamBlock/save", `{"url":"https://model.example.com","timeout":"3000","threshold":"25"}`) result := s.SettingsHtml(&context.Context{IsAdmin: true}, "SpamBlock/save", `{"url":"https://model.example.com","timeout":"3000","threshold":"25"}`)
if result != "保存成功" { if result != "旧版插件配置保存接口已停用,请使用新版插件配置接口" {
t.Fatalf("save result = %q", result) t.Fatalf("save result = %q", result)
} }
s.mu.RLock() s.mu.RLock()
config := s.cfg config := s.cfg
client := s.hc client := s.hc
s.mu.RUnlock() s.mu.RUnlock()
if config.ApiURL != "https://model.example.com" || config.ApiTimeout != 3000 || config.Threshold != 25 || client == nil { if config != (SpamBlockConfig{}) || client != nil {
t.Fatalf("unexpected applied configuration: %+v", config) t.Fatalf("legacy save changed configuration: %+v", config)
} }
} }
@ -225,9 +225,9 @@ func TestBuildSpamBlockModelTextHandlesNilAndLimitsSource(t *testing.T) {
func TestSettingsHtmlRejectsNonAdminSave(t *testing.T) { func TestSettingsHtmlRejectsNonAdminSave(t *testing.T) {
s := &SpamBlock{} s := &SpamBlock{}
result := s.SettingsHtml(&context.Context{}, "SpamBlock/save", `{"url":"https://model.example.com/predict"}`) result := s.SettingsHtml(&context.Context{}, "SpamBlock/index.html", "")
if result != "没有访问权限" { if !strings.Contains(result, "请联系管理员") {
t.Fatalf("non-admin save result = %q, want access denial", result) t.Fatalf("non-admin page result = %q, want access denial", result)
} }
if s.cfg.ApiURL != "" { if s.cfg.ApiURL != "" {
t.Fatal("non-admin save changed the configuration") t.Fatal("non-admin save changed the configuration")

View File

@ -5,6 +5,7 @@ import (
"git.pcux.cn/xuans/Smail/config" "git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/dto/parsemail" "git.pcux.cn/xuans/Smail/dto/parsemail"
"git.pcux.cn/xuans/Smail/services/access"
"git.pcux.cn/xuans/Smail/utils/context" "git.pcux.cn/xuans/Smail/utils/context"
) )
@ -50,17 +51,35 @@ func TestAuthorizedSubmissionEmailLimitsAdminToLocalDomains(t *testing.T) {
oldConfig := config.Instance oldConfig := config.Instance
t.Cleanup(func() { config.Instance = oldConfig }) t.Cleanup(func() { config.Instance = oldConfig })
config.Instance = &config.Config{Domains: []string{"example.com"}} config.Instance = &config.Config{Domains: []string{"example.com"}}
ctx := &context.Context{UserID: 1, UserAccount: "admin", IsAdmin: true} ctx := &context.Context{UserID: 1, UserAccount: "admin", Role: access.RoleAdmin}
local := &parsemail.Email{ local := &parsemail.Email{
From: &parsemail.User{EmailAddress: "alias@example.com"}, From: &parsemail.User{EmailAddress: "admin@example.com"},
Sender: &parsemail.User{EmailAddress: "alias@example.com"}, Sender: &parsemail.User{EmailAddress: "admin@example.com"},
} }
if !authorizedSubmissionEmail(local, "alias@example.com", ctx) { if !authorizedSubmissionEmail(local, "admin@example.com", ctx) {
t.Fatal("admin local alias was rejected") t.Fatal("admin own sender was rejected")
}
local.From.EmailAddress = "alias@example.com"
local.Sender.EmailAddress = "alias@example.com"
if authorizedSubmissionEmail(local, "alias@example.com", ctx) {
t.Fatal("admin local alias was accepted")
} }
local.From.EmailAddress = "alias@external.example" local.From.EmailAddress = "alias@external.example"
if authorizedSubmissionEmail(local, "alias@example.com", ctx) { local.Sender.EmailAddress = "alias@external.example"
if authorizedSubmissionEmail(local, "alias@external.example", ctx) {
t.Fatal("admin external sender domain was accepted") t.Fatal("admin external sender domain was accepted")
} }
superAdmin := &context.Context{UserID: 2, UserAccount: "root", Role: access.RoleSuperAdmin}
local.From.EmailAddress = "alias@example.com"
local.Sender.EmailAddress = "alias@example.com"
if !authorizedSubmissionEmail(local, "alias@example.com", superAdmin) {
t.Fatal("super admin local alias was rejected")
}
local.From.EmailAddress = "alias@external.example"
local.Sender.EmailAddress = "alias@external.example"
if authorizedSubmissionEmail(local, "alias@external.example", superAdmin) {
t.Fatal("super admin external sender domain was accepted")
}
} }

View File

@ -4,7 +4,10 @@ import "testing"
func TestLogMigrationChecksum(t *testing.T) { func TestLogMigrationChecksum(t *testing.T) {
items := DefaultMigrations() items := DefaultMigrations()
if got := items[len(items)-1].Checksum(); got != "1dad66acb5e7fecf6c39f4a78526b994748d90045220f446a2e80c1b5fd881e7" { if got := items[16].Checksum(); got != "1dad66acb5e7fecf6c39f4a78526b994748d90045220f446a2e80c1b5fd881e7" {
t.Fatalf("unexpected log migration checksum: %s", got) t.Fatalf("unexpected log migration checksum: %s", got)
} }
if got := items[17].Checksum(); got != "17622df3406657f199d78a3a1aa1f3a8693baf85aa7707910cc6d6cb44c13ee4" {
t.Fatalf("unexpected operation log role migration checksum: %s", got)
}
} }

View File

@ -9,10 +9,7 @@ import (
func TestLogScopeFiltersBeforePagination(t *testing.T) { func TestLogScopeFiltersBeforePagination(t *testing.T) {
s := testStore(t) s := testStore(t)
if _, err := s.db.Exec(`CREATE TABLE "user" (id INTEGER PRIMARY KEY, role TEXT, is_admin INTEGER)`); err != nil { if _, err := s.db.Exec(`INSERT INTO "user" (id, account, name, password, role, is_admin) VALUES (1,'root','Root','hash','super_admin',1),(2,'manager','Manager','hash','admin',1),(3,'other-manager','Other','hash','admin',1),(4,'alice','Alice','hash','user',0),(5,'legacy','Legacy','hash','legacy',0),(6,'legacy-manager','Legacy admin','hash','legacy',1),(7,'user-admin-flag','User admin flag','hash','user',1)`); err != nil {
t.Fatal(err)
}
if _, err := s.db.Exec(`INSERT INTO "user" VALUES (1,'super_admin',1),(2,'admin',1),(3,'admin',1),(4,'user',0),(5,NULL,0),(6,'legacy',1),(7,'user',1)`); err != nil {
t.Fatal(err) t.Fatal(err)
} }
ctx := context.Background() ctx := context.Background()

View File

@ -73,7 +73,7 @@ func TestLogStoreServerDatabaseIntegration(t *testing.T) {
t.Fatal("dedicated test database is unavailable") t.Fatal("dedicated test database is unavailable")
} }
migrations := migration.DefaultMigrations() migrations := migration.DefaultMigrations()
for _, statement := range migrations[len(migrations)-1].StatementsFor(test.dialect) { for _, statement := range migrations[len(migrations)-2].StatementsFor(test.dialect) {
if strings.HasPrefix(statement, "CREATE TABLE IF NOT EXISTS ") { if strings.HasPrefix(statement, "CREATE TABLE IF NOT EXISTS ") {
statement = strings.Replace(statement, "CREATE TABLE IF NOT EXISTS ", "CREATE TEMPORARY TABLE ", 1) statement = strings.Replace(statement, "CREATE TABLE IF NOT EXISTS ", "CREATE TEMPORARY TABLE ", 1)
} }
@ -81,6 +81,10 @@ func TestLogStoreServerDatabaseIntegration(t *testing.T) {
t.Fatal("temporary log schema creation failed") t.Fatal("temporary log schema creation failed")
} }
} }
actorRoleDefinition := "VARCHAR(20) NULL"
if _, err := database.ExecContext(ctx, "ALTER TABLE operation_log ADD COLUMN actor_role "+actorRoleDefinition); err != nil {
t.Fatal("temporary operation log role column creation failed")
}
store, err := New(database, test.dialect) store, err := New(database, test.dialect)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -24,11 +24,9 @@ func testStore(t *testing.T) *Store {
} }
database.SetMaxOpenConns(1) database.SetMaxOpenConns(1)
t.Cleanup(func() { _ = database.Close() }) t.Cleanup(func() { _ = database.Close() })
items := migration.DefaultMigrations() runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()}
for _, statement := range items[len(items)-1].StatementsFor(migration.DialectSQLite) { if _, err := runner.Run(context.Background()); err != nil {
if _, err := database.Exec(statement); err != nil { t.Fatal(err)
t.Fatal(err)
}
} }
store, err := New(database, migration.DialectSQLite) store, err := New(database, migration.DialectSQLite)
if err != nil { if err != nil {

View File

@ -28,11 +28,9 @@ func TestLogsAPIRejectsUnscopedAccessAndValidatesQueries(t *testing.T) {
defer database.Close() defer database.Close()
// 与正式 SQLite 连接池一致,避免测试自行引入多写连接锁冲突。 // 与正式 SQLite 连接池一致,避免测试自行引入多写连接锁冲突。
database.SetMaxOpenConns(1) database.SetMaxOpenConns(1)
items := migration.DefaultMigrations() runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()}
for _, statement := range items[len(items)-1].StatementsFor(migration.DialectSQLite) { if _, err := runner.Run(context.Background()); err != nil {
if _, err := database.Exec(statement); err != nil { t.Fatal(err)
t.Fatal(err)
}
} }
store, err := logstore.New(database, migration.DialectSQLite) store, err := logstore.New(database, migration.DialectSQLite)
if err != nil { if err != nil {

View File

@ -43,10 +43,9 @@ func TestOperationLogUsesCompletedIdentityWithoutSensitivePayload(t *testing.T)
defer database.Close() defer database.Close()
database.SetMaxOpenConns(1) database.SetMaxOpenConns(1)
migrations := migration.DefaultMigrations() migrations := migration.DefaultMigrations()
for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migrations}
if _, err := database.Exec(statement); err != nil { if _, err := runner.Run(context.Background()); err != nil {
t.Fatal(err) t.Fatal(err)
}
} }
store, err := logstore.New(database, migration.DialectSQLite) store, err := logstore.New(database, migration.DialectSQLite)
if err != nil { if err != nil {
@ -85,10 +84,9 @@ func TestOperationResponseDistinguishesPendingHTTPFailureAndUnknown(t *testing.T
// 与生产 SQLite 连接池一致,避免后台清理和写入争抢无等待的独立连接。 // 与生产 SQLite 连接池一致,避免后台清理和写入争抢无等待的独立连接。
database.SetMaxOpenConns(1) database.SetMaxOpenConns(1)
migrations := migration.DefaultMigrations() migrations := migration.DefaultMigrations()
for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migrations}
if _, err := database.Exec(statement); err != nil { if _, err := runner.Run(context.Background()); err != nil {
t.Fatal(err) t.Fatal(err)
}
} }
store, err := logstore.New(database, migration.DialectSQLite) store, err := logstore.New(database, migration.DialectSQLite)
if err != nil { if err != nil {

View File

@ -97,16 +97,23 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) {
} }
for _, ctx := range []*appcontext.Context{admin, ordinary} { for _, ctx := range []*appcontext.Context{admin, ordinary} {
ctx.IsAdmin = true ctx.IsAdmin = true
if got := call(ctx, GetPluginList, `{}`); got.ErrorNo != response.NoAccessPrivileges { got := call(ctx, GetPluginList, `{}`)
if got.ErrorNo != 0 {
t.Fatal("已登录角色无法读取可用插件列表")
}
pageRecorder := httptest.NewRecorder()
SettingsHtml(ctx, pageRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/settings/missing/index.html", nil))
if ctx == ordinary && !strings.Contains(pageRecorder.Body.String(), "无权访问") {
t.Fatal("旧管理员标志绕过插件页面授权") t.Fatal("旧管理员标志绕过插件页面授权")
} }
ctx.IsAdmin = false if ctx == admin && strings.Contains(pageRecorder.Body.String(), "无权访问") {
t.Fatal("管理员角色未获得插件页面授权")
}
} }
migrations := migration.DefaultMigrations() migrations := migration.DefaultMigrations()
for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migrations}
if _, err := engine.Exec(statement); err != nil { if _, err := runner.Run(context.Background()); err != nil {
t.Fatal(err) t.Fatal(err)
}
} }
store, err := logstore.New(engine.DB().DB, migration.DialectSQLite) store, err := logstore.New(engine.DB().DB, migration.DialectSQLite)
if err != nil { if err != nil {
@ -115,7 +122,7 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) {
runtime := logstore.Start(store) runtime := logstore.Start(store)
defer runtime.Stop() defer runtime.Stop()
for _, user := range users { for _, user := range users {
if err := store.Write(context.Background(), logstore.Entry{Kind: "operation", ActorID: itoa(user.ID), CreatedAt: time.Now(), Action: "权限验证", Result: "success", Payload: []byte(`{}`)}); err != nil { if err := store.Write(context.Background(), logstore.Entry{Kind: "operation", ActorID: itoa(user.ID), ActorRole: string(user.EffectiveRole()), CreatedAt: time.Now(), Action: "权限验证", Result: "success", Payload: []byte(`{}`)}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View File

@ -26,11 +26,9 @@ func TestInboundMailLogBelongsToEachActualRecipient(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
migrations := migration.DefaultMigrations() runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()}
for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { if _, err := runner.Run(context.Background()); err != nil {
if _, err := engine.Exec(statement); err != nil { t.Fatal(err)
t.Fatal(err)
}
} }
store, err := logstore.New(engine.DB().DB, migration.DialectSQLite) store, err := logstore.New(engine.DB().DB, migration.DialectSQLite)
if err != nil { if err != nil {
@ -67,11 +65,9 @@ func (failedMailReader) Read([]byte) (int, error) { return 0, errors.New("privat
func TestSMTPDataLogDistinguishesDiscardedMailAndReadFailure(t *testing.T) { func TestSMTPDataLogDistinguishesDiscardedMailAndReadFailure(t *testing.T) {
engine, _ := newAuditTestEngine(t) engine, _ := newAuditTestEngine(t)
config.Instance.SpamFilterLevel = 3 config.Instance.SpamFilterLevel = 3
migrations := migration.DefaultMigrations() runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()}
for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { if _, err := runner.Run(context.Background()); err != nil {
if _, err := engine.Exec(statement); err != nil { t.Fatal(err)
t.Fatal(err)
}
} }
store, err := logstore.New(engine.DB().DB, migration.DialectSQLite) store, err := logstore.New(engine.DB().DB, migration.DialectSQLite)
if err != nil { if err != nil {

View File

@ -21,11 +21,9 @@ func TestRelayMailLogRecordsResultWithoutChallengeSecrets(t *testing.T) {
} }
defer database.Close() defer database.Close()
database.SetMaxOpenConns(1) database.SetMaxOpenConns(1)
migrations := migration.DefaultMigrations() runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()}
for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { if _, err := runner.Run(context.Background()); err != nil {
if _, err := database.Exec(statement); err != nil { t.Fatal(err)
t.Fatal(err)
}
} }
store, err := logstore.New(database, migration.DialectSQLite) store, err := logstore.New(database, migration.DialectSQLite)
if err != nil { if err != nil {