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")
}