Smail/plugins/pmail_telegram_push/hook/runtime_test.go

149 lines
6.0 KiB
Go

package hook
import (
"encoding/json"
"strings"
"sync"
"testing"
)
func validRuntimePayload() []byte {
payload := pluginRuntimeConfig{
PluginID: "telegram_push",
Generation: 1,
Settings: json.RawMessage(`{"timeout":30,"webDomain":"mail.example.test","httpsEnabled":true}`),
Secrets: map[string]string{"botToken": "secret-token"},
Capabilities: []string{"network"},
Network: []pluginNetworkTarget{{Scheme: "https", Host: "api.telegram.org", Port: 443}},
}
data, err := json.Marshal(payload)
if err != nil {
panic(err)
}
return data
}
func TestParseRuntimeConfigAcceptsAuthorizedPayload(t *testing.T) {
runtime, settings, token, err := parseRuntimeConfig(validRuntimePayload())
if err != nil {
t.Fatalf("parseRuntimeConfig() error = %v", err)
}
if runtime.PluginID != pluginID || runtime.Generation != 1 || settings.Timeout != defaultTimeout || token != "secret-token" {
t.Fatalf("unexpected parsed configuration: id=%q timeout=%d token=%q", runtime.PluginID, settings.Timeout, token)
}
}
func TestParseRuntimeConfigRejectsZeroGeneration(t *testing.T) {
var payload pluginRuntimeConfig
if err := json.Unmarshal(validRuntimePayload(), &payload); err != nil {
t.Fatal(err)
}
payload.Generation = 0
data, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
if _, _, _, err = parseRuntimeConfig(data); err == nil {
t.Fatal("zero plugin configuration generation was accepted")
}
}
func TestParseRuntimeConfigRejectsSecurityBoundaryViolations(t *testing.T) {
tests := []struct {
name string
mutate func(*pluginRuntimeConfig)
wantError string
}{
{name: "plugin id mismatch", mutate: func(payload *pluginRuntimeConfig) { payload.PluginID = "other" }, wantError: "\u63d2\u4ef6 ID \u4e0d\u5339\u914d"},
{name: "missing token", mutate: func(payload *pluginRuntimeConfig) { payload.Secrets = nil }, wantError: "Telegram Bot Token \u672a\u914d\u7f6e"},
{name: "unknown secret", mutate: func(payload *pluginRuntimeConfig) { payload.Secrets["other"] = "value" }, wantError: "Telegram 插件密钥字段无效"},
{name: "missing capability", mutate: func(payload *pluginRuntimeConfig) { payload.Capabilities = nil }, wantError: "Telegram 插件能力权限无效"},
{name: "extra capability", mutate: func(payload *pluginRuntimeConfig) { payload.Capabilities = []string{"network", "filesystem"} }, wantError: "Telegram 插件能力权限无效"},
{name: "missing network permission", mutate: func(payload *pluginRuntimeConfig) { payload.Network = nil }, wantError: "Telegram \u7f51\u7edc\u6743\u9650\u672a\u914d\u7f6e"},
{name: "wrong network host", mutate: func(payload *pluginRuntimeConfig) {
payload.Network = []pluginNetworkTarget{{Scheme: "https", Host: "example.test", Port: 443}}
}, wantError: "Telegram \u7f51\u7edc\u6743\u9650\u672a\u914d\u7f6e"},
{name: "wrong network port", mutate: func(payload *pluginRuntimeConfig) {
payload.Network = []pluginNetworkTarget{{Scheme: "https", Host: "api.telegram.org", Port: 8443}}
}, wantError: "Telegram \u7f51\u7edc\u6743\u9650\u672a\u914d\u7f6e"},
{name: "extra network target", mutate: func(payload *pluginRuntimeConfig) {
payload.Network = append(payload.Network, pluginNetworkTarget{Scheme: "https", Host: "example.test", Port: 443})
}, wantError: "Telegram \u7f51\u7edc\u6743\u9650\u672a\u914d\u7f6e"},
{name: "proxy bypass", mutate: func(payload *pluginRuntimeConfig) {
payload.Settings = json.RawMessage(`{"proxy":"http://127.0.0.1:8080"}`)
}, wantError: "Telegram \u63d2\u4ef6\u4e0d\u5141\u8bb8\u4f7f\u7528\u672a\u6388\u6743\u4ee3\u7406"},
{name: "timeout too large", mutate: func(payload *pluginRuntimeConfig) { payload.Settings = json.RawMessage(`{"timeout":121}`) }, wantError: "Telegram \u8bf7\u6c42\u8d85\u65f6\u65f6\u95f4\u65e0\u6548"},
{name: "unknown setting", mutate: func(payload *pluginRuntimeConfig) { payload.Settings = json.RawMessage(`{"unknown":true}`) }, wantError: "Telegram \u63d2\u4ef6\u914d\u7f6e\u65e0\u6548"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var payload pluginRuntimeConfig
if err := json.Unmarshal(validRuntimePayload(), &payload); err != nil {
t.Fatal(err)
}
test.mutate(&payload)
data, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
_, _, _, err = parseRuntimeConfig(data)
if err == nil || !strings.Contains(err.Error(), test.wantError) {
t.Fatalf("error = %v, want %q", err, test.wantError)
}
if strings.Contains(err.Error(), "secret-token") {
t.Fatalf("error exposed secret material: %v", err)
}
})
}
}
func TestParseRuntimeConfigRejectsTrailingJSON(t *testing.T) {
data := append(validRuntimePayload(), []byte(` {}`)...)
_, _, _, err := parseRuntimeConfig(data)
if err == nil || !strings.Contains(err.Error(), "\u63d2\u4ef6\u8fd0\u884c\u65f6\u914d\u7f6e\u65e0\u6548") {
t.Fatalf("error = %v, want trailing JSON rejection", err)
}
}
func TestSettingStoreKeepsSettingsScopedByUser(t *testing.T) {
store := newSettingStore()
if err := store.update(10, telegramSetting{ChatID: "-1001234567890", ShowContent: true}); err != nil {
t.Fatal(err)
}
if got := store.get(10); got.ChatID != "-1001234567890" {
t.Fatalf("user setting was not saved: %+v", got)
}
if got := store.get(11); got.ChatID != "" {
t.Fatalf("user setting crossed user boundary: %+v", got)
}
}
func TestSettingStoreRejectsInvalidChatID(t *testing.T) {
store := newSettingStore()
for _, chatID := range []string{"0", "abc", "--1", "+123"} {
if err := store.update(1, telegramSetting{ChatID: chatID}); err == nil {
t.Fatalf("chat ID %q was accepted", chatID)
}
}
}
func TestSettingStoreConcurrentUpdates(t *testing.T) {
store := newSettingStore()
var wg sync.WaitGroup
for userID := 1; userID <= 32; userID++ {
userID := userID
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 50; i++ {
if err := store.update(userID, telegramSetting{ChatID: "-1001234567890"}); err != nil {
t.Errorf("update() error = %v", err)
}
_ = store.get(userID)
}
}()
}
wg.Wait()
}