From fa53f7c641ea3dbf99078d0b080962aa3a7e735b Mon Sep 17 00:00:00 2001 From: smail Date: Mon, 28 Sep 2026 21:53:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BE=AE=E4=BF=A1=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E8=BF=87=E6=9C=9F=E5=8F=96=E6=B6=88=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E4=BC=9A=E8=AF=9D=E9=9A=94=E7=A6=BB=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/plugin/config_test.go | 35 +++++++++++++++++++++++ backend/services/plugin/wechat_binding.go | 8 ++++-- test/frontend/plugin-navigation.test.mjs | 2 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/backend/services/plugin/config_test.go b/backend/services/plugin/config_test.go index 79d45e3..377f355 100644 --- a/backend/services/plugin/config_test.go +++ b/backend/services/plugin/config_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "path/filepath" + "strings" "testing" "time" @@ -202,6 +203,40 @@ func TestWeChatTestMessagesAreRateLimitedPerAccount(t *testing.T) { } } +func TestWeChatBindingCancellationAllowsExpiredAndMissingTickets(t *testing.T) { + service, engine := newPluginConfigService(t) + user := insertPluginTestUser(t, engine, "wechat-cancel", access.RoleSuperAdmin) + actor := NotificationBindingActor{UserID: user.ID, SessionHash: strings.Repeat("a", 64), + SessionVersion: user.SessionVersion, SecurityVersion: user.SecurityVersion} + service.wechatBindings = map[string]weChatBinding{ + "expired": {Actor: actor, Status: WeChatBindingStatus{ExpiresAt: time.Now().Add(-time.Minute)}}, + } + otherActor := actor + otherActor.SessionHash = strings.Repeat("b", 64) + if err := service.CancelWeChatBinding(otherActor, "expired"); !errors.Is(err, ErrWeChatBinding) { + t.Fatalf("不得撤销其他会话的绑定:%v", err) + } + if len(service.wechatBindings) != 1 { + t.Fatal("其他会话的取消请求不应删除原绑定") + } + if err := service.CancelWeChatBinding(actor, "expired"); err != nil { + t.Fatalf("过期请求应允许取消:%v", err) + } + if len(service.wechatBindings) != 0 { + t.Fatal("已取消的绑定不应残留") + } + if err := service.CancelWeChatBinding(actor, "expired"); err != nil { + t.Fatalf("重复取消应保持幂等:%v", err) + } + if err := service.CancelWeChatBinding(actor, "missing"); err != nil { + t.Fatalf("不存在的请求应允许关闭:%v", err) + } + actor.SecurityVersion++ + if err := service.CancelWeChatBinding(actor, "missing"); !errors.Is(err, ErrWeChatBinding) { + t.Fatalf("幂等取消仍必须验证当前账户和会话:%v", err) + } +} + func TestEveryRoleKeepsIndependentOwnTelegramSettings(t *testing.T) { service, engine := newPluginConfigService(t) var users []models.User diff --git a/backend/services/plugin/wechat_binding.go b/backend/services/plugin/wechat_binding.go index d605210..b6496e2 100644 --- a/backend/services/plugin/wechat_binding.go +++ b/backend/services/plugin/wechat_binding.go @@ -152,12 +152,16 @@ func (s *ConfigService) GetWeChatBinding(actor NotificationBindingActor, ticket } func (s *ConfigService) CancelWeChatBinding(actor NotificationBindingActor, ticket string) error { - if _, err := s.GetWeChatBinding(actor, ticket); err != nil { + if err := s.validateBindingActor(actor); err != nil { return err } s.bindingMu.Lock() + defer s.bindingMu.Unlock() + // 过期、回调失败或重复取消都允许关闭页面,但不得撤销其他会话的请求。 + if binding, found := s.wechatBindings[ticket]; found && binding.Actor != actor { + return ErrWeChatBinding + } delete(s.wechatBindings, ticket) - s.bindingMu.Unlock() return nil } diff --git a/test/frontend/plugin-navigation.test.mjs b/test/frontend/plugin-navigation.test.mjs index 5b8e536..d6844c1 100644 --- a/test/frontend/plugin-navigation.test.mjs +++ b/test/frontend/plugin-navigation.test.mjs @@ -121,6 +121,8 @@ test('未知插件只使用安全名称生成隔离页面,内置插件别名 assert.doesNotMatch(template, /allow-same-origin|v-for=".*pluginEntries/) assert.match(template, /:key="selectedSection"[\s\S]*:page-key="selectedSection"/) assert.match(template, /defineExpose\(\{ beforeLeave \}\)/) + assert.match(template, / {