// 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 string, edit api.RepoEdit, by string) 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)
}
}