From 587986d2bdaae03e40fbd73313e07957374f298a Mon Sep 17 00:00:00 2001 From: boreq Date: Wed, 29 Nov 2023 19:34:39 +0100 Subject: [PATCH] Add and endpoint which can be used for listing events --- .gitignore | 1 + cmd/event-service/di/inject_application.go | 1 + cmd/event-service/di/inject_config.go | 15 +++ cmd/event-service/di/inject_logging.go | 7 +- cmd/event-service/di/wire.go | 44 +++++++++ cmd/event-service/di/wire_gen.go | 47 ++++++++- internal/fixtures/fixtures.go | 9 ++ service/adapters/mocks/contact_repository.go | 38 +++++++ service/adapters/mocks/event_repository.go | 45 +++++++++ service/adapters/mocks/metrics.go | 50 ++++++++++ .../public_keys_to_monitor_repository.go | 26 +++++ service/adapters/mocks/publisher.go | 18 ++++ service/adapters/mocks/relay_repository.go | 26 +++++ service/adapters/mocks/transaction.go | 21 ++++ service/adapters/sqlite/event_repository.go | 46 ++++++++- .../adapters/sqlite/event_repository_test.go | 66 +++++++++++++ service/app/app.go | 3 + service/app/handler_get_events.go | 78 +++++++++++++++ service/app/handler_get_events_test.go | 99 +++++++++++++++++++ service/ports/http/http.go | 54 +++++++++- 20 files changed, 685 insertions(+), 9 deletions(-) create mode 100644 cmd/event-service/di/inject_config.go create mode 100644 service/adapters/mocks/contact_repository.go create mode 100644 service/adapters/mocks/event_repository.go create mode 100644 service/adapters/mocks/metrics.go create mode 100644 service/adapters/mocks/public_keys_to_monitor_repository.go create mode 100644 service/adapters/mocks/publisher.go create mode 100644 service/adapters/mocks/relay_repository.go create mode 100644 service/adapters/mocks/transaction.go create mode 100644 service/app/handler_get_events.go create mode 100644 service/app/handler_get_events_test.go diff --git a/.gitignore b/.gitignore index 80fa825..4415bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ nos-notification-service-*.json database.sqlite database.sqlite-journal run.sh +_testdata diff --git a/cmd/event-service/di/inject_application.go b/cmd/event-service/di/inject_application.go index cdcf649..62d5eee 100644 --- a/cmd/event-service/di/inject_application.go +++ b/cmd/event-service/di/inject_application.go @@ -20,4 +20,5 @@ var applicationSet = wire.NewSet( app.NewAddPublicKeyToMonitorHandler, app.NewGetEventHandler, app.NewGetPublicKeyInfoHandler, + app.NewGetEventsHandler, ) diff --git a/cmd/event-service/di/inject_config.go b/cmd/event-service/di/inject_config.go new file mode 100644 index 0000000..59b2196 --- /dev/null +++ b/cmd/event-service/di/inject_config.go @@ -0,0 +1,15 @@ +package di + +import ( + "github.com/google/wire" + "github.com/planetary-social/nos-event-service/internal/logging" + "github.com/planetary-social/nos-event-service/service/config" +) + +var extractConfigSet = wire.NewSet( + logLevelFromConfig, +) + +func logLevelFromConfig(conf config.Config) logging.Level { + return conf.LogLevel() +} diff --git a/cmd/event-service/di/inject_logging.go b/cmd/event-service/di/inject_logging.go index b2b5d24..f045258 100644 --- a/cmd/event-service/di/inject_logging.go +++ b/cmd/event-service/di/inject_logging.go @@ -5,7 +5,6 @@ import ( "github.com/boreq/errors" "github.com/google/wire" "github.com/planetary-social/nos-event-service/internal/logging" - "github.com/planetary-social/nos-event-service/service/config" "github.com/sirupsen/logrus" ) @@ -16,13 +15,13 @@ var loggingSet = wire.NewSet( wire.Bind(new(watermill.LoggerAdapter), new(*logging.WatermillAdapter)), ) -func newLogger(conf config.Config) (logging.Logger, error) { - if conf.LogLevel() == logging.LevelDisabled { +func newLogger(level logging.Level) (logging.Logger, error) { + if level == logging.LevelDisabled { return logging.NewDevNullLogger(), nil } v := logrus.New() - switch conf.LogLevel() { + switch level { case logging.LevelTrace: v.SetLevel(logrus.TraceLevel) case logging.LevelDebug: diff --git a/cmd/event-service/di/wire.go b/cmd/event-service/di/wire.go index e24e272..996a3f5 100644 --- a/cmd/event-service/di/wire.go +++ b/cmd/event-service/di/wire.go @@ -11,6 +11,7 @@ import ( "github.com/google/wire" "github.com/planetary-social/nos-event-service/internal/fixtures" "github.com/planetary-social/nos-event-service/internal/logging" + "github.com/planetary-social/nos-event-service/service/adapters/mocks" "github.com/planetary-social/nos-event-service/service/adapters/sqlite" "github.com/planetary-social/nos-event-service/service/app" "github.com/planetary-social/nos-event-service/service/config" @@ -33,6 +34,7 @@ func BuildService(context.Context, config.Config) (Service, func(), error) { migrationsAdaptersSet, domainSet, externalPubsubSet, + extractConfigSet, ) return Service{}, nil, nil } @@ -46,10 +48,52 @@ func BuildTestAdapters(context.Context, testing.TB) (sqlite.TestedItems, func(), loggingSet, newTestAdaptersConfig, migrationsAdaptersSet, + extractConfigSet, ) return sqlite.TestedItems{}, nil, nil } +type TestApplication struct { + GetEventsHandler *app.GetEventsHandler + + EventRepository *mocks.EventRepository +} + +func BuildTestApplication(context.Context, testing.TB) (TestApplication, error) { + wire.Build( + wire.Struct(new(TestApplication), "*"), + + wire.Struct(new(app.Adapters), "*"), + + mocks.NewTransactionProvider, + wire.Bind(new(app.TransactionProvider), new(*mocks.TransactionProvider)), + + mocks.NewEventRepository, + wire.Bind(new(app.EventRepository), new(*mocks.EventRepository)), + + mocks.NewRelayRepository, + wire.Bind(new(app.RelayRepository), new(*mocks.RelayRepository)), + + mocks.NewContactRepository, + wire.Bind(new(app.ContactRepository), new(*mocks.ContactRepository)), + + mocks.NewPublicKeysToMonitorRepository, + wire.Bind(new(app.PublicKeysToMonitorRepository), new(*mocks.PublicKeysToMonitorRepository)), + + mocks.NewPublisher, + wire.Bind(new(app.Publisher), new(*mocks.Publisher)), + + mocks.NewMetrics, + wire.Bind(new(app.Metrics), new(*mocks.Metrics)), + + applicationSet, + loggingSet, + + wire.Value(logging.LevelError), + ) + return TestApplication{}, nil +} + func newTestAdaptersConfig(tb testing.TB) (config.Config, error) { return config.NewConfig( fixtures.SomeString(), diff --git a/cmd/event-service/di/wire_gen.go b/cmd/event-service/di/wire_gen.go index 3410dd4..52e7487 100644 --- a/cmd/event-service/di/wire_gen.go +++ b/cmd/event-service/di/wire_gen.go @@ -18,6 +18,7 @@ import ( "github.com/planetary-social/nos-event-service/service/adapters" "github.com/planetary-social/nos-event-service/service/adapters/gcp" "github.com/planetary-social/nos-event-service/service/adapters/memorypubsub" + "github.com/planetary-social/nos-event-service/service/adapters/mocks" "github.com/planetary-social/nos-event-service/service/adapters/prometheus" "github.com/planetary-social/nos-event-service/service/adapters/sqlite" "github.com/planetary-social/nos-event-service/service/app" @@ -33,7 +34,8 @@ import ( // Injectors from wire.go: func BuildService(contextContext context.Context, configConfig config.Config) (Service, func(), error) { - logger, err := newLogger(configConfig) + level := logLevelFromConfig(configConfig) + logger, err := newLogger(level) if err != nil { return Service{}, nil, err } @@ -71,6 +73,7 @@ func BuildService(contextContext context.Context, configConfig config.Config) (S addPublicKeyToMonitorHandler := app.NewAddPublicKeyToMonitorHandler(genericTransactionProvider, logger, prometheusPrometheus) getEventHandler := app.NewGetEventHandler(genericTransactionProvider, logger, prometheusPrometheus) getPublicKeyInfoHandler := app.NewGetPublicKeyInfoHandler(genericTransactionProvider, logger, prometheusPrometheus) + getEventsHandler := app.NewGetEventsHandler(genericTransactionProvider, logger, prometheusPrometheus) application := app.Application{ SaveReceivedEvent: saveReceivedEventHandler, ProcessSavedEvent: processSavedEventHandler, @@ -78,6 +81,7 @@ func BuildService(contextContext context.Context, configConfig config.Config) (S AddPublicKeyToMonitor: addPublicKeyToMonitorHandler, GetEvent: getEventHandler, GetPublicKeyInfo: getPublicKeyInfoHandler, + GetEvents: getEventsHandler, } server := http.NewServer(configConfig, logger, application, prometheusPrometheus) bootstrapRelaySource := relays.NewBootstrapRelaySource() @@ -113,7 +117,8 @@ func BuildTestAdapters(contextContext context.Context, tb testing.TB) (sqlite.Te if err != nil { return sqlite.TestedItems{}, nil, err } - logger, err := newLogger(configConfig) + level := logLevelFromConfig(configConfig) + logger, err := newLogger(level) if err != nil { return sqlite.TestedItems{}, nil, err } @@ -157,6 +162,38 @@ func BuildTestAdapters(contextContext context.Context, tb testing.TB) (sqlite.Te }, nil } +func BuildTestApplication(contextContext context.Context, tb testing.TB) (TestApplication, error) { + eventRepository := mocks.NewEventRepository() + relayRepository := mocks.NewRelayRepository() + contactRepository := mocks.NewContactRepository() + publicKeysToMonitorRepository := mocks.NewPublicKeysToMonitorRepository() + publisher := mocks.NewPublisher() + appAdapters := app.Adapters{ + Events: eventRepository, + Relays: relayRepository, + Contacts: contactRepository, + PublicKeysToMonitor: publicKeysToMonitorRepository, + Publisher: publisher, + } + transactionProvider := mocks.NewTransactionProvider(appAdapters) + level := _wireLevelValue + logger, err := newLogger(level) + if err != nil { + return TestApplication{}, err + } + metrics := mocks.NewMetrics() + getEventsHandler := app.NewGetEventsHandler(transactionProvider, logger, metrics) + testApplication := TestApplication{ + GetEventsHandler: getEventsHandler, + EventRepository: eventRepository, + } + return testApplication, nil +} + +var ( + _wireLevelValue = logging.LevelError +) + func buildTransactionSqliteAdapters(db *sql.DB, tx *sql.Tx, diBuildTransactionSqliteAdaptersDependencies buildTransactionSqliteAdaptersDependencies) (app.Adapters, error) { eventRepository, err := sqlite.NewEventRepository(tx) if err != nil { @@ -207,6 +244,12 @@ func buildTestTransactionSqliteAdapters(db *sql.DB, tx *sql.Tx, diBuildTransacti // wire.go: +type TestApplication struct { + GetEventsHandler *app.GetEventsHandler + + EventRepository *mocks.EventRepository +} + func newTestAdaptersConfig(tb testing.TB) (config.Config, error) { return config.NewConfig(fixtures.SomeString(), config.EnvironmentDevelopment, logging.LevelDebug, fixtures.SomeString(), nil, fixtures.SomeFile(tb)) } diff --git a/internal/fixtures/fixtures.go b/internal/fixtures/fixtures.go index 3f8f223..e0ff46f 100644 --- a/internal/fixtures/fixtures.go +++ b/internal/fixtures/fixtures.go @@ -13,6 +13,7 @@ import ( "github.com/planetary-social/nos-event-service/internal" "github.com/planetary-social/nos-event-service/internal/logging" "github.com/planetary-social/nos-event-service/service/domain" + "github.com/stretchr/testify/require" ) func SomePublicKey() domain.PublicKey { @@ -198,3 +199,11 @@ func randSeq(n int) string { func somePrivateKeyHex() string { return nostr.GeneratePrivateKey() } + +func RequireEqualEventSlices(tb testing.TB, a, b []domain.Event) { + require.Equal(tb, len(a), len(b)) + for i := 0; i < len(a); i++ { + require.Equal(tb, a[i].Id(), b[i].Id()) + require.Equal(tb, a[i].Raw(), b[i].Raw()) + } +} diff --git a/service/adapters/mocks/contact_repository.go b/service/adapters/mocks/contact_repository.go new file mode 100644 index 0000000..816f0a7 --- /dev/null +++ b/service/adapters/mocks/contact_repository.go @@ -0,0 +1,38 @@ +package mocks + +import ( + "context" + + "github.com/planetary-social/nos-event-service/service/domain" +) + +type ContactRepository struct { +} + +func NewContactRepository() *ContactRepository { + return &ContactRepository{} +} + +func (c ContactRepository) GetCurrentContactsEvent(ctx context.Context, author domain.PublicKey) (domain.Event, error) { + panic("implement me") +} + +func (c ContactRepository) SetContacts(ctx context.Context, event domain.Event, contacts []domain.PublicKey) error { + panic("implement me") +} + +func (c ContactRepository) GetFollowees(ctx context.Context, publicKey domain.PublicKey) ([]domain.PublicKey, error) { + panic("implement me") +} + +func (c ContactRepository) IsFolloweeOfMonitoredPublicKey(ctx context.Context, publicKey domain.PublicKey) (bool, error) { + panic("implement me") +} + +func (c ContactRepository) CountFollowers(ctx context.Context, publicKey domain.PublicKey) (int, error) { + panic("implement me") +} + +func (c ContactRepository) CountFollowees(ctx context.Context, publicKey domain.PublicKey) (int, error) { + panic("implement me") +} diff --git a/service/adapters/mocks/event_repository.go b/service/adapters/mocks/event_repository.go new file mode 100644 index 0000000..73ff445 --- /dev/null +++ b/service/adapters/mocks/event_repository.go @@ -0,0 +1,45 @@ +package mocks + +import ( + "context" + + "github.com/planetary-social/nos-event-service/service/domain" +) + +type EventRepository struct { + ListCalls []EventRepositoryListCall + ListReturnValue []domain.Event +} + +func NewEventRepository() *EventRepository { + return &EventRepository{} +} + +func (e EventRepository) Save(ctx context.Context, event domain.Event) error { + panic("implement me") +} + +func (e EventRepository) Get(ctx context.Context, eventID domain.EventId) (domain.Event, error) { + panic("implement me") +} + +func (e EventRepository) Exists(ctx context.Context, eventID domain.EventId) (bool, error) { + panic("implement me") +} + +func (e EventRepository) Count(ctx context.Context) (int, error) { + panic("implement me") +} + +func (e *EventRepository) List(ctx context.Context, after *domain.EventId, limit int) ([]domain.Event, error) { + e.ListCalls = append(e.ListCalls, EventRepositoryListCall{ + After: after, + Limit: limit, + }) + return e.ListReturnValue, nil +} + +type EventRepositoryListCall struct { + After *domain.EventId + Limit int +} diff --git a/service/adapters/mocks/metrics.go b/service/adapters/mocks/metrics.go new file mode 100644 index 0000000..e411f6c --- /dev/null +++ b/service/adapters/mocks/metrics.go @@ -0,0 +1,50 @@ +package mocks + +import ( + "time" + + "github.com/planetary-social/nos-event-service/service/app" + "github.com/planetary-social/nos-event-service/service/domain" +) + +type Metrics struct { +} + +func NewMetrics() *Metrics { + return &Metrics{} +} + +func (m Metrics) StartApplicationCall(handlerName string) app.ApplicationCall { + return NewApplicationCall() +} + +func (m Metrics) ReportNumberOfRelayDownloaders(n int) { +} + +func (m Metrics) ReportReceivedEvent(address domain.RelayAddress) { +} + +func (m Metrics) ReportQueueLength(topic string, n int) { +} + +func (m Metrics) ReportQueueOldestMessageAge(topic string, age time.Duration) { +} + +func (m Metrics) ReportNumberOfStoredRelayAddresses(n int) { +} + +func (m Metrics) ReportNumberOfStoredEvents(n int) { +} + +func (m Metrics) ReportEventSentToRelay(address domain.RelayAddress, decision app.SendEventToRelayDecision, result app.SendEventToRelayResult) { +} + +type ApplicationCall struct { +} + +func NewApplicationCall() *ApplicationCall { + return &ApplicationCall{} +} + +func (a ApplicationCall) End(err *error) { +} diff --git a/service/adapters/mocks/public_keys_to_monitor_repository.go b/service/adapters/mocks/public_keys_to_monitor_repository.go new file mode 100644 index 0000000..3c225ba --- /dev/null +++ b/service/adapters/mocks/public_keys_to_monitor_repository.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "context" + + "github.com/planetary-social/nos-event-service/service/domain" +) + +type PublicKeysToMonitorRepository struct { +} + +func NewPublicKeysToMonitorRepository() *PublicKeysToMonitorRepository { + return &PublicKeysToMonitorRepository{} +} + +func (p PublicKeysToMonitorRepository) Save(ctx context.Context, publicKeyToMonitor domain.PublicKeyToMonitor) error { + panic("implement me") +} + +func (p PublicKeysToMonitorRepository) List(ctx context.Context) ([]domain.PublicKeyToMonitor, error) { + panic("implement me") +} + +func (p PublicKeysToMonitorRepository) Get(ctx context.Context, publicKey domain.PublicKey) (domain.PublicKeyToMonitor, error) { + panic("implement me") +} diff --git a/service/adapters/mocks/publisher.go b/service/adapters/mocks/publisher.go new file mode 100644 index 0000000..f45f7cb --- /dev/null +++ b/service/adapters/mocks/publisher.go @@ -0,0 +1,18 @@ +package mocks + +import ( + "context" + + "github.com/planetary-social/nos-event-service/service/domain" +) + +type Publisher struct { +} + +func NewPublisher() *Publisher { + return &Publisher{} +} + +func (p Publisher) PublishEventSaved(ctx context.Context, id domain.EventId) error { + panic("implement me") +} diff --git a/service/adapters/mocks/relay_repository.go b/service/adapters/mocks/relay_repository.go new file mode 100644 index 0000000..174ea60 --- /dev/null +++ b/service/adapters/mocks/relay_repository.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "context" + + "github.com/planetary-social/nos-event-service/service/domain" +) + +type RelayRepository struct { +} + +func NewRelayRepository() *RelayRepository { + return &RelayRepository{} +} + +func (r RelayRepository) Save(ctx context.Context, eventID domain.EventId, relayAddress domain.MaybeRelayAddress) error { + panic("implement me") +} + +func (r RelayRepository) List(ctx context.Context) ([]domain.MaybeRelayAddress, error) { + panic("implement me") +} + +func (r RelayRepository) Count(ctx context.Context) (int, error) { + panic("implement me") +} diff --git a/service/adapters/mocks/transaction.go b/service/adapters/mocks/transaction.go new file mode 100644 index 0000000..122acd1 --- /dev/null +++ b/service/adapters/mocks/transaction.go @@ -0,0 +1,21 @@ +package mocks + +import ( + "context" + + "github.com/planetary-social/nos-event-service/service/app" +) + +type TransactionProvider struct { + adapters app.Adapters +} + +func NewTransactionProvider(adapters app.Adapters) *TransactionProvider { + return &TransactionProvider{ + adapters: adapters, + } +} + +func (t TransactionProvider) Transact(ctx context.Context, f func(context.Context, app.Adapters) error) error { + return f(ctx, t.adapters) +} diff --git a/service/adapters/sqlite/event_repository.go b/service/adapters/sqlite/event_repository.go index 77dbaaa..aaec652 100644 --- a/service/adapters/sqlite/event_repository.go +++ b/service/adapters/sqlite/event_repository.go @@ -75,10 +75,52 @@ func (r *EventRepository) Exists(ctx context.Context, eventID domain.EventId) (b return true, nil } -func (m *EventRepository) readEvent(result *sql.Row) (domain.Event, error) { +func (r *EventRepository) List(ctx context.Context, after *domain.EventId, limit int) ([]domain.Event, error) { + rows, err := r.listQuery(after, limit) + if err != nil { + return nil, errors.Wrap(err, "error querying") + } + + return r.readEvents(rows) +} + +func (r *EventRepository) listQuery(after *domain.EventId, limit int) (*sql.Rows, error) { + if after != nil { + return r.tx.Query(` + SELECT payload + FROM events + WHERE event_id > $1 + ORDER BY event_id + LIMIT $2`, + after.Hex(), limit, + ) + } + + return r.tx.Query(` + SELECT payload + FROM events + ORDER BY event_id + LIMIT $1`, + limit, + ) +} + +func (r *EventRepository) readEvents(rows *sql.Rows) ([]domain.Event, error) { + var events []domain.Event + for rows.Next() { + event, err := r.readEvent(rows) + if err != nil { + return nil, errors.Wrap(err, "error reading an event") + } + events = append(events, event) + } + return events, nil +} + +func (m *EventRepository) readEvent(scanner scanner) (domain.Event, error) { var payload []byte - if err := result.Scan(&payload); err != nil { + if err := scanner.Scan(&payload); err != nil { if errors.Is(err, sql.ErrNoRows) { return domain.Event{}, app.ErrEventNotFound } diff --git a/service/adapters/sqlite/event_repository_test.go b/service/adapters/sqlite/event_repository_test.go index e2ce13d..4368cee 100644 --- a/service/adapters/sqlite/event_repository_test.go +++ b/service/adapters/sqlite/event_repository_test.go @@ -2,11 +2,15 @@ package sqlite_test import ( "context" + "slices" + "strings" "testing" + "github.com/planetary-social/nos-event-service/internal" "github.com/planetary-social/nos-event-service/internal/fixtures" "github.com/planetary-social/nos-event-service/service/adapters/sqlite" "github.com/planetary-social/nos-event-service/service/app" + "github.com/planetary-social/nos-event-service/service/domain" "github.com/stretchr/testify/require" ) @@ -144,3 +148,65 @@ func TestEventRepository_ExistsChecksIfEventsExist(t *testing.T) { }) require.NoError(t, err) } + +func TestEventRepository_ListReturnsNoEventsIfRepositoryIsEmpty(t *testing.T) { + ctx := fixtures.TestContext(t) + adapters := NewTestAdapters(ctx, t) + + err := adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + events, err := adapters.EventRepository.List(ctx, nil, 10) + require.NoError(t, err) + require.Empty(t, events) + + return nil + }) + require.NoError(t, err) +} + +func TestEventRepository_ListReturnsEventsIfRepositoryIsNotEmpty(t *testing.T) { + ctx := fixtures.TestContext(t) + adapters := NewTestAdapters(ctx, t) + + var savedEvents []domain.Event + for i := 0; i < 10; i++ { + savedEvents = append(savedEvents, fixtures.SomeEvent()) + } + + slices.SortFunc(savedEvents, func(a, b domain.Event) int { + return strings.Compare(a.Id().Hex(), b.Id().Hex()) + }) + + err := adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + for _, event := range savedEvents { + err := adapters.EventRepository.Save(ctx, event) + require.NoError(t, err) + } + return nil + }) + require.NoError(t, err) + + err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + events, err := adapters.EventRepository.List(ctx, nil, 2) + require.NoError(t, err) + fixtures.RequireEqualEventSlices(t, + []domain.Event{ + savedEvents[0], + savedEvents[1], + }, + events, + ) + + events, err = adapters.EventRepository.List(ctx, internal.Pointer(savedEvents[1].Id()), 2) + require.NoError(t, err) + fixtures.RequireEqualEventSlices(t, + []domain.Event{ + savedEvents[2], + savedEvents[3], + }, + events, + ) + + return nil + }) + require.NoError(t, err) +} diff --git a/service/app/app.go b/service/app/app.go index f22de6e..fd5cf88 100644 --- a/service/app/app.go +++ b/service/app/app.go @@ -35,6 +35,8 @@ type EventRepository interface { Exists(ctx context.Context, eventID domain.EventId) (bool, error) Count(ctx context.Context) (int, error) + + List(ctx context.Context, after *domain.EventId, limit int) ([]domain.Event, error) } type RelayRepository interface { @@ -76,6 +78,7 @@ type Application struct { AddPublicKeyToMonitor *AddPublicKeyToMonitorHandler GetEvent *GetEventHandler GetPublicKeyInfo *GetPublicKeyInfoHandler + GetEvents *GetEventsHandler } type ReceivedEvent struct { diff --git a/service/app/handler_get_events.go b/service/app/handler_get_events.go new file mode 100644 index 0000000..6a89ea1 --- /dev/null +++ b/service/app/handler_get_events.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + + "github.com/boreq/errors" + "github.com/planetary-social/nos-event-service/internal/logging" + "github.com/planetary-social/nos-event-service/service/domain" +) + +const getEventsLimit = 100 + +type GetEvents struct { + after *domain.EventId +} + +func NewGetEvents(after *domain.EventId) GetEvents { + return GetEvents{after: after} +} + +type GetEventsResult struct { + events []domain.Event + thereIsMoreEvents bool +} + +func NewGetEventsResult(events []domain.Event, thereIsMoreEvents bool) GetEventsResult { + return GetEventsResult{ + events: events, + thereIsMoreEvents: thereIsMoreEvents, + } +} + +func (g GetEventsResult) Events() []domain.Event { + return g.events +} + +func (g GetEventsResult) ThereIsMoreEvents() bool { + return g.thereIsMoreEvents +} + +type GetEventsHandler struct { + transactionProvider TransactionProvider + logger logging.Logger + metrics Metrics +} + +func NewGetEventsHandler( + transactionProvider TransactionProvider, + logger logging.Logger, + metrics Metrics, +) *GetEventsHandler { + return &GetEventsHandler{ + transactionProvider: transactionProvider, + logger: logger.New("getEventsHandler"), + metrics: metrics, + } +} + +func (h *GetEventsHandler) Handle(ctx context.Context, cmd GetEvents) (result GetEventsResult, err error) { + defer h.metrics.StartApplicationCall("getEvents").End(&err) + + var events []domain.Event + if err := h.transactionProvider.Transact(ctx, func(ctx context.Context, adapters Adapters) error { + tmp, err := adapters.Events.List(ctx, cmd.after, getEventsLimit+1) + if err != nil { + return errors.Wrap(err, "error getting the event") + } + events = tmp + return nil + }); err != nil { + return GetEventsResult{}, errors.Wrap(err, "transaction error") + } + + if len(events) > getEventsLimit { + return NewGetEventsResult(events[:len(events)-1], true), nil + } + return NewGetEventsResult(events, false), nil +} diff --git a/service/app/handler_get_events_test.go b/service/app/handler_get_events_test.go new file mode 100644 index 0000000..d15e124 --- /dev/null +++ b/service/app/handler_get_events_test.go @@ -0,0 +1,99 @@ +package app_test + +import ( + "testing" + + "github.com/planetary-social/nos-event-service/cmd/event-service/di" + "github.com/planetary-social/nos-event-service/internal" + "github.com/planetary-social/nos-event-service/internal/fixtures" + "github.com/planetary-social/nos-event-service/service/adapters/mocks" + "github.com/planetary-social/nos-event-service/service/app" + "github.com/planetary-social/nos-event-service/service/domain" + "github.com/stretchr/testify/require" +) + +func TestGetEventsHandler(t *testing.T) { + someEventID := fixtures.SomeEventID() + + var oneHundredEvents []domain.Event + for i := 0; i < 100; i++ { + oneHundredEvents = append(oneHundredEvents, fixtures.SomeEvent()) + } + event := fixtures.SomeEvent() + + testCases := []struct { + Name string + After *domain.EventId + EventRepositoryListReturnValue []domain.Event + ExpectedEventRepositoryCalls []mocks.EventRepositoryListCall + ExpectedResult app.GetEventsResult + }{ + { + Name: "one_event", + After: internal.Pointer(someEventID), + EventRepositoryListReturnValue: []domain.Event{ + event, + }, + ExpectedEventRepositoryCalls: []mocks.EventRepositoryListCall{ + { + After: internal.Pointer(someEventID), + Limit: 101, + }, + }, + ExpectedResult: app.NewGetEventsResult( + []domain.Event{ + event, + }, + false, + ), + }, + { + Name: "one_hundred_events", + After: internal.Pointer(someEventID), + EventRepositoryListReturnValue: oneHundredEvents, + ExpectedEventRepositoryCalls: []mocks.EventRepositoryListCall{ + { + After: internal.Pointer(someEventID), + Limit: 101, + }, + }, + ExpectedResult: app.NewGetEventsResult( + oneHundredEvents, + false, + ), + }, + { + Name: "one_hundred_events_plus_one", + After: internal.Pointer(someEventID), + EventRepositoryListReturnValue: append(oneHundredEvents, event), + ExpectedEventRepositoryCalls: []mocks.EventRepositoryListCall{ + { + After: internal.Pointer(someEventID), + Limit: 101, + }, + }, + ExpectedResult: app.NewGetEventsResult( + oneHundredEvents, + true, + ), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + ctx := fixtures.TestContext(t) + ts, err := di.BuildTestApplication(ctx, t) + require.NoError(t, err) + + ts.EventRepository.ListReturnValue = testCase.EventRepositoryListReturnValue + + result, err := ts.GetEventsHandler.Handle(ctx, app.NewGetEvents(testCase.After)) + require.NoError(t, err) + + fixtures.RequireEqualEventSlices(t, testCase.ExpectedResult.Events(), result.Events()) + require.Equal(t, testCase.ExpectedResult.ThereIsMoreEvents(), result.ThereIsMoreEvents()) + + require.Equal(t, testCase.ExpectedEventRepositoryCalls, ts.EventRepository.ListCalls) + }) + } +} diff --git a/service/ports/http/http.go b/service/ports/http/http.go index f04cefa..6612566 100644 --- a/service/ports/http/http.go +++ b/service/ports/http/http.go @@ -62,7 +62,8 @@ func (s *Server) ListenAndServe(ctx context.Context) error { func (s *Server) createMux() http.Handler { r := mux.NewRouter() r.Handle("/metrics", promhttp.HandlerFor(s.prometheus.Registry(), promhttp.HandlerOpts{})) - r.HandleFunc("/events/{id}", rest.Wrap(s.serveEvents)) + r.HandleFunc("/events", rest.Wrap(s.serveEvents)) + r.HandleFunc("/events/{id}", rest.Wrap(s.serveEvent)) r.HandleFunc("/public-keys/{hex}", rest.Wrap(s.servePublicKey)) r.HandleFunc("/", s.serveWs) return r @@ -97,6 +98,57 @@ func (s *Server) serveWs(rw http.ResponseWriter, r *http.Request) { } func (s *Server) serveEvents(r *http.Request) rest.RestResponse { + switch r.Method { + case http.MethodGet: + var after *domain.EventId + if afterString := r.URL.Query().Get("after"); afterString != "" { + id, err := domain.NewEventIdFromHex(afterString) + if err != nil { + return rest.ErrBadRequest.WithMessage("After must be a hex encoded public key.") + } + after = &id + } + + result, err := s.app.GetEvents.Handle(r.Context(), app.NewGetEvents(after)) + if err != nil { + s.logger.Error().WithError(err).Message("error getting events") + return rest.ErrInternalServerError + } + + response, err := newGetEventsResponse(result) + if err != nil { + s.logger.Error().WithError(err).Message("error ccreting a response") + return rest.ErrInternalServerError + } + + return rest.NewResponse(response) + default: + return rest.ErrMethodNotAllowed + } +} + +type getEventsResponse struct { + Events []json.RawMessage `json:"events"` + ThereIsMoreEvents bool `json:"thereIsMoreEvents"` +} + +func newGetEventsResponse(result app.GetEventsResult) (*getEventsResponse, error) { + events := make([]json.RawMessage, 0) // avoid sending null in responses + for _, event := range result.Events() { + eventJSON, err := event.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "error marshaling") + } + events = append(events, eventJSON) + } + + return &getEventsResponse{ + Events: events, + ThereIsMoreEvents: result.ThereIsMoreEvents(), + }, nil +} + +func (s *Server) serveEvent(r *http.Request) rest.RestResponse { switch r.Method { case http.MethodGet: vars := mux.Vars(r)