package web
import (
"net/http"
"net/http/httptest"
"regexp"
"sync"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/ops"
)
// Building a handler must not write anything a handler already serving
// reads. The parallel tests build handlers while others serve, and a
// forge that ever builds a second one (a reload, a test harness) would
// otherwise hand a request half of one configuration and half of the
// other.
//
// The later handlers place orchard somewhere else, so a handler whose
// routing is overwritten by one built after it fails the final
// assertion even without the race detector, which CI does not run.
// Under -race the concurrent building is caught as well.
func TestBuildingAHandlerDoesNotRaceOneServing(t *testing.T) {
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v", Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
t.Fatal(err)
}
opts := func(ns string) []Option {
return []Option{
WithNamespaces(place(map[string]string{"orchard": ns})),
WithNamespaceHistory(func(string, string) bool { return false }),
WithMarks(func(string, string) (string, bool) { return "", false }),
WithMarksChanged(func() {}),
WithAgents(func(string) string { return "" }),
}
}
serving := HandlerWith(r, nil, nil, opts("grove"))
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(2)
go func() {
defer wg.Done()
for _, path := range []string{"/r/orchard", "/r/grove/orchard", "/"} {
serving.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", path, nil))
}
}()
go func() {
defer wg.Done()
HandlerWith(r, nil, nil, opts("elsewhere"))
}()
}
wg.Wait()
// The first handler still answers by its own configuration.
rec := httptest.NewRecorder()
serving.ServeHTTP(rec, httptest.NewRequest("GET", "/r/orchard", nil))
if rec.Code != 302 || rec.Header().Get("Location") != "/r/grove/orchard" {
t.Errorf("FAIL: a flat path answered %d %q, want 302 to /r/grove/orchard", rec.Code, rec.Header().Get("Location"))
}
}
// Each handler draws its pages with its own hooks. Two handlers that
// place the same repository under different namespaces, serving at the
// same time, must each link the repository where THEY place it; with
// one set of hooks shared by every page, the handler built last decides
// what both draw.
func TestEachHandlerDrawsWithItsOwnHooks(t *testing.T) {
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v",
Repos: api.NewRepos(map[string]*api.Repo{"orchard": {Index: activityIndex(t)}})}); err != nil {
t.Fatal(err)
}
build := func(ns string) http.Handler {
return HandlerWith(r, nil, nil, []Option{WithNamespaces(place(map[string]string{"orchard": ns}))})
}
handlers := map[string]http.Handler{"grove": build("grove"), "copse": build("copse")}
repoLink := regexp.MustCompile(`<a class="repo" href="([^"]+)"`)
var wg sync.WaitGroup
var mu sync.Mutex
got := map[string]map[string]int{}
for ns, h := range handlers {
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/r/"+ns+"/orchard/timeline", nil))
href := ""
if m := repoLink.FindStringSubmatch(rec.Body.String()); m != nil {
href = m[1]
}
mu.Lock()
defer mu.Unlock()
if got[ns] == nil {
got[ns] = map[string]int{}
}
got[ns][href]++
}()
}
}
wg.Wait()
for ns, hrefs := range got {
want := "/r/" + ns + "/orchard"
if len(hrefs) != 1 || hrefs[want] == 0 {
t.Errorf("FAIL: the handler placing orchard under %s linked it at %v, want only %s", ns, hrefs, want)
continue
}
t.Logf("ok: the handler placing orchard under %s links %s", ns, want)
}
}
// drawWith is a handler's hooks h, with its own copy of the pages.
func drawWith(h handlerHooks) *handlerHooks {
h.drawPages()
return &h
}