Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/artifact/artifact_test.go

Raw
package artifact

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"bugabinga.net/luci/internal/ciconfig"
)

func TestCollectStagesImmutableFiles(t *testing.T) {
	root, workspace := t.TempDir(), t.TempDir()
	if err := os.MkdirAll(filepath.Join(workspace, "dist"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, "dist", "result.txt"), []byte("ok"), 0o644); err != nil {
		t.Fatal(err)
	}
	store := Store{DataDir: root}
	if err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "build", Paths: []string{"dist"}}); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, "dist", "result.txt"), []byte("changed"), 0o644); err != nil {
		t.Fatal(err)
	}
	file, err := store.Open("123", "001/build/dist/result.txt")
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()
	data, err := os.ReadFile(file.Name())
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != "ok" {
		t.Fatalf("stored = %q", data)
	}
	if _, err := store.Open("123", "../build"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("traversal error = %v", err)
	}
}

func TestCollectRequiredMissingAndOversizedFilesStayInvisible(t *testing.T) {
	root, workspace := t.TempDir(), t.TempDir()
	store := Store{DataDir: root}
	if err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "missing", Paths: []string{"none"}, Required: true}); err == nil {
		t.Fatal("missing required artifact accepted")
	}
	if err := os.WriteFile(filepath.Join(workspace, "large"), make([]byte, maxFile+1), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "large", Paths: []string{"large"}}); err == nil {
		t.Fatal("large artifact accepted")
	}
	for _, name := range []string{"missing", "large"} {
		if _, err := os.Stat(filepath.Join(root, "artifacts", "123", "001", name)); !errors.Is(err, os.ErrNotExist) {
			t.Fatalf("partial %q visible: %v", name, err)
		}
	}
}

func TestCollectBoundsFilesAcrossSets(t *testing.T) {
	root, workspace := t.TempDir(), t.TempDir()
	for i := 0; i < maxFiles; i++ {
		name := filepath.Join(workspace, fmt.Sprintf("file-%03d", i))
		if err := os.WriteFile(name, []byte("x"), 0o644); err != nil {
			t.Fatal(err)
		}
	}
	store := Store{DataDir: root}
	paths := make([]string, maxFiles)
	for i := range paths {
		paths[i] = fmt.Sprintf("file-%03d", i)
	}
	if err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "first", Paths: paths}); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, "extra"), []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "second", Paths: []string{"extra"}}); err == nil {
		t.Fatal("child file budget exceeded")
	}
	if _, err := os.Stat(filepath.Join(root, "artifacts", "123", "001", "second")); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("over-budget set visible: %v", err)
	}
}

func TestListAndOpenHideIncompleteStage(t *testing.T) {
	root := t.TempDir()
	stage := filepath.Join(root, "artifacts", "123", "001", ".stage-copy")
	if err := os.MkdirAll(stage, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(stage, "private.txt"), []byte("private"), 0o644); err != nil {
		t.Fatal(err)
	}
	store := Store{DataDir: root}
	files, err := store.List("123", "")
	if err != nil {
		t.Fatal(err)
	}
	if len(files) != 0 {
		t.Fatalf("staged files listed: %#v", files)
	}
	if _, err := store.Open("123", "001/.stage-copy/private.txt"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("staged file opened: %v", err)
	}
}

func TestCollectRejectsSourceAncestorSymlink(t *testing.T) {
	root, workspace, outside := t.TempDir(), t.TempDir(), t.TempDir()
	if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("private"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(outside, filepath.Join(workspace, "dist")); err != nil {
		t.Fatal(err)
	}
	store := Store{DataDir: root}
	if err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "set", Paths: []string{"dist/secret"}, Required: true}); err == nil {
		t.Fatal("ancestor symlink accepted")
	}
	if _, err := store.Open("123", "001/set/dist/secret"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("leaked artifact opened: %v", err)
	}
}

func TestOpenRejectsStorageAncestorSymlink(t *testing.T) {
	root, outside := t.TempDir(), t.TempDir()
	if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("private"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(root, "artifacts", "123"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(outside, filepath.Join(root, "artifacts", "123", "001")); err != nil {
		t.Fatal(err)
	}
	if _, err := (Store{DataDir: root}).Open("123", "001/secret"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("ancestor symlink opened: %v", err)
	}
}

func TestCollectRejectsSymlinkWithoutPartialVisibility(t *testing.T) {
	root, workspace := t.TempDir(), t.TempDir()
	if err := os.Symlink("/etc/passwd", filepath.Join(workspace, "leak")); err != nil {
		t.Fatal(err)
	}
	store := Store{DataDir: root}
	err := store.Collect(workspace, "123", "001", ciconfig.Artifact{Name: "set", Paths: []string{"leak"}, Required: true})
	if err == nil {
		t.Fatal("symlink accepted")
	}
	if _, err := os.Stat(filepath.Join(root, "artifacts", "123", "001", "set")); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("partial set visible: %v", err)
	}
}