658bda1d24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
195 lines
6.4 KiB
Go
195 lines
6.4 KiB
Go
package updater
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/ulikunitz/xz"
|
|
|
|
modulepkg "github.com/dbledeez/panel/pkg/module"
|
|
)
|
|
|
|
// makeFactorioTarXz builds a tiny tar.xz shaped like factorio's
|
|
// headless tarball (factorio/bin/x64/factorio).
|
|
func makeFactorioTarXz(t *testing.T) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
xw, err := xz.NewWriter(&buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tw := tar.NewWriter(xw)
|
|
body := []byte("ELF-not-really")
|
|
if err := tw.WriteHeader(&tar.Header{Name: "factorio/bin/x64/factorio", Typeflag: tar.TypeReg, Mode: 0o755, Size: int64(len(body))}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tw.Write(body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := xw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func testCtx(t *testing.T, dataPath string) *Context {
|
|
t.Helper()
|
|
return &Context{
|
|
InstanceID: "test",
|
|
DataPath: dataPath,
|
|
Log: func(string) {},
|
|
}
|
|
}
|
|
|
|
// The factorio case: archive content + extensionless URL + extract:true
|
|
// must land the extracted tree under TargetPath, not a raw blob.
|
|
func TestDirectExtractsArchiveIntoTargetDir(t *testing.T) {
|
|
blob := makeFactorioTarXz(t)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write(blob)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/get-download/stable/headless/linux64", TargetPath: "/game", Extract: true}
|
|
if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bin := filepath.Join(dir, "game", "factorio", "bin", "x64", "factorio")
|
|
data, err := os.ReadFile(bin)
|
|
if err != nil {
|
|
t.Fatalf("expected extracted binary at %s: %v", bin, err)
|
|
}
|
|
if string(data) != "ELF-not-really" {
|
|
t.Fatalf("binary content mismatch: %q", data)
|
|
}
|
|
// The old bug: raw blob written AS the target path.
|
|
if fi, err := os.Stat(filepath.Join(dir, "game")); err != nil || !fi.IsDir() {
|
|
t.Fatalf("target path should be a directory, got err=%v", err)
|
|
}
|
|
}
|
|
|
|
// Same archive, no explicit extract flag: extensionless TargetPath
|
|
// still triggers extraction (directory heuristic).
|
|
func TestDirectHeuristicExtractsWithoutFlag(t *testing.T) {
|
|
blob := makeFactorioTarXz(t)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write(blob)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/linux64", TargetPath: "/game"}
|
|
if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "game", "factorio", "bin", "x64", "factorio")); err != nil {
|
|
t.Fatalf("expected extraction: %v", err)
|
|
}
|
|
}
|
|
|
|
// The terraria/minecraft guard: a TargetPath WITH a file extension is
|
|
// written verbatim even when the content has archive magic (jars are
|
|
// zips; terraria's entrypoint unzips its own .zip).
|
|
func TestDirectWritesArchiveVerbatimWhenTargetHasExtension(t *testing.T) {
|
|
// A zip blob (like a .jar or terraria-server zip).
|
|
var zipBuf bytes.Buffer
|
|
zipBuf.Write([]byte{0x50, 0x4b, 0x03, 0x04}) // zip local-header magic
|
|
zipBuf.WriteString("rest-of-zip-not-parsed")
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write(zipBuf.Bytes())
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/terraria-server-1449.zip", TargetPath: "/terraria-server.zip"}
|
|
if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, "terraria-server.zip"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(data, zipBuf.Bytes()) {
|
|
t.Fatal("zip should have been written verbatim")
|
|
}
|
|
}
|
|
|
|
// extract:true on non-archive content is a hard error, not a silent
|
|
// verbatim write.
|
|
func TestDirectExtractFlagOnNonArchiveErrors(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("just text"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/blob", TargetPath: "/game", Extract: true}
|
|
if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err == nil {
|
|
t.Fatal("expected error for extract on non-archive content")
|
|
}
|
|
}
|
|
|
|
// containerTargetAbs: absolute targets matching a declared root or
|
|
// volume mount stay container-absolute (factorio's /game volume);
|
|
// anything else keeps the historical BrowseableRoot join (terraria's
|
|
// /terraria-server.zip → /game-saves/terraria-server.zip).
|
|
func TestContainerTargetAbs(t *testing.T) {
|
|
uc := &Context{
|
|
BrowseableRoot: "/game-saves",
|
|
Manifest: &modulepkg.Manifest{
|
|
Runtime: modulepkg.Runtime{Docker: &modulepkg.DockerSpec{
|
|
BrowseableRoots: []modulepkg.BrowseableRoot{{Name: "Saves", Path: "/game-saves"}},
|
|
Volumes: []modulepkg.Volume{
|
|
{Type: "volume", Name: "panel-$INSTANCE_ID-saves", Container: "/game-saves"},
|
|
{Type: "volume", Name: "panel-$INSTANCE_ID-game", Container: "/game"},
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
cases := map[string]string{
|
|
"/game": "/game",
|
|
"/game/sub": "/game/sub",
|
|
"/game-saves/mods": "/game-saves/mods",
|
|
"/terraria-server.zip": "/game-saves/terraria-server.zip",
|
|
"relative/file": "/game-saves/relative/file",
|
|
}
|
|
for in, want := range cases {
|
|
if got := containerTargetAbs(uc, in); got != want {
|
|
t.Errorf("containerTargetAbs(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Non-archive content with an extensionless target keeps the old
|
|
// single-file write behavior.
|
|
func TestDirectNonArchiveStillWritesFile(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("binary-ish payload"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/blob", TargetPath: "/some/file"}
|
|
if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, "some", "file"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != "binary-ish payload" {
|
|
t.Fatalf("content mismatch: %q", data)
|
|
}
|
|
}
|