Reimplement mute list regeneration and enhance event deletion logic

Restore code for regenerating owners' mute lists and ensure muted pubkeys are excluded from the followed list. Improve deletion event checks by disallowing owners from deleting their own mute or follow lists to prevent potential misuse. Add comprehensive tests for event acceptance and publishing logic, covering edge cases and improved error handling.
This commit is contained in:
2025-06-26 21:55:57 +01:00
parent 7890340767
commit 81b9882c29
6 changed files with 653 additions and 113 deletions

View File

@@ -2,6 +2,7 @@ package realy
import (
"bytes"
"realy.lol/hex"
"realy.lol/chk"
"realy.lol/context"
@@ -20,14 +21,22 @@ func (s *Server) acceptEvent(c context.T, evt *event.T, authedPubkey []byte,
defer s.Unlock()
// if the authenticator is enabled we require auth to accept events
if authRequired && len(s.owners) == 0 {
log.W.F("%s auth not required and no ACL enabled, accepting event %0x", remote, evt.Id)
return true, "", nil
if len(authedPubkey) == 0 {
notice = "auth required but user not authed"
return false, notice, nil
}
if len(authedPubkey) == schnorr.PubKeyBytesLen {
log.W.F("%s auth required, no ACL enabled, accepting authenticated event %0x", remote, evt.Id)
return true, "", nil
}
notice = "auth required but user not authed"
return false, notice, nil
}
// check ACL
if len(s.owners) > 0 {
if len(authedPubkey) == 0 {
notice = "auth required but user not authed"
return
return false, notice, nil
}
// if one of the follows of the owners or follows of the follows changes
if evt.Kind.Equal(kind.FollowList) || evt.Kind.Equal(kind.MuteList) {
@@ -45,74 +54,69 @@ func (s *Server) acceptEvent(c context.T, evt *event.T, authedPubkey []byte,
}
}
}
// check the mute list, and reject events authored by muted pubkeys, even if
// they come from a pubkey that is on the follow list.
//check the mute list, and reject events authored by muted pubkeys, even if
//they come from a pubkey that is on the follow list.
//
// note that some clients hide this info in the kind 10000 mute list, unfortunately.
// such as jumble. use old nostrudel or similar which still gives public readable info.
// log.I.S(s.muted)
// for pk := range s.muted {
// if bytes.Equal(evt.Pubkey, []byte(pk)) {
// notice = "rejecting event with pubkey " + hex.Enc(evt.Pubkey) +
// " because on owner mute list"
// log.I.F("%s %s", remote, notice)
// return false, notice, nil
// }
// }
// for _, o := range s.owners {
// log.T.F("%0x,%0x", o, evt.Pubkey)
// if bytes.Equal(o, evt.Pubkey) {
// // prevent owners from deleting their own mute/follow lists in case of bad
// // client implementation
if evt.Kind.Equal(kind.Deletion) {
// check all a tags present are not follow/mute lists of the owners
aTags := evt.Tags.GetAll(tag.New("a"))
for _, at := range aTags.ToSliceOfTags() {
a := &atag.T{}
var rem []byte
var err error
if rem, err = a.Unmarshal(at.Value()); chk.E(err) {
continue
}
if len(rem) > 0 {
log.I.S("remainder", evt, rem)
}
log.I.S(a)
if a.Kind == nil {
log.I.F("a tag is empty!")
continue
}
if a.Kind.Equal(kind.Deletion) {
// we don't delete delete events, period
return false, "delete event kind may not be deleted", nil
}
// if the kind is not parameterized replaceable, the tag is invalid and the
// delete event will not be saved.
if !a.Kind.IsParameterizedReplaceable() {
return false, "delete tags with a tags containing " +
"non-parameterized-replaceable events cannot be processed", nil
}
for _, own := range s.owners {
// don't allow owners to delete their mute or follow lists because
// they should not want to, can simply replace it, and malicious
// clients may do this specifically to attack the owner's realy (s)
if bytes.Equal(own, a.PubKey) ||
a.Kind.Equal(kind.MuteList) ||
a.Kind.Equal(kind.FollowList) {
notice = "owners may not delete their own " +
"mute or follow lists, they can be replaced"
log.I.F("%s %s", remote, notice)
return false, notice, nil
}
}
//note that some clients hide this info in the kind 10000 mute list, unfortunately.
//such as jumble. use old nostrudel or similar which still gives public readable info.
log.I.S(s.muted)
for pk := range s.muted {
if bytes.Equal(evt.Pubkey, []byte(pk)) {
notice = "rejecting event with pubkey " + hex.Enc(evt.Pubkey) +
" because on owner mute list"
log.I.F("%s %s", remote, notice)
return false, notice, nil
}
}
for _, o := range s.owners {
log.T.F("%0x,%0x", o, evt.Pubkey)
if bytes.Equal(o, evt.Pubkey) {
// prevent owners from deleting their own mute/follow lists in case of bad
// client implementation
if evt.Kind.Equal(kind.Deletion) {
// check all a tags present are not follow/mute lists of the owners
aTags := evt.Tags.GetAll(tag.New("a"))
for _, at := range aTags.ToSliceOfTags() {
a := &atag.T{}
var rem []byte
var err error
if rem, err = a.Unmarshal(at.Value()); chk.E(err) {
continue
}
if len(rem) > 0 {
log.I.S("remainder", evt, rem)
}
log.I.S(a)
if a.Kind == nil {
log.I.F("a tag is empty!")
continue
}
if a.Kind.Equal(kind.Deletion) {
// we don't delete delete events, period
return false, "delete event kind may not be deleted", nil
}
// Special handling for owner follow/mute lists - check if this is an owner's list
// that should be protected from deletion
for _, own := range s.owners {
// don't allow owners to delete their mute or follow lists because
// they should not want to, can simply replace it, and malicious
// clients may do this specifically to attack the owner's realy (s)
if bytes.Equal(own, a.PubKey) &&
(a.Kind.Equal(kind.MuteList) || a.Kind.Equal(kind.FollowList)) {
notice = "owners may not delete their own " +
"mute or follow lists, they can be replaced"
log.I.F("%s %s", remote, notice)
return false, notice, nil
}
}
}
return false, "", nil
}
// log.W.Ln("event is from owner")
// accept = true
return
}
return
}
// // log.W.Ln("event is from owner")
// // accept = true
// return
// }
// }
// check the authed pubkey is in the follow list
for pk := range s.followed {
// log.I.F("%0x %0x", authedPubkey, []byte(pk))
@@ -129,8 +133,7 @@ func (s *Server) acceptEvent(c context.T, evt *event.T, authedPubkey []byte,
// if auth is enabled and there is no moderators we just check that the pubkey
// has been loaded via the auth function.
if len(authedPubkey) == schnorr.PubKeyBytesLen && authRequired {
notice = "auth required but user not authed"
return
return true, "", nil
}
return
return false, "", nil
}

191
realy/acceptevent_test.go Normal file
View File

@@ -0,0 +1,191 @@
package realy
import (
"testing"
"realy.lol/context"
"realy.lol/ec/schnorr"
"realy.lol/event"
"realy.lol/hex"
"realy.lol/kind"
"realy.lol/realy/config"
"realy.lol/tag"
"realy.lol/tags"
)
func TestServer_acceptEvent(t *testing.T) {
tests := []struct {
name string
authRequired bool
owners [][]byte
followed map[string]struct{}
ownersFollowed map[string]struct{}
evt *event.T
authedPubkey []byte
remote string
wantAccept bool
wantNotice string
wantAfterSave bool
}{
{
name: "auth required but no owners - should reject unauthenticated",
authRequired: true,
owners: [][]byte{},
evt: &event.T{Kind: kind.TextNote},
authedPubkey: []byte{},
remote: "test",
wantAccept: false,
wantNotice: "auth required but user not authed",
},
{
name: "auth required but no owners - should accept authenticated",
authRequired: true,
owners: [][]byte{},
evt: &event.T{Kind: kind.TextNote},
authedPubkey: make([]byte, schnorr.PubKeyBytesLen),
remote: "test",
wantAccept: true,
wantNotice: "",
},
{
name: "owners exist but user not authenticated",
authRequired: false,
owners: [][]byte{make([]byte, 32)},
evt: &event.T{Kind: kind.TextNote},
authedPubkey: []byte{},
remote: "test",
wantAccept: false,
wantNotice: "auth required but user not authed",
},
{
name: "owners exist and user in followed list",
authRequired: false,
owners: [][]byte{make([]byte, 32)},
followed: map[string]struct{}{string(make([]byte, 32)): {}},
evt: &event.T{Kind: kind.TextNote},
authedPubkey: make([]byte, 32),
remote: "test",
wantAccept: true,
wantNotice: "",
},
{
name: "owners exist but user not in followed list",
authRequired: false,
owners: [][]byte{make([]byte, 32)},
followed: map[string]struct{}{},
evt: &event.T{Kind: kind.TextNote},
authedPubkey: make([]byte, 32),
remote: "test",
wantAccept: false,
wantNotice: "",
},
{
name: "follow list update from owners followed",
authRequired: false,
owners: [][]byte{make([]byte, 32)},
ownersFollowed: map[string]struct{}{string(make([]byte, 32)): {}},
evt: &event.T{Kind: kind.FollowList, Pubkey: make([]byte, 32)},
authedPubkey: make([]byte, 32),
remote: "test",
wantAccept: true,
wantNotice: "",
wantAfterSave: true,
},
{
name: "deletion event with delete kind should be rejected",
authRequired: false,
owners: [][]byte{make([]byte, 32)},
evt: &event.T{
Kind: kind.Deletion,
Tags: tags.New(tag.New("a", "5:"+hex.Enc(make([]byte, 32))+":test")),
},
authedPubkey: make([]byte, 32),
remote: "test",
wantAccept: false,
wantNotice: "delete event kind may not be deleted",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Server{
owners: tt.owners,
followed: tt.followed,
ownersFollowed: tt.ownersFollowed,
configuration: config.C{AuthRequired: tt.authRequired},
}
accept, notice, afterSave := s.acceptEvent(context.Bg(), tt.evt, tt.authedPubkey, tt.remote)
if accept != tt.wantAccept {
t.Errorf("acceptEvent() accept = %v, want %v", accept, tt.wantAccept)
}
if notice != tt.wantNotice {
t.Errorf("acceptEvent() notice = %v, want %v", notice, tt.wantNotice)
}
if (afterSave != nil) != tt.wantAfterSave {
t.Errorf("acceptEvent() afterSave = %v, want %v", afterSave != nil, tt.wantAfterSave)
}
})
}
}
func TestServer_acceptEvent_DeletionLogic(t *testing.T) {
owner1 := make([]byte, 32)
owner1[0] = 1
owner2 := make([]byte, 32)
owner2[0] = 2
tests := []struct {
name string
owners [][]byte
aTagValue string
wantAccept bool
wantNotice string
}{
{
name: "prevent owner from deleting own follow list",
owners: [][]byte{owner1},
aTagValue: "3:" + hex.Enc(owner1) + ":follow",
wantAccept: false,
wantNotice: "owners may not delete their own mute or follow lists, they can be replaced",
},
{
name: "prevent owner from deleting own mute list",
owners: [][]byte{owner1},
aTagValue: "10000:" + hex.Enc(owner1) + ":mute",
wantAccept: false,
wantNotice: "owners may not delete their own mute or follow lists, they can be replaced",
},
{
name: "allow owner to delete other's lists",
owners: [][]byte{owner1},
aTagValue: "3:" + hex.Enc(owner2) + ":follow",
wantAccept: false, // Will be rejected for other reasons, but not the owner check
wantNotice: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Server{
owners: tt.owners,
followed: map[string]struct{}{string(owner1): {}},
}
evt := &event.T{
Kind: kind.Deletion,
Tags: tags.New(tag.New("a", tt.aTagValue)),
}
accept, notice, _ := s.acceptEvent(context.Bg(), evt, owner1, "test")
if accept != tt.wantAccept {
t.Errorf("acceptEvent() accept = %v, want %v", accept, tt.wantAccept)
}
if notice != tt.wantNotice {
t.Errorf("acceptEvent() notice = %v, want %v", notice, tt.wantNotice)
}
})
}
}

View File

@@ -114,37 +114,37 @@ func (s *Server) CheckOwnerLists(c context.T) {
}
evs = nil
}
// if len(s.muted) < 1 {
// log.D.Ln("regenerating owners mute lists")
// s.muted = make(map[string]struct{})
// if evs, err = s.Store.QueryEvents(c,
// &filter.T{Authors: tag.New(s.owners...),
// Kinds: kinds.New(kind.MuteList)}); chk.E(err) {
// }
// for _, ev := range evs {
// s.ownersMuteLists = append(s.ownersMuteLists, ev.Id)
// for _, t := range ev.Tags.ToSliceOfTags() {
// if bytes.Equal(t.Key(), []byte("p")) {
// var p []byte
// if p, err = hex.Dec(string(t.Value())); chk.E(err) {
// continue
// }
// // log.I.F("muted %0x", p)
// s.muted[string(p)] = struct{}{}
// }
// }
// }
// evs = nil
// }
// // remove muted from the followed list
// for m := range s.muted {
// for f := range s.followed {
// if f == m {
// // delete muted element from followed list
// delete(s.followed, m)
// }
// }
// }
if len(s.muted) < 1 {
log.D.Ln("regenerating owners mute lists")
s.muted = make(map[string]struct{})
if evs, err = s.Store.QueryEvents(c,
&filter.T{Authors: tag.New(s.owners...),
Kinds: kinds.New(kind.MuteList)}); chk.E(err) {
}
for _, ev := range evs {
s.ownersMuteLists = append(s.ownersMuteLists, ev.Id)
for _, t := range ev.Tags.ToSliceOfTags() {
if bytes.Equal(t.Key(), []byte("p")) {
var p []byte
if p, err = hex.Dec(string(t.Value())); chk.E(err) {
continue
}
// log.I.F("muted %0x", p)
s.muted[string(p)] = struct{}{}
}
}
}
evs = nil
}
// remove muted from the followed list
for m := range s.muted {
for f := range s.followed {
if f == m {
// delete muted element from followed list
delete(s.followed, m)
}
}
}
log.I.F("%d allowed npubs ", len(s.followed))
}
}

View File

@@ -51,7 +51,7 @@ func (s *Server) Publish(c context.T, evt *event.T) (err error) {
}
// defer the delete until after the save, further down, has completed.
if del {
defer func() {
defer func(eventToDelete *event.T) {
if err != nil {
// something went wrong saving the replacement, so we won't delete
// the event.
@@ -59,14 +59,14 @@ func (s *Server) Publish(c context.T, evt *event.T) (err error) {
}
log.T.C(func() string {
return fmt.Sprintf("%s\nreplacing\n%s", evt.Serialize(),
ev.Serialize())
eventToDelete.Serialize())
})
// replaceable events we don't tombstone when replacing, so if deleted, old
// versions can be restored
if err = sto.DeleteEvent(c, ev.EventId(), true); chk.E(err) {
if delErr := sto.DeleteEvent(c, eventToDelete.EventId(), true); chk.E(delErr) {
return
}
}()
}(ev)
}
}
}
@@ -103,7 +103,7 @@ func (s *Server) Publish(c context.T, evt *event.T) (err error) {
continue
}
if del {
defer func() {
defer func(eventToDelete *event.T) {
if err != nil {
// something went wrong saving the replacement, so we won't delete
// the event.
@@ -111,14 +111,14 @@ func (s *Server) Publish(c context.T, evt *event.T) (err error) {
}
log.T.C(func() string {
return fmt.Sprintf("%s\nreplacing\n%s", evt.Serialize(),
ev.Serialize())
eventToDelete.Serialize())
})
// replaceable events we don't tombstone when replacing, so if deleted, old
// versions can be restored
if err = sto.DeleteEvent(c, ev.EventId(), true); chk.E(err) {
if delErr := sto.DeleteEvent(c, eventToDelete.EventId(), true); chk.E(delErr) {
return
}
}()
}(ev)
}
}
}

View File

@@ -0,0 +1,346 @@
package realy
import (
"bytes"
"errors"
"io"
"testing"
"realy.lol/context"
"realy.lol/event"
"realy.lol/eventid"
"realy.lol/eventidserial"
"realy.lol/filter"
"realy.lol/kind"
"realy.lol/store"
"realy.lol/tag"
"realy.lol/tags"
"realy.lol/timestamp"
)
// MockStore implements store.I for testing
type MockStore struct {
events []*event.T
queryError error
saveError error
deleteError error
deletedEvents [][]byte
}
func (m *MockStore) QueryEvents(c context.T, f *filter.T) (event.Ts, error) {
if m.queryError != nil {
return nil, m.queryError
}
// For testing purposes, return all events that match basic criteria
// This is a simplified implementation for testing
var result event.Ts
for _, evt := range m.events {
result = append(result, evt)
}
return result, nil
}
func (m *MockStore) SaveEvent(c context.T, evt *event.T) error {
if m.saveError != nil {
return m.saveError
}
m.events = append(m.events, evt)
return nil
}
func (m *MockStore) DeleteEvent(c context.T, id *eventid.T, tombstone ...bool) error {
if m.deleteError != nil {
return m.deleteError
}
m.deletedEvents = append(m.deletedEvents, id.Bytes())
return nil
}
func (m *MockStore) Close() error { return nil }
// Implement remaining store.I interface methods as no-ops
func (m *MockStore) Init(path string) error { return nil }
func (m *MockStore) Path() string { return "" }
func (m *MockStore) Nuke() error { return nil }
func (m *MockStore) EventCount() (uint64, error) { return 0, nil }
func (m *MockStore) FetchIds(w io.Writer, c context.T, evIds *tag.T, binary bool) error { return nil }
func (m *MockStore) Import(r io.Reader) chan struct{} { return nil }
func (m *MockStore) Export(c context.T, w io.Writer, pubkeys ...[]byte) {}
func (m *MockStore) Sync() error { return nil }
func (m *MockStore) SetLogLevel(level string) {}
func (m *MockStore) EventIdsBySerial(start uint64, count int) ([]eventidserial.E, error) { return nil, nil }
func (m *MockStore) QueryForIds(c context.T, f *filter.T) ([]store.IdTsPk, error) { return nil, nil }
func TestServer_Publish_EphemeralEvent(t *testing.T) {
s := &Server{
Store: &MockStore{},
}
evt := &event.T{
Kind: kind.New(20000), // Ephemeral event
}
err := s.Publish(context.Bg(), evt)
if err != nil {
t.Errorf("Publish() error = %v, want nil", err)
}
// Ephemeral events should not be stored
mockStore := s.Store.(*MockStore)
if len(mockStore.events) != 0 {
t.Errorf("Expected no events to be stored for ephemeral event, got %d", len(mockStore.events))
}
}
func TestServer_Publish_ReplaceableEvent(t *testing.T) {
pubkey := make([]byte, 32)
pubkey[0] = 1
oldEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.ProfileMetadata,
CreatedAt: timestamp.New(1000),
}
oldEvent.Id[0] = 1
newEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.ProfileMetadata,
CreatedAt: timestamp.New(2000),
}
newEvent.Id[0] = 2
mockStore := &MockStore{
events: []*event.T{oldEvent},
}
s := &Server{
Store: mockStore,
}
t.Logf("Before Publish: events=%d, deletedEvents=%d", len(mockStore.events), len(mockStore.deletedEvents))
err := s.Publish(context.Bg(), newEvent)
t.Logf("After Publish: events=%d, deletedEvents=%d", len(mockStore.events), len(mockStore.deletedEvents))
if err != nil {
t.Errorf("Publish() error = %v, want nil", err)
}
// Should have saved the new event
if len(mockStore.events) != 2 {
t.Errorf("Expected 2 events after publish, got %d", len(mockStore.events))
}
// Should have deleted the old event (deferred functions execute after Publish returns)
if len(mockStore.deletedEvents) != 1 {
t.Errorf("Expected 1 deleted event, got %d", len(mockStore.deletedEvents))
}
if len(mockStore.deletedEvents) > 0 && !bytes.Equal(mockStore.deletedEvents[0], oldEvent.Id) {
t.Errorf("Expected to delete old event %x, got %x", oldEvent.Id, mockStore.deletedEvents[0])
}
}
func TestServer_Publish_ReplaceableEvent_NewerExists(t *testing.T) {
pubkey := make([]byte, 32)
pubkey[0] = 1
newerEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.ProfileMetadata,
CreatedAt: timestamp.New(2000),
}
newerEvent.Id[0] = 1
olderEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.ProfileMetadata,
CreatedAt: timestamp.New(1000),
}
olderEvent.Id[0] = 2
mockStore := &MockStore{
events: []*event.T{newerEvent},
}
s := &Server{
Store: mockStore,
}
err := s.Publish(context.Bg(), olderEvent)
if err == nil {
t.Error("Expected error when trying to replace newer event, got nil")
}
if !bytes.Contains([]byte(err.Error()), []byte("not replacing newer replaceable event")) {
t.Errorf("Expected error about not replacing newer event, got: %v", err)
}
}
func TestServer_Publish_ParameterizedReplaceableEvent(t *testing.T) {
pubkey := make([]byte, 32)
pubkey[0] = 1
dTag := "test-identifier"
oldEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.New(30000), // Parameterized replaceable
CreatedAt: timestamp.New(1000),
Tags: tags.New(tag.New("d", dTag)),
}
oldEvent.Id[0] = 1
newEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.New(30000),
CreatedAt: timestamp.New(2000),
Tags: tags.New(tag.New("d", dTag)),
}
newEvent.Id[0] = 2
mockStore := &MockStore{
events: []*event.T{oldEvent},
}
s := &Server{
Store: mockStore,
}
err := s.Publish(context.Bg(), newEvent)
if err != nil {
t.Errorf("Publish() error = %v, want nil", err)
}
// Should have saved the new event
if len(mockStore.events) != 2 {
t.Errorf("Expected 2 events after publish, got %d", len(mockStore.events))
}
// Should have deleted the old event
if len(mockStore.deletedEvents) != 1 {
t.Errorf("Expected 1 deleted event, got %d", len(mockStore.deletedEvents))
}
}
func TestServer_Publish_ParameterizedReplaceableEvent_DifferentDTag(t *testing.T) {
pubkey := make([]byte, 32)
pubkey[0] = 1
oldEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.New(30000),
CreatedAt: timestamp.New(1000),
Tags: tags.New(tag.New("d", "old-identifier")),
}
oldEvent.Id[0] = 1
newEvent := &event.T{
Id: make([]byte, 32),
Pubkey: pubkey,
Kind: kind.New(30000),
CreatedAt: timestamp.New(2000),
Tags: tags.New(tag.New("d", "new-identifier")),
}
newEvent.Id[0] = 2
mockStore := &MockStore{
events: []*event.T{oldEvent},
}
s := &Server{
Store: mockStore,
}
err := s.Publish(context.Bg(), newEvent)
if err != nil {
t.Errorf("Publish() error = %v, want nil", err)
}
// Should have saved the new event
if len(mockStore.events) != 2 {
t.Errorf("Expected 2 events after publish, got %d", len(mockStore.events))
}
// Should NOT have deleted the old event (different d tag)
if len(mockStore.deletedEvents) != 0 {
t.Errorf("Expected 0 deleted events for different d tag, got %d", len(mockStore.deletedEvents))
}
}
func TestServer_Publish_QueryError(t *testing.T) {
pubkey := make([]byte, 32)
evt := &event.T{
Pubkey: pubkey,
Kind: kind.ProfileMetadata,
}
mockStore := &MockStore{
queryError: errors.New("query failed"),
}
s := &Server{
Store: mockStore,
}
err := s.Publish(context.Bg(), evt)
if err == nil {
t.Error("Expected error from query failure, got nil")
}
if !bytes.Contains([]byte(err.Error()), []byte("failed to query before replacing")) {
t.Errorf("Expected query error message, got: %v", err)
}
}
func TestServer_Publish_SaveError(t *testing.T) {
evt := &event.T{
Kind: kind.TextNote,
}
mockStore := &MockStore{
saveError: errors.New("save failed"),
}
s := &Server{
Store: mockStore,
}
err := s.Publish(context.Bg(), evt)
if err == nil {
t.Error("Expected error from save failure, got nil")
}
if !bytes.Contains([]byte(err.Error()), []byte("failed to save")) {
t.Errorf("Expected save error message, got: %v", err)
}
}
func TestServer_Publish_DuplicateEvent(t *testing.T) {
evt := &event.T{
Kind: kind.TextNote,
}
mockStore := &MockStore{
saveError: store.ErrDupEvent,
}
s := &Server{
Store: mockStore,
}
err := s.Publish(context.Bg(), evt)
// Duplicate events should not return an error
if err != nil {
t.Errorf("Expected no error for duplicate event, got: %v", err)
}
}

View File

@@ -47,9 +47,9 @@ type Server struct {
// OwnersFollowed are "guests" of the followed and have full access but with
// rate limiting enabled.
ownersFollowed list.L
// // muted are on Owners' mute lists and do not have write access to the relay,
// // even if they would be in the OwnersFollowed list, they can only read.
// muted list.L
// muted are on Owners' mute lists and do not have write access to the relay,
// even if they would be in the OwnersFollowed list, they can only read.
muted list.L
// ownersFollowLists are the event IDs of owners follow lists, which must not be
// deleted, only replaced.
ownersFollowLists [][]byte