Skip to content

Commit

Permalink
adds an alerts client
Browse files Browse the repository at this point in the history
BACK-2500
  • Loading branch information
ewollesen committed Nov 6, 2023
1 parent cc187d4 commit 4bbe804
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 3 deletions.
71 changes: 71 additions & 0 deletions alerts/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package alerts

import (
"context"
"net/http"

"github.com/tidepool-org/platform/auth"
"github.com/tidepool-org/platform/request"
)

// Client for managing alerts configs.
type Client struct {
client PlatformClient
token TokenProvider
}

// NewClient builds a client for interacting with alerts API endpoints.
func NewClient(client PlatformClient, token TokenProvider) *Client {
return &Client{
client: client,
token: token,
}
}

// platform.Client is one implementation
type PlatformClient interface {
ConstructURL(paths ...string) string
RequestData(ctx context.Context, method string, url string, mutators []request.RequestMutator,
requestBody interface{}, responseBody interface{}, inspectors ...request.ResponseInspector) error
}

// client.External is one implementation
type TokenProvider interface {
// ServerSessionToken provides a server-to-server API authentication token.
ServerSessionToken() (string, error)
}

// requestWithAuth injects an auth token before calling platform.Client.RequestData.
//
// At time of writing, this is the only way to inject credentials into
// 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 {
authCtx, err := c.ctxWithAuth(ctx)
if err != nil {
return err
}
return c.client.RequestData(authCtx, method, url, nil, body, nil)
}

// 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", "alerts", cfg.UserID, cfg.FollowedUserID)
return c.requestWithAuth(ctx, http.MethodPost, url, cfg)
}

// Delete the alerts config.
func (c *Client) Delete(ctx context.Context, cfg *Config) error {
url := c.client.ConstructURL("v1", "alerts", cfg.UserID, cfg.FollowedUserID)
return c.requestWithAuth(ctx, http.MethodDelete, url, nil)
}

// ctxWithAuth injects a server session token into the context.
func (c *Client) ctxWithAuth(ctx context.Context) (context.Context, error) {
token, err := c.token.ServerSessionToken()
if err != nil {
return nil, err
}
return auth.NewContextWithServerSessionToken(ctx, token), nil
}
112 changes: 112 additions & 0 deletions alerts/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package alerts

import (
"context"
"net/http"
"net/http/httptest"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/tidepool-org/platform/auth"
"github.com/tidepool-org/platform/client"
"github.com/tidepool-org/platform/log"
"github.com/tidepool-org/platform/log/null"
"github.com/tidepool-org/platform/platform"
)

const testToken = "auth-me"

var _ = Describe("Client", func() {
t := GinkgoT()
test404Server := testServer(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
})
test200Server := testServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
testAuthServer := func(token *string) *httptest.Server {
return testServer(t, func(w http.ResponseWriter, r *http.Request) {
*token = r.Header.Get(auth.TidepoolSessionTokenHeaderKey)
w.WriteHeader(http.StatusOK)
})
}

Context("Delete", func() {
It("returns an error on non-200 responses", func() {
client, ctx := newAlertsClientTest(test404Server)
err := client.Delete(ctx, &Config{})
Expect(err).Should(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("resource not found")))
})

It("returns nil on success", func() {
client, ctx := newAlertsClientTest(test200Server)
err := client.Delete(ctx, &Config{})
Expect(err).ShouldNot(HaveOccurred())
})

It("injects an auth token", func() {
token := ""
client, ctx := newAlertsClientTest(testAuthServer(&token))
_ = client.Delete(ctx, &Config{})
Expect(token).To(Equal(testToken))
})
})

Context("Upsert", func() {
It("returns an error on non-200 responses", func() {
client, ctx := newAlertsClientTest(test404Server)
err := client.Upsert(ctx, &Config{})
Expect(err).Should(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("resource not found")))
})

It("returns nil on success", func() {
client, ctx := newAlertsClientTest(test200Server)
err := client.Upsert(ctx, &Config{})
Expect(err).ShouldNot(HaveOccurred())
})

It("injects an auth token", func() {
token := ""
client, ctx := newAlertsClientTest(testAuthServer(&token))
_ = client.Upsert(ctx, &Config{})
Expect(token).To(Equal(testToken))
})
})
})

func buildTestClient(s *httptest.Server) *Client {
pCfg := &platform.Config{
Config: &client.Config{
Address: s.URL,
UserAgent: "foo",
},
}
token := mockTokenProvider(testToken)
pc, err := platform.NewClient(pCfg, platform.AuthorizeAsService)
Expect(err).ToNot(HaveOccurred())
client := NewClient(pc, token)
return client
}

func newAlertsClientTest(server *httptest.Server) (*Client, context.Context) {
return buildTestClient(server), contextWithNullLogger()
}

func contextWithNullLogger() context.Context {
return log.NewContextWithLogger(context.Background(), null.NewLogger())
}

type mockTokenProvider string

func (p mockTokenProvider) ServerSessionToken() (string, error) {
return string(p), nil
}

func testServer(t GinkgoTInterface, handler http.HandlerFunc) *httptest.Server {
s := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(s.Close)
return s
}
1 change: 0 additions & 1 deletion alerts/repo.go

This file was deleted.

10 changes: 8 additions & 2 deletions auth/client/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,15 @@ type ExternalConfig struct {
}

func NewExternalConfig() *ExternalConfig {
return NewExternalConfigFromPlatform(platform.NewConfig())
}

const ServerSessionTokenTimeout = time.Hour

func NewExternalConfigFromPlatform(config *platform.Config) *ExternalConfig {
return &ExternalConfig{
Config: platform.NewConfig(),
ServerSessionTokenTimeout: 3600 * time.Second,
Config: config,
ServerSessionTokenTimeout: ServerSessionTokenTimeout,
}
}

Expand Down
4 changes: 4 additions & 0 deletions platform/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func (c *Client) Mutators(ctx context.Context) ([]request.RequestMutator, error)
} else if serverSessionToken := auth.ServerSessionTokenFromContext(ctx); serverSessionToken != "" {
authorizationMutator = NewSessionTokenHeaderMutator(serverSessionToken)
} else {
// TODO: Should this really error? It might be nice to allow other
// clients the option of handling authentication on their own if
// they'd like, rather than enforcing that this method must be
// used.
return nil, errors.New("service secret is missing")
}
} else {
Expand Down

0 comments on commit 4bbe804

Please sign in to comment.