From 374bae2acd64ef7265899b6115d8c40227d562c4 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Mon, 6 May 2024 09:53:25 -0600 Subject: [PATCH] adds List and Get methods to alerts client The Get endpoint already exists on the service, so only the List endpoint needed to be added there. BACK-2554 --- alerts/client.go | 39 +++++++++++++++++++++++++----- alerts/config.go | 1 + data/service/api/v1/alerts.go | 37 ++++++++++++++++++++++++++++ data/service/api/v1/alerts_test.go | 21 +++++++++++++--- data/store/mongo/mongo_alerts.go | 21 ++++++++++++++++ 5 files changed, 110 insertions(+), 9 deletions(-) diff --git a/alerts/client.go b/alerts/client.go index bc6db1f888..9abaaae9f3 100644 --- a/alerts/client.go +++ b/alerts/client.go @@ -9,6 +9,7 @@ import ( "github.com/tidepool-org/platform/auth" "github.com/tidepool-org/platform/client" + "github.com/tidepool-org/platform/errors" platformlog "github.com/tidepool-org/platform/log" "github.com/tidepool-org/platform/log/null" "github.com/tidepool-org/platform/platform" @@ -43,6 +44,8 @@ type PlatformClient interface { requestBody interface{}, responseBody interface{}, inspectors ...request.ResponseInspector) error } +// TokenProvider retrieves session tokens for calling the alerts API. +// // client.External is one implementation type TokenProvider interface { // ServerSessionToken provides a server-to-server API authentication token. @@ -51,12 +54,12 @@ type TokenProvider interface { // request performs common operations before passing a request off to the // underlying platform.Client. -func (c *Client) request(ctx context.Context, method, url string, body any) error { +func (c *Client) request(ctx context.Context, method, url string, reqBody, resBody any) error { // Platform's client.Client expects a logger to exist in the request's // context. If it doesn't exist, request processing will panic. loggingCtx := platformlog.NewContextWithLogger(ctx, c.logger) // Make sure the auth token is injected into the request's headers. - return c.requestWithAuth(loggingCtx, method, url, body) + return c.requestWithAuth(loggingCtx, method, url, reqBody, resBody) } // requestWithAuth injects an auth token before calling platform.Client.RequestData. @@ -65,24 +68,48 @@ func (c *Client) request(ctx context.Context, method, url string, body any) erro // platform.Client. It might be nice to be able to use a mutator, but the auth // is specifically handled by the platform.Client via the context field, and // if left blank, platform.Client errors. -func (c *Client) requestWithAuth(ctx context.Context, method, url string, body any) error { +func (c *Client) requestWithAuth(ctx context.Context, method, url string, reqBody, resBody any) error { authCtx, err := c.ctxWithAuth(ctx) if err != nil { return err } - return c.client.RequestData(authCtx, method, url, nil, body, nil) + return c.client.RequestData(authCtx, method, url, nil, reqBody, resBody) } // Upsert updates cfg if it exists or creates it if it doesn't. func (c *Client) Upsert(ctx context.Context, cfg *Config) error { url := c.client.ConstructURL("v1", "users", cfg.FollowedUserID, "followers", cfg.UserID, "alerts") - return c.request(ctx, http.MethodPost, url, cfg) + return c.request(ctx, http.MethodPost, url, cfg, nil) } // Delete the alerts config. func (c *Client) Delete(ctx context.Context, cfg *Config) error { url := c.client.ConstructURL("v1", "users", cfg.FollowedUserID, "followers", cfg.UserID, "alerts") - return c.request(ctx, http.MethodDelete, url, nil) + return c.request(ctx, http.MethodDelete, url, nil, nil) +} + +// Get a user's alerts configuration for the followed user. +func (c *Client) Get(ctx context.Context, followedUserID, userID string) (*Config, error) { + url := c.client.ConstructURL("v1", "users", followedUserID, "followers", userID, "alerts") + cfg := &Config{} + err := c.request(ctx, http.MethodGet, url, nil, cfg) + if err != nil { + return nil, errors.Wrap(err, "Unable to request alerts config") + } + return cfg, nil +} + +// List the alerts configurations that follow the given user. +// +// This method should only be called via an authenticated service session. +func (c *Client) List(ctx context.Context, followedUserID string) ([]*Config, error) { + url := c.client.ConstructURL("v1", "users", followedUserID, "followers", "alerts") + configs := []*Config{} + err := c.request(ctx, http.MethodGet, url, nil, &configs) + if err != nil { + return nil, errors.Wrap(err, "Unable to request alerts configs list") + } + return configs, nil } // ctxWithAuth injects a server session token into the context. diff --git a/alerts/config.go b/alerts/config.go index 93cbdc975a..133be2d1a9 100644 --- a/alerts/config.go +++ b/alerts/config.go @@ -234,6 +234,7 @@ type Repository interface { Get(ctx context.Context, conf *Config) (*Config, error) Upsert(ctx context.Context, conf *Config) error Delete(ctx context.Context, conf *Config) error + List(ctx context.Context, userID string) ([]*Config, error) EnsureIndexes() error } diff --git a/data/service/api/v1/alerts.go b/data/service/api/v1/alerts.go index d07891247e..70941b9e20 100644 --- a/data/service/api/v1/alerts.go +++ b/data/service/api/v1/alerts.go @@ -24,6 +24,7 @@ func AlertsRoutes() []service.Route { service.Get("/v1/users/:userId/followers/:followerUserId/alerts", GetAlert, api.RequireAuth), service.Post("/v1/users/:userId/followers/:followerUserId/alerts", UpsertAlert, api.RequireAuth), service.Delete("/v1/users/:userId/followers/:followerUserId/alerts", DeleteAlert, api.RequireAuth), + service.Get("/v1/users/:userId/followers/alerts", ListAlerts, api.RequireServer), } } @@ -134,6 +135,42 @@ func UpsertAlert(dCtx service.Context) { } } +func ListAlerts(dCtx service.Context) { + r := dCtx.Request() + ctx := r.Context() + authDetails := request.GetAuthDetails(ctx) + repo := dCtx.AlertsRepository() + lgr := log.LoggerFromContext(ctx) + + if err := checkAuthentication(authDetails); err != nil { + lgr.Debug("authentication failed") + dCtx.RespondWithError(platform.ErrorUnauthorized()) + return + } + + pathsUserID := r.PathParam("userId") + if err := checkUserIDConsistency(authDetails, pathsUserID); err != nil { + lgr.WithFields(log.Fields{"path": pathsUserID, "auth": authDetails.UserID()}). + Debug("user id consistency failed") + dCtx.RespondWithError(platform.ErrorUnauthorized()) + return + } + + alerts, err := repo.List(ctx, pathsUserID) + if err != nil { + dCtx.RespondWithInternalServerFailure("listing alerts configs", err) + lgr.WithError(err).Error("listing alerts config") + return + } + if len(alerts) == 0 { + dCtx.RespondWithError(ErrorUserIDNotFound(pathsUserID)) + lgr.Debug("no alerts configs found") + } + + responder := request.MustNewResponder(dCtx.Response(), r) + responder.Data(http.StatusOK, alerts) +} + // checkUserIDConsistency verifies the userIDs in a request. // // For safety reasons, if these values don't agree, return an error. diff --git a/data/service/api/v1/alerts_test.go b/data/service/api/v1/alerts_test.go index c3b4b2f2a5..d48be38a6f 100644 --- a/data/service/api/v1/alerts_test.go +++ b/data/service/api/v1/alerts_test.go @@ -160,12 +160,15 @@ var _ = Describe("Alerts endpoints", func() { }) type mockRepo struct { - UserID string - Error error + UserID string + Error error + AlertsForUserID map[string][]*alerts.Config } func newMockRepo() *mockRepo { - return &mockRepo{} + return &mockRepo{ + AlertsForUserID: make(map[string][]*alerts.Config), + } } func (r *mockRepo) ReturnsError(err error) { @@ -202,6 +205,18 @@ func (r *mockRepo) Delete(ctx context.Context, conf *alerts.Config) error { return nil } +func (r *mockRepo) List(ctx context.Context, userID string) ([]*alerts.Config, error) { + if r.Error != nil { + return nil, r.Error + } + r.UserID = userID + alerts, ok := r.AlertsForUserID[userID] + if !ok { + return nil, nil + } + return alerts, nil +} + func (r *mockRepo) EnsureIndexes() error { return nil } diff --git a/data/store/mongo/mongo_alerts.go b/data/store/mongo/mongo_alerts.go index ee313f3ffb..cbb49a1c5c 100644 --- a/data/store/mongo/mongo_alerts.go +++ b/data/store/mongo/mongo_alerts.go @@ -9,6 +9,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" "github.com/tidepool-org/platform/alerts" + "github.com/tidepool-org/platform/errors" structuredmongo "github.com/tidepool-org/platform/store/structured/mongo" ) @@ -34,6 +35,26 @@ func (r *alertsRepo) Delete(ctx context.Context, cfg *alerts.Config) error { return nil } +// List will retrieve any Configs that are defined by followers of the given user. +func (r *alertsRepo) List(ctx context.Context, userID string) ([]*alerts.Config, error) { + filter := bson.D{ + {Key: "followedUserId", Value: userID}, + } + cursor, err := r.Find(ctx, filter, nil) + if err != nil { + return nil, errors.Wrapf(err, "Unable to list alerts.Config(s) for user %s", userID) + } + defer cursor.Close(ctx) + out := []*alerts.Config{} + if err := cursor.All(ctx, &out); err != nil { + return nil, errors.Wrapf(err, "Unable to decode alerts.Config(s) for user %s", userID) + } + if err := cursor.Err(); err != nil { + return nil, errors.Wrapf(err, "Unexpected error for user %s", userID) + } + return out, nil +} + // Get will retrieve the given Config. func (r *alertsRepo) Get(ctx context.Context, cfg *alerts.Config) (*alerts.Config, error) { res := r.FindOne(ctx, r.filter(cfg), nil)