165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
package nginxactivation
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/processlock"
|
|
)
|
|
|
|
func requestIDForTest(t *testing.T) string {
|
|
t.Helper()
|
|
id, err := newID()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func TestRequestFencePersistsAcrossRestartAndNextTransaction(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
id := requestIDForTest(t)
|
|
for range 2 {
|
|
state, err := f.rotation.ResolveRequest(t.Context(), id)
|
|
if err != nil || state.ID != id || state.Status != "canceled" {
|
|
t.Fatal("未送达请求应持久化取消栅栏", state, err)
|
|
}
|
|
}
|
|
nextID := requestIDForTest(t)
|
|
if _, err := f.rotation.StageRequest(t.Context(), nextID, f.candidate.chain, f.candidate.key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.upstream.active.Store(&f.candidate.pair)
|
|
if err := f.rotation.Commit(t.Context(), nextID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted := f.rotation
|
|
if _, err := restarted.StageRequest(t.Context(), id, f.candidate.chain, f.candidate.key); err == nil {
|
|
t.Fatal("重启且后续事务提交后仍须拒绝旧请求")
|
|
}
|
|
if _, err := restarted.StageRequest(t.Context(), nextID, f.candidate.chain, f.candidate.key); err == nil {
|
|
t.Fatal("已经提交的请求不能重放")
|
|
}
|
|
if f.reloader.reloadCount != 1 {
|
|
t.Fatal("重复请求触发了额外重载")
|
|
}
|
|
}
|
|
|
|
func TestRequestReservationFailureDoesNotActivate(t *testing.T) {
|
|
for _, invalid := range []string{"symlink", "corrupt", "directory"} {
|
|
t.Run(invalid, func(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
id := requestIDForTest(t)
|
|
path := filepath.Join(f.rotation.Directory, "request-"+id+".json")
|
|
switch invalid {
|
|
case "symlink":
|
|
if err := os.Symlink("missing", path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "directory":
|
|
if err := os.Mkdir(path, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "corrupt":
|
|
writeTestFile(t, path, []byte(`{"status":"canceled"}`))
|
|
}
|
|
if _, err := f.rotation.StageRequest(t.Context(), id, f.candidate.chain, f.candidate.key); err == nil {
|
|
t.Fatal("无法可信保存请求时不能激活")
|
|
}
|
|
if _, err := f.rotation.ResolveRequest(t.Context(), id); err == nil {
|
|
t.Fatal("不能把损坏请求记录当成尚未执行")
|
|
}
|
|
if f.reloader.reloadCount != 0 {
|
|
t.Fatal("请求记录故障不能重载代理")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestResolveCannotOvertakeActiveStage(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
id := requestIDForTest(t)
|
|
started, release := make(chan struct{}), make(chan struct{})
|
|
f.reloader.check = func(number int) error {
|
|
if number == 1 {
|
|
close(started)
|
|
<-release
|
|
}
|
|
return nil
|
|
}
|
|
done := make(chan error, 1)
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
go func() {
|
|
_, err := f.rotation.StageRequest(ctx, id, f.candidate.chain, f.candidate.key)
|
|
done <- err
|
|
}()
|
|
select {
|
|
case <-started:
|
|
case <-time.After(3 * time.Second):
|
|
close(release)
|
|
t.Fatal("暂存没有开始")
|
|
}
|
|
_, err := f.rotation.ResolveRequest(ctx, id)
|
|
close(release)
|
|
stageErr := <-done
|
|
if !errors.Is(err, processlock.ErrLocked) || stageErr != nil {
|
|
t.Fatal("恢复不能越过正在执行的暂存", err, stageErr)
|
|
}
|
|
state, err := f.rotation.ResolveRequest(ctx, id)
|
|
if err != nil || state.ID != id || state.Status != "staged" {
|
|
t.Fatal("已执行请求应返回对应事务", state, err)
|
|
}
|
|
}
|
|
|
|
func TestRequestReservationBeforeTransactionCanBeCanceled(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
id := requestIDForTest(t)
|
|
if _, err := f.rotation.StageRequest(t.Context(), id, []byte("invalid"), []byte("invalid")); err == nil {
|
|
t.Fatal("无效材料必须拒绝")
|
|
}
|
|
state, err := f.rotation.ResolveRequest(t.Context(), id)
|
|
if err != nil || state.Status != "canceled" {
|
|
t.Fatal("已占位但未写入事务的请求应允许取消", state, err)
|
|
}
|
|
if _, err := f.rotation.StageRequest(t.Context(), id, f.candidate.chain, f.candidate.key); err == nil {
|
|
t.Fatal("同一编号不能在失败后替换材料重试")
|
|
}
|
|
}
|
|
|
|
func TestRequestResolveRefusesChangedBaseline(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
id := requestIDForTest(t)
|
|
if _, err := f.rotation.StageRequest(t.Context(), id, []byte("invalid"), []byte("invalid")); err == nil {
|
|
t.Fatal("无效材料必须拒绝")
|
|
}
|
|
if _, err := f.rotation.Stage(t.Context(), f.candidate.chain, f.candidate.key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := f.rotation.ResolveRequest(t.Context(), id); err == nil {
|
|
t.Fatal("基准事务已变化时不能推断旧占位尚未执行")
|
|
}
|
|
f.assertStatus(t, "staged")
|
|
if f.reloader.reloadCount != 1 {
|
|
t.Fatal("恢复查询不能改动无关事务")
|
|
}
|
|
}
|
|
|
|
func TestRequestProtocolRejectsMissingIDAndExtraMaterial(t *testing.T) {
|
|
id := requestIDForTest(t)
|
|
for _, message := range []request{
|
|
{Version: protocolVersion, Action: "stage_request", Chain: []byte("chain"), Key: []byte("key")},
|
|
{Version: protocolVersion, Action: "stage_request", ID: "../request", Chain: []byte("chain"), Key: []byte("key")},
|
|
{Version: protocolVersion, Action: "resolve_request"},
|
|
{Version: protocolVersion, Action: "resolve_request", ID: id, Chain: []byte("chain")},
|
|
} {
|
|
if validRequest(message) {
|
|
t.Fatal("无效请求不应通过协议检查", message.Action)
|
|
}
|
|
}
|
|
}
|