package static
import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestCopyDir_CopiesRegularFiles(t *testing.T) {
src := t.TempDir()
dst := filepath.Join(t.TempDir(), "out")
if err := os.MkdirAll(filepath.Join(src, "sub"), 0o755); err != nil {
t.Fatalf("mkdir sub: %v", err)
}
if err := os.WriteFile(filepath.Join(src, "index.html"), []byte("
hi
"), 0o644); err != nil {
t.Fatalf("write index: %v", err)
}
if err := os.WriteFile(filepath.Join(src, "sub", "nested.txt"), []byte("nested"), 0o644); err != nil {
t.Fatalf("write nested: %v", err)
}
if err := copyDir(src, dst); err != nil {
t.Fatalf("copyDir: %v", err)
}
gotIndex, err := os.ReadFile(filepath.Join(dst, "index.html"))
if err != nil {
t.Fatalf("read index: %v", err)
}
if string(gotIndex) != "hi
" {
t.Errorf("index content = %q", string(gotIndex))
}
gotNested, err := os.ReadFile(filepath.Join(dst, "sub", "nested.txt"))
if err != nil {
t.Fatalf("read nested: %v", err)
}
if string(gotNested) != "nested" {
t.Errorf("nested content = %q", string(gotNested))
}
}
func TestCopyDir_PreservesFileMode(t *testing.T) {
// File modes are only meaningful outside Windows — Windows reports
// 0666 for any writable file regardless of the source mode.
if runtime.GOOS == "windows" {
t.Skip("file modes are not preserved meaningfully on Windows")
}
src := t.TempDir()
dst := filepath.Join(t.TempDir(), "out")
if err := os.WriteFile(filepath.Join(src, "script.sh"), []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatalf("write: %v", err)
}
if err := copyDir(src, dst); err != nil {
t.Fatalf("copyDir: %v", err)
}
info, err := os.Stat(filepath.Join(dst, "script.sh"))
if err != nil {
t.Fatalf("stat: %v", err)
}
if info.Mode().Perm() != 0o755 {
t.Errorf("mode = %v, want 0755", info.Mode().Perm())
}
}
func TestCopyDir_RejectsSymlinks(t *testing.T) {
src := t.TempDir()
dst := filepath.Join(t.TempDir(), "out")
target := filepath.Join(src, "real.txt")
if err := os.WriteFile(target, []byte("real"), 0o644); err != nil {
t.Fatalf("write target: %v", err)
}
if err := os.Symlink(target, filepath.Join(src, "link.txt")); err != nil {
// Windows non-admin users cannot create symlinks. The defense
// is still valuable on Linux, so just skip when unsupported.
t.Skipf("symlink not supported in this environment: %v", err)
}
err := copyDir(src, dst)
if err == nil {
t.Fatal("copyDir accepted a symlink; expected refusal")
}
if !strings.Contains(err.Error(), "non-regular") {
t.Errorf("error = %v, want substring \"non-regular\"", err)
}
}
func TestVerifyDownloadInsideRoot_AcceptsCleanTree(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "sub"), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(root, "sub", "file.txt"), []byte("ok"), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
if err := verifyDownloadInsideRoot(root); err != nil {
t.Fatalf("verifyDownloadInsideRoot rejected clean tree: %v", err)
}
}
func TestVerifyDownloadInsideRoot_RejectsSymlink(t *testing.T) {
root := t.TempDir()
target := filepath.Join(t.TempDir(), "outside.txt")
if err := os.WriteFile(target, []byte("outside"), 0o644); err != nil {
t.Fatalf("write target: %v", err)
}
if err := os.Symlink(target, filepath.Join(root, "escape.txt")); err != nil {
t.Skipf("symlink not supported in this environment: %v", err)
}
err := verifyDownloadInsideRoot(root)
if err == nil {
t.Fatal("verifyDownloadInsideRoot accepted symlink; expected refusal")
}
if !strings.Contains(err.Error(), "non-regular") {
t.Errorf("error = %v, want substring \"non-regular\"", err)
}
}
func TestVerifyDownloadInsideRoot_MissingRootSurfacesError(t *testing.T) {
err := verifyDownloadInsideRoot(filepath.Join(t.TempDir(), "does-not-exist"))
if err == nil {
t.Fatal("expected error for missing root, got nil")
}
if !errors.Is(err, os.ErrNotExist) {
t.Errorf("error = %v, want os.ErrNotExist in chain", err)
}
}
func TestPrepareStaticBuild_WritesDockerfileAndCopiesFiles(t *testing.T) {
src := t.TempDir()
ctxDir := filepath.Join(t.TempDir(), "ctx")
if err := os.WriteFile(filepath.Join(src, "index.html"), []byte("hello"), 0o644); err != nil {
t.Fatalf("write index: %v", err)
}
if err := prepareStaticBuild(src, ctxDir); err != nil {
t.Fatalf("prepareStaticBuild: %v", err)
}
if _, err := os.Stat(filepath.Join(ctxDir, "Dockerfile")); err != nil {
t.Fatalf("Dockerfile missing: %v", err)
}
got, err := os.ReadFile(filepath.Join(ctxDir, "index.html"))
if err != nil {
t.Fatalf("read copied index: %v", err)
}
if string(got) != "hello" {
t.Errorf("copied index content = %q", string(got))
}
}
func TestPrepareStaticBuild_EmptySrcStillWritesDockerfile(t *testing.T) {
// An empty repo folder shouldn't crash the build — nginx will just
// serve an empty image. The Dockerfile must still land.
src := t.TempDir()
ctxDir := filepath.Join(t.TempDir(), "ctx")
if err := prepareStaticBuild(src, ctxDir); err != nil {
t.Fatalf("prepareStaticBuild: %v", err)
}
if _, err := os.Stat(filepath.Join(ctxDir, "Dockerfile")); err != nil {
t.Fatalf("Dockerfile missing: %v", err)
}
}