-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into jellyfish-migration-updates_0
- Loading branch information
Showing
24 changed files
with
601 additions
and
171 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package alerts | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/kelseyhightower/envconfig" | ||
|
||
"github.com/tidepool-org/platform/auth" | ||
"github.com/tidepool-org/platform/client" | ||
platformlog "github.com/tidepool-org/platform/log" | ||
"github.com/tidepool-org/platform/log/null" | ||
"github.com/tidepool-org/platform/platform" | ||
"github.com/tidepool-org/platform/request" | ||
) | ||
|
||
// Client for managing alerts configs. | ||
type Client struct { | ||
client PlatformClient | ||
logger platformlog.Logger | ||
token TokenProvider | ||
} | ||
|
||
// NewClient builds a client for interacting with alerts API endpoints. | ||
// | ||
// If no logger is provided, a null logger is used. | ||
func NewClient(client PlatformClient, token TokenProvider, logger platformlog.Logger) *Client { | ||
if logger == nil { | ||
logger = null.NewLogger() | ||
} | ||
return &Client{ | ||
client: client, | ||
logger: logger, | ||
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) | ||
} | ||
|
||
// 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 { | ||
// 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) | ||
} | ||
|
||
// 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.request(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.request(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, fmt.Errorf("retrieving token: %w", err) | ||
} | ||
return auth.NewContextWithServerSessionToken(ctx, token), nil | ||
} | ||
|
||
// ConfigLoader abstracts the method by which config values are loaded. | ||
type ConfigLoader interface { | ||
Load(*ClientConfig) error | ||
} | ||
|
||
// envconfigLoader adapts envconfig to implement ConfigLoader. | ||
type envconfigLoader struct { | ||
platform.ConfigLoader | ||
} | ||
|
||
// NewEnvconfigLoader loads values via envconfig. | ||
// | ||
// If loader is nil, it defaults to envconfig for platform values. | ||
func NewEnvconfigLoader(loader platform.ConfigLoader) *envconfigLoader { | ||
if loader == nil { | ||
loader = platform.NewEnvconfigLoader(nil) | ||
} | ||
return &envconfigLoader{ | ||
ConfigLoader: loader, | ||
} | ||
} | ||
|
||
// Load implements ConfigLoader. | ||
func (l *envconfigLoader) Load(cfg *ClientConfig) error { | ||
if err := l.ConfigLoader.Load(cfg.Config); err != nil { | ||
return err | ||
} | ||
if err := envconfig.Process(client.EnvconfigEmptyPrefix, cfg); err != nil { | ||
return err | ||
} | ||
// Override client.Client.Address to point to the data service. | ||
cfg.Address = cfg.DataServiceAddress | ||
return nil | ||
} | ||
|
||
type ClientConfig struct { | ||
*platform.Config | ||
// DataServiceAddress is used to override client.Client.Address. | ||
DataServiceAddress string `envconfig:"TIDEPOOL_DATA_SERVICE_ADDRESS" required:"true"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package alerts | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "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() { | ||
var test404Server, test200Server *httptest.Server | ||
var testAuthServer func(*string) *httptest.Server | ||
|
||
BeforeEach(func() { | ||
t := GinkgoT() | ||
// There's no need to create these before each test, but I can't get | ||
// Ginkgo to let me start these just once. | ||
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, | ||
}, | ||
} | ||
token := mockTokenProvider(testToken) | ||
pc, err := platform.NewClient(pCfg, platform.AuthorizeAsService) | ||
Expect(err).ToNot(HaveOccurred()) | ||
client := NewClient(pc, token, null.NewLogger()) | ||
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 | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.