Skip to content

Commit

Permalink
adds List and Get methods to alerts client
Browse files Browse the repository at this point in the history
The Get endpoint already exists on the service, so only the List endpoint
needed to be added there.

BACK-2554
  • Loading branch information
ewollesen committed May 6, 2024
1 parent 6523dec commit 374bae2
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 9 deletions.
39 changes: 33 additions & 6 deletions alerts/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions alerts/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
37 changes: 37 additions & 0 deletions data/service/api/v1/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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.
Expand Down
21 changes: 18 additions & 3 deletions data/service/api/v1/alerts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions data/store/mongo/mongo_alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Expand Down

0 comments on commit 374bae2

Please sign in to comment.