clone

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

feat/tinyagent
tinyrelay / internal / community / community_test.go
community_test.go 7.9K
1package community23import (4 "context"5 "crypto/sha256"6 "database/sql"7 "encoding/hex"8 "encoding/json"9 "errors"10 "strings"11 "testing"1213 "github.com/FelineStateMachine/tinyrelay/internal/event"14 "github.com/FelineStateMachine/tinyrelay/internal/policy"15 "github.com/FelineStateMachine/tinyrelay/internal/storage"16)1718func TestMembershipAuthorizationAndInviteClaim(t *testing.T) {19 ctx := context.Background()20 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")21 if err != nil {22 t.Fatal(err)23 }24 defer st.Close()25 owner, moderator, member, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64), strings.Repeat("d", 64)26 svc, err := New(ctx, st, owner)27 if err != nil {28 t.Fatal(err)29 }30 if _, err := svc.Execute(ctx, stranger, "setmember", raws(member, "alice", "member")); err == nil {31 t.Fatal("stranger changed membership")32 }33 if _, err := svc.Execute(ctx, owner, "setmember", raws(moderator, map[string]any{"name": "mod", "role": "moderator"})); err != nil {34 t.Fatal(err)35 }36 if _, err := svc.Execute(ctx, moderator, "setmember", raws(member, map[string]any{"name": "alice", "role": "member"})); err != nil {37 t.Fatal(err)38 }39 inv, err := svc.Execute(ctx, owner, "createinvite", raws(int64(3600), 1, "test"))40 if err != nil {41 t.Fatal(err)42 }43 code := inv.(Invite).Code44 if _, err := svc.Claim(ctx, code, stranger); err != nil {45 t.Fatal(err)46 }47 if _, err := svc.Claim(ctx, code, strings.Repeat("e", 64)); err == nil {48 t.Fatal("single-use claim spent twice")49 }50 if got, err := svc.Role(ctx, stranger); err != nil || got != "member" {51 t.Fatalf("role=%q err=%v", got, err)52 }53 if inv == nil {54 t.Fatal("missing invite result")55 }56}5758func TestMemberInvitePolicyTermsAndRevokedParent(t *testing.T) {59 ctx := context.Background()60 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")61 if err != nil {62 t.Fatal(err)63 }64 defer st.Close()65 owner, member, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)66 p := policy.Defaults(owner)67 p.MemberInvites = policy.MemberInvites{Depth: 2, Quota: 1}68 p.JoinTerms = "be kind"69 svc, err := New(ctx, st, owner)70 if err != nil {71 t.Fatal(err)72 }73 svc.ConfigurePolicy(func() policy.Policy { return p })74 if _, err := svc.Execute(ctx, owner, "setmember", raws(member, map[string]any{"name": "member", "role": "member"})); err != nil {75 t.Fatal(err)76 }77 inv, err := svc.Execute(ctx, member, "createinvite", raws(int64(3600), 1, "child"))78 if err != nil {79 t.Fatal(err)80 }81 code := inv.(Invite).Code82 if _, err := svc.Execute(ctx, member, "createinvite", raws(int64(3600), 1, "over-quota")); err == nil {83 t.Fatal("member exceeded live invite quota")84 }85 terms := sha256.Sum256([]byte(p.JoinTerms))86 if _, err := svc.ClaimWithTerms(ctx, code, stranger, "wrong"); err == nil {87 t.Fatal("terms mismatch accepted")88 }89 if role, _ := svc.Role(ctx, stranger); role != "" {90 t.Fatalf("terms mismatch wrote membership: %q", role)91 }92 if _, err := svc.Execute(ctx, owner, "removesubtree", raws(member)); err != nil {93 t.Fatal(err)94 }95 if _, err := svc.ClaimWithTerms(ctx, code, stranger, hex.EncodeToString(terms[:])); err == nil {96 t.Fatal("revoked parent invite accepted")97 }98}99100func TestIssueNIP43InviteRequiresMembership(t *testing.T) {101 ctx := context.Background()102 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")103 if err != nil {104 t.Fatal(err)105 }106 defer st.Close()107 owner, member, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)108 svc, err := New(ctx, st, owner)109 if err != nil {110 t.Fatal(err)111 }112 p := policy.Defaults(owner)113 p.MemberInvites = policy.MemberInvites{Depth: 1, Quota: 1}114 svc.ConfigurePolicy(func() policy.Policy { return p })115 if _, err := svc.IssueNIP43Invite(ctx, stranger); err == nil {116 t.Fatal("stranger received invite")117 }118 if _, err := svc.Execute(ctx, owner, "setmember", raws(member, "member", "member")); err != nil {119 t.Fatal(err)120 }121 invite, err := svc.IssueNIP43Invite(ctx, member)122 if err != nil {123 t.Fatal(err)124 }125 if invite.Code == "" || invite.ExpiresAt <= invite.CreatedAt {126 t.Fatalf("invalid invite: %+v", invite)127 }128}129130func TestNIP56PubkeyReportDoesNotHideEventID(t *testing.T) {131 ctx := context.Background()132 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")133 if err != nil {134 t.Fatal(err)135 }136 defer st.Close()137 owner, reporter, target := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)138 svc, err := New(ctx, st, owner)139 if err != nil {140 t.Fatal(err)141 }142 if err := svc.SubmitReportTarget(ctx, reporter, target, "pubkey", "spam", "bad actor", 1); err != nil {143 t.Fatal(err)144 }145 var hidden int146 if err := st.DB().QueryRowContext(ctx, `SELECT count(*) FROM hidden_events WHERE id=?`, target).Scan(&hidden); err != nil {147 t.Fatal(err)148 }149 if hidden != 0 {150 t.Fatal("pubkey report was treated as an event hide")151 }152}153154func TestMembershipProjectionIntentRollsBackWithTransaction(t *testing.T) {155 ctx := context.Background()156 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")157 if err != nil {158 t.Fatal(err)159 }160 defer st.Close()161 owner := strings.Repeat("a", 64)162 if _, err := New(ctx, st, owner); err != nil {163 t.Fatal(err)164 }165 var before int166 if err := st.DB().QueryRow(`SELECT count(*) FROM work_intents WHERE kind='records-projection'`).Scan(&before); err != nil {167 t.Fatal(err)168 }169 member := strings.Repeat("b", 64)170 if err := st.WithTx(ctx, func(tx *sql.Tx) error {171 if _, err := tx.Exec(`INSERT INTO community_members(pubkey,role,created_at) VALUES(?,?,?)`, member, "member", 1); err != nil {172 return err173 }174 return errors.New("force rollback")175 }); err == nil {176 t.Fatal("expected rollback")177 }178 var n int179 if err := st.DB().QueryRow(`SELECT count(*) FROM community_members WHERE pubkey=?`, member).Scan(&n); err != nil {180 t.Fatal(err)181 }182 if n != 0 {183 t.Fatal("membership committed despite rollback")184 }185 if err := st.DB().QueryRow(`SELECT count(*) FROM work_intents WHERE kind='records-projection'`).Scan(&n); err != nil {186 t.Fatal(err)187 }188 if n != before {189 t.Fatal("projection intent committed despite rollback")190 }191}192193func TestTransactionalMembershipPersistenceRollback(t *testing.T) {194 ctx := context.Background()195 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")196 if err != nil {197 t.Fatal(err)198 }199 defer st.Close()200 owner, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64)201 svc, err := New(ctx, st, owner)202 if err != nil {203 t.Fatal(err)204 }205 p := policy.Defaults(owner)206 svc.ConfigurePolicy(func() policy.Policy { return p })207 _, err = svc.HandleMembershipEventTx(ctx, event.Event{ID: strings.Repeat("c", 64), PubKey: stranger, Kind: event.KIND_JOIN}, func(*sql.Tx) error { return errors.New("event write failed") })208 if err == nil {209 t.Fatal("expected event persistence failure")210 }211 if role, _ := svc.Role(ctx, stranger); role != "" {212 t.Fatal("membership committed after event failure")213 }214 var n int215 if err := st.DB().QueryRow(`SELECT count(*) FROM work_intents WHERE event_id=?`, strings.Repeat("c", 64)).Scan(&n); err != nil {216 t.Fatal(err)217 }218 if n != 0 {219 t.Fatal("projection intent committed after event failure")220 }221}222223func TestTransferIsAtomicAndDemotesOldOwner(t *testing.T) {224 ctx := context.Background()225 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")226 if err != nil {227 t.Fatal(err)228 }229 defer st.Close()230 owner, member := strings.Repeat("a", 64), strings.Repeat("b", 64)231 svc, err := New(ctx, st, owner)232 if err != nil {233 t.Fatal(err)234 }235 if _, err := svc.Execute(ctx, owner, "setmember", raws(member, "new", "member")); err != nil {236 t.Fatal(err)237 }238 if _, err := svc.Execute(ctx, owner, "transferowner", raws(member)); err != nil {239 t.Fatal(err)240 }241 if role, _ := svc.Role(ctx, owner); role != "moderator" {242 t.Fatalf("old owner role=%q", role)243 }244 if role, _ := svc.Role(ctx, member); role != "owner" {245 t.Fatalf("new owner role=%q", role)246 }247}248249func raws(values ...any) []json.RawMessage {250 out := make([]json.RawMessage, len(values))251 for i, value := range values {252 out[i], _ = json.Marshal(value)253 }254 return out255}