tinyrelayfile
tinyrelay/internal/gates/agents_test.go/

internal/gates/agents_test.go raw
1package gates
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "strconv"
8 "strings"
9 "testing"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/community"
12 "github.com/FelineStateMachine/tinyrelay/internal/event"
13 "github.com/FelineStateMachine/tinyrelay/internal/policy"
14 "github.com/FelineStateMachine/tinyrelay/internal/relay"
15 "github.com/FelineStateMachine/tinyrelay/internal/sites"
16 "github.com/FelineStateMachine/tinyrelay/internal/storage"
17)
18
19const (
20 agentOwnerSecret = "0000000000000000000000000000000000000000000000000000000000000001"
21 agentSecret = "0000000000000000000000000000000000000000000000000000000000000002"
22 agentOtherSecret = "0000000000000000000000000000000000000000000000000000000000000003"
23 agentNow = int64(1_700_000_000)
24)
25
26type agentFixture struct {
27 gate *Gate
28 community *community.Service
29 store *storage.Store
30 owner string
31 agent string
32}
33
34func newAgentFixture(t *testing.T) agentFixture {
35 t.Helper()
36 ctx := context.Background()
37 store, err := storage.Open(ctx, t.TempDir()+"/relay.db")
38 if err != nil {
39 t.Fatal(err)
40 }
41 t.Cleanup(func() { _ = store.Close() })
42 owner, _ := event.PublicKey(agentOwnerSecret)
43 agent, _ := event.PublicKey(agentSecret)
44 svc, err := community.New(ctx, store, owner)
45 if err != nil {
46 t.Fatal(err)
47 }
48 p := policy.Defaults(owner)
49 gate, err := New(Config{Store: store, Community: svc, Policy: func() policy.Policy { return p }, Slug: "general"})
50 if err != nil {
51 t.Fatal(err)
52 }
53 return agentFixture{gate: gate, community: svc, store: store, owner: owner, agent: agent}
54}
55
56func signed(t *testing.T, secret string, kind int, createdAt int64, tags [][]string, content string) event.Event {
57 t.Helper()
58 if tags == nil {
59 tags = [][]string{}
60 }
61 e := event.Event{Kind: kind, CreatedAt: createdAt, Tags: tags, Content: content}
62 if err := event.Sign(&e, secret); err != nil {
63 t.Fatal(err)
64 }
65 return e
66}
67
68func grantTags(agent string, expires int64, extra ...[]string) [][]string {
69 tags := [][]string{{"d", agent}, {"p", agent}, {"name", "helper"}, {"expiration", strconv.FormatInt(expires, 10)}}
70 return append(tags, extra...)
71}
72
73// grant publishes a grant through the gate and mirrors it as the tenant
74// would. Later grants for the same agent need a later createdAt to replace
75// the earlier one.
76func (f agentFixture) grant(t *testing.T, createdAt int64, tags [][]string) event.Event {
77 t.Helper()
78 ctx := context.Background()
79 e := signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, createdAt, tags, "")
80 if err := f.gate.Write(ctx, e, relay.Session{PubKeys: []string{f.owner}}, agentNow); err != nil {
81 t.Fatalf("grant rejected: %v", err)
82 }
83 if err := f.store.WithTx(ctx, func(tx *sql.Tx) error {
84 if _, err := storage.SaveTx(ctx, tx, e, storage.SaveOptions{Now: agentNow}); err != nil {
85 return err
86 }
87 return f.community.ApplyAgentEventTx(ctx, tx, e, agentNow)
88 }); err != nil {
89 t.Fatal(err)
90 }
91 return e
92}
93
94func expectRestricted(t *testing.T, err error, fragment string) {
95 t.Helper()
96 if err == nil || !strings.HasPrefix(err.Error(), "restricted: agent grant") || !strings.Contains(err.Error(), fragment) {
97 t.Fatalf("want restricted agent grant error containing %q, got %v", fragment, err)
98 }
99}
100
101func TestAgentGrantScopesKindsRoomsAndRepositories(t *testing.T) {
102 f := newAgentFixture(t)
103 ctx := context.Background()
104 repo := f.owner + ":project"
105 f.grant(t, agentNow, grantTags(f.agent, agentNow+3600, []string{"room", "general"}, []string{"repo", repo + ":read"}, []string{"k", "1"}, []string{"k", "1621"}, []string{"k", "1630"}, []string{"k", "1111"}))
106 if role, _ := f.community.Role(ctx, f.agent); role != "agent" {
107 t.Fatalf("role = %q, want agent", role)
108 }
109 session := relay.Session{PubKeys: []string{f.agent}}
110 if err := f.gate.Write(ctx, signed(t, agentSecret, 1, agentNow, nil, "hello"), session, agentNow); err != nil {
111 t.Fatalf("allowed kind rejected: %v", err)
112 }
113 if err := f.gate.Write(ctx, signed(t, agentSecret, 0, agentNow, nil, "{}"), session, agentNow); err != nil {
114 t.Fatalf("profile rejected: %v", err)
115 }
116 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 7, agentNow, nil, "+"), session, agentNow), "kind 7")
117 if err := f.gate.Write(ctx, signed(t, agentSecret, 1, agentNow, [][]string{{"h", "general"}}, "in room"), session, agentNow); err != nil {
118 t.Fatalf("allowed room rejected: %v", err)
119 }
120 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 1, agentNow, [][]string{{"h", "other"}}, "wrong room"), session, agentNow), "room other")
121 address := "30617:" + repo
122 if err := f.gate.Write(ctx, signed(t, agentSecret, 1621, agentNow, [][]string{{"a", address}}, "issue"), session, agentNow); err != nil {
123 t.Fatalf("issue in readable repository rejected: %v", err)
124 }
125 if err := f.gate.Write(ctx, signed(t, agentSecret, 1111, agentNow, [][]string{{"A", address}}, "comment"), session, agentNow); err != nil {
126 t.Fatalf("comment in readable repository rejected: %v", err)
127 }
128 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 1630, agentNow, [][]string{{"a", address}}, "status"), session, agentNow), "status changes")
129 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 1621, agentNow, [][]string{{"a", "30617:" + f.owner + ":elsewhere"}}, "issue"), session, agentNow), "repository")
130 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 1621, agentNow, nil, "issue"), session, agentNow), "repository address")
131 f.grant(t, agentNow+1, grantTags(f.agent, agentNow+3600, []string{"repo", repo + ":maintain"}, []string{"k", "1630"}))
132 if err := f.gate.Write(ctx, signed(t, agentSecret, 1630, agentNow, [][]string{{"a", address}}, "status"), session, agentNow); err != nil {
133 t.Fatalf("maintainer status rejected: %v", err)
134 }
135 // Propose publishes the same kinds as read; status changes still need
136 // maintain.
137 f.grant(t, agentNow+2, grantTags(f.agent, agentNow+3600, []string{"repo", repo + ":propose"}, []string{"k", "1621"}, []string{"k", "1618"}, []string{"k", "1617"}, []string{"k", "1111"}, []string{"k", "1630"}))
138 for _, kind := range []int{1621, 1618, 1617, 1111} {
139 if err := f.gate.Write(ctx, signed(t, agentSecret, kind, agentNow, [][]string{{"a", address}}, "proposal"), session, agentNow); err != nil {
140 t.Fatalf("kind %d in proposable repository rejected: %v", kind, err)
141 }
142 }
143 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 1630, agentNow, [][]string{{"a", address}}, "status"), session, agentNow), "status changes")
144 if _, err := community.ParseAgentGrant(signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, agentNow, grantTags(f.agent, agentNow+3600, []string{"repo", repo + ":admin"}), ""), agentNow); err == nil || !strings.Contains(err.Error(), ":propose, :read or :maintain") {
145 t.Fatalf("unknown level accepted: %v", err)
146 }
147 // Wiki access carries the wiki kinds without listing them.
148 f.grant(t, agentNow+3, grantTags(f.agent, agentNow+3600, []string{"wiki", "propose"}))
149 for _, kind := range []int{event.KIND_WIKI_ARTICLE, event.KIND_WIKI_MERGE} {
150 if err := f.gate.Write(ctx, signed(t, agentSecret, kind, agentNow, [][]string{{"d", "notes"}, {"title", "Notes"}, {"a", "30818:" + f.owner + ":notes"}, {"p", f.owner}, {"e", strings.Repeat("e", 64), "", "source"}}, "wiki"), session, agentNow); err != nil {
151 t.Fatalf("wiki kind %d rejected for propose: %v", kind, err)
152 }
153 }
154 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, event.KIND_WIKI_REDIRECT, agentNow, [][]string{{"d", "notes"}}, ""), session, agentNow), "kind 30819")
155 f.grant(t, agentNow+4, grantTags(f.agent, agentNow+3600, []string{"wiki", "edit"}))
156 if err := f.gate.Write(ctx, signed(t, agentSecret, event.KIND_WIKI_REDIRECT, agentNow, [][]string{{"d", "notes"}, {"redirect", "30818:" + f.owner + ":notes"}}, ""), session, agentNow); err != nil {
157 t.Fatalf("redirect rejected for edit: %v", err)
158 }
159}
160
161func TestAgentGrantStateRejectsWrites(t *testing.T) {
162 f := newAgentFixture(t)
163 ctx := context.Background()
164 session := relay.Session{PubKeys: []string{f.agent}}
165 note := signed(t, agentSecret, 1, agentNow, nil, "hello")
166 f.grant(t, agentNow, grantTags(f.agent, agentNow+60, []string{"k", "1"}))
167 if err := f.gate.Write(ctx, note, session, agentNow); err != nil {
168 t.Fatalf("active grant rejected: %v", err)
169 }
170 expectRestricted(t, f.gate.Write(ctx, note, session, agentNow+61), "expired")
171 if _, err := f.community.SetAgentPaused(ctx, f.owner, f.agent, true); err != nil {
172 t.Fatal(err)
173 }
174 expectRestricted(t, f.gate.Write(ctx, note, session, agentNow), "paused")
175 if _, err := f.community.SetAgentPaused(ctx, f.owner, f.agent, false); err != nil {
176 t.Fatal(err)
177 }
178 if err := f.gate.Write(ctx, note, session, agentNow); err != nil {
179 t.Fatalf("resumed grant rejected: %v", err)
180 }
181 if _, err := f.community.RevokeAgent(ctx, f.owner, f.agent, agentNow); err != nil {
182 t.Fatal(err)
183 }
184 expectRestricted(t, f.gate.Write(ctx, note, session, agentNow), "revoked")
185 if role, _ := f.community.Role(ctx, f.agent); role != "" {
186 t.Fatalf("revoked agent still has role %q", role)
187 }
188 f.grant(t, agentNow+1, grantTags(f.agent, agentNow-1, []string{"k", "1"}))
189 expectRestricted(t, f.gate.Write(ctx, note, session, agentNow), "revoked")
190}
191
192func TestAgentGrantRateLimitSlidesOverOneMinute(t *testing.T) {
193 f := newAgentFixture(t)
194 ctx := context.Background()
195 session := relay.Session{PubKeys: []string{f.agent}}
196 f.grant(t, agentNow, grantTags(f.agent, agentNow+3600, []string{"k", "1"}, []string{"rate", "2"}))
197 for i := 0; i < 2; i++ {
198 if err := f.gate.Write(ctx, signed(t, agentSecret, 1, agentNow+int64(i), nil, "n"), session, agentNow+int64(i)); err != nil {
199 t.Fatalf("event %d rejected: %v", i, err)
200 }
201 }
202 expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 1, agentNow+2, nil, "n"), session, agentNow+2), "2 events per minute")
203 if err := f.gate.Write(ctx, signed(t, agentSecret, 1, agentNow+61, nil, "n"), session, agentNow+61); err != nil {
204 t.Fatalf("event after the window rejected: %v", err)
205 }
206}
207
208func TestHumanMemberWithGrantKeepsMemberPermissions(t *testing.T) {
209 f := newAgentFixture(t)
210 ctx := context.Background()
211 raw, _ := json.Marshal(f.agent)
212 if _, err := f.community.Execute(ctx, f.owner, "setmember", []json.RawMessage{raw}); err != nil {
213 t.Fatal(err)
214 }
215 f.grant(t, agentNow, grantTags(f.agent, agentNow+3600, []string{"k", "1"}))
216 if role, _ := f.community.Role(ctx, f.agent); role != "member" {
217 t.Fatalf("grant changed a human role to %q", role)
218 }
219 if err := f.gate.Write(ctx, signed(t, agentSecret, 7, agentNow, nil, "+"), relay.Session{PubKeys: []string{f.agent}}, agentNow); err != nil {
220 t.Fatalf("member constrained by grant: %v", err)
221 }
222}
223
224func TestImportEnforcesAgentGrant(t *testing.T) {
225 f := newAgentFixture(t)
226 ctx := context.Background()
227 f.grant(t, agentNow, grantTags(f.agent, agentNow+3600, []string{"k", "1"}))
228 if err := f.gate.Import(ctx, signed(t, agentSecret, 1, agentNow, nil, "synced"), agentNow); err != nil {
229 t.Fatalf("allowed import rejected: %v", err)
230 }
231 expectRestricted(t, f.gate.Import(ctx, signed(t, agentSecret, 7, agentNow, nil, "+"), agentNow), "kind 7")
232}
233
234func TestAgentGrantShapeAndSigner(t *testing.T) {
235 f := newAgentFixture(t)
236 ctx := context.Background()
237 other, _ := event.PublicKey(agentOtherSecret)
238 cases := []struct {
239 name string
240 secret string
241 tags [][]string
242 want string
243 }{
244 {"stranger signer", agentOtherSecret, grantTags(f.agent, agentNow+60), "restricted: only the owner or a moderator"},
245 {"missing expiration", agentOwnerSecret, [][]string{{"d", f.agent}, {"p", f.agent}}, "invalid: agent grant needs an expiration"},
246 {"too far out", agentOwnerSecret, grantTags(f.agent, agentNow+366*86400), "invalid: agent grant expiration"},
247 {"mismatched p", agentOwnerSecret, [][]string{{"d", f.agent}, {"p", other}, {"expiration", "1700003600"}}, "invalid: agent grant p tag"},
248 {"self grant", agentOwnerSecret, grantTags(f.owner, agentNow+60), "invalid: agent grant may not name its signer"},
249 {"bad repo", agentOwnerSecret, grantTags(f.agent, agentNow+60, []string{"repo", "nope"}), "invalid: agent grant repo tag"},
250 {"bad rate", agentOwnerSecret, grantTags(f.agent, agentNow+60, []string{"rate", "601"}), "invalid: agent grant rate"},
251 {"bad wiki", agentOwnerSecret, grantTags(f.agent, agentNow+60, []string{"wiki", "admin"}), "invalid: agent grant wiki tag"},
252 }
253 for _, tc := range cases {
254 t.Run(tc.name, func(t *testing.T) {
255 e := signed(t, tc.secret, event.KIND_AGENT_GRANT, agentNow, tc.tags, "")
256 err := f.gate.Write(ctx, e, relay.Session{PubKeys: []string{e.PubKey}}, agentNow)
257 if err == nil || !strings.HasPrefix(err.Error(), tc.want) {
258 t.Fatalf("want %q, got %v", tc.want, err)
259 }
260 })
261 }
262 e := signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, agentNow, grantTags(f.agent, agentNow+60, []string{"k", "1"}, []string{"wiki", "propose"}), `{"note":"ok"}`)
263 if err := f.gate.Write(ctx, e, relay.Session{PubKeys: []string{f.owner}}, agentNow); err != nil {
264 t.Fatalf("valid grant rejected: %v", err)
265 }
266 grant, err := community.ParseAgentGrant(e, agentNow)
267 if err != nil || grant.Scope.Rate != community.AgentRateDefault || grant.Scope.Wiki != "propose" || grant.Name != "helper" {
268 t.Fatalf("parsed grant %+v %v", grant, err)
269 }
270}
271
272func TestAgentGrantSitesCoverLabelsAndRequireExpiry(t *testing.T) {
273 f := newAgentFixture(t)
274 ctx := context.Background()
275 session := relay.Session{PubKeys: []string{f.agent}}
276 hash := strings.Repeat("a", 64)
277 paths := [][]string{{"path", "/index.html", hash}}
278 own := sites.SiteLabel(event.Event{Kind: sites.KindSite, PubKey: f.agent})
279 base, _ := sites.Base36(f.agent)
280 named := base + "docs"
281 manifest := func(kind int, tags ...[]string) event.Event {
282 return signed(t, agentSecret, kind, agentNow, append(append([][]string{}, paths...), tags...), "")
283 }
284 // Without a sites tag the manifest kinds stay closed.
285 f.grant(t, agentNow, grantTags(f.agent, agentNow+3600, []string{"k", "1"}))
286 expectRestricted(t, f.gate.Write(ctx, manifest(sites.KindSite), session, agentNow), "kind 15128")
287 // One label without a ttl: that site only, no expiration needed.
288 f.grant(t, agentNow+1, grantTags(f.agent, agentNow+3600, []string{"sites", named}))
289 if err := f.gate.Write(ctx, manifest(sites.KindNamedSite, []string{"d", "docs"}), session, agentNow); err != nil {
290 t.Fatalf("covered named site rejected: %v", err)
291 }
292 expectRestricted(t, f.gate.Write(ctx, manifest(sites.KindNamedSite, []string{"d", "blog"}), session, agentNow), "site "+base+"blog")
293 expectRestricted(t, f.gate.Write(ctx, manifest(sites.KindSite), session, agentNow), "site "+own)
294 // A ttl requires an expiration tag within it; the reason says what to add.
295 f.grant(t, agentNow+2, grantTags(f.agent, agentNow+3600, []string{"sites", own, "ttl=2"}, []string{"sites", named, "ttl=1", "encrypted"}))
296 latest := strconv.FormatInt(agentNow+2*86400, 10)
297 err := f.gate.Write(ctx, manifest(sites.KindSite), session, agentNow)
298 if err == nil || !strings.HasPrefix(err.Error(), "invalid: agent grant allows site "+own+" for 2 days: add an expiration tag no later than "+latest) {
299 t.Fatalf("manifest without expiration: %v", err)
300 }
301 err = f.gate.Write(ctx, manifest(sites.KindSite, []string{"expiration", strconv.FormatInt(agentNow+3*86400, 10)}), session, agentNow)
302 if err == nil || !strings.Contains(err.Error(), "set the expiration tag no later than "+latest) {
303 t.Fatalf("manifest expiring too late: %v", err)
304 }
305 if err := f.gate.Write(ctx, manifest(sites.KindSite, []string{"expiration", latest}), session, agentNow); err != nil {
306 t.Fatalf("manifest within the ttl rejected: %v", err)
307 }
308 if err := f.gate.Write(ctx, manifest(sites.KindNamedSite, []string{"d", "docs"}, []string{"expiration", strconv.FormatInt(agentNow+86400, 10)}), session, agentNow); err != nil {
309 t.Fatalf("named manifest within its own ttl rejected: %v", err)
310 }
311 if err := f.gate.Write(ctx, manifest(sites.KindNamedSite, []string{"d", "docs"}, []string{"expiration", latest}), session, agentNow); err == nil {
312 t.Fatal("named manifest beyond its own ttl accepted")
313 }
314 // A star covers every label under the key and the longest ttl applies.
315 grant := f.grant(t, agentNow+3, grantTags(f.agent, agentNow+3600, []string{"sites", "*", "ttl=5"}, []string{"sites", named, "ttl=1"}))
316 if err := f.gate.Write(ctx, manifest(sites.KindNamedSite, []string{"d", "blog"}, []string{"expiration", strconv.FormatInt(agentNow+5*86400, 10)}), session, agentNow); err != nil {
317 t.Fatalf("star label rejected: %v", err)
318 }
319 if err := f.gate.Write(ctx, manifest(sites.KindNamedSite, []string{"d", "docs"}, []string{"expiration", strconv.FormatInt(agentNow+5*86400, 10)}), session, agentNow); err != nil {
320 t.Fatalf("longest ttl not applied: %v", err)
321 }
322 parsed, err := community.ParseAgentGrant(grant, agentNow)
323 if err != nil || parsed.UploadTTLDays() != 5 || parsed.UploadsEncrypted() {
324 t.Fatalf("parsed sites %+v %v", parsed.Scope.Sites, err)
325 }
326 // Snapshots are not admitted by the sites tag.
327 expectRestricted(t, f.gate.Write(ctx, manifest(sites.KindSiteSnapshot), session, agentNow), "kind 5128")
328 // Paused and revoked grants stop manifests like every other event.
329 if _, err := f.community.SetAgentPaused(ctx, f.owner, f.agent, true); err != nil {
330 t.Fatal(err)
331 }
332 expectRestricted(t, f.gate.Write(ctx, manifest(sites.KindSite, []string{"expiration", latest}), session, agentNow), "paused")
333 if _, err := f.community.RevokeAgent(ctx, f.owner, f.agent, agentNow); err != nil {
334 t.Fatal(err)
335 }
336 expectRestricted(t, f.gate.Write(ctx, manifest(sites.KindSite, []string{"expiration", latest}), session, agentNow), "revoked")
337}
338
339func TestAgentGrantSitesShape(t *testing.T) {
340 f := newAgentFixture(t)
341 ctx := context.Background()
342 other, _ := event.PublicKey(agentOtherSecret)
343 own := sites.SiteLabel(event.Event{Kind: sites.KindSite, PubKey: f.agent})
344 foreign := sites.SiteLabel(event.Event{Kind: sites.KindSite, PubKey: other})
345 for _, tc := range []struct {
346 name string
347 tag []string
348 want string
349 }{
350 {"empty", []string{"sites"}, "invalid: agent grant sites tag needs a site label"},
351 {"bad label", []string{"sites", "not-a-site"}, "invalid: agent grant sites tag needs a site label"},
352 {"other key", []string{"sites", foreign}, "invalid: agent grant sites label must be a site under the agent's own key"},
353 {"snapshot", []string{"sites", "v" + strings.Repeat("0", 50)}, "invalid: agent grant sites tag needs a site label"},
354 {"ttl zero", []string{"sites", "*", "ttl=0"}, "invalid: agent grant sites ttl must be between 1 and 365"},
355 {"ttl long", []string{"sites", "*", "ttl=366"}, "invalid: agent grant sites ttl must be between 1 and 365"},
356 {"ttl text", []string{"sites", "*", "ttl=soon"}, "invalid: agent grant sites ttl must be between 1 and 365"},
357 {"unknown flag", []string{"sites", own, "public"}, "invalid: agent grant sites tag allows only ttl=<days> and encrypted"},
358 } {
359 t.Run(tc.name, func(t *testing.T) {
360 e := signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, agentNow, grantTags(f.agent, agentNow+60, tc.tag), "")
361 err := f.gate.Write(ctx, e, relay.Session{PubKeys: []string{f.owner}}, agentNow)
362 if err == nil || !strings.HasPrefix(err.Error(), tc.want) {
363 t.Fatalf("want %q, got %v", tc.want, err)
364 }
365 })
366 }
367 e := signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, agentNow, grantTags(f.agent, agentNow+60, []string{"sites", own, "ttl=30", "encrypted"}, []string{"sites", own, "ttl=7"}, []string{"sites", "*"}), "")
368 if err := f.gate.Write(ctx, e, relay.Session{PubKeys: []string{f.owner}}, agentNow); err != nil {
369 t.Fatalf("valid sites grant rejected: %v", err)
370 }
371 grant, err := community.ParseAgentGrant(e, agentNow)
372 if err != nil {
373 t.Fatal(err)
374 }
375 want := []community.AgentSite{{Label: own, TTLDays: 30, Encrypted: true}, {Label: "*"}}
376 if len(grant.Scope.Sites) != len(want) || grant.Scope.Sites[0] != want[0] || grant.Scope.Sites[1] != want[1] {
377 t.Fatalf("sites = %+v, want %+v", grant.Scope.Sites, want)
378 }
379 if !grant.UploadsEncrypted() || grant.UploadTTLDays() != 0 {
380 t.Fatalf("an entry without a ttl should leave uploads unlimited: ttl=%d encrypted=%v", grant.UploadTTLDays(), grant.UploadsEncrypted())
381 }
382}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs