CI / verify (push) Canceled after 0s
- 配置版本:fsutil 版本化备份(内容相同自动去重),主 nginx.conf 与 VPS vhost 的版本列表/逐行差异/一键回滚,回滚先校验语法与 nginx -t 再写回 - agent 0.12.2:写 vhost 前留版本;nginx -t 校验失败改为恢复上一个可用版本(此前直接删文件),新增域名 vhost 版本端点,双架构二进制已重编 - 通知:补企业微信 / 钉钉 / 飞书机器人,切换预设时服务地址跟随该预设默认值 - 阈值告警:新增 internal/monitor 巡检(CPU/内存/磁盘持续超阈值、隧道连续 N 次未注册),默认关闭,带静默期与恢复通知 - API Token:api_tokens 表 + Authorization Bearer 认证,只读/读写两种范围;令牌管理与改密/退出仅限登录会话
201 lines
5.3 KiB
Go
201 lines
5.3 KiB
Go
package fsutil
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWriteFileAtomicReplacesContent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "frps.toml")
|
|
if err := os.WriteFile(path, []byte("old"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteFileAtomic(path, []byte("new"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != "new" {
|
|
t.Fatalf("内容未替换:%q", data)
|
|
}
|
|
}
|
|
|
|
// TestWriteFileAtomicLeavesNoTempFiles:临时文件必须被 rename 或清理,不能在配置目录里留垃圾。
|
|
func TestWriteFileAtomicLeavesNoTempFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "nginx.conf")
|
|
if err := WriteFileAtomic(path, []byte("worker_processes 1;"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, entry := range entries {
|
|
if strings.Contains(entry.Name(), ".tmp-") {
|
|
t.Fatalf("残留临时文件:%s", entry.Name())
|
|
}
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("目录里应只剩目标文件,实际 %d 个", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestBackupSkipsMissingFile(t *testing.T) {
|
|
if err := Backup(filepath.Join(t.TempDir(), "nope.conf")); err != nil {
|
|
t.Fatalf("文件不存在时应视为无需备份,实际 %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBackupCopiesContent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "frpc.toml")
|
|
if err := os.WriteFile(path, []byte("token = \"x\""), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Backup(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(path + ".bak")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != "token = \"x\"" {
|
|
t.Fatalf("备份内容不符:%q", data)
|
|
}
|
|
}
|
|
|
|
func TestBackupVersionedStoresContent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "nginx.conf")
|
|
if err := os.WriteFile(path, []byte("worker_processes 1;"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
version, err := BackupVersioned(path, 5)
|
|
if err != nil || version == "" {
|
|
t.Fatalf("应返回版本路径:%q %v", version, err)
|
|
}
|
|
if !strings.Contains(version, VersionsDir(path)) {
|
|
t.Fatalf("版本应落在版本目录内:%s", version)
|
|
}
|
|
data, err := os.ReadFile(version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != "worker_processes 1;" {
|
|
t.Fatalf("版本内容不符:%q", data)
|
|
}
|
|
}
|
|
|
|
func TestBackupVersionedPrunesToKeep(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "nginx.conf")
|
|
// 每次写入不同内容:内容相同会被去重,剪枝也就无从检验
|
|
for _, content := range []string{"v1", "v2", "v3", "v4", "v5"} {
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := BackupVersioned(path, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
entries, err := os.ReadDir(VersionsDir(path))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 2 {
|
|
t.Fatalf("应只保留 2 份版本,实际 %d", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestBackupVersionedSkipsMissingFile(t *testing.T) {
|
|
version, err := BackupVersioned(filepath.Join(t.TempDir(), "nope.conf"), 5)
|
|
if err != nil || version != "" {
|
|
t.Fatalf("文件不存在时应返回空路径且无错误:%q %v", version, err)
|
|
}
|
|
}
|
|
|
|
func TestListVersionsNewestFirst(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "nginx.conf")
|
|
if err := os.WriteFile(path, []byte("v1"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := BackupVersioned(path, 5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("v2"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := BackupVersioned(path, 5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
versions, err := ListVersions(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(versions) != 2 || versions[0].Name <= versions[1].Name {
|
|
t.Fatalf("版本应按名称倒序(新的在前)列出:%v", versions)
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, version := range versions {
|
|
data, err := ReadVersion(path, version.Name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if version.Size != int64(len(data)) {
|
|
t.Fatalf("版本大小不符:%d vs %d", version.Size, len(data))
|
|
}
|
|
seen[string(data)] = true
|
|
}
|
|
if !seen["v1"] || !seen["v2"] {
|
|
t.Fatalf("两个版本的内容都应在:%v", seen)
|
|
}
|
|
}
|
|
|
|
func TestBackupVersionedSkipsIdenticalContent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "nginx.conf")
|
|
if err := os.WriteFile(path, []byte("same"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := BackupVersioned(path, 5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := BackupVersioned(path, 5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first != second {
|
|
t.Fatalf("内容未变时不应新增版本:%q vs %q", first, second)
|
|
}
|
|
versions, err := ListVersions(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(versions) != 1 {
|
|
t.Fatalf("内容未变时应只有 1 个版本,实际 %d", len(versions))
|
|
}
|
|
}
|
|
|
|
func TestReadVersionRejectsTraversal(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "nginx.conf")
|
|
if _, err := ReadVersion(path, "../nginx.conf"); err == nil {
|
|
t.Fatal("带路径上跳的版本名应被拒绝")
|
|
}
|
|
if _, err := ReadVersion(path, "missing.conf"); err == nil {
|
|
t.Fatal("不存在的版本应报错")
|
|
}
|
|
versions, err := ListVersions(path)
|
|
if err != nil || len(versions) != 0 {
|
|
t.Fatalf("版本目录不存在时应返回空列表且无错误:%v %v", versions, err)
|
|
}
|
|
}
|