Wrote ValidatedEvent
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
// ValidatedEvent is an immutable wrapper around a fully validated event. It
|
||||||
|
// shares no memory with the original Event struct.
|
||||||
|
//
|
||||||
|
// When created with NewValidatedEvent, the wrapped event is guaranteed to:
|
||||||
|
// - have a valid structure
|
||||||
|
// - have a valid ID
|
||||||
|
// - have a valid signature
|
||||||
|
type ValidatedEvent struct {
|
||||||
|
event Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidatedEvent validates the provided Event. If valid, returns an
|
||||||
|
// immutable ValidatedEvent. Otherwise returns an error.
|
||||||
|
func NewValidatedEvent(e Event) (ValidatedEvent, error) {
|
||||||
|
if err := Validate(e); err != nil {
|
||||||
|
return ValidatedEvent{}, err
|
||||||
|
}
|
||||||
|
e.Tags = deepCopyTags(e.Tags)
|
||||||
|
return ValidatedEvent{event: e}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ValidatedEvent) ID() string { return v.event.ID }
|
||||||
|
func (v ValidatedEvent) PubKey() string { return v.event.PubKey }
|
||||||
|
func (v ValidatedEvent) CreatedAt() int64 { return v.event.CreatedAt }
|
||||||
|
func (v ValidatedEvent) Kind() int { return v.event.Kind }
|
||||||
|
func (v ValidatedEvent) Content() string { return v.event.Content }
|
||||||
|
func (v ValidatedEvent) Sig() string { return v.event.Sig }
|
||||||
|
|
||||||
|
// Tags returns a deep copy of the event's tag list. The returned slice shares
|
||||||
|
// no memory with the ValidatedEvent.
|
||||||
|
func (v ValidatedEvent) Tags() []Tag { return deepCopyTags(v.event.Tags) }
|
||||||
|
|
||||||
|
// Event returns a deep copy of the underlying Event. The returned Event shares
|
||||||
|
// no memory with the ValidatedEvent.
|
||||||
|
func (v ValidatedEvent) Event() Event {
|
||||||
|
e := v.event
|
||||||
|
e.Tags = deepCopyTags(v.event.Tags)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepCopyTags(tags []Tag) []Tag {
|
||||||
|
cp := make([]Tag, len(tags))
|
||||||
|
for i, tag := range tags {
|
||||||
|
tagcp := make(Tag, len(tag))
|
||||||
|
copy(tagcp, tag)
|
||||||
|
cp[i] = tagcp
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ValidatedEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
return v.event.MarshalJSON()
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testEventWithTags = NewEvent(
|
||||||
|
WithID("c9a0f84fcaa889654da8992105eb122eb210c8cbd58210609a5ef7e170b51400"),
|
||||||
|
WithPubKey(testPK),
|
||||||
|
WithCreatedAt(1760740551),
|
||||||
|
WithKind(1),
|
||||||
|
WithTag(Tag{"a", "value"}),
|
||||||
|
WithTag(Tag{"b", "value", "optional"}),
|
||||||
|
WithContent("valid event"),
|
||||||
|
WithSig("668a715f1eb983172acf230d17bd283daedb2598adf8de4290bcc7eb0b802fdb60669d1e7d1104ac70393f4dbccd07e8abf897152af6ce6c0a75499874e27f14"),
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidatedEventConstruction(t *testing.T) {
|
||||||
|
t.Run("accepts valid event", func(t *testing.T) {
|
||||||
|
_, err := NewValidatedEvent(testEvent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects invalid structure", func(t *testing.T) {
|
||||||
|
bad := NewEvent(
|
||||||
|
WithID(testEvent.ID),
|
||||||
|
WithPubKey("notavalidpubkey"),
|
||||||
|
WithCreatedAt(testEvent.CreatedAt),
|
||||||
|
WithKind(testEvent.Kind),
|
||||||
|
WithContent(testEvent.Content),
|
||||||
|
WithSig(testEvent.Sig),
|
||||||
|
)
|
||||||
|
_, err := NewValidatedEvent(bad)
|
||||||
|
assert.ErrorContains(t, err, "public key must be 64 lowercase hex characters")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects id mismatch", func(t *testing.T) {
|
||||||
|
bad := NewEvent(
|
||||||
|
WithID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||||
|
WithPubKey(testEvent.PubKey),
|
||||||
|
WithCreatedAt(testEvent.CreatedAt),
|
||||||
|
WithKind(testEvent.Kind),
|
||||||
|
WithContent(testEvent.Content),
|
||||||
|
WithSig(testEvent.Sig),
|
||||||
|
)
|
||||||
|
_, err := NewValidatedEvent(bad)
|
||||||
|
assert.ErrorContains(t, err, "does not match computed id")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects invalid signature", func(t *testing.T) {
|
||||||
|
bad := NewEvent(
|
||||||
|
WithID(testEvent.ID),
|
||||||
|
WithPubKey(testEvent.PubKey),
|
||||||
|
WithCreatedAt(testEvent.CreatedAt),
|
||||||
|
WithKind(testEvent.Kind),
|
||||||
|
WithContent(testEvent.Content),
|
||||||
|
WithSig("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||||
|
)
|
||||||
|
_, err := NewValidatedEvent(bad)
|
||||||
|
assert.ErrorContains(t, err, "event signature is invalid")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatedEventAccessors(t *testing.T) {
|
||||||
|
ve, err := NewValidatedEvent(testEventWithTags)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("ID", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.ID, ve.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PubKey", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.PubKey, ve.PubKey())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreatedAt", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.CreatedAt, ve.CreatedAt())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Kind", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.Kind, ve.Kind())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Content", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.Content, ve.Content())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Sig", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.Sig, ve.Sig())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Tags", func(t *testing.T) {
|
||||||
|
assert.Equal(t, testEventWithTags.Tags, ve.Tags())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatedEventTagsImmutability(t *testing.T) {
|
||||||
|
t.Run("outer slice mutation is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
tags := ve.Tags()
|
||||||
|
tags[0] = Tag{"z", "injected"}
|
||||||
|
assert.Equal(t, Tag{"a", "value"}, ve.Tags()[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inner slice mutation is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
tags := ve.Tags()
|
||||||
|
tags[0][1] = "mutated"
|
||||||
|
assert.Equal(t, "value", ve.Tags()[0][1])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("append to outer slice is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
originalLen := len(ve.Tags())
|
||||||
|
tags := ve.Tags()
|
||||||
|
tags = append(tags, Tag{"new", "tag"})
|
||||||
|
_ = tags
|
||||||
|
assert.Equal(t, originalLen, len(ve.Tags()))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("append to inner slice is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
originalLen := len(ve.Tags()[1])
|
||||||
|
tags := ve.Tags()
|
||||||
|
tags[1] = append(tags[1], "extra")
|
||||||
|
assert.Equal(t, originalLen, len(ve.Tags()[1]))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("successive calls return independent copies", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
first := ve.Tags()
|
||||||
|
second := ve.Tags()
|
||||||
|
first[0][0] = "z"
|
||||||
|
assert.Equal(t, "a", second[0][0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatedEventEventImmutability(t *testing.T) {
|
||||||
|
t.Run("outer tags mutation is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
e := ve.Event()
|
||||||
|
e.Tags[0] = Tag{"z", "injected"}
|
||||||
|
assert.Equal(t, Tag{"a", "value"}, ve.Tags()[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inner tags mutation is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
e := ve.Event()
|
||||||
|
e.Tags[0][1] = "mutated"
|
||||||
|
assert.Equal(t, "value", ve.Tags()[0][1])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scalar field mutation is isolated", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEvent)
|
||||||
|
e := ve.Event()
|
||||||
|
e.Content = "tampered"
|
||||||
|
assert.Equal(t, testEvent.Content, ve.Content())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("successive calls return independent copies", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
first := ve.Event()
|
||||||
|
second := ve.Event()
|
||||||
|
first.Tags[0][0] = "z"
|
||||||
|
assert.Equal(t, "a", second.Tags[0][0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returned event passes validation", func(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEventWithTags)
|
||||||
|
assert.NoError(t, Validate(ve.Event()))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("source event mutation after construction is isolated", func(t *testing.T) {
|
||||||
|
source := NewEvent(
|
||||||
|
WithID(testEventWithTags.ID),
|
||||||
|
WithPubKey(testEventWithTags.PubKey),
|
||||||
|
WithCreatedAt(testEventWithTags.CreatedAt),
|
||||||
|
WithKind(testEventWithTags.Kind),
|
||||||
|
WithTag(Tag{"a", "value"}),
|
||||||
|
WithTag(Tag{"b", "value", "optional"}),
|
||||||
|
WithContent(testEventWithTags.Content),
|
||||||
|
WithSig(testEventWithTags.Sig),
|
||||||
|
)
|
||||||
|
ve, err := NewValidatedEvent(source)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
source.Tags[0][1] = "mutated"
|
||||||
|
source.Content = "tampered"
|
||||||
|
assert.Equal(t, "value", ve.Tags()[0][1])
|
||||||
|
assert.Equal(t, testEventWithTags.Content, ve.Content())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatedEventMarshalJSON(t *testing.T) {
|
||||||
|
ve, _ := NewValidatedEvent(testEvent)
|
||||||
|
jsonBytes, err := json.Marshal(ve)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testEventJSON, string(jsonBytes))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user