Skip to content
quarry Artifact [89ec84b44e]
Login

Artifact [89ec84b44e]

Artifact 89ec84b44ee7cde0b0e7403434c45cba9d91fa4adc6a6e6bd080fef5ab598516:


package main

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/web"
)

// An address is read as readers see it (RFC 0025): a private team's
// repository is at its owner's namespace while the team is private, and
// at the team's when it is public, never at both.
func TestAnAddressIsWhereReadersSeeTheRepository(t *testing.T) {
	db := newTestRegistrar(t, 25).odb
	if err := db.AddRepo("quiet", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if err := db.SetOwner("quiet", "alice"); err != nil {
		t.Fatal(err)
	}
	if err := db.AddTeam("hush", "Hush", "", "alice"); err != nil {
		t.Fatal(err)
	}
	if err := db.SetTeamRepo("hush", "quiet", "alice"); err != nil {
		t.Fatal(err)
	}
	a := addressesOf(db)
	if ns, handle, ok := a.Of("quiet"); !ok || ns != "hush" || handle != "quiet" {
		t.Errorf("FAIL: quiet is at %s/%s (%v), want hush/quiet", ns, handle, ok)
	}
	if key, ok := a.At("hush", "quiet"); !ok || key != "quiet" {
		t.Errorf("FAIL: hush/quiet is %q (%v)", key, ok)
	}
	if _, err := db.SetTeamPrivate("hush", true); err != nil {
		t.Fatal(err)
	}
	if key, ok := a.At("alice", "quiet"); !ok || key != "quiet" {
		t.Errorf("FAIL: with the team private, alice/quiet is %q (%v)", key, ok)
	}
	if _, ok := a.At("hush", "quiet"); ok {
		t.Error("FAIL: a private team's name reaches its repository")
	}
	if got := a.Named("quiet"); len(got) != 1 || got[0] != "quiet" {
		t.Errorf("FAIL: the handle quiet names %v", got)
	}
	if _, _, ok := a.Of("nothere"); ok {
		t.Error("FAIL: a name no repository has has an address")
	}
}

// The API reads an address through the server as it is wired, not just
// through the middleware: /api/v1/r/<namespace>/<handle> answers as the
// flat path does, a private repository's included (RFC 0025).
func TestTheAPIReadsAnAddressThroughTheServer(t *testing.T) {
	g := newSiteRig(t)
	for _, name := range []string{"granted", "secret"} {
		if err := g.db.SetNamespace(name, "alice", "alice", time.Now()); err != nil {
			t.Fatal(err)
		}
	}
	site, _ := g.mint(t, oltp.ScopeAPI, oltp.ScopeWeb, oltp.ScopeSite)
	for _, name := range []string{"granted", "secret"} {
		flat, _ := g.do(http.MethodGet, "/api/v1/r/"+name+"/info", site, "", nil)
		at, body := g.do(http.MethodGet, "/api/v1/r/alice/"+name+"/info", site, "", nil)
		if at != flat {
			t.Errorf("FAIL: %s at its address answered %d, its flat path %d: %s", name, at, flat, body)
		}
	}
	if code, _ := g.do(http.MethodGet, "/api/v1/r/alice/granted/info", site, "", nil); code != http.StatusOK {
		t.Errorf("FAIL: granted at alice/granted: %d", code)
	}
}

// Sync reaches a repository at its address as it does at its key, and a
// post to a repository's own page still goes to the web (RFC 0025).
func TestSyncReachesARepositoryAtItsAddress(t *testing.T) {
	addrs := &web.Addresses{
		Of: func(key string) (string, string, bool) {
			switch key {
			case "quarry":
				return "dmestas", "quarry", true
			case "r1":
				return "dan", "tools", true
			}
			return "", "", false
		},
		At: func(ns, handle string) (string, bool) {
			switch ns + "/" + handle {
			case "dmestas/quarry":
				return "quarry", true
			case "dan/tools":
				return "r1", true
			}
			return "", false
		},
		Named: func(handle string) []string {
			if handle == "tools" {
				return []string{"r1"}
			}
			return nil
		},
	}
	var got string
	sync := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { got = "sync:" + req.PathValue("repo") })
	page := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { got = "web:" + req.URL.Path })
	synced := syncAddressed(addrs, sync, page)
	mount := http.NewServeMux()
	mount.Handle("POST /r/{repo}", synced)
	mount.Handle("POST /r/{repo}/xfer", synced)
	mount.Handle("POST /r/{ns}/{handle}", synced)
	mount.Handle("POST /r/{ns}/{handle}/xfer", synced)
	mount.Handle("/", page)
	for path, want := range map[string]string{
		"/r/quarry":              "sync:quarry",
		"/r/quarry/xfer":         "sync:quarry",
		"/r/dmestas/quarry":      "sync:quarry",
		"/r/dmestas/quarry/xfer": "sync:quarry",
		"/r/dan/tools":           "sync:r1",
		"/r/tools":               "sync:r1",
		"/r/quarry/stamp":        "web:/r/quarry/stamp",
		"/r/nobody/tools":        "web:/r/nobody/tools",
	} {
		got = ""
		mount.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", path, nil))
		if got != want {
			t.Errorf("FAIL: POST %s went to %q, want %q", path, got, want)
		}
	}
}

// A name is checked where the repository would appear (RFC 0025): ada's
// tools leaves bob free to make his own, but not ada a second, nor a
// private team of ada's, whose repositories appear under ada; and the
// second tools gets a minted key, since the first holds "tools".
func TestANameIsCheckedWhereTheRepositoryWouldAppear(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	db := rg.odb
	for _, who := range []string{"ada", "bob"} {
		if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.AddTeam("crew", "Crew", "", "ada"); err != nil {
		t.Fatal(err)
	}
	if err := db.AddRepo("tools", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	rg.claim(api.RepoSpec{Name: "tools", Author: "ada"})
	for _, c := range []struct {
		ns, who string
		free    bool
	}{
		{"", "bob", true},
		{"", "ada", false},
		{"crew", "ada", true},
	} {
		err := rg.NameFree(c.ns, "tools", c.who)
		if (err == nil) != c.free {
			t.Errorf("FAIL: %s naming tools in %q: %v, want free=%v", c.who, c.ns, err, c.free)
		}
	}
	if _, err := db.SetTeamPrivate("crew", true); err != nil {
		t.Fatal(err)
	}
	if err := rg.NameFree("crew", "tools", "ada"); err == nil {
		t.Error("FAIL: a private team of ada's took tools, which would appear beside ada's")
	}
	if key, err := rg.keyFor("tools"); err != nil || !strings.HasPrefix(key, oltp.RepoKeyPrefix) {
		t.Errorf("FAIL: the second tools gets key %q (%v), want a minted one", key, err)
	}
}