package web
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// twoTools is a forge after RFC 0025: two repositories called tools, one
// under dan and one under a team, with keys that are not their handles,
// beside quarry, whose key is its handle as every older repository's is.
func twoTools() *Addresses {
where := map[string][2]string{
"r1": {"dan", "tools"},
"r2": {"crew", "tools"},
"quarry": {"dmestas", "quarry"},
}
return &Addresses{
Of: func(key string) (string, string, bool) {
a, ok := where[key]
return a[0], a[1], ok
},
At: func(ns, handle string) (string, bool) {
for key, a := range where {
if a[0] == ns && a[1] == handle {
return key, true
}
}
return "", false
},
Named: func(handle string) []string {
var out []string
for _, key := range []string{"quarry", "r1", "r2"} {
if where[key][1] == handle {
out = append(out, key)
}
}
return out
},
}
}
func serveAddr(addr *Addresses, see func(string, *http.Request) bool, method, path string) (code int, served, location, body string) {
inner := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { served = req.URL.Path })
w := httptest.NewRecorder()
namespaced(addr, nil, see, inner).ServeHTTP(w, httptest.NewRequest(method, path, nil))
return w.Code, served, w.Header().Get("Location"), w.Body.String()
}
// An address reaches its repository's key, on pages, sync and the API;
// a key is sent to its address; and a handle two repositories share is
// never guessed.
func TestAnAddressReachesItsRepository(t *testing.T) {
a := twoTools()
for _, c := range []struct {
method, path string
served, moved string
code int
bodyHas string
}{
{"GET", "/r/dan/tools/timeline", "/r/r1/timeline", "", 200, ""},
{"POST", "/r/crew/tools/xfer", "/r/r2/xfer", "", 200, ""},
{"GET", "/api/v1/r/dan/tools/ticket", "/api/v1/r/r1/ticket", "", 200, ""},
{"GET", "/r/r1/wiki", "", "/r/dan/tools/wiki", 302, ""},
{"GET", "/r/quarry/timeline", "", "/r/dmestas/quarry/timeline", 302, ""},
{"GET", "/r/tools", "/named/tools", "", 200, ""},
{"POST", "/r/tools/xfer", "", "", 409, "/r/dan/tools"},
// The API has no reader to filter by, so a shared handle answers
// as a name nobody has rather than listing private repositories.
{"GET", "/api/v1/r/tools/ticket", "/api/v1/r/tools/ticket", "", 200, ""},
{"GET", "/r/nobody/tools", "/r/nobody/tools", "", 200, ""},
} {
code, served, loc, body := serveAddr(a, allVisible, c.method, c.path)
if code != c.code || served != c.served || loc != c.moved || !strings.Contains(body, c.bodyHas) {
t.Errorf("FAIL: %s %s = %d served %q moved %q body %q; want %d %q %q",
c.method, c.path, code, served, loc, strings.TrimSpace(body), c.code, c.served, c.moved)
}
}
if got := a.url("r2"); got != "/r/crew/tools" {
t.Errorf("FAIL: r2's link is %q", got)
}
if got := repoOfPath("/r/dan/tools/forum", a); got != "r1" {
t.Errorf("FAIL: the path's repository is %q, want r1", got)
}
}
// A handle the reader can see on only one repository is that one: the
// other is private to them, so it is not there to be confused with.
func TestAHandleOnlyOneVisibleRepositoryHasIsThatOne(t *testing.T) {
onlyR1 := func(key string, _ *http.Request) bool { return key != "r2" }
if code, _, loc, _ := serveAddr(twoTools(), onlyR1, "GET", "/r/tools/timeline"); code != 302 || loc != "/r/dan/tools/timeline" {
t.Errorf("FAIL: GET /r/tools = %d to %q, want the one the reader can see", code, loc)
}
if _, served, _, _ := serveAddr(twoTools(), onlyR1, "POST", "/r/tools/xfer"); served != "/r/r1/xfer" {
t.Errorf("FAIL: sync to /r/tools served %q, want r1", served)
}
if _, served, loc, _ := serveAddr(twoTools(), onlyR1, "GET", "/r/crew/tools"); served != "/r/crew/tools" || loc != "" {
t.Errorf("FAIL: a repository the reader cannot see was reached at its address (%q, %q)", served, loc)
}
}
// A bare handle two repositories share is a page listing both, by
// address, through the whole handler.
func TestABareSharedHandleListsItsRepositories(t *testing.T) {
r := ops.NewRegistry()
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"`
}{"t"}, nil
},
}); err != nil {
t.Fatal(err)
}
h := HandlerWith(r, nil, nil, []Option{WithAddresses(*twoTools())})
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest("GET", "/r/tools", nil))
body := w.Body.String()
if w.Code != 200 || !strings.Contains(body, `data-hook="named"`) ||
!strings.Contains(body, `href="/r/dan/tools"`) || !strings.Contains(body, `href="/r/crew/tools"`) {
t.Errorf("FAIL: /r/tools = %d, without both repositories listed:\n%s", w.Code, body)
}
}
// A repository under no namespace whose handle is not its key is at
// /r/<handle>: its key is sent there, and there it is served, never sent
// to itself again.
func TestAnAddressThatIsAlreadyHereIsServed(t *testing.T) {
a := &Addresses{
Of: func(key string) (string, string, bool) { return "", "h", key == "k" },
At: func(string, string) (string, bool) { return "", false },
Named: func(handle string) []string {
if handle == "h" {
return []string{"k"}
}
return nil
},
}
if code, _, loc, _ := serveAddr(a, allVisible, "GET", "/r/k/timeline"); code != 302 || loc != "/r/h/timeline" {
t.Errorf("FAIL: the key went %d to %q, want its address", code, loc)
}
if code, served, loc, _ := serveAddr(a, allVisible, "GET", "/r/h/timeline"); code != 200 || served != "/r/k/timeline" || loc != "" {
t.Errorf("FAIL: its address answered %d served %q moved %q, want it served", code, served, loc)
}
}
// A page says a repository's name, not its key (RFC 0025): r1 is dan's
// tools, and its bar and links say so. A key is minted only when the
// name could not be one, so this is the case the reader would otherwise
// meet as rp-letters.
func TestAPageSaysTheRepositorysName(t *testing.T) {
h := &handlerHooks{addr: twoTools()}
h.drawPages()
var buf strings.Builder
if err := h.template(aboutPage).Execute(&buf, pageData{Repo: "r1", Title: "about"}); err != nil {
t.Fatal(err)
}
page := buf.String()
if !strings.Contains(page, `href="/r/dan/tools"`) || !strings.Contains(page, `</span>tools</a>`) {
t.Errorf("FAIL: the bar does not name dan/tools")
}
if strings.Contains(page, `>r1<`) || strings.Contains(page, `</span>r1</a>`) {
t.Errorf("FAIL: the page says the key r1 where a reader reads the name")
}
if got := h.repoName("quarry"); got != "quarry" {
t.Errorf("FAIL: a repository whose key is its name is called %q", got)
}
}
// A chat channel is named as its repository is: the rail says the name
// and links to the address, never the minted key (RFC 0025).
func TestAChatChannelIsNamedAsItsRepository(t *testing.T) {
h := &handlerHooks{addr: twoTools()}
got := string(h.chatChannelsHTML([]chatChannel{{Name: "r1"}, {Name: "quarry"}}, "r1"))
if !strings.Contains(got, `href="/r/dan/tools/chat"`) || !strings.Contains(got, `</span>tools</a>`) {
t.Errorf("FAIL: the rail does not name dan's tools: %s", got)
}
if strings.Contains(got, ">r1<") || strings.Contains(got, "/r/r1/") {
t.Errorf("FAIL: the rail shows the key r1: %s", got)
}
}