package install import ( "archive/zip" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "testing" "softbox.local/core/catalog" "softbox.local/core/installer" "softbox.local/core/storage" ) func TestInstallServiceInstallsVerifiedPackageAndRecordsPayloadFiles(t *testing.T) { archivePath, publishedPackage := writeInstallPackage(t, "1.2.3", "new executable") appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) service := newInstallService(t, store, func(currentPath string) error { _, err := os.Stat(filepath.Join(currentPath, "bin", "App.exe")) return err }) result, err := service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.2.3"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if err != nil { t.Fatalf("Install() error = %v", err) } if result.AppID != "test-app" || result.Version != "1.2.3" || result.Recovery.Action != installer.RecoveryNone { t.Fatalf("result = %#v", result) } if got := mustReadFile(t, filepath.Join(appsRoot, "test-app", "current", "bin", "App.exe")); got != "new executable" { t.Fatalf("current entrypoint = %q", got) } record, found, err := store.Read("test-app") if err != nil || !found { t.Fatalf("Read() found=%t err=%v", found, err) } if record.Version != "1.2.3" || len(record.Files) != 2 { t.Fatalf("record = %#v", record) } if record.Entrypoint != "bin/App.exe" || record.WorkingDirectory != "." || record.MinOS != "windows-10" || record.RequiresAdmin { t.Fatalf("launch metadata = %#v", record) } if record.Files[0].Path != "bin/App.exe" || record.Files[0].Size != int64(len("new executable")) { t.Fatalf("record first file = %#v", record.Files[0]) } hash := sha256.Sum256([]byte("new executable")) if record.Files[0].SHA256 != hex.EncodeToString(hash[:]) { t.Fatalf("record first hash = %q", record.Files[0].SHA256) } } func TestInstallServiceRejectsCatalogSelectionAndHashBeforeStaging(t *testing.T) { archivePath, publishedPackage := writeInstallPackage(t, "1.2.3", "new executable") tests := []struct { name string entry catalog.Entry wantErr error wantCode FailureCode }{ { name: "selected package differs from architecture package", entry: func() catalog.Entry { entry := installEntry(publishedPackage, "1.2.3") forged := publishedPackage forged.SHA256 = "0000000000000000000000000000000000000000000000000000000000000000" entry.Package = &forged return entry }(), wantErr: ErrInstallRequestInvalid, wantCode: FailureCodeInstallFailed, }, { name: "download hash differs from Catalog", entry: func() catalog.Entry { entry := installEntry(publishedPackage, "1.2.3") entry.App.Packages[catalog.ArchitectureAMD64] = catalog.Package{ Size: publishedPackage.Size, SHA256: "0000000000000000000000000000000000000000000000000000000000000000", Signature: publishedPackage.Signature, URL: publishedPackage.URL, } *entry.Package = entry.App.Packages[catalog.ArchitectureAMD64] return entry }(), wantErr: installer.ErrPackageHashMismatch, wantCode: FailureCodeHashMismatch, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) service := newInstallService(t, store, func(string) error { return nil }) _, err := service.Install(InstallRequest{ Entry: test.entry, Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if !errors.Is(err, test.wantErr) { t.Fatalf("Install() error = %v, want %v", err, test.wantErr) } if stage := installErrorStage(t, err); stage != InstallStageVerify { t.Fatalf("stage = %q, want %q", stage, InstallStageVerify) } if code := installErrorCode(t, err); code != test.wantCode { t.Fatalf("code = %q, want %q", code, test.wantCode) } if _, statErr := os.Stat(filepath.Join(appsRoot, "test-app", "staging")); !os.IsNotExist(statErr) { t.Fatalf("rejected install left staging, stat error = %v", statErr) } }) } } func TestInstallServiceRollsBackHealthAndRecordWriteFailure(t *testing.T) { appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) oldArchive, oldPackage := writeInstallPackage(t, "1.0.0", "old executable") initial := newInstallService(t, store, func(string) error { return nil }) if _, err := initial.Install(InstallRequest{ Entry: installEntry(oldPackage, "1.0.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: oldArchive, }); err != nil { t.Fatalf("initial Install() error = %v", err) } tests := []struct { name string service func(t *testing.T) *InstallService wantStage InstallStage wantErr error }{ { name: "health failure", service: func(t *testing.T) *InstallService { return newInstallService(t, store, func(string) error { return errors.New("health failed") }) }, wantStage: InstallStageHealth, wantErr: installer.ErrHealthCheckFailed, }, { name: "record write failure", service: func(t *testing.T) *InstallService { return newInstallService(t, &failingRecordStore{ InstalledAppStore: store, writeErr: errors.New("record disk error"), }, func(string) error { return nil }) }, wantStage: InstallStageRecord, wantErr: ErrInstallRecordWrite, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { archivePath, publishedPackage := writeInstallPackage(t, "1.1.0", "new executable") _, err := test.service(t).Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.1.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if !errors.Is(err, test.wantErr) { t.Fatalf("Install() error = %v, want %v", err, test.wantErr) } if stage := installErrorStage(t, err); stage != test.wantStage { t.Fatalf("stage = %q, want %q", stage, test.wantStage) } if got := mustReadFile(t, filepath.Join(appsRoot, "test-app", "current", "bin", "App.exe")); got != "old executable" { t.Fatalf("current entrypoint after failure = %q", got) } record, found, readErr := store.Read("test-app") if readErr != nil || !found || record.Version != "1.0.0" { t.Fatalf("record after failure found=%t record=%#v err=%v", found, record, readErr) } }) } } func TestInstallServiceRechecksTargetImmediatelyBeforeUpdateSwitch(t *testing.T) { appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) oldArchive, oldPackage := writeInstallPackage(t, "1.0.0", "old executable") initial := newInstallService(t, store, func(string) error { return nil }) if _, err := initial.Install(InstallRequest{ Entry: installEntry(oldPackage, "1.0.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: oldArchive, }); err != nil { t.Fatalf("initial Install() error = %v", err) } archivePath, publishedPackage := writeInstallPackage(t, "1.1.0", "new executable") appRoot := filepath.Join(appsRoot, "test-app") oldRecord := mustReadFile(t, filepath.Join(appRoot, "installed-app.json")) targetErr := errors.New("target probe failed") tests := []struct { name string probe func(int) (bool, error) wantErr error wantCode FailureCode }{ { name: "target starts during extraction", probe: func(call int) (bool, error) { return call == 2, nil }, wantErr: ErrTargetRunning, wantCode: FailureCodeAppRunning, }, { name: "target state fails at switch", probe: func(call int) (bool, error) { if call == 2 { return false, targetErr } return false, nil }, wantErr: targetErr, wantCode: FailureCodeTargetStateUnavailable, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { calls := 0 service := newInstallServiceWithCheckers( t, store, func(string) error { return nil }, diskSpaceCheckerFunc(func(string) (int64, error) { return StagingDiskReserveBytes + 64*1024, nil }), targetStateCheckerFunc(func(appID, entrypoint string) (bool, error) { calls++ if appID != "test-app" || entrypoint != filepath.Join(appRoot, "current", "bin", "App.exe") { t.Fatalf("target check = (%q, %q)", appID, entrypoint) } return test.probe(calls) }), ) _, err := service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.1.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if !errors.Is(err, test.wantErr) { t.Fatalf("Install() error = %v, want %v", err, test.wantErr) } if stage := installErrorStage(t, err); stage != InstallStageSwitch { t.Fatalf("stage = %q, want %q", stage, InstallStageSwitch) } if code := installErrorCode(t, err); code != test.wantCode { t.Fatalf("code = %q, want %q", code, test.wantCode) } if calls != 2 { t.Fatalf("target check calls = %d, want 2", calls) } if got := mustReadFile(t, filepath.Join(appRoot, "current", "bin", "App.exe")); got != "old executable" { t.Fatalf("current after failed update = %q", got) } if got := mustReadFile(t, filepath.Join(appRoot, "installed-app.json")); got != oldRecord { t.Fatal("installed-app record changed after failed update") } for _, path := range []string{"staging", "backup", "install-transaction.json"} { if _, statErr := os.Stat(filepath.Join(appRoot, path)); !os.IsNotExist(statErr) { t.Fatalf("failed update left %s, stat error = %v", path, statErr) } } }) } } func TestNewInstallServiceRequiresPreflightCheckers(t *testing.T) { extractor, err := installer.NewExtractor(installTestLimits()) if err != nil { t.Fatalf("NewExtractor() error = %v", err) } store := storage.NewInstalledAppStore(filepath.Join(t.TempDir(), "apps")) config := InstallServiceConfig{ Extractor: extractor, Records: store, Health: func(string) error { return nil }, DiskSpace: diskSpaceCheckerFunc(func(string) (int64, error) { return StagingDiskReserveBytes, nil }), StorageFailures: storageFailureClassifierFunc(func(error) bool { return false }), TargetState: targetStateCheckerFunc(func(string, string) (bool, error) { return false, nil }), } tests := []struct { name string modify func(*InstallServiceConfig) }{ { name: "disk space checker", modify: func(config *InstallServiceConfig) { config.DiskSpace = nil }, }, { name: "target state checker", modify: func(config *InstallServiceConfig) { config.TargetState = nil }, }, { name: "storage failure classifier", modify: func(config *InstallServiceConfig) { config.StorageFailures = nil }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { candidate := config test.modify(&candidate) if _, err := NewInstallService(candidate); !errors.Is(err, ErrInstallServiceConfig) { t.Fatalf("NewInstallService() error = %v, want %v", err, ErrInstallServiceConfig) } }) } } func TestFailureCodeForPackageFailures(t *testing.T) { tests := []struct { err error code FailureCode }{ {err: installer.ErrPackageHashMismatch, code: FailureCodeHashMismatch}, {err: installer.ErrPathEscape, code: FailureCodeZIPPathEscape}, {err: installer.ErrArchiveCorrupt, code: FailureCodeZIPCorrupt}, {err: installer.ErrAppManifestInvalid, code: FailureCodePackageInvalid}, {err: ErrInstallRequestInvalid, code: FailureCodeInstallFailed}, } for _, test := range tests { if got := failureCodeFor(test.err); got != test.code { t.Fatalf("failureCodeFor(%v) = %q, want %q", test.err, got, test.code) } } } func TestInstallServiceClassifiesStagingOutputFailures(t *testing.T) { errDiskFull := errors.New("injected disk full") errOutput := errors.New("injected output failure") service := &InstallService{ storageFailures: storageFailureClassifierFunc(func(err error) bool { return errors.Is(err, errDiskFull) }), } for _, test := range []struct { name string err error code FailureCode }{ { name: "write disk full", err: fmt.Errorf("%w: write staging file: %w", installer.ErrStagingOutput, errDiskFull), code: FailureCodeDiskFull, }, { name: "sync disk full", err: fmt.Errorf("%w: sync staging file: %w", installer.ErrStagingOutput, errDiskFull), code: FailureCodeDiskFull, }, { name: "close disk full", err: fmt.Errorf("%w: close staging file: %w", installer.ErrStagingOutput, errDiskFull), code: FailureCodeDiskFull, }, { name: "generic output I O", err: fmt.Errorf("%w: write staging file: %w", installer.ErrStagingOutput, errOutput), code: FailureCodeInstallFailed, }, { name: "ZIP input remains corrupt", err: fmt.Errorf("%w: %w", installer.ErrArchiveCorrupt, errDiskFull), code: FailureCodeZIPCorrupt, }, } { t.Run(test.name, func(t *testing.T) { wrapped := service.installError(InstallStageExtract, test.err) var installErr *InstallError if !errors.As(wrapped, &installErr) { t.Fatalf("install error = %v, want InstallError", wrapped) } if installErr.Code != test.code { t.Fatalf("code = %q, want %q", installErr.Code, test.code) } if !errors.Is(wrapped, test.err) { t.Fatalf("install error = %v, want preserved cause", wrapped) } }) } } func TestInstallServiceAcceptsExactStagingCapacity(t *testing.T) { archivePath, publishedPackage := writeInstallPackage(t, "1.2.3", "new executable") appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) required := StagingDiskReserveBytes + int64(len("new executable")+len("readme")) var diskRoot string var targetAppID, targetEntrypoint string service := newInstallServiceWithCheckers( t, store, func(string) error { return nil }, diskSpaceCheckerFunc(func(appRoot string) (int64, error) { diskRoot = appRoot return required, nil }), targetStateCheckerFunc(func(appID, entrypointPath string) (bool, error) { targetAppID = appID targetEntrypoint = entrypointPath return false, nil }), ) if _, err := service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.2.3"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }); err != nil { t.Fatalf("Install() error = %v", err) } appRoot := filepath.Join(appsRoot, "test-app") if diskRoot != appRoot { t.Fatalf("disk check root = %q, want %q", diskRoot, appRoot) } if targetAppID != "test-app" || targetEntrypoint != filepath.Join(appRoot, "current", "bin", "App.exe") { t.Fatalf("target check = (%q, %q)", targetAppID, targetEntrypoint) } } func TestInstallServiceInsufficientDiskLeavesNoCurrent(t *testing.T) { archivePath, publishedPackage := writeInstallPackage(t, "1.2.3", "new executable") appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) service := newInstallServiceWithCheckers( t, store, func(string) error { return nil }, diskSpaceCheckerFunc(func(string) (int64, error) { return 0, nil }), targetStateCheckerFunc(func(string, string) (bool, error) { return false, nil }), ) _, err := service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.2.3"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if !errors.Is(err, ErrDiskSpaceInsufficient) { t.Fatalf("Install() error = %v, want %v", err, ErrDiskSpaceInsufficient) } if code := installErrorCode(t, err); code != FailureCodeDiskFull { t.Fatalf("code = %q, want %q", code, FailureCodeDiskFull) } appRoot := filepath.Join(appsRoot, "test-app") for _, managed := range []string{"staging", "current"} { if _, statErr := os.Stat(filepath.Join(appRoot, managed)); !os.IsNotExist(statErr) { t.Fatalf("first install left %s, stat error = %v", managed, statErr) } } } func TestInstallServicePreflightFailuresPreserveExistingVersion(t *testing.T) { appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) oldArchive, oldPackage := writeInstallPackage(t, "1.0.0", "old executable") initial := newInstallService(t, store, func(string) error { return nil }) if _, err := initial.Install(InstallRequest{ Entry: installEntry(oldPackage, "1.0.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: oldArchive, }); err != nil { t.Fatalf("initial Install() error = %v", err) } archivePath, publishedPackage := writeInstallPackage(t, "1.1.0", "new executable") required := StagingDiskReserveBytes + int64(len("new executable")+len("readme")) diskProbeErr := errors.New("disk probe unavailable") targetProbeErr := errors.New("target state unavailable") tests := []struct { name string disk DiskSpaceChecker target TargetStateChecker wantErr error wantCode FailureCode }{ { name: "insufficient disk space", disk: diskSpaceCheckerFunc(func(string) (int64, error) { return required - 1, nil }), target: targetStateCheckerFunc(func(string, string) (bool, error) { return false, nil }), wantErr: ErrDiskSpaceInsufficient, wantCode: FailureCodeDiskFull, }, { name: "disk capacity check fails", disk: diskSpaceCheckerFunc(func(string) (int64, error) { return 0, diskProbeErr }), target: targetStateCheckerFunc(func(string, string) (bool, error) { return false, nil }), wantErr: diskProbeErr, wantCode: FailureCodeDiskCheckFailed, }, { name: "current app is running", disk: diskSpaceCheckerFunc(func(string) (int64, error) { return required, nil }), target: targetStateCheckerFunc(func(string, string) (bool, error) { return true, nil }), wantErr: ErrTargetRunning, wantCode: FailureCodeAppRunning, }, { name: "target state check fails", disk: diskSpaceCheckerFunc(func(string) (int64, error) { return required, nil }), target: targetStateCheckerFunc(func(string, string) (bool, error) { return false, targetProbeErr }), wantErr: targetProbeErr, wantCode: FailureCodeTargetStateUnavailable, }, } appRoot := filepath.Join(appsRoot, "test-app") oldRecord := mustReadFile(t, filepath.Join(appRoot, "installed-app.json")) for _, test := range tests { t.Run(test.name, func(t *testing.T) { service := newInstallServiceWithCheckers(t, store, func(string) error { return nil }, test.disk, test.target) _, err := service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.1.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if !errors.Is(err, test.wantErr) { t.Fatalf("Install() error = %v, want %v", err, test.wantErr) } if stage := installErrorStage(t, err); stage != InstallStagePreflight { t.Fatalf("stage = %q, want %q", stage, InstallStagePreflight) } if code := installErrorCode(t, err); code != test.wantCode { t.Fatalf("code = %q, want %q", code, test.wantCode) } if got := mustReadFile(t, filepath.Join(appRoot, "current", "bin", "App.exe")); got != "old executable" { t.Fatalf("current entrypoint after failure = %q", got) } if got := mustReadFile(t, filepath.Join(appRoot, "installed-app.json")); got != oldRecord { t.Fatal("installed-app record changed after preflight failure") } if _, statErr := os.Stat(filepath.Join(appRoot, "staging")); !os.IsNotExist(statErr) { t.Fatalf("preflight failure left staging, stat error = %v", statErr) } }) } } func TestInstallServiceReportsCorruptPackageWithoutReplacingCurrent(t *testing.T) { appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) oldArchive, oldPackage := writeInstallPackage(t, "1.0.0", "old executable") initial := newInstallService(t, store, func(string) error { return nil }) if _, err := initial.Install(InstallRequest{ Entry: installEntry(oldPackage, "1.0.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: oldArchive, }); err != nil { t.Fatalf("initial Install() error = %v", err) } archivePath, publishedPackage := writeInstallPackage(t, "1.1.0", "new executable") corruptInstallPackageEntry(t, archivePath, "payload/bin/App.exe") document, err := os.ReadFile(archivePath) if err != nil { t.Fatalf("read corrupted archive: %v", err) } hash := sha256.Sum256(document) publishedPackage.Size = int64(len(document)) publishedPackage.SHA256 = hex.EncodeToString(hash[:]) service := newInstallService(t, store, func(string) error { return nil }) _, err = service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.1.0"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if !errors.Is(err, installer.ErrArchiveCorrupt) { t.Fatalf("Install() error = %v, want %v", err, installer.ErrArchiveCorrupt) } if stage := installErrorStage(t, err); stage != InstallStageExtract { t.Fatalf("stage = %q, want %q", stage, InstallStageExtract) } if code := installErrorCode(t, err); code != FailureCodeZIPCorrupt { t.Fatalf("code = %q, want %q", code, FailureCodeZIPCorrupt) } appRoot := filepath.Join(appsRoot, "test-app") if got := mustReadFile(t, filepath.Join(appRoot, "current", "bin", "App.exe")); got != "old executable" { t.Fatalf("current entrypoint after corruption = %q", got) } if _, statErr := os.Stat(filepath.Join(appRoot, "staging")); !os.IsNotExist(statErr) { t.Fatalf("corrupt package left staging, stat error = %v", statErr) } } func TestInstallServiceRecoversPreparedTransactionBeforeExtracting(t *testing.T) { appsRoot := filepath.Join(t.TempDir(), "apps") store := storage.NewInstalledAppStore(appsRoot) appRoot, err := store.EnsureAppRoot("test-app") if err != nil { t.Fatalf("EnsureAppRoot() error = %v", err) } if err := os.MkdirAll(filepath.Join(appRoot, "current"), 0o700); err != nil { t.Fatalf("create current: %v", err) } if err := os.MkdirAll(filepath.Join(appRoot, "staging"), 0o700); err != nil { t.Fatalf("create stale staging: %v", err) } if err := os.WriteFile( filepath.Join(appRoot, "install-transaction.json"), []byte(`{"schema_version":1,"phase":"prepared","had_current":true}`), 0o600, ); err != nil { t.Fatalf("write transaction: %v", err) } archivePath, publishedPackage := writeInstallPackage(t, "1.2.3", "new executable") service := newInstallService(t, store, func(string) error { return nil }) result, err := service.Install(InstallRequest{ Entry: installEntry(publishedPackage, "1.2.3"), Architecture: catalog.ArchitectureAMD64, DownloadPath: archivePath, }) if err != nil { t.Fatalf("Install() error = %v", err) } if result.Recovery.Action != installer.RecoveryAborted { t.Fatalf("recovery action = %q, want %q", result.Recovery.Action, installer.RecoveryAborted) } if got := mustReadFile(t, filepath.Join(appRoot, "current", "bin", "App.exe")); got != "new executable" { t.Fatalf("current entrypoint = %q", got) } if _, statErr := os.Stat(filepath.Join(appRoot, "staging")); !os.IsNotExist(statErr) { t.Fatalf("staging remains, stat error = %v", statErr) } if _, statErr := os.Stat(filepath.Join(appRoot, "install-transaction.json")); !os.IsNotExist(statErr) { t.Fatalf("transaction remains, stat error = %v", statErr) } } type failingRecordStore struct { *storage.InstalledAppStore writeErr error } func (store *failingRecordStore) Write(storage.InstalledApp) error { return store.writeErr } func newInstallService( t *testing.T, store InstallRecordStore, health installer.HealthCheck, ) *InstallService { return newInstallServiceWithCheckers( t, store, health, diskSpaceCheckerFunc(func(string) (int64, error) { return StagingDiskReserveBytes + 64*1024, nil }), targetStateCheckerFunc(func(string, string) (bool, error) { return false, nil }), ) } func newInstallServiceWithCheckers( t *testing.T, store InstallRecordStore, health installer.HealthCheck, diskSpace DiskSpaceChecker, targetState TargetStateChecker, ) *InstallService { t.Helper() extractor, err := installer.NewExtractor(installTestLimits()) if err != nil { t.Fatalf("NewExtractor() error = %v", err) } service, err := NewInstallService(InstallServiceConfig{ Extractor: extractor, Records: store, Health: health, DiskSpace: diskSpace, StorageFailures: storageFailureClassifierFunc(func(error) bool { return false }), TargetState: targetState, }) if err != nil { t.Fatalf("NewInstallService() error = %v", err) } return service } type diskSpaceCheckerFunc func(string) (int64, error) func (check diskSpaceCheckerFunc) AvailableBytes(appRoot string) (int64, error) { return check(appRoot) } type storageFailureClassifierFunc func(error) bool func (classifier storageFailureClassifierFunc) IsDiskFull(err error) bool { return classifier(err) } type targetStateCheckerFunc func(string, string) (bool, error) func (check targetStateCheckerFunc) IsRunning(appID string, entrypointPath string) (bool, error) { return check(appID, entrypointPath) } func installTestLimits() installer.Limits { return installer.Limits{ MaxEntries: 20, MaxArchiveBytes: 64 * 1024, MaxCentralDirectoryBytes: 4 * 1024, MaxUncompressedBytes: 64 * 1024, MaxCompressionRatio: 100, } } func installEntry(publishedPackage catalog.Package, version string) catalog.Entry { selectedPackage := publishedPackage return catalog.Entry{ App: catalog.App{ ID: "test-app", Version: version, Channel: catalog.ReleaseStable, Status: catalog.CatalogStatusActive, MinOS: catalog.Windows10, Architectures: []catalog.Architecture{catalog.ArchitectureAMD64}, EntryEXE: "bin/App.exe", Packages: map[catalog.Architecture]catalog.Package{ catalog.ArchitectureAMD64: publishedPackage, }, }, Package: &selectedPackage, Installable: true, } } func writeInstallPackage(t *testing.T, version, executable string) (string, catalog.Package) { t.Helper() path := filepath.Join(t.TempDir(), "package.download") file, err := os.Create(path) if err != nil { t.Fatalf("create package: %v", err) } writer := zip.NewWriter(file) entries := []struct { name string body []byte mode os.FileMode }{ {name: "app.json", body: installManifest(version)}, {name: "payload/bin/App.exe", body: []byte(executable), mode: 0o755}, {name: "payload/readme.txt", body: []byte("readme")}, } for _, entry := range entries { header := &zip.FileHeader{Name: entry.name} mode := entry.mode if mode == 0 { mode = 0o600 } header.SetMode(mode) part, err := writer.CreateHeader(header) if err != nil { t.Fatalf("create ZIP entry: %v", err) } if _, err := part.Write(entry.body); err != nil { t.Fatalf("write ZIP entry: %v", err) } } if err := writer.Close(); err != nil { file.Close() t.Fatalf("close ZIP writer: %v", err) } if err := file.Close(); err != nil { t.Fatalf("close package: %v", err) } document, err := os.ReadFile(path) if err != nil { t.Fatalf("read package: %v", err) } hash := sha256.Sum256(document) return path, catalog.Package{ URL: "https://download.invalid/test-app.zip", Size: int64(len(document)), SHA256: hex.EncodeToString(hash[:]), Signature: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", } } func installManifest(version string) []byte { return []byte(`{"schema_version":1,"id":"test-app","name":"Test App","vendor":"SoftBox","version":"` + version + `","channel":"stable","min_os":"windows-10","architecture":"amd64","entrypoint":"bin/App.exe","working_directory":".","product_id":"test-product","supports_trial":false,"requires_admin":false,"data_policy":"local-app-data","update_policy":"managed-by-softbox"}`) } func installErrorStage(t *testing.T, err error) InstallStage { t.Helper() var installErr *InstallError if !errors.As(err, &installErr) { t.Fatalf("error %v is not InstallError", err) } return installErr.Stage } func installErrorCode(t *testing.T, err error) FailureCode { t.Helper() var installErr *InstallError if !errors.As(err, &installErr) { t.Fatalf("error %v is not InstallError", err) } return installErr.Code } func corruptInstallPackageEntry(t *testing.T, archivePath, entryName string) { t.Helper() reader, err := zip.OpenReader(archivePath) if err != nil { t.Fatalf("open ZIP for corruption: %v", err) } var offset int64 = -1 for _, file := range reader.File { if file.Name != entryName { continue } offset, err = file.DataOffset() if err != nil { _ = reader.Close() t.Fatalf("entry data offset: %v", err) } break } if err := reader.Close(); err != nil { t.Fatalf("close ZIP reader: %v", err) } if offset < 0 { t.Fatalf("entry %s not found", entryName) } document, err := os.ReadFile(archivePath) if err != nil { t.Fatalf("read ZIP for corruption: %v", err) } document[offset] ^= 0xff if err := os.WriteFile(archivePath, document, 0o600); err != nil { t.Fatalf("write corrupted ZIP: %v", err) } } func mustReadFile(t *testing.T, path string) string { t.Helper() document, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } return string(document) }