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",
// An address that names nothing is the fronter's to answer, as
// it answers a repository the caller may not see (#428).
"/r/nobody/tools": "sync:nobody/tools",
"/r/dmestas/nothing": "sync:dmestas/nothing",
"/r/nobody/nothing/xfer": "sync:nobody/nothing",
// A bare handle's page, posted to, is still a page.
"/r/tools/stamp": "web:/r/tools/stamp",
} {
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)
}
}
// A repository made in a team goes into the team before it takes its
// name, so ada's own tools does not stand in the way of crew/tools; and
// one that would show beside another (a private team's, under ada) is
// refused, leaving nothing behind (RFC 0025).
func TestARepositoryMadeInATeamIsNamedThere(t *testing.T) {
rg := newTestRegistrar(t, 25)
db := rg.odb
if err := db.AddAccount("ada", "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
for _, team := range []string{"crew", "hush"} {
if err := db.AddTeam(team, team, "", "ada"); err != nil {
t.Fatal(err)
}
}
if _, err := db.SetTeamPrivate("hush", true); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("tools", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := rg.claim(api.RepoSpec{Name: "tools", Author: "ada"}); err != nil {
t.Fatal(err)
}
make := func(ns string) error {
key, err := rg.keyFor("tools")
if err != nil {
t.Fatal(err)
}
if err := db.AddRepo(key, t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := rg.settle(api.RepoSpec{Name: key, Namespace: ns, Author: "ada"}, "tools"); err != nil {
if _, found, _ := db.RepoByName(key); found {
t.Errorf("FAIL: a refused create left %s behind", key)
}
return err
}
if got, _ := db.NamespaceOf(key); got != ns {
t.Errorf("FAIL: %s/tools was placed under %q", ns, got)
}
return nil
}
if err := make("crew"); err != nil {
t.Errorf("FAIL: crew/tools was refused beside ada/tools: %v", err)
}
if err := make("hush"); err == nil {
t.Error("FAIL: hush/tools was made, which shows under ada beside ada's own tools")
}
// Moving one into the private team later is refused the same way.
other, _ := rg.keyFor("tools")
if err := db.AddRepo(other, t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner(other, "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetHandle(other, "tools"); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hush", other, "ada"); !oltp.IsRefusal(err) {
t.Errorf("FAIL: a tools moved into a private team of ada's beside ada/tools: %v", err)
}
}
// A node that names repositories flat (--repo-names=flat, RFC 0025): a
// name is unique across the node whatever namespace a repository sits
// in, no team is offered to make one in, and a repository's address is
// its name alone.
func TestAFlatNodeNamesRepositoriesAcrossTheNode(t *testing.T) {
rg := newTestRegistrar(t, 25)
rg.flatNames = true
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.AddRepo("tools", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := rg.claim(api.RepoSpec{Name: "tools", Author: "ada"}); err != nil {
t.Fatal(err)
}
if err := rg.NameFree("", "tools", "bob"); err == nil {
t.Error("FAIL: bob may name a second tools on a flat node")
}
if err := rg.NameFree("crew", "hammer", "ada"); err == nil {
t.Error("FAIL: a flat node offered a namespace")
}
if err := rg.NameFree("", "hammer", "bob"); err != nil {
t.Errorf("FAIL: a free name was refused: %v", err)
}
a := flatAddressesOf(db)
if ns, handle, ok := a.Of("tools"); !ok || ns != "" || handle != "tools" {
t.Errorf("FAIL: tools is at %q/%q (%v), want its name alone", ns, handle, ok)
}
if _, ok := a.At("ada", "tools"); ok {
t.Error("FAIL: a namespaced path names a repository on a flat node")
}
}
// The address book a listing reads in one query says the same as the
// per-key readers it replaces, for a person's repository, a public
// team's and a private team's.
func TestTheAddressBookAgreesWithTheKeyAtATimeReaders(t *testing.T) {
db := newTestRegistrar(t, 25).odb
for _, name := range []string{"mine", "shared", "quiet"} {
if err := db.AddRepo(name, t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner(name, "alice"); err != nil {
t.Fatal(err)
}
}
for _, team := range []string{"open", "hush"} {
if err := db.AddTeam(team, team, "", "alice"); err != nil {
t.Fatal(err)
}
}
if err := db.SetTeamRepo("open", "shared", "alice"); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hush", "quiet", "alice"); err != nil {
t.Fatal(err)
}
if _, err := db.SetTeamPrivate("hush", true); err != nil {
t.Fatal(err)
}
book, err := db.AddressBook()
if err != nil {
t.Fatal(err)
}
of := addressesOf(db).Of
for _, key := range []string{"mine", "shared", "quiet"} {
ns, handle, ok := of(key)
got, in := book[key]
if !ok || !in || got[0] != ns || got[1] != handle {
t.Errorf("FAIL: %s: book %v (%v), per-key %s/%s (%v)", key, got, in, ns, handle, ok)
}
}
if got := book["quiet"]; got[0] != "alice" {
t.Errorf("FAIL: a private team's repository appears under %q, want its owner", got[0])
}
t.Log("ok")
}