91 lines
4.2 KiB
Go
91 lines
4.2 KiB
Go
package link
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The subject and the signing context the controller holds the same words for (mesh-controller internal/link).
|
|
func TestTheSetuidSearchAskKeepsItsSubjectAndContext(t *testing.T) {
|
|
if AskSetuidSearchSubject("laptop") != "mesh.node.laptop.ask.setuid-search" {
|
|
t.Fatalf("the subject is %q", AskSetuidSearchSubject("laptop"))
|
|
}
|
|
if SetuidSearchContext != "novox-mesh setuid-search v1\n" || SetuidSearchContext == HandOverContext {
|
|
t.Fatalf("the signing context is %q", SetuidSearchContext)
|
|
}
|
|
}
|
|
|
|
func signedSearch(t *testing.T, key ed25519.PrivateKey, context string, ask HandOverAsk) []byte {
|
|
t.Helper()
|
|
raw, _ := json.Marshal(ask)
|
|
body, _ := json.Marshal(SignedHandOver{Ask: raw, Signature: ed25519.Sign(key, append([]byte(context), raw...))})
|
|
return body
|
|
}
|
|
|
|
// A search ask is believed as a hand-over is: signed by the mesh under its own context (a hand-over's signature
|
|
// starts no search), for this node, unexpired, once, and naming no path.
|
|
func TestASetuidSearchAskIsBelievedOnlySignedForThisNodeAndOnce(t *testing.T) {
|
|
m, key := aMember(t)
|
|
now := time.Date(2026, 10, 10, 2, 0, 0, 0, time.UTC)
|
|
var asked []string
|
|
handle := func(by string) (string, error) { asked = append(asked, by); return "a new search starts", nil }
|
|
check := HandOverCheck{Signer: m.Signer, Node: "n1", Now: func() time.Time { return now }, Nonces: &Nonces{}}
|
|
good := HandOverAsk{Node: "n1", By: " jo ", Expires: now.Add(HandOverGood), Nonce: aNonce}
|
|
for name, body := range map[string][]byte{
|
|
"a hand-over's signature": signedSearch(t, key, HandOverContext, good),
|
|
"another node": signedSearch(t, key, SetuidSearchContext, HandOverAsk{Node: "n2", By: "jo", Expires: good.Expires, Nonce: aNonce}),
|
|
"a path": signedSearch(t, key, SetuidSearchContext, HandOverAsk{Node: "n1", Path: "/x", By: "jo", Expires: good.Expires, Nonce: aNonce}),
|
|
"expired": signedSearch(t, key, SetuidSearchContext, HandOverAsk{Node: "n1", By: "jo", Expires: now, Nonce: aNonce}),
|
|
} {
|
|
if a := AnswerSetuidSearch(check, handle, body); !strings.HasSuffix(a.Refused, "no search was started") || a.Said != "" {
|
|
t.Errorf("%s: %+v", name, a)
|
|
}
|
|
}
|
|
if len(asked) != 0 {
|
|
t.Fatalf("a refused ask reached the handler: %v", asked)
|
|
}
|
|
body := signedSearch(t, key, SetuidSearchContext, good)
|
|
if a := AnswerSetuidSearch(check, handle, body); a.Said != "a new search starts" || len(asked) != 1 || asked[0] != "jo" {
|
|
t.Fatalf("the good ask: %+v, %v", a, asked)
|
|
}
|
|
if a := AnswerSetuidSearch(check, handle, body); !strings.Contains(a.Refused, "taken already") {
|
|
t.Fatalf("the same ask again: %+v", a)
|
|
}
|
|
if a := AnswerSetuidSearch(check, nil, body); a.Refused == "" {
|
|
t.Fatalf("no handler: %+v", a)
|
|
}
|
|
// 774 (ADR 0563 §2): the node seat's setuid-search starts none while one runs, at most one in any hour, naming when the
|
|
// next may start, and records who asked.
|
|
t.Run("the node seat's search is bounded", func(t *testing.T) {
|
|
at, running, started := now, false, 0
|
|
h := &HandActs{Node: "n1", State: &MemState{}, Now: func() time.Time { return at },
|
|
Searching: func() bool { return running },
|
|
SetuidSearch: func(by string) (string, error) { started++; return "a new search starts", nil }}
|
|
for _, row := range []struct {
|
|
name string
|
|
after time.Duration
|
|
running bool
|
|
refused string
|
|
started int
|
|
}{
|
|
{name: "one runs", running: true, refused: "runs on this machine now", started: 0},
|
|
{name: "the first", started: 1},
|
|
{name: "within the hour", after: 59 * time.Minute, refused: "the next may start at 2026-10-10T03:00:00Z", started: 1},
|
|
{name: "after the hour", after: time.Hour + time.Second, started: 2},
|
|
} {
|
|
at, running = now.Add(row.after), row.running
|
|
_, err := h.Call(EngineCall{Verb: ActSetuidSearch, Caller: "ace.node-tools"})
|
|
if (row.refused == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), row.refused)) || started != row.started {
|
|
t.Errorf("%s: %v, %d started", row.name, err, started)
|
|
}
|
|
}
|
|
acts, _ := h.Acts(1)
|
|
if got := acts.(map[string]any)["acts"].([]HandAct); len(got) != 1 || got[0].Caller != "ace.node-tools" || got[0].State != HandDone {
|
|
t.Errorf("the last search is recorded as %+v", got)
|
|
}
|
|
})
|
|
}
|