531 lines
22 KiB
Go
531 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// The controller's secret verbs reach the vault only for a module filled the new way (design 57's V2, issue 675,
|
|
// the release lead's decision on it; novox/hq ADR 0349 clauses 8 to 10 and 17).
|
|
|
|
// standInVault answers the vault's two-step verbs as mesh-vault does: a key made for each call, the value opened
|
|
// with it here and nowhere else. It keeps what it took and every argument it was sent, as the bus carried them.
|
|
type standInVault struct {
|
|
t *testing.T
|
|
mu sync.Mutex
|
|
keys map[string]string // call → private key
|
|
taken map[string]string // secret → value
|
|
sent []string // every argument, as JSON
|
|
refuse error // refuses every first step when set
|
|
outcome []string
|
|
again bool // the value is held already
|
|
// perHolder answers `issued` with a value per holder named, the last one not answered (issue 749).
|
|
perHolder bool
|
|
// asked is how the vault's own ask at a desk ends, as mesh-vault answers `secret-ask` (issue 723): the vault
|
|
// asks the desk itself, so the controller sends one step and is told how it ended.
|
|
asked string
|
|
}
|
|
|
|
func newStandInVault(t *testing.T) *standInVault {
|
|
return &standInVault{t: t, keys: map[string]string{}, taken: map[string]string{}}
|
|
}
|
|
|
|
func (v *standInVault) ask(verb string, args map[string]any) (json.RawMessage, error) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
raw, _ := json.Marshal(map[string]any{"verb": verb, "args": args})
|
|
v.sent = append(v.sent, string(raw))
|
|
if verb == "rotate" {
|
|
return json.Marshal(vaultChanged{Name: "own/g14/dunst/token", Generation: 2, By: fmt.Sprint(args["by"])})
|
|
}
|
|
// The vault asks the desk's prompt and the provider's tool itself, in its own name (issue 723): one step from
|
|
// the controller, which carries no key and no sealed value.
|
|
switch verb {
|
|
case "secret-ask":
|
|
if v.refuse != nil {
|
|
return nil, v.refuse
|
|
}
|
|
outcome := v.asked
|
|
if outcome == "" {
|
|
outcome = "given"
|
|
}
|
|
return json.Marshal(vaultAsked{Secret: fmt.Sprintf("own/%v/%v/%v", args["node"], args["module"], args["name"]),
|
|
Generation: 1, AskedBy: "as-the-vault-recorded", Outcome: outcome})
|
|
case "issued":
|
|
if v.refuse != nil {
|
|
return nil, v.refuse
|
|
}
|
|
// A tool answering a value per holder (issue 749): the vault keeps one for each holder named, and says which
|
|
// it was not given.
|
|
if holders, ok := args["holders"].([]issuedHolder); ok && v.perHolder {
|
|
out := vaultIssued{Secret: fmt.Sprintf("provision/%v/%v", args["provider"], args["provision"]), Unchanged: v.again}
|
|
for i, h := range holders {
|
|
if i == len(holders)-1 && len(holders) > 1 {
|
|
out.NotAnswered = append(out.NotAnswered, h)
|
|
continue
|
|
}
|
|
out.Holders = append(out.Holders, vaultIssuedHolder{Node: h.Node, Module: h.Module,
|
|
Secret: fmt.Sprintf("%s/%s/%s", out.Secret, h.Node, h.Module), Generation: 1, Unchanged: v.again})
|
|
}
|
|
return json.Marshal(out)
|
|
}
|
|
return json.Marshal(vaultIssued{Secret: fmt.Sprintf("provision/%v/%v", args["provider"], args["provision"]),
|
|
Generation: 1, Unchanged: v.again})
|
|
}
|
|
call, _ := args["call"].(string)
|
|
if call == "" {
|
|
if v.refuse != nil {
|
|
return nil, v.refuse
|
|
}
|
|
public, private, err := secrets.Keypair()
|
|
if err != nil {
|
|
v.t.Fatal(err)
|
|
}
|
|
call = fmt.Sprintf("call-%d", len(v.keys)+1)
|
|
v.keys[call] = private
|
|
secret := fmt.Sprintf("%s/%v", verb, args["name"])
|
|
return json.Marshal(vaultOpened{Call: call, SealTo: public, Secret: secret, AskedBy: "as-the-vault-recorded"})
|
|
}
|
|
private, ok := v.keys[call]
|
|
if !ok {
|
|
return nil, errors.New("no such call")
|
|
}
|
|
delete(v.keys, call)
|
|
if o, _ := args["outcome"].(string); o != "" {
|
|
v.outcome = append(v.outcome, o)
|
|
return json.Marshal(vaultChanged{Unchanged: true})
|
|
}
|
|
value, err := secrets.Open(private, fmt.Sprint(args["sealed"]))
|
|
if err != nil {
|
|
return nil, errors.New("not sealed to this call")
|
|
}
|
|
// As mesh-vault takes a typed value: its line ending off (intake.go AskEnd).
|
|
v.taken[verb] = strings.TrimRight(string(value), "\r\n")
|
|
return json.Marshal(vaultChanged{Name: verb, Generation: 1, Unchanged: v.again})
|
|
}
|
|
|
|
func (v *standInVault) said() string {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return strings.Join(v.sent, "\n")
|
|
}
|
|
|
|
// A module's secrets are the vault's once the catalogue marks it moved (design 57 V4, issue 647): its machine's
|
|
// client asks the vault for them. Every module not marked, and one the catalogue does not hold, keeps today's path.
|
|
func TestAModulesSecretsAreTheVaultsOnceItIsMarkedMoved(t *testing.T) {
|
|
open := aMesh(t)
|
|
register(t, open, aBot("moved-bot", true))
|
|
register(t, open, aBot("kept-bot", false))
|
|
for module, want := range map[string]bool{"moved-bot": true, "kept-bot": false, "nowhere": false} {
|
|
got, err := secretsOfTheVault(module)
|
|
if err != nil || got != want {
|
|
t.Errorf("%s: the vault's %v (%v), want %v", module, got, err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A switch that cannot be read does nothing, and says why: never today's path by default.
|
|
func TestASwitchThatCannotBeReadDoesNothing(t *testing.T) {
|
|
var ran []string
|
|
err := bySwitch("dunst", func(string) (bool, error) { return false, errors.New("the store is gone") },
|
|
func() error { ran = append(ran, "today"); return nil },
|
|
func() error { ran = append(ran, "vault"); return nil })
|
|
if err == nil || len(ran) > 0 || !strings.Contains(err.Error(), "the store is gone") {
|
|
t.Errorf("an unreadable switch ran %v (%v)", ran, err)
|
|
}
|
|
}
|
|
|
|
// The switch: today's path for a module filled the old way, the vault's for one filled the new way, and never
|
|
// both.
|
|
func TestTheSwitchRunsOnePathByTheModulesForm(t *testing.T) {
|
|
moved := switchOf(func(m string) bool { return m == "dunst" })
|
|
for module, want := range map[string]string{"telegram": "today", "dunst": "vault"} {
|
|
var ran []string
|
|
err := bySwitch(module, moved,
|
|
func() error { ran = append(ran, "today"); return nil },
|
|
func() error { ran = append(ran, "vault"); return nil })
|
|
if err != nil || strings.Join(ran, ",") != want {
|
|
t.Errorf("%s ran %v (%v), not %s", module, ran, err, want)
|
|
}
|
|
}
|
|
var ran []string
|
|
_ = bySwitch("dunst", nil, func() error { ran = append(ran, "today"); return nil },
|
|
func() error { ran = append(ran, "vault"); return nil })
|
|
if strings.Join(ran, ",") != "today" {
|
|
t.Errorf("no switch ran %v", ran)
|
|
}
|
|
}
|
|
|
|
// `secret accept` for a module filled the new way: the value read at the terminal is sealed there to the key the
|
|
// vault made for this one call; no clear value crosses the bus or reaches the controller's log. Over a real bus,
|
|
// with a tap on every subject.
|
|
func TestAnAcceptedValueCrossesTheBusOnlySealedToTheVault(t *testing.T) {
|
|
conn, err := nats.Connect(testbus.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(conn.Close)
|
|
before := handActConn
|
|
handActConn = conn
|
|
t.Cleanup(func() { handActConn = before })
|
|
|
|
var tapped bytes.Buffer
|
|
var tapMu sync.Mutex
|
|
tap, err := conn.Subscribe(">", func(m *nats.Msg) {
|
|
tapMu.Lock()
|
|
defer tapMu.Unlock()
|
|
tapped.WriteString(m.Subject + " " + string(m.Data) + "\n")
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tap.Unsubscribe()
|
|
|
|
vault := newStandInVault(t)
|
|
var subjects []string
|
|
holder, err := conn.Subscribe(broker.CalledPattern(link.SeatToolSubject("mesh-vault", "accept")), func(m *nats.Msg) {
|
|
subjects = append(subjects, m.Subject)
|
|
var args map[string]any
|
|
_ = json.Unmarshal(m.Data, &args)
|
|
result, err := vault.ask("accept", args)
|
|
answer := link.Answer{Result: result}
|
|
if err != nil {
|
|
answer = link.Answer{Error: err.Error()}
|
|
}
|
|
raw, _ := json.Marshal(answer)
|
|
_ = m.Respond(raw)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer holder.Unsubscribe()
|
|
|
|
var logged bytes.Buffer
|
|
was := log.Writer()
|
|
log.SetOutput(&logged)
|
|
t.Cleanup(func() { log.SetOutput(was) })
|
|
|
|
const value = "pw-of-last-year-that-must-not-show"
|
|
words, err := acceptIntoVault(theVault(context.Background()), provisionArgs("novox", "postgres"), value, "jochen")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if vault.taken["accept"] != value {
|
|
t.Fatalf("the vault opened %q", vault.taken["accept"])
|
|
}
|
|
if err := conn.Flush(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tapMu.Lock()
|
|
crossed := tapped.String()
|
|
tapMu.Unlock()
|
|
if !strings.Contains(crossed, "mesh.seat.mesh-vault.call.accept.controller") {
|
|
t.Fatalf("the tap heard no call to the vault: %s", crossed)
|
|
}
|
|
for _, s := range subjects {
|
|
if !strings.HasSuffix(s, ".controller") {
|
|
t.Errorf("the vault was asked on %s, not in the controller's name", s)
|
|
}
|
|
}
|
|
for where, text := range map[string]string{"the bus": crossed, "the log": logged.String(), "the answer": words} {
|
|
if strings.Contains(text, value) || strings.Contains(text, "must-not-show") {
|
|
t.Errorf("the value crossed %s in the clear", where)
|
|
}
|
|
}
|
|
if !strings.Contains(words, "sealed at this terminal") {
|
|
t.Errorf("the answer: %q", words)
|
|
}
|
|
// A vault that refuses the first step: nothing is sealed or sent.
|
|
vault.refuse = errors.New("the vault has no operator key")
|
|
if _, err := acceptIntoVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), value, "jochen"); err == nil ||
|
|
!strings.Contains(err.Error(), "nothing was read or sealed") {
|
|
t.Errorf("a refused call said %v", err)
|
|
}
|
|
}
|
|
|
|
// The desk's path for a module filled the new way (issue 723): the controller asks the vault once; the vault
|
|
// records the ask, holds its bounds and asks the desk's prompt itself, so the controller never reaches the desk and
|
|
// carries no key and no sealed value; today's store is not touched. For a module filled the old way, the vault is
|
|
// never asked.
|
|
func TestAnAskForAModuleFilledTheNewWayIsTheVaults(t *testing.T) {
|
|
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
|
|
d.askedBy = "g14/claude-code"
|
|
d.open = func(node, module, name, desk string) (int64, error) {
|
|
t.Fatal("today's record was asked")
|
|
return 0, nil
|
|
}
|
|
vault := newStandInVault(t)
|
|
d.vault = vault.ask
|
|
words, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*accepted) != 0 {
|
|
t.Fatal("the controller opened and kept what was typed")
|
|
}
|
|
if len(*asked) != 0 {
|
|
t.Fatalf("the controller asked the desk's prompt itself: %v", *asked)
|
|
}
|
|
said := vault.said()
|
|
if !strings.Contains(said, `"verb":"secret-ask"`) || !strings.Contains(said, `"asked_by":"g14/claude-code"`) ||
|
|
!strings.Contains(said, `"desk":"laptop"`) || strings.Count(said, "\n") != 0 {
|
|
t.Errorf("the vault was not asked once, naming who asked and the desk: %s", said)
|
|
}
|
|
if strings.Contains(said, "seal_to") || strings.Contains(said, "sealed") || strings.Contains(said, `"call"`) {
|
|
t.Errorf("the controller carried a key or a sealed value: %s", said)
|
|
}
|
|
if len(*acts) != 1 || (*acts)[0].Cause != "given-at-the-desk" || !strings.Contains((*acts)[0].Why, "kept in the vault") ||
|
|
!strings.Contains((*acts)[0].Why, "as-the-vault-recorded") {
|
|
t.Errorf("the act: %+v", *acts)
|
|
}
|
|
if !strings.Contains(words, "own/anchor/telegram/telegram-token") || strings.Contains(words, typed) {
|
|
t.Errorf("said %q", words)
|
|
}
|
|
|
|
// Dismissed and timed out: nothing given, said so.
|
|
for _, outcome := range []string{"dismissed", "timed-out", "empty"} {
|
|
d2, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
v2 := newStandInVault(t)
|
|
v2.asked = outcome
|
|
d2.vault = v2.ask
|
|
if _, err := d2.give("anchor", "telegram", "telegram-token", "laptop"); !errors.Is(err, errNothingGiven) {
|
|
t.Errorf("%s: %v", outcome, err)
|
|
}
|
|
}
|
|
// Failed at the desk: an error, not "nothing given".
|
|
d5, _, acts5, _ := aDesk(t, sealedTo(t, typed))
|
|
v5 := newStandInVault(t)
|
|
v5.asked = "failed"
|
|
d5.vault = v5.ask
|
|
if _, err := d5.give("anchor", "telegram", "telegram-token", "laptop"); err == nil || errors.Is(err, errNothingGiven) ||
|
|
len(*acts5) != 0 {
|
|
t.Errorf("a failed ask: %v, acts %d", err, len(*acts5))
|
|
}
|
|
// The vault's bounds refuse: said so, nobody is asked by the controller.
|
|
d3, _, _, asked3 := aDesk(t, sealedTo(t, typed))
|
|
v3 := newStandInVault(t)
|
|
v3.refuse = errors.New("an ask for telegram-token of telegram on anchor is still open")
|
|
d3.vault = v3.ask
|
|
if _, err := d3.give("anchor", "telegram", "telegram-token", "laptop"); err == nil ||
|
|
!strings.Contains(err.Error(), "still open") || len(*asked3) != 0 {
|
|
t.Errorf("a refused ask: %v, prompts %d", err, len(*asked3))
|
|
}
|
|
|
|
// Filled the old way: today's path, the vault never asked.
|
|
d4, accepted4, _, _ := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d4.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*accepted4) != 1 || d4.vault != nil {
|
|
t.Error("a module filled the old way did not take today's path")
|
|
}
|
|
}
|
|
|
|
// `secret issued` for a provider filled the new way (issue 723): the controller asks the vault once, naming the
|
|
// provider's machine, module and tool; the vault asks the tool itself; the controller never reaches the tool and
|
|
// nothing is kept, sealed or sent here. A provider filled the old way keeps today's path and the vault is never
|
|
// asked.
|
|
func TestAnIssuedValueForAProviderFilledTheNewWayGoesToTheVault(t *testing.T) {
|
|
var r issuedRecorder
|
|
vault := newStandInVault(t)
|
|
taker := r.taker(t, func(issuedProvider, map[string]any) (link.Answer, error) {
|
|
t.Fatal("the controller asked the provider's tool itself")
|
|
return link.Answer{}, nil
|
|
})
|
|
taker.keep = func(string, string, string) (inventory.IssuedKept, error) {
|
|
t.Fatal("today's store was asked to keep it")
|
|
return inventory.IssuedKept{}, nil
|
|
}
|
|
taker.ofTheVault = switchOf(func(m string) bool { return m == "plex" })
|
|
taker.vault = vault.ask
|
|
words, err := taker.take("plex-api")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(r.sent) != 0 {
|
|
t.Errorf("machines were sent: %v", r.sent)
|
|
}
|
|
if len(r.acts) != 1 || r.acts[0].Verb != handActIssued || !strings.Contains(r.acts[0].Why, "kept in the vault") {
|
|
t.Errorf("the act: %+v", r.acts)
|
|
}
|
|
said := vault.said()
|
|
for _, want := range []string{`"verb":"issued"`, `"provider":"ace"`, `"module":"plex"`, `"tool":"plex_issued_credential"`} {
|
|
if !strings.Contains(said, want) {
|
|
t.Errorf("the vault was told %s, without %s", said, want)
|
|
}
|
|
}
|
|
if strings.Contains(said, "seal_to") || strings.Contains(said, "sealed") || strings.Contains(said, `"call"`) {
|
|
t.Errorf("the controller carried a key or a sealed value: %s", said)
|
|
}
|
|
if !strings.Contains(words, "provision/ace/plex-api") {
|
|
t.Errorf("said %q", words)
|
|
}
|
|
// The same value again: the vault writes nothing, and nothing is recorded.
|
|
again := newStandInVault(t)
|
|
again.again = true
|
|
r2 := issuedRecorder{}
|
|
t2 := r2.taker(t, issuedSealed(t, "the-servers-own-token"))
|
|
t2.ofTheVault, t2.vault = switchOf(func(string) bool { return true }), again.ask
|
|
if words, err := t2.take("plex-api"); err != nil || len(r2.acts) != 0 || !strings.Contains(words, "nothing was written") {
|
|
t.Errorf("%q %v %+v", words, err, r2.acts)
|
|
}
|
|
// The vault refuses (its tool refused it): said, nothing recorded.
|
|
refused := newStandInVault(t)
|
|
refused.refuse = errors.New("its tool plex_issued_credential refused: this tool answers the vault alone")
|
|
r4 := issuedRecorder{}
|
|
t4 := r4.taker(t, issuedSealed(t, "x"))
|
|
t4.ofTheVault, t4.vault = switchOf(func(string) bool { return true }), refused.ask
|
|
if _, err := t4.take("plex-api"); err == nil || !strings.Contains(err.Error(), "answers the vault alone") || len(r4.acts) != 0 {
|
|
t.Errorf("a refusal: %v, acts %d", err, len(r4.acts))
|
|
}
|
|
|
|
// Filled the old way: today's path.
|
|
var r3 issuedRecorder
|
|
t3 := r3.taker(t, issuedSealed(t, "the-servers-own-token"))
|
|
t3.ofTheVault = switchOf(func(string) bool { return false })
|
|
t3.vault = func(string, map[string]any) (json.RawMessage, error) {
|
|
t.Fatal("the vault was asked for a provider filled the old way")
|
|
return nil, nil
|
|
}
|
|
if _, err := t3.take("plex-api"); err != nil || len(r3.kept) != 1 {
|
|
t.Errorf("today's path: %v, kept %d", err, len(r3.kept))
|
|
}
|
|
}
|
|
|
|
// `secret issued` names to the vault the provision's holders from that provider (issue 749): a tool may answer a
|
|
// value for each, which the vault keeps as that holder's own; the controller says what was kept for whom and which
|
|
// holder the tool gave nothing for, never a value. A provider answering one value is told the holders all the same
|
|
// and keeps the provision's one credential.
|
|
func TestAnIssuedValuePerHolderNamesTheHoldersToTheVault(t *testing.T) {
|
|
var r issuedRecorder
|
|
vault := newStandInVault(t)
|
|
vault.perHolder = true
|
|
taker := r.taker(t, func(issuedProvider, map[string]any) (link.Answer, error) {
|
|
t.Fatal("the controller asked the provider's tool itself")
|
|
return link.Answer{}, nil
|
|
})
|
|
taker.providers = func(string) ([]issuedProvider, bool, error) {
|
|
return []issuedProvider{{Node: "novox", Module: "gitea", Tool: "gitea_agent_token", Holders: []issuedHolder{
|
|
{Node: "anchor", Module: "claude-code"}, {Node: "laptop", Module: "claude-code"},
|
|
{Node: "shanks", Module: "claude-code"}}}}, true, nil
|
|
}
|
|
taker.ofTheVault = switchOf(func(m string) bool { return m == "gitea" })
|
|
taker.vault = vault.ask
|
|
words, err := taker.take("gitea-read")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
said := vault.said()
|
|
if !strings.Contains(said, `"holders":[{"node":"anchor","module":"claude-code"},{"node":"laptop","module":"claude-code"},{"node":"shanks","module":"claude-code"}]`) {
|
|
t.Errorf("the vault was told %s", said)
|
|
}
|
|
for _, want := range []string{"provision/novox/gitea-read/anchor/claude-code", "provision/novox/gitea-read/laptop/claude-code",
|
|
"gave nothing for claude-code on shanks"} {
|
|
if !strings.Contains(words, want) {
|
|
t.Errorf("said %q, without %q", words, want)
|
|
}
|
|
}
|
|
if len(r.acts) != 1 || !strings.Contains(r.acts[0].Why, "claude-code on anchor") || !strings.Contains(r.acts[0].Why, "claude-code on laptop") {
|
|
t.Errorf("the act: %+v", r.acts)
|
|
}
|
|
if len(r.sent) != 0 || len(r.kept) != 0 {
|
|
t.Error("the controller kept or sent something")
|
|
}
|
|
// The same values again: nothing written, nothing recorded.
|
|
again := newStandInVault(t)
|
|
again.perHolder, again.again = true, true
|
|
r2 := issuedRecorder{}
|
|
t2 := r2.taker(t, nil)
|
|
t2.providers, t2.ofTheVault, t2.vault = taker.providers, taker.ofTheVault, again.ask
|
|
if words, err := t2.take("gitea-read"); err != nil || len(r2.acts) != 0 || !strings.Contains(words, "nothing was written") {
|
|
t.Errorf("again: %q %v %+v", words, err, r2.acts)
|
|
}
|
|
}
|
|
|
|
// The holders named are the provision's holders from that provider, each consuming module on each machine once.
|
|
func TestTheHoldersNamedAreThoseOfThatProvider(t *testing.T) {
|
|
holders := []inventory.Holder{
|
|
{Provision: "gitea-read", Consumer: "anchor", ConsumerModule: "claude-code", Provider: "novox"},
|
|
{Provision: "gitea-read", Consumer: "anchor", ConsumerModule: "claude-code", Local: "second", Provider: "novox"},
|
|
{Provision: "gitea-read", Consumer: "laptop", ConsumerModule: "claude-code", Provider: "novox"},
|
|
{Provision: "gitea-read", Consumer: "ace", ConsumerModule: "claude-code", Provider: "shanks"},
|
|
}
|
|
got := issuedHoldersFrom(holders, "novox")
|
|
if fmt.Sprint(got) != "[claude-code on anchor claude-code on laptop]" {
|
|
t.Errorf("the holders named: %v", got)
|
|
}
|
|
if got := issuedHoldersFrom(holders, "g14"); len(got) != 0 {
|
|
t.Errorf("a provider holding nothing named %v", got)
|
|
}
|
|
shelf := map[string]catalogue.Manifest{"gitea": {Module: "gitea", Provides: []catalogue.Offer{{Name: "gitea-read",
|
|
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "gitea_agent_token"}}}}}
|
|
providers, err := issuedProvidersFrom(shelf, holders, func(string) ([]string, error) { return []string{"gitea"}, nil },
|
|
"gitea-read")
|
|
if err != nil || len(providers) != 2 || providers[0].Node != "novox" || fmt.Sprint(providers[0].Holders) !=
|
|
"[claude-code on anchor claude-code on laptop]" || fmt.Sprint(providers[1].Holders) != "[claude-code on ace]" {
|
|
t.Errorf("the providers asked: %+v %v", providers, err)
|
|
}
|
|
}
|
|
|
|
// `secret rotate` for a module filled the new way asks the vault, naming who asked; nothing is sent.
|
|
func TestARotationForAModuleFilledTheNewWayIsTheVaults(t *testing.T) {
|
|
vault := newStandInVault(t)
|
|
if _, err := rotateInVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), "g14/claude-code", " "); err == nil ||
|
|
vault.said() != "" {
|
|
t.Fatalf("a rotation without a why reached the vault: %v", err)
|
|
}
|
|
words, err := rotateInVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), "g14/claude-code", "leaked")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(vault.said(), `"verb":"rotate"`) || !strings.Contains(vault.said(), `"by":"g14/claude-code"`) ||
|
|
!strings.Contains(vault.said(), `"module":"dunst"`) || !strings.Contains(vault.said(), `"why":"leaked"`) {
|
|
t.Errorf("the vault was told %s", vault.said())
|
|
}
|
|
if !strings.Contains(words, "generation 2") || !strings.Contains(words, "takes it from the vault") {
|
|
t.Errorf("%q", words)
|
|
}
|
|
}
|
|
|
|
// The module that decides `accept --provider` is the one on the provider's machine that offers the provision; none,
|
|
// or several, is none, and today's path then says what is wrong.
|
|
func TestTheProvidersModuleIsTheOneThatOffersTheProvision(t *testing.T) {
|
|
offer := func(module string, provisions ...string) catalogue.Manifest {
|
|
m := catalogue.Manifest{Module: module}
|
|
for _, p := range provisions {
|
|
m.Provides = append(m.Provides, catalogue.Offer{Name: p})
|
|
}
|
|
return m
|
|
}
|
|
shelf := map[string]catalogue.Manifest{"tv": offer("tv", "tv-api"), "plex": offer("plex", "plex-api"),
|
|
"other-tv": offer("other-tv", "tv-api"), "dunst": offer("dunst")}
|
|
for _, c := range []struct {
|
|
assigned []string
|
|
want string
|
|
}{
|
|
{[]string{"dunst", "tv", "plex"}, "tv"},
|
|
{[]string{"dunst", "plex"}, ""},
|
|
{[]string{"tv", "other-tv"}, ""},
|
|
} {
|
|
if got := offeredOn(shelf, c.assigned, "tv-api"); got != c.want {
|
|
t.Errorf("%v: %q, want %q", c.assigned, got, c.want)
|
|
}
|
|
}
|
|
}
|