Files
Cyrene_v2/internal/config/runtimeconfig/channels_test.go
T
AskaEth 29aa60d0eb test(platform): 修 Windows 上平台脆弱的断言(不削弱测试意图)
- trace/task: 亚 tick 耗时合法读到 0(本机时钟步长约 0.44ms),改为断言非负且 Duration == EndedAt-StartedAt
- user/runtimeconfig: Windows 无法表示普通文件的 POSIX 权限位(Stat 恒 0666),0600 断言仅在非 Windows 生效,注释写明实现确实请求 0600
- exec: MSYS pwd 把同一目录渲染成 /tmp/...;改用「标记文件相对名解析」+「与显式 root 的 pwd 输出逐字相等」两条跨平台断言 + 变异验证
- 只读排查全仓同类脆弱断言,结果记入 DevDocs/工作小结
2026-10-03 16:08:52 +08:00

266 lines
9.0 KiB
Go

package runtimeconfig
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
)
func TestLoadChannelsFileWins(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
if err := os.WriteFile(path, []byte(`{"kook":[{"id":"a","config":{"bot_token":"tk"}}]}`), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
accounts, warn := LoadChannels(path, map[string][]config.ChannelAccount{
"telegram": {{ID: "env", Config: map[string]any{"bot_token": "env"}}},
})
if warn != nil {
t.Fatalf("warn = %v, want nil", warn)
}
if len(accounts["kook"]) != 1 || accounts["kook"][0].ID != "a" || accounts["kook"][0].Config["bot_token"] != "tk" {
t.Fatalf("file accounts not used: %+v", accounts)
}
if _, ok := accounts["telegram"]; ok {
t.Fatalf("file should win over env fallback, telegram leaked in: %+v", accounts)
}
}
func TestLoadChannelsMissingFallsBackToEnv(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "nope.json")
fallback := map[string][]config.ChannelAccount{"telegram": {{ID: "env", Config: map[string]any{"bot_token": "tk"}}}}
accounts, warn := LoadChannels(path, fallback)
if warn != nil {
t.Fatalf("warn = %v, want nil for missing file", warn)
}
if len(accounts["telegram"]) != 1 || accounts["telegram"][0].ID != "env" {
t.Fatalf("fallback not used: %+v", accounts)
}
}
func TestLoadChannelsInvalidFallsBackWithWarn(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
if err := os.WriteFile(path, []byte(`{not-json`), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
fallback := map[string][]config.ChannelAccount{"kook": {{Config: map[string]any{}}}}
accounts, warn := LoadChannels(path, fallback)
if warn == nil {
t.Fatal("warn = nil, want a warning for an invalid file")
}
if len(accounts["kook"]) != 1 {
t.Fatalf("invalid file did not fall back to env: %+v", accounts)
}
}
func TestLoadChannelsEmptyFileFallsBackWithWarn(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
if err := os.WriteFile(path, []byte(" \n"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
fallback := map[string][]config.ChannelAccount{"kook": {{Config: map[string]any{}}}}
accounts, warn := LoadChannels(path, fallback)
if warn == nil {
t.Fatal("warn = nil, want a warning for an empty file")
}
if len(accounts["kook"]) != 1 {
t.Fatalf("empty file did not fall back to env: %+v", accounts)
}
}
func TestLoadChannelsEmptyMapFallsBackWithWarn(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
if err := os.WriteFile(path, []byte(`{}`), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
fallback := map[string][]config.ChannelAccount{"kook": {{Config: map[string]any{}}}}
accounts, warn := LoadChannels(path, fallback)
if warn == nil {
t.Fatal("warn = nil, want a warning for an empty map")
}
if len(accounts["kook"]) != 1 {
t.Fatalf("empty map did not fall back to env: %+v", accounts)
}
}
func TestLoadChannelsCleansEmptyTokenKook(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
// A stale channels.json holding a bare empty-token kook plus a real account.
raw := `{"kook":[
{"id":"","config":{"base_url":"","bot_token":""}},
{"id":"3fc8ef9434bddfbbe4f4daa8da21c093","config":{"bot_token":"real-token"}}
]}`
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
accounts, warn := LoadChannels(path, nil)
if warn != nil {
t.Fatalf("warn = %v, want nil", warn)
}
kook := accounts["kook"]
if len(kook) != 1 {
t.Fatalf("kook = %+v, want only the token-bearing account", kook)
}
if kook[0].ID != "3fc8ef9434bddfbbe4f4daa8da21c093" || kook[0].Config["bot_token"] != "real-token" {
t.Fatalf("kook = %+v, want the token-bearing account", kook[0])
}
}
func TestLoadChannelsKeepsNonTokenPlatforms(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
// qq-obv11/webhook carry no bot token and must be preserved.
raw := `{"qq-obv11":[{"id":"","config":{"mode":"server"}}],"webhook":[{"id":"","config":{"endpointPath":""}}]}`
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
accounts, warn := LoadChannels(path, nil)
if warn != nil {
t.Fatalf("warn = %v", warn)
}
if len(accounts["qq-obv11"]) != 1 || len(accounts["webhook"]) != 1 {
t.Fatalf("non-token platforms should be preserved: %+v", accounts)
}
}
func TestSaveChannelsDropsEmptyTokenBareAccount(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
accts := map[string][]config.ChannelAccount{
"kook": {
{ID: "", Config: map[string]any{"bot_token": ""}},
{ID: "a", Config: map[string]any{"bot_token": "tk"}},
},
"webhook": {{ID: "", Config: map[string]any{"endpointPath": ""}}},
}
if err := SaveChannels(path, accts); err != nil {
t.Fatalf("save: %v", err)
}
got, warn := LoadChannels(path, nil)
if warn != nil {
t.Fatalf("load warn = %v", warn)
}
if len(got["kook"]) != 1 || got["kook"][0].ID != "a" {
t.Fatalf("kook = %+v, want only the token-bearing account", got["kook"])
}
if _, ok := got["webhook"]; !ok || len(got["webhook"]) != 1 {
t.Fatalf("webhook should be preserved untouched: %+v", got["webhook"])
}
}
func TestSaveChannelsRoundTrip(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "channels.json")
accts := map[string][]config.ChannelAccount{
"kook": {{ID: "a", Config: map[string]any{"bot_token": "tk"}}},
}
if err := SaveChannels(path, accts); err != nil {
t.Fatalf("save: %v", err)
}
got, warn := LoadChannels(path, nil)
if warn != nil {
t.Fatalf("load warn = %v", warn)
}
if len(got["kook"]) != 1 || got["kook"][0].Config["bot_token"] != "tk" {
t.Fatalf("round trip mismatch: %+v", got)
}
info, err := os.Stat(path)
if err != nil {
t.Fatalf("stat: %v", err)
}
if runtime.GOOS != "windows" {
// Windows has no POSIX permission bits for regular files: Chmod only
// toggles the read-only flag and Stat reports 0666 regardless of the
// requested 0600, so this assertion is only meaningful on Unix-like
// platforms. SaveChannels still requests 0600 (see channels.go).
if info.Mode().Perm()&0o077 != 0 {
t.Fatalf("channels.json mode = %v, want owner-only (0600)", info.Mode().Perm())
}
}
}
func TestUpsertChannelAssignsIDAndReplaces(t *testing.T) {
t.Parallel()
accounts := map[string][]config.ChannelAccount{
"kook": {{ID: "a", Config: map[string]any{"bot_token": "old"}}},
}
// Blank ID → generated.
added := UpsertChannel(accounts, "kook", config.ChannelAccount{Config: map[string]any{"bot_token": "new"}})
if len(added["kook"]) != 2 {
t.Fatalf("want 2 accounts after add, got %d", len(added["kook"]))
}
if added["kook"][1].ID == "" {
t.Fatal("new account should get a generated id")
}
// Same ID → replaced in place.
replaced := UpsertChannel(accounts, "kook", config.ChannelAccount{ID: "a", Config: map[string]any{"bot_token": "updated"}})
if len(replaced["kook"]) != 2 {
t.Fatalf("want 2 accounts after replace, got %d", len(replaced["kook"]))
}
if replaced["kook"][0].Config["bot_token"] != "updated" {
t.Fatalf("replace did not update in place: %+v", replaced["kook"][0])
}
}
func TestDeleteChannelRemovesAccountAndDropsPlatform(t *testing.T) {
t.Parallel()
accounts := map[string][]config.ChannelAccount{
"kook": {{ID: "a", Config: map[string]any{}}, {ID: "b", Config: map[string]any{}}},
"telegram": {{ID: "t", Config: map[string]any{}}},
}
out := DeleteChannel(accounts, "kook", "a")
if len(out["kook"]) != 1 || out["kook"][0].ID != "b" {
t.Fatalf("kook after delete: %+v", out["kook"])
}
// Deleting the last account drops the platform key entirely.
out2 := DeleteChannel(out, "kook", "b")
if _, ok := out2["kook"]; ok {
t.Fatalf("kook should be dropped when empty: %+v", out2)
}
// Other platforms untouched.
if len(out2["telegram"]) != 1 {
t.Fatalf("telegram affected: %+v", out2)
}
}
func TestMaskedRedactsTokensKeepsOriginal(t *testing.T) {
t.Parallel()
acct := config.ChannelAccount{
ID: "a",
Config: map[string]any{"bot_token": "super-secret-token", "base_url": "https://x", "access_token": "abc", "secret": "s"},
DenyUsers: []string{"u1"},
}
masked := Masked(acct)
if got := masked.Config["bot_token"]; got != "****oken" {
t.Fatalf("bot_token = %v, want masked", got)
}
if got := masked.Config["access_token"]; got != "****" {
t.Fatalf("access_token = %v, want fully masked", got)
}
if got := masked.Config["secret"]; got != "****" {
t.Fatalf("secret = %v, want fully masked", got)
}
if got := masked.Config["base_url"]; got != "https://x" {
t.Fatalf("base_url = %v, want unchanged", got)
}
if masked.DenyUsers[0] != "u1" {
t.Fatalf("deny user list lost: %+v", masked)
}
// The original account still carries the raw token.
if acct.Config["bot_token"] != "super-secret-token" {
t.Fatalf("Masked mutated the original account")
}
if strings.Contains(masked.Config["bot_token"].(string), "super-secret") {
t.Fatalf("masked leaked the full token: %v", masked.Config["bot_token"])
}
}