feat(owner): 扫码绑定确认时自动把主人id登记进owner.yaml白名单(热生效)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
Aska committed 2026-09-08 02:53:01 +08:00
1 parent 0aa387f83c
commit 5a7ee9d772
7 files changed
+487 -65

No files matched your search

@@ -6,6 +6,7 @@ package gateway
// unwrap the gateway uses to reach the concrete *weixin_ilink.Adapter.
import (
"context"
"encoding/json"
"io"
"log/slog"
@@ -81,9 +82,9 @@ func newWeixinGateway(t *testing.T, name string, channels ChannelConfigStore) (s
wsrv := weixinLoginServer(t)
wa, err := weixin_ilink.New(
weixin_ilink.Config{
BaseURL: wsrv.URL,
LoginInterval: time.Millisecond,
LoginTimeout: 5 * time.Second,
BaseURL: wsrv.URL,
LoginInterval: time.Millisecond,
LoginTimeout: 5 * time.Second,
},
wsrv.Client(),
slog.New(slog.NewTextHandler(io.Discard, nil)),
@@ -176,7 +177,9 @@ func TestAdminWeixinLoginStatus_Waiting(t *testing.T) {
t.Fatalf("login-status status = %d, body=%s", resp.StatusCode, body)
}
var out struct {
Data struct{ Status string `json:"status"` } `json:"data"`
Data struct {
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(body, &out); err != nil {
t.Fatalf("decode login-status: %v", err)
@@ -290,7 +293,9 @@ func TestAdminWeixinLoginStatus_AlreadyConfirmed(t *testing.T) {
t.Fatalf("login-qr status = %d, body=%s", resp.StatusCode, body)
}
var qr struct {
Data struct{ Status string `json:"status"` } `json:"data"`
Data struct {
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(body, &qr); err != nil {
t.Fatalf("decode login-qr: %v", err)
@@ -327,3 +332,66 @@ func TestAdminWeixinLogin_NonWeixinNotImplemented(t *testing.T) {
t.Fatalf("status = %d, want 501 (body=%s)", resp.StatusCode, body)
}
}
// ownerRegCall records one RegisterChannelOwner invocation.
type ownerRegCall struct {
platform, accountID string
}
// captureOwnerRegistrar records whitelist registrations for the weixin confirm path.
type captureOwnerRegistrar struct {
calls []ownerRegCall
}
func (c *captureOwnerRegistrar) RegisterChannelOwner(_ context.Context, platform, accountID string) error {
c.calls = append(c.calls, ownerRegCall{platform, accountID})
return nil
}
// TestPersistWeixinConfirmed_RegistersOwner verifies that once a scan-login
// confirmation persists the channels.json credentials, the bound owner id is
// handed to the OwnerRegistrar as (weixin-ilink, ilink_user_id), and that an empty
// owner id skips the registration entirely.
func TestPersistWeixinConfirmed_RegistersOwner(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
chs := &fakeWeixinChannels{
path: path,
data: map[string][]config.ChannelAccount{
"weixin-ilink": {{ID: "conf0", Config: map[string]any{"base_url": "https://base"}}},
},
}
reg := &captureOwnerRegistrar{}
s := &Server{
channels: chs,
ownerRegistrar: reg,
log: slog.New(slog.NewTextHandler(io.Discard, nil)),
}
if err := s.persistWeixinConfirmed(context.Background(), "weixin-ilink", "conf0", "tok-x", "bot-x", "o-1"); err != nil {
t.Fatalf("persistWeixinConfirmed: %v", err)
}
if len(reg.calls) != 1 || reg.calls[0].platform != "weixin-ilink" || reg.calls[0].accountID != "o-1" {
t.Fatalf("registrar calls = %+v, want [weixin-ilink/o-1]", reg.calls)
}
// An empty owner id (a confirmed bind that yields no bound user) must not
// register anything.
if err := s.persistWeixinConfirmed(context.Background(), "weixin-ilink", "conf0", "tok-y", "bot-y", ""); err != nil {
t.Fatalf("persistWeixinConfirmed(no owner): %v", err)
}
if len(reg.calls) != 1 {
t.Fatalf("registrar calls after empty ownerID = %+v, want still 1", reg.calls)
}
}
// TestNew_WiresOwnerRegistrar verifies the composition point: Deps.OwnerRegistrar
// is threaded onto the Server so the weixin confirm flow has a live registrar.
func TestNew_WiresOwnerRegistrar(t *testing.T) {
t.Parallel()
reg := &captureOwnerRegistrar{}
srv, _, _ := buildEnv(t, cfgDefault(), Deps{OwnerRegistrar: reg})
if srv.ownerRegistrar != reg {
t.Fatalf("ownerRegistrar = %#v, want the injected registrar", srv.ownerRegistrar)
}
}