Files
kandev/apps/backend/internal/tools/installer/github_tarball_strategy_test.go
T

324 lines
7.8 KiB
Go

package installer
import (
"archive/tar"
"bytes"
"compress/gzip"
"os"
"path/filepath"
"testing"
)
func TestSanitizeTarPath(t *testing.T) {
destDir := "/install"
tests := []struct {
name string
path string
wantErr bool
wantClean string
}{
{
name: "simple path",
path: "code-server/bin/code-server",
wantErr: false,
wantClean: "code-server/bin/code-server",
},
{
name: "nested path",
path: "code-server-4.96.4-macos-arm64/bin/code-server",
wantErr: false,
wantClean: "code-server-4.96.4-macos-arm64/bin/code-server",
},
{
name: "path traversal with dotdot",
path: "../etc/passwd",
wantErr: true,
},
{
name: "absolute path",
path: "/etc/passwd",
wantErr: true,
},
{
name: "current directory",
path: ".",
wantErr: false,
wantClean: ".",
},
{
name: "hidden traversal via dotdot in middle",
path: "foo/../../etc/passwd",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clean, err := sanitizeTarPath(tt.path, destDir)
if tt.wantErr {
if err == nil {
t.Errorf("expected error for path %q, got nil", tt.path)
}
return
}
if err != nil {
t.Errorf("unexpected error for path %q: %v", tt.path, err)
return
}
if clean != tt.wantClean {
t.Errorf("expected clean path %q, got %q", tt.wantClean, clean)
}
})
}
}
func TestExpandTemplate(t *testing.T) {
s := &GithubTarballStrategy{
config: GithubTarballConfig{
Version: "4.96.4",
},
}
tests := []struct {
name string
tmpl string
target string
expected string
}{
{
name: "asset pattern",
tmpl: "code-server-{version}-{os}-{arch}.tar.gz",
target: "macos-arm64",
expected: "code-server-4.96.4-macos-arm64.tar.gz",
},
{
name: "binary path pattern",
tmpl: "code-server-{version}-{os}-{arch}/bin/code-server",
target: "linux-amd64",
expected: "code-server-4.96.4-linux-amd64/bin/code-server",
},
{
name: "no placeholders",
tmpl: "binary",
target: "linux-amd64",
expected: "binary",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := s.expandTemplate(tt.tmpl, tt.target)
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
func TestExtractTarGz(t *testing.T) {
// Create a tar.gz archive in memory with a file and a directory
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
// Add a directory
_ = tarWriter.WriteHeader(&tar.Header{
Name: "myapp/",
Typeflag: tar.TypeDir,
Mode: 0o755,
})
// Add a file
content := []byte("#!/bin/sh\necho hello")
_ = tarWriter.WriteHeader(&tar.Header{
Name: "myapp/bin/hello",
Typeflag: tar.TypeReg,
Mode: 0o755,
Size: int64(len(content)),
})
_, _ = tarWriter.Write(content)
_ = tarWriter.Close()
_ = gzWriter.Close()
// Extract into temp dir
destDir := t.TempDir()
if err := extractTarGz(&buf, destDir); err != nil {
t.Fatalf("extractTarGz failed: %v", err)
}
// Verify the file was extracted
extractedPath := filepath.Join(destDir, "myapp", "bin", "hello")
data, err := os.ReadFile(extractedPath)
if err != nil {
t.Fatalf("failed to read extracted file: %v", err)
}
if string(data) != string(content) {
t.Errorf("expected %q, got %q", string(content), string(data))
}
}
func TestExtractTarGz_RejectsTraversal(t *testing.T) {
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
// Add a malicious entry
content := []byte("malicious")
_ = tarWriter.WriteHeader(&tar.Header{
Name: "../etc/passwd",
Typeflag: tar.TypeReg,
Mode: 0o644,
Size: int64(len(content)),
})
_, _ = tarWriter.Write(content)
_ = tarWriter.Close()
_ = gzWriter.Close()
destDir := t.TempDir()
err := extractTarGz(&buf, destDir)
if err == nil {
t.Error("expected error for path traversal, got nil")
}
}
func TestExtractTarGz_SymlinkEscapeBlocked(t *testing.T) {
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
// Add a symlink that escapes the dest dir
_ = tarWriter.WriteHeader(&tar.Header{
Name: "escape",
Typeflag: tar.TypeSymlink,
Linkname: "../../../../etc/passwd",
})
_ = tarWriter.Close()
_ = gzWriter.Close()
destDir := t.TempDir()
err := extractTarGz(&buf, destDir)
if err == nil {
t.Error("expected error for symlink escape, got nil")
}
}
func TestExtractTarGz_SymlinkWithRelativeDotDot(t *testing.T) {
// Legitimate relative symlinks using ".." (like npm .bin links) should be allowed
// as long as the resolved target stays within destDir.
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
// Create directory structure: pkg/node_modules/.bin/ and pkg/node_modules/semver/bin/
for _, dir := range []string{
"pkg/node_modules/.bin/",
"pkg/node_modules/semver/bin/",
} {
_ = tarWriter.WriteHeader(&tar.Header{
Name: dir,
Typeflag: tar.TypeDir,
Mode: 0o755,
})
}
// Create the target file
content := []byte("#!/bin/sh\necho semver")
_ = tarWriter.WriteHeader(&tar.Header{
Name: "pkg/node_modules/semver/bin/semver.js",
Typeflag: tar.TypeReg,
Mode: 0o755,
Size: int64(len(content)),
})
_, _ = tarWriter.Write(content)
// Create symlink: .bin/semver -> ../semver/bin/semver.js (stays within destDir)
_ = tarWriter.WriteHeader(&tar.Header{
Name: "pkg/node_modules/.bin/semver",
Typeflag: tar.TypeSymlink,
Linkname: "../semver/bin/semver.js",
})
_ = tarWriter.Close()
_ = gzWriter.Close()
destDir := t.TempDir()
if err := extractTarGz(&buf, destDir); err != nil {
t.Fatalf("extractTarGz should allow relative '..' symlinks within destDir, got: %v", err)
}
// Verify the symlink was created
linkPath := filepath.Join(destDir, "pkg/node_modules/.bin/semver")
linkTarget, err := os.Readlink(linkPath)
if err != nil {
t.Fatalf("failed to read symlink: %v", err)
}
if linkTarget != "../semver/bin/semver.js" {
t.Errorf("expected symlink target '../semver/bin/semver.js', got %q", linkTarget)
}
}
func TestExtractTarGz_SymlinkDotDotEscapeBlocked(t *testing.T) {
// A ".." symlink that escapes destDir must still be blocked by the resolved-path check.
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
_ = tarWriter.WriteHeader(&tar.Header{
Name: "pkg/",
Typeflag: tar.TypeDir,
Mode: 0o755,
})
// Symlink that uses ".." to escape the destDir
_ = tarWriter.WriteHeader(&tar.Header{
Name: "pkg/escape",
Typeflag: tar.TypeSymlink,
Linkname: "../../etc/passwd",
})
_ = tarWriter.Close()
_ = gzWriter.Close()
destDir := t.TempDir()
err := extractTarGz(&buf, destDir)
if err == nil {
t.Error("expected error for '..' symlink escaping destDir, got nil")
}
}
func TestResolveTarget_Unsupported(t *testing.T) {
s := &GithubTarballStrategy{
config: GithubTarballConfig{
Targets: map[string]string{
"linux/amd64": "linux-amd64",
},
},
}
// resolveTarget uses runtime.GOOS/GOARCH — if the test platform isn't in Targets,
// it should return an error. We can't guarantee this, so just verify the method works.
_, err := s.resolveTarget()
// The result depends on the test platform; just verify no panic.
_ = err
}
func TestBuildURL(t *testing.T) {
s := &GithubTarballStrategy{
config: GithubTarballConfig{
Owner: "coder",
Repo: "code-server",
Version: "4.96.4",
AssetPattern: "code-server-{version}-{os}-{arch}.tar.gz",
},
}
url := s.buildURL("macos-arm64")
expected := "https://github.com/coder/code-server/releases/download/v4.96.4/code-server-4.96.4-macos-arm64.tar.gz"
if url != expected {
t.Errorf("expected %q, got %q", expected, url)
}
}