Skip to content
quarry Artifact [401ff772aa]
Login

Artifact [401ff772aa]

Artifact 401ff772aa447dac0fc87209d7007d4604a4b44325f2ddd10ba423b96aeb1ddc:


package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/ops"
)

// newRepoAddIn is every field the flow can send repo.add.
type newRepoAddIn struct {
	Name        string `json:"name" validate:"max=64"`
	Description string `json:"description"`
	Private     bool   `json:"private"`
	Create      bool   `json:"create"`
	Clone       string `json:"clone"`
	Readme      bool   `json:"readme"`
	License     string `json:"license"`
	Ignore      bool   `json:"ignore"`
	NoMirror    bool   `json:"noMirror"`
	Git         bool   `json:"git"`
	ZitOnly     bool   `json:"zitOnly"`
	MirrorEvery int    `json:"mirrorEvery"`
}

type newRepoName struct {
	Name string `json:"name"`
}

// newRepoRig is the flow behind a signed-in session, with repo.add and
// repo.import answered by doubles: added records what was asked for,
// and state is what repo.import reports.
func newRepoRig(t *testing.T) (h http.Handler, added *[]newRepoAddIn, state *string) {
	t.Helper()
	r := ops.NewRegistry()
	var got []newRepoAddIn
	st := "copying"
	if err := ops.Register(r, ops.Op[newRepoAddIn, newRepoName]{
		Name: "repo.add", Kind: ops.Command, Auth: ops.Token, Route: "POST /api/v1/repos", Doc: "test double",
		Run: func(_ context.Context, in newRepoAddIn) (newRepoName, error) {
			got = append(got, in)
			return newRepoName{in.Name}, nil
		},
	}); err != nil {
		t.Fatal(err)
	}
	if err := ops.Register(r, ops.Op[newRepoName, newRepoImport]{
		Name: "repo.import", Kind: ops.Query, Auth: ops.Token, Route: "GET /api/v1/repos/import", Doc: "test double",
		Run: func(_ context.Context, in newRepoName) (newRepoImport, error) {
			return newRepoImport{Name: in.Name, State: st, Bytes: 2048}, nil
		},
	}); err != nil {
		t.Fatal(err)
	}
	// "taken" is the one name somebody already holds.
	type nameOut struct {
		Name string `json:"name"`
		Free bool   `json:"free"`
		Why  string `json:"why,omitempty"`
	}
	if err := ops.Register(r, ops.Op[newRepoName, nameOut]{
		Name: "repo.name", Kind: ops.Query, Auth: ops.Token, Route: "GET /api/v1/repos/name", Doc: "test double",
		Run: func(_ context.Context, in newRepoName) (nameOut, error) {
			if in.Name == "taken" {
				return nameOut{Name: in.Name, Why: `repository "taken" already exists`}, nil
			}
			return nameOut{Name: in.Name, Free: true}, nil
		},
	}); err != nil {
		t.Fatal(err)
	}
	// Every page's chrome reads the version.
	if err := ops.Register(r, ops.Op[struct{}, struct {
		Version string `json:"version"`
	}]{
		Name: "meta.version", Doc: "test double", Kind: ops.Query, Auth: ops.Anonymous, Route: "GET /api/v1/version",
		Run: func(context.Context, struct{}) (struct {
			Version string `json:"version"`
		}, error) {
			return struct {
				Version string `json:"version"`
			}{Version: "t"}, nil
		},
	}); err != nil {
		t.Fatal(err)
	}
	h = HandlerWith(r, nil, nil, []Option{WithSessions(fakeSessions{user: "jan", roles: map[string]ops.Auth{"": ops.Token}})})
	return h, &got, &st
}

func newRepoDo(h http.Handler, method, path string, form url.Values) *httptest.ResponseRecorder {
	var body *strings.Reader
	if form != nil {
		body = strings.NewReader(form.Encode())
	} else {
		body = strings.NewReader("")
	}
	req := httptest.NewRequest(method, path, body)
	if form != nil {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)
	return w
}

// The start page offers the four paths, and each opens its own setup.
func TestNewRepoStartsWithFourPaths(t *testing.T) {
	h, _, _ := newRepoRig(t)
	w := newRepoDo(h, "GET", "/new", nil)
	for _, kind := range []string{"empty", "import", "github", "fossil"} {
		if !strings.Contains(w.Body.String(), `href="/new/`+kind+`"`) {
			t.Errorf("FAIL: the start page does not offer %s", kind)
		}
		page := newRepoDo(h, "GET", "/new/"+kind, nil)
		if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), `data-hook="new-setup" data-kind="`+kind+`"`) {
			t.Errorf("FAIL: /new/%s: %d", kind, page.Code)
		}
	}
	if w := newRepoDo(h, "GET", "/new/elsewhere", nil); w.Code != http.StatusNotFound {
		t.Errorf("FAIL: an unknown path answered %d", w.Code)
	}
}

// Each path sends repo.add what it means, and a copy lands on its
// progress page while an empty repository opens straight away.
func TestNewRepoPathsCreateWhatTheySay(t *testing.T) {
	for _, tc := range []struct {
		kind, from, source, format, to string
		want                           newRepoAddIn
	}{
		{"empty", "", "", "", "/r/fresh", newRepoAddIn{Name: "fresh", Create: true, Readme: true}},
		{"import", "https://fossil-scm.org/home", "fossil", "", "/new/progress/fresh", newRepoAddIn{Name: "fresh", Clone: "https://fossil-scm.org/home", NoMirror: true}},
		{"import", "https://github.com/a/b", "git", "zit", "/new/progress/fresh", newRepoAddIn{Name: "fresh", Clone: "https://github.com/a/b", NoMirror: true, Git: true, ZitOnly: true}},
		{"github", "https://github.com/a/b", "", "flat", "/new/progress/fresh", newRepoAddIn{Name: "fresh", Clone: "https://github.com/a/b", Git: true, MirrorEvery: 21600}},
		{"fossil", "https://fossil-scm.org/home", "", "zit", "/new/progress/fresh", newRepoAddIn{Name: "fresh", Clone: "https://fossil-scm.org/home", MirrorEvery: 21600}},
	} {
		h, added, _ := newRepoRig(t)
		form := url.Values{"kind": {tc.kind}, "name": {"fresh"}, "from": {tc.from}, "source": {tc.source},
			"format": {tc.format}, "every": {"21600"}, "readme": {"true"}, "ack": {"zit"}}
		w := newRepoDo(h, "POST", "/new", form)
		if w.Code != http.StatusSeeOther || w.Header().Get("Location") != tc.to {
			t.Errorf("FAIL: %s/%s: %d to %q, want %s", tc.kind, tc.format, w.Code, w.Header().Get("Location"), tc.to)
			continue
		}
		if len(*added) != 1 || (*added)[0] != tc.want {
			t.Errorf("FAIL: %s/%s asked repo.add for %+v, want %+v", tc.kind, tc.format, *added, tc.want)
		}
	}
}

// A zit-only repository is not made until the review's acknowledgement
// says Fossil cannot read it; the review asks for it.
func TestAZitOnlyRepositoryNeedsItsAcknowledgement(t *testing.T) {
	h, added, _ := newRepoRig(t)
	form := url.Values{"kind": {"github"}, "from": {"https://github.com/NetBSD/src"}, "format": {"zit"}}
	review := newRepoDo(h, "POST", "/new/review", form)
	if review.Code != http.StatusOK || !strings.Contains(review.Body.String(), `name="ack" value="zit" required`) {
		t.Fatalf("FAIL: the review of a zit-only mirror asks nothing: %d", review.Code)
	}
	// The name came from the address, and rides to the create.
	if !strings.Contains(review.Body.String(), `name="name" value="src"`) {
		t.Errorf("FAIL: the review did not name the mirror after its upstream")
	}
	form.Set("name", "src")
	if w := newRepoDo(h, "POST", "/new", form); w.Code != http.StatusBadRequest || len(*added) != 0 {
		t.Errorf("FAIL: created without the acknowledgement: %d, %v", w.Code, *added)
	}
	form.Set("ack", "zit")
	if w := newRepoDo(h, "POST", "/new", form); w.Code != http.StatusSeeOther || len(*added) != 1 || !(*added)[0].ZitOnly {
		t.Errorf("FAIL: with the acknowledgement: %d, %v", w.Code, *added)
	}
}

// Back from the review keeps every answer.
func TestBackFromTheReviewKeepsTheAnswers(t *testing.T) {
	h, _, _ := newRepoRig(t)
	w := newRepoDo(h, "POST", "/new/github", url.Values{"name": {"src"}, "from": {"https://github.com/NetBSD/src"},
		"format": {"zit"}, "every": {"86400"}, "private": {"true"}})
	body := w.Body.String()
	for _, want := range []string{`name="from" type="url" value="https://github.com/NetBSD/src"`, `value="src"`,
		`value="zit" checked`, `value="86400" selected`, `id="private" name="private" value="true" checked`} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: back from the review lost %s", want)
		}
	}
}

// The progress page streams while the copy runs, and stops asking once
// it has finished; a ready copy opens.
func TestTheProgressPageFollowsTheCopy(t *testing.T) {
	h, _, state := newRepoRig(t)
	w := newRepoDo(h, "GET", "/new/progress/fresh", nil)
	if !strings.Contains(w.Body.String(), `data-state="copying" data-init="@get('/new/progress/fresh/live')"`) {
		t.Errorf("FAIL: a copy in progress does not stream")
	}
	*state = "ready"
	w = newRepoDo(h, "GET", "/new/progress/fresh", nil)
	body := w.Body.String()
	if strings.Contains(body, "/live") || !strings.Contains(body, `href="/r/fresh" data-hook="new-open"`) {
		t.Errorf("FAIL: a finished copy still streams, or does not open")
	}
}

// The name is checked as it is typed, and again at the review, so a
// taken name is found before the create rather than by it.
func TestATakenNameIsSaidBeforeTheCreate(t *testing.T) {
	h, added, _ := newRepoRig(t)
	if w := newRepoDo(h, "GET", "/new/name?name=taken", nil); !strings.Contains(w.Body.String(), `data-hook="name-taken"`) {
		t.Errorf("FAIL: a taken name was not said as it was typed:\n%s", w.Body.String())
	}
	if w := newRepoDo(h, "GET", "/new/name?name=fresh", nil); !strings.Contains(w.Body.String(), `data-hook="name-free"`) {
		t.Errorf("FAIL: a free name was not said to be free:\n%s", w.Body.String())
	}
	w := newRepoDo(h, "POST", "/new/review", url.Values{"kind": {"empty"}, "name": {"taken"}})
	if w.Code != http.StatusConflict || strings.Contains(w.Body.String(), `action="/new"`) {
		t.Errorf("FAIL: the review went ahead with a taken name (%d)", w.Code)
	}
	if len(*added) != 0 {
		t.Error("FAIL: a taken name reached repo.add")
	}
	// The setup page asks again as soon as it opens with a name in it.
	w = newRepoDo(h, "POST", "/new/empty", url.Values{"kind": {"empty"}, "name": {"taken"}})
	if !strings.Contains(w.Body.String(), `data-init="@get('/new/name?name=taken')"`) {
		t.Error("FAIL: the setup page does not check the name it opens with")
	}
}