Files
RenewCMS/test/update_restart_fakes_test.go

593 lines
14 KiB
Go

package test
import (
"RenewCMS/internal/domain/update"
"RenewCMS/internal/infrastructure/useCases"
"errors"
"io/fs"
"path"
"sort"
"strings"
"sync"
"time"
)
func newUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, running string) *useCases.GetUpdateStatusUseCase {
return useCases.NewGetUpdateStatusUseCase(store, jobs, helper, &fakeResultStore{}, &fakeRestartRequestStore{}, running, false, time.Now)
}
type fakeRestartRequestStore struct {
mu sync.Mutex
request update.RestartRequest
present bool
corrupt bool
writeErr error
writes int
deletes int
}
var _ update.RestartRequestStore = &fakeRestartRequestStore{}
func (f *fakeRestartRequestStore) Write(request update.RestartRequest) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.writeErr != nil {
return f.writeErr
}
f.request = request
f.present = true
f.writes++
return nil
}
func (f *fakeRestartRequestStore) Read() (update.RestartRequest, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.corrupt {
return update.RestartRequest{}, errors.New("corrupt marker")
}
if !f.present {
return update.RestartRequest{}, update.ErrNoRestartRequest
}
return f.request, nil
}
func (f *fakeRestartRequestStore) Delete() error {
f.mu.Lock()
defer f.mu.Unlock()
f.present = false
f.deletes++
return nil
}
type fakeResultStore struct {
mu sync.Mutex
result update.Result
found bool
writeErr error
writes []update.Result
deletes int
}
var _ update.ResultStore = &fakeResultStore{}
func (f *fakeResultStore) Read() (update.Result, bool) {
f.mu.Lock()
defer f.mu.Unlock()
return f.result, f.found
}
func (f *fakeResultStore) Write(result update.Result) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.writeErr != nil {
return f.writeErr
}
f.result = result
f.found = true
f.writes = append(f.writes, result)
return nil
}
func (f *fakeResultStore) Delete() error {
f.mu.Lock()
defer f.mu.Unlock()
f.found = false
f.result = update.Result{}
f.deletes++
return nil
}
type fakeRunningInfoStore struct {
mu sync.Mutex
info update.RunningInfo
found bool
writeErr error
writes []update.RunningInfo
}
var _ update.RunningInfoStore = &fakeRunningInfoStore{}
func (f *fakeRunningInfoStore) Write(info update.RunningInfo) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.writeErr != nil {
return f.writeErr
}
f.info = info
f.found = true
f.writes = append(f.writes, info)
return nil
}
func (f *fakeRunningInfoStore) Read() (update.RunningInfo, bool) {
f.mu.Lock()
defer f.mu.Unlock()
return f.info, f.found
}
func (f *fakeRunningInfoStore) set(info update.RunningInfo) {
f.mu.Lock()
defer f.mu.Unlock()
f.info = info
f.found = true
}
func (f *fakeRunningInfoStore) unreadable() {
f.mu.Lock()
defer f.mu.Unlock()
f.info = update.RunningInfo{}
f.found = false
}
type fakeClock struct {
mu sync.Mutex
now time.Time
sleeps int
onSleep func(sleeps int)
}
var _ update.Clock = &fakeClock{}
func (f *fakeClock) Now() time.Time {
f.mu.Lock()
defer f.mu.Unlock()
return f.now
}
func (f *fakeClock) Sleep(duration time.Duration) {
f.mu.Lock()
f.now = f.now.Add(duration)
f.sleeps++
sleeps, hook := f.sleeps, f.onSleep
f.mu.Unlock()
if hook != nil {
hook(sleeps)
}
}
func (f *fakeClock) advance(duration time.Duration) {
f.mu.Lock()
defer f.mu.Unlock()
f.now = f.now.Add(duration)
}
type fakeServiceControl struct {
mu sync.Mutex
restarts []time.Time
clock *fakeClock
failOn map[int]error
panicOn int
onRestart func(call int)
events *[]string
}
var _ update.ServiceControl = &fakeServiceControl{}
func (f *fakeServiceControl) Restart() error {
f.mu.Lock()
f.restarts = append(f.restarts, f.clock.Now())
call := len(f.restarts)
failure := f.failOn[call]
shouldPanic := f.panicOn == call
hook := f.onRestart
f.mu.Unlock()
if f.events != nil {
*f.events = append(*f.events, "restart")
}
if shouldPanic {
panic("service control exploded")
}
if failure != nil {
return failure
}
if hook != nil {
hook(call)
}
return nil
}
func (f *fakeServiceControl) Status() (bool, error) {
return true, nil
}
func (f *fakeServiceControl) restartCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.restarts)
}
type fakeHealthChecker struct {
mu sync.Mutex
healthy bool
asked int
}
var _ update.HealthChecker = &fakeHealthChecker{}
func (f *fakeHealthChecker) Healthy() bool {
f.mu.Lock()
defer f.mu.Unlock()
f.asked++
return f.healthy
}
type fsNode struct {
kind string
content []byte
mode uint32
target string
owner string
}
type fsOperation struct {
name string
path string
resolved string
}
var errFakeLoop = errors.New("too many levels of symbolic links")
type fakeHelperFS struct {
mu sync.Mutex
nodes map[string]*fsNode
ops []fsOperation
counts map[string]int
failures map[string]map[int]error
hooks map[string]map[int]func(*fakeHelperFS)
tempDirs int
events *[]string
history map[string][]string
}
var _ update.HelperFilesystem = &fakeHelperFS{}
func newFakeHelperFS() *fakeHelperFS {
f := &fakeHelperFS{
nodes: map[string]*fsNode{"/": {kind: "dir", mode: 0o755, owner: "root"}},
counts: map[string]int{},
failures: map[string]map[int]error{},
hooks: map[string]map[int]func(*fakeHelperFS){},
history: map[string][]string{},
}
return f
}
func (f *fakeHelperFS) failNth(operation string, call int, err error) {
if f.failures[operation] == nil {
f.failures[operation] = map[int]error{}
}
f.failures[operation][call] = err
}
func (f *fakeHelperFS) after(operation string, call int, mutate func(*fakeHelperFS)) {
if f.hooks[operation] == nil {
f.hooks[operation] = map[int]func(*fakeHelperFS){}
}
f.hooks[operation][call] = mutate
}
func (f *fakeHelperFS) putDir(target string) {
target = path.Clean(target)
for current := target; current != "/"; current = path.Dir(current) {
if _, found := f.nodes[current]; !found {
f.nodes[current] = &fsNode{kind: "dir", mode: 0o755, owner: "app"}
}
}
}
func (f *fakeHelperFS) putFile(target string, content string) {
target = path.Clean(target)
f.putDir(path.Dir(target))
f.nodes[target] = &fsNode{kind: "file", content: []byte(content), mode: 0o644, owner: "app"}
}
func (f *fakeHelperFS) putSymlink(target string, destination string) {
target = path.Clean(target)
f.putDir(path.Dir(target))
f.nodes[target] = &fsNode{kind: "symlink", target: destination, owner: "app"}
}
func (f *fakeHelperFS) content(target string) (string, bool) {
node, found := f.nodes[path.Clean(target)]
if !found || node.kind != "file" {
return "", false
}
return string(node.content), true
}
func (f *fakeHelperFS) exists(target string) bool {
_, found := f.nodes[path.Clean(target)]
return found
}
func (f *fakeHelperFS) mode(target string) uint32 {
if node, found := f.nodes[path.Clean(target)]; found {
return node.mode
}
return 0
}
func (f *fakeHelperFS) resolve(target string, followFinal bool) (string, error) {
return f.resolveDepth(path.Clean(target), followFinal, 0)
}
func (f *fakeHelperFS) resolveDepth(target string, followFinal bool, depth int) (string, error) {
if depth > 8 {
return "", errFakeLoop
}
parts := strings.Split(strings.Trim(target, "/"), "/")
current := "/"
for index, part := range parts {
if part == "" {
continue
}
next := path.Join(current, part)
node, found := f.nodes[next]
last := index == len(parts)-1
if found && node.kind == "symlink" && (!last || followFinal) {
resolved, err := f.resolveDepth(node.target, true, depth+1)
if err != nil {
return "", err
}
current = resolved
continue
}
current = next
}
return current, nil
}
func (f *fakeHelperFS) begin(operation string, target string, followFinal bool) (string, error) {
f.counts[operation]++
resolved, err := f.resolve(target, followFinal)
f.ops = append(f.ops, fsOperation{name: operation, path: target, resolved: resolved})
if f.events != nil {
*f.events = append(*f.events, operation+" "+target)
}
if failure := f.failures[operation][f.counts[operation]]; failure != nil {
return resolved, failure
}
return resolved, err
}
func (f *fakeHelperFS) end(operation string) {
if hook := f.hooks[operation][f.counts[operation]]; hook != nil {
hook(f)
}
}
func missing(operation string, target string) error {
return &fs.PathError{Op: operation, Path: target, Err: fs.ErrNotExist}
}
func (f *fakeHelperFS) Lstat(target string) (update.FileInfo, error) {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("lstat")
resolved, err := f.begin("lstat", target, false)
if err != nil {
return update.FileInfo{}, err
}
node, found := f.nodes[resolved]
if !found {
return update.FileInfo{}, missing("lstat", target)
}
return update.FileInfo{IsSymlink: node.kind == "symlink", IsDir: node.kind == "dir", IsRegular: node.kind == "file"}, nil
}
func (f *fakeHelperFS) ReadFile(target string) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("read")
resolved, err := f.begin("read", target, false)
if err != nil {
return nil, err
}
node, found := f.nodes[resolved]
if !found {
return nil, missing("read", target)
}
if node.kind == "symlink" {
return nil, errFakeLoop
}
if node.kind != "file" {
return nil, errors.New("is a directory")
}
return append([]byte(nil), node.content...), nil
}
func (f *fakeHelperFS) createNode(operation string, target string, content []byte, mode uint32) error {
resolved, err := f.begin(operation, target, false)
if err != nil {
return err
}
if _, found := f.nodes[resolved]; found {
return &fs.PathError{Op: operation, Path: target, Err: fs.ErrExist}
}
parent, found := f.nodes[path.Dir(resolved)]
if !found || parent.kind != "dir" {
return missing(operation, target)
}
f.nodes[resolved] = &fsNode{kind: "file", content: content, mode: mode, owner: "root"}
return nil
}
func (f *fakeHelperFS) CreateExclusive(target string, content []byte, mode uint32) error {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("create")
return f.createNode("create", target, append([]byte(nil), content...), mode)
}
func (f *fakeHelperFS) Copy(source string, destination string, mode uint32) error {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("copy")
resolved, err := f.begin("copy", source, false)
if err != nil {
return err
}
node, found := f.nodes[resolved]
if !found {
return missing("copy", source)
}
if node.kind == "symlink" {
return errFakeLoop
}
if node.kind != "file" {
return errors.New("not a regular file")
}
f.ops = append(f.ops, fsOperation{name: "copy-to", path: destination})
return f.createNode("copy-to", destination, append([]byte(nil), node.content...), mode)
}
func (f *fakeHelperFS) Rename(oldPath string, newPath string) error {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("rename")
from, err := f.begin("rename", oldPath, false)
if err != nil {
return err
}
to, err := f.resolve(newPath, false)
if err != nil {
return err
}
node, found := f.nodes[from]
if !found {
return missing("rename", oldPath)
}
if existing, found := f.nodes[to]; found && existing.kind == "dir" {
return errors.New("is a directory")
}
f.ops = append(f.ops, fsOperation{name: "rename-to", path: newPath, resolved: to})
f.nodes[to] = node
delete(f.nodes, from)
f.history[to] = append(f.history[to], string(node.content))
return nil
}
func (f *fakeHelperFS) Remove(target string) error {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("remove")
resolved, err := f.begin("remove", target, false)
if err != nil {
return err
}
if _, found := f.nodes[resolved]; !found {
return missing("remove", target)
}
delete(f.nodes, resolved)
return nil
}
func (f *fakeHelperFS) RemoveAll(target string) error {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("removeall")
resolved, err := f.begin("removeall", target, false)
if err != nil {
return err
}
prefix := resolved + "/"
for name := range f.nodes {
if name == resolved || strings.HasPrefix(name, prefix) {
delete(f.nodes, name)
}
}
return nil
}
func (f *fakeHelperFS) Chmod(target string, mode uint32) error {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("chmod")
resolved, err := f.begin("chmod", target, false)
if err != nil {
return err
}
node, found := f.nodes[resolved]
if !found {
return missing("chmod", target)
}
if node.kind == "symlink" {
return errFakeLoop
}
node.mode = mode
return nil
}
func (f *fakeHelperFS) MakePrivateTempDir() (string, error) {
f.mu.Lock()
defer f.mu.Unlock()
defer f.end("tempdir")
f.tempDirs++
directory := "/tmp/renewcms-update-" + string(rune('0'+f.tempDirs))
if _, err := f.begin("tempdir", directory, false); err != nil {
return "", err
}
f.nodes["/tmp"] = &fsNode{kind: "dir", mode: 0o1777, owner: "root"}
f.nodes[directory] = &fsNode{kind: "dir", mode: 0o700, owner: "root"}
return directory, nil
}
func (f *fakeHelperFS) touched(operations ...string) []fsOperation {
wanted := map[string]bool{}
for _, name := range operations {
wanted[name] = true
}
var touched []fsOperation
for _, operation := range f.ops {
if wanted[operation.name] {
touched = append(touched, operation)
}
}
return touched
}
func (f *fakeHelperFS) resolvedOutside(allowed ...string) []string {
var outside []string
for _, operation := range f.ops {
if operation.resolved == "" {
continue
}
inside := false
for _, directory := range allowed {
if operation.resolved == directory || strings.HasPrefix(operation.resolved, directory+"/") {
inside = true
}
}
if !inside {
outside = append(outside, operation.name+" "+operation.resolved)
}
}
sort.Strings(outside)
return outside
}
func (f *fakeHelperFS) operationCount(name string) int {
return f.counts[name]
}