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, / {