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)
}
// 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")
}
}