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] }