From c6071414df325fb110c3611c13f6eafd509dedae Mon Sep 17 00:00:00 2001 From: smail Date: Thu, 10 Sep 2026 11:49:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=92=E8=89=B2=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=BF=81=E7=A7=BB=E4=B8=8E=E6=8F=92=E4=BB=B6=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/plugin.go | 4 +- backend/controllers/plugin_test.go | 19 ++++-- backend/db/migration/registry_test.go | 62 +++++++++++++++++++ backend/hooks/spam_block/spam_block.go | 10 --- backend/hooks/spam_block/spam_block_test.go | 12 ++-- .../smtp_server/submission_sender_test.go | 31 ++++++++-- test/backend/log_migration_test.go | 5 +- test/backend/log_scope_test.go | 5 +- test/backend/log_store_integration_test.go | 6 +- test/backend/log_store_test.go | 8 +-- test/backend/logs_api_test.go | 8 +-- test/backend/operation_log_test.go | 14 ++--- test/backend/role_scope_test.go | 21 ++++--- test/backend/smtp_mail_log_test.go | 16 ++--- test/backend/verification_mail_log_test.go | 8 +-- 15 files changed, 153 insertions(+), 76 deletions(-) diff --git a/backend/controllers/plugin.go b/backend/controllers/plugin.go index 0798b72..2b2e3b5 100644 --- a/backend/controllers/plugin.go +++ b/backend/controllers/plugin.go @@ -19,7 +19,7 @@ const ( ) 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) 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) { - 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) return } diff --git a/backend/controllers/plugin_test.go b/backend/controllers/plugin_test.go index 160720c..1736859 100644 --- a/backend/controllers/plugin_test.go +++ b/backend/controllers/plugin_test.go @@ -10,6 +10,7 @@ import ( "git.pcux.cn/xuans/Smail/hooks" hooksframework "git.pcux.cn/xuans/Smail/hooks/framework" "git.pcux.cn/xuans/Smail/models" + "git.pcux.cn/xuans/Smail/services/access" "git.pcux.cn/xuans/Smail/utils/context" ) @@ -43,7 +44,7 @@ func TestSettingsHtmlRejectsPathTraversal(t *testing.T) { recorder := httptest.NewRecorder() 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") { t.Fatal("path traversal reached a plugin") } @@ -56,13 +57,13 @@ func TestSettingsHtmlSeparatesStaticAndSaveRequests(t *testing.T) { hooks.HookList = map[string]hooksframework.EmailHook{"PluginTest": hook} 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") { t.Fatalf("static plugin page was not served safely: resource=%s type=%s", hook.resource, getRecorder.Header().Get("Content-Type")) } 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 { 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} 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) { t.Fatal("oversized plugin response was returned") } @@ -109,11 +110,17 @@ func TestPluginConfigurationRejectsNonAdminAccess(t *testing.T) { listRecorder := httptest.NewRecorder() GetPluginList(&context.Context{}, listRecorder, httptest.NewRequest(http.MethodGet, "/api/plugin/list", nil)) 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() - 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") { t.Fatalf("non-admin plugin page was not rejected: body=%s", pageRecorder.Body.String()) } diff --git a/backend/db/migration/registry_test.go b/backend/db/migration/registry_test.go index 9285b00..f3906e6 100644 --- a/backend/db/migration/registry_test.go +++ b/backend/db/migration/registry_test.go @@ -55,6 +55,13 @@ func TestDefaultMigrationsCreateCompleteSQLiteSchema(t *testing.T) { 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 { @@ -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) { statements := securityEmailStatements()[DialectMySQL] for _, statement := range statements { @@ -221,6 +282,7 @@ func TestDefaultMigrationChecksumsAreImmutable(t *testing.T) { 15: "59f0ff9acfed3619c17f078d21a1362ddd5f1feacb9a2d77626f69040d25de65", 16: "00cc275529e522d93b41e608692fb6e2271f1aa9300ebe020fcaed4d68d0ff76", 17: "1dad66acb5e7fecf6c39f4a78526b994748d90045220f446a2e80c1b5fd881e7", + 18: "17622df3406657f199d78a3a1aa1f3a8693baf85aa7707910cc6d6cb44c13ee4", } migrations := DefaultMigrations() if len(migrations) != len(expected) { diff --git a/backend/hooks/spam_block/spam_block.go b/backend/hooks/spam_block/spam_block.go index ede2cca..5c035ce 100644 --- a/backend/hooks/spam_block/spam_block.go +++ b/backend/hooks/spam_block/spam_block.go @@ -123,16 +123,6 @@ func (s *SpamBlock) SettingsHtml(ctx *context.Context, url string, requestData s return "没有访问权限" } return "没有访问权限" - - cfg, err := decodeSpamBlockConfig([]byte(requestData)) - if err != nil { - return err.Error() - } - - s.applyConfig(cfg) - - return "保存成功" - } func canManageSpamBlockSettings(ctx *context.Context) bool { diff --git a/backend/hooks/spam_block/spam_block_test.go b/backend/hooks/spam_block/spam_block_test.go index 34c95be..cc5f15f 100644 --- a/backend/hooks/spam_block/spam_block_test.go +++ b/backend/hooks/spam_block/spam_block_test.go @@ -179,15 +179,15 @@ func TestDecodeSpamBlockConfigUsesUnifiedValidation(t *testing.T) { func TestSettingsHtmlAppliesValidatedConfiguration(t *testing.T) { s := &SpamBlock{} 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) } s.mu.RLock() config := s.cfg client := s.hc s.mu.RUnlock() - if config.ApiURL != "https://model.example.com" || config.ApiTimeout != 3000 || config.Threshold != 25 || client == nil { - t.Fatalf("unexpected applied configuration: %+v", config) + if config != (SpamBlockConfig{}) || client != nil { + t.Fatalf("legacy save changed configuration: %+v", config) } } @@ -225,9 +225,9 @@ func TestBuildSpamBlockModelTextHandlesNilAndLimitsSource(t *testing.T) { func TestSettingsHtmlRejectsNonAdminSave(t *testing.T) { s := &SpamBlock{} - result := s.SettingsHtml(&context.Context{}, "SpamBlock/save", `{"url":"https://model.example.com/predict"}`) - if result != "没有访问权限" { - t.Fatalf("non-admin save result = %q, want access denial", result) + result := s.SettingsHtml(&context.Context{}, "SpamBlock/index.html", "") + if !strings.Contains(result, "请联系管理员") { + t.Fatalf("non-admin page result = %q, want access denial", result) } if s.cfg.ApiURL != "" { t.Fatal("non-admin save changed the configuration") diff --git a/backend/listen/smtp_server/submission_sender_test.go b/backend/listen/smtp_server/submission_sender_test.go index 0d8d3a2..fda6703 100644 --- a/backend/listen/smtp_server/submission_sender_test.go +++ b/backend/listen/smtp_server/submission_sender_test.go @@ -5,6 +5,7 @@ import ( "git.pcux.cn/xuans/Smail/config" "git.pcux.cn/xuans/Smail/dto/parsemail" + "git.pcux.cn/xuans/Smail/services/access" "git.pcux.cn/xuans/Smail/utils/context" ) @@ -50,17 +51,35 @@ func TestAuthorizedSubmissionEmailLimitsAdminToLocalDomains(t *testing.T) { oldConfig := config.Instance t.Cleanup(func() { config.Instance = oldConfig }) 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{ - From: &parsemail.User{EmailAddress: "alias@example.com"}, - Sender: &parsemail.User{EmailAddress: "alias@example.com"}, + From: &parsemail.User{EmailAddress: "admin@example.com"}, + Sender: &parsemail.User{EmailAddress: "admin@example.com"}, } - if !authorizedSubmissionEmail(local, "alias@example.com", ctx) { - t.Fatal("admin local alias was rejected") + if !authorizedSubmissionEmail(local, "admin@example.com", ctx) { + 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" - 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") } + + 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") + } } diff --git a/test/backend/log_migration_test.go b/test/backend/log_migration_test.go index e9656d2..078a102 100644 --- a/test/backend/log_migration_test.go +++ b/test/backend/log_migration_test.go @@ -4,7 +4,10 @@ import "testing" func TestLogMigrationChecksum(t *testing.T) { 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) } + if got := items[17].Checksum(); got != "17622df3406657f199d78a3a1aa1f3a8693baf85aa7707910cc6d6cb44c13ee4" { + t.Fatalf("unexpected operation log role migration checksum: %s", got) + } } diff --git a/test/backend/log_scope_test.go b/test/backend/log_scope_test.go index 7b23c75..dd8c10d 100644 --- a/test/backend/log_scope_test.go +++ b/test/backend/log_scope_test.go @@ -9,10 +9,7 @@ import ( func TestLogScopeFiltersBeforePagination(t *testing.T) { s := testStore(t) - if _, err := s.db.Exec(`CREATE TABLE "user" (id INTEGER PRIMARY KEY, role TEXT, is_admin INTEGER)`); 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 { + 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) } ctx := context.Background() diff --git a/test/backend/log_store_integration_test.go b/test/backend/log_store_integration_test.go index 6db1c1e..0e3b508 100644 --- a/test/backend/log_store_integration_test.go +++ b/test/backend/log_store_integration_test.go @@ -73,7 +73,7 @@ func TestLogStoreServerDatabaseIntegration(t *testing.T) { t.Fatal("dedicated test database is unavailable") } 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 ") { 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") } } + 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) if err != nil { t.Fatal(err) diff --git a/test/backend/log_store_test.go b/test/backend/log_store_test.go index 240df30..78fbaee 100644 --- a/test/backend/log_store_test.go +++ b/test/backend/log_store_test.go @@ -24,11 +24,9 @@ func testStore(t *testing.T) *Store { } database.SetMaxOpenConns(1) t.Cleanup(func() { _ = database.Close() }) - items := migration.DefaultMigrations() - for _, statement := range items[len(items)-1].StatementsFor(migration.DialectSQLite) { - if _, err := database.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := New(database, migration.DialectSQLite) if err != nil { diff --git a/test/backend/logs_api_test.go b/test/backend/logs_api_test.go index 9a7e1b7..aaca8df 100644 --- a/test/backend/logs_api_test.go +++ b/test/backend/logs_api_test.go @@ -28,11 +28,9 @@ func TestLogsAPIRejectsUnscopedAccessAndValidatesQueries(t *testing.T) { defer database.Close() // 与正式 SQLite 连接池一致,避免测试自行引入多写连接锁冲突。 database.SetMaxOpenConns(1) - items := migration.DefaultMigrations() - for _, statement := range items[len(items)-1].StatementsFor(migration.DialectSQLite) { - if _, err := database.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(database, migration.DialectSQLite) if err != nil { diff --git a/test/backend/operation_log_test.go b/test/backend/operation_log_test.go index d9ad94d..41985fe 100644 --- a/test/backend/operation_log_test.go +++ b/test/backend/operation_log_test.go @@ -43,10 +43,9 @@ func TestOperationLogUsesCompletedIdentityWithoutSensitivePayload(t *testing.T) defer database.Close() database.SetMaxOpenConns(1) migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { - if _, err := database.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migrations} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(database, migration.DialectSQLite) if err != nil { @@ -85,10 +84,9 @@ func TestOperationResponseDistinguishesPendingHTTPFailureAndUnknown(t *testing.T // 与生产 SQLite 连接池一致,避免后台清理和写入争抢无等待的独立连接。 database.SetMaxOpenConns(1) migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { - if _, err := database.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migrations} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(database, migration.DialectSQLite) if err != nil { diff --git a/test/backend/role_scope_test.go b/test/backend/role_scope_test.go index a1a3666..77604bb 100644 --- a/test/backend/role_scope_test.go +++ b/test/backend/role_scope_test.go @@ -97,16 +97,23 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) { } for _, ctx := range []*appcontext.Context{admin, ordinary} { 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("旧管理员标志绕过插件页面授权") } - ctx.IsAdmin = false + if ctx == admin && strings.Contains(pageRecorder.Body.String(), "无权访问") { + t.Fatal("管理员角色未获得插件页面授权") + } } migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { - if _, err := engine.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migrations} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(engine.DB().DB, migration.DialectSQLite) if err != nil { @@ -115,7 +122,7 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) { runtime := logstore.Start(store) defer runtime.Stop() 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) } } diff --git a/test/backend/smtp_mail_log_test.go b/test/backend/smtp_mail_log_test.go index 1fb37fb..e41820c 100644 --- a/test/backend/smtp_mail_log_test.go +++ b/test/backend/smtp_mail_log_test.go @@ -26,11 +26,9 @@ func TestInboundMailLogBelongsToEachActualRecipient(t *testing.T) { t.Fatal(err) } } - migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { - if _, err := engine.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(engine.DB().DB, migration.DialectSQLite) if err != nil { @@ -67,11 +65,9 @@ func (failedMailReader) Read([]byte) (int, error) { return 0, errors.New("privat func TestSMTPDataLogDistinguishesDiscardedMailAndReadFailure(t *testing.T) { engine, _ := newAuditTestEngine(t) config.Instance.SpamFilterLevel = 3 - migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { - if _, err := engine.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(engine.DB().DB, migration.DialectSQLite) if err != nil { diff --git a/test/backend/verification_mail_log_test.go b/test/backend/verification_mail_log_test.go index f573074..e3efbc1 100644 --- a/test/backend/verification_mail_log_test.go +++ b/test/backend/verification_mail_log_test.go @@ -21,11 +21,9 @@ func TestRelayMailLogRecordsResultWithoutChallengeSecrets(t *testing.T) { } defer database.Close() database.SetMaxOpenConns(1) - migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-1].StatementsFor(migration.DialectSQLite) { - if _, err := database.Exec(statement); err != nil { - t.Fatal(err) - } + runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()} + if _, err := runner.Run(context.Background()); err != nil { + t.Fatal(err) } store, err := logstore.New(database, migration.DialectSQLite) if err != nil {