Skip to content
quarry Artifact [5c1fb7a72f]
Login

Artifact [5c1fb7a72f]

Artifact 5c1fb7a72f1ad1154882346616c21aee4561daa92c947d3e07c400e5a83c52e9:


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
}