Files
scriptorium/internal/adapter/http/artifact_reader_test.go

186 lines
6.0 KiB
Go

package httpadapter
import (
"context"
"errors"
"mime"
"os"
"path/filepath"
"testing"
"gitea.maximumdirect.net/eric/promptkit"
)
func TestRestrictedArtifactReaderReadsContainedFiles(t *testing.T) {
root := t.TempDir()
outside := t.TempDir()
inputPath := filepath.Join(root, "input.html")
if err := os.WriteFile(inputPath, []byte("allowed"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "input.unknown"), []byte("unknown type"), 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)
}
expectedContentType := mime.TypeByExtension(filepath.Ext(inputPath))
if expectedContentType == "" {
t.Fatal("expected built-in HTML content type")
}
reader, err := NewRestrictedArtifactReader(root, 0)
if err != nil {
t.Fatalf("construct restricted reader: %v", err)
}
for _, ref := range []promptkit.ArtifactRef{
{Type: promptkit.ArtifactRefFile, URI: "nested/../input.html"},
{Type: promptkit.ArtifactRefFile, URI: inputPath},
} {
artifact, err := reader.Read(context.Background(), ref)
if err != nil {
t.Fatalf("read contained path %q: %v", ref.URI, err)
}
if artifact.Name != "input.html" || artifact.URI != inputPath || artifact.Size != int64(len("allowed")) || string(artifact.Body) != "allowed" {
t.Fatalf("unexpected artifact metadata: %#v", artifact)
}
if artifact.ContentType != expectedContentType {
t.Fatalf("unexpected artifact content type: got %q, want %q", artifact.ContentType, expectedContentType)
}
if artifact.Hash != artifactHash([]byte("allowed")) {
t.Fatalf("unexpected artifact hash: %q", artifact.Hash)
}
}
artifact, err := reader.Read(context.Background(), promptkit.File("input.unknown"))
if err != nil {
t.Fatalf("read unknown-extension path: %v", err)
}
if artifact.ContentType != fallbackArtifactContentType {
t.Fatalf("unexpected fallback content type: %q", artifact.ContentType)
}
for _, ref := range []promptkit.ArtifactRef{
{Type: promptkit.ArtifactRefFile, URI: filepath.Join("..", filepath.Base(outside), "secret.txt")},
{Type: promptkit.ArtifactRefFile, URI: filepath.Join(outside, "secret.txt")},
} {
_, err := reader.Read(context.Background(), ref)
if !errors.Is(err, ErrFileOutsideRoot) {
t.Fatalf("expected ErrFileOutsideRoot for %q, got %v", ref.URI, err)
}
}
}
func TestRestrictedArtifactReaderFollowsSymlinkAfterLexicalCheck(t *testing.T) {
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)
}
if err := os.Symlink(target, filepath.Join(root, "linked.txt")); err != nil {
t.Skipf("symlink creation unavailable: %v", err)
}
reader, err := NewRestrictedArtifactReader(root, 0)
if err != nil {
t.Fatalf("construct restricted reader: %v", err)
}
artifact, err := reader.Read(context.Background(), promptkit.File("linked.txt"))
if err != nil {
t.Fatalf("read symlink inside root: %v", err)
}
if string(artifact.Body) != "linked outside root" {
t.Fatalf("unexpected symlink artifact body: %q", artifact.Body)
}
}
func TestRestrictedArtifactReaderWithoutRootDeniesFiles(t *testing.T) {
reader, err := NewRestrictedArtifactReader("", 0)
if err != nil {
t.Fatalf("construct rootless reader: %v", err)
}
artifact, err := reader.Read(context.Background(), promptkit.Inline("inline"))
if err != nil {
t.Fatalf("read inline artifact: %v", err)
}
if artifact.ContentType != fallbackArtifactContentType || string(artifact.Body) != "inline" || artifact.Hash != artifactHash([]byte("inline")) {
t.Fatalf("unexpected inline artifact: %#v", artifact)
}
_, err = reader.Read(context.Background(), promptkit.File("input.txt"))
if !errors.Is(err, ErrFileNotAllowed) {
t.Fatalf("expected ErrFileNotAllowed, got %v", err)
}
}
func TestRestrictedArtifactReaderEnforcesLimits(t *testing.T) {
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 := NewRestrictedArtifactReader(root, 5)
if err != nil {
t.Fatalf("construct limited reader: %v", err)
}
artifact, err := reader.Read(context.Background(), promptkit.File("exact.txt"))
if err != nil || string(artifact.Body) != "12345" {
t.Fatalf("expected exact-limit artifact, got %#v and %v", artifact, err)
}
_, err = reader.Read(context.Background(), promptkit.File("large.txt"))
if !errors.Is(err, ErrFileTooLarge) {
t.Fatalf("expected ErrFileTooLarge, got %v", err)
}
unlimited, err := NewRestrictedArtifactReader(root, 0)
if err != nil {
t.Fatalf("construct unlimited reader: %v", err)
}
artifact, err = unlimited.Read(context.Background(), promptkit.File("large.txt"))
if err != nil || string(artifact.Body) != "123456" {
t.Fatalf("expected unlimited artifact, got %#v and %v", artifact, err)
}
if _, err := NewRestrictedArtifactReader(root, -1); err == nil {
t.Fatal("expected negative limit to fail")
}
}
func TestRestrictedArtifactReaderRejectsCanceledAndMalformedReferences(t *testing.T) {
reader, err := NewRestrictedArtifactReader(t.TempDir(), 0)
if err != nil {
t.Fatalf("construct reader: %v", err)
}
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
for _, ref := range []promptkit.ArtifactRef{
promptkit.Inline("input"),
promptkit.File("input.txt"),
} {
_, err := reader.Read(canceledCtx, ref)
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected cancellation for %#v, got %v", ref, err)
}
}
for _, ref := range []promptkit.ArtifactRef{
{Type: promptkit.ArtifactRefType("unsupported")},
{Type: promptkit.ArtifactRefInline},
{Type: promptkit.ArtifactRefFile},
} {
if _, err := reader.Read(context.Background(), ref); err == nil {
t.Fatalf("expected malformed reference %#v to fail", ref)
}
}
}