Skip to content
quarry Artifact [a374b1320c]
Login

Artifact [a374b1320c]

Artifact a374b1320c2aac9e81f25c962039a7bdbe666a1843274f5171324659db948c93:


package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"

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

// repoAgentForge is alpha's settings page with jva signed in as its
// admin, over a real store.
func repoAgentForge(t *testing.T) (http.Handler, *oltp.DB) {
	return repoAgentForgeHeld(t, "")
}

// repoAgentForgeHeld is repoAgentForge with alpha under a hold saying
// held, which refuses the grant door and not the making of an agent.
func repoAgentForgeHeld(t *testing.T, held string) (http.Handler, *oltp.DB) {
	t.Helper()
	dir := t.TempDir()
	ix, err := index.Open(filepath.Join(dir, "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	db, err := oltp.Open(filepath.Join(dir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	for _, who := range []string{"root", "jva"} {
		if err := db.AddAccount(who, "pw-"+who+"-12345", who == "root"); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.AddRepo("alpha", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if _, err := db.GrantGuarded("root", "jva", "alpha", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	prefs := newMemPrefs()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "t",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix, Code: "c1"}}),
		Prefs: prefs, SyncTokens: db, Accounts: agentAccts{db}, AgentsPerPerson: 3,
		Held:    func(context.Context, string) string { return held },
		CapsFor: func(a, repo string) api.CapSet { return db.EffectiveCaps(a, repo) },
	}); err != nil {
		t.Fatal(err)
	}
	h := HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "jva", roles: map[string]ops.Auth{"alpha": ops.Admin}}), WithPrefs(prefs)},
		Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied }))
	return h, db
}

func repoAgentDo(t *testing.T, h http.Handler, method, path string, form url.Values) *httptest.ResponseRecorder {
	t.Helper()
	var req *http.Request
	if form != nil {
		req = httptest.NewRequest(method, path, strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	return rec
}

// One form makes a repository's agent and gives it a role here: the
// form offers the role, the agent ends holding it, and the row says
// what it holds, or that it holds nothing with a button to change that.
func TestARepositoryAgentIsMadeWithItsRoleFromOneForm(t *testing.T) {
	h, db := repoAgentForge(t)

	page := repoAgentDo(t, h, "GET", "/r/alpha/settings", nil)
	if page.Code != 200 {
		t.Fatalf("FAIL: the settings page answered %d", page.Code)
	}
	body := page.Body.String()
	for _, hook := range []string{"repo-agents", "repo-agent-new", "repo-agent-role"} {
		if !hasHook(body, hook) {
			t.Errorf("FAIL: the settings page has no %s", hook)
		}
	}
	if !strings.Contains(body, `<option value="developer" selected>`) {
		t.Error("FAIL: developer is not preselected")
	}
	if hasHook(body, "repo-agent") {
		t.Error("FAIL: a row is drawn before any agent exists")
	}

	rec := repoAgentDo(t, h, "POST", "/r/alpha/settings/agents", url.Values{"name": {"suit-bot"}, "role": {"maintainer"}})
	if rec.Code != http.StatusSeeOther || !strings.HasPrefix(rec.Header().Get("Location"), "/me/agents/suit-bot") {
		t.Fatalf("FAIL: making the agent answered %d to %q", rec.Code, rec.Header().Get("Location"))
	}
	if db.Operator("suit-bot") != "repo:alpha" {
		t.Errorf("FAIL: the operator is %q", db.Operator("suit-bot"))
	}
	if got := db.EffectiveCaps("suit-bot", "alpha"); !got.HasAny("i") || got.HasAny("a") {
		t.Errorf("FAIL: the agent holds %q after a maintainer grant", got)
	}
	body = repoAgentDo(t, h, "GET", "/r/alpha/settings", nil).Body.String()
	if !strings.Contains(body, `data-hook="repo-agent-caps" data-state="held"`) || strings.Contains(body, `data-state="none"`) {
		t.Error("FAIL: the row does not show what the agent holds")
	}

	// A role nobody offers is refused before anything is made.
	rec = repoAgentDo(t, h, "POST", "/r/alpha/settings/agents", url.Values{"name": {"other-bot"}, "role": {"admin"}})
	if rec.Code != http.StatusSeeOther || !strings.Contains(rec.Header().Get("Location"), "e=") || db.Operator("other-bot") != "" {
		t.Errorf("FAIL: an unoffered role: %d %q, operator %q", rec.Code, rec.Header().Get("Location"), db.Operator("other-bot"))
	}
	t.Log("ok")
}

// An agent that holds nothing here says so, with a one-button grant;
// the button gives developer.
func TestARepositoryAgentThatHoldsNothingCanBeGivenDeveloper(t *testing.T) {
	h, db := repoAgentForge(t)
	if err := db.NewDedicatedAgent("idle-bot", "repo:alpha", "jva"); err != nil {
		t.Fatal(err)
	}
	body := repoAgentDo(t, h, "GET", "/r/alpha/settings", nil).Body.String()
	if !strings.Contains(body, `data-hook="repo-agent-caps" data-state="none"`) || !hasHook(body, "repo-agent-grant") {
		t.Fatalf("FAIL: an agent holding nothing is not marked with a grant button")
	}
	rec := repoAgentDo(t, h, "POST", "/r/alpha/settings/agents/grant", url.Values{"name": {"idle-bot"}, "role": {"developer"}})
	if rec.Code != http.StatusSeeOther || strings.Contains(rec.Header().Get("Location"), "e=") {
		t.Fatalf("FAIL: the grant answered %d to %q", rec.Code, rec.Header().Get("Location"))
	}
	if !db.EffectiveCaps("idle-bot", "alpha").HasAny("i") {
		t.Error("FAIL: the agent still holds no check-in letter")
	}
	body = repoAgentDo(t, h, "GET", "/r/alpha/settings", nil).Body.String()
	if strings.Contains(body, `data-state="none"`) {
		t.Error("FAIL: the row still says it holds nothing")
	}
	t.Log("ok")
}

// A grant that fails after the agent is made leaves it made, holding
// nothing, and the redirect carries the error.
func TestARepositoryAgentWhoseGrantFailsIsLeftHoldingNothing(t *testing.T) {
	h, db := repoAgentForgeHeld(t, "this repository is under a hold")
	rec := repoAgentDo(t, h, "POST", "/r/alpha/settings/agents", url.Values{"name": {"held-bot"}, "role": {"developer"}})
	loc := rec.Header().Get("Location")
	if rec.Code != http.StatusSeeOther || !strings.Contains(loc, "e=") || !strings.Contains(loc, "#agents") {
		t.Fatalf("FAIL: a refused grant answered %d to %q", rec.Code, loc)
	}
	if db.Operator("held-bot") != "repo:alpha" {
		t.Errorf("FAIL: the agent was not left made: operator %q", db.Operator("held-bot"))
	}
	if db.EffectiveCaps("held-bot", "alpha").HasAny("i") {
		t.Error("FAIL: a refused grant granted anyway")
	}
	body := repoAgentDo(t, h, "GET", "/r/alpha/settings", nil).Body.String()
	if !strings.Contains(body, `data-agent="held-bot"`) || !strings.Contains(body, `data-state="none"`) {
		t.Error("FAIL: the row does not show that it holds nothing")
	}
	t.Log("ok")
}

// The one-button route gives a role to this repository's own agents
// alone: a person's agent, or another repository's, is refused.
func TestTheRepositoryGrantRouteTakesOnlyItsOwnAgents(t *testing.T) {
	h, db := repoAgentForge(t)
	if err := db.NewAgent("jva-bot", "jva", -1); err != nil {
		t.Fatal(err)
	}
	if err := db.AddRepo("beta", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if err := db.NewDedicatedAgent("beta-bot", "repo:beta", "root"); err != nil {
		t.Fatal(err)
	}
	for _, name := range []string{"jva-bot", "beta-bot", "ghost"} {
		rec := repoAgentDo(t, h, "POST", "/r/alpha/settings/agents/grant", url.Values{"name": {name}, "role": {"developer"}})
		if rec.Code != http.StatusSeeOther || !strings.Contains(rec.Header().Get("Location"), "e=") {
			t.Errorf("FAIL: %s was not refused: %d %q", name, rec.Code, rec.Header().Get("Location"))
		}
		if stored, _ := db.CapsOf(name, "alpha"); stored != "" {
			t.Errorf("FAIL: a row was written for %s: %q", name, stored)
		}
	}
	t.Log("ok")
}