Scert/backend/internal/nginxactivation/initialize_linux_test.go

190 lines
5.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package nginxactivation
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/processlock"
"golang.org/x/sys/unix"
)
func initializationFixture(t *testing.T) (*rotationFixture, string, string) {
t.Helper()
f := newRotationFixture(t)
f.rotation.Directory = t.TempDir()
if err := os.Chmod(f.rotation.Directory, 0o700); err != nil {
t.Fatal(err)
}
material := t.TempDir()
chain, key := filepath.Join(material, "fullchain.pem"), filepath.Join(material, "key.pem")
writeTestFile(t, chain, f.old.chain)
writeTestFile(t, key, f.old.key)
return f, chain, key
}
func TestInitializeCreatesPrivateBaselineWithoutReload(t *testing.T) {
f, chain, key := initializationFixture(t)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
id, err := f.rotation.Initialize(ctx, chain, key)
if err != nil || !validID(id) {
t.Fatal("初始化失败或版本编号无效", err)
}
if f.reloader.reloadCount != 0 || f.reloader.checkCount != 1 {
t.Fatal("初始化只能检查既有配置,不能重载")
}
for path, mode := range map[string]os.FileMode{
id: 0o700, filepath.Join(id, "fullchain.pem"): 0o600,
filepath.Join(id, "key.pem"): 0o600, includeName: 0o600,
} {
info, err := os.Lstat(filepath.Join(f.rotation.Directory, path))
if err != nil || info.Mode().Perm() != mode {
t.Fatalf("初始化材料权限错误:%s,%v", path, err)
}
}
root, err := os.OpenRoot(f.rotation.Directory)
if err != nil {
t.Fatal(err)
}
defer root.Close()
include, err := readRegular(root, includeName, 4096)
if err != nil || !bytes.Equal(include, renderInclude(f.rotation.Directory, id)) {
t.Fatal("专用引用格式错误", err)
}
if err := verifyIncludeMaterial(root, include, f.old.pair.Certificate[0]); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(f.rotation.Directory, journalName)); !errors.Is(err, os.ErrNotExist) {
t.Fatal("初始化不应创建轮换事务", err)
}
if _, err := f.rotation.Initialize(ctx, chain, key); err == nil {
t.Fatal("不能重复初始化已有状态")
}
if err := requireInclude(root, include); err != nil {
t.Fatal("重复初始化改变了原引用", err)
}
}
func TestInitializeRejectsUnsafeMaterial(t *testing.T) {
for _, scenario := range []string{
"key-readable", "key-writable", "parent-writable", "symlink", "hardlink", "fifo",
"large", "mismatch", "different-leaf", "upstream-mismatch", "relative", "missing",
} {
t.Run(scenario, func(t *testing.T) {
f, chain, key := initializationFixture(t)
switch scenario {
case "key-readable", "key-writable", "parent-writable":
path, mode := key, os.FileMode(0o644)
if scenario == "key-writable" {
mode = 0o666
}
if scenario == "parent-writable" {
path, mode = filepath.Dir(key), 0o777
}
if err := os.Chmod(path, mode); err != nil {
t.Fatal(err)
}
case "symlink":
if err := os.Symlink(key, key+".link"); err != nil {
t.Fatal(err)
}
key += ".link"
case "hardlink":
if err := os.Link(key, key+".link"); err != nil {
t.Fatal(err)
}
case "fifo":
key += ".fifo"
if err := unix.Mkfifo(key, 0o600); err != nil {
t.Fatal(err)
}
case "large":
writeTestFile(t, chain, make([]byte, (1<<20)+1))
case "mismatch":
writeTestFile(t, key, f.candidate.key)
case "different-leaf":
writeTestFile(t, chain, f.candidate.chain)
writeTestFile(t, key, f.candidate.key)
case "upstream-mismatch":
f.upstream.active.Store(&f.candidate.pair)
case "relative":
key = "key.pem"
case "missing":
key += ".missing"
}
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
if _, err := f.rotation.Initialize(ctx, chain, key); err == nil {
t.Fatal("不应接受不可信或不一致的初始化材料")
}
if f.reloader.checkCount != 0 || f.reloader.reloadCount != 0 {
t.Fatal("材料预检失败不应执行 Nginx 命令")
}
assertInitializationEmpty(t, f.rotation.Directory)
})
}
}
func TestInitializePreservesExistingState(t *testing.T) {
for _, scenario := range []string{"include", "transaction", "partial", "external-change", "config-failure", "cancel", "locked"} {
t.Run(scenario, func(t *testing.T) {
f, chain, key := initializationFixture(t)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
name, content := "", []byte("保留原有状态\n")
switch scenario {
case "include", "transaction", "partial":
name = map[string]string{"include": includeName, "transaction": journalName, "partial": ".pending-old"}[scenario]
writeTestFile(t, filepath.Join(f.rotation.Directory, name), content)
case "external-change":
name = includeName
f.reloader.check = func(int) error {
writeTestFile(t, filepath.Join(f.rotation.Directory, name), content)
return nil
}
case "config-failure":
f.reloader.check = func(int) error { return errors.New("注入配置检查失败") }
case "cancel":
cancel()
case "locked":
lock, err := processlock.Acquire(filepath.Join(f.rotation.Directory, ".activation.lock"))
if err != nil {
t.Fatal(err)
}
defer lock.Close()
}
if _, err := f.rotation.Initialize(ctx, chain, key); err == nil {
t.Fatal("已有状态、取消或故障不应初始化成功")
}
if f.reloader.reloadCount != 0 {
t.Fatal("初始化不能重载 Nginx")
}
if name == "" {
assertInitializationEmpty(t, f.rotation.Directory)
return
}
actual, err := os.ReadFile(filepath.Join(f.rotation.Directory, name))
if err != nil || !bytes.Equal(actual, content) {
t.Fatal("初始化不能覆盖已有状态", err)
}
})
}
}
func assertInitializationEmpty(t *testing.T, directory string) {
t.Helper()
root, err := os.OpenRoot(directory)
if err != nil {
t.Fatal(err)
}
defer root.Close()
if err := requireUninitialized(root); err != nil {
t.Fatal("预检失败不应留下证书或引用", err)
}
}