Ctrl K
ComunidadGuías…

pkg/recording_file_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package pkg

import (
	"context"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"go.keploy.io/server/v3/pkg/models"
	"go.uber.org/zap"
)

// recordingDirs lays out a recording at <tmp>/keploy and a file outside it,
// <tmp>/outside.txt, with a symbolic link to that file inside the recording.
func recordingDirs(t *testing.T) (root, outside string) {
	t.Helper()
	tmp := t.TempDir()
	root = filepath.Join(tmp, "keploy")
	if err := os.MkdirAll(filepath.Join(root, "test-set-0", "assets", "test-1"), 0o755); err != nil {
		t.Fatal(err)
	}
	outside = filepath.Join(tmp, "outside.txt")
	if err := os.WriteFile(outside, []byte("not the recording's"), 0o600); err != nil {
		t.Fatal(err)
	}
	// A machine that cannot make a link (Windows without the privilege)
	// leaves the link's row out.
	if err := os.Symlink(outside, filepath.Join(root, "test-set-0", "assets", "test-1", "link.txt")); err != nil {
		t.Logf("no symbolic link: %v", err)
	}
	return root, outside
}

// hasLink says whether recordingDirs made its link.
func hasLink(root string) bool {
	_, err := os.Lstat(filepath.Join(root, "test-set-0", "assets", "test-1", "link.txt"))
	return err == nil
}

func writeRecordingFile(t *testing.T, root, rel, text string) {
	t.Helper()
	if err := os.WriteFile(filepath.Join(root, rel), []byte(text), 0o600); err != nil {
		t.Fatal(err)
	}
}

// partsApp is an app that keeps each request it is sent: its body, and the
// parts of a multipart one as "name|file name|text".
type partsApp struct {
	mu       sync.Mutex
	requests int
	bodies   []string
	parts    [][]string
}

func (a *partsApp) start(t *testing.T) string {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		a.mu.Lock()
		defer a.mu.Unlock()
		a.requests++
		if mt, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")); err == nil && mt == "multipart/form-data" {
			var got []string
			mr := multipart.NewReader(r.Body, params["boundary"])
			for {
				p, err := mr.NextPart()
				if err != nil {
					break
				}
				b, _ := io.ReadAll(p)
				got = append(got, p.FormName()+"|"+p.FileName()+"|"+string(b))
			}
			a.parts = append(a.parts, got)
		} else {
			b, _ := io.ReadAll(r.Body)
			a.bodies = append(a.bodies, string(b))
		}
		_, _ = io.WriteString(w, "ok")
	}))
	t.Cleanup(srv.Close)
	return srv.URL
}

func (a *partsApp) sent() (int, []string, [][]string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.requests, append([]string(nil), a.bodies...), append([][]string(nil), a.parts...)
}

func bodyRefCase(url, path string) *models.TestCase {
	return &models.TestCase{Kind: models.HTTP, Name: "test-1", HTTPReq: models.HTTPReq{
		Method: http.MethodPost, URL: url + "/upload", ProtoMajor: 1, ProtoMinor: 1,
		Header:  map[string]string{"Content-Type": "text/plain"},
		BodyRef: models.BodyRef{Path: path, Size: 2 << 20},
	}}
}

func formCase(url string, form models.FormData) *models.TestCase {
	return &models.TestCase{Kind: models.HTTP, Name: "test-1", HTTPReq: models.HTTPReq{
		Method: http.MethodPost, URL: url + "/upload", ProtoMajor: 1, ProtoMinor: 1,
		Header: map[string]string{"Content-Type": "multipart/form-data; boundary=x"},
		Form:   []models.FormData{form},
	}}
}

// A test case's file is read only inside its recording, wherever the
// process's working directory is: a body, a form value and a form file kept
// beside the test case are sent from the recording's directory, and a path
// that leads out of it (by "..", as an absolute path, or through a symbolic
// link), or any path with no recording directory, is refused with why, and
// nothing is read or sent.
func TestATestCasesFileIsReadOnlyInsideItsRecording(t *testing.T) {
	root, outside := recordingDirs(t)
	body := strings.Repeat("b", 2<<20)
	writeRecordingFile(t, root, "test-set-0/assets/test-1/body.txt", body)
	writeRecordingFile(t, root, "test-set-0/assets/test-1/value_0.txt", "a value kept beside")
	writeRecordingFile(t, root, "test-set-0/assets/test-1/report.csv", "a,b\n1,2\n")
	// The working directory holds a file at each path too: it is never read.
	cwd := t.TempDir()
	t.Chdir(cwd)
	for _, rel := range []string{"body.txt", "value.txt", "report.csv"} {
		writeRecordingFile(t, cwd, rel, "the working directory's")
	}

	const inside = "is read only inside its recording"
	const noRoot = "no recording directory was given"
	link := "test-set-0/assets/test-1/link.txt"
	refused := []struct {
		name, root, path, why string
	}{
		{"up and out", root, "../outside.txt", "path escapes"},
		{"absolute, outside", root, outside, "path escapes"},
		{"absolute, inside", root, filepath.Join(root, "test-set-0/assets/test-1/body.txt"), "path escapes"},
		{"no recording directory", "", "body.txt", noRoot},
		{"a recording directory that is not there", filepath.Join(root, "gone"), "body.txt", "open the recording directory"},
	}
	if hasLink(root) {
		refused = append(refused, struct{ name, root, path, why string }{"a link out", root, link, "path escapes"})
	}

	t.Run("body", func(t *testing.T) {
		app := &partsApp{}
		url := app.start(t)
		resp, err := SimulateHTTP(context.Background(), bodyRefCase(url, "test-set-0/assets/test-1/body.txt"), "test-set-0", zap.NewNop(), SimulationConfig{APITimeout: 10, KeployPath: root})
		if err != nil || resp.StatusCode != 200 {
			t.Fatalf("SimulateHTTP: %v", err)
		}
		if n, bodies, _ := app.sent(); n != 1 || len(bodies) != 1 || bodies[0] != body {
			t.Fatalf("the app was sent %d requests; want one with the %d bytes kept beside the test case", n, len(body))
		}
		for _, c := range refused {
			t.Run(c.name, func(t *testing.T) {
				app := &partsApp{}
				url := app.start(t)
				_, err := SimulateHTTP(context.Background(), bodyRefCase(url, c.path), "test-set-0", zap.NewNop(), SimulationConfig{APITimeout: 10, KeployPath: c.root})
				if err == nil || !strings.Contains(err.Error(), c.why) || !strings.Contains(err.Error(), fmt.Sprintf("%q", c.path)) {
					t.Fatalf("SimulateHTTP: %v; want it refused, naming %q, with %q", err, c.path, c.why)
				}
				if c.why == "path escapes" && !strings.Contains(err.Error(), inside) {
					t.Fatalf("SimulateHTTP: %v; want %q", err, inside)
				}
				if n, _, _ := app.sent(); n != 0 {
					t.Fatalf("the app was sent %d requests; want none", n)
				}
			})
		}
	})

	t.Run("form value", func(t *testing.T) {
		app := &partsApp{}
		url := app.start(t)
		form := models.FormData{Key: "note", Values: []string{"", "small"}, Paths: []string{"test-set-0/assets/test-1/value_0.txt", ""}}
		if _, err := SimulateHTTP(context.Background(), formCase(url, form), "test-set-0", zap.NewNop(), SimulationConfig{APITimeout: 10, KeployPath: root}); err != nil {
			t.Fatalf("SimulateHTTP: %v", err)
		}
		if n, _, parts := app.sent(); n != 1 || len(parts) != 1 || strings.Join(parts[0], ";") != "note||a value kept beside;note||small" {
			t.Fatalf("the app was sent %d requests, parts %q; want the value kept beside the test case, then the small one", n, parts)
		}
		for _, c := range refused {
			t.Run(c.name, func(t *testing.T) {
				app := &partsApp{}
				url := app.start(t)
				form := models.FormData{Key: "note", Values: []string{""}, Paths: []string{c.path}}
				_, err := SimulateHTTP(context.Background(), formCase(url, form), "test-set-0", zap.NewNop(), SimulationConfig{APITimeout: 10, KeployPath: c.root})
				if err == nil || !strings.Contains(err.Error(), c.why) || !strings.Contains(err.Error(), fmt.Sprintf("%q", c.path)) {
					t.Fatalf("SimulateHTTP: %v; want it refused, naming %q, with %q", err, c.path, c.why)
				}
				if n, _, _ := app.sent(); n != 0 {
					t.Fatalf("the app was sent %d requests; want none", n)
				}
			})
		}
	})

	t.Run("form file", func(t *testing.T) {
		app := &partsApp{}
		url := app.start(t)
		form := models.FormData{Key: "report", FileNames: []string{"report.csv"}, Paths: []string{"test-set-0/assets/test-1/report.csv"}}
		if _, err := SimulateHTTP(context.Background(), formCase(url, form), "test-set-0", zap.NewNop(), SimulationConfig{APITimeout: 10, KeployPath: root}); err != nil {
			t.Fatalf("SimulateHTTP: %v", err)
		}
		if n, _, parts := app.sent(); n != 1 || len(parts) != 1 || strings.Join(parts[0], ";") != "report|report.csv|a,b\n1,2\n" {
			t.Fatalf("the app was sent %d requests, parts %q; want the file kept beside the test case", n, parts)
		}
		for _, c := range refused {
			t.Run(c.name, func(t *testing.T) {
				app := &partsApp{}
				url := app.start(t)
				form := models.FormData{Key: "report", FileNames: []string{"report.csv"}, Paths: []string{c.path}}
				_, err := SimulateHTTP(context.Background(), formCase(url, form), "test-set-0", zap.NewNop(), SimulationConfig{APITimeout: 10, KeployPath: c.root})
				if err == nil || !strings.Contains(err.Error(), c.why) || !strings.Contains(err.Error(), fmt.Sprintf("%q", c.path)) {
					t.Fatalf("SimulateHTTP: %v; want it refused, naming %q, with %q", err, c.path, c.why)
				}
				if n, _, _ := app.sent(); n != 0 {
					t.Fatalf("the app was sent %d requests; want none", n)
				}
			})
		}
	})
}

// Reading a test case's file leaves no file open: neither the recording's
// directory nor the file.
func TestReadingATestCasesFileLeavesNothingOpen(t *testing.T) {
	if _, err := os.Stat("/proc/self/fd"); err != nil {
		t.Skip("no /proc/self/fd to count open files")
	}
	root, _ := recordingDirs(t)
	writeRecordingFile(t, root, "test-set-0/assets/test-1/body.txt", "kept beside")
	open := func() int {
		ents, err := os.ReadDir("/proc/self/fd")
		if err != nil {
			t.Fatal(err)
		}
		return len(ents)
	}
	before := open()
	for i := 0; i < 50; i++ {
		if b, err := ReadRecordingFile(root, "test-set-0/assets/test-1/body.txt"); err != nil || string(b) != "kept beside" {
			t.Fatalf("ReadRecordingFile: %q, %v", b, err)
		}
		if _, err := ReadRecordingFile(root, "../outside.txt"); err == nil {
			t.Fatal("ReadRecordingFile read a file out of the recording")
		}
		f, err := OpenRecordingFile(root, "test-set-0/assets/test-1/body.txt")
		if err != nil {
			t.Fatalf("OpenRecordingFile: %v", err)
		}
		// The file stays readable once its recording's directory is closed.
		if b, err := io.ReadAll(f); err != nil || string(b) != "kept beside" {
			t.Fatalf("read the opened file: %q, %v", b, err)
		}
		if err := f.Close(); err != nil {
			t.Fatal(err)
		}
		if _, err := OpenRecordingFile(root, "../outside.txt"); err == nil {
			t.Fatal("OpenRecordingFile opened a file out of the recording")
		}
	}
	if after := open(); after > before+5 {
		t.Fatalf("%d files open after 50 reads, %d before; want none left open", after, before)
	}
}

// A recording directory that cannot be opened is said so; the read neither
// panics nor reads anything.
func TestARecordingDirectoryThatCannotBeOpenedIsSaid(t *testing.T) {
	gone := filepath.Join(t.TempDir(), "gone")
	for _, read := range []func() error{
		func() error { _, err := ReadRecordingFile(gone, "body.txt"); return err },
		func() error { _, err := OpenRecordingFile(gone, "body.txt"); return err },
	} {
		err := func() (err error) {
			defer func() {
				if p := recover(); p != nil {
					t.Errorf("the read panicked: %v", p)
				}
			}()
			return read()
		}()
		if err == nil || !strings.Contains(err.Error(), "open the recording directory") || !strings.Contains(err.Error(), "body.txt") {
			t.Fatalf("read in a directory that is not there: %v; want it said", err)
		}
	}
}