clone

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

feat/tinyagent
tinyrelay / internal / gates / rooms_test.go
rooms_test.go 6.9K
1package gates23import (4 "context"5 "database/sql"6 "encoding/json"7 "strings"8 "testing"9 "time"1011 "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)1718func 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 e51 }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 err56 }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, p70}7172func 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 e82 }83 for _, tc := range []struct {84 name string85 e event.Event86 reject string87 }{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}121122func 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 string131 e event.Event132 who []string133 want bool134 }{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}159160func mustRaw(v any) json.RawMessage {161 b, _ := json.Marshal(v)162 return b163}