593 lines
14 KiB
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]
|
|
}
|