A licence store rebuilt after issue 241 counted generations from one again, and nodes that apply only a higher number discarded the login move and the rotations unseen. Nodes now apply any binding other than the one applied; the manager moves its sequence past every generation a node reports. hq issue 243.
464 lines
19 KiB
Go
464 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var now = time.Now().UnixMilli()
|
|
|
|
func node(t *testing.T, name string) (Paths, map[string]string) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
p := Paths{State: filepath.Join(root, "state"), Facts: filepath.Join(root, "state", "facts.json"),
|
|
Settings: filepath.Join(root, "state", "settings.json"), Home: filepath.Join(root, "home"), Node: name}
|
|
_ = os.MkdirAll(p.State, 0o700)
|
|
_ = os.MkdirAll(filepath.Join(p.Home, ".claude"), 0o700)
|
|
_ = os.WriteFile(p.Facts, []byte(`{"node":"`+name+`","console":"http://127.0.0.1:4270/mcp"}`), 0o600)
|
|
_ = os.WriteFile(p.Settings, []byte(`{"role":"","mcp_servers":{}}`), 0o600)
|
|
return p, map[string]string{}
|
|
}
|
|
|
|
func writer(w map[string]string) WriteManaged {
|
|
return func(name, content string) (string, error) { w[name] = content; return name + ": written", nil }
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func creds(t *testing.T, p Paths) map[string]any {
|
|
t.Helper()
|
|
var c map[string]any
|
|
raw, _ := os.ReadFile(p.credentials())
|
|
if err := json.Unmarshal(raw, &c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c["claudeAiOauth"].(map[string]any)
|
|
}
|
|
|
|
// ---- the renderer, held to the TypeScript it replaced -------------------------------------------------
|
|
|
|
func TestTheRendererWritesWhatTheTypeScriptOneWrote(t *testing.T) {
|
|
raw, err := os.ReadFile("testdata/rendered-by-typescript.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var f struct {
|
|
Facts Facts `json:"facts"`
|
|
Settings Settings `json:"settings"`
|
|
Registered Servers `json:"registered"`
|
|
WithKey map[string]string `json:"withKey"`
|
|
Plain map[string]string `json:"plain"`
|
|
}
|
|
if err := json.Unmarshal(raw, &f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
same := func(label string, got, want map[string]string) {
|
|
if got["CLAUDE.md"] != want["CLAUDE.md"] {
|
|
t.Errorf("%s: CLAUDE.md differs from the TypeScript's:\n--- go\n%s\n--- typescript\n%s", label, got["CLAUDE.md"], want["CLAUDE.md"])
|
|
}
|
|
for _, file := range []string{"managed-mcp.json", "managed-settings.json"} {
|
|
var a, b any
|
|
_ = json.Unmarshal([]byte(got[file]), &a)
|
|
_ = json.Unmarshal([]byte(want[file]), &b)
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Errorf("%s: %s means something else:\n--- go\n%s\n--- typescript\n%s", label, file, got[file], want[file])
|
|
}
|
|
}
|
|
}
|
|
same("with an API key", Render(f.Facts, f.Settings, &Binding{Licence: "api", Kind: "api-key"}, "/state/api-key-helper", f.Registered), f.WithKey)
|
|
same("plain", Render(f.Facts, Settings{}, nil, "/h", Servers{}), f.Plain)
|
|
}
|
|
|
|
func TestASettingCannotReplaceTheMeshsOwnEntryAndABadNameIsLeftOut(t *testing.T) {
|
|
out := Render(Facts{Node: "w", Console: "http://127.0.0.1:4270/mcp"},
|
|
Settings{MCPServers: map[string]map[string]any{"mesh": {"type": "http", "url": "http://evil"}, "bad name": {}}}, nil, "/h", nil)
|
|
var mcp struct {
|
|
MCPServers map[string]map[string]any `json:"mcpServers"`
|
|
}
|
|
_ = json.Unmarshal([]byte(out["managed-mcp.json"]), &mcp)
|
|
if mcp.MCPServers["mesh"]["url"] != "http://127.0.0.1:4270/mcp" || mcp.MCPServers["bad name"] != nil {
|
|
t.Fatalf("%v", mcp.MCPServers)
|
|
}
|
|
if !reflect.DeepEqual(Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil), Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil)) {
|
|
t.Fatal("rendering is not deterministic")
|
|
}
|
|
}
|
|
|
|
func TestTheOperatorsManagedSettingsAreLaidUnderTheMeshsOwnKeys(t *testing.T) {
|
|
settings := Settings{ManagedSettings: map[string]any{
|
|
"autoMode": map[string]any{"allow": []any{"merging an approved pull request"}},
|
|
"permissions": map[string]any{"deny": []any{"Bash(rm -rf /)"}},
|
|
"attribution": map[string]any{"commit": "made by a machine"},
|
|
"allowAllClaudeAiMcps": false,
|
|
"apiKeyHelper": "/somewhere/else",
|
|
}}
|
|
read := func(binding *Binding) map[string]any {
|
|
var m map[string]any
|
|
out := Render(Facts{Console: "x"}, settings, binding, "/h", nil)
|
|
if err := json.Unmarshal([]byte(out["managed-settings.json"]), &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m
|
|
}
|
|
m := read(nil)
|
|
if allow := m["autoMode"].(map[string]any)["allow"].([]any); len(allow) != 1 || allow[0] != "merging an approved pull request" {
|
|
t.Errorf("the operator's auto mode was not carried: %v", m["autoMode"])
|
|
}
|
|
if m["permissions"] == nil {
|
|
t.Errorf("the operator's permissions were not carried: %v", m)
|
|
}
|
|
if !reflect.DeepEqual(m["attribution"], map[string]any{"commit": "", "pr": ""}) || m["allowAllClaudeAiMcps"] != true {
|
|
t.Errorf("a setting replaced the mesh's own keys: %v", m)
|
|
}
|
|
if _, ok := m["apiKeyHelper"]; ok {
|
|
t.Errorf("a setting named a key-helper the licence did not: %v", m)
|
|
}
|
|
if helper := read(&Binding{Licence: "api", Kind: "api-key"})["apiKeyHelper"]; helper != "/h" {
|
|
t.Errorf("an API-key licence's key-helper was replaced: %v", helper)
|
|
}
|
|
}
|
|
|
|
// ---- the credentials file -----------------------------------------------------------------------------
|
|
|
|
func i64(v int64) *int64 { return &v }
|
|
|
|
func TestTheLineageRules(t *testing.T) {
|
|
const hour = 3_600_000
|
|
g := func(at string, exp int64, rtExp int64) Grant {
|
|
return Grant{AccessToken: at, ExpiresAt: exp, RefreshTokenExpiresAt: i64(rtExp)}
|
|
}
|
|
month := now + 30*24*hour
|
|
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + hour, RefreshTokenExpiresAt: i64(month)}, g("B", now+2*hour, month), false); !d.Apply {
|
|
t.Fatal("a newer rotation was refused")
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "new", ExpiresAt: now + 2*hour, RefreshTokenExpiresAt: i64(month)}, g("old", now+hour, month), false); d.Apply || d.Reason != "not-newer" {
|
|
t.Fatalf("a late older rotation: %+v", d)
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + 8*hour, RefreshTokenExpiresAt: i64(month)}, g("re", now+hour, now+5*24*hour), false); !d.Apply || !d.Reissued {
|
|
t.Fatalf("a re-issued grant: %+v", d)
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + 8*hour}, g("other", now+hour, month), true); !d.Apply {
|
|
t.Fatal("a switch was refused")
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "A"}, Grant{AccessToken: "A"}, true); d.Apply || d.Reason != "already-current" {
|
|
t.Fatalf("the same token: %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestALoginIsSeenAndStrippedWhenTheNodesOwnGrantIsWritten(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-login","refreshToken":"rt-login","expiresAt":1700000000000},"other":1}`)
|
|
login := ReadCredentials(p.credentials())
|
|
if !HoldsLogin(login) {
|
|
t.Fatal("a login was not seen")
|
|
}
|
|
if err := WriteCredentials(p.credentials(), WithGrant(login, Grant{AccessToken: "at-mesh", ExpiresAt: 1, Scopes: []string{"user:inference"}})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back := ReadCredentials(p.credentials())
|
|
raw, _ := os.ReadFile(p.credentials())
|
|
info, _ := os.Stat(p.credentials())
|
|
if HoldsLogin(back) || GrantOf(back).AccessToken != "at-mesh" || back["other"] == nil || strings.Contains(string(raw), "rt-login") || info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("written %s (mode %v)", raw, info.Mode())
|
|
}
|
|
}
|
|
|
|
func TestTheAccountIsReadFromTheAgentsStateFileAndNeverGuessed(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-1","emailAddress":"a@example.org"},"other":2}`)
|
|
if id := ReadIdentity(p.account()); id == nil || id.AccountUUID != "u-1" || id.EmailAddress != "a@example.org" {
|
|
t.Fatalf("%+v", id)
|
|
}
|
|
if ReadIdentity("/nonexistent/.claude.json") != nil {
|
|
t.Fatal("an identity from nothing")
|
|
}
|
|
writeFile(t, p.account(), `{}`)
|
|
if ReadIdentity(p.account()) != nil {
|
|
t.Fatal("an identity from an empty file")
|
|
}
|
|
}
|
|
|
|
// ---- the licence, ADR 0206 ----------------------------------------------------------------------------
|
|
|
|
func TestWhatANodeHoldsIsReportedWithFingerprintsAndItsAccountNeverAToken(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-secret","refreshToken":"rt-secret","expiresAt":2000,"refreshTokenExpiresAt":9000}}`)
|
|
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-1","emailAddress":"a@example.org"}}`)
|
|
h := HoldingsOf(p)
|
|
if h.Node != "laptop" || h.Identity.AccountUUID != "u-1" || *h.Kind != "subscription" || !h.Refresh.Present ||
|
|
!strings.HasPrefix(*h.Refresh.Fingerprint, "sha256:") || h.Access.ExpiresAt != 2000 || h.ChangedAt == nil {
|
|
t.Fatalf("%+v", h)
|
|
}
|
|
raw, _ := json.Marshal(h)
|
|
if strings.Contains(string(raw), "at-secret") || strings.Contains(string(raw), "rt-secret") {
|
|
t.Fatalf("a token is in the report: %s", raw)
|
|
}
|
|
var keys map[string]any
|
|
_ = json.Unmarshal(raw, &keys)
|
|
for k := range keys {
|
|
if strings.Contains(strings.ToLower(k), "token") || strings.Contains(strings.ToLower(k), "secret") {
|
|
t.Fatalf("a field the runtime would refuse: %s", k)
|
|
}
|
|
}
|
|
// The manager reads exactly this shape.
|
|
if _, err := time.Parse(time.RFC3339Nano, *h.ChangedAt); err != nil {
|
|
t.Fatalf("the manager cannot read the report's time: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheGrantAnswersOnlyAWaitingLoginSealedToTheManagersKey(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
manager, _ := GenerateKeyPair()
|
|
if a, _ := GrantFor(p, manager.PublicKey); a.Sealed != nil || a.Waiting == nil || *a.Waiting {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at","refreshToken":"rt-login","expiresAt":1}}`)
|
|
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-9"}}`)
|
|
a, err := GrantFor(p, manager.PublicKey)
|
|
if err != nil || a.Identity.AccountUUID != "u-9" {
|
|
t.Fatalf("%+v %v", a, err)
|
|
}
|
|
plain, _ := Open(*a.Sealed, manager.PrivateKey)
|
|
if !strings.Contains(plain, `"refreshToken":"rt-login"`) {
|
|
t.Fatalf("opened %s", plain)
|
|
}
|
|
raw, _ := json.Marshal(a)
|
|
if strings.Contains(string(raw), "rt-login") {
|
|
t.Fatal("the refresh token crossed in the clear")
|
|
}
|
|
}
|
|
|
|
// seat answers `current` as the manager does: the grant sealed to the key the node sent.
|
|
func seat(t *testing.T, licence, token string, gen int64, asked *[]string) Ask {
|
|
return func(address string, args any) (json.RawMessage, error) {
|
|
*asked = append(*asked, address)
|
|
key := args.(map[string]any)["public_key"].(string)
|
|
g, _ := json.Marshal(Grant{AccessToken: token, ExpiresAt: now + 3_600_000})
|
|
box, err := Seal(string(g), key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return json.Marshal(Current{Licence: licence, Kind: "subscription", Generation: gen, Sealed: &box})
|
|
}
|
|
}
|
|
|
|
func TestANewerGenerationFetchesTheTokenOnceByTheSeatsVerb(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
var asked []string
|
|
ask := seat(t, "personal", "at-1", 3, &asked)
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked) != 1 || asked[0] != "seat:anthropic-licence-manager.current" || creds(t, p)["accessToken"] != "at-1" {
|
|
t.Fatalf("asked %v, credentials %v", asked, creds(t, p))
|
|
}
|
|
if done, _ := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); done != "" || len(asked) != 1 {
|
|
t.Fatal("an equal generation asked again")
|
|
}
|
|
if HoldingsOf(p).Generation != 3 || w["managed-mcp.json"] == "" {
|
|
t.Fatal("the generation or the managed files were not written")
|
|
}
|
|
}
|
|
|
|
func TestTheTokenANodeIsHandedReplacesALoginsGrantAndLeavesNoRefreshToken(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-old","refreshToken":"rt-spent","expiresAt":`+
|
|
strings.TrimSpace(string(mustJSON(now+7_200_000)))+`}}`)
|
|
var asked []string
|
|
out, err := Pull(p, seat(t, "personal", "at-new", 1, &asked), writer(w))
|
|
if err != nil || out["applied"] != true {
|
|
t.Fatalf("%v %v", out, err)
|
|
}
|
|
c := creds(t, p)
|
|
if c["accessToken"] != "at-new" || c["refreshToken"] != nil || HoldingsOf(p).Refresh.Present {
|
|
t.Fatalf("%v", c)
|
|
}
|
|
}
|
|
|
|
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
|
|
|
|
// ---- MCP servers in state, ADR 0201 -------------------------------------------------------------------
|
|
|
|
// bus is the `servers` state as every node in a test shares it, with each node's watch.
|
|
type bus struct {
|
|
kept map[string]map[string]any
|
|
watchers []func(ServerChange)
|
|
}
|
|
|
|
func (b *bus) Put(key string, value any) error {
|
|
v := value.(map[string]any)
|
|
b.kept[key] = v
|
|
for _, w := range b.watchers {
|
|
w(ServerChange{Key: key, Op: "put", Value: v})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *bus) Delete(key string) error {
|
|
delete(b.kept, key)
|
|
for _, w := range b.watchers {
|
|
w(ServerChange{Key: key, Op: "delete"})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *bus) Keys() ([]string, error) {
|
|
var out []string
|
|
for k := range b.kept {
|
|
out = append(out, k)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// join is a node joining: its view takes the current state, then every change.
|
|
func (b *bus) join(p Paths, w map[string]string) *ServerView {
|
|
v := NewServerView(p)
|
|
for k, val := range b.kept {
|
|
_, _ = OnServerChange(v, ServerChange{Key: k, Op: "put", Value: val}, p, writer(w))
|
|
}
|
|
b.watchers = append(b.watchers, func(c ServerChange) { _, _ = OnServerChange(v, c, p, writer(w)) })
|
|
return v
|
|
}
|
|
|
|
func noOthers() ([]string, error) { return nil, nil }
|
|
|
|
func TestRegisteringHerePutsItUnderThisNodesKeyAndAsksAboutTheOthers(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
v := b.join(p, w)
|
|
r, err := RegisterServer(p, Registration{Name: "search", Entry: map[string]any{"type": "http", "url": "https://s.example/mcp"}}, b, v, writer(w),
|
|
func() ([]string, error) { return []string{"laptop", "server", "desktop"}, nil })
|
|
if err != nil || r["here"] != "changed" || !strings.Contains(r["also"].(string), "server, desktop") || b.kept["laptop.search"] == nil {
|
|
t.Fatalf("%v %v %v", r, err, b.kept)
|
|
}
|
|
if !strings.Contains(w["managed-mcp.json"], `"search"`) {
|
|
t.Fatal("not rendered")
|
|
}
|
|
}
|
|
|
|
func TestEveryNodeRegistrationReachesTheOthersAndALateNodeReadsIt(t *testing.T) {
|
|
a, wa := node(t, "laptop")
|
|
s, ws := node(t, "server")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
va := b.join(a, wa)
|
|
b.join(s, ws)
|
|
_, _ = RegisterServer(a, Registration{Name: "docs", Entry: map[string]any{"type": "stdio", "command": "docs-mcp"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
|
if Registered(s)["docs"] == nil || !strings.Contains(ws["managed-mcp.json"], "docs-mcp") {
|
|
t.Fatalf("the other node did not take it: %v", Registered(s))
|
|
}
|
|
late, wl := node(t, "desktop")
|
|
b.join(late, wl)
|
|
if Registered(late)["docs"] == nil {
|
|
t.Fatal("a node joining later did not read the current set")
|
|
}
|
|
_, _ = RegisterServer(a, Registration{Name: "docs", Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
|
if Registered(s)["docs"] != nil || Registered(late)["docs"] != nil {
|
|
t.Fatal("an unregistration did not reach every node")
|
|
}
|
|
}
|
|
|
|
func TestANodesOwnRegistrationOverridesTheOneForEveryNode(t *testing.T) {
|
|
a, wa := node(t, "laptop")
|
|
s, ws := node(t, "server")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
va := b.join(a, wa)
|
|
b.join(s, ws)
|
|
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://all"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
|
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://laptop"}}, b, va, writer(wa), noOthers)
|
|
if Registered(a)["x"]["url"] != "https://laptop" || Registered(s)["x"]["url"] != "https://all" {
|
|
t.Fatalf("%v %v", Registered(a), Registered(s))
|
|
}
|
|
r, _ := RegisterServer(a, Registration{Name: "x"}, b, va, writer(wa), noOthers)
|
|
if !strings.Contains(r["still"].(string), "still applies here") || Registered(a)["x"]["url"] != "https://all" {
|
|
t.Fatalf("%v", r)
|
|
}
|
|
}
|
|
|
|
func TestABadEntryIsRefusedBeforeAnythingIsPut(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
v := b.join(p, w)
|
|
r, _ := RegisterServer(p, Registration{Name: "mesh", Entry: map[string]any{"type": "http", "url": "https://x"}}, b, v, writer(w), noOthers)
|
|
if r["registered"] != false || len(b.kept) != 0 {
|
|
t.Fatalf("%v %v", r, b.kept)
|
|
}
|
|
if done, _ := OnServerChange(v, ServerChange{Key: "server.b", Op: "put", Value: map[string]any{"type": "http", "url": "https://b"}}, p, writer(w)); done != "" {
|
|
t.Fatal("another node's key changed this one")
|
|
}
|
|
}
|
|
|
|
// An API key goes to the manager sealed to its key, never in the clear, and its file is gone afterwards.
|
|
func TestAnAPIKeyIsHandedOverSealedAndItsFileRemoved(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
manager, _ := GenerateKeyPair()
|
|
file := filepath.Join(p.Home, "api-key")
|
|
writeFile(t, file, "sk-ant-api03-secret\n")
|
|
var sent []string
|
|
ask := func(address string, args any) (json.RawMessage, error) {
|
|
raw, _ := json.Marshal(args)
|
|
sent = append(sent, address+" "+string(raw))
|
|
switch address {
|
|
case "seat:anthropic-licence-manager.public-key":
|
|
return json.Marshal(map[string]any{"public_key": manager.PublicKey})
|
|
case "seat:anthropic-licence-manager.adopt":
|
|
box := args.(map[string]any)["sealed"].(SealedBox)
|
|
if key, err := Open(box, manager.PrivateKey); err != nil || key != "sk-ant-api03-secret" {
|
|
t.Fatalf("the manager opened %q, %v", key, err)
|
|
}
|
|
return json.Marshal(map[string]any{"adopted": true})
|
|
case "seat:anthropic-licence-manager.switch":
|
|
return json.Marshal(map[string]any{"licence": "api"})
|
|
}
|
|
t.Fatalf("asked %s", address)
|
|
return nil, nil
|
|
}
|
|
out, err := AddAPIKey(p, "api", file, true, ask)
|
|
if err != nil || out["file"] != "removed" || out["switched"] == nil {
|
|
t.Fatalf("%v %v", out, err)
|
|
}
|
|
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
|
t.Fatal("the key file is still there")
|
|
}
|
|
if strings.Contains(strings.Join(sent, "\n"), "sk-ant") {
|
|
t.Fatalf("the key crossed in the clear: %v", sent)
|
|
}
|
|
}
|
|
|
|
// A manager whose store was rebuilt counts generations from one again (novox/hq issue 243): a binding with a
|
|
// lower generation than the one applied is still a binding to apply, and only the one applied is skipped.
|
|
func TestALowerGenerationAfterTheManagerWasRebuiltIsStillApplied(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
var asked []string
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 16},
|
|
seat(t, "personal", "at-old", 16, &asked), writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The rotation that came with it: a later token, as a refresh hands one over.
|
|
later := func(address string, args any) (json.RawMessage, error) {
|
|
asked = append(asked, address)
|
|
g, _ := json.Marshal(Grant{AccessToken: "at-new", ExpiresAt: now + 7_200_000})
|
|
box, err := Seal(string(g), args.(map[string]any)["public_key"].(string))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return json.Marshal(Current{Licence: "personal", Kind: "subscription", Generation: 3, Sealed: &box})
|
|
}
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3},
|
|
later, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked) != 2 || creds(t, p)["accessToken"] != "at-new" || HoldingsOf(p).Generation != 3 {
|
|
t.Fatalf("asked %v, credentials %v: a lower generation was ignored", asked, creds(t, p))
|
|
}
|
|
}
|