//go:build darwin package installer import ( "context" "os" "os/exec" "path/filepath" "testing" "time" "gitgud.io/mike/mpv-manager/internal/process" "github.com/stretchr/testify/require" ) // This opt-in native test creates and mounts only a disposable empty image. // No application/config installation or existing volume is changed. func TestNativeInterruptedDMGAttachDetachesOwnedMount(t *testing.T) { if os.Getenv("MPV_MANAGER_NATIVE_DMG_QA") != "1" { t.Skip("set MPV_MANAGER_NATIVE_DMG_QA=1 on disposable native macOS QA") } root := t.TempDir() image := filepath.Join(root, "owned.dmg") mountRoot := filepath.Join(root, "mounts") require.NoError(t, os.Mkdir(mountRoot, 0700)) // macOS /var resolves through /private/var; hdiutil reports resolved paths. var err error mountRoot, err = filepath.EvalSymlinks(mountRoot) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() _, err = process.Output(ctx, exec.CommandContext(ctx, "hdiutil", "create", "-size", "16m", "-fs", "HFS+", "-volname", "MPVManagerOwnedMountQA", image)) require.NoError(t, err) mi := &MacOSInstaller{} for _, cause := range []error{context.Canceled, context.DeadlineExceeded} { t.Run(cause.Error(), func(t *testing.T) { // Emulate an attach that mounted the image, then failed/canceled before its // complete plist was accepted. Production uses this same cleanup lifetime. err := withOwnedDMGMounts(mountRoot, func() ([]dmgMount, error) { attachCtx, attachCancel := context.WithTimeout(context.Background(), 30*time.Second) defer attachCancel() output, err := process.Output(attachCtx, exec.CommandContext(attachCtx, "hdiutil", "attach", "-plist", "-nobrowse", "-noautoopen", "-readonly", "-mountroot", mountRoot, image)) if err != nil { return nil, err } mounts, err := parseHDIUtilAttachPlist(output) if err != nil { return nil, err } require.NotEmpty(t, mounts) return nil, cause }, mi.mountedDMGs, mi.unmountDMG, func([]dmgMount) error { t.Fatal("failed attach must not install"); return nil }) require.ErrorIs(t, err, cause) mounts, err := mi.mountedDMGs() require.NoError(t, err) for _, mount := range mounts { require.False(t, pathIsWithinRoot(mountRoot, mount.MountPoint), "owned mount remained attached: %s", mount.MountPoint) } }) } } func TestNativeInstallerRecoveryInApplications(t *testing.T) { if os.Getenv("MPV_MANAGER_NATIVE_DMG_QA") != "1" { t.Skip("native disposable QA application-parent policy") } isolateConfig(t) backup, err := os.MkdirTemp("/Applications", ".mpv-manager-file-backup-native-") require.NoError(t, err) live := backup + "-player" journalPath := backup + installerJournalSuffix t.Cleanup(func() { _ = os.Remove(live); _ = os.Remove(journalPath); _ = os.RemoveAll(backup) }) original := filepath.Join(backup, "original") require.NoError(t, os.WriteFile(live, []byte("new native payload"), 0600)) require.NoError(t, os.WriteFile(original, []byte("old native payload"), 0600)) txn := &installerTransaction{journalPath: journalPath, journal: installerTransactionJournal{Schema: installerTransactionSchema, Kind: installerTransactionFile, State: "applying", Destination: live, BackupRoot: backup, Changes: []installerJournalChange{{Target: live, Backup: original, HadOriginal: true}}}} require.NoError(t, txn.write()) journalPath = txn.journalPath require.NoError(t, RecoverInstallerTransactions(live)) assertTransactionFile(t, live, "old native payload") require.NoFileExists(t, journalPath) require.NoDirExists(t, backup) }