package runner
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
type entry struct {
name, body, link string
mode int64
dir bool
}
func tarball(t *testing.T, entries ...entry) []byte {
t.Helper()
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
tw := tar.NewWriter(gz)
for _, e := range entries {
h := &tar.Header{Name: e.name, Mode: e.mode}
switch {
case e.dir:
h.Typeflag = tar.TypeDir
case e.link != "":
h.Typeflag, h.Linkname = tar.TypeSymlink, e.link
default:
h.Typeflag, h.Size = tar.TypeReg, int64(len(e.body))
}
if h.Mode == 0 {
h.Mode = 0o644
}
if err := tw.WriteHeader(h); err != nil {
t.Fatal(err)
}
if h.Typeflag == tar.TypeReg {
tw.Write([]byte(e.body))
}
}
tw.Close()
gz.Close()
return buf.Bytes()
}
// serve answers GET /r/demo/archive/<checkin> with body, and remembers
// the Basic credentials it was sent.
func serve(t *testing.T, body []byte, gotUser *string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !strings.HasPrefix(req.URL.Path, "/r/demo/archive/") {
http.NotFound(w, req)
return
}
if u, _, ok := req.BasicAuth(); ok && gotUser != nil {
*gotUser = u
}
w.Write(body)
}))
}
func TestAnArchiveCheckoutUnpacksTheTreeWithoutItsTopDirectory(t *testing.T) {
srv := serve(t, tarball(t,
entry{name: "demo-abc123/", dir: true},
entry{name: "demo-abc123/ci/runner.json", body: `{"tasks":{"demo.test":["true"]}}`},
entry{name: "demo-abc123/run.sh", body: "#!/bin/sh\n", mode: 0o755},
entry{name: "demo-abc123/docs/link", link: "../ci/runner.json"},
), nil)
defer srv.Close()
dir := filepath.Join(t.TempDir(), "ws")
os.MkdirAll(dir, 0o755)
os.WriteFile(filepath.Join(dir, "stale.txt"), []byte("from the last task"), 0o644)
a := ArchiveCheckout{URL: srv.URL + "/r/demo"}
if err := a.Checkout(context.Background(), dir, "abc123"); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, "ci", "runner.json")); err != nil {
t.Errorf("the manifest is not at the tree's root: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "stale.txt")); err == nil {
t.Error("a file from the previous check-in survived the checkout")
}
if st, err := os.Stat(filepath.Join(dir, "run.sh")); err != nil || st.Mode().Perm()&0o100 == 0 {
t.Errorf("an executable arrived without its exec bit: %v %v", st, err)
}
if got, err := os.ReadFile(filepath.Join(dir, "docs", "link")); err != nil || !strings.Contains(string(got), "demo.test") {
t.Errorf("a link inside the tree was not kept: %q %v", got, err)
}
}
func TestAnArchiveThatReachesOutsideTheTreeIsRefused(t *testing.T) {
for name, e := range map[string]entry{
"dotdot": {name: "demo-x/../../escape.txt", body: "x"},
"absolute link": {name: "demo-x/l", link: "/etc/passwd"},
"escaping link": {name: "demo-x/l", link: "../../outside"},
} {
t.Run(name, func(t *testing.T) {
srv := serve(t, tarball(t, e), nil)
defer srv.Close()
root := t.TempDir()
dir := filepath.Join(root, "ws")
err := ArchiveCheckout{URL: srv.URL + "/r/demo"}.Checkout(context.Background(), dir, "x")
if err == nil {
t.Fatal("the checkout accepted it")
}
if _, err := os.Stat(filepath.Join(root, "escape.txt")); err == nil {
t.Error("a file was written outside the workspace")
}
if _, err := os.Stat(dir); err == nil {
t.Error("a refused archive left a workspace behind")
}
})
}
}
func TestAPrivateRepositorysArchiveIsFetchedAsItsReader(t *testing.T) {
var got string
srv := serve(t, tarball(t, entry{name: "demo-x/a", body: "a"}), &got)
defer srv.Close()
a := ArchiveCheckout{URL: srv.URL + "/r/demo", User: "jan", Password: "sync-token"}
if err := a.Checkout(context.Background(), filepath.Join(t.TempDir(), "ws"), "x"); err != nil {
t.Fatal(err)
}
if got != "jan" {
t.Errorf("the archive was fetched as %q, want jan", got)
}
}
func TestAMissingArchiveFailsTheCheckoutWithTheForgesAnswer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "no such repository", http.StatusNotFound)
}))
defer srv.Close()
err := ArchiveCheckout{URL: srv.URL + "/r/demo"}.Checkout(context.Background(), filepath.Join(t.TempDir(), "ws"), "x")
if err == nil || !strings.Contains(err.Error(), "404") {
t.Fatalf("got %v", err)
}
}