package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"sync"
"testing"
"time"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
type recNotify struct {
mu sync.Mutex
n int
}
func (r *recNotify) Trigger(context.Context, NotifyEvent) error {
r.mu.Lock()
defer r.mu.Unlock()
r.n++
return nil
}
// startRig is the setup rig with the start route in front of it.
type startRig struct {
*setupRig
st *SetupStarter
notify *recNotify
clock time.Time
addr int
}
func newStartRig(t *testing.T) *startRig {
t.Helper()
g := newSetupRig(t)
n := &recNotify{}
d := g.deps
d.Notify = n
sr := &startRig{setupRig: g, notify: n, clock: time.Now()}
sr.st = NewSetupStarter(d, nil)
sr.st.now = func() time.Time { return sr.clock }
return sr
}
// start posts a start from its own address unless from is named.
func (g *startRig) start(body, from string) (int, string, http.Header) {
g.t.Helper()
if from == "" {
g.addr++
from = fmt.Sprintf("198.51.100.%d", g.addr)
}
req := httptest.NewRequest("POST", "/api/v1/setup/start", strings.NewReader(body))
req.RemoteAddr = from + ":5000"
w := httptest.NewRecorder()
g.st.ServeHTTP(w, req)
return w.Code, w.Body.String(), w.Header()
}
func startBody(name, kind, pickup string) string {
return fmt.Sprintf(`{"name":%q,"kind":%q,"pickup":%q}`, name, kind, pickup)
}
func (g *startRig) begin(name, kind, pickup string) string {
g.t.Helper()
status, body, _ := g.start(startBody(name, kind, pickup), "")
var out setupStartOut
if err := json.Unmarshal([]byte(body), &out); err != nil || status != 200 || out.Code == "" {
g.t.Fatalf("FAIL: start answered %d", status)
}
return out.Code
}
func (g *startRig) look(who, code string) (setupClaimLookOut, error) {
out, err := g.r.Invoke(asUser(who), "setup.start.look", setupClaimLookIn{Code: code})
if err != nil {
return setupClaimLookOut{}, err
}
return out.(setupClaimLookOut), nil
}
func (g *startRig) claim(who, code, repos, role string) (string, error) {
out, err := g.r.Invoke(asUser(who), "setup.start.claim", setupClaimIn{Code: code, Repos: repos, Role: role})
if err != nil {
return "", err
}
return out.(setupClaimOut).Request, nil
}
func (g *startRig) requestsOf(person string) []SignOffRequest {
g.t.Helper()
rows, err := g.deps.SignOffs.SignOffsFor(person, 100)
if err != nil {
g.t.Fatal(err)
}
return rows
}
func repoNames(cs []setupRepoChoice) []string {
out := make([]string, 0, len(cs))
for _, c := range cs {
out = append(out, c.Name)
}
return out
}
var shortCode = regexp.MustCompile(`^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$`)
// RFC 0030, Starting without a credential: a start answers a code, a
// link without the code, and when it lapses, and files nothing at all.
func TestAStartAnswersACodeALinkAndAnExpiryAndFilesNothing(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
status, body, _ := g.start(startBody("e-desktop", "read", pickup), "")
var out map[string]any
if err := json.Unmarshal([]byte(body), &out); err != nil || status != 200 {
t.Fatalf("FAIL: start answered %d", status)
}
if len(out) != 3 {
t.Errorf("FAIL: the answer has %d keys, want code, link, expires", len(out))
}
code, _ := out["code"].(string)
link, _ := out["link"].(string)
expires, _ := out["expires"].(string)
if !shortCode.MatchString(code) {
t.Errorf("FAIL: the code is not XXXX-XXXX in the alphabet")
}
if link != "https://forge.test/setup" || strings.Contains(link, strings.ReplaceAll(code, "-", "")) || strings.Contains(link, "?") {
t.Errorf("FAIL: the link: %q", link)
}
if at, err := time.Parse(time.RFC3339, expires); err != nil || !at.Equal(g.clock.UTC().Add(oltp.SetupStartLife).Truncate(time.Second)) {
t.Errorf("FAIL: expires %q (%v)", expires, err)
}
for _, who := range []string{"dmestas", "jva", "dmestas-bot", "jva-bot", "zeitgeist"} {
if rows := g.requestsOf(who); len(rows) != 0 {
t.Errorf("FAIL: %s has %d sign-off rows after a start", who, len(rows))
}
if ev := g.attn.sent[who]; len(ev) != 0 {
t.Errorf("FAIL: %s was attended %d times by a start", who, len(ev))
}
}
if g.notify.n != 0 || len(g.rec.rows) != 0 {
t.Errorf("FAIL: a start notified (%d) or logged a setting (%d)", g.notify.n, len(g.rec.rows))
}
if len(g.attn.sent) != 0 {
t.Errorf("FAIL: a start raised attention events")
}
// What the client may say is what it is held to.
for what, body := range map[string]string{
"a repository named": `{"name":"x","kind":"read","pickup":"` + pickup + `","repos":"suit"}`,
"an account named": `{"name":"x","kind":"read","pickup":"` + pickup + `","repos":"","account":"dmestas"}`,
"a reason given": `{"name":"x","kind":"read","pickup":"` + pickup + `","reason":"let me in"}`,
"a name with a space": startBody("bad name", "read", pickup),
"a name that is long": startBody(strings.Repeat("a", 65), "read", pickup),
"a kind that is not": startBody("x", "admin", pickup),
"a pickup not a hash": startBody("x", "read", "nothex"),
} {
if status, _, _ := g.start(body, ""); status != 400 {
t.Errorf("FAIL: %s answered %d, want 400", what, status)
}
}
if !t.Failed() {
t.Log("ok")
}
}
// A name that is already an account starts exactly as a free one does:
// the route is no oracle for accounts.
func TestAStartForATakenNameAnswersAsAFreeOneDoes(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
shape := func(name string) (int, map[string]string) {
status, body, _ := g.start(startBody(name, "push", pickup), "")
var out map[string]string
_ = json.Unmarshal([]byte(body), &out)
return status, out
}
freeStatus, free := shape("brand-new-agent")
for _, taken := range []string{"dmestas", "Dmestas", "zeitgeist", "suit-ci"} {
status, out := shape(taken)
if status != freeStatus || len(out) != len(free) || out["link"] != free["link"] || !shortCode.MatchString(out["code"]) || out["expires"] == "" {
t.Errorf("FAIL: a start for %q answers %d %v, a free name %d %v", taken, status, out, freeStatus, free)
}
}
// The claim says so, plainly, to the person: no repository is offered.
code := g.begin("dmestas", "read", pickup)
look, err := g.look("dmestas", code)
if err != nil || look.Problem == "" || len(look.Repos) != 0 {
t.Errorf("FAIL: the claim of a taken name: %+v %v", look, err)
}
if _, err := g.claim("dmestas", code, "suit", ""); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: claiming a taken name: %v", err)
}
if rows := g.requestsOf("dmestas"); len(rows) != 0 {
t.Errorf("FAIL: a taken name filed a request")
}
if !t.Failed() {
t.Log("ok")
}
}
func TestStartsAreLimitedPerAddressAndInAll(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
var last http.Header
var status int
for i := 0; i <= setupStartPerAddress; i++ {
status, _, last = g.start(startBody("a"+fmt.Sprint(i), "read", pickup), "203.0.113.9")
}
if status != 429 || last.Get("Retry-After") == "" {
t.Errorf("FAIL: the sixth start from one address answered %d (Retry-After %q)", status, last.Get("Retry-After"))
}
// Another address is not held by it.
if status, _, _ := g.start(startBody("other", "read", pickup), "203.0.113.10"); status != 200 {
t.Errorf("FAIL: a neighbour was held: %d", status)
}
// The forge holds a fixed number unclaimed.
db := g.db
for n, _ := db.SetupStartRows(); n < setupStartMax; n++ {
if _, err := db.StartSetup(oltp.SetupStart{Pickup: pickup, Name: "n", Kind: "read", Addr: fmt.Sprint("x", n)}, setupStartMax, setupStartPerShare, g.clock); err != nil {
t.Fatal(err)
}
}
status, _, h := g.start(startBody("one-too-many", "read", pickup), "")
if status != 429 || h.Get("Retry-After") == "" {
t.Errorf("FAIL: past the cap a start answered %d (Retry-After %q)", status, h.Get("Retry-After"))
}
// A moment later, once they lapse, starts are taken again.
g.clock = g.clock.Add(oltp.SetupStartLife + time.Minute)
if status, _, _ := g.start(startBody("after-the-lapse", "read", pickup), ""); status != 200 {
t.Errorf("FAIL: the cap did not release as starts lapsed: %d", status)
}
if !t.Failed() {
t.Log("ok")
}
}
// collectAs polls the collect route with key from its own address.
func (g *startRig) collectKey(key, from string) (int, string) {
req := httptest.NewRequest("POST", collectPath, nil)
req.Header.Set("Authorization", "Pickup "+key)
req.RemoteAddr = from + ":4000"
w := httptest.NewRecorder()
g.coll.ServeHTTP(w, req)
return w.Code, w.Body.String()
}
// An unclaimed start, a lapsed one, a discarded one and a key never seen
// are one answer at collect; a lapsed, a discarded, an unknown and a
// malformed code are one answer at claim.
func TestUnclaimedLapsedDiscardedAndUnknownAreOneAnswer(t *testing.T) {
g := newStartRig(t)
keys := map[string]string{}
pickups := map[string]string{}
for _, which := range []string{"unclaimed", "lapsed", "discarded", "unknown"} {
k, p := newPickupKey(t)
keys[which], pickups[which] = k, p
}
g.begin("e-unclaimed", "read", pickups["unclaimed"])
// Lapsed: started in the past.
lapsedCode, err := g.db.StartSetup(oltp.SetupStart{Pickup: pickups["lapsed"], Name: "e-lapsed", Kind: "read", Addr: "x"}, setupStartMax, setupStartPerShare, time.Now().Add(-oltp.SetupStartLife-time.Minute))
if err != nil {
t.Fatal(err)
}
discardedCode := g.begin("e-discarded", "read", pickups["discarded"])
if _, err := g.r.Invoke(asUser("dmestas"), "setup.start.discard", setupClaimLookIn{Code: discardedCode}); err != nil {
t.Fatal(err)
}
type seen struct {
status int
body string
}
got := map[string]seen{}
i := 0
for which, key := range keys {
i++
status, body := g.collectKey(key, fmt.Sprintf("192.0.2.%d", i))
got[which] = seen{status, body}
}
for which, s := range got {
if s != got["unknown"] || s.status != 404 {
t.Errorf("FAIL: collect for %s answered %d %q, unknown answered %d %q", which, s.status, s.body, got["unknown"].status, got["unknown"].body)
}
}
// At claim, by a signed-in person.
answers := map[string]string{}
for which, code := range map[string]string{"lapsed": lapsedCode, "discarded": discardedCode, "unknown": "ZZZZ-ZZZZ", "malformed": "not a code!", "long": strings.Repeat("A", 40)} {
_, lerr := g.look("dmestas", code)
_, cerr := g.claim("jva", code, "suit", "")
if !errors.Is(lerr, ops.ErrNotFound) || !errors.Is(cerr, ops.ErrNotFound) {
t.Errorf("FAIL: %s: look %v, claim %v", which, lerr, cerr)
continue
}
answers[which] = lerr.Error() + "|" + cerr.Error()
}
for which, a := range answers {
if a != answers["unknown"] {
t.Errorf("FAIL: %s answers %q, unknown %q", which, a, answers["unknown"])
}
}
if !t.Failed() {
t.Log("ok")
}
}
func TestATokenCannotClaimOrLookOrDiscard(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
code := g.begin("e-desktop", "read", pickup)
for op, in := range map[string]any{
"setup.start.look": setupClaimLookIn{Code: code},
"setup.start.claim": setupClaimIn{Code: code, Repos: "suit"},
"setup.start.discard": setupClaimLookIn{Code: code},
} {
if _, err := g.r.Invoke(tokenCtx("dmestas"), op, in); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: %s on a token: %v", op, err)
}
// An agent signed in by its own account is no person either.
if _, err := g.r.Invoke(asUser("dmestas-bot"), op, in); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: %s as an agent: %v", op, err)
}
}
if _, ok, _ := g.db.SetupStartByCode(code, time.Now()); !ok {
t.Errorf("FAIL: the start did not survive the refusals")
}
if rows := g.requestsOf("dmestas"); len(rows) != 0 {
t.Errorf("FAIL: a refused claim filed a request")
}
if !t.Failed() {
t.Log("ok")
}
}
// What a person may pick is what agent.setup would accept from them, and
// the request it files is theirs, with their picks, the start's pickup
// and the forge's own reason.
func TestAClaimOffersOnlyWhatThePersonMaySetUpAndFilesTheirRequest(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
read := g.begin("e-desktop", "read", pickup)
// jva reads only the public ones; dmestas administers all three.
jv, err := g.look("jva", read)
if err != nil {
t.Fatal(err)
}
if strings.Join(repoNames(jv.Repos), ",") != "ctx,suit" || jv.Name != "e-desktop" || jv.Kind != "read" || jv.Address == "" || jv.Created == "" {
t.Errorf("FAIL: jva is offered %+v", jv)
}
dm, _ := g.look("dmestas", read)
if strings.Join(repoNames(dm.Repos), ",") != "ctx,secret,suit" {
t.Errorf("FAIL: dmestas is offered %v", dm.Repos)
}
// A repository outside the list, posted by hand, is refused and the
// start survives.
if _, err := g.claim("jva", read, "suit,secret", ""); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: a hand-posted repository: %v", err)
}
if _, err := g.claim("jva", read, "no-such-repo", ""); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: a repository that is not there: %v", err)
}
if rows := g.requestsOf("jva"); len(rows) != 0 {
t.Fatalf("FAIL: a refused claim filed a request")
}
id, err := g.claim("jva", read, "ctx, suit", "")
if err != nil {
t.Fatal(err)
}
rows := g.requestsOf("jva")
if len(rows) != 1 || rows[0].ID != id {
t.Fatalf("FAIL: jva's requests: %+v", rows)
}
s := rows[0]
var in agentSetupIn
if err := json.Unmarshal([]byte(s.Input), &in); err != nil {
t.Fatal(err)
}
if s.Owner != "jva" || s.Op != setupOp || s.State != "pending" || s.Pickup != pickup || s.Reason != setupClaimReason ||
s.Agent != clientAsker("jva") || in.Name != "e-desktop" || in.Kind != "read" || in.Repos != "ctx, suit" || in.Ceiling != oltp.SetupReadCeiling {
t.Errorf("FAIL: the request: %+v %+v", s, in)
}
if _, ok := g.db.AccountByName(s.Agent); ok {
t.Errorf("FAIL: the asker is an account")
}
if rows := g.requestsOf("dmestas"); len(rows) != 0 {
t.Errorf("FAIL: dmestas was handed jva's request")
}
// The start is spent.
if _, err := g.claim("jva", read, "suit", ""); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: claiming twice: %v", err)
}
// No attention event or notification is raised by a claim either: the
// person is looking at the request.
if len(g.attn.sent["jva"]) != 0 || g.notify.n != 0 {
t.Errorf("FAIL: a claim raised events (%d) or notifications (%d)", len(g.attn.sent["jva"]), g.notify.n)
}
if !t.Failed() {
t.Log("ok")
}
}
func TestAPushClaimIsOneRepositoryAndARoleSetupAllows(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
push := g.begin("suit-push", "push", pickup)
// jva holds nothing that a push role needs on any repository.
jv, err := g.look("jva", push)
if err != nil || len(jv.Repos) != 0 || jv.Problem == "" {
t.Errorf("FAIL: jva's push offer: %+v %v", jv, err)
}
dm, err := g.look("dmestas", push)
if err != nil || strings.Join(repoNames(dm.Repos), ",") != "ctx,secret,suit" || strings.Join(dm.Roles, ",") != "developer,publisher,maintainer" {
t.Errorf("FAIL: dmestas's push offer: %+v %v", dm, err)
}
for what, c := range map[string][2]string{
"two repositories": {"suit,ctx", ""},
"an admin role": {"suit", "admin"},
"an unknown role": {"suit", "owner"},
} {
if _, err := g.claim("dmestas", push, c[0], c[1]); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: %s: %v", what, err)
}
}
if _, err := g.claim("jva", push, "suit", ""); err == nil {
t.Errorf("FAIL: jva claimed a push setup on a repository they cannot give a role on")
}
if _, ok, _ := g.db.SetupStartByCode(push, time.Now()); !ok {
t.Fatalf("FAIL: refusals consumed the start")
}
id, err := g.claim("dmestas", push, "suit", "maintainer")
if err != nil {
t.Fatal(err)
}
got := g.get("dmestas", id)
if got.Setup == nil || got.Setup.Kind != "push" || got.Setup.Role != "maintainer" || strings.Join(got.Setup.Repos, ",") != "suit" || got.Setup.Pickup != pickup {
t.Errorf("FAIL: the request's setup view: %+v", got.Setup)
}
if !t.Failed() {
t.Log("ok")
}
}
func TestTwoClaimsAtOnceFileOneRequest(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
code := g.begin("e-desktop", "read", pickup)
var wg sync.WaitGroup
var mu sync.Mutex
won := 0
for i := 0; i < 8; i++ {
who := []string{"dmestas", "jva"}[i%2]
wg.Add(1)
go func() {
defer wg.Done()
if _, err := g.claim(who, code, "suit", ""); err == nil {
mu.Lock()
won++
mu.Unlock()
}
}()
}
wg.Wait()
total := len(g.requestsOf("dmestas")) + len(g.requestsOf("jva"))
if won != 1 || total != 1 {
t.Errorf("FAIL: %d claims won and %d requests were filed", won, total)
}
if !t.Failed() {
t.Log("ok")
}
}
// A claim that cannot file puts the start back.
func TestAClaimThatCannotBeFiledLeavesTheStart(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
// Another request already holds the pickup code.
other := g.begin("e-first", "read", pickup)
second := g.begin("e-second", "read", pickup)
if _, err := g.claim("dmestas", other, "suit", ""); err != nil {
t.Fatal(err)
}
if _, err := g.claim("dmestas", second, "suit", ""); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: a second start with the same pickup: %v", err)
}
if _, ok, _ := g.db.SetupStartByCode(second, time.Now()); !ok {
t.Errorf("FAIL: the start was lost with the refusal")
}
if !t.Failed() {
t.Log("ok")
}
}
// The whole way: start, claim, approve, collect once.
func TestAClaimedStartIsApprovedAndCollectedOnceByTheKeyHolder(t *testing.T) {
g := newStartRig(t)
key, pickup := newPickupKey(t)
code := g.begin("e-desktop", "read", pickup)
// Before the claim there is nothing to collect.
if status, _ := g.collectKey(key, "192.0.2.50"); status != 404 {
t.Fatalf("FAIL: collect before the claim: %d", status)
}
id, err := g.claim("dmestas", code, "suit,ctx", "")
if err != nil {
t.Fatal(err)
}
if status, body := g.collectKey(key, "192.0.2.51"); status != 202 {
t.Fatalf("FAIL: collect while the request waits: %d %s", status, body)
}
if err := g.decide("dmestas", id, true); err != nil {
t.Fatalf("FAIL: approving a claimed request: %v", err)
}
a, ok := g.db.AccountByName("e-desktop")
if !ok || a.Operator != "dmestas" {
t.Fatalf("FAIL: the agent: %+v", a)
}
status, body := g.collectKey(key, "192.0.2.52")
var got struct {
Agent string `json:"agent"`
Secret json.RawMessage `json:"secret"`
}
_ = json.Unmarshal([]byte(body), &got)
if status != 200 || !strings.Contains(string(got.Secret), "qat_") || got.Agent != clientAsker("dmestas") {
t.Fatalf("FAIL: the collect: %d (agent %q)", status, got.Agent)
}
if status, _ := g.collectKey(key, "192.0.2.53"); status != 410 {
t.Errorf("FAIL: the second collect answered %d", status)
}
// Denying a claimed request needs no account behind its asker either.
_, p2 := newPickupKey(t)
id2, err := g.claim("dmestas", g.begin("e-other", "read", p2), "suit", "")
if err != nil {
t.Fatal(err)
}
if err := g.decide("dmestas", id2, false); err != nil {
t.Errorf("FAIL: denying: %v", err)
}
if !t.Failed() {
t.Log("ok")
}
}
func TestCodesNotHeldAreLimitedPerAccountAndPerAddressOnEveryDoor(t *testing.T) {
g := newStartRig(t)
var err error
for i := 0; i <= setupMissesPerUser; i++ {
_, err = g.look("jva", "ZZZZ-ZZZZ")
}
if !errors.Is(err, ops.ErrRateLimited) {
t.Errorf("FAIL: the call past the account's misses: %v", err)
}
// A held account is held for a right code too.
_, pickup := newPickupKey(t)
code := g.begin("e-desktop", "read", pickup)
if _, err := g.look("jva", code); !errors.Is(err, ops.ErrRateLimited) {
t.Errorf("FAIL: a held account looked a code up: %v", err)
}
if _, err := g.look("dmestas", code); err != nil {
t.Errorf("FAIL: another account was held: %v", err)
}
// A right code spends nothing.
for i := 0; i < 3*setupMissesPerUser; i++ {
if _, err := g.look("dmestas", code); err != nil {
t.Fatalf("FAIL: a right code was counted: %v", err)
}
}
// Per address: accounts that each stay under their own limit are held
// together.
var aerr error
for i := 0; i <= setupMissesPerAddr; i++ {
_, aerr = g.r.Invoke(ops.WithAddr(asUser(fmt.Sprint("p", i)), "203.0.113.77"), "setup.start.look", setupClaimLookIn{Code: "ZZZZ-ZZZZ"})
}
if !errors.Is(aerr, ops.ErrRateLimited) {
t.Errorf("FAIL: the address was not held: %v", aerr)
}
if !t.Failed() {
t.Log("ok")
}
}
// One address cannot hold more than its share; two addresses in one IPv6
// /64 are one address; another address still starts.
func TestOneAddressHoldsOnlyAShareOfTheStarts(t *testing.T) {
g := newStartRig(t)
_, pickup := newPickupKey(t)
var status int
for i := 0; i <= setupStartPerShare; i++ {
g.clock = g.clock.Add(time.Minute) // under the per-minute rate
status, _, _ = g.start(startBody(fmt.Sprint("a", i), "read", pickup), "198.51.100.200")
if i < setupStartPerShare && status != 200 {
t.Fatalf("FAIL: start %d answered %d", i, status)
}
}
if status != 429 {
t.Errorf("FAIL: a fourth live start from one address answered %d", status)
}
if status, _, _ := g.start(startBody("other", "read", pickup), "198.51.100.201"); status != 200 {
t.Errorf("FAIL: a second address was held by the first: %d", status)
}
// Two addresses of one /64 share the limit.
for i := 0; i < setupStartPerShare; i++ {
g.clock = g.clock.Add(time.Minute)
a := fmt.Sprintf("2001:db8:1:2::%x", i+1)
if status, _, _ := g.start(startBody(fmt.Sprint("v", i), "read", pickup), "["+a+"]"); status != 200 {
t.Fatalf("FAIL: v6 start %d answered %d", i, status)
}
}
if status, _, _ := g.start(startBody("v-more", "read", pickup), "[2001:db8:1:2:ffff::9]"); status != 429 {
t.Errorf("FAIL: another address in the same /64 started: %d", status)
}
if status, _, _ := g.start(startBody("v-else", "read", pickup), "[2001:db8:1:3::1]"); status != 200 {
t.Errorf("FAIL: a different /64 was held: %d", status)
}
// A refusal for want of room needs no write: the lapsed rows stay
// until a start that can be held sweeps them.
if !t.Failed() {
t.Log("ok")
}
}
// What the key holder is told when the request was not approved carries
// no account name.
func TestACollectOfADeniedClaimedRequestNamesNobody(t *testing.T) {
g := newStartRig(t)
key, pickup := newPickupKey(t)
id, err := g.claim("dmestas", g.begin("e-desktop", "read", pickup), "suit", "")
if err != nil {
t.Fatal(err)
}
if err := g.decide("dmestas", id, false); err != nil {
t.Fatal(err)
}
status, body := g.collectKey(key, "192.0.2.60")
if status != 410 || strings.Contains(body, "dmestas") || strings.Contains(body, clientAskerPrefix) {
t.Errorf("FAIL: a denied claimed request collected as %d %s", status, body)
}
if !t.Failed() {
t.Log("ok")
}
}