Skip to content
quarry Artifact [4a3554c338]
Login

Artifact [4a3554c338]

Artifact 4a3554c338309b771e33db982694baf6abb9bce5ac865eef598005e3d099a429:


// repodelete_test.go proves the forge/repository line for deletion
// (docs/hosting.md): a repository belongs to its own admin, deletion
// included, not only to the forge's site admin.
package harness

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

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

// fakeRepoRegistry is the minimal RepoRegistry repo.delete/repo.remove
// need: it records what it was asked to destroy rather than touching a
// real store.
type fakeRepoRegistry struct{ deleted, removed []string }

func (f *fakeRepoRegistry) Add(spec api.RepoSpec) (string, error) { return "", nil }
func (f *fakeRepoRegistry) Remove(name string) error {
	f.removed = append(f.removed, name)
	return nil
}
func (f *fakeRepoRegistry) Delete(name, confirm string) error {
	if confirm != name {
		return fmt.Errorf("confirm must equal name: %w", ops.ErrInvalid)
	}
	f.deleted = append(f.deleted, name)
	return nil
}
func (f *fakeRepoRegistry) Update(name, description, visibility, homepage, topics string, mirrorable bool) error {
	return nil
}
func (f *fakeRepoRegistry) SetChat(name string, on bool) error { return nil }
func (f *fakeRepoRegistry) Imports() ([]api.RepoImport, error) { return nil, nil }

const deletePW = "pw12345678"

// An account holding `a` on the repository it created may delete it
// without being a site admin, and an account with no role there may
// not — the same verify/MayAt path production wires (cmd/quarry).
func TestRepoDeleteIsTheRepositoryOwnersCallNotOnlySiteAdmin(t *testing.T) {
	odb, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	for _, name := range []string{"owner", "stranger"} {
		if err := odb.AddAccount(name, deletePW, false); err != nil {
			t.Fatal(err)
		}
	}
	if err := odb.Grant("owner", "alpha", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}

	reg := &fakeRepoRegistry{}
	deps := api.Deps{Version: "0.0.0-harness", Repos: api.NewRepos(map[string]*api.Repo{"alpha": {}}),
		Registry: reg}
	deps.CapsFor = func(account, repo string) api.CapSet { return odb.EffectiveCaps(account, repo) }
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	// The SAME verify/MayAt path cmd/quarry wires: authenticate, then
	// let the op's own Auth floor and Caps letter decide the rest.
	verify := func(repo, user, pass string, need ops.Auth) api.Decision {
		acct, ok := odb.Authenticate(user, pass)
		if !ok {
			return api.Denied
		}
		if odb.MayAt(acct, repo, need == ops.Admin) {
			return api.Allowed
		}
		return api.Insufficient
	}
	mux, err := api.RouterWithCaps(r, "0.0.0-harness", deps.CapsFor, verify)
	if err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(mux)
	defer srv.Close()

	call := func(user, path, body string) int {
		req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(body))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.SetBasicAuth(user, deletePW)
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		resp.Body.Close()
		return resp.StatusCode
	}

	// No role on alpha at all: refused, not merely "not site admin".
	if got := call("stranger", "/api/v1/repos/delete", "name=alpha&confirm=alpha"); got == http.StatusOK {
		t.Fatalf("an account with no role on the repository was allowed to delete it: %d", got)
	}
	if len(reg.deleted) != 0 {
		t.Fatalf("the repository was deleted despite the refusal: %+v", reg.deleted)
	}

	// The repository's own admin — not a site admin — succeeds.
	if got := call("owner", "/api/v1/repos/delete", "name=alpha&confirm=alpha"); got != http.StatusOK {
		t.Fatalf("the repository's own admin was refused: %d", got)
	}
	if len(reg.deleted) != 1 || reg.deleted[0] != "alpha" {
		t.Fatalf("delete was not carried out for the owner: %+v", reg.deleted)
	}
}

// repo.remove (unregister without destroying the store) follows the
// same rule.
func TestRepoRemoveIsTheRepositoryOwnersCallNotOnlySiteAdmin(t *testing.T) {
	odb, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	for _, name := range []string{"owner", "stranger"} {
		if err := odb.AddAccount(name, deletePW, false); err != nil {
			t.Fatal(err)
		}
	}
	if err := odb.Grant("owner", "alpha", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}

	reg := &fakeRepoRegistry{}
	deps := api.Deps{Version: "0.0.0-harness", Repos: api.NewRepos(map[string]*api.Repo{"alpha": {}}),
		Registry: reg}
	deps.CapsFor = func(account, repo string) api.CapSet { return odb.EffectiveCaps(account, repo) }
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	verify := func(repo, user, pass string, need ops.Auth) api.Decision {
		acct, ok := odb.Authenticate(user, pass)
		if !ok {
			return api.Denied
		}
		if odb.MayAt(acct, repo, need == ops.Admin) {
			return api.Allowed
		}
		return api.Insufficient
	}
	mux, err := api.RouterWithCaps(r, "0.0.0-harness", deps.CapsFor, verify)
	if err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(mux)
	defer srv.Close()

	call := func(user, body string) int {
		req, _ := http.NewRequest("POST", srv.URL+"/api/v1/repos/remove", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.SetBasicAuth(user, deletePW)
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		resp.Body.Close()
		return resp.StatusCode
	}
	if got := call("stranger", "name=alpha"); got == http.StatusOK {
		t.Fatalf("an account with no role was allowed to remove the repository: %d", got)
	}
	if got := call("owner", "name=alpha"); got != http.StatusOK {
		t.Fatalf("the repository's own admin was refused remove: %d", got)
	}
	if len(reg.removed) != 1 || reg.removed[0] != "alpha" {
		t.Fatalf("remove was not carried out for the owner: %+v", reg.removed)
	}
}