clone

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

main
tinyrelay / internal / daemon / grant_approvals_test.go
grant_approvals_test.go 7.1K
1package daemon23import (4 "context"5 "encoding/json"6 "strings"7 "testing"8 "time"910 "github.com/FelineStateMachine/tinyrelay/internal/community"11 "github.com/FelineStateMachine/tinyrelay/internal/event"12)1314func approvalGrantRequest(t *testing.T, grant event.Event, createdAt int64) event.Event {15 t.Helper()16 raw, err := json.Marshal(community.AgentGrantRequest{Base: grant.ID, Changes: community.AgentGrantChanges{Kinds: []int{30617, 30618}, Repos: []community.AgentRepo{{Owner: event.Tag(grant, "d"), Identifier: "seedmark", Level: "maintain"}}}})17 if err != nil {18 t.Fatal(err)19 }20 return signedEvent(t, testAgentSecret, 1111, createdAt, [][]string{{"request", "grant"}, {"grant", string(raw)}, {"E", grant.ID, "", grant.PubKey}, {"K", "30392"}, {"P", grant.PubKey}, {"e", grant.ID, "", grant.PubKey}, {"k", "30392"}, {"p", grant.PubKey}}, "Publish the Seedmark repository and its hosting manifest.")21}2223func TestGrantApprovalAppliesAndRetainsDecision(t *testing.T) {24 _, tenant := testTenant(t)25 ctx := context.Background()26 now := time.Now().Unix()27 agent, _ := event.PublicKey(testAgentSecret)28 grant := agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"}, []string{"room", tenant.meta.Name}, []string{"sites", "*"})29 if err := publishAs(t, tenant, grant); err != nil {30 t.Fatal(err)31 }32 request := approvalGrantRequest(t, grant, now-20)33 if err := publishAs(t, tenant, request); err != nil {34 t.Fatalf("request with no comment scope: %v", err)35 }36 detail, err := approvalCall(t, tenant, grant.PubKey, "browseapproval", map[string]any{"id": request.ID})37 if err != nil {38 t.Fatal(err)39 }40 item := detail["item"].(map[string]any)41 if item["type"] != "grant" || item["state"] != "open" || item["grant"] == nil {42 t.Fatalf("missing actionable review: %v", item)43 }44 for _, actor := range []string{agent, grant.PubKey} {45 if _, err := approvalCall(t, tenant, actor, "browseapproval", map[string]any{"id": request.ID}); err != nil {46 t.Fatalf("participant cannot read review: %v", err)47 }48 }49 if _, err := approvalCall(t, tenant, wikiKey(t, approvalOtherSecret), "browseapproval", map[string]any{"id": request.ID}); err == nil {50 t.Fatal("stranger read grant request")51 }52 // A reaction alone must never make the UI claim access was granted.53 positive := signedEvent(t, testOwnerSecret, 7, now-10, [][]string{{"e", request.ID}, {"p", agent}}, "+")54 if err := publishAs(t, tenant, positive); err != nil {55 t.Fatal(err)56 }57 detail, err = approvalCall(t, tenant, grant.PubKey, "browseapproval", map[string]any{"id": request.ID})58 if err != nil || detail["item"].(map[string]any)["state"] != "open" {59 t.Fatalf("reaction settled an unapplied grant: %v %v", detail, err)60 }61 review, err := tenant.community.ReviewAgentGrantRequest(ctx, request, now)62 if err != nil {63 t.Fatal(err)64 }65 replacement := review.Unsigned66 if err := event.Sign(&replacement, testOwnerSecret); err != nil {67 t.Fatal(err)68 }69 if err := publishAs(t, tenant, replacement); err != nil {70 t.Fatalf("reviewed replacement rejected: %v", err)71 }72 current, found, err := tenant.community.AgentGrant(ctx, agent)73 if err != nil || !found || !containsInt(current.Scope.Kinds, 30617) || len(current.Scope.Sites) != 1 {74 t.Fatalf("grant not applied or existing site lost: %+v %v", current, err)75 }76 // A later ordinary edit must not erase the approval's receipt.77 manual := agentGrantEvent(t, agent, now+1, now+3600, []string{"k", "9"})78 if err := publishAs(t, tenant, manual); err != nil {79 t.Fatal(err)80 }81 for _, method := range []string{"browseapproval", "browseapprovals"} {82 result, err := approvalCall(t, tenant, grant.PubKey, method, map[string]any{"id": request.ID})83 if err != nil {84 t.Fatal(err)85 }86 got := result["item"]87 if method == "browseapprovals" {88 got = approvalByID(wikiItems(result, "items"), request.ID)89 }90 item := got.(map[string]any)91 answer := item["answer"].(map[string]any)92 if item["state"] != "answered" || answer["decision"] != "approved" || answer["id"] != replacement.ID {93 t.Fatalf("lost grant approval history: %v", item)94 }95 }96}9798func TestGrantApprovalDenialAndStaleReview(t *testing.T) {99 for _, scenario := range []string{"deny", "supersede"} {100 t.Run(scenario, func(t *testing.T) {101 _, tenant := testTenant(t)102 now := time.Now().Unix()103 agent, _ := event.PublicKey(testAgentSecret)104 grant := agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"})105 if err := publishAs(t, tenant, grant); err != nil {106 t.Fatal(err)107 }108 request := approvalGrantRequest(t, grant, now-20)109 if err := publishAs(t, tenant, request); err != nil {110 t.Fatal(err)111 }112 review, err := tenant.community.ReviewAgentGrantRequest(context.Background(), request, now)113 if err != nil {114 t.Fatal(err)115 }116 if scenario == "deny" {117 if err := publishAs(t, tenant, signedEvent(t, testOwnerSecret, 7, now-10, [][]string{{"e", request.ID}, {"p", agent}}, "\n-\t")); err != nil {118 t.Fatal(err)119 }120 } else if err := publishAs(t, tenant, agentGrantEvent(t, agent, now-5, now+3600, []string{"k", "1"})); err != nil {121 t.Fatal(err)122 }123 replacement := review.Unsigned124 if err := event.Sign(&replacement, testOwnerSecret); err != nil {125 t.Fatal(err)126 }127 if err := publishAs(t, tenant, replacement); err == nil {128 t.Fatal("obsolete request changed the grant")129 }130 detail, err := approvalCall(t, tenant, grant.PubKey, "browseapproval", map[string]any{"id": request.ID})131 if err != nil {132 t.Fatal(err)133 }134 item := detail["item"].(map[string]any)135 if item["grant"] != nil {136 t.Fatalf("inactive request still actionable: %v", item)137 }138 if scenario == "deny" && (item["state"] != "answered" || item["answer"].(map[string]any)["decision"] != "denied") {139 t.Fatalf("denial not shown: %v", item)140 }141 if scenario == "supersede" && (item["state"] != "expired" || !strings.HasPrefix(item["grant_error"].(string), "conflict:")) {142 t.Fatalf("stale review not marked: %v", item)143 }144 })145 }146}147148func TestGrantRequestNotificationRequiresReview(t *testing.T) {149 _, tenant := testTenant(t)150 now := time.Now().Unix()151 agent, _ := event.PublicKey(testAgentSecret)152 request := approvalGrantRequest(t, agentGrantEvent(t, agent, now-30, now+3600, []string{"k", "9"}), now-20)153 notices := tenant.pushNotices(context.Background(), request)154 if len(notices) != 1 || notices[0].recipient != tenant.Policy().Owner || notices[0].category != pushApprovals || len(notices[0].actions) != 1 || notices[0].actions[0].Action != "review" || !strings.Contains(notices[0].url, "/approvals/"+request.ID) {155 t.Fatalf("grant request notification: %+v", notices)156 }157}158159func TestGrantReviewReplacementAdvancesTimestamp(t *testing.T) {160 _, tenant := testTenant(t)161 now := time.Now().Unix()162 agent, _ := event.PublicKey(testAgentSecret)163 grant := agentGrantEvent(t, agent, now, now+3600, []string{"k", "9"})164 if err := publishAs(t, tenant, grant); err != nil {165 t.Fatal(err)166 }167 request := approvalGrantRequest(t, grant, now)168 review, err := tenant.community.ReviewAgentGrantRequest(context.Background(), request, now)169 if err != nil {170 t.Fatal(err)171 }172 if review.Unsigned.CreatedAt <= grant.CreatedAt {173 t.Fatal("replacement can lose the same-second event ordering tie")174 }175}