1package main23import (4 "bytes"5 "context"6 "io"7 "net/http"8 "os/exec"9 "path/filepath"10 "reflect"11 "runtime"12 "strings"13 "testing"14)1516func TestOpArgv(t *testing.T) {17 src, _ := sourceBySlug("ign-wiki")18 argv, err := fetchArgv(src, "cyberpunk-2077/night-city", "/depot/gamemap/fmg-archive")19 if err != nil {20 t.Fatal(err)21 }22 want := []string{23 "go", "run", "./tools/crawl",24 "-archive", "/depot/gamemap/fmg-archive",25 "-ign", "cyberpunk-2077/night-city",26 }27 if !reflect.DeepEqual(argv, want) {28 t.Errorf("fetch argv %v, want %v", argv, want)29 }30 if _, err := fetchArgv(src, "not a target", "/depot/gamemap/fmg-archive"); err == nil {31 t.Error("a bad target built an argv")32 }3334 argv = tilesArgv("/depot/gamemap/fmg-archive")35 want = []string{"go", "run", "./tools/tiles", "-source", "/depot/gamemap/fmg-archive", "-output", "build/tiles"}36 if !reflect.DeepEqual(argv, want) {37 t.Errorf("tiles argv %v, want %v", argv, want)38 }3940 argv, err = generateArgv("/depot/gamemap/fmg-archive", "/library/bundles")41 if err != nil {42 t.Fatal(err)43 }44 want = []string{45 "go", "run", "./tools/generate",46 "-source", "/depot/gamemap",47 "-tiles", "build/tiles/index.json",48 "-bundles", "/library/bundles",49 }50 if !reflect.DeepEqual(argv, want) {51 t.Errorf("generate argv %v, want %v", argv, want)52 }53 // tools/generate finds captures under a directory literally named54 // fmg-archive; pointing it anywhere else must refuse, not guess.55 if _, err := generateArgv("/depot/gamemap/archive", "/library/bundles"); err == nil {56 t.Error("generate accepted an archive that is not an fmg-archive")57 }58}5960func TestOpsHandlerRefusals(t *testing.T) {61 server, dir := testServer(t)62 ts := newTestSite(t, server)63 post := func(form string, header ...string) (int, string) {64 t.Helper()65 request, err := http.NewRequest("POST", ts.URL+"/ops/run", strings.NewReader(form))66 if err != nil {67 t.Fatal(err)68 }69 request.Header.Set("Content-Type", "application/x-www-form-urlencoded")70 for at := 0; at+1 < len(header); at += 2 {71 request.Header.Set(header[at], header[at+1])72 }73 response, err := ts.Client().Do(request)74 if err != nil {75 t.Fatal(err)76 }77 defer response.Body.Close()78 body, _ := io.ReadAll(response.Body)79 return response.StatusCode, string(body)80 }8182 // Without a repository and an archive nothing may run, whatever is asked.83 if status, _ := post("op=tiles"); status != 400 {84 t.Errorf("op without a repo answered %d", status)85 }86 server.shop.repo = t.TempDir()87 if status, _ := post("op=tiles"); status != 400 {88 t.Errorf("op without an archive answered %d", status)89 }9091 server.shop.archive = filepath.Join(dir, "fmg-archive")92 if status, _ := post("op=vanish"); status != 400 {93 t.Errorf("unknown op answered %d", status)94 }95 if status, _ := post("op=fetch&source=nowhere&target=x"); status != 400 {96 t.Errorf("unknown source answered %d", status)97 }98 if status, _ := post("op=fetch&source=mapgenie&target=bad/target"); status != 400 {99 t.Errorf("bad target answered %d", status)100 }101 // A cross-site POST carries a foreign Origin and must be turned away.102 if status, _ := post("op=tiles", "Origin", "http://evil.example"); status != 403 {103 t.Errorf("foreign origin answered %d", status)104 }105}106107func TestOpsHandlerStreams(t *testing.T) {108 if _, err := exec.LookPath("go"); err != nil {109 t.Skip("no go binary on PATH")110 }111 server, dir := testServer(t)112 ts := newTestSite(t, server)113 // The repo points at an empty directory, so the tool run fails at once --114 // but its refusal must arrive through the page as streamed text, which115 // is the whole wiring under test.116 server.shop.repo = t.TempDir()117 server.shop.archive = filepath.Join(dir, "fmg-archive")118119 response, err := ts.Client().Post(ts.URL+"/ops/run", "application/x-www-form-urlencoded",120 strings.NewReader("op=generate"))121 if err != nil {122 t.Fatal(err)123 }124 defer response.Body.Close()125 body, err := io.ReadAll(response.Body)126 if err != nil {127 t.Fatal(err)128 }129 text := string(body)130 if response.StatusCode != 200 {131 t.Fatalf("streamed op answered %d: %s", response.StatusCode, text)132 }133 if !strings.Contains(text, "go run ./tools/generate") {134 t.Errorf("stream does not announce its command: %q", text)135 }136 if !strings.Contains(text, "cartograph: ") {137 t.Errorf("stream does not carry the failure: %q", text)138 }139}140141func TestStreamCommand(t *testing.T) {142 if _, err := exec.LookPath("go"); err != nil {143 t.Skip("no go binary on PATH")144 }145 var out bytes.Buffer146 if err := streamCommand(context.Background(), &out, t.TempDir(), []string{"go", "env", "GOOS"}); err != nil {147 t.Fatal(err)148 }149 if got := strings.TrimSpace(out.String()); got != runtime.GOOS {150 t.Errorf("streamed %q, want %q", got, runtime.GOOS)151 }152153 out.Reset()154 err := streamCommand(context.Background(), &out, t.TempDir(), []string{"go", "definitely-not-a-subcommand"})155 if err == nil {156 t.Fatal("a failing command reported no error")157 }158 if out.Len() == 0 {159 t.Error("the failing command's own words were not streamed")160 }161}