tinyrelayfile
tinyrelay/internal/gates/rooms_test.go/

internal/gates/rooms_test.go raw
1package gates
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "strings"
8 "testing"
9 "time"
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/storage"
16)
17
18func roomGate(t *testing.T) (*Gate, *community.Service, map[string]string, policy.Policy) {
19 t.Helper()
20 ctx := context.Background()
21 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
22 if err != nil {
23 t.Fatal(err)
24 }
25 t.Cleanup(func() { _ = st.Close() })
26 secrets := map[string]string{"owner": strings.Repeat("0", 63) + "1", "alice": strings.Repeat("0", 63) + "2", "bob": strings.Repeat("0", 63) + "3", "stranger": strings.Repeat("0", 63) + "4"}
27 keys := map[string]string{}
28 for name, secret := range secrets {
29 keys[name], _ = event.PublicKey(secret)
30 }
31 p := policy.Defaults(keys["owner"])
32 svc, err := community.New(ctx, st, keys["owner"])
33 if err != nil {
34 t.Fatal(err)
35 }
36 svc.ConfigurePolicy(func() policy.Policy { return p })
37 if err := svc.EnsureSlugRoom(ctx, "main", community.RoomOpen); err != nil {
38 t.Fatal(err)
39 }
40 for _, name := range []string{"alice", "bob"} {
41 if _, err := svc.Execute(ctx, keys["owner"], "setmember", []json.RawMessage{mustRaw(keys[name]), mustRaw(map[string]any{"role": "member"})}); err != nil {
42 t.Fatal(err)
43 }
44 }
45 sign := func(who string, kind int, tags [][]string) event.Event {
46 e := event.Event{Kind: kind, CreatedAt: time.Now().Unix(), Tags: tags, Content: "x"}
47 if err := event.Sign(&e, secrets[who]); err != nil {
48 t.Fatal(err)
49 }
50 return e
51 }
52 persist := func(e event.Event) func(*sql.Tx) error {
53 return func(tx *sql.Tx) error {
54 _, err := storage.SaveTx(ctx, tx, e, storage.SaveOptions{Now: time.Now().Unix()})
55 return err
56 }
57 }
58 for _, room := range []struct{ id, access string }{{"pub", "open"}, {"priv", "members"}} {
59 e := sign("alice", event.KIND_CREATE_GROUP, [][]string{{"h", room.id}, {"visibility", room.access}})
60 if _, err := svc.HandleRoomEventTx(ctx, e, persist(e)); err != nil {
61 t.Fatal(err)
62 }
63 }
64 g, err := New(Config{Store: st, Community: svc, Policy: func() policy.Policy { return p }, Slug: "main"})
65 if err != nil {
66 t.Fatal(err)
67 }
68 keys["secret:alice"], keys["secret:bob"], keys["secret:stranger"], keys["secret:owner"] = secrets["alice"], secrets["bob"], secrets["stranger"], secrets["owner"]
69 return g, svc, keys, p
70}
71
72func TestRoomWritesNeedMembership(t *testing.T) {
73 g, _, keys, _ := roomGate(t)
74 ctx := context.Background()
75 now := time.Now().Unix()
76 sign := func(who string, kind int, tags [][]string) event.Event {
77 e := event.Event{Kind: kind, CreatedAt: now, Tags: tags, Content: "x"}
78 if err := event.Sign(&e, keys["secret:"+who]); err != nil {
79 t.Fatal(err)
80 }
81 return e
82 }
83 for _, tc := range []struct {
84 name string
85 e event.Event
86 reject string
87 }{
88 {"unknown room", sign("alice", 9, [][]string{{"h", "nope"}}), "invalid: unknown room"},
89 {"room owner posts", sign("alice", 9, [][]string{{"h", "priv"}}), ""},
90 {"tenant member outside a members-only room", sign("bob", 9, [][]string{{"h", "priv"}}), "restricted:"},
91 {"tenant owner moderates any room", sign("owner", 9, [][]string{{"h", "priv"}}), ""},
92 {"tenant member in an open room", sign("bob", 9, [][]string{{"h", "pub"}}), ""},
93 {"stranger in an open room", sign("stranger", 9, [][]string{{"h", "pub"}}), "restricted:"},
94 {"reaction in an open room", sign("bob", 7, [][]string{{"h", "pub"}, {"e", strings.Repeat("a", 64)}}), ""},
95 {"rich content in an open room", sign("bob", event.KIND_RICH_CONTENT, [][]string{{"h", "pub"}}), ""},
96 {"presence from a member", sign("bob", event.KIND_ROOM_PRESENCE, [][]string{{"h", "pub"}}), ""},
97 {"presence from a stranger", sign("stranger", event.KIND_ROOM_TYPING, [][]string{{"h", "pub"}}), "restricted:"},
98 {"member notices are relay signed", sign("alice", event.KIND_ROOM_MEMBER_ADDED, [][]string{{"h", "pub"}, {"p", keys["bob"]}}), "blocked:"},
99 {"room creation names a new room", sign("bob", event.KIND_CREATE_GROUP, [][]string{{"h", "fresh"}}), ""},
100 {"join request reaches the room handler", sign("stranger", event.KIND_JOIN, [][]string{{"h", "pub"}}), ""},
101 {"slug room keeps the open write rule", sign("stranger", 9, [][]string{{"h", "main"}}), ""},
102 {"no h tag is outside rooms", sign("stranger", 9, nil), ""},
103 } {
104 t.Run(tc.name, func(t *testing.T) {
105 err := g.Write(ctx, tc.e, relay.Session{PubKeys: []string{tc.e.PubKey}}, now)
106 if tc.reject == "" && err != nil {
107 t.Fatalf("rejected: %v", err)
108 }
109 if tc.reject != "" && (err == nil || !strings.HasPrefix(err.Error(), tc.reject)) {
110 t.Fatalf("want %q, got %v", tc.reject, err)
111 }
112 })
113 }
114 if err := g.Import(ctx, sign("alice", 9, [][]string{{"h", "gone"}}), now); err == nil {
115 t.Fatal("import accepted an unknown room")
116 }
117 if err := g.Import(ctx, sign("stranger", 9, [][]string{{"h", "priv"}}), now); err != nil {
118 t.Fatalf("import applies room existence only: %v", err)
119 }
120}
121
122func TestRoomReadsFollowRoomAccess(t *testing.T) {
123 g, _, keys, _ := roomGate(t)
124 ctx := context.Background()
125 private := event.Event{Kind: 9, PubKey: keys["alice"], CreatedAt: 1, Tags: [][]string{{"h", "priv"}}, Content: "secret"}
126 public := event.Event{Kind: 9, PubKey: keys["alice"], CreatedAt: 1, Tags: [][]string{{"h", "pub"}}, Content: "open"}
127 members := event.Event{Kind: event.KIND_GROUP_MEMBERS, PubKey: strings.Repeat("f", 64), CreatedAt: 1, Tags: [][]string{{"-"}, {"d", "priv"}, {"p", keys["alice"]}}}
128 main := event.Event{Kind: 9, PubKey: keys["alice"], CreatedAt: 1, Tags: [][]string{{"h", "main"}}, Content: "main"}
129 for _, tc := range []struct {
130 name string
131 e event.Event
132 who []string
133 want bool
134 }{
135 {"member sees a members-only room", private, []string{keys["alice"]}, true},
136 {"tenant owner sees every room", private, []string{keys["owner"]}, true},
137 {"tenant member outside the room does not", private, []string{keys["bob"]}, false},
138 {"anonymous does not", private, nil, false},
139 {"member list of a members-only room is hidden", members, []string{keys["bob"]}, false},
140 {"member list is served to members", members, []string{keys["alice"]}, true},
141 {"open rooms follow the tenant read rule", public, nil, true},
142 {"slug room is unchanged", main, nil, true},
143 } {
144 t.Run(tc.name, func(t *testing.T) {
145 if got := g.CanSee(ctx, tc.e, relay.Session{PubKeys: tc.who}, nil); got != tc.want {
146 t.Fatalf("visible = %v, want %v", got, tc.want)
147 }
148 })
149 }
150 hint, err := g.Read(ctx, []event.Filter{{Kinds: []int{9}, Tags: map[string][]string{"h": {"priv"}}}}, relay.Session{})
151 if err != nil || !hint {
152 t.Fatalf("members-only room read hint = %v %v", hint, err)
153 }
154 hint, err = g.Read(ctx, []event.Filter{{Kinds: []int{9}, Tags: map[string][]string{"h": {"pub"}}}}, relay.Session{})
155 if err != nil || hint {
156 t.Fatalf("open room read hint = %v %v", hint, err)
157 }
158}
159
160func mustRaw(v any) json.RawMessage {
161 b, _ := json.Marshal(v)
162 return b
163}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs