package installer import ( "archive/tar" "bytes" "compress/gzip" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "testing" "time" "gitgud.io/mike/mpv-manager/pkg/config" "github.com/stretchr/testify/require" "lukechampine.com/blake3" ) func TestOctoberAuditRestorePermissions(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Unix mode regression") } isolateConfig(t) path, err := GetMPVConfigPath() require.NoError(t, err) require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) require.NoError(t, os.WriteFile(path, []byte("private=yes\n"), 0600)) backup, err := CreateFullBackup(OSFileSystem{}, path, nil) require.NoError(t, err) require.NoError(t, os.WriteFile(path, []byte("private=no\n"), 0600)) require.NoError(t, RestoreBackup(OSFileSystem{}, backup, path, nil)) info, err := os.Stat(path) require.NoError(t, err) t.Logf("RestoreBackup original mode=0600 resulting mode=%04o", info.Mode().Perm()) require.Equal(t, os.FileMode(0644), info.Mode().Perm(), "audit reproduces lost permission metadata") } func TestOctoberAuditForgedJournalDeletesUnrelatedSibling(t *testing.T) { parent := t.TempDir() legitimate := filepath.Join(parent, "configured-install") require.NoError(t, os.Mkdir(legitimate, 0700)) victim := filepath.Join(parent, "unrelated-private-documents") require.NoError(t, os.Mkdir(victim, 0700)) sentinel := filepath.Join(victim, "keep.txt") require.NoError(t, os.WriteFile(sentinel, []byte("keep me"), 0600)) backupRoot := filepath.Join(parent, ".mpv-manager-file-backup-planted") require.NoError(t, os.Mkdir(backupRoot, 0700)) planted := installerTransactionJournal{Schema:1, Kind:installerTransactionFile, State:"applying", Destination:victim, BackupRoot:backupRoot, Changes:[]installerJournalChange{{Target:victim, HadOriginal:false}}} data, err := json.Marshal(planted) require.NoError(t, err) journalPath := backupRoot+installerJournalSuffix require.NoError(t, os.WriteFile(journalPath, data, 0644)) require.NoError(t, RecoverInstallerTransactions(legitimate)) _, err = os.Stat(sentinel) t.Logf("recovery requested only configured-install; unrelated victim deleted=%t", os.IsNotExist(err)) require.True(t, os.IsNotExist(err)) _, err = os.Stat(journalPath) require.True(t, os.IsNotExist(err), "forged journal removed after acceptance") } func TestOctoberAuditFFmpegFixedScratchDeletesUnrelatedData(t *testing.T) { isolateConfig(t) installDir := t.TempDir() unrelated := filepath.Join(installDir, "ffmpeg-temp", "user-document.txt") require.NoError(t, os.MkdirAll(filepath.Dir(unrelated), 0700)) require.NoError(t, os.WriteFile(unrelated, []byte("private data"), 0600)) body := []byte("not a 7z archive") digest := blake3.Sum256(body) manifest := ReleaseInfo{} manifest.FFmpeg.X8664.URL = "https://fixture.invalid/ffmpeg.7z" manifest.FFmpeg.X8664.BLAKE3 = fmt.Sprintf("blake3:%x", digest) manifest.FFmpeg.X8664v3 = manifest.FFmpeg.X8664 client := &MockHTTPClient{DoFunc:func(req *http.Request)(*http.Response,error) { return &http.Response{StatusCode:http.StatusOK, ContentLength:int64(len(body)), Body:io.NopCloser(bytes.NewReader(body))},nil }} inst := NewInstaller(manifest, installDir, WithHTTPClient(client)) runner := NewCommandRunner(make(chan string, 100), nil) err := inst.UpdateFFmpegWithOutput(runner, installDir) require.Error(t, err) _, statErr := os.Stat(unrelated) t.Logf("FFmpeg extraction failed=%v; pre-existing ffmpeg-temp/user-document.txt deleted=%t", err, os.IsNotExist(statErr)) require.True(t, os.IsNotExist(statErr)) } func TestOctoberAuditWebShortcutOverwritesExistingManager(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("Linux shortcut implementation") } isolateConfig(t) home, err := os.UserHomeDir() require.NoError(t, err) require.NoError(t, os.Mkdir(filepath.Join(home,"Desktop"), 0700)) target := filepath.Join(home,".config","mpv","mpv-manager") require.NoError(t, os.MkdirAll(filepath.Dir(target), 0700)) previous := []byte("NEWER MANAGER SENTINEL - MUST BE PRESERVED ON STARTUP") require.NoError(t, os.WriteFile(target, previous, 0755)) require.NoError(t, CreateLinuxWebUIShortcutWithOutput(NewCommandRunner(make(chan string,100), nil))) actual, err := os.ReadFile(target) require.NoError(t, err) execPath, err := os.Executable() require.NoError(t, err) source, err := os.ReadFile(execPath) require.NoError(t, err) t.Logf("existing installed manager replaced=%t; now equals running executable=%t", !bytes.Equal(actual,previous), bytes.Equal(actual,source)) require.Equal(t, source, actual) require.Equal(t,target,config.GetManagerBinPath()) } type auditZeroReader struct{} func (auditZeroReader) Read(p []byte)(int,error) { clear(p); return len(p),nil } func TestOctoberAuditTarPreflightIgnoresCancellation(t *testing.T) { root := t.TempDir() archivePath := filepath.Join(root,"large.tar.gz") output, err := os.Create(archivePath) require.NoError(t, err) gzipWriter := gzip.NewWriter(output) tarWriter := tar.NewWriter(gzipWriter) const expandedSize = int64(256<<20) require.NoError(t, tarWriter.WriteHeader(&tar.Header{Name:"payload",Mode:0644,Size:expandedSize})) _, err = io.CopyN(tarWriter,auditZeroReader{},expandedSize) require.NoError(t,err) require.NoError(t,tarWriter.Close()) require.NoError(t,gzipWriter.Close()) require.NoError(t,output.Close()) ctx,cancel := context.WithCancel(context.Background()) cancel() started := time.Now() err = extractTarTransactional(ctx,archivePath,filepath.Join(root,"destination"),"gzip",nil) duration := time.Since(started) require.ErrorIs(t,err,context.Canceled) t.Logf("already canceled tar extraction returned only after full 256 MiB preflight; elapsed=%s",duration) // Error from malformed archive instead of cancellation additionally proves // that context is not checked before opening/preflighting. require.NoError(t,os.WriteFile(archivePath,[]byte("bad"),0600)) err = extractTarTransactional(ctx,archivePath,filepath.Join(root,"destination"),"gzip",nil) t.Logf("already canceled corrupt tar returns archive error instead of context cancellation=%v",err) require.False(t,errors.Is(err,context.Canceled)) } func TestOctoberAuditFlatpakQuietProbeIgnoresOperationDeadline(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("Linux installer") } isolateConfig(t) fixtureDir := t.TempDir() fixture := filepath.Join(fixtureDir,"flatpak") require.NoError(t,os.WriteFile(fixture,[]byte("#!/bin/sh\nif [ \"$1\" = remote-list ]; then sleep 1; echo flathub; fi\n"),0755)) t.Setenv("PATH",fixtureDir+string(os.PathListSeparator)+os.Getenv("PATH")) ctx,cancel := context.WithTimeout(context.Background(),50*time.Millisecond) defer cancel() runner := NewCommandRunnerWithContext(ctx,make(chan string,100),nil) runner.SetInteractiveSudoAllowed(false) linuxInstaller := NewLinuxInstaller(NewInstaller(ReleaseInfo{},t.TempDir()),nil) started := time.Now() err := linuxInstaller.InstallFlatpakWithOutput(runner,"io.mpv.Mpv","none",false) elapsed := time.Since(started) require.ErrorIs(t,err,context.DeadlineExceeded) t.Logf("operation deadline=50ms; plain exec flatpak remote-list returned after %s",elapsed) require.Greater(t,elapsed,800*time.Millisecond) }