mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 20:12:39 +08:00
纯宿主侧改动,向后兼容。 - 契约(pkg/pluginapi/v1/plugin.proto,已重新生成):ListAccounts 增补 repeated AccountInfo accounts(保留 account_ids 向后兼容),新增 metadata_json 只读快照字段。HostServiceAPIVersion 1→2(InitHostServices 运行时协商,加法式, 旧调用方忽略新字段即可)。 - 权限边界:可见范围由清单能力经 pluginCapabilityAccountScopeGrants 推导, ListAccounts 与 ResolveOutboundIdentity 均以该范围为权威边界。 - metadata_json 采用 denylist:剥离 Credentials 与含反向引用的关联对象 Groups/AccountGroups(避免 encoding/json 循环引用),其余字段自动透传, 数据模型新增非机密字段无需改宿主。 测试:范围/元数据/无凭据泄漏用例、denylist 反射护栏 + 循环引用安全断言、纵深过滤用例; build/vet/gofmt、-tags unit 与 -race 全绿。
131 lines
4.8 KiB
Go
131 lines
4.8 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
pluginv1 "github.com/Wei-Shaw/sub2api/pkg/pluginapi/v1"
|
||
hcplugin "github.com/hashicorp/go-plugin"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// brokerProbePlugin 是仅用于测试的插件传输实现:它实现 HostBrokerReceiver 以拿到
|
||
// broker,并在 InitHostServices 中用宿主下发的 broker id 拨号回宿主的 HostService,
|
||
// 走一次真实 gRPC 的 KVSet/KVGet 往返。
|
||
type brokerProbePlugin struct {
|
||
pluginv1.UnimplementedTransportPluginServer
|
||
broker *hcplugin.GRPCBroker
|
||
|
||
mu sync.Mutex
|
||
ready bool
|
||
getFound bool
|
||
getValue []byte
|
||
failure string
|
||
}
|
||
|
||
func (p *brokerProbePlugin) SetHostBroker(broker *hcplugin.GRPCBroker) { p.broker = broker }
|
||
|
||
func (p *brokerProbePlugin) InitHostServices(ctx context.Context, req *pluginv1.InitHostServicesRequest) (*pluginv1.InitHostServicesResponse, error) {
|
||
if p.broker == nil {
|
||
return &pluginv1.InitHostServicesResponse{Ready: false, Message: "broker 未注入"}, nil
|
||
}
|
||
conn, err := p.broker.Dial(req.HostServiceId)
|
||
if err != nil {
|
||
p.record("dial: " + err.Error())
|
||
return &pluginv1.InitHostServicesResponse{Ready: false, Message: err.Error()}, nil
|
||
}
|
||
defer func() { _ = conn.Close() }()
|
||
client := pluginv1.NewHostServiceClient(conn)
|
||
if _, err := client.KVSet(ctx, &pluginv1.KVSetRequest{Namespace: "state", Key: "probe", Value: []byte("hello"), TtlSeconds: 60}); err != nil {
|
||
p.record("set: " + err.Error())
|
||
return &pluginv1.InitHostServicesResponse{Ready: false, Message: err.Error()}, nil
|
||
}
|
||
got, err := client.KVGet(ctx, &pluginv1.KVGetRequest{Namespace: "state", Key: "probe"})
|
||
if err != nil {
|
||
p.record("get: " + err.Error())
|
||
return &pluginv1.InitHostServicesResponse{Ready: false, Message: err.Error()}, nil
|
||
}
|
||
p.mu.Lock()
|
||
p.ready = true
|
||
p.getFound = got.Found
|
||
p.getValue = got.Value
|
||
p.mu.Unlock()
|
||
return &pluginv1.InitHostServicesResponse{Ready: true}, nil
|
||
}
|
||
|
||
func (p *brokerProbePlugin) record(failure string) {
|
||
p.mu.Lock()
|
||
p.failure = failure
|
||
p.mu.Unlock()
|
||
}
|
||
|
||
// noHostServicesPlugin 模拟老插件:不实现 InitHostServices(返回 Unimplemented),
|
||
// 也不接收 broker。
|
||
type noHostServicesPlugin struct {
|
||
pluginv1.UnimplementedTransportPluginServer
|
||
}
|
||
|
||
func dispenseTransportClient(t *testing.T, impl pluginv1.TransportPluginServer) *pluginv1.TransportClient {
|
||
t.Helper()
|
||
client, _ := hcplugin.TestPluginGRPCConn(t, false, map[string]hcplugin.Plugin{
|
||
pluginv1.TransportPluginName: &pluginv1.GRPCPlugin{Impl: impl},
|
||
})
|
||
t.Cleanup(func() { _ = client.Close() })
|
||
dispensed, err := client.Dispense(pluginv1.TransportPluginName)
|
||
require.NoError(t, err)
|
||
tc, ok := dispensed.(*pluginv1.TransportClient)
|
||
require.True(t, ok)
|
||
require.NotNil(t, tc.Broker)
|
||
return tc
|
||
}
|
||
|
||
// 端到端验证 broker 反向通道:宿主服务 AcceptAndServe + 插件 Dial 回来,经真实 gRPC
|
||
// 完成 KV 往返,且写入落到宿主注入的 pluginKey 命名空间下。
|
||
func TestOfferPluginHostServices_BrokerRoundtrip(t *testing.T) {
|
||
probe := &brokerProbePlugin{}
|
||
tc := dispenseTransportClient(t, probe)
|
||
|
||
store := newFakePluginKVStore()
|
||
hostServer := newPluginHostServiceServer("test.plugin", store, nil, PluginAccountScope{})
|
||
offerPluginHostServices(context.Background(), &PluginInstallation{PluginKey: "test.plugin"}, tc.TransportPluginClient, tc.Broker, hostServer, 5*time.Second)
|
||
|
||
probe.mu.Lock()
|
||
defer probe.mu.Unlock()
|
||
require.Empty(t, probe.failure)
|
||
require.True(t, probe.ready)
|
||
assert.True(t, probe.getFound)
|
||
assert.Equal(t, []byte("hello"), probe.getValue)
|
||
|
||
stored, found, err := store.Get(context.Background(), "test.plugin", "state", "probe")
|
||
require.NoError(t, err)
|
||
require.True(t, found)
|
||
assert.Equal(t, []byte("hello"), stored)
|
||
}
|
||
|
||
// 老插件不实现 InitHostServices 时,offerPluginHostServices 必须优雅降级、不 panic、
|
||
// 不写入任何状态。
|
||
func TestOfferPluginHostServices_UnimplementedIsGraceful(t *testing.T) {
|
||
tc := dispenseTransportClient(t, &noHostServicesPlugin{})
|
||
|
||
store := newFakePluginKVStore()
|
||
hostServer := newPluginHostServiceServer("test.plugin", store, nil, PluginAccountScope{})
|
||
require.NotPanics(t, func() {
|
||
offerPluginHostServices(context.Background(), &PluginInstallation{PluginKey: "test.plugin"}, tc.TransportPluginClient, tc.Broker, hostServer, 5*time.Second)
|
||
})
|
||
|
||
_, found, err := store.Get(context.Background(), "test.plugin", "state", "probe")
|
||
require.NoError(t, err)
|
||
assert.False(t, found)
|
||
}
|
||
|
||
// hostServices 为 nil(未配置键值存储)时不得触发任何 broker 交互。
|
||
func TestOfferPluginHostServices_NilHostServicesNoop(t *testing.T) {
|
||
tc := dispenseTransportClient(t, &noHostServicesPlugin{})
|
||
require.NotPanics(t, func() {
|
||
offerPluginHostServices(context.Background(), &PluginInstallation{PluginKey: "test.plugin"}, tc.TransportPluginClient, tc.Broker, nil, 5*time.Second)
|
||
})
|
||
}
|