Skip to content

Commit 304ab58

Browse files
committed
test(query): add user.GetByEmail query example to test Scenario API
1 parent ad79163 commit 304ab58

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

internal/user/user_by_email.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package user
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"sync"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
12+
"github.com/get-eventually/go-eventually/event"
13+
"github.com/get-eventually/go-eventually/query"
14+
"github.com/get-eventually/go-eventually/version"
15+
)
16+
17+
// View is a public-facing representation of a User entity.
18+
// Can be obtained through a Query handler.
19+
type View struct {
20+
ID uuid.UUID
21+
Email string
22+
FirstName, LastName string
23+
BirthDate time.Time
24+
25+
Version version.Version // NOTE: used to avoid re-processing of already-processed events.
26+
}
27+
28+
// ErrNotFound is returned by a Query when a specific User has not been found.
29+
var ErrNotFound = errors.New("user: not found")
30+
31+
var (
32+
_ query.Query = GetByEmail("test@email.com")
33+
_ query.ProcessorHandler[GetByEmail, View] = new(GetByEmailHandler)
34+
)
35+
36+
// GetByEmail is a Domain Query that can be used to fetch a specific User given its email.
37+
type GetByEmail string
38+
39+
// Name implements query.Query.
40+
func (GetByEmail) Name() string { return "GetUserByEmail" }
41+
42+
// GetByEmailHandler is a stateful Query Handler that maintains a list of Users
43+
// indexed by their email.
44+
//
45+
// It can be used to answer GetByEmail queries.
46+
//
47+
// GetByEmailHandler is thread-safe.
48+
type GetByEmailHandler struct {
49+
mx sync.RWMutex
50+
data map[string]View
51+
idToEmail map[uuid.UUID]string
52+
}
53+
54+
// NewGetByEmailHandler creates a new GetByEmailHandler instance.
55+
func NewGetByEmailHandler() *GetByEmailHandler {
56+
handler := new(GetByEmailHandler)
57+
handler.data = make(map[string]View)
58+
handler.idToEmail = make(map[uuid.UUID]string)
59+
60+
return handler
61+
}
62+
63+
// Handle implements query.Handler.
64+
func (handler *GetByEmailHandler) Handle(_ context.Context, q query.Envelope[GetByEmail]) (View, error) {
65+
handler.mx.RLock()
66+
defer handler.mx.RUnlock()
67+
68+
user, ok := handler.data[string(q.Message)]
69+
if !ok {
70+
return View{}, fmt.Errorf("user.GetByEmailHandler: failed to get User by email, %w", ErrNotFound)
71+
}
72+
73+
return user, nil
74+
}
75+
76+
// Process implements event.Processor.
77+
func (handler *GetByEmailHandler) Process(_ context.Context, evt event.Persisted) error {
78+
handler.mx.Lock()
79+
defer handler.mx.Unlock()
80+
81+
userEvent, ok := evt.Envelope.Message.(*Event)
82+
if !ok {
83+
return fmt.Errorf("user.GetByEmailHandler: unexpected event type, %T", evt.Envelope.Message)
84+
}
85+
86+
switch kind := userEvent.Kind.(type) {
87+
case *WasCreated:
88+
handler.idToEmail[userEvent.ID] = kind.Email
89+
handler.data[kind.Email] = View{
90+
ID: userEvent.ID,
91+
Email: kind.Email,
92+
FirstName: kind.FirstName,
93+
LastName: kind.LastName,
94+
BirthDate: kind.BirthDate,
95+
Version: evt.Version,
96+
}
97+
98+
case *EmailWasUpdated:
99+
previousEmail, ok := handler.idToEmail[userEvent.ID]
100+
if !ok {
101+
return fmt.Errorf("user.GetByEmailHandler: expected id to have been registered, none found")
102+
}
103+
104+
view, ok := handler.data[previousEmail]
105+
if !ok {
106+
return fmt.Errorf("user.GetByEmailHandler: expected view to be registered, none found")
107+
}
108+
109+
if view.Version >= evt.Version {
110+
return nil
111+
}
112+
113+
view.Email = kind.Email
114+
handler.idToEmail[userEvent.ID] = view.Email
115+
handler.data[view.Email] = view
116+
117+
default:
118+
return fmt.Errorf("user.GetByEmailHandler: unexpected User event kind, %T", kind)
119+
}
120+
121+
return nil
122+
}

query/scenario_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package query_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/google/uuid"
8+
9+
"github.com/get-eventually/go-eventually/event"
10+
"github.com/get-eventually/go-eventually/internal/user"
11+
"github.com/get-eventually/go-eventually/query"
12+
)
13+
14+
func TestScenario(t *testing.T) {
15+
id := uuid.New()
16+
now := time.Now()
17+
before := now.Add(-1 * time.Minute)
18+
19+
expected := user.View{
20+
ID: id,
21+
Version: 1,
22+
Email: "me@email.com",
23+
FirstName: "John",
24+
LastName: "Doe",
25+
BirthDate: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
26+
}
27+
28+
makeQueryHandler := func(es event.Store) *user.GetByEmailHandler {
29+
return user.NewGetByEmailHandler()
30+
}
31+
32+
t.Run("returns the expected User by its email when it was just created", func(t *testing.T) {
33+
query.
34+
Scenario[user.GetByEmail, user.View, *user.GetByEmailHandler]().
35+
Given(event.Persisted{
36+
StreamID: event.StreamID(id.String()),
37+
Version: 1,
38+
Envelope: event.ToEnvelope(&user.Event{
39+
ID: id,
40+
RecordTime: before,
41+
Kind: &user.WasCreated{
42+
FirstName: expected.FirstName,
43+
LastName: expected.LastName,
44+
BirthDate: expected.BirthDate,
45+
Email: expected.Email,
46+
},
47+
}),
48+
}).
49+
When(query.ToEnvelope(user.GetByEmail(expected.Email))).
50+
Then(expected).
51+
AssertOn(t, makeQueryHandler)
52+
})
53+
54+
t.Run("returns user.ErrNotFound if the requested User does not exist", func(t *testing.T) {
55+
query.
56+
Scenario[user.GetByEmail, user.View, *user.GetByEmailHandler]().
57+
Given().
58+
When(query.ToEnvelope(user.GetByEmail(expected.Email))).
59+
ThenError(user.ErrNotFound).
60+
AssertOn(t, makeQueryHandler)
61+
})
62+
}

0 commit comments

Comments
 (0)