Skip to content

Commit a1b7712

Browse files
committed
consolidate care partner alerts pusher configs
The tasks service and data service can both push alerts for care partners. This change consolidates that configuration into one set of environment variables loaded via the alerts package. I wish that alerts didn't need to know about envconfig, but at least for the moment it's the only way to consolidate the information about the configuration into a single re-usable struct. Naming of the pusher in both services is prefixed with "alerts" to communicate that this pusher is configured for care partner alerts. BACK-2559
1 parent 516750a commit a1b7712

File tree

7 files changed

+177
-136
lines changed

7 files changed

+177
-136
lines changed

alerts/pusher.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package alerts
2+
3+
import (
4+
"context"
5+
6+
"github.com/kelseyhightower/envconfig"
7+
8+
"github.com/tidepool-org/platform/devicetokens"
9+
"github.com/tidepool-org/platform/errors"
10+
"github.com/tidepool-org/platform/push"
11+
)
12+
13+
// Pusher is a service-agnostic interface for sending push notifications.
14+
type Pusher interface {
15+
// Push a notification to a device.
16+
Push(context.Context, *devicetokens.DeviceToken, *push.Notification) error
17+
}
18+
19+
// ToPushNotification converts Notification to push.Notification.
20+
func ToPushNotification(notification *Notification) *push.Notification {
21+
return &push.Notification{
22+
Message: notification.Message,
23+
}
24+
}
25+
26+
type cpaPusherEnvconfig struct {
27+
// SigningKey is the raw token signing key received from Apple (.p8 file containing
28+
// PEM-encoded private key)
29+
//
30+
// https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
31+
SigningKey []byte `envconfig:"TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_SIGNING_KEY" required:"true"`
32+
KeyID string `envconfig:"TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_KEY_ID" required:"true"`
33+
BundleID string `envconfig:"TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_BUNDLE_ID" required:"true"`
34+
TeamID string `envconfig:"TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_TEAM_ID" required:"true"`
35+
}
36+
37+
// NewPusher handles the loading of care partner configuration for push notifications.
38+
func NewPusher() (*push.APNSPusher, error) {
39+
config, err := loadPusherViaEnvconfig()
40+
if err != nil {
41+
return nil, errors.Wrap(err, "unable to care partner pusher config")
42+
}
43+
44+
client, err := push.NewAPNS2Client(config.SigningKey, config.KeyID, config.TeamID)
45+
if err != nil {
46+
return nil, errors.Wrap(err, "unable to create care partner pusher client")
47+
}
48+
49+
return push.NewAPNSPusher(client, config.BundleID), nil
50+
}
51+
52+
func loadPusherViaEnvconfig() (*cpaPusherEnvconfig, error) {
53+
c := &cpaPusherEnvconfig{}
54+
if err := envconfig.Process("", c); err != nil {
55+
return nil, errors.Wrap(err, "Unable to process APNs pusher config")
56+
}
57+
58+
// envconfig's "required" tag won't error on values that are defined but empty, so
59+
// manually check
60+
61+
if len(c.SigningKey) == 0 {
62+
return nil, errors.New("Unable to build APNSPusherConfig: APNs signing key is blank")
63+
}
64+
65+
if c.BundleID == "" {
66+
return nil, errors.New("Unable to build APNSPusherConfig: bundleID is blank")
67+
}
68+
69+
if c.KeyID == "" {
70+
return nil, errors.New("Unable to build APNSPusherConfig: keyID is blank")
71+
}
72+
73+
if c.TeamID == "" {
74+
return nil, errors.New("Unable to build APNSPusherConfig: teamID is blank")
75+
}
76+
77+
return c, nil
78+
}

alerts/pusher_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package alerts
2+
3+
import (
4+
"os"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
var _ = Describe("APNSPusher", func() {
11+
Describe("NewAPNSPusherFromEnv", func() {
12+
It("succeeds", func() {
13+
configureEnvconfig()
14+
pusher, err := NewPusher()
15+
Expect(err).To(Succeed())
16+
Expect(pusher).ToNot(Equal(nil))
17+
})
18+
})
19+
})
20+
21+
var _ = Describe("LoadAPNSPusherConfigFromEnv", func() {
22+
BeforeEach(func() {
23+
configureEnvconfig()
24+
})
25+
26+
It("errors if key data is empty or blank", func() {
27+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_SIGNING_KEY", "")
28+
_, err := NewPusher()
29+
Expect(err).To(MatchError(ContainSubstring("APNs signing key is blank")))
30+
31+
os.Unsetenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_SIGNING_KEY")
32+
_, err = NewPusher()
33+
Expect(err).To(MatchError(ContainSubstring("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_SIGNING_KEY missing value")))
34+
})
35+
36+
It("errors if key data is invalid", func() {
37+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_SIGNING_KEY", "invalid")
38+
_, err := NewPusher()
39+
Expect(err).To(MatchError(ContainSubstring("AuthKey must be a valid .p8 PEM file")))
40+
})
41+
42+
It("errors if bundleID is blank", func() {
43+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_BUNDLE_ID", "")
44+
_, err := NewPusher()
45+
Expect(err).To(MatchError(ContainSubstring("bundleID is blank")))
46+
})
47+
48+
It("errors if teamID is blank", func() {
49+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_TEAM_ID", "")
50+
_, err := NewPusher()
51+
Expect(err).To(MatchError(ContainSubstring("teamID is blank")))
52+
})
53+
54+
It("errors if keyID is blank", func() {
55+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_KEY_ID", "")
56+
_, err := NewPusher()
57+
Expect(err).To(MatchError(ContainSubstring("keyID is blank")))
58+
})
59+
})
60+
61+
func configureEnvconfig() {
62+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_SIGNING_KEY", string(validTestKey))
63+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_KEY_ID", "key")
64+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_TEAM_ID", "team")
65+
GinkgoT().Setenv("TIDEPOOL_CARE_PARTNER_ALERTS_PUSHER_APNS_BUNDLE_ID", "bundle")
66+
}
67+
68+
// validTestKey is a random private key for testing
69+
var validTestKey = []byte(`-----BEGIN PRIVATE KEY-----
70+
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDNrXT9ZRWPUAAg38Qi
71+
Z553y7sGqOgMxUCG36eCIcRCy1QiTJBgGDxIhWvkE8Sx4N6hZANiAATrsRyRXLa0
72+
Tgczq8tmFomMP212HdkPF3gFEl/CkqGHUodR2EdZBW1zVcmuLjIN4zvqVVXMJm/U
73+
eHZz9xAZ95y3irAfkMuOD/Bw88UYvhKnipOHBeS8BwqyfFQ+NRB6xYU=
74+
-----END PRIVATE KEY-----
75+
`)

alerts/tasks.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import (
66
"time"
77

88
"github.com/tidepool-org/platform/auth"
9-
"github.com/tidepool-org/platform/devicetokens"
109
"github.com/tidepool-org/platform/errors"
1110
"github.com/tidepool-org/platform/log"
1211
"github.com/tidepool-org/platform/permission"
1312
"github.com/tidepool-org/platform/pointer"
14-
"github.com/tidepool-org/platform/push"
1513
"github.com/tidepool-org/platform/task"
1614
)
1715

@@ -202,16 +200,3 @@ func (r *CarePartnerRunner) pushNotifications(ctx context.Context,
202200
}
203201
}
204202
}
205-
206-
// Pusher is a service-agnostic interface for sending push notifications.
207-
type Pusher interface {
208-
// Push a notification to a device.
209-
Push(context.Context, *devicetokens.DeviceToken, *push.Notification) error
210-
}
211-
212-
// ToPushNotification converts Notification to push.Notification.
213-
func ToPushNotification(notification *Notification) *push.Notification {
214-
return &push.Notification{
215-
Message: notification.Message,
216-
}
217-
}

data/service/service/standard.go

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"time"
77

88
"github.com/IBM/sarama"
9-
"github.com/kelseyhightower/envconfig"
109

1110
eventsCommon "github.com/tidepool-org/go-common/events"
1211

@@ -47,7 +46,7 @@ type Standard struct {
4746
dataClient *Client
4847
clinicsClient *clinics.Client
4948
dataSourceClient *dataSourceServiceClient.Client
50-
pusher dataEvents.Pusher
49+
alertsPusher dataEvents.Pusher
5150
userEventsHandler events.Runner
5251
alertsEventsHandler events.Runner
5352
api *api.Standard
@@ -95,7 +94,7 @@ func (s *Standard) Initialize(provider application.Provider) error {
9594
if err := s.initializeSaramaLogger(); err != nil {
9695
return err
9796
}
98-
if err := s.initializePusher(); err != nil {
97+
if err := s.initializeAlertsPusher(); err != nil {
9998
return err
10099
}
101100
if err := s.initializeUserEventsHandler(); err != nil {
@@ -451,27 +450,15 @@ func (s *Standard) initializeSaramaLogger() error {
451450
return nil
452451
}
453452

454-
func (s *Standard) initializePusher() error {
453+
func (s *Standard) initializeAlertsPusher() error {
455454
var err error
456-
457-
apns2Config := &struct {
458-
SigningKey []byte `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_SIGNING_KEY"`
459-
KeyID string `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_KEY_ID"`
460-
BundleID string `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_BUNDLE_ID"`
461-
TeamID string `envconfig:"TIDEPOOL_DATA_SERVICE_PUSHER_APNS_TEAM_ID"`
462-
}{}
463-
if err := envconfig.Process("", apns2Config); err != nil {
464-
return errors.Wrap(err, "Unable to process APNs pusher config")
465-
}
466-
467455
var pusher dataEvents.Pusher
468-
pusher, err = push.NewAPNSPusherFromKeyData(apns2Config.SigningKey, apns2Config.KeyID,
469-
apns2Config.TeamID, apns2Config.BundleID)
456+
pusher, err = alerts.NewPusher()
470457
if err != nil {
471-
s.Logger().WithError(err).Warn("falling back to logging of push notifications")
458+
s.Logger().WithError(err).Warn("falling back to logging of alerts push notifications")
472459
pusher = push.NewLogPusher(s.Logger())
473460
}
474-
s.pusher = pusher
461+
s.alertsPusher = pusher
475462

476463
return nil
477464
}
@@ -508,7 +495,7 @@ func (s *Standard) initializeAlertsEventsHandler() error {
508495
DeviceTokens: s.AuthClient(),
509496
Logger: s.Logger(),
510497
Permissions: s.PermissionClient(),
511-
Pusher: s.pusher,
498+
Pusher: s.alertsPusher,
512499
LastCommunications: dataEvents.NewLastCommunicationRecorder(lastCommunicationsRepo),
513500
TokensProvider: s.AuthClient(),
514501
}

push/push.go

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,53 +34,14 @@ type APNSPusher struct {
3434
clientMu sync.Mutex
3535
}
3636

37-
// NewAPNSPusher creates a Pusher for sending device notifications via Apple's
38-
// APNs.
37+
// NewAPNSPusher creates an APNSPusher for sending device notifications via Apple's APNs.
3938
func NewAPNSPusher(client APNS2Client, bundleID string) *APNSPusher {
4039
return &APNSPusher{
4140
BundleID: bundleID,
4241
client: client,
4342
}
4443
}
4544

46-
// NewAPNSPusherFromKeyData creates an APNSPusher for sending device
47-
// notifications via Apple's APNs.
48-
//
49-
// The signingKey is the raw token signing key received from Apple (.p8 file
50-
// containing PEM-encoded private key), along with its respective team id, key
51-
// id, and application bundle id.
52-
//
53-
// https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
54-
func NewAPNSPusherFromKeyData(signingKey []byte, keyID, teamID, bundleID string) (*APNSPusher, error) {
55-
if len(signingKey) == 0 {
56-
return nil, errors.New("Unable to build APNSPusher: APNs signing key is blank")
57-
}
58-
59-
if bundleID == "" {
60-
return nil, errors.New("Unable to build APNSPusher: bundleID is blank")
61-
}
62-
63-
if keyID == "" {
64-
return nil, errors.New("Unable to build APNSPusher: keyID is blank")
65-
}
66-
67-
if teamID == "" {
68-
return nil, errors.New("Unable to build APNSPusher: teamID is blank")
69-
}
70-
71-
authKey, err := token.AuthKeyFromBytes(signingKey)
72-
if err != nil {
73-
return nil, err
74-
}
75-
token := &token.Token{
76-
AuthKey: authKey,
77-
KeyID: keyID,
78-
TeamID: teamID,
79-
}
80-
client := &apns2Client{Client: apns2.NewTokenClient(token)}
81-
return NewAPNSPusher(client, bundleID), nil
82-
}
83-
8445
func (p *APNSPusher) Push(ctx context.Context, deviceToken *devicetokens.DeviceToken,
8546
notification *Notification) error {
8647

@@ -146,6 +107,19 @@ type apns2Client struct {
146107
*apns2.Client
147108
}
148109

110+
func NewAPNS2Client(signingKey []byte, keyID, teamID string) (*apns2Client, error) {
111+
authKey, err := token.AuthKeyFromBytes(signingKey)
112+
if err != nil {
113+
return nil, err
114+
}
115+
token := &token.Token{
116+
AuthKey: authKey,
117+
KeyID: keyID,
118+
TeamID: teamID,
119+
}
120+
return &apns2Client{apns2.NewTokenClient(token)}, nil
121+
}
122+
149123
func (c apns2Client) Development() APNS2Client {
150124
d := c.Client.Development()
151125
return &apns2Client{Client: d}

push/push_test.go

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -49,50 +49,6 @@ func testDeps() (context.Context, *APNSPusher, *pushTestDeps) {
4949
}
5050

5151
var _ = Describe("APNSPusher", func() {
52-
Describe("NewAPNSPusherFromKeyData", func() {
53-
It("errors if key data is empty or blank", func() {
54-
_, err := NewAPNSPusherFromKeyData([]byte(""), "key", "team", "bundle")
55-
Expect(err).To(MatchError(ContainSubstring("APNs signing key is blank")))
56-
57-
_, err = NewAPNSPusherFromKeyData(nil, "key", "team", "bundle")
58-
Expect(err).To(MatchError(ContainSubstring("APNs signing key is blank")))
59-
})
60-
61-
It("errors if key data is invalid", func() {
62-
_, err := NewAPNSPusherFromKeyData([]byte("foo"), "key", "team", "bundle")
63-
Expect(err).To(MatchError(ContainSubstring("AuthKey must be a valid .p8 PEM file")))
64-
})
65-
66-
It("errors if bundleID is blank", func() {
67-
_, err := NewAPNSPusherFromKeyData([]byte("hi"), "key", "team", "")
68-
Expect(err).To(MatchError(ContainSubstring("bundleID is blank")))
69-
})
70-
71-
It("errors if teamID is blank", func() {
72-
_, err := NewAPNSPusherFromKeyData([]byte("hi"), "key", "", "bundle")
73-
Expect(err).To(MatchError(ContainSubstring("teamID is blank")))
74-
})
75-
76-
It("errors if keyID is blank", func() {
77-
_, err := NewAPNSPusherFromKeyData([]byte("hi"), "", "team", "bundle")
78-
Expect(err).To(MatchError(ContainSubstring("keyID is blank")))
79-
})
80-
81-
It("succeeds", func() {
82-
// random private key for testing
83-
data := []byte(`-----BEGIN PRIVATE KEY-----
84-
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDNrXT9ZRWPUAAg38Qi
85-
Z553y7sGqOgMxUCG36eCIcRCy1QiTJBgGDxIhWvkE8Sx4N6hZANiAATrsRyRXLa0
86-
Tgczq8tmFomMP212HdkPF3gFEl/CkqGHUodR2EdZBW1zVcmuLjIN4zvqVVXMJm/U
87-
eHZz9xAZ95y3irAfkMuOD/Bw88UYvhKnipOHBeS8BwqyfFQ+NRB6xYU=
88-
-----END PRIVATE KEY-----
89-
`)
90-
pusher, err := NewAPNSPusherFromKeyData(data, "key", "team", "bundle")
91-
Expect(err).To(Succeed())
92-
Expect(pusher).ToNot(Equal(nil))
93-
})
94-
})
95-
9652
Describe("Push", func() {
9753
It("requires an Apple token", func() {
9854
ctx, pusher, deps := testDeps()

0 commit comments

Comments
 (0)