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:
@@ -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
191
realy/acceptevent_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
346
realy/server-publish_test.go
Normal file
346
realy/server-publish_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user