clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / internal / daemon / grant_admission_test.go
grant_admission_test.go 7.0K
1package daemon23import (4 "context"5 "encoding/json"6 "strconv"7 "strings"8 "testing"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/community"12 "github.com/FelineStateMachine/tinyrelay/internal/event"13)1415func grantRequestEvent(t *testing.T, secret string, grant event.Event, at int64, content, grantJSON string, tags ...[]string) event.Event {16 t.Helper()17 base := [][]string{{"request", "grant"}, {"grant", grantJSON}, {"E", grant.ID, "", grant.PubKey}, {"K", "30392"}, {"P", grant.PubKey}, {"e", grant.ID, "", grant.PubKey}, {"k", "30392"}, {"p", grant.PubKey}}18 return signedEvent(t, secret, 1111, at, append(base, tags...), content)19}2021func grantRequestJSON(t *testing.T, grant event.Event, changes community.AgentGrantChanges) string {22 t.Helper()23 raw, err := json.Marshal(community.AgentGrantRequest{Base: grant.ID, Changes: changes})24 if err != nil {25 t.Fatal(err)26 }27 return string(raw)28}2930func setGrantTag(t *testing.T, e *event.Event, name, value string) {31 t.Helper()32 for _, tag := range e.Tags {33 if len(tag) > 1 && tag[0] == name {34 tag[1] = value35 return36 }37 }38 t.Fatalf("grant replacement has no %s tag", name)39}4041func TestGrantRequestAdmissionRequiresAgentAndValidCurrentGrant(t *testing.T) {42 _, tenant := testTenant(t)43 now := time.Now().Unix()44 agent, _ := event.PublicKey(testAgentSecret)45 member, _ := event.PublicKey(testMemberSecret)46 grant := agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"}, []string{"k", "1111"})47 if err := publishAs(t, tenant, grant); err != nil {48 t.Fatal(err)49 }50 reqJSON := grantRequestJSON(t, grant, community.AgentGrantChanges{Kinds: []int{11}})51 human := grantRequestEvent(t, testMemberSecret, grant, now, "please", reqJSON)52 if err := publishAs(t, tenant, human); err == nil {53 t.Fatal("human request accepted")54 }5556 for name, mutate := range map[string]func(event.Event) event.Event{57 "rationale over limit": func(e event.Event) event.Event { e.Content = strings.Repeat("x", 501); return e },58 "rationale whitespace": func(e event.Event) event.Event { e.Content = " \t\n "; return e },59 "duplicate operator": func(e event.Event) event.Event { e.Tags = append(e.Tags, []string{"p", grant.PubKey}); return e },60 "wrong operator": func(e event.Event) event.Event { e.Tags[4][1] = member; e.Tags[7][1] = member; return e },61 "wrong base": func(e event.Event) event.Event {62 e.Tags[2][1] = strings.Repeat("f", 64)63 e.Tags[5][1] = strings.Repeat("f", 64)64 return e65 },66 "unknown change": func(e event.Event) event.Event {67 e.Tags[1][1] = `{"base":"` + grant.ID + `","changes":{"nope":true}}`68 return e69 },70 "noop": func(e event.Event) event.Event {71 e.Tags[1][1] = grantRequestJSON(t, grant, community.AgentGrantChanges{Kinds: []int{9}})72 return e73 },74 } {75 t.Run(name, func(t *testing.T) {76 e := grantRequestEvent(t, testAgentSecret, grant, now+1, "please", reqJSON)77 e = mutate(e)78 if err := event.Sign(&e, testAgentSecret); err != nil {79 t.Fatal(err)80 }81 if err := publishAs(t, tenant, e); err == nil {82 t.Fatalf("malformed request accepted: %v", e)83 }84 })85 }86}8788func TestGrantRequestAdmissionRejectsPausedAndRevokedAgents(t *testing.T) {89 for _, action := range []string{"pauseagent", "revokeagent"} {90 t.Run(action, func(t *testing.T) {91 _, tenant := testTenant(t)92 now := time.Now().Unix()93 agent, _ := event.PublicKey(testAgentSecret)94 grant := agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"})95 if err := publishAs(t, tenant, grant); err != nil {96 t.Fatal(err)97 }98 args := []json.RawMessage{json.RawMessage(`"` + agent + `"`)}99 if _, err := tenant.Execute(context.Background(), tenant.Policy().Owner, action, args); err != nil {100 t.Fatal(err)101 }102 req := grantRequestEvent(t, testAgentSecret, grant, now+1, "please", grantRequestJSON(t, grant, community.AgentGrantChanges{Kinds: []int{11}}))103 if err := publishAs(t, tenant, req); err == nil {104 t.Fatalf("request accepted after %s", action)105 }106 })107 }108}109110func TestGrantRequestReplacementMustMatchPreparedReview(t *testing.T) {111 mutations := map[string]func(*event.Event){112 "expiry": func(e *event.Event) { setGrantTag(t, e, "expiration", "9999999999") },113 "name": func(e *event.Event) { setGrantTag(t, e, "name", "changed") },114 "content": func(e *event.Event) { e.Content = "changed" },115 "tags": func(e *event.Event) { e.Tags = append(e.Tags, []string{"note", "changed"}) },116 }117 for name, mutate := range mutations {118 t.Run(name, func(t *testing.T) {119 _, tenant := testTenant(t)120 now := time.Now().Unix()121 agent, _ := event.PublicKey(testAgentSecret)122 grant := agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"})123 if err := publishAs(t, tenant, grant); err != nil {124 t.Fatal(err)125 }126 req := grantRequestEvent(t, testAgentSecret, grant, now, "please", grantRequestJSON(t, grant, community.AgentGrantChanges{Kinds: []int{11}}))127 if err := publishAs(t, tenant, req); err != nil {128 t.Fatal(err)129 }130 review, err := tenant.community.ReviewAgentGrantRequest(context.Background(), req, now)131 if err != nil {132 t.Fatal(err)133 }134 replacement := review.Unsigned135 mutate(&replacement)136 if err := event.Sign(&replacement, testOwnerSecret); err != nil {137 t.Fatal(err)138 }139 if err := publishAs(t, tenant, replacement); err == nil {140 t.Fatalf("mutated %s replacement accepted", name)141 }142 })143 }144}145146func TestGrantRequestPreparedReplacementGoesStale(t *testing.T) {147 for _, scenario := range []string{"pause", "revoke", "deny", "expire"} {148 t.Run(scenario, func(t *testing.T) {149 _, tenant := testTenant(t)150 now := time.Now().Unix()151 agent, _ := event.PublicKey(testAgentSecret)152 grant := agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"})153 if err := publishAs(t, tenant, grant); err != nil {154 t.Fatal(err)155 }156 tags := grantRequestJSON(t, grant, community.AgentGrantChanges{Kinds: []int{11}})157 requestTags := [][]string(nil)158 if scenario == "expire" {159 requestTags = [][]string{{"expiration", strconv.FormatInt(now+1, 10)}}160 }161 request := grantRequestEvent(t, testAgentSecret, grant, now, "please", tags, requestTags...)162 if err := publishAs(t, tenant, request); err != nil {163 t.Fatal(err)164 }165 review, err := tenant.community.ReviewAgentGrantRequest(context.Background(), request, now)166 if err != nil {167 t.Fatal(err)168 }169 replacement := review.Unsigned170 if scenario == "deny" {171 if err := publishAs(t, tenant, signedEvent(t, testOwnerSecret, 7, now+1, [][]string{{"e", request.ID}, {"p", agent}}, "-")); err != nil {172 t.Fatal(err)173 }174 } else if scenario != "expire" {175 action := "pauseagent"176 if scenario == "revoke" {177 action = "revokeagent"178 }179 if _, err := tenant.Execute(context.Background(), tenant.Policy().Owner, action, []json.RawMessage{json.RawMessage(`"` + agent + `"`)}); err != nil {180 t.Fatal(err)181 }182 }183 if err := event.Sign(&replacement, testOwnerSecret); err != nil {184 t.Fatal(err)185 }186 if scenario == "expire" {187 time.Sleep(2 * time.Second)188 }189 if err := publishAs(t, tenant, replacement); err == nil {190 t.Fatalf("stale %s replacement accepted", scenario)191 }192 })193 }194}