258 lines
7.7 KiB
Go
258 lines
7.7 KiB
Go
package artifact
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
|
)
|
|
|
|
func TestCompositeReader_Read(t *testing.T) {
|
|
reader := NewCompositeReader()
|
|
ctx := context.Background()
|
|
|
|
t.Run("inline artifact", func(t *testing.T) {
|
|
ref := domain.ArtifactRef{
|
|
Type: domain.ArtifactRefInline,
|
|
Body: "hello world",
|
|
}
|
|
art, err := reader.Read(ctx, ref)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(art.Body) != "hello world" {
|
|
t.Errorf("expected 'hello world', got %s", string(art.Body))
|
|
}
|
|
if art.ContentType != "text/plain" {
|
|
t.Errorf("expected text/plain content type, got %q", art.ContentType)
|
|
}
|
|
if art.Hash != "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" {
|
|
t.Errorf("unexpected hash: %s", art.Hash)
|
|
}
|
|
})
|
|
|
|
t.Run("inline artifact missing body", func(t *testing.T) {
|
|
ref := domain.ArtifactRef{
|
|
Type: domain.ArtifactRefInline,
|
|
Body: "",
|
|
}
|
|
_, err := reader.Read(ctx, ref)
|
|
if !errors.Is(err, ErrMissingInlineBody) {
|
|
t.Errorf("expected ErrMissingInlineBody, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unsupported ref type", func(t *testing.T) {
|
|
ref := domain.ArtifactRef{
|
|
Type: domain.ArtifactRefType("unsupported"),
|
|
URI: "unsupported://bucket/key",
|
|
}
|
|
_, err := reader.Read(ctx, ref)
|
|
if !errors.Is(err, ErrUnsupportedRefType) {
|
|
t.Error("expected error for unsupported type")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRestrictedCompositeReader(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
|
|
if err := os.WriteFile(filepath.Join(root, "input.txt"), []byte("allowed"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(outside, "secret.txt"), []byte("denied"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader, err := NewRestrictedCompositeReader(root)
|
|
if err != nil {
|
|
t.Fatalf("expected restricted reader construction, got %v", err)
|
|
}
|
|
|
|
t.Run("accepts relative contained path", func(t *testing.T) {
|
|
art, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: "nested/../input.txt"})
|
|
if err != nil {
|
|
t.Fatalf("expected contained relative path to succeed, got %v", err)
|
|
}
|
|
if string(art.Body) != "allowed" {
|
|
t.Fatalf("unexpected artifact body: %q", string(art.Body))
|
|
}
|
|
})
|
|
|
|
t.Run("accepts absolute contained path", func(t *testing.T) {
|
|
art, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: filepath.Join(root, "input.txt")})
|
|
if err != nil {
|
|
t.Fatalf("expected contained absolute path to succeed, got %v", err)
|
|
}
|
|
if art.Name != "input.txt" {
|
|
t.Fatalf("unexpected artifact name: %q", art.Name)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects relative traversal outside root", func(t *testing.T) {
|
|
_, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: filepath.Join("..", filepath.Base(outside), "secret.txt")})
|
|
if !errors.Is(err, ErrFileOutsideRoot) {
|
|
t.Fatalf("expected ErrFileOutsideRoot, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects absolute path outside root", func(t *testing.T) {
|
|
_, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: filepath.Join(outside, "secret.txt")})
|
|
if !errors.Is(err, ErrFileOutsideRoot) {
|
|
t.Fatalf("expected ErrFileOutsideRoot, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRestrictedCompositeReaderFollowsSymlinkInsideRoot(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
|
|
target := filepath.Join(outside, "linked.txt")
|
|
if err := os.WriteFile(target, []byte("linked outside root"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
link := filepath.Join(root, "linked.txt")
|
|
if err := os.Symlink(target, link); err != nil {
|
|
t.Skipf("symlink creation unavailable: %v", err)
|
|
}
|
|
|
|
reader, err := NewRestrictedCompositeReader(root)
|
|
if err != nil {
|
|
t.Fatalf("expected restricted reader construction, got %v", err)
|
|
}
|
|
|
|
art, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: "linked.txt"})
|
|
if err != nil {
|
|
t.Fatalf("expected symlink inside root to be followed, got %v", err)
|
|
}
|
|
if string(art.Body) != "linked outside root" {
|
|
t.Fatalf("unexpected artifact body: %q", string(art.Body))
|
|
}
|
|
}
|
|
|
|
func TestRestrictedCompositeReaderWithoutRootDeniesFileRefs(t *testing.T) {
|
|
reader, err := NewRestrictedCompositeReader("")
|
|
if err != nil {
|
|
t.Fatalf("expected restricted reader construction, got %v", err)
|
|
}
|
|
|
|
art, err := reader.Read(context.Background(), domain.ArtifactRef{Type: domain.ArtifactRefInline, Body: "inline"})
|
|
if err != nil {
|
|
t.Fatalf("expected inline ref to work without artifact root, got %v", err)
|
|
}
|
|
if string(art.Body) != "inline" {
|
|
t.Fatalf("unexpected inline body: %q", string(art.Body))
|
|
}
|
|
|
|
_, err = reader.Read(context.Background(), domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: "input.txt"})
|
|
if !errors.Is(err, ErrFileNotAllowed) {
|
|
t.Fatalf("expected ErrFileNotAllowed, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRestrictedCompositeReaderFileSizeLimit(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "exact.txt"), []byte("12345"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "large.txt"), []byte("123456"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader, err := NewRestrictedCompositeReaderWithLimit(root, 5)
|
|
if err != nil {
|
|
t.Fatalf("expected restricted reader construction, got %v", err)
|
|
}
|
|
|
|
art, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: "exact.txt"})
|
|
if err != nil {
|
|
t.Fatalf("expected file at limit to succeed, got %v", err)
|
|
}
|
|
if string(art.Body) != "12345" {
|
|
t.Fatalf("unexpected artifact body: %q", string(art.Body))
|
|
}
|
|
|
|
_, err = reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: "large.txt"})
|
|
if !errors.Is(err, ErrFileTooLarge) {
|
|
t.Fatalf("expected ErrFileTooLarge, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRestrictedCompositeReaderFileSizeLimitZeroDisablesLimit(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "large.txt"), []byte("123456"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader, err := NewRestrictedCompositeReaderWithLimit(root, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected restricted reader construction, got %v", err)
|
|
}
|
|
art, err := reader.Read(context.Background(), domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: "large.txt"})
|
|
if err != nil {
|
|
t.Fatalf("expected unlimited reader to succeed, got %v", err)
|
|
}
|
|
if string(art.Body) != "123456" {
|
|
t.Fatalf("unexpected artifact body: %q", string(art.Body))
|
|
}
|
|
}
|
|
|
|
func TestFileReader_Read(t *testing.T) {
|
|
content := []byte("test file content")
|
|
tmpFile, err := os.CreateTemp("", "artifact_test_*.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.Remove(tmpFile.Name())
|
|
|
|
if _, err := tmpFile.Write(content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tmpFile.Close()
|
|
|
|
reader := NewCompositeReader()
|
|
ctx := context.Background()
|
|
|
|
t.Run("file artifact loading", func(t *testing.T) {
|
|
ref := domain.ArtifactRef{
|
|
Type: domain.ArtifactRefFile,
|
|
URI: tmpFile.Name(),
|
|
}
|
|
art, err := reader.Read(ctx, ref)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(art.Body) != string(content) {
|
|
t.Errorf("expected %s, got %s", string(content), string(art.Body))
|
|
}
|
|
if art.Name == "" {
|
|
t.Error("expected name to be inferred from filename")
|
|
}
|
|
if art.Hash != "60f5237ed4049f0382661ef009d2bc42e48c3ceb3edb6600f7024e7ab3b838f3" {
|
|
t.Errorf("unexpected hash: %s", art.Hash)
|
|
}
|
|
})
|
|
|
|
t.Run("missing file path", func(t *testing.T) {
|
|
ref := domain.ArtifactRef{
|
|
Type: domain.ArtifactRefFile,
|
|
URI: "",
|
|
}
|
|
_, err := reader.Read(ctx, ref)
|
|
if !errors.Is(err, ErrMissingFilePath) {
|
|
t.Errorf("expected ErrMissingFilePath, got %v", err)
|
|
}
|
|
})
|
|
}
|